aweshare 0.5.9 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.ai.md +12 -10
  2. package/README.md +51 -28
  3. package/apps/agent/dist/cli.js +41 -9
  4. package/apps/agent/dist/cli.js.map +1 -1
  5. package/apps/agent/dist/hub.d.ts +6 -0
  6. package/apps/agent/dist/hub.js +1 -1
  7. package/apps/agent/dist/hub.js.map +1 -1
  8. package/apps/agent/dist/table.d.ts +11 -2
  9. package/apps/agent/dist/table.js +3 -2
  10. package/apps/agent/dist/table.js.map +1 -1
  11. package/apps/consumer/dist/cli.js +16 -0
  12. package/apps/consumer/dist/cli.js.map +1 -1
  13. package/apps/consumer/dist/table.d.ts +1 -1
  14. package/apps/consumer/dist/table.js +1 -1
  15. package/apps/hub/dist/admin.js +21 -9
  16. package/apps/hub/dist/admin.js.map +1 -1
  17. package/apps/hub/dist/auth.js +1 -1
  18. package/apps/hub/dist/auth.js.map +1 -1
  19. package/apps/hub/dist/cli.js +327 -207
  20. package/apps/hub/dist/cli.js.map +1 -1
  21. package/apps/hub/dist/config.d.ts +8 -5
  22. package/apps/hub/dist/config.js +35 -3
  23. package/apps/hub/dist/config.js.map +1 -1
  24. package/apps/hub/dist/consumer.js +7 -5
  25. package/apps/hub/dist/consumer.js.map +1 -1
  26. package/apps/hub/dist/db.d.ts +42 -18
  27. package/apps/hub/dist/db.js +97 -46
  28. package/apps/hub/dist/db.js.map +1 -1
  29. package/apps/hub/dist/invites.js +2 -2
  30. package/apps/hub/dist/invites.js.map +1 -1
  31. package/apps/hub/dist/landing.d.ts +39 -0
  32. package/apps/hub/dist/landing.js +276 -0
  33. package/apps/hub/dist/landing.js.map +1 -0
  34. package/apps/hub/dist/local.js +21 -2
  35. package/apps/hub/dist/local.js.map +1 -1
  36. package/apps/hub/dist/server.js +4 -0
  37. package/apps/hub/dist/server.js.map +1 -1
  38. package/apps/hub/dist/table.d.ts +21 -9
  39. package/apps/hub/dist/table.js +12 -15
  40. package/apps/hub/dist/table.js.map +1 -1
  41. package/apps/hub/dist/tunnel.js +1 -1
  42. package/apps/hub/dist/tunnel.js.map +1 -1
  43. package/bin/aweshare.mjs +1 -1
  44. package/package.json +1 -1
@@ -3,7 +3,7 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
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 { refreshTargets, 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`' +
@@ -78,14 +79,34 @@ Options:
78
79
  aweshare hub produce refresh glm-5.3 --clear # drop both markers
79
80
 
80
81
  Options:
81
- --host ADDR Address to listen on (default from AWESHARE_HUB_HOST, else 0.0.0.0).
82
- --port N Port to listen on, 1..65535 (default from AWESHARE_HUB_PORT, else 8787).
83
82
  --add N refresh only: raise today's cap by N tokens until Beijing midnight.
84
83
  --clear refresh only: drop the refresh marker and any bonus.
85
84
  --all refresh only: bare-refresh every hub/... model (no ALIAS, --add or --clear).
86
85
  --json refresh only: print the raw API response(s).
87
86
  -h, --help Show this message and exit.`,
88
- 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]
89
110
 
90
111
  The invite lifecycle. One code, one identity — and the invite is the
91
112
  operator's handle from mint to suspension: revoking a redeemed code
@@ -93,7 +114,7 @@ Options:
93
114
  a consumer's key stops working); restoring brings both back.
94
115
 
95
116
  Commands:
96
- mint Mint one-time invite codes (default). Printed once; recover
117
+ mint Mint one-time invite codes. Printed once; recover
97
118
  with: aweshare hub list invites --reveal. Listing lives under
98
119
  'list invites' on purpose — minting sends real codes, reading
99
120
  the ledger never should.
@@ -114,19 +135,38 @@ Options:
114
135
  token's lifetime; 'none' mints a code (and identity) that
115
136
  never expires.
