@astrosheep/square 0.3.3 → 0.3.5

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.
@@ -0,0 +1,461 @@
1
+ import { loadSquare } from '../artifact.js';
2
+ import { runClaudeHook, runCodexHook } from '../claude-hook.js';
3
+ import { coreActivities, coreParticipants, coreStatus } from '../decisions.js';
4
+ import { sessionInbox } from '../inbox.js';
5
+ import { cmdListSquares } from '../list.js';
6
+ import { sameName } from '../model.js';
7
+ import { commandPrefix, participantCommandPrefix, renderActivitiesView, renderGrepActivitiesView, renderVisibleEvent, withPathOutput, } from '../presentation.js';
8
+ import { recordLocalJoin } from '../registry.js';
9
+ import { actId, inSquareCount, isCurrentlyJoined, nowMs, resolveRosterName, sayNumberFor, } from '../runtime.js';
10
+ import { cmdStream, cmdStreamNdjson } from '../stream.js';
11
+ import { formatRelativeTime, formatTimestamp, parseTimeOrRelative } from '../time.js';
12
+ import { cmdWatch } from '../watch.js';
13
+ import { fail, parseDurationMs, parseNameList, parseNonNegativeInteger, parsePositiveInteger, readStdinSync, requireParticipant, requireValue, usage, } from './context.js';
14
+ export const listCommand = {
15
+ parse: (argv) => argv,
16
+ execute(argv, context) {
17
+ cmdListSquares(argv, () => usage(context.command));
18
+ },
19
+ present: () => { },
20
+ };
21
+ export const streamCommand = {
22
+ parse(argv, context) {
23
+ let ndjson = false;
24
+ let forName;
25
+ for (let index = 0; index < argv.length; index++) {
26
+ if (argv[index] === '--ndjson')
27
+ ndjson = true;
28
+ else if (argv[index] === '--for') {
29
+ forName = requireValue(argv, index, argv[index]);
30
+ index += 1;
31
+ }
32
+ else
33
+ usage(context.command);
34
+ }
35
+ if (forName !== undefined && !ndjson)
36
+ usage(context.command);
37
+ return { ndjson, forName };
38
+ },
39
+ async execute(intent, context) {
40
+ if (intent.ndjson)
41
+ await cmdStreamNdjson(context.squarePath, intent.forName);
42
+ else
43
+ await cmdStream(context.squarePath);
44
+ },
45
+ present: () => { },
46
+ };
47
+ export const catchCommand = {
48
+ parse(argv, context) {
49
+ const name = requireParticipant(context.name);
50
+ let activityCount = 1;
51
+ let idleMs;
52
+ let mention;
53
+ let force = false;
54
+ let now = false;
55
+ let follow = false;
56
+ const participants = [];
57
+ for (let index = 0; index < argv.length; index++) {
58
+ if (argv[index] === '--count') {
59
+ activityCount = parsePositiveInteger(requireValue(argv, index, argv[index]), argv[index]);
60
+ index += 1;
61
+ }
62
+ else if (argv[index] === '--by') {
63
+ participants.push(...parseNameList(requireValue(argv, index, argv[index]), argv[index]));
64
+ index += 1;
65
+ }
66
+ else if (argv[index] === '--idle') {
67
+ idleMs = parseDurationMs(requireValue(argv, index, argv[index]), argv[index]);
68
+ index += 1;
69
+ }
70
+ else if (argv[index] === '--mention') {
71
+ const value = argv[index + 1];
72
+ if (value !== undefined && !value.startsWith('--')) {
73
+ mention = value;
74
+ index += 1;
75
+ }
76
+ else {
77
+ mention = name;
78
+ }
79
+ }
80
+ else if (argv[index] === '-f' || argv[index] === '--force')
81
+ force = true;
82
+ else if (argv[index] === '--now')
83
+ now = true;
84
+ else if (argv[index] === '--follow')
85
+ follow = true;
86
+ else
87
+ usage(context.command);
88
+ }
89
+ if (now && follow)
90
+ fail('--follow cannot be combined with --now');
91
+ return {
92
+ activityCount,
93
+ ...(participants.length > 0 ? { participants } : {}),
94
+ ...(mention === undefined ? {} : { mention }),
95
+ ...(idleMs === undefined ? {} : { idleMs }),
96
+ ...(force ? { force } : {}),
97
+ ...(now ? { now } : {}),
98
+ ...(follow ? { follow } : {}),
99
+ };
100
+ },
101
+ async execute(intent, context) {
102
+ await cmdWatch(context.squarePath, requireParticipant(context.name), intent);
103
+ },
104
+ present: () => { },
105
+ };
106
+ function parseActRef(value, flag) {
107
+ const match = value.trim().match(/^(?:act_)?(\d+)$/i);
108
+ if (!match)
109
+ fail(`Invalid ${flag}: expected act id like act_12 or 12.`);
110
+ return Number(match[1]);
111
+ }
112
+ function parseTimestamp(value, flag) {
113
+ const timestamp = parseTimeOrRelative(value, nowMs());
114
+ if (!Number.isFinite(timestamp))
115
+ fail(`Invalid ${flag} timestamp: ${value}`);
116
+ return timestamp;
117
+ }
118
+ function parseEcho(argv, viewer) {
119
+ let lastN = 10;
120
+ let lastNExplicit = false;
121
+ let before;
122
+ let after;
123
+ let afterIndex;
124
+ let atIndex;
125
+ let beforeContext;
126
+ let afterContext;
127
+ let mention;
128
+ let mentionsViewer = false;
129
+ let pending = false;
130
+ let full = false;
131
+ let grep;
132
+ let fixed;
133
+ let ids;
134
+ let order;
135
+ let format;
136
+ let countOnly = false;
137
+ let json = false;
138
+ const participants = [];
139
+ for (let index = 0; index < argv.length; index++) {
140
+ const flag = argv[index];
141
+ if (flag === '--last' || flag === '--limit') {
142
+ lastN = parsePositiveInteger(requireValue(argv, index, flag), flag);
143
+ lastNExplicit = true;
144
+ index += 1;
145
+ }
146
+ else if (flag === '--all') {
147
+ lastN = null;
148
+ lastNExplicit = true;
149
+ }
150
+ else if (flag === '--by' || flag === '--from') {
151
+ participants.push(...parseNameList(requireValue(argv, index, flag), flag));
152
+ index += 1;
153
+ }
154
+ else if (flag === '--before' || flag === '--until') {
155
+ before = parseTimestamp(requireValue(argv, index, flag), flag);
156
+ index += 1;
157
+ }
158
+ else if (flag === '--since') {
159
+ after = parseTimestamp(requireValue(argv, index, flag), flag);
160
+ index += 1;
161
+ }
162
+ else if (flag === '--after') {
163
+ afterIndex = parseActRef(requireValue(argv, index, flag), flag);
164
+ index += 1;
165
+ }
166
+ else if (flag === '--at') {
167
+ atIndex = parseActRef(requireValue(argv, index, flag), flag);
168
+ index += 1;
169
+ }
170
+ else if (flag === '-B') {
171
+ beforeContext = parseNonNegativeInteger(requireValue(argv, index, flag), flag);
172
+ index += 1;
173
+ }
174
+ else if (flag === '-A') {
175
+ afterContext = parseNonNegativeInteger(requireValue(argv, index, flag), flag);
176
+ index += 1;
177
+ }
178
+ else if (flag === '-C') {
179
+ const context = parseNonNegativeInteger(requireValue(argv, index, flag), flag);
180
+ beforeContext = context;
181
+ afterContext = context;
182
+ index += 1;
183
+ }
184
+ else if (flag === '--full')
185
+ full = true;
186
+ else if (flag === '--mention') {
187
+ mention = requireValue(argv, index, flag);
188
+ index += 1;
189
+ }
190
+ else if (flag === '--mentions') {
191
+ const value = requireValue(argv, index, flag);
192
+ if (value !== 'me')
193
+ fail(`Invalid --mentions: only 'me' is supported (got ${value}).`);
194
+ mentionsViewer = true;
195
+ index += 1;
196
+ }
197
+ else if (flag === '--pending')
198
+ pending = true;
199
+ else if (flag === '--grep') {
200
+ grep = requireValue(argv, index, flag);
201
+ index += 1;
202
+ }
203
+ else if (flag === '--fixed') {
204
+ fixed = requireValue(argv, index, flag);
205
+ index += 1;
206
+ }
207
+ else if (flag === '--ids') {
208
+ ids = requireValue(argv, index, flag)
209
+ .split(',')
210
+ .map((item) => item.trim())
211
+ .filter(Boolean)
212
+ .map((item) => parseActRef(item, flag));
213
+ index += 1;
214
+ }
215
+ else if (flag === '--order') {
216
+ const value = requireValue(argv, index, flag);
217
+ if (value !== 'asc' && value !== 'desc')
218
+ fail('Invalid --order: expected asc or desc.');
219
+ order = value;
220
+ index += 1;
221
+ }
222
+ else if (flag === '--format') {
223
+ format = requireValue(argv, index, flag).split(',').map((item) => item.trim()).filter(Boolean);
224
+ index += 1;
225
+ }
226
+ else if (flag === '--count')
227
+ countOnly = true;
228
+ else if (flag === '--json')
229
+ json = true;
230
+ else
231
+ fail('✕ invalid arguments for echo');
232
+ }
233
+ if ((mentionsViewer || pending) && !viewer)
234
+ fail('--mentions me / --pending require --as <name>.');
235
+ if (grep !== undefined && fixed !== undefined)
236
+ fail('--grep and --fixed cannot be combined.');
237
+ if (grep === '' || fixed === '')
238
+ fail('--grep and --fixed require non-empty text.');
239
+ if (!lastNExplicit && (atIndex !== undefined || ids !== undefined || pending))
240
+ lastN = null;
241
+ return {
242
+ lastN,
243
+ participants,
244
+ before,
245
+ after,
246
+ afterIndex,
247
+ atIndex,
248
+ beforeContext,
249
+ afterContext,
250
+ mention,
251
+ mentionsViewer,
252
+ pending,
253
+ viewer,
254
+ full,
255
+ grep,
256
+ fixed,
257
+ ids,
258
+ order,
259
+ format,
260
+ countOnly,
261
+ json,
262
+ };
263
+ }
264
+ function renderFields(doc, item, fields) {
265
+ return fields.map((field) => {
266
+ switch (field) {
267
+ case 'id': return actId(item.index);
268
+ case 'author':
269
+ case 'actor': return item.act.actor ?? '';
270
+ case 'ts':
271
+ case 'at': return formatTimestamp(item.act.at);
272
+ case 'kind': return item.act.kind;
273
+ case 'body': return 'body' in item.act && typeof item.act.body === 'string' ? item.act.body.replace(/\s+/g, ' ').trim() : '';
274
+ case 'number': return item.act.kind === 'say' ? String(sayNumberFor(doc.acts, item.act)) : '';
275
+ default: return '';
276
+ }
277
+ }).join('\t');
278
+ }
279
+ function jsonLine(doc, item) {
280
+ const act = item.act;
281
+ return JSON.stringify({
282
+ id: actId(item.index),
283
+ index: item.index,
284
+ kind: act.kind,
285
+ author: act.actor ?? null,
286
+ at: act.at,
287
+ ts: formatTimestamp(act.at),
288
+ body: 'body' in act && typeof act.body === 'string' ? act.body : '',
289
+ number: act.kind === 'say' ? sayNumberFor(doc.acts, act) : null,
290
+ reach: act.kind === 'say' ? act.reach ?? null : null,
291
+ });
292
+ }
293
+ export const echoCommand = {
294
+ parse(argv, context) { return parseEcho(argv, context.name); },
295
+ execute(options, context) {
296
+ const doc = loadSquare(context.squarePath);
297
+ let events = coreActivities(doc, options);
298
+ const searching = options.grep !== undefined || options.fixed !== undefined;
299
+ const totalMatches = searching ? events.length : 0;
300
+ if (options.lastN != null) {
301
+ events = options.order === 'desc'
302
+ ? events.slice(0, options.lastN)
303
+ : events.slice(-options.lastN);
304
+ }
305
+ if (options.countOnly)
306
+ return `${searching ? totalMatches : events.length}\n`;
307
+ if (options.json)
308
+ return events.map((item) => jsonLine(doc, item)).join('\n') + (events.length > 0 ? '\n' : '');
309
+ if (options.format !== undefined && options.format.length > 0) {
310
+ return events.map((item) => renderFields(doc, item, options.format)).join('\n') + (events.length > 0 ? '\n' : '');
311
+ }
312
+ const pattern = options.grep ?? options.fixed;
313
+ const output = pattern === undefined || pattern === ''
314
+ ? renderActivitiesView(doc, events, null, options.full, context.squarePath, options.viewer ?? '')
315
+ : renderGrepActivitiesView(events, totalMatches, options.full, context.squarePath, pattern, options.fixed !== undefined);
316
+ return withPathOutput(context.squarePath, output, { participantCount: inSquareCount(doc) });
317
+ },
318
+ present: (result) => process.stdout.write(result),
319
+ };
320
+ export const warmupCommand = {
321
+ parse(argv, context) { if (argv.length > 0)
322
+ usage(context.command); return undefined; },
323
+ execute(_intent, context) {
324
+ const doc = loadSquare(context.squarePath);
325
+ return withPathOutput(context.squarePath, doc.warmup.join('\n'), {
326
+ participantCount: inSquareCount(doc),
327
+ });
328
+ },
329
+ present: (result) => process.stdout.write(result),
330
+ };
331
+ export const participantsCommand = {
332
+ parse(argv, context) { if (argv.length > 0)
333
+ usage(context.command); return undefined; },
334
+ execute(_intent, context) {
335
+ const doc = loadSquare(context.squarePath);
336
+ const result = coreParticipants(doc, nowMs());
337
+ const lines = result.participants.map((participant) => {
338
+ const glyph = participant.state === 'done' ? '×' : participant.presence === 'watching' ? '◎' : participant.activityCount > 0 ? '●' : '○';
339
+ const state = participant.state === 'done' ? 'done' : participant.presence === 'watching' ? 'catching' : participant.state;
340
+ const last = participant.lastActiveAt === undefined ? '—' : formatRelativeTime(participant.lastActiveAt, result.now);
341
+ return ` ${glyph} ${participant.name} · ${state} · ${participant.activityCount} act${participant.activityCount === 1 ? '' : 's'} · ${last}`;
342
+ });
343
+ const participantCount = result.participants.filter((participant) => participant.state === 'active').length;
344
+ return withPathOutput(context.squarePath, ['participants', ...lines].join('\n'), {
345
+ participantCount,
346
+ });
347
+ },
348
+ present: (result) => process.stdout.write(result),
349
+ };
350
+ export const statusCommand = {
351
+ parse(argv, context) { if (argv.length > 0)
352
+ usage(context.command); return undefined; },
353
+ execute(_intent, context) {
354
+ const doc = loadSquare(context.squarePath);
355
+ const result = coreStatus(doc, nowMs());
356
+ const active = result.participants.filter((participant) => participant.state === 'active').sort((a, b) => {
357
+ const aViewer = context.name !== undefined && sameName(a.name, context.name);
358
+ const bViewer = context.name !== undefined && sameName(b.name, context.name);
359
+ if (aViewer !== bViewer)
360
+ return aViewer ? -1 : 1;
361
+ return (b.lastActiveAt ?? -Infinity) - (a.lastActiveAt ?? -Infinity) || a.name.localeCompare(b.name);
362
+ });
363
+ const people = active.length === 0 ? [' ○ nobody in the square'] : active.map((participant) => {
364
+ const glyph = participant.presence === 'watching'
365
+ ? '◎'
366
+ : participant.activityCount > 0 ? '●' : '○';
367
+ const summary = participant.activityCount > 0
368
+ ? `${participant.activityCount} act${participant.activityCount === 1 ? '' : 's'} · ${participant.lastActiveAt === undefined
369
+ ? 'just now'
370
+ : formatRelativeTime(participant.lastActiveAt, result.now)}`
371
+ : `quiet · ${participant.lastActiveAt === undefined ? '—' : formatRelativeTime(participant.lastActiveAt, result.now)}`;
372
+ const showAttention = context.name === undefined || sameName(participant.name, context.name);
373
+ const attention = !showAttention
374
+ ? ''
375
+ : participant.pendingMentionCount > 0
376
+ ? `${participant.pendingMentionCount} mention${participant.pendingMentionCount === 1 ? '' : 's'} waiting`
377
+ : participant.unreadActivityCount > 0
378
+ ? `${participant.unreadActivityCount} change${participant.unreadActivityCount === 1 ? '' : 's'} waiting`
379
+ : 'caught up';
380
+ return ` ${glyph} ${participant.name} · ${summary}${attention === '' ? '' : ` · ${attention}`}`;
381
+ });
382
+ const cap = result.hardCap === null ? 'unlimited' : String(result.hardCap);
383
+ const hold = result.holdActive
384
+ ? `· ${result.holdActor ?? 'someone'} raised a hand${result.holdReason ? ` — ${result.holdReason}` : ''} · ${result.holdAt === undefined
385
+ ? 'just now'
386
+ : formatRelativeTime(result.holdAt, result.now)}`
387
+ : undefined;
388
+ const visible = result.latestAct === undefined
389
+ ? ''
390
+ : renderVisibleEvent(doc.acts, result.latestAct, context.name ?? '', {
391
+ now: result.now,
392
+ preview: 200,
393
+ actNumber: result.latestAct.kind === 'say'
394
+ ? sayNumberFor(doc.acts, result.latestAct)
395
+ : undefined,
396
+ });
397
+ const latest = visible === ''
398
+ ? [result.latestAct === undefined
399
+ ? ' ○ no public activity yet'
400
+ : ' · latest activity is private to another participant']
401
+ : [` ${visible.replace(/\n/g, '\n ')}`];
402
+ if (visible.includes('more chars') && result.latestAct !== undefined) {
403
+ const prefix = context.name === undefined ? commandPrefix(context.squarePath) : participantCommandPrefix(context.squarePath, context.name);
404
+ latest.push(`» ${prefix} echo --at ${actId(result.latestAct)} -C 2 --full`);
405
+ }
406
+ const output = [
407
+ `${result.activeCount} active · ${result.doneCount} done · cap ${cap} · throttle ${result.throttlePerMinute === undefined ? 'none' : `${result.throttlePerMinute}/min`}`,
408
+ ...(hold === undefined ? [] : ['', hold]), '', 'around the square', ...people, '', 'latest', ...latest,
409
+ ].join('\n');
410
+ return withPathOutput(context.squarePath, output, { participantCount: result.activeCount, held: result.holdActive });
411
+ },
412
+ present: (result) => process.stdout.write(result),
413
+ };
414
+ export const inboxCommand = {
415
+ parse(argv, context) {
416
+ let sessionId;
417
+ let json = false;
418
+ for (let index = 0; index < argv.length; index++) {
419
+ if (argv[index] === '--for-session') {
420
+ sessionId = requireValue(argv, index, argv[index]);
421
+ index += 1;
422
+ }
423
+ else if (argv[index] === '--json')
424
+ json = true;
425
+ else
426
+ usage(context.command);
427
+ }
428
+ if (!sessionId)
429
+ fail('inbox requires --for-session <session-id>.');
430
+ return { sessionId, json };
431
+ },
432
+ execute(intent) {
433
+ const inbox = sessionInbox(intent.sessionId);
434
+ return intent.json ? `${JSON.stringify(inbox)}\n` : inbox.map((membership) => `${membership.name}\t${membership.squarePath}\t${membership.notifications.length}\n`).join('');
435
+ },
436
+ present: (result) => process.stdout.write(result),
437
+ };
438
+ function hookCommand(runHook) {
439
+ return {
440
+ parse(argv, context) { if (argv.length > 0)
441
+ usage(context.command); return undefined; },
442
+ execute: () => runHook(readStdinSync()),
443
+ present: (result) => process.stdout.write(result),
444
+ };
445
+ }
446
+ export const claudeHookCommand = hookCommand(runClaudeHook);
447
+ export const codexHookCommand = hookCommand(runCodexHook);
448
+ /** Maintain the local discovery cache before participant-facing adapters run. */
449
+ export function refreshLocalRegistration(squarePath, name) {
450
+ if (name === undefined)
451
+ return;
452
+ try {
453
+ const doc = loadSquare(squarePath);
454
+ const known = resolveRosterName(doc, name);
455
+ if (known !== undefined && isCurrentlyJoined(doc.acts, known))
456
+ recordLocalJoin(known, squarePath);
457
+ }
458
+ catch {
459
+ // The machine-local discovery cache never makes a Square command fail.
460
+ }
461
+ }
@@ -0,0 +1,48 @@
1
+ import { helpRequest } from '../help.js';
2
+ import { SquareError } from '../model.js';
3
+ import { defaultContext, parseGlobalArgs } from './context.js';
4
+ import { refreshLocalRegistration } from './observation-commands.js';
5
+ import { executeRegisteredCommand, findCommand } from './registry.js';
6
+ function isMutatingCommand(command, argv) {
7
+ if (['build', 'join', 'catch', 'act', 'done', 'hold', 'resume', 'compact'].includes(command))
8
+ return true;
9
+ return command === 'doctor' && argv.some((argument) => argument === '--fix' || argument === 'reconcile-backlog');
10
+ }
11
+ function handleSquareError(error) {
12
+ if (error instanceof SquareError) {
13
+ process.stderr.write(`${error.message}\n`);
14
+ process.exit(error.code === 'not_found' ? 1 : 2);
15
+ }
16
+ throw error;
17
+ }
18
+ /** Parse global flags, select an executable adapter, and leave all command work to the registry. */
19
+ export async function runCli(rawArgs = process.argv.slice(2)) {
20
+ try {
21
+ const requestedHelp = helpRequest(rawArgs);
22
+ if (requestedHelp !== undefined) {
23
+ await executeRegisteredCommand('help', requestedHelp.command === undefined ? [] : [requestedHelp.command], defaultContext('help', '.square/SQUARE.md'));
24
+ return;
25
+ }
26
+ const parsed = parseGlobalArgs(rawArgs);
27
+ if (parsed.args.length === 0 || parsed.args[0] === '--help' || parsed.args[0] === '-h') {
28
+ await executeRegisteredCommand('help', [], defaultContext('help', parsed.squarePath, parsed.name));
29
+ return;
30
+ }
31
+ const command = parsed.args[0];
32
+ if (findCommand(command) === undefined) {
33
+ process.stderr.write(`unknown command: ${command}\nrun 'square' for usage\n`);
34
+ process.exit(2);
35
+ }
36
+ if (!parsed.explicitSquarePath && parsed.multipleSquares && isMutatingCommand(command, parsed.args.slice(1))) {
37
+ process.stderr.write('✕ more than one square is active here; choose the path before changing or consuming activity.\n» square ls\n');
38
+ process.exit(2);
39
+ }
40
+ if (['act', 'catch', 'done', 'hold', 'resume'].includes(command)) {
41
+ refreshLocalRegistration(parsed.squarePath, parsed.name);
42
+ }
43
+ await executeRegisteredCommand(command, parsed.args.slice(1), defaultContext(command, parsed.squarePath, parsed.name));
44
+ }
45
+ catch (error) {
46
+ handleSquareError(error);
47
+ }
48
+ }
@@ -0,0 +1,40 @@
1
+ import { actCommand, buildCommand, compactCommand, doneCommand, holdCommand, joinCommand, resumeCommand } from './square-commands.js';
2
+ import { doctorCommand } from './maintenance-commands.js';
3
+ import { harnessCommand } from './harness-command.js';
4
+ import { helpCommand, versionCommand } from './meta-commands.js';
5
+ import { catchCommand, claudeHookCommand, codexHookCommand, echoCommand, inboxCommand, listCommand, participantsCommand, statusCommand, streamCommand, warmupCommand, } from './observation-commands.js';
6
+ /** Every public command is an executable adapter, including aliases and utility commands. */
7
+ export const commandRegistry = [
8
+ { names: ['build'], spec: buildCommand },
9
+ { names: ['ls', 'list'], spec: listCommand },
10
+ { names: ['join'], spec: joinCommand },
11
+ { names: ['stream'], spec: streamCommand },
12
+ { names: ['inbox'], spec: inboxCommand },
13
+ { names: ['claude-hook'], spec: claudeHookCommand },
14
+ { names: ['codex-hook'], spec: codexHookCommand },
15
+ { names: ['catch'], spec: catchCommand },
16
+ { names: ['act'], spec: actCommand },
17
+ { names: ['done'], spec: doneCommand },
18
+ { names: ['hold'], spec: holdCommand },
19
+ { names: ['resume'], spec: resumeCommand },
20
+ { names: ['harness'], spec: harnessCommand },
21
+ { names: ['compact'], spec: compactCommand },
22
+ { names: ['doctor'], spec: doctorCommand },
23
+ { names: ['echo'], spec: echoCommand },
24
+ { names: ['warmup'], spec: warmupCommand },
25
+ { names: ['status'], spec: statusCommand },
26
+ { names: ['participants'], spec: participantsCommand },
27
+ { names: ['help'], spec: helpCommand },
28
+ { names: ['version', '--version', '-v'], spec: versionCommand },
29
+ ];
30
+ export function findCommand(name) {
31
+ return commandRegistry.find((command) => command.names.includes(name));
32
+ }
33
+ export async function executeRegisteredCommand(name, argv, context) {
34
+ const command = findCommand(name);
35
+ if (command === undefined)
36
+ throw new Error(`No registered command named ${name}`);
37
+ const intent = command.spec.parse(argv, context);
38
+ const result = await command.spec.execute(intent, context);
39
+ command.spec.present(result, context);
40
+ }