@ours.network/fleet 0.19.0-nightly.1 → 0.19.0-nightly.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/dist/build-info.json +4 -4
- package/dist/cli.js +5 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.js +44 -3
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +28 -0
- package/dist/doctor.js +125 -1
- package/dist/owner-channel/channel.d.ts +7 -0
- package/dist/owner-channel/channel.js +47 -0
- package/dist/owner-channel/commands.d.ts +9 -0
- package/dist/owner-channel/commands.js +393 -0
- package/dist/rooms-tasks/cli.d.ts +4 -0
- package/dist/rooms-tasks/cli.js +1102 -0
- package/dist/rooms-tasks/close.d.ts +21 -0
- package/dist/rooms-tasks/close.js +159 -0
- package/dist/rooms-tasks/config.d.ts +15 -0
- package/dist/rooms-tasks/config.js +174 -0
- package/dist/rooms-tasks/cowork-adapter.d.ts +77 -0
- package/dist/rooms-tasks/cowork-adapter.js +272 -0
- package/dist/rooms-tasks/external-worker.d.ts +2 -0
- package/dist/rooms-tasks/external-worker.js +40 -0
- package/dist/rooms-tasks/index.d.ts +10 -0
- package/dist/rooms-tasks/index.js +10 -0
- package/dist/rooms-tasks/provision.d.ts +21 -0
- package/dist/rooms-tasks/provision.js +243 -0
- package/dist/rooms-tasks/room-state.d.ts +28 -0
- package/dist/rooms-tasks/room-state.js +221 -0
- package/dist/rooms-tasks/task-state.d.ts +43 -0
- package/dist/rooms-tasks/task-state.js +277 -0
- package/dist/rooms-tasks/templates.d.ts +6 -0
- package/dist/rooms-tasks/templates.js +94 -0
- package/dist/rooms-tasks/terminal.d.ts +23 -0
- package/dist/rooms-tasks/terminal.js +53 -0
- package/dist/rooms-tasks/types.d.ts +187 -0
- package/dist/rooms-tasks/types.js +20 -0
- package/dist/temp-lifecycle.d.ts +9 -2
- package/dist/temp-lifecycle.js +61 -0
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFile, spawn } from 'node:child_process';
|
|
2
|
+
import { launchFleetWorker } from '../rooms-tasks/external-worker.js';
|
|
2
3
|
import { OWNER_COMMENT_LABEL, ownerNotices } from './notices.js';
|
|
3
4
|
/** A malformed invocation; the dispatcher answers it with annotated help. */
|
|
4
5
|
class OwnerCommandUsageError extends Error {
|
|
@@ -142,6 +143,396 @@ export const ownerCommands = [
|
|
|
142
143
|
name: 'version', summary: 'report the fleet version',
|
|
143
144
|
execute: noArgs('/version', async (ctx) => ctx.reply(`ℹ️ ours-fleet ${ctx.version}`)),
|
|
144
145
|
},
|
|
146
|
+
{
|
|
147
|
+
name: 'tasks', usage: '/tasks [state]',
|
|
148
|
+
summary: 'list tasks (optionally filter by state)',
|
|
149
|
+
execute: async (ctx, args) => {
|
|
150
|
+
const { listTasks } = await import('../rooms-tasks/task-state.js');
|
|
151
|
+
const stateFilter = args?.trim() || undefined;
|
|
152
|
+
const tasks = listTasks(stateFilter && stateFilter !== 'all' ? { state: stateFilter } : undefined);
|
|
153
|
+
if (!tasks.length)
|
|
154
|
+
return ctx.reply('📋 No tasks.');
|
|
155
|
+
const lines = tasks.map(t => {
|
|
156
|
+
const blocked = t.blocked ? ` [BLOCKED: ${t.blocked.reason}]` : '';
|
|
157
|
+
return `${t.task_id} ${t.state}${blocked} ${t.title}`;
|
|
158
|
+
});
|
|
159
|
+
await ctx.reply(tail(`📋 Tasks:\n${lines.join('\n')}`, REPLY_MAX_CHARS));
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: 'task',
|
|
164
|
+
usage: '/task <create|list|show|start|block|unblock|review|done|cancel|recover> ...',
|
|
165
|
+
summary: 'task lifecycle subcommands',
|
|
166
|
+
execute: async (ctx, args) => {
|
|
167
|
+
if (!args)
|
|
168
|
+
throw new OwnerCommandUsageError('usage: /task <subcommand> <id>');
|
|
169
|
+
const argLines = args.trim().split('\n');
|
|
170
|
+
const firstLineTokens = argLines[0].trim().split(/\s+/);
|
|
171
|
+
const sub = firstLineTokens[0];
|
|
172
|
+
const rest = firstLineTokens.slice(1);
|
|
173
|
+
const trailingLines = argLines.slice(1).join('\n').trim() || undefined;
|
|
174
|
+
const showTask = async (id) => {
|
|
175
|
+
const { getTask } = await import('../rooms-tasks/task-state.js');
|
|
176
|
+
const t = getTask(id);
|
|
177
|
+
const lines = [
|
|
178
|
+
`📋 Task: ${t.task_id}`,
|
|
179
|
+
`Title: ${t.title}`,
|
|
180
|
+
`State: ${t.state}${t.blocked ? ` [BLOCKED: ${t.blocked.reason}]` : ''}`,
|
|
181
|
+
...(t.template ? [`Template: ${t.template.name}@${t.template.version}`] : []),
|
|
182
|
+
...(t.room_id ? [`Room: ${t.room_id}`] : []),
|
|
183
|
+
`Origin: ${t.origin.type}`,
|
|
184
|
+
`Created: ${t.created_at}`,
|
|
185
|
+
];
|
|
186
|
+
await ctx.reply(lines.join('\n'));
|
|
187
|
+
};
|
|
188
|
+
try {
|
|
189
|
+
switch (sub) {
|
|
190
|
+
case 'create': {
|
|
191
|
+
if (!rest[0])
|
|
192
|
+
throw new OwnerCommandUsageError('usage: /task create [--backlog] [--template=<name>|--no-room] <title>');
|
|
193
|
+
const backlog = rest.includes('--backlog');
|
|
194
|
+
const tplFlag = rest.find(r => r.startsWith('--template='));
|
|
195
|
+
const template = tplFlag ? tplFlag.slice('--template='.length) : undefined;
|
|
196
|
+
const noRoom = rest.includes('--no-room');
|
|
197
|
+
const titleParts = rest.filter(r => r !== '--backlog' && !r.startsWith('--template=') && r !== '--no-room');
|
|
198
|
+
if (!titleParts.length)
|
|
199
|
+
throw new OwnerCommandUsageError('usage: /task create [--backlog] [--template=<name>|--no-room] <title>');
|
|
200
|
+
const { createTask } = await import('../rooms-tasks/task-state.js');
|
|
201
|
+
const t = createTask({
|
|
202
|
+
title: titleParts.join(' '),
|
|
203
|
+
origin: { type: 'owner_channel' },
|
|
204
|
+
start: !backlog,
|
|
205
|
+
...(template ? { template: { name: template, version: 1, content_hash: '' } } : {}),
|
|
206
|
+
...(noRoom ? { no_room: true } : {}),
|
|
207
|
+
...(trailingLines ? { brief: trailingLines } : {}),
|
|
208
|
+
});
|
|
209
|
+
const lines = [`Task ${t.task_id} created · ${t.state}`];
|
|
210
|
+
if (template)
|
|
211
|
+
lines.push(`Template: ${template}`);
|
|
212
|
+
if (noRoom)
|
|
213
|
+
lines.push('No room requested');
|
|
214
|
+
await ctx.reply(lines.join('\n'));
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
case 'list': {
|
|
218
|
+
const { listTasks } = await import('../rooms-tasks/task-state.js');
|
|
219
|
+
const filter = rest[0];
|
|
220
|
+
const stateMap = {
|
|
221
|
+
backlog: 'backlog', active: 'active', blocked: 'active',
|
|
222
|
+
done: 'done', all: ['backlog', 'provisioning', 'active', 'review', 'done', 'cancelled', 'failed'],
|
|
223
|
+
};
|
|
224
|
+
const stateFilter = filter && stateMap[filter]
|
|
225
|
+
? { state: stateMap[filter] }
|
|
226
|
+
: undefined;
|
|
227
|
+
const tasks = listTasks(stateFilter);
|
|
228
|
+
if (filter === 'blocked') {
|
|
229
|
+
const blocked = tasks.filter(t => t.blocked);
|
|
230
|
+
if (!blocked.length) {
|
|
231
|
+
await ctx.reply('📋 No blocked tasks.');
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
const lines = blocked.map(t => `${t.task_id} ${t.state} ${t.title} [BLOCKED: ${t.blocked.reason}]`);
|
|
235
|
+
await ctx.reply(tail(`📋 Blocked tasks:\n${lines.join('\n')}`, REPLY_MAX_CHARS));
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
if (!tasks.length) {
|
|
239
|
+
await ctx.reply('📋 No tasks.');
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
const lines = tasks.map(t => `${t.task_id} ${t.state} ${t.title}${t.blocked ? ` [BLOCKED]` : ''}`);
|
|
243
|
+
await ctx.reply(tail(`📋 Tasks:\n${lines.join('\n')}`, REPLY_MAX_CHARS));
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
case 'show': {
|
|
247
|
+
if (!rest[0])
|
|
248
|
+
throw new OwnerCommandUsageError('usage: /task show <id>');
|
|
249
|
+
await showTask(rest[0]);
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
case 'start': {
|
|
253
|
+
if (!rest[0])
|
|
254
|
+
throw new OwnerCommandUsageError('usage: /task start <id>');
|
|
255
|
+
const { startTask } = await import('../rooms-tasks/task-state.js');
|
|
256
|
+
const t = startTask(rest[0]);
|
|
257
|
+
await ctx.reply(`✅ Task ${t.task_id} → provisioning`);
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
case 'block': {
|
|
261
|
+
if (!rest[0] || !rest[1])
|
|
262
|
+
throw new OwnerCommandUsageError('usage: /task block <id> <reason>');
|
|
263
|
+
const { blockTask } = await import('../rooms-tasks/task-state.js');
|
|
264
|
+
const reason = rest.slice(1).join(' ');
|
|
265
|
+
const t = blockTask(rest[0], reason);
|
|
266
|
+
await ctx.reply(`🚧 Task ${t.task_id} blocked: ${reason}`);
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
case 'unblock': {
|
|
270
|
+
if (!rest[0])
|
|
271
|
+
throw new OwnerCommandUsageError('usage: /task unblock <id>');
|
|
272
|
+
const { unblockTask } = await import('../rooms-tasks/task-state.js');
|
|
273
|
+
const t = unblockTask(rest[0]);
|
|
274
|
+
await ctx.reply(`✅ Task ${t.task_id} unblocked`);
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
case 'review': {
|
|
278
|
+
if (!rest[0])
|
|
279
|
+
throw new OwnerCommandUsageError('usage: /task review <id>');
|
|
280
|
+
const { reviewTask } = await import('../rooms-tasks/task-state.js');
|
|
281
|
+
const t = reviewTask(rest[0]);
|
|
282
|
+
await ctx.reply(`📝 Task ${t.task_id} → review`);
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
case 'done': {
|
|
286
|
+
if (!rest[0])
|
|
287
|
+
throw new OwnerCommandUsageError('usage: /task done <id> [summary]');
|
|
288
|
+
const summary = rest.slice(1).join(' ') || undefined;
|
|
289
|
+
await ctx.terminalTask(rest[0], 'done', summary ? { summary } : undefined);
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
case 'cancel': {
|
|
293
|
+
if (rest.length < 2 || rest[0] !== rest[1])
|
|
294
|
+
throw new OwnerCommandUsageError('destructive: /task cancel <id> <id> — provide the task ID twice');
|
|
295
|
+
await ctx.terminalTask(rest[0], 'cancelled');
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
case 'recover': {
|
|
299
|
+
if (!rest[0])
|
|
300
|
+
throw new OwnerCommandUsageError('usage: /task recover <id>');
|
|
301
|
+
const { getTask } = await import('../rooms-tasks/task-state.js');
|
|
302
|
+
const { getRoomRecord } = await import('../rooms-tasks/room-state.js');
|
|
303
|
+
const t = getTask(rest[0]);
|
|
304
|
+
if (t.terminal_intent?.status === 'pending') {
|
|
305
|
+
await ctx.terminalTask(t.task_id, t.terminal_intent.kind, t.terminal_intent.outcome);
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
const room = t.room_id ? getRoomRecord(t.room_id) : undefined;
|
|
309
|
+
const hints = [];
|
|
310
|
+
if (t.state === 'provisioning' && room) {
|
|
311
|
+
if (room.provisioning_detail === 'waiting_cowork')
|
|
312
|
+
hints.push('Cowork socket unreachable');
|
|
313
|
+
if (room.provisioning_detail === 'waiting_owner_invite')
|
|
314
|
+
hints.push('Owner invite missing or invalid');
|
|
315
|
+
if (room.provisioning_detail === 'owner_cid_mismatch')
|
|
316
|
+
hints.push('Owner CID mismatch');
|
|
317
|
+
if (room.provisioning_detail === 'member_failed')
|
|
318
|
+
hints.push(`Member failed at step ${room.saga.step_index}`);
|
|
319
|
+
}
|
|
320
|
+
const lines = [`Task ${t.task_id} · ${t.state}`];
|
|
321
|
+
if (room)
|
|
322
|
+
lines.push(`Room ${room.room_id} · ${room.state} · saga: ${room.saga.phase}`);
|
|
323
|
+
if (hints.length)
|
|
324
|
+
lines.push(`Hints: ${hints.join('; ')}`);
|
|
325
|
+
else
|
|
326
|
+
lines.push('No automated recovery actions available.');
|
|
327
|
+
await ctx.reply(lines.join('\n'));
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
default:
|
|
331
|
+
// bare /task <id> → show
|
|
332
|
+
await showTask(sub);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
catch (e) {
|
|
336
|
+
if (e instanceof OwnerCommandUsageError)
|
|
337
|
+
throw e;
|
|
338
|
+
await ctx.reply(`⚠️ ${e instanceof Error ? e.message : String(e)}`);
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
name: 'rooms', summary: 'list rooms',
|
|
344
|
+
execute: noArgs('/rooms', async (ctx) => {
|
|
345
|
+
const { listRoomRecords } = await import('../rooms-tasks/room-state.js');
|
|
346
|
+
const rooms = listRoomRecords();
|
|
347
|
+
if (!rooms.length)
|
|
348
|
+
return ctx.reply('🏠 No rooms.');
|
|
349
|
+
const lines = rooms.map(r => `${r.room_id} ${r.state} ${r.room_name}${r.task_id ? ` (task: ${r.task_id})` : ''}`);
|
|
350
|
+
await ctx.reply(tail(`🏠 Rooms:\n${lines.join('\n')}`, REPLY_MAX_CHARS));
|
|
351
|
+
}),
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
name: 'room',
|
|
355
|
+
usage: '/room <create|list|show|close|recover> ...',
|
|
356
|
+
summary: 'room lifecycle subcommands',
|
|
357
|
+
execute: async (ctx, args) => {
|
|
358
|
+
if (!args)
|
|
359
|
+
throw new OwnerCommandUsageError('usage: /room <subcommand> <id>');
|
|
360
|
+
const argLines = args.trim().split('\n');
|
|
361
|
+
const firstLineTokens = argLines[0].trim().split(/\s+/);
|
|
362
|
+
const sub = firstLineTokens[0];
|
|
363
|
+
const rest = firstLineTokens.slice(1);
|
|
364
|
+
const roomTrailingLines = argLines.slice(1).join('\n').trim() || undefined;
|
|
365
|
+
const showRoom = async (id) => {
|
|
366
|
+
const { getRoomRecord } = await import('../rooms-tasks/room-state.js');
|
|
367
|
+
const r = getRoomRecord(id);
|
|
368
|
+
if (!r)
|
|
369
|
+
return ctx.reply(`⚠️ room not found: ${id}`);
|
|
370
|
+
const lines = [
|
|
371
|
+
`🏠 Room: ${r.room_id}`,
|
|
372
|
+
`Name: ${r.room_name}`,
|
|
373
|
+
`State: ${r.state}`,
|
|
374
|
+
`Saga: ${r.saga.phase} (step ${r.saga.step_index})`,
|
|
375
|
+
...(r.task_id ? [`Task: ${r.task_id}`] : []),
|
|
376
|
+
...(r.provisioning_detail ? [`Detail: ${r.provisioning_detail}`] : []),
|
|
377
|
+
...(r.saga.error ? [`Error: ${r.saga.error}`] : []),
|
|
378
|
+
`Created: ${r.created_at}`,
|
|
379
|
+
];
|
|
380
|
+
await ctx.reply(lines.join('\n'));
|
|
381
|
+
};
|
|
382
|
+
try {
|
|
383
|
+
switch (sub) {
|
|
384
|
+
case 'create': {
|
|
385
|
+
const tplFlag = rest.find(r => r.startsWith('--template='));
|
|
386
|
+
const templateName = tplFlag?.slice('--template='.length);
|
|
387
|
+
const nameParts = rest.filter(r => !r.startsWith('--'));
|
|
388
|
+
if (!nameParts.length)
|
|
389
|
+
throw new OwnerCommandUsageError('usage: /room create --template=<name> <room name>');
|
|
390
|
+
const { randomUUID } = await import('node:crypto');
|
|
391
|
+
const { createRoomRecord } = await import('../rooms-tasks/room-state.js');
|
|
392
|
+
const { resolveTemplate, snapshotTemplate } = await import('../rooms-tasks/templates.js');
|
|
393
|
+
let templateSnapshot;
|
|
394
|
+
if (templateName) {
|
|
395
|
+
const tpl = resolveTemplate(templateName, {});
|
|
396
|
+
if (!tpl)
|
|
397
|
+
throw new OwnerCommandUsageError(`unknown template: ${templateName}`);
|
|
398
|
+
templateSnapshot = snapshotTemplate(tpl);
|
|
399
|
+
}
|
|
400
|
+
const r = createRoomRecord({
|
|
401
|
+
room_id: randomUUID().replace(/-/g, '').slice(0, 16),
|
|
402
|
+
room_name: nameParts.join(' '),
|
|
403
|
+
...(templateSnapshot ? { template_snapshot: templateSnapshot } : {}),
|
|
404
|
+
...(roomTrailingLines ? { goal: roomTrailingLines } : {}),
|
|
405
|
+
});
|
|
406
|
+
const lines = [`🏠 Room ${r.room_id} created · ${r.state}`];
|
|
407
|
+
if (templateName)
|
|
408
|
+
lines.push(`Template: ${templateName}`);
|
|
409
|
+
await ctx.reply(lines.join('\n'));
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
case 'list': {
|
|
413
|
+
const { listRoomRecords } = await import('../rooms-tasks/room-state.js');
|
|
414
|
+
const filter = rest[0];
|
|
415
|
+
let rooms = listRoomRecords();
|
|
416
|
+
if (filter === 'active')
|
|
417
|
+
rooms = rooms.filter(r => r.state === 'active');
|
|
418
|
+
if (!rooms.length) {
|
|
419
|
+
await ctx.reply('🏠 No rooms.');
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
const lines = rooms.map(r => `${r.room_id} ${r.state} ${r.room_name}${r.task_id ? ` (task: ${r.task_id})` : ''}`);
|
|
423
|
+
await ctx.reply(tail(`🏠 Rooms:\n${lines.join('\n')}`, REPLY_MAX_CHARS));
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
case 'show': {
|
|
427
|
+
if (!rest[0])
|
|
428
|
+
throw new OwnerCommandUsageError('usage: /room show <id>');
|
|
429
|
+
await showRoom(rest[0]);
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
case 'close': {
|
|
433
|
+
if (rest.length < 2 || rest[0] !== rest[1])
|
|
434
|
+
throw new OwnerCommandUsageError('destructive: /room close <id> <id> — provide the room ID twice');
|
|
435
|
+
await ctx.closeRoom(rest[0]);
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
case 'recover': {
|
|
439
|
+
if (!rest[0])
|
|
440
|
+
throw new OwnerCommandUsageError('usage: /room recover <id>');
|
|
441
|
+
const { getRoomRecord } = await import('../rooms-tasks/room-state.js');
|
|
442
|
+
const r = getRoomRecord(rest[0]);
|
|
443
|
+
if (!r) {
|
|
444
|
+
await ctx.reply(`⚠️ room not found: ${rest[0]}`);
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
if (r.state === 'closing') {
|
|
448
|
+
await ctx.closeRoom(r.room_id);
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
const lines = [`🏠 Room ${r.room_id} · ${r.state} · saga: ${r.saga.phase}`];
|
|
452
|
+
if (r.saga.error)
|
|
453
|
+
lines.push(`Error: ${r.saga.error}`);
|
|
454
|
+
if (r.provisioning_detail)
|
|
455
|
+
lines.push(`Detail: ${r.provisioning_detail}`);
|
|
456
|
+
lines.push(r.saga.error ? 'Run `ours-fleet room recover` from CLI for full recovery.' : 'No recovery needed.');
|
|
457
|
+
await ctx.reply(lines.join('\n'));
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
default:
|
|
461
|
+
// bare /room <id> → show
|
|
462
|
+
await showRoom(sub);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
catch (e) {
|
|
466
|
+
if (e instanceof OwnerCommandUsageError)
|
|
467
|
+
throw e;
|
|
468
|
+
await ctx.reply(`⚠️ ${e instanceof Error ? e.message : String(e)}`);
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
name: 'templates', aliases: ['template-list'],
|
|
474
|
+
summary: 'list available room templates',
|
|
475
|
+
execute: noArgs('/templates', async (ctx) => {
|
|
476
|
+
const { listTemplates } = await import('../rooms-tasks/templates.js');
|
|
477
|
+
const templates = listTemplates({});
|
|
478
|
+
if (!templates.length)
|
|
479
|
+
return ctx.reply('📐 No templates.');
|
|
480
|
+
const lines = templates.map(t => {
|
|
481
|
+
const tag = t.builtin ? ' (built-in)' : '';
|
|
482
|
+
return `${t.name}@${t.version}${tag} ${t.description}`;
|
|
483
|
+
});
|
|
484
|
+
await ctx.reply(`📐 Templates:\n${lines.join('\n')}`);
|
|
485
|
+
}),
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
name: 'template',
|
|
489
|
+
usage: '/template <show|list> <name[@version]>',
|
|
490
|
+
summary: 'template subcommands (show, list)',
|
|
491
|
+
execute: async (ctx, args) => {
|
|
492
|
+
if (!args)
|
|
493
|
+
throw new OwnerCommandUsageError('usage: /template show <name[@version]>');
|
|
494
|
+
const parts = args.trim().split(/\s+/);
|
|
495
|
+
const sub = parts[0];
|
|
496
|
+
const showTemplate = async (nameStr) => {
|
|
497
|
+
const { resolveTemplate } = await import('../rooms-tasks/templates.js');
|
|
498
|
+
const t = resolveTemplate(nameStr, {});
|
|
499
|
+
if (!t)
|
|
500
|
+
return ctx.reply(`⚠️ template not found: ${nameStr}`);
|
|
501
|
+
const lines = [
|
|
502
|
+
`📐 Template: ${t.name}@${t.version}`,
|
|
503
|
+
`Description: ${t.description}`,
|
|
504
|
+
...(t.builtin ? ['Source: built-in'] : []),
|
|
505
|
+
...(t.contract ? [`Contract: ${t.contract}`] : []),
|
|
506
|
+
'Members:',
|
|
507
|
+
...t.members.map(m => ` ${m.slot} (${m.role}) ×${m.count} → role_ref: ${m.role_ref}`),
|
|
508
|
+
];
|
|
509
|
+
await ctx.reply(lines.join('\n'));
|
|
510
|
+
};
|
|
511
|
+
switch (sub) {
|
|
512
|
+
case 'show': {
|
|
513
|
+
if (!parts[1])
|
|
514
|
+
throw new OwnerCommandUsageError('usage: /template show <name[@version]>');
|
|
515
|
+
await showTemplate(parts[1]);
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
case 'list': {
|
|
519
|
+
const { listTemplates } = await import('../rooms-tasks/templates.js');
|
|
520
|
+
const templates = listTemplates({});
|
|
521
|
+
if (!templates.length)
|
|
522
|
+
return ctx.reply('📐 No templates.');
|
|
523
|
+
const lines = templates.map(t => {
|
|
524
|
+
const tag = t.builtin ? ' (built-in)' : '';
|
|
525
|
+
return `${t.name}@${t.version}${tag} ${t.description}`;
|
|
526
|
+
});
|
|
527
|
+
await ctx.reply(`📐 Templates:\n${lines.join('\n')}`);
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
default:
|
|
531
|
+
// bare /template <name> → show
|
|
532
|
+
await showTemplate(sub);
|
|
533
|
+
}
|
|
534
|
+
},
|
|
535
|
+
},
|
|
145
536
|
];
|
|
146
537
|
/** Trimmed slash-prefixed text is a command attempt and is never forwarded. */
|
|
147
538
|
export const isOwnerCommandText = (text) => text.trim().startsWith('/');
|
|
@@ -195,5 +586,7 @@ export function fleetCliOps(role, configPath) {
|
|
|
195
586
|
list: () => new Promise((resolve, reject) => {
|
|
196
587
|
execFile(process.execPath, [process.argv[1], 'ls'], { timeout: 15_000, maxBuffer: 256 * 1024 }, (error, stdout) => error ? reject(error) : resolve(String(stdout).trim()));
|
|
197
588
|
}),
|
|
589
|
+
closeRoom: roomId => launchFleetWorker(['room', '_close', roomId], `room-close-${roomId}`, configPath),
|
|
590
|
+
settleTask: taskId => launchFleetWorker(['task', '_settle', taskId], `task-settle-${taskId}`, configPath),
|
|
198
591
|
};
|
|
199
592
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
export declare function registerTemplateCommands(parent: Command, cOpt: (cmd: Command) => Command): void;
|
|
3
|
+
export declare function registerTaskCommands(parent: Command, cOpt: (cmd: Command) => Command): void;
|
|
4
|
+
export declare function registerRoomCommands(parent: Command, cOpt: (cmd: Command) => Command): void;
|