@wji.ai/cli 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +4 -1
  2. package/dist/index.js +7162 -76
  3. package/package.json +6 -9
  4. package/skill/short-drama/SKILL.md +22 -10
  5. package/skill/short-drama/reference/bgm-sfx.md +124 -0
  6. package/skill/short-drama/reference/canvas-recipes.md +19 -2
  7. package/skill/short-drama/reference/film-mixing-guideline.md +447 -0
  8. package/skill/short-drama/reference/mix-over-video.md +89 -0
  9. package/skill/short-drama/reference/operations-cli.md +1 -1
  10. package/skill/short-drama/reference/schema.md +4 -2
  11. package/skill/short-drama/reference/stage-characters.md +2 -2
  12. package/skill/short-drama/reference/stage-storyboard.md +29 -6
  13. package/skill/short-drama/reference/workspace-layout.md +2 -2
  14. package/skill/short-drama/scripts/selftest.mjs +54 -9
  15. package/skill/short-drama/scripts/short-drama.mjs +150 -12
  16. package/dist/commands/agent.js +0 -294
  17. package/dist/commands/asset.js +0 -241
  18. package/dist/commands/auth.js +0 -216
  19. package/dist/commands/backreel-enquiry.js +0 -171
  20. package/dist/commands/backreel-episode.js +0 -243
  21. package/dist/commands/backreel-fund.js +0 -299
  22. package/dist/commands/backreel.js +0 -443
  23. package/dist/commands/canvas.js +0 -1706
  24. package/dist/commands/class.js +0 -395
  25. package/dist/commands/config.js +0 -113
  26. package/dist/commands/crew.js +0 -205
  27. package/dist/commands/fsnode.js +0 -288
  28. package/dist/commands/gen.js +0 -356
  29. package/dist/commands/model.js +0 -136
  30. package/dist/commands/npc-album.js +0 -201
  31. package/dist/commands/npc-live.js +0 -316
  32. package/dist/commands/npc.js +0 -437
  33. package/dist/commands/org.js +0 -85
  34. package/dist/commands/preset.js +0 -176
  35. package/dist/commands/profile.js +0 -129
  36. package/dist/commands/skill.js +0 -272
  37. package/dist/commands/workspace.js +0 -470
  38. package/dist/core/archive.js +0 -95
  39. package/dist/core/backreel.js +0 -23
  40. package/dist/core/client.js +0 -269
  41. package/dist/core/config.js +0 -95
  42. package/dist/core/crew.js +0 -546
  43. package/dist/core/image.js +0 -106
  44. package/dist/core/members.js +0 -54
  45. package/dist/core/npc.js +0 -108
  46. package/dist/core/output.js +0 -174
  47. package/dist/core/prompt.js +0 -118
  48. package/dist/core/vod.js +0 -149
  49. package/dist/core/zip.js +0 -87