116
137
  -h, --help Show this message and exit.`,
117
- 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]
118
152
 
119
- Read hub state (default: invites). The rosters and offering health live in
120
- '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'.
121
155
 
122
156
  Commands:
123
- invites The invite ledger: every code, the identity it minted and its
124
- lifecycle (pending/used/suspended/revoked/expired).
125
- usage Who used how much: aggregated on the hub's SQLite, one row per
126
- consumer × model (default) — a person's rows stay together,
127
- busiest person and busiest model first. --details switches to the
128
- request log instead: newest first, one row per request, zero
129
- 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), most recently used first. --details
167
+ switches to the request log instead: newest first, one row per
168
+ request, zero content stored, each row naming the consumer
169
+ that made it.
130
170
 
131
171
  Options:
132
172
  --reveal Invites only: show the full invite codes (asi_...).
@@ -138,8 +178,12 @@ Options:
138
178
  --alias ALIAS Usage only: only rows for this alias (ns/model).
139
179
  --group-by KEY Usage only: consumer × model (default), consumer or alias.
140
180
  --since DUR Usage only: window: 30m, 12h, 7d (default), 30d… or all.
181
+ --sort KEY Usage summary only: time (default, newest first), consumer,
182
+ producer or model (alphabetical, newest first within), or
183
+ tokens / requests (busiest first; tokens keeps a person's
184
+ rows together).
141
185
  --limit N Usage --details only: rows to show, 1..1000 (default 100).
142
- --json Print the raw API response instead of a table.
186
+ --json Print the raw API rows instead of a table.
143
187
  -h, --help Show this message and exit.`,
144
188
  limits: `Usage: aweshare hub limits NAME [OPTIONS]
145
189
 
@@ -156,31 +200,16 @@ Options:
156
200
  --clear Drop every override (back to hub-wide defaults).
157
201
  --json Print the raw API response instead of text.
158
202
  -h, --help Show this message and exit.`,
159
- status: `Usage: aweshare hub status [OPTIONS]
203
+ status: `Usage: aweshare hub status
160
204
 
161
- The hub's live state: capacity, offering health and the effective hub-wide
162
- consumer defaults. No tokens or request contents appear. Offerings are
163
- counted per deduplicated alias (one alias, several protocols, one verdict —
164
- the worst); the alias table uses the same columns as 'consumer list' and
165
- 'producer list', sorted worst status first, with live occupancy (IN USE
166
- n/max) and today's remaining daily tokens. The last-5m health line comes
167
- from the usage summary — admission 429s are never metered, so it reflects
168
- relayed outcomes only. The producer/consumer rosters are counts here; pass
169
- --all to print them in full.
170
-
171
- Options:
172
- --all Also print the producer and consumer rosters (name, status,
173
- online state, last seen).
174
- -h, --help Show this message and exit.`,
175
- offering: `Usage: aweshare hub offering block|restore ALIAS
176
-
177
- Block one offering alias (every protocol row of it) or bring it back — the
178
- scalpel between doing nothing and revoking the whole producer. A block 503s
179
- new requests with OFFERING_BLOCKED while the producer's other offerings
180
- keep serving; status shows 'blocked'. Manual blocks survive producer
181
- re-registers; auto blocks (repeated model mismatch, only when
182
- autoBlockModelMismatch is enabled) clear once the producer re-declares a
183
- different upstreamModel.
205
+ The hub's live dashboard: capacity, the last-5m health, admission
206
+ (rate-limit) pressure and the effective hub-wide consumer defaults.
207
+ No tokens or request contents appear, and no tables — every table
208
+ this summarizes lives under 'aweshare hub list' (producers, consumers,
209
+ offerings, usage). The last-5m health line comes from the usage
210
+ summary — admission 429s are never metered, so it reflects relayed
211
+ outcomes only; the rejection lines are the throttling that summary
212
+ cannot see, the tuning evidence for 'limits' and per-offering caps.
184
213
 
185
214
  Options:
