@wji.ai/cli 0.7.0 → 0.7.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.
@@ -1,201 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { existsSync } from 'node:fs';
3
- import { basename } from 'node:path';
4
- import { Command } from 'commander';
5
- import { WjiClient, isObjectId } from '../core/client.js';
6
- import { RESIDENTS, npcLabel, resolveNpc } from '../core/npc.js';
7
- import { color, info, render, success, table, truncate } from '../core/output.js';
8
- import { confirm } from '../core/prompt.js';
9
- const LABEL_MAX = 120;
10
- const DESCRIPTION_MAX = 500;
11
- async function loadAlbums(client, residentId) {
12
- const rows = (await client.getFresh(`${RESIDENTS}/${residentId}/albums`)) || [];
13
- return rows.map((a, i) => ({ ...a, n: i + 1 }));
14
- }
15
- /** `<album>` is a 1-based number or an album id. */
16
- function pickAlbum(albums, token) {
17
- const album = /^\d+$/.test(token) ? albums[Number(token) - 1] : albums.find((a) => a.id === token);
18
- if (!album)
19
- throw new Error(`Album ${token} not found — there are ${albums.length}`);
20
- return album;
21
- }
22
- /**
23
- * A photo token is `<assetId|local file|https url>[:label]`. Local files are
24
- * uploaded; asset ids are resolved to their URL (albums store the URL).
25
- */
26
- async function toPhoto(client, token) {
27
- const m = token.match(/^(https?:\/\/[^\s]+?|[^:]+)(?::(.*))?$/);
28
- const source = m?.[1] ?? token;
29
- const label = (m?.[2] ?? '').trim();
30
- if (/^https?:\/\//.test(source))
31
- return { id: `photo_${randomUUID().slice(0, 12)}`, url: source, label };
32
- let asset;
33
- if (existsSync(source)) {
34
- info(color.dim(`Uploading ${basename(source)}…`));
35
- asset = await client.upload('/assets/v2/upload', source, {});
36
- }
37
- else if (isObjectId(source)) {
38
- asset = await client.get(`/assets/list/${source}`);
39
- }
40
- else {
41
- throw new Error(`"${source}" is not a file, an asset id or a URL`);
42
- }
43
- if (!asset?.url)
44
- throw new Error(`Asset ${asset?.id ?? source} has no URL yet`);
45
- return { id: `photo_${randomUUID().slice(0, 12)}`, assetId: asset.id, url: asset.url, label };
46
- }
47
- function printAlbums(albums) {
48
- table(albums, [
49
- { header: '#', value: (a) => a.n },
50
- { header: 'id', value: (a) => a.id },
51
- { header: 'name', value: (a) => truncate(a.name, 28) },
52
- { header: 'photos', value: (a) => (a.photos || []).length },
53
- { header: 'description', value: (a) => truncate(a.description, 44) },
54
- ]);
55
- }
56
- function printPhotos(album) {
57
- info(`${color.bold(album.name)}${album.description ? color.dim(` — ${truncate(album.description, 80)}`) : ''}`);
58
- table((album.photos || []).map((p, i) => ({ ...p, n: i + 1 })), [
59
- { header: '#', value: (p) => p.n + (album.cover === p.url ? color.green(' ★') : '') },
60
- { header: 'label', value: (p) => truncate(p.label, 30) },
61
- { header: 'asset', value: (p) => p.assetId || '' },
62
- { header: 'url', value: (p) => truncate(p.url, 48) },
63
- ]);
64
- }
65
- const checkText = (label, description) => {
66
- if (label !== undefined && label.length > LABEL_MAX)
67
- throw new Error(`--name is ${label.length} chars; the limit is ${LABEL_MAX}`);
68
- if (description !== undefined && description.length > DESCRIPTION_MAX) {
69
- throw new Error(`--description is ${description.length} chars; the limit is ${DESCRIPTION_MAX}`);
70
- }
71
- };
72
- export function albumCommand(globals) {
73
- const album = new Command('album')
74
- .aliases(['albums'])
75
- .description("An NPC's photo albums on its profile — each one a moment in its life");
76
- album.command('list <npc> [album]', { isDefault: true })
77
- .description('List albums, or the photos of one (number or id)')
78
- .action(async (token, which) => {
79
- const client = new WjiClient(globals());
80
- const r = await resolveNpc(client, token);
81
- const albums = await loadAlbums(client, r.id);
82
- if (which) {
83
- const a = pickAlbum(albums, which);
84
- return render(a, () => printPhotos(a));
85
- }
86
- render(albums, () => printAlbums(albums));
87
- });
88
- album.command('create <npc>')
89
- .description('Create an album; photos are asset ids, local files or URLs, each optionally :label')
90
- .requiredOption('--name <text>', `album title (≤${LABEL_MAX})`)
91
- .option('-d, --description <text>', `what this moment was (≤${DESCRIPTION_MAX})`)
92
- .option('--photo <photo...>', 'assetId[:label] | ./file.jpg[:label] | https://…[:label], in order')
93
- .action(async (token, opts) => {
94
- checkText(opts.name, opts.description);
95
- const client = new WjiClient(globals());
96
- const r = await resolveNpc(client, token);
97
- const photos = [];
98
- for (const p of opts.photo || [])
99
- photos.push(await toPhoto(client, p));
100
- const created = await client.post(`${RESIDENTS}/${r.id}/albums`, {
101
- label: opts.name,
102
- description: opts.description,
103
- photos,
104
- cover: photos[0]?.url,
105
- });
106
- render(created, (a) => {
107
- success(`Created album "${a.name}" for ${npcLabel(r)} with ${(a.photos || []).length} photo(s)`);
108
- printPhotos(a);
109
- });
110
- });
111
- album.command('add <npc> <album> <photo...>')
112
- .description('Append photos (assetId[:label] | file[:label] | url[:label])')
113
- .action(async (token, which, tokens) => {
114
- const client = new WjiClient(globals());
115
- const r = await resolveNpc(client, token);
116
- const a = pickAlbum(await loadAlbums(client, r.id), which);
117
- const added = [];
118
- for (const p of tokens)
119
- added.push(await toPhoto(client, p));
120
- const updated = await client.patch(`${RESIDENTS}/${r.id}/albums/${a.id}`, { photos: [...(a.photos || []), ...added] });
121
- render(updated, (u) => {
122
- success(`Added ${added.length} photo(s) to "${u.name}"`);
123
- printPhotos(u);
124
- });
125
- });
126
- album.command('set <npc> <album>')
127
- .description('Rename, re-describe, pick the cover (photo number) or relabel a photo')
128
- .option('--name <text>', 'title')
129
- .option('-d, --description <text>', 'description')
130
- .option('--cover <n>', 'photo number to use as the cover')
131
- .option('--label <n=text...>', 'relabel photos, e.g. --label 2="the night the mic drowned"')
132
- .action(async (token, which, opts) => {
133
- checkText(opts.name, opts.description);
134
- const client = new WjiClient(globals());
135
- const r = await resolveNpc(client, token);
136
- const a = pickAlbum(await loadAlbums(client, r.id), which);
137
- const photos = [...(a.photos || [])];
138
- const patch = {};
139
- if (opts.name !== undefined)
140
- patch.label = opts.name;
141
- if (opts.description !== undefined)
142
- patch.description = opts.description;
143
- if (opts.cover !== undefined) {
144
- const p = photos[Number(opts.cover) - 1];
145
- if (!p)
146
- throw new Error(`Photo ${opts.cover} does not exist`);
147
- patch.cover = p.url;
148
- }
149
- for (const pair of opts.label || []) {
150
- const [n, ...rest] = String(pair).split('=');
151
- const p = photos[Number(n) - 1];
152
- if (!p)
153
- throw new Error(`Photo ${n} does not exist`);
154
- photos[Number(n) - 1] = { ...p, label: rest.join('=') };
155
- patch.photos = photos;
156
- }
157
- if (!Object.keys(patch).length)
158
- throw new Error('Nothing to change');
159
- const updated = await client.patch(`${RESIDENTS}/${r.id}/albums/${a.id}`, patch);
160
- render(updated, (u) => {
161
- success(`Updated "${u.name}"`);
162
- printPhotos(u);
163
- });
164
- });
165
- album.command('rm-photo <npc> <album> <n...>')
166
- .description('Remove photos by number')
167
- .option('-y, --yes', 'skip confirmation')
168
- .action(async (token, which, ns, opts) => {
169
- const client = new WjiClient(globals());
170
- const r = await resolveNpc(client, token);
171
- const a = pickAlbum(await loadAlbums(client, r.id), which);
172
- const drop = new Set(ns.map((n) => {
173
- const i = Number(n) - 1;
174
- if (!a.photos?.[i])
175
- throw new Error(`Photo ${n} does not exist`);
176
- return i;
177
- }));
178
- if (!(await confirm(`Remove ${drop.size} photo(s) from "${a.name}"?`, opts.yes)))
179
- return info('Aborted');
180
- const photos = (a.photos || []).filter((_, i) => !drop.has(i));
181
- const coverGone = !photos.some((p) => p.url === a.cover);
182
- const updated = await client.patch(`${RESIDENTS}/${r.id}/albums/${a.id}`, { photos, ...(coverGone ? { cover: '' } : {}) });
183
- render(updated, (u) => {
184
- success(`Removed ${drop.size} photo(s)`);
185
- printPhotos(u);
186
- });
187
- });
188
- album.command('rm <npc> <album>')
189
- .description('Delete an album (the photo assets stay in your library)')
190
- .option('-y, --yes', 'skip confirmation')
191
- .action(async (token, which, opts) => {
192
- const client = new WjiClient(globals());
193
- const r = await resolveNpc(client, token);
194
- const a = pickAlbum(await loadAlbums(client, r.id), which);
195
- if (!(await confirm(`Delete album "${a.name}" (${(a.photos || []).length} photos)?`, opts.yes)))
196
- return info('Aborted');
197
- const result = await client.delete(`${RESIDENTS}/${r.id}/albums/${a.id}`);
198
- render(result, () => success(`Deleted "${a.name}"`));
199
- });
200
- return album;
201
- }
@@ -1,316 +0,0 @@
1
- import { writeFile } from 'node:fs/promises';
2
- import { Command } from 'commander';
3
- import { WjiClient } from '../core/client.js';
4
- import { RESIDENTS, npcLabel, resolveNpc } from '../core/npc.js';
5
- import { color, detail, info, isJsonMode, render, relativeTime, success, table, truncate, warn } from '../core/output.js';
6
- import { confirm, readValue } from '../core/prompt.js';
7
- const LIVE = '/npc-city/live';
8
- export const MEMORY_TYPES = ['formative', 'trauma', 'joy', 'connection', 'revelation'];
9
- /** What the live reply prompt actually reads: top memories by weight, content clipped. */
10
- const PROMPT_MEMORIES = 3;
11
- const PROMPT_MEMORY_CHARS = 140;
12
- const onOff = (value, flag) => {
13
- const v = String(value).toLowerCase();
14
- if (!['on', 'off', 'true', 'false'].includes(v))
15
- throw new Error(`${flag} takes on | off`);
16
- return v === 'on' || v === 'true';
17
- };
18
- function weightOf(value) {
19
- const n = Number(value);
20
- if (!Number.isInteger(n) || n < 1 || n > 5)
21
- throw new Error('--weight must be 1–5');
22
- return n;
23
- }
24
- function typeOf(value) {
25
- if (!MEMORY_TYPES.includes(value))
26
- throw new Error(`--type must be one of ${MEMORY_TYPES.join(', ')}`);
27
- return value;
28
- }
29
- /** Memories in table order (oldest first), with the ones the live prompt sees marked. */
30
- export async function loadMemories(client, residentId) {
31
- const rows = (await client.getFresh(`${RESIDENTS}/${residentId}/memories`)) || [];
32
- const top = [...rows]
33
- // Same order as the server's prompt query: weight desc, then newest first.
34
- .sort((a, b) => (b.emotionalWeight || 0) - (a.emotionalWeight || 0) ||
35
- new Date(b.createdAt || 0).getTime() - new Date(a.createdAt || 0).getTime())
36
- .slice(0, PROMPT_MEMORIES)
37
- .map((m) => m.id);
38
- return rows.map((m, i) => ({ ...m, n: i + 1, inPrompt: top.includes(m.id) }));
39
- }
40
- function printMemories(rows) {
41
- table(rows, [
42
- { header: '#', value: (m) => m.n },
43
- { header: 'live', value: (m) => (m.inPrompt ? color.green('●') : '') },
44
- { header: 'date', value: (m) => m.date },
45
- { header: 'type', value: (m) => m.type },
46
- { header: 'w', value: (m) => m.emotionalWeight },
47
- { header: 'title', value: (m) => truncate(m.title, 28) },
48
- { header: 'content', value: (m) => truncate(m.content, 44) },
49
- ]);
50
- if (rows.length)
51
- info(color.dim(`● = among the top ${PROMPT_MEMORIES} by weight — the only memories the live reply model sees (content clipped to ${PROMPT_MEMORY_CHARS} chars)`));
52
- }
53
- /** Replace the memory table with `memories` (delete the old rows, create the new ones in order). */
54
- export async function replaceMemories(client, residentId, memories) {
55
- const existing = (await client.getFresh(`${RESIDENTS}/${residentId}/memories`)) || [];
56
- // Legacy rows (pre-migration fallback) have no real id and cannot be deleted one by one.
57
- for (const m of existing)
58
- if (m.id)
59
- await client.delete(`/npc-city/memories/${m.id}`);
60
- const created = [];
61
- for (const m of memories) {
62
- created.push(await client.post(`${RESIDENTS}/${residentId}/memories`, {
63
- date: String(m.date ?? '').slice(0, 40),
64
- title: String(m.title ?? '').slice(0, 200),
65
- content: String(m.content ?? ''),
66
- emotionalWeight: Math.min(5, Math.max(1, Math.round(Number(m.emotionalWeight) || 3))),
67
- type: MEMORY_TYPES.includes(m.type) ? m.type : 'formative',
68
- }));
69
- }
70
- return created;
71
- }
72
- export function memoryCommand(globals) {
73
- const mem = new Command('memory')
74
- .aliases(['memories', 'mem'])
75
- .description("An NPC's memories — scenes from its past; the 3 heaviest feed every live reply");
76
- mem.command('list <npc>', { isDefault: true }).action(async (token) => {
77
- const client = new WjiClient(globals());
78
- const r = await resolveNpc(client, token);
79
- const rows = await loadMemories(client, r.id);
80
- render(rows, () => printMemories(rows));
81
- });
82
- mem.command('add <npc>')
83
- .requiredOption('--title <text>', 'short title (≤200)')
84
- .requiredOption('--content <text>', 'the scene: place, one sensory detail, what changed (@file / - supported)')
85
- .option('--date <date>', 'date or free text (≤40), e.g. 2025-03-01 or "the winter she turned 17"', '')
86
- .option('--type <type>', MEMORY_TYPES.join(' | '), 'formative')
87
- .option('--weight <1-5>', 'emotional weight; the top 3 reach the live prompt', '3')
88
- .action(async (token, opts) => {
89
- const client = new WjiClient(globals());
90
- const r = await resolveNpc(client, token);
91
- const created = await client.post(`${RESIDENTS}/${r.id}/memories`, {
92
- date: opts.date,
93
- title: opts.title,
94
- content: await readValue(opts.content),
95
- type: typeOf(opts.type),
96
- emotionalWeight: weightOf(opts.weight),
97
- });
98
- const rows = await loadMemories(client, r.id);
99
- render(created, () => {
100
- success(`Added memory "${opts.title}" to ${npcLabel(r)}`);
101
- printMemories(rows);
102
- });
103
- });
104
- mem.command('set <npc> <n>')
105
- .option('--title <text>', 'title')
106
- .option('--content <text>', 'content (@file / - supported)')
107
- .option('--date <date>', 'date')
108
- .option('--type <type>', MEMORY_TYPES.join(' | '))
109
- .option('--weight <1-5>', 'emotional weight')
110
- .action(async (token, n, opts) => {
111
- const client = new WjiClient(globals());
112
- const r = await resolveNpc(client, token);
113
- const rows = await loadMemories(client, r.id);
114
- const m = rows[Number(n) - 1];
115
- if (!m)
116
- throw new Error(`Memory ${n} does not exist — ${npcLabel(r)} has ${rows.length}`);
117
- if (!m.id)
118
- throw new Error('This memory is legacy data without an id — re-create it with `memory add`');
119
- const patch = {};
120
- if (opts.title !== undefined)
121
- patch.title = opts.title;
122
- if (opts.content !== undefined)
123
- patch.content = await readValue(opts.content);
124
- if (opts.date !== undefined)
125
- patch.date = opts.date;
126
- if (opts.type !== undefined)
127
- patch.type = typeOf(opts.type);
128
- if (opts.weight !== undefined)
129
- patch.emotionalWeight = weightOf(opts.weight);
130
- if (!Object.keys(patch).length)
131
- throw new Error('Nothing to change');
132
- const updated = await client.patch(`/npc-city/memories/${m.id}`, patch);
133
- const after = await loadMemories(client, r.id);
134
- render(updated, () => {
135
- success(`Updated memory ${n}`);
136
- printMemories(after);
137
- });
138
- });
139
- mem.command('rm <npc> <n...>')
140
- .option('-y, --yes', 'skip confirmation')
141
- .action(async (token, ns, opts) => {
142
- const client = new WjiClient(globals());
143
- const r = await resolveNpc(client, token);
144
- const rows = await loadMemories(client, r.id);
145
- const targets = ns.map((n) => {
146
- const m = rows[Number(n) - 1];
147
- if (!m)
148
- throw new Error(`Memory ${n} does not exist`);
149
- return m;
150
- });
151
- if (!(await confirm(`Delete ${targets.map((m) => `"${m.title}"`).join(', ')}?`, opts.yes)))
152
- return info('Aborted');
153
- for (const m of targets)
154
- await client.delete(`/npc-city/memories/${m.id}`);
155
- render({ removed: targets.map((m) => m.id) }, () => success(`Deleted ${targets.length} memory(ies)`));
156
- });
157
- return mem;
158
- }
159
- export function readinessCommand(globals) {
160
- return new Command('readiness')
161
- .argument('<npc>')
162
- .description('Can this NPC go live? Soul integrity, live gate reasons, scenes, voice, stages')
163
- .action(async (token) => {
164
- const client = new WjiClient(globals());
165
- const r = await resolveNpc(client, token);
166
- const ready = await client.getFresh(`${LIVE}/residents/${r.id}/readiness`);
167
- render(ready, () => {
168
- detail([
169
- ['npc', npcLabel(r)],
170
- ['soul integrity', `${ready.soulIntegrity ?? '?'}%`],
171
- ['can prepare', ready.canPrepare ? color.green('yes') : color.red('no')],
172
- ['can go live', ready.canGoLive ? color.green('yes') : 'no'],
173
- ['live enabled', ready.liveEnabled ? 'yes' : 'no'],
174
- ['scenes', (ready.scenes || []).map((s) => s.name).join(', ') || color.dim('none')],
175
- ['outfits', (ready.outfits || []).map((s) => s.name).join(', ') || undefined],
176
- ['props', (ready.props || []).map((s) => s.name).join(', ') || undefined],
177
- ['voice sample', ready.voiceSample ? ready.voiceSample.label || 'yes' : color.dim('none')],
178
- ['stages', (ready.stages || []).map((s) => `${s.label || s.id} (${s.status}${s.enabled ? ', on air' : ''})`).join(', ') || color.dim('none')],
179
- ]);
180
- for (const reason of ready.reasons || [])
181
- info(color.red(` ✗ ${reason}`));
182
- });
183
- });
184
- }
185
- function printStages(rows) {
186
- table(rows, [
187
- { header: 'id', value: (s) => s.id },
188
- { header: 'label', value: (s) => truncate(s.label, 24) },
189
- { header: 'status', value: (s) => s.status },
190
- { header: 'on', value: (s) => (s.enabled ? color.green('●') : '') },
191
- { header: 'debug', value: (s) => (s.debugMode ? color.yellow('debug') : '') },
192
- { header: 'scene', value: (s) => truncate(s.sceneName, 20) },
193
- { header: 'ratio', value: (s) => s.aspectRatio },
194
- { header: 'credits', value: (s) => s.creditsSpent ?? 0 },
195
- { header: 'updated', value: (s) => relativeTime(s.updatedAt) },
196
- ]);
197
- }
198
- function printChanges(changes, warnings = []) {
199
- for (const w of warnings)
200
- warn(w);
201
- if (!changes.length)
202
- return;
203
- table(changes, [
204
- { header: 'field', value: (c) => c.path },
205
- { header: 'from', value: (c) => truncate(typeof c.from === 'string' ? c.from : JSON.stringify(c.from), 36) },
206
- { header: 'to', value: (c) => truncate(typeof c.to === 'string' ? c.to : JSON.stringify(c.to), 36) },
207
- ]);
208
- }
209
- export function stageCommand(globals) {
210
- const stage = new Command('stage')
211
- .aliases(['stages'])
212
- .description('Live stages: list, export / apply / import the stage document, debug mode (no rendering)');
213
- stage.command('list <npc>', { isDefault: true }).action(async (token) => {
214
- const client = new WjiClient(globals());
215
- const r = await resolveNpc(client, token);
216
- const rows = await client.getFresh(`${LIVE}/residents/${r.id}/stages`);
217
- render(rows, () => printStages(rows || []));
218
- });
219
- stage.command('export <stageId>')
220
- .description('Print or save the stage document')
221
- .option('--format <format>', 'md | json', 'md')
222
- .option('-o, --out <file>', 'write to a file')
223
- .action(async (id, opts) => {
224
- const client = new WjiClient(globals());
225
- const result = await client.getFresh(`${LIVE}/stages/${id}/document`, { format: opts.format });
226
- if (opts.out) {
227
- await writeFile(opts.out, result.text);
228
- return render({ file: opts.out, format: result.format }, () => success(`Wrote ${opts.out}`));
229
- }
230
- render(result, () => process.stdout.write(result.text.endsWith('\n') ? result.text : `${result.text}\n`));
231
- });
232
- stage.command('apply <stageId> <text>')
233
- .description('Preview a stage document (@file or -), confirm, apply. Text only — nothing is rendered or charged')
234
- .option('--dry-run', 'only show the changes')
235
- .option('-y, --yes', 'skip confirmation')
236
- .action(async (id, source, opts) => {
237
- const client = new WjiClient(globals());
238
- const text = await readValue(source);
239
- const preview = await client.post(`${LIVE}/stages/${id}/document/preview`, { text });
240
- const changes = preview.changes || [];
241
- if (opts.dryRun || !changes.length) {
242
- return render(preview, () => {
243
- printChanges(changes, preview.warnings);
244
- if (!changes.length)
245
- info('Nothing to apply');
246
- });
247
- }
248
- if (!isJsonMode())
249
- printChanges(changes, preview.warnings);
250
- if (!(await confirm(`Apply ${changes.length} change(s) to stage ${id}?`, opts.yes)))
251
- return info('Aborted');
252
- const applied = await client.post(`${LIVE}/stages/${id}/document`, { text });
253
- render(applied, (a) => {
254
- for (const w of a.warnings || [])
255
- warn(w);
256
- success(`Applied ${(a.applied || changes).length} change(s)`);
257
- info(color.dim('Changed keyframe / clip text does not re-render anything — regenerate those slots on the web (costs credits)'));
258
- });
259
- });
260
- stage.command('import <npc> <text>')
261
- .description('Create a Draft stage from a document (@file or -). Free: nothing is rendered until you generate it on the web')
262
- .option('-y, --yes', 'skip confirmation')
263
- .action(async (token, source, opts) => {
264
- const client = new WjiClient(globals());
265
- const r = await resolveNpc(client, token);
266
- const text = await readValue(source);
267
- if (!(await confirm(`Create a new Draft stage for ${npcLabel(r)} from this document?`, opts.yes)))
268
- return info('Aborted');
269
- const created = await client.post(`${LIVE}/residents/${r.id}/stages/import`, { text });
270
- const s = created.stage ?? created;
271
- render(created, () => {
272
- for (const w of created.warnings || [])
273
- warn(w);
274
- success(`Created Draft stage "${s.label || ''}" (${s.id}) for ${npcLabel(r)}`);
275
- info(color.dim('Next: render keyframe A and the clips on the web (credits), check them, then enable the stage'));
276
- });
277
- });
278
- stage.command('set <stageId>')
279
- .description('Stage switches: --debug runs the reply model without rendering video (free rehearsal); --on puts it on air')
280
- .option('--debug <on|off>', 'debug mode — replies come back as text + a trace, no video')
281
- .option('--on <on|off>', 'the stage viewers get (one per NPC; only a Ready stage)')
282
- .option('--label <text>', 'stage label (≤80)')
283
- .action(async (id, opts) => {
284
- const client = new WjiClient(globals());
285
- const patch = {};
286
- if (opts.debug !== undefined)
287
- patch.debugMode = onOff(opts.debug, '--debug');
288
- if (opts.on !== undefined)
289
- patch.enabled = onOff(opts.on, '--on');
290
- if (opts.label !== undefined)
291
- patch.label = String(opts.label).slice(0, 80);
292
- if (!Object.keys(patch).length)
293
- throw new Error('Nothing to change — pass --debug, --on or --label');
294
- const before = await client.getFresh(`${LIVE}/stages/${id}`);
295
- if (patch.debugMode === true && before.enabled) {
296
- warn('This stage is on air: viewers will get text-only replies while debug mode is on');
297
- if (!(await confirm('Turn debug mode on anyway?')))
298
- return info('Aborted');
299
- }
300
- const updated = await client.patch(`${LIVE}/stages/${id}`, patch);
301
- render(updated, (s) => success(`Stage ${s.label || id}: ${s.enabled ? 'on air' : 'off air'}${s.debugMode ? ', debug' : ''}`));
302
- });
303
- stage.command('rm <stageId>')
304
- .description('Delete a stage (its rendered keyframes and clips go with it)')
305
- .option('-y, --yes', 'skip confirmation')
306
- .action(async (id, opts) => {
307
- const client = new WjiClient(globals());
308
- const s = await client.getFresh(`${LIVE}/stages/${id}`);
309
- const spent = s.creditsSpent ? ` — ${s.creditsSpent} credits of renders are lost` : '';
310
- if (!(await confirm(`Delete stage "${s.label || id}" (${s.status})${spent}?`, opts.yes)))
311
- return info('Aborted');
312
- const result = await client.delete(`${LIVE}/stages/${id}`);
313
- render(result, () => success(`Deleted stage ${s.label || id}`));
314
- });
315
- return stage;
316
- }