@@ -1,470 +0,0 @@
1
- import { Command } from 'commander';
2
- import { WjiClient } from '../core/client.js';
3
- import { getProfile, updateProfile } from '../core/config.js';
4
- import { confirm, parseKeyValues } from '../core/prompt.js';
5
- import { color, detail, detailLevel, info, relativeTime, render, success, table, truncate } from '../core/output.js';
6
- import { loadMembers, memberLabel, resolveMember } from '../core/members.js';
7
- import { fetchModels } from './model.js';
8
- /** App tokens accepted by `--app`; mirrors WorkspaceApplication on the backend. */
9
- const APPS = {
10
- dataset: 'Dataset',
11
- datasetimagegeneration: 'DatasetImageGeneration',
12
- infinitecanvas: 'InfiniteCanvas',
13
- canvas: 'InfiniteCanvas',
14
- document: 'Document',
15
- draw: 'Draw',
16
- files: 'Files',
17
- playscript: 'PlayScript',
18
- batch: 'Batch',
19
- digitalhost: 'DigitalHost',
20
- };
21
- /** Apps every new workspace gets unless `-a` or `--no-apps` says otherwise — what the drama and canvas workflows need. */
22
- const DEFAULT_APPS = ['dataset', 'datasetimagegeneration', 'canvas'];
23
- const APP_TYPES = {
24
- Dataset: 'DATASET',
25
- DatasetImageGeneration: 'DATASET_IMAGE_GENERATION',
26
- InfiniteCanvas: 'INFINITE_CANVAS',
27
- Document: 'DOCUMENT',
28
- Draw: 'DRAW',
29
- Files: 'FILES',
30
- PlayScript: 'PLAYSCRIPT',
31
- Batch: 'BATCH',
32
- DigitalHost: 'DIGITAL_HOST',
33
- };
34
- function resolveApp(token) {
35
- const app = APPS[token.toLowerCase().replace(/[-_\s]/g, '')];
36
- if (!app) {
37
- throw new Error(`Unknown app "${token}". Known: ${Object.keys(APPS).sort().join(', ')}`);
38
- }
39
- return { app, type: APP_TYPES[app] };
40
- }
41
- export function workspaceCommand(globals) {
42
- const ws = new Command('ws')
43
- .aliases(['workspace'])
44
- .description('Manage workspaces');
45
- ws.command('list', { isDefault: true })
46
- .description('List workspaces you can access')
47
- .option('-k, --keyword <text>', 'match name or description')
48
- .option('-s, --scope <scope>', 'all | me | shared | favorite', 'all')
49
- .option('--page <n>', 'page number', '1')
50
- .option('--limit <n>', 'page size (max 100; -1 returns at most 500)', '20')
51
- .action(async (opts) => {
52
- const client = new WjiClient(globals());
53
- const result = await client.get('/workspace/search', {
54
- keyword: opts.keyword,
55
- scope: opts.scope,
56
- page: Number(opts.page),
57
- limit: Number(opts.limit),
58
- detail: detailLevel(),
59
- });
60
- const current = client.workspace();
61
- render(result, (r) => {
62
- table(r.data || [], [
63
- { header: '', value: (w) => (w.id === current ? color.green('*') : ' ') },
64
- { header: 'id', value: (w) => w.id },
65
- { header: 'name', value: (w) => truncate(w.name, 32) },
66
- { header: 'type', value: (w) => w.workspaceType },
67
- { header: 'ratio', value: (w) => w.aspectRatio },
68
- { header: 'fav', value: (w) => (w.favoritedByMe ? '★' : '') },
69
- { header: 'updated', value: (w) => relativeTime(w.updatedAt) },
70
- ]);
71
- info(color.dim(`${r.pagination?.total ?? 0} total · page ${r.pagination?.page}`));
72
- // The server clamps page size, so "everything" may still span pages.
73
- if (r.pagination?.hasNext) {
74
- info(color.dim(`more: --page ${Number(r.pagination.page) + 1}`));
75
- }
76
- });
77
- });
78
- ws.command('get [id]')
79
- .description('Show a workspace (defaults to the selected one)')
80
- .action(async (id) => {
81
- const client = new WjiClient(globals());
82
- const w = await client.get(`/workspace/list/${client.requireWorkspace(id)}`, {
83
- detail: detailLevel(),
84
- });
85
- render(w, () => detail([
86
- ['id', w.id],
87
- ['name', w.name],
88
- ['description', w.description],
89
- ['type', w.workspaceType],
90
- ['aspectRatio', w.aspectRatio],
91
- ['language', w.language],
92
- ['uiVersion', w.uiVersion],
93
- ['organization', w.organization?.id || w.organization || 'personal'],
94
- ['deductionMode', w.deductionMode],
95
- ['apps', (w.apps || []).map((a) => `${a.app}(${a.id})`).join(', ')],
96
- ['createdAt', w.createdAt],
97
- ]));
98
- });
99
- ws.command('use <id>')
100
- .description('Set the default workspace for this profile')
101
- .action(async (id) => {
102
- const client = new WjiClient(globals());
103
- const w = await client.get(`/workspace/list/${id}/basic`);
104
- updateProfile(globals().profile, { workspace: id });
105
- success(`Default workspace: ${w.name} (${id})`);
106
- });
107
- ws.command('create <name>')
108
- .description('Create a workspace')
109
- .option('-d, --description <text>', 'description')
110
- .option('-r, --aspect-ratio <ratio>', '16:9 | 9:16 | 21:9', '16:9')
111
- .option('-l, --language <lang>', 'dialogue language', 'auto')
112
- .option('-t, --type <type>', 'workspaceType')
113
- .option('-a, --app <app...>', `apps to create (default: ${DEFAULT_APPS.join(' ')})`)
114
- .option('--no-apps', 'create an empty workspace with no apps')
115
- .action(async (name, opts) => {
116
- const client = new WjiClient(globals());
117
- const tokens = opts.app?.length ? opts.app : opts.apps ? DEFAULT_APPS : [];
118
- const apps = tokens.map((token) => {
119
- const { app, type } = resolveApp(token);
120
- return { app, type, name: app };
121
- });
122
- const created = await client.post('/workspace/create', {
123
- name,
124
- description: opts.description,
125
- aspectRatio: opts.aspectRatio,
126
- language: opts.language,
127
- workspaceType: opts.type,
128
- apps: apps.length ? apps : undefined,
129
- });
130
- render(created, (w) => success(`Created workspace ${w.name} (${w.id})`));
131
- });
132
- ws.command('update [id]')
133
- .description('Update workspace fields')
134
- .option('-n, --name <name>', 'new name')
135
- .option('-d, --description <text>', 'new description')
136
- .option('-r, --aspect-ratio <ratio>', 'new aspect ratio')
137
- .option('-l, --language <lang>', 'new language')
138
- .option('--set <key=value...>', 'arbitrary field, dotted keys allowed')
139
- .action(async (id, opts) => {
140
- const client = new WjiClient(globals());
141
- const body = {
142
- name: opts.name,
143
- description: opts.description,
144
- aspectRatio: opts.aspectRatio,
145
- language: opts.language,
146
- ...parseKeyValues(opts.set),
147
- };
148
- const updated = await client.put(`/workspace/list/${client.requireWorkspace(id)}`, body);
149
- render(updated, () => success('Workspace updated'));
150
- });
151
- ws.command('rm <id>')
152
- .description('Delete a workspace (owner only)')
153
- .option('-y, --yes', 'skip confirmation')
154
- .action(async (id, opts) => {
155
- const client = new WjiClient(globals());
156
- const w = await client.get(`/workspace/list/${id}/basic`).catch(() => ({ name: id }));
157
- if (!(await confirm(`Delete workspace "${w.name}" (${id})?`, opts.yes))) {
158
- return info('Aborted');
159
- }
160
- const result = await client.delete(`/workspace/list/${id}`);
161
- render(result, () => success(`Deleted ${id}`));
162
- });
163
- ws.command('favorite <id>')
164
- .description('Favorite or unfavorite a workspace')
165
- .option('--off', 'remove from favorites')
166
- .action(async (id, opts) => {
167
- const client = new WjiClient(globals());
168
- const result = await client.put(`/workspace/list/${id}/favorite`, { favorite: !opts.off });
169
- render(result, () => success(opts.off ? 'Removed from favorites' : 'Favorited'));
170
- });
171
- ws.command('fs [id]')
172
- .description('Show the workspace file tree (optionally for one app)')
173
- .option('-a, --app <appId>', 'workspace app id to scope the tree')
174
- .action(async (id, opts) => {
175
- const client = new WjiClient(globals());
176
- const wsId = client.requireWorkspace(id);
177
- const path = opts.app
178
- ? `/workspace/list/${wsId}/fs/${opts.app}`
179
- : `/workspace/list/${wsId}/fs`;
180
- const root = await client.get(path, { detail: detailLevel() });
181
- render(root, (node) => printTree(node));
182
- });
183
- // ── apps ───────────────────────────────────────────────────────────────────
184
- const apps = ws.command('app').description('Manage workspace apps');
185
- apps
186
- .command('list [id]')
187
- .description('List apps in a workspace')
188
- .action(async (id) => {
189
- const client = new WjiClient(globals());
190
- const w = await client.get(`/workspace/list/${client.requireWorkspace(id)}`);
191
- render(w.apps || [], (rows) => table(rows, [
192
- { header: 'id', value: (a) => a.id },
193
- { header: 'app', value: (a) => a.app },
194
- { header: 'type', value: (a) => a.type },
195
- { header: 'name', value: (a) => a.name },
196
- ]));
197
- });
198
- apps
199
- .command('add <app> [id]')
200
- .description('Add an app (dataset, files, canvas, document, draw, …)')
201
- .option('-n, --name <name>', 'display name')
202
- .action(async (app, id, opts) => {
203
- const client = new WjiClient(globals());
204
- const resolved = resolveApp(app);
205
- const created = await client.post(`/workspace/list/${client.requireWorkspace(id)}/apps`, { app: resolved.app, type: resolved.type, name: opts.name || resolved.app });
206
- render(created, (a) => success(`Added ${a.app} (${a.id})`));
207
- });
208
- apps
209
- .command('rm <appId> [id]')
210
- .description('Remove an app from a workspace')
211
- .option('-y, --yes', 'skip confirmation')
212
- .action(async (appId, id, opts) => {
213
- if (!(await confirm(`Remove app ${appId}?`, opts.yes)))
214
- return info('Aborted');
215
- const client = new WjiClient(globals());
216
- const result = await client.delete(`/workspace/list/${client.requireWorkspace(id)}/apps/${appId}`);
217
- render(result, () => success(`Removed ${appId}`));
218
- });
219
- // ── members ────────────────────────────────────────────────────────────────
220
- const members = ws.command('member').description('Manage workspace members and invitations');
221
- const who = 'member: name, email, user id or membership id';
222
- members
223
- .command('list [id]')
224
- .description('List members')
225
- .option('-r, --role <role>', 'only this role: owner | editor | viewer')
226
- .action(async (id, opts) => {
227
- const client = new WjiClient(globals());
228
- const { members: rows } = await loadMembers(client, client.requireWorkspace(id));
229
- const shown = rows.filter((m) => !opts.role || m.role === opts.role);
230
- render(shown, () => table(shown, [
231
- { header: 'name', value: (m) => m.name },
232
- { header: 'email', value: (m) => m.email },
233
- { header: 'role', value: (m) => m.role },
234
- { header: 'user', value: (m) => color.dim(m.userId) },
235
- { header: 'memberId', value: (m) => color.dim(m.memberId) },
236
- ]));
237
- });
238
- members
239
- .command('add <userIds...>')
240
- .description('Add registered users by user id (at most 50; existing members are skipped). Invite by email with `member invite`.')
241
- .option('-r, --role <role>', 'editor | viewer', 'editor')
242
- .option('-w, --workspace <id>', 'workspace (defaults to the selected one)')
243
- .action(async (userIds, opts) => {
244
- if (userIds.length > 50)
245
- throw new Error('At most 50 users per call');
246
- const client = new WjiClient(globals());
247
- const result = await client.post(`/workspace/list/${client.requireWorkspace(opts.workspace)}/member/add-batch`, {
248
- users: userIds,
249
- role: opts.role,
250
- });
251
- render(result, (r) => {
252
- success(`Added ${r.added?.length ?? 0} as ${opts.role}`);
253
- if (r.skipped?.length)
254
- info(color.dim(`Already members: ${r.skipped.join(', ')}`));
255
- });
256
- });
257
- members
258
- .command('invite <emails...>')
259
- .description('Invite by email (at most 50, owner only): registered users join at once, the others get an invitation email. ' +
260
- 'Emailing an invitation replaces the current share link.')
261
- .option('-r, --role <role>', 'editor | viewer (students who generate need editor)', 'editor')
262
- .option('-w, --workspace <id>', 'workspace (defaults to the selected one)')
263
- .action(async (emails, opts) => {
264
- const list = emails.flatMap((e) => e.split(/[\s,;]+/)).filter(Boolean);
265
- if (list.length > 50)
266
- throw new Error(`At most 50 emails per call (got ${list.length}) — split the list`);
267
- const client = new WjiClient(globals());
268
- const r = await client.post(`/invitation/workspace/${client.requireWorkspace(opts.workspace)}/invite-emails`, {
269
- emails: list,
270
- role: opts.role,
271
- });
272
- render(r, () => {
273
- detail([
274
- ['added', r.added?.length ? r.added.join(', ') : undefined],
275
- ['invited', r.invited?.length ? r.invited.join(', ') : undefined],
276
- ['skipped', r.skipped?.length ? `${r.skipped.join(', ')} ${color.dim('(already members)')}` : undefined],
277
- ['invalid', r.invalid?.length ? color.red(r.invalid.join(', ')) : undefined],
278
- ]);
279
- if (r.invited?.length)
280
- info(color.dim('Invited people join once they sign up and open the emailed link.'));
281
- });
282
- });
283
- members
284
- .command('link [id]')
285
- .description('Create the workspace share link (owner only, valid 7 days). A new link revokes the previous one; anyone with it can join until it expires.')
286
- .option('-r, --role <role>', 'editor | viewer', 'editor')
287
- .option('--show', 'show the active link instead of creating a new one')
288
- .action(async (id, opts) => {
289
- const client = new WjiClient(globals());
290
- const wsId = client.requireWorkspace(id);
291
- if (opts.show) {
292
- const rows = await client.getFresh(`/invitation/workspace/${wsId}`);
293
- return render(rows, (list) => list.length
294
- ? table(list, [
295
- { header: 'url', value: (it) => it.url },
296
- { header: 'role', value: (it) => it.role },
297
- { header: 'expires', value: (it) => relativeTime(it.expiredAt) },
298
- ])
299
- : info('No active link. Create one with `wji ws member link`.'));
300
- }
301
- if (opts.role === 'owner')
302
- throw new Error('A share link cannot make owners');
303
- // No expiredAt: the DTO converts it to a Date before checking it is a string, so any value is
304
- // rejected (400 "expiredAt must be a string"); the server default is 7 days.
305
- const it = await client.post('/invitation/create', { targetType: 'workspace', targetId: wsId, role: opts.role });
306
- render(it, () => {
307
- success(`Share link (${it.role}, expires ${relativeTime(it.expiredAt)}):`);
308
- process.stdout.write(`${it.url}\n`);
309
- });
310
- });
311
- members
312
- .command('role <member> <role>')
313
- .description(`Change a member role (editor | viewer); ${who}`)
314
- .option('-w, --workspace <id>', 'workspace (defaults to the selected one)')
315
- .action(async (token, role, opts) => {
316
- if (!['editor', 'viewer'].includes(role))
317
- throw new Error('Role must be editor or viewer (ownership transfers from the web)');
318
- const client = new WjiClient(globals());
319
- const wsId = client.requireWorkspace(opts.workspace);
320
- const m = resolveMember((await loadMembers(client, wsId)).members, token);
321
- if (m.role === 'owner')
322
- throw new Error(`${memberLabel(m)} is the owner; their role cannot change`);
323
- const result = await client.put(`/workspace/list/${wsId}/member/${m.memberId}/role`, { role });
324
- render(result, () => success(`${memberLabel(m)} is now ${role}`));
325
- });
326
- members
327
- .command('rm <members...>')
328
- .description(`Remove members; ${who}. Their canvases stay.`)
329
- .option('-w, --workspace <id>', 'workspace (defaults to the selected one)')
330
- .option('-y, --yes', 'skip confirmation')
331
- .action(async (tokens, opts) => {
332
- const client = new WjiClient(globals());
333
- const wsId = client.requireWorkspace(opts.workspace);
334
- const all = (await loadMembers(client, wsId)).members;
335
- const targets = tokens.map((t) => resolveMember(all, t));
336
- const owner = targets.find((m) => m.role === 'owner');
337
- if (owner)
338
- throw new Error(`${memberLabel(owner)} is the owner and cannot be removed`);
339
- const names = targets.map((m) => `${memberLabel(m)} (${m.role})`).join(', ');
340
- if (!(await confirm(`Remove ${names} from the workspace?`, opts.yes)))
341
- return info('Aborted');
342
- for (const m of targets)
343
- await client.delete(`/workspace/list/${wsId}/member/${m.memberId}`);
344
- render({ removed: targets.map((m) => m.userId) }, () => success(`Removed ${names}`));
345
- });
346
- ws.command('leave <id>')
347
- .description('Leave a workspace you were invited to (owners cannot leave)')
348
- .option('-y, --yes', 'skip confirmation')
349
- .action(async (id, opts) => {
350
- const g = globals();
351
- const client = new WjiClient(g);
352
- const w = await client.get(`/workspace/list/${id}/basic`).catch(() => ({ name: id }));
353
- if (!(await confirm(`Leave workspace "${w.name}" (${id})? You need a new invite to come back.`, opts.yes))) {
354
- return info('Aborted');
355
- }
356
- const result = await client.post(`/workspace/list/${id}/leave`);
357
- // A left workspace 403s from now on, so stop pointing the profile at it.
358
- if (getProfile(g.profile).profile.workspace === id)
359
- updateProfile(g.profile, { workspace: undefined });
360
- render(result, () => success(`Left ${w.name} (${id})`));
361
- });
362
- // ── default models ─────────────────────────────────────────────────────────
363
- ws.command('defaults [id]')
364
- .description('Show or set the workspace default models (pass "none" to clear one)')
365
- .option('--chat <model>', 'default chat model for new conversations')
366
- .option('--image <model>', 'default image model')
367
- .option('--video <model>', 'default video model')
368
- .option('--storyboard <model>', 'default storyboard model')
369
- .option('--audio <model>', 'default audio model')
370
- .option('--image-resolution <value>', 'default image resolution, e.g. 2K')
371
- .option('--video-resolution <value>', 'default video resolution, e.g. 720p')
372
- .action(async (id, opts) => {
373
- const client = new WjiClient(globals());
374
- const wsId = client.requireWorkspace(id);
375
- // settings only comes back at full detail, and PUT replaces settings whole —
376
- // read it fresh so prompts / defaultApp / improvePromptDefault survive.
377
- const w = await client.getFresh(`/workspace/list/${wsId}`, { detail: 'full' });
378
- const current = { ...(w.settings?.defaultModels || {}) };
379
- const changes = DEFAULT_MODEL_KEYS.filter((key) => opts[key] !== undefined);
380
- if (!changes.length) {
381
- return render(current, () => detail(DEFAULT_MODEL_KEYS.map((key) => [key, current[key] ?? color.dim('—')])));
382
- }
383
- const next = { ...current };
384
- const needsModels = changes.some((key) => !key.endsWith('Resolution') && !isNone(opts[key]));
385
- const models = needsModels ? await fetchModels(client, { workspace: wsId }) : [];
386
- for (const key of changes) {
387
- const raw = String(opts[key]).trim();
388
- next[key] = isNone(raw) ? null : key.endsWith('Resolution') ? raw : resolveDefaultModel(models, key, raw);
389
- }
390
- await client.put(`/workspace/list/${wsId}`, { settings: { ...(w.settings || {}), defaultModels: next } });
391
- render(next, () => {
392
- success('Default models updated');
393
- detail(DEFAULT_MODEL_KEYS.map((key) => [key, next[key] ?? color.dim('—')]));
394
- });
395
- });
396
- // ── quota / stats ──────────────────────────────────────────────────────────
397
- ws.command('quota [id]')
398
- .description('Show credit quota usage')
399
- .option('--set <key=value...>', 'update quota, e.g. monthlyLimit=1000')
400
- .action(async (id, opts) => {
401
- const client = new WjiClient(globals());
402
- const wsId = client.requireWorkspace(id);
403
- if (opts.set?.length) {
404
- const updated = await client.put(`/workspace/list/${wsId}/quota`, parseKeyValues(opts.set));
405
- return render(updated, () => success('Quota updated'));
406
- }
407
- const usage = await client.get(`/workspace/list/${wsId}/quota-usage`);
408
- render(usage, (u) => detail(Object.entries(u)));
409
- });
410
- ws.command('stats [id]')
411
- .description('Show workspace stats (owner only)')
412
- .action(async (id) => {
413
- const client = new WjiClient(globals());
414
- const stats = await client.get(`/workspace/list/${client.requireWorkspace(id)}/stats`);
415
- render(stats, (s) => detail(Object.entries(s)));
416
- });
417
- ws.command('templates')
418
- .description('List workspace templates you can clone')
419
- .action(async () => {
420
- const client = new WjiClient(globals());
421
- const templates = await client.get('/workspace/start/templates');
422
- render(templates, (rows) => table(rows || [], [
423
- { header: 'id', value: (t) => t.id },
424
- { header: 'name', value: (t) => t.name },
425
- { header: 'type', value: (t) => t.workspaceType },
426
- { header: 'description', value: (t) => truncate(t.description, 40) },
427
- ]));
428
- });
429
- ws.command('clone <templateId>')
430
- .description('Create a workspace from a template')
431
- .action(async (templateId) => {
432
- const client = new WjiClient(globals());
433
- const created = await client.post(`/workspace/create/${templateId}`);
434
- render(created, (w) => success(`Created workspace ${w.name} (${w.id})`));
435
- });
436
- return ws;
437
- }
438
- const DEFAULT_MODEL_KEYS = [
439
- 'chat',
440
- 'image',
441
- 'video',
442
- 'storyboard',
443
- 'audio',
444
- 'imageResolution',
445
- 'videoResolution',
446
- ];
447
- const isNone = (value) => ['none', 'null', ''].includes(String(value ?? '').trim().toLowerCase());
448
- /**
449
- * The web stores the chat default as the model `name` (what conversations carry)
450
- * and the media defaults as the `nickname` its pickers emit — the value `-m` takes.
451
- * Accept either spelling and store what the web would.
452
- */
453
- function resolveDefaultModel(models, key, value) {
454
- const model = models.find((m) => m.nickname === value || m.name === value);
455
- if (!model) {
456
- throw new Error(`Unknown model "${value}" for --${key}. See \`wji models list\`.`);
457
- }
458
- return key === 'chat' ? model.name : model.nickname || model.name;
459
- }
460
- export function printTree(node, prefix = '', connector = '') {
461
- if (!node)
462
- return;
463
- const label = node.nickname ? `${node.name} ${color.dim(`(${node.nickname})`)}` : node.name;
464
- const kind = node.type === 'folder' ? color.blue('/') : '';
465
- process.stdout.write(`${prefix}${connector}${label}${kind} ${color.dim(node.id)}\n`);
466
- const children = node.children || [];
467
- // The root prints flush left; every level below it gets the box-drawing gutter.
468
- const childPrefix = connector ? prefix + (connector === '└─ ' ? ' ' : '│ ') : prefix;
469
- children.forEach((child, i) => printTree(child, childPrefix, i === children.length - 1 ? '└─ ' : '├─ '));
470
- }
@@ -1,95 +0,0 @@
1
- import { gunzipSync, inflateRawSync } from 'node:zlib';
2
- /**
3
- * Minimal archive readers — enough to open a GitHub source tarball
4
- * (`codeload.github.com/.../tar.gz/...`) and a plain zip, without a dependency.
5
- * Both return forward-slash paths → file contents; directories are dropped.
6
- */
7
- /** ustar / GNU tar, gzipped. Handles GNU long names (`L`) and skips pax headers. */
8
- export function readTarGz(buffer) {
9
- const tar = gunzipSync(buffer);
10
- const files = new Map();
11
- let offset = 0;
12
- let longName;
13
- const field = (start, len) => tar.subarray(offset + start, offset + start + len).toString('utf8').replace(/\0.*$/s, '');
14
- while (offset + 512 <= tar.length) {
15
- const header = tar.subarray(offset, offset + 512);
16
- if (header.every((b) => b === 0))
17
- break;
18
- const size = parseInt(field(124, 12).trim() || '0', 8);
19
- const type = field(156, 1);
20
- const prefix = field(345, 155);
21
- let name = field(0, 100);
22
- if (prefix)
23
- name = `${prefix}/${name}`;
24
- if (longName)
25
- (name = longName), (longName = undefined);
26
- const body = tar.subarray(offset + 512, offset + 512 + size);
27
- offset += 512 + Math.ceil(size / 512) * 512;
28
- if (type === 'L') {
29
- longName = body.toString('utf8').replace(/\0.*$/s, '');
30
- continue;
31
- }
32
- if (type === 'x' || type === 'g' || type === '5')
33
- continue; // pax headers, directories
34
- if (/(^|\/)\._[^/]*$/.test(name))
35
- continue; // AppleDouble metadata from macOS tar
36
- if (type === '0' || type === '' || type === '\0')
37
- files.set(name, Buffer.from(body));
38
- }
39
- return files;
40
- }
41
- /** Zip with stored or deflated entries, read from the central directory. */
42
- export function readZip(buffer) {
43
- const files = new Map();
44
- // End of central directory record: signature 0x06054b50, search backwards (comment ≤ 64 KiB)
45
- let eocd = -1;
46
- for (let i = buffer.length - 22; i >= Math.max(0, buffer.length - 65558); i--) {
47
- if (buffer.readUInt32LE(i) === 0x06054b50) {
48
- eocd = i;
49
- break;
50
- }
51
- }
52
- if (eocd < 0)
53
- throw new Error('Not a zip file (no central directory)');
54
- const count = buffer.readUInt16LE(eocd + 10);
55
- let p = buffer.readUInt32LE(eocd + 16);
56
- for (let n = 0; n < count; n++) {
57
- if (buffer.readUInt32LE(p) !== 0x02014b50)
58
- throw new Error('Corrupt zip central directory');
59
- const method = buffer.readUInt16LE(p + 10);
60
- const compressed = buffer.readUInt32LE(p + 20);
61
- const nameLen = buffer.readUInt16LE(p + 28);
62
- const extraLen = buffer.readUInt16LE(p + 30);
63
- const commentLen = buffer.readUInt16LE(p + 32);
64
- const localOffset = buffer.readUInt32LE(p + 42);
65
- const name = buffer.subarray(p + 46, p + 46 + nameLen).toString('utf8');
66
- p += 46 + nameLen + extraLen + commentLen;
67
- if (name.endsWith('/') || /(^|\/)\._[^/]*$/.test(name) || name.startsWith('__MACOSX/'))
68
- continue;
69
- // local header: 30 bytes + name + extra (the local extra length can differ from the central one)
70
- const lNameLen = buffer.readUInt16LE(localOffset + 26);
71
- const lExtraLen = buffer.readUInt16LE(localOffset + 28);
72
- const start = localOffset + 30 + lNameLen + lExtraLen;
73
- const data = buffer.subarray(start, start + compressed);
74
- if (method === 0)
75
- files.set(name, Buffer.from(data));
76
- else if (method === 8)
77
- files.set(name, inflateRawSync(data));
78
- else
79
- throw new Error(`Unsupported zip compression method ${method} for ${name}`);
80
- }
81
- return files;
82
- }
83
- /**
84
- * Source archives wrap everything in one top-level folder (`repo-ref/…`,
85
- * `short-drama-crew-export/…`). Strip it when every path shares it.
86
- */
87
- export function stripCommonRoot(files) {
88
- const roots = new Set([...files.keys()].map((p) => p.split('/')[0]));
89
- if (roots.size !== 1 || ![...files.keys()].every((p) => p.includes('/')))
90
- return files;
91
- const out = new Map();
92
- for (const [path, content] of files)
93
- out.set(path.slice(path.indexOf('/') + 1), content);
94
- return out;
95
- }
@@ -1,23 +0,0 @@
1
- import { WjiClient } from './client.js';
2
- export const POSTS = '/backreel/post';
3
- /** BackreelAccessTier, lowest to highest. `owner` is internal and never set. */
4
- export const TIERS = ['public', 'member', 'supporter', 'partner'];
5
- /** Every Backreel call says `X-App: backreel`, so notifications and mail link to Backreel, not WJI. */
6
- export const backreelClient = (globals) => new WjiClient({ ...globals(), app: 'backreel' });
7
- /** The owner's view of a post (episodes with vodVid / video ids, campaign internals, donation config). */
8
- export const loadPost = (client, id) => client.getFresh(`${POSTS}/list/${id}`);
9
- export function oneOf(value, allowed, flag) {
10
- if (!allowed.includes(value))
11
- throw new Error(`${flag} must be one of ${allowed.join(', ')}`);
12
- return value;
13
- }
14
- /** Ref fields come back as an id or a populated object; the API takes the id. */
15
- export const idOf = (ref) => (ref && typeof ref === 'object' ? ref.id : ref) || undefined;
16
- /** `n` on the command line is 1-based; returns the 0-based index or throws. */
17
- export function episodeIndex(n, count) {
18
- const i = Number(n);
19
- if (!Number.isInteger(i) || i < 1 || i > count) {
20
- throw new Error(`Episode ${n} does not exist — this post has ${count} episode(s)`);
21
- }
22
- return i - 1;
23
- }