186
215
  -h, --help Show this message and exit.`,
@@ -212,6 +241,26 @@ function fail(message) {
212
241
  console.error(`error: ${message}`);
213
242
  process.exit(1);
214
243
  }
244
+ /** Flags each command accepts (beyond -h/--help); anything else is a typo
245
+ * and must fail loudly. Commands whose subcommands take different flags
246
+ * (produce, admin, list) resolve the allowed list first, then call this. */
247
+ function checkFlags(cmd, flags, allowed) {
248
+ for (const name of flags.keys()) {
249
+ if (name !== 'help' && !allowed.includes(name))
250
+ fail(`unknown option '--${name}' (see: aweshare hub ${cmd} -h)`);
251
+ }
252
+ }
253
+ /** Deepest matching help entry: 'admin invite mint -h' shows the invite
254
+ * help, 'hub produce refresh -h' the produce help. */
255
+ function helpFor(cmd, positional) {
256
+ const words = [cmd, ...positional.filter((a) => !a.startsWith('-'))];
257
+ for (let n = words.length; n >= 1; n--) {
258
+ const help = COMMAND_HELP[words.slice(0, n).join(' ')];
259
+ if (help)
260
+ return help;
261
+ }
262
+ return undefined;
263
+ }
215
264
  /** Listen, turning bind errors (EADDRINUSE, unbindable host) into the CLI's
216
265
  * one-line error shape instead of an uncaught 'error' event stack trace. */
217
266
  function listenOrFail(hub, host, port) {
@@ -339,6 +388,28 @@ function fmtLimits(name, overrides) {
339
388
  ? `${name}: ${set.join(', ')}`
340
389
  : `${name}: no overrides — hub-wide defaults apply (AWESHARE_CONSUMER_* env vars)`;
341
390
  }
391
+ /** The five readable tables; bare `list` defaults to the ledger (a read,
392
+ * and the one operators reach for first). */
393
+ const LIST_TARGETS = ['invites', 'producers', 'consumers', 'offerings', 'usage'];
394
+ /** Flags each list target accepts — a flag from another target is a typo. */
395
+ const LIST_FLAGS = {
396
+ invites: ['reveal', 'token', 'json'],
397
+ producers: ['json'],
398
+ consumers: ['json'],
399
+ offerings: ['json'],
400
+ usage: ['details', 'consumer', 'producer', 'alias', 'group-by', 'since', 'sort', 'limit', 'json'],
401
+ };
402
+ /** Sort keys `hub list usage --sort` accepts, mirroring the API's sort param. */
403
+ const USAGE_SORTS = ['time', 'consumer', 'producer', 'model', 'tokens', 'requests'];
404
+ /** What the summary header says for each sort key. */
405
+ const SORT_LABELS = {
406
+ time: 'recent first',
407
+ consumer: 'consumer',
408
+ producer: 'producer',
409
+ model: 'model',
410
+ tokens: 'tokens, busiest first',
411
+ requests: 'requests, busiest first',
412
+ };
342
413
  async function main() {
343
414
  const [cmd, ...rest] = process.argv.slice(2);
344
415
  const { positional, flags } = parseArgs(rest);
@@ -350,7 +421,7 @@ async function main() {
350
421
  console.log(USAGE);
351
422
  return;
352
423
  }
353
- const help = COMMAND_HELP[cmd];
424
+ const help = helpFor(cmd, positional);
354
425
  if (help && (positional.includes('-h') || flags.get('help') === 'true')) {
355
426
  console.log(help);
356
427
  return;
@@ -358,6 +429,8 @@ async function main() {
358
429
  const paths = resolvePaths();
359
430
  switch (cmd) {
360
431
  case 'init': {
432
+ checkFlags('init', flags, []);
433
+ noPositionals('init', positional);
361
434
  ensureDataDir(paths);
362
435
  loadOrCreatePepper(paths);
363
436
  const tokenExisted = existsSync(paths.adminTokenFile);
@@ -376,8 +449,7 @@ async function main() {
376
449
  return;
377
450
  }
378
451
  case 'serve': {
379
- // same runner — 'produce' just names the intent; the local catalog is
380
- // config-driven and loads either way
452
+ checkFlags('serve', flags, ['host', 'port']);
381
453
  noPositionals('serve', positional);
382
454
  const hub = createHubServer({ paths });
383
455
  installConfigReloader(hub);
@@ -390,11 +462,20 @@ async function main() {
390
462
  return;
391
463
  }
392
464
  case 'produce': {
393
- if (positional[0] === 'init') {
465
+ const sub = positional[0];
466
+ if (!sub) {
467
+ // serve absorbed the runner: the local catalog is config-driven and
468
+ // mounts either way, so a second runner only invited drift
469
+ fail("'hub produce' no longer runs the server — 'aweshare hub serve' is the only runner and mounts config.produce.toml by itself;\n" +
470
+ "this group is produce init (scaffold) and produce refresh (re-anchor today's budgets)");
471
+ }
472
+ if (sub !== 'init' && sub !== 'refresh') {
473
+ fail(`unknown produce action '${sub}' — expected init or refresh (the runner is: aweshare hub serve)`);
474
+ }
475
+ if (sub === 'init') {
476
+ checkFlags('produce', flags, []);
394
477
  if (positional[1])
395
478
  fail(`unexpected argument '${positional[1]}' (see: aweshare hub produce -h)`);
396
- if (flags.size > 0)
397
- fail(`'aweshare hub produce init' takes no options`);
398
479
  const files = initHubProduceFiles(paths);
399
480
  loadOrCreatePepper(paths);
400
481
  const adminToken = loadOrCreateAdminToken(paths);
@@ -404,80 +485,127 @@ async function main() {
404
485
  console.log(`secrets file: ${paths.secretsFile} (${files.secretsCreated ? 'created' : 'already exists; kept'})`);
405
486
  console.log(`data dir: ${paths.dataDir}`);
406
487
  console.log(`admin token: ${adminToken}`);
407
- console.log('next: add [[backends]] / [[offerings]] to config.produce.toml, fill secrets.json, then run: aweshare hub produce');
488
+ console.log('next: add [[backends]] / [[offerings]] to config.produce.toml, fill secrets.json, then run: aweshare hub serve');
408
489
  return;
409
490
  }
410
- if (positional[0] === 'refresh') {
411
- const raw = positional[1];
412
- const all = flags.get('all') === 'true';
413
- if (positional[2])
414
- fail(`unexpected argument '${positional[2]}' (see: aweshare hub produce -h)`);
415
- if (all) {
416
- if (raw)
417
- fail(`--all refreshes every hub/ model — drop the alias '${raw}'`);
418
- if (flags.get('add') !== undefined || flags.get('clear') === 'true')
419
- fail('--all is a bare refresh of every hub/ model — --add/--clear stay single-alias');
420
- const api = adminApi();
421
- await runProduceRefreshAll(api, flags.get('json') === 'true');
422
- return;
423
- }
424
- if (!raw)
425
- fail('produce refresh: alias is required (hub/model — the hub/ prefix is optional), or --all for every hub/ model');
426
- // hub-hosted offerings live in the reserved hub/ namespace — accept
427
- // the bare name too; anything else the hub rejects as not its own
428
- const alias = raw.includes('/') ? raw : `hub/${raw}`;
429
- const params = new URLSearchParams({ alias });
430
- const add = flags.get('add');
431
- if (add !== undefined) {
432
- const n = Number(add);
433
- if (!Number.isSafeInteger(n) || n <= 0)
434
- fail(`--add must be a positive integer, got '${add}'`);
435
- params.set('add', String(n));
436
- }
437
- if (flags.get('clear') === 'true') {
438
- if (add !== undefined)
439
- fail('--clear drops both markers — drop --add too');
440
- params.set('clear', 'true');
441
- }
491
+ checkFlags('produce', flags, ['add', 'clear', 'all', 'json']);
492
+ const raw = positional[1];
493
+ const all = flags.get('all') === 'true';
494
+ if (positional[2])
495
+ fail(`unexpected argument '${positional[2]}' (see: aweshare hub produce -h)`);
496
+ if (all) {
497
+ if (raw)
498
+ fail(`--all refreshes every hub/ model — drop the alias '${raw}'`);
499
+ if (flags.get('add') !== undefined || flags.get('clear') === 'true')
500
+ fail('--all is a bare refresh of every hub/ model — --add/--clear stay single-alias');
442
501
  const api = adminApi();
443
- const out = (await api('POST', `/admin/v1/offerings/refresh?${params}`));
444
- if (flags.get('json') === 'true') {
445
- console.log(JSON.stringify(out, null, 2));
446
- return;
447
- }
448
- printRefreshResult(out);
449
- if (out.reset)
450
- console.log(` undo: aweshare hub produce refresh ${out.alias} --clear`);
502
+ await runProduceRefreshAll(api, flags.get('json') === 'true');
451
503
  return;
452
504
  }
453
- // same runner as serve — 'produce' names the intent; the local catalog
454
- // is config-driven and loads either way.
455
- noPositionals('produce', positional);
456
- const hub = createHubServer({ paths });
457
- installConfigReloader(hub);
458
- const host = flags.get('host') ?? hub.config.host;
459
- const port = Number(flags.get('port') ?? hub.config.port);
460
- if (!Number.isInteger(port) || port < 1 || port > 65535) {
461
- fail(`--port must be an integer between 1 and 65535, got '${flags.get('port')}'`);
505
+ if (!raw)
506
+ fail('produce refresh: alias is required (hub/model — the hub/ prefix is optional), or --all for every hub/ model');
507
+ // hub-hosted offerings live in the reserved hub/ namespace — accept
508
+ // the bare name too; anything else the hub rejects as not its own
509
+ const alias = raw.includes('/') ? raw : `hub/${raw}`;
510
+ const params = new URLSearchParams({ alias });
511
+ const add = flags.get('add');
512
+ if (add !== undefined) {
513
+ const n = Number(add);
514
+ if (!Number.isSafeInteger(n) || n <= 0)
515
+ fail(`--add must be a positive integer, got '${add}'`);
516
+ params.set('add', String(n));
462
517
  }
463
- listenOrFail(hub, host, port);
518
+ if (flags.get('clear') === 'true') {
519
+ if (add !== undefined)
520
+ fail('--clear drops both markers — drop --add too');
521
+ params.set('clear', 'true');
522
+ }
523
+ const api = adminApi();
524
+ const out = (await api('POST', `/admin/v1/offerings/refresh?${params}`));
525
+ if (flags.get('json') === 'true') {
526
+ console.log(JSON.stringify(out, null, 2));
527
+ return;
528
+ }
529
+ printRefreshResult(out);
530
+ if (out.reset)
531
+ console.log(` undo: aweshare hub produce refresh ${out.alias} --clear`);
464
532
  return;
465
533
  }
466
- case 'invite': {
467
- // the invite lifecycle: mint (default), revoke, restore — one code, one
468
- // identity, and every suspension travels through the invite that minted it
469
- const sub = positional[0] ?? 'mint';
470
- if (sub !== 'mint' && sub !== 'revoke' && sub !== 'restore') {
471
- fail(`unknown invite action '${sub}' — expected mint, revoke or restore`);
534
+ case 'admin': {
535
+ // the write gate: every state-changing verb lives here — bare calls
536
+ // print help (the safest action), nothing mutates by accident
537
+ const group = positional[0];
538
+ if (!group) {
539
+ console.log(COMMAND_HELP.admin);
540
+ return;
541
+ }
542
+ if (group !== 'invite' && group !== 'offering') {
543
+ fail(`unknown admin target '${group}' — expected invite or offering (see: aweshare hub admin -h)`);
544
+ }
545
+ const action = positional[1];
546
+ if (!action) {
547
+ console.log(COMMAND_HELP[`admin ${group}`]);
548
+ return;
472
549
  }
473
550
  const api = adminApi();
474
- if (sub === 'revoke' || sub === 'restore') {
475
- const id = positional[1];
551
+ if (group === 'invite') {
552
+ if (action !== 'mint' && action !== 'revoke' && action !== 'restore') {
553
+ fail(`unknown invite action '${action}' — expected mint, revoke or restore`);
554
+ }
555
+ if (action === 'mint') {
556
+ checkFlags('admin invite', flags, ['role', 'name', 'count', 'expires-in']);
557
+ if (positional[2])
558
+ fail(`unexpected argument '${positional[2]}' (see: aweshare hub admin invite -h)`);
559
+ const body = {};
560
+ const role = flags.get('role') ?? 'producer';
561
+ if (role !== 'producer' && role !== 'consumer')
562
+ fail("--role must be 'producer' or 'consumer'");
563
+ body.role = role;
564
+ const name = flags.get('name');
565
+ if (name)
566
+ body.name = name;
567
+ if (role === 'consumer' && !name)
568
+ fail('consumer invites need --name (limits, usage and suspension reference consumers by name)');
569
+ const count = flags.get('count');
570
+ if (role === 'consumer' && count && count !== '1')
571
+ fail('consumer invites are one per name — drop --count and mint one per consumer');
572
+ // absent flag still mints a 7d code — an unexpiring secret should be a
573
+ // deliberate choice (`none` here; API callers may omit expiresAt). Since
574
+ // v12 the expiry also bounds the minted identity's lifetime.
575
+ const expiresIn = flags.get('expires-in') ?? '7d';
576
+ if (expiresIn !== 'none') {
577
+ body.expiresAt = new Date(Date.now() + parseDuration(expiresIn)).toISOString();
578
+ }
579
+ if (count) {
580
+ const n = Number(count);
581
+ if (!Number.isInteger(n) || n < 1 || n > 100) {
582
+ fail(`--count must be an integer between 1 and 100, got '${count}'`);
583
+ }
584
+ body.count = n;
585
+ }
586
+ const out = (await api('POST', '/admin/v1/invites', body));
587
+ for (const invite of out.invites)
588
+ console.log(invite.code);
589
+ if (role === 'consumer') {
590
+ console.error('(save this now; codes print only here — re-view later with: aweshare hub list invites --reveal.\n' +
591
+ `hand it to '${name}' — they redeem it and keep the returned asc_ token:\n` +
592
+ ' aweshare consumer join --hub https://YOUR-HUB --code CODE\n' +
593
+ 'no aweshare installed? curl -s -X POST https://YOUR-HUB/invites/v1/redeem -H \'content-type: application/json\' -d \'{"code":"asi_..."}\')');
594
+ }
595
+ else {
596
+ console.error(name
597
+ ? '(save these now; codes print only here — re-view later with: aweshare hub list invites --reveal. redeem: aweshare producer join --hub URL --code CODE)'
598
+ : '(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)');
599
+ }
600
+ return;
601
+ }
602
+ checkFlags('admin invite', flags, []);
603
+ const id = positional[2];
476
604
  if (!id)
477
- fail(`invite ${sub}: id is required (find ids with: aweshare hub list invites)`);
478
- if (positional[2])
479
- fail(`unexpected argument '${positional[2]}' (see: aweshare hub invite -h)`);
480
- if (sub === 'revoke') {
605
+ fail(`invite ${action}: id is required (find ids with: aweshare hub list invites)`);
606
+ if (positional[3])
607
+ fail(`unexpected argument '${positional[3]}' (see: aweshare hub admin invite -h)`);
608
+ if (action === 'revoke') {
481
609
  const out = (await api('DELETE', `/admin/v1/invites/${id}`));
482
610
  if (!out.revoked && !out.suspended) {
483
611
  console.log(`invite ${id} was already revoked — nothing to do`);
@@ -489,7 +617,7 @@ async function main() {
489
617
  else {
490
618
  console.log(`✓ invite ${id} revoked — the code no longer pairs`);
491
619
  }
492
- console.log(` undo: aweshare hub invite restore ${id}`);
620
+ console.log(` undo: aweshare hub admin invite restore ${id}`);
493
621
  return;
494
622
  }
495
623
  const out = (await api('POST', `/admin/v1/invites/${id}/restore`));
@@ -508,65 +636,39 @@ async function main() {
508
636
  }
509
637
  return;
510
638
  }
511
- if (positional[1])
512
- fail(`unexpected argument '${positional[1]}' (see: aweshare hub invite -h)`);
513
- // minting is the one state-changing read-adjacent verb — refuse the old
514
- // ledger flag loudly instead of silently minting a code
515
- if (flags.has('list')) {
516
- fail("'hub invite --list' became its own command — the ledger is: aweshare hub list invites");
517
- }
518
- const body = {};
519
- const role = flags.get('role') ?? 'producer';
520
- if (role !== 'producer' && role !== 'consumer')
521
- fail("--role must be 'producer' or 'consumer'");
522
- body.role = role;
523
- const name = flags.get('name');
524
- if (name)
525
- body.name = name;
526
- if (role === 'consumer' && !name)
527
- fail('consumer invites need --name (limits, usage and suspension reference consumers by name)');
528
- const count = flags.get('count');
529
- if (role === 'consumer' && count && count !== '1')
530
- fail('consumer invites are one per name — drop --count and mint one per consumer');
531
- // absent flag still mints a 7d code — an unexpiring secret should be a
532
- // deliberate choice (`none` here; API callers may omit expiresAt). Since
533
- // v12 the expiry also bounds the minted identity's lifetime.
534
- const expiresIn = flags.get('expires-in') ?? '7d';
535
- if (expiresIn !== 'none') {
536
- body.expiresAt = new Date(Date.now() + parseDuration(expiresIn)).toISOString();
537
- }
538
- if (count) {
539
- const n = Number(count);
540
- if (!Number.isInteger(n) || n < 1 || n > 100) {
541
- fail(`--count must be an integer between 1 and 100, got '${count}'`);
542
- }
543
- body.count = n;
639
+ // offering: the per-alias scalpel ('block' renamed to 'revoke' — the
640
+ // hub error code stays OFFERING_BLOCKED, the API path stays /block)
641
+ if (action === 'block') {
642
+ fail("'block' became 'revoke' — it is 'aweshare hub admin offering revoke ALIAS' now");
544
643
  }
545
- const out = (await api('POST', '/admin/v1/invites', body));
546
- for (const invite of out.invites)
547
- console.log(invite.code);
548
- if (role === 'consumer') {
549
- console.error('(save this now; codes print only here — re-view later with: aweshare hub list invites --reveal.\n' +
550
- `hand it to '${name}' — they redeem it and keep the returned asc_ token:\n` +
551
- ' aweshare consumer join --hub https://YOUR-HUB --code CODE\n' +
552
- 'no aweshare installed? curl -s -X POST https://YOUR-HUB/invites/v1/redeem -H \'content-type: application/json\' -d \'{"code":"asi_..."}\')');
644
+ if (action !== 'revoke' && action !== 'restore') {
645
+ fail(`unknown offering action '${action}' — expected revoke or restore`);
553
646
  }
554
- else {
555
- console.error(name
556
- ? '(save these now; codes print only here — re-view later with: aweshare hub list invites --reveal. redeem: aweshare producer join --hub URL --code CODE)'
557
- : '(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)');
647
+ checkFlags('admin offering', flags, []);
648
+ const alias = positional[2];
649
+ if (!alias)
650
+ fail(`offering ${action}: alias is required (ns/model)`);
651
+ if (positional[3])
652
+ fail(`unexpected argument '${positional[3]}' (see: aweshare hub admin offering -h)`);
653
+ if (action === 'revoke') {
654
+ const out = (await api('POST', `/admin/v1/offerings/block?alias=${encodeURIComponent(alias)}`));
655
+ console.log(`✓ ${alias} revoked (${out.offerings} protocol row(s)) — new requests get 503 OFFERING_BLOCKED`);
656
+ console.log(` undo: aweshare hub admin offering restore ${alias}`);
657
+ return;
558
658
  }
659
+ const out = (await api('POST', `/admin/v1/offerings/restore?alias=${encodeURIComponent(alias)}`));
660
+ console.log(`✓ ${alias} restored (${out.offerings} protocol row(s)) — requests flow again`);
559
661
  return;
560
662
  }
561
663
  case 'list': {
562
- // the read verbs: the invite ledger and the meter (rosters and offering
563
- // health are `status`); bare `list` defaults to the ledger
564
- const sub = positional[0] ?? 'invites';
664
+ // the read verbs, one table per noun; bare `list` defaults to the ledger
665
+ const sub = (positional[0] ?? 'invites');
565
666
  if (positional[1])
566
667
  fail(`unexpected argument '${positional[1]}' (see: aweshare hub list -h)`);
567
- if (sub !== 'invites' && sub !== 'usage') {
568
- fail(`unknown list target '${sub}' — expected invites or usage`);
668
+ if (!LIST_TARGETS.includes(sub)) {
669
+ fail(`unknown list target '${sub}' — expected ${new Intl.ListFormat('en', { type: 'disjunction' }).format(LIST_TARGETS)}`);
569
670
  }
671
+ checkFlags('list', flags, LIST_FLAGS[sub]);
570
672
  const api = adminApi();
571
673
  if (sub === 'invites') {
572
674
  const reveal = flags.get('reveal') === 'true';
@@ -588,6 +690,26 @@ async function main() {
588
690
  }
589
691
  return;
590
692
  }
693
+ if (sub === 'producers' || sub === 'consumers') {
694
+ const roster = (await api('GET', '/admin/v1/tokens'));
695
+ if (flags.get('json') === 'true') {
696
+ console.log(JSON.stringify(sub === 'producers' ? roster.producers : roster.consumers));
697
+ return;
698
+ }
699
+ console.log(sub === 'producers'
700
+ ? renderProducers(roster.producers)
701
+ : renderConsumers(roster.consumers));
702
+ return;
703
+ }
704
+ if (sub === 'offerings') {
705
+ const data = (await api('GET', '/admin/v1/offerings'));
706
+ if (flags.get('json') === 'true') {
707
+ console.log(JSON.stringify(data.offerings, null, 2));
708
+ return;
709
+ }
710
+ console.log(renderOfferings(data.offerings));
711
+ return;
712
+ }
591
713
  const params = new URLSearchParams();
592
714
  const consumer = flags.get('consumer');
593
715
  if (consumer)
@@ -607,8 +729,19 @@ async function main() {
607
729
  const since = flags.get('since') ?? '7d';
608
730
  if (since !== 'all')
609
731
  parseDuration(since); // validate early with a local error
732
+ const sort = flags.get('sort') ?? 'time';
733
+ if (!USAGE_SORTS.includes(sort))
734
+ fail(`--sort must be time (default), consumer, producer, model, tokens or requests, got '${sort}'`);
735
+ const sortKey = sort;
736
+ // a sort key needs its column: the alias view has no consumer, the
737
+ // consumer view no model — reject instead of silently not sorting
738
+ if (group === 'alias' && sort === 'consumer')
739
+ fail('--sort consumer has no consumer column in the alias view — use --group-by consumer-alias (default) or consumer');
740
+ if (group === 'consumer' && sort === 'model')
741
+ fail('--sort model has no model column in the consumer view — use --group-by consumer-alias (default) or alias');
610
742
  params.set('group', group);
611
743
  params.set('since', since);
744
+ params.set('sort', sortKey);
612
745
  const qs = params.toString();
613
746
  const rows = (await api('GET', `/admin/v1/usage/summary?${qs}`));
614
747
  if (flags.get('json') === 'true') {
@@ -619,7 +752,7 @@ async function main() {
619
752
  ? 'all time'
620
753
  : `since ${fmtDate(new Date(Date.now() - parseDuration(since)))} (${since})`;
621
754
  const grouping = group === 'alias' ? 'alias' : group === 'consumer' ? 'consumer' : 'consumer × model';
622
- console.log(`usage ${window} — group: ${grouping}`);
755
+ console.log(`usage ${window} — group: ${grouping}, sort: ${SORT_LABELS[sortKey]}`);
623
756
  console.log(renderUsageSummary(rows, group));
624
757
  return;
625
758
  }
@@ -636,6 +769,8 @@ async function main() {
636
769
  fail('--since applies to the summary, not --details');
637
770
  if (flags.get('group-by') !== undefined)
638
771
  fail('--group-by applies to the summary, not --details');
772
+ if (flags.get('sort') !== undefined)
773
+ fail('--sort applies to the summary, not --details (the log is newest first)');
639
774
  const qs = params.toString();
640
775
  const rows = (await api('GET', `/admin/v1/usage${qs ? `?${qs}` : ''}`));
641
776
  if (flags.get('json') === 'true') {
@@ -658,10 +793,8 @@ async function main() {
658
793
  console.log(renderUsage(rows, (id) => names.get(id) ?? `#${id}`));
659
794
  return;
660
795
  }
661
- case 'usage':
662
- // usage became a `list` subcommand — the read surface is list + status
663
- return fail("'hub usage' moved — it is 'aweshare hub list usage' now");
664
796
  case 'limits': {
797
+ checkFlags('limits', flags, [...LIMIT_FLAGS.map(([flag]) => flag), 'clear', 'json']);
665
798
  const name = positional[0];
666
799
  if (!name)
667
800
  fail('consumer name is required (see: aweshare hub limits -h)');
@@ -700,7 +833,8 @@ async function main() {
700
833
  return;
701
834
  }
702
835
  case 'status': {
703
- noPositionals(cmd, positional);
836
+ checkFlags('status', flags, []);
837
+ noPositionals('status', positional);
704
838
  const api = adminApi();
705
839
  const [roster, offeringsRes, usageRes, rejectionsRes] = await Promise.all([
706
840
  api('GET', '/admin/v1/tokens'),
@@ -736,37 +870,23 @@ async function main() {
736
870
  offerings: offeringsRes.offerings,
737
871
  recent,
738
872
  rejections,
739
- all: flags.get('all') === 'true',
740
873
  }));
741
874
  return;
742
875
  }
743
- case 'offering': {
744
- const action = positional[0];
745
- if (action !== 'block' && action !== 'restore') {
746
- fail(`expected 'block' or 'restore' (see: aweshare hub offering -h), got '${action ?? ''}'`);
747
- }
748
- const alias = positional[1];
749
- if (!alias)
750
- fail(`offering ${action}: alias is required (ns/model)`);
751
- if (positional[2])
752
- fail(`unexpected argument '${positional[2]}' (see: aweshare hub offering -h)`);
753
- const api = adminApi();
754
- if (action === 'block') {
755
- const out = (await api('POST', `/admin/v1/offerings/block?alias=${encodeURIComponent(alias)}`));
756
- console.log(`✓ ${alias} blocked (${out.offerings} protocol row(s)) — new requests get 503 OFFERING_BLOCKED`);
757
- console.log(` undo: aweshare hub offering restore ${alias}`);
758
- return;
759
- }
760
- const out = (await api('POST', `/admin/v1/offerings/restore?alias=${encodeURIComponent(alias)}`));
761
- console.log(`✓ ${alias} restored (${out.offerings} protocol row(s)) — requests flow again`);
762
- return;
763
- }
876
+ case 'invite':
877
+ // invite mint/revoke/restore moved under the admin gate
878
+ return fail("'hub invite' moved — it is 'aweshare hub admin invite mint|revoke|restore' now");
879
+ case 'offering':
880
+ return fail("'hub offering' moved — block/restore live under 'aweshare hub admin offering revoke|restore' now");
881
+ case 'usage':
882
+ // usage became a `list` subcommand — the read surface is list + status
883
+ return fail("'hub usage' moved — it is 'aweshare hub list usage' now");
764
884
  case 'revoke':
765
- // revoke/restore became `invite` subcommands — the invite is the
766
- // lifecycle handle, same shape as `offering block|restore`
767
- return fail("'hub revoke --id N' moved — it is 'aweshare hub invite revoke N' now");
885
+ // revoke/restore became `admin invite` subcommands — the invite is the
886
+ // lifecycle handle, same shape as `admin offering revoke|restore`
887
+ return fail("'hub revoke --id N' moved — it is 'aweshare hub admin invite revoke N' now");
768
888
  case 'restore':
769
- return fail("'hub restore --id N' moved — it is 'aweshare hub invite restore N' now");
889
+ return fail("'hub restore --id N' moved — it is 'aweshare hub admin invite restore N' now");
770
890
  default:
771
891
  fail(`unknown command: ${cmd}\n\n${USAGE}`);
772
892
  }