@wji.ai/cli 0.6.1 → 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.
Files changed (49) hide show
  1. package/README.md +23 -3
  2. package/dist/index.js +7162 -76
  3. package/package.json +6 -9
  4. package/skill/backreel/SKILL.md +171 -0
  5. package/skill/backreel/reference/crowdfunding.md +82 -0
  6. package/skill/backreel/reference/enquiries.md +80 -0
  7. package/skill/backreel/reference/episodes.md +104 -0
  8. package/skill/backreel/reference/images.md +76 -0
  9. package/skill/npc-character/SKILL.md +111 -0
  10. package/skill/npc-character/examples/nagi.character.md +117 -0
  11. package/skill/npc-character/examples/nagi.stage.md +34 -0
  12. package/skill/npc-character/examples/yuzu.character.md +110 -0
  13. package/skill/npc-character/examples/yuzu.stage.md +30 -0
  14. package/skill/npc-character/reference/character-craft.md +153 -0
  15. package/skill/npc-character/reference/document-format.md +151 -0
  16. package/skill/npc-character/reference/interview.md +44 -0
  17. package/skill/npc-character/reference/life.md +92 -0
  18. package/skill/npc-character/reference/runtime.md +95 -0
  19. package/skill/npc-character/reference/stage-craft.md +109 -0
  20. package/skill/npc-character/reference/visual-kit.md +81 -0
  21. package/skill/npc-character/reference/voice.md +101 -0
  22. package/skill/wji/SKILL.md +1 -1
  23. package/skill/wji/reference/commands.md +25 -5
  24. package/dist/commands/agent.js +0 -294
  25. package/dist/commands/asset.js +0 -241
  26. package/dist/commands/auth.js +0 -216
  27. package/dist/commands/backreel.js +0 -320
  28. package/dist/commands/canvas.js +0 -1706
  29. package/dist/commands/class.js +0 -395
  30. package/dist/commands/config.js +0 -113
  31. package/dist/commands/crew.js +0 -205
  32. package/dist/commands/fsnode.js +0 -288
  33. package/dist/commands/gen.js +0 -352
  34. package/dist/commands/model.js +0 -136
  35. package/dist/commands/npc.js +0 -413
  36. package/dist/commands/org.js +0 -85
  37. package/dist/commands/preset.js +0 -176
  38. package/dist/commands/profile.js +0 -129
  39. package/dist/commands/skill.js +0 -272
  40. package/dist/commands/workspace.js +0 -470
  41. package/dist/core/archive.js +0 -95
  42. package/dist/core/client.js +0 -252
  43. package/dist/core/config.js +0 -95
  44. package/dist/core/crew.js +0 -546
  45. package/dist/core/members.js +0 -54
  46. package/dist/core/npc.js +0 -108
  47. package/dist/core/output.js +0 -174
  48. package/dist/core/prompt.js +0 -118
  49. package/dist/core/zip.js +0 -87
@@ -1,395 +0,0 @@
1
- import { mkdir, stat } from 'node:fs/promises';
2
- import { createWriteStream } from 'node:fs';
3
- import { join } from 'node:path';
4
- import { Readable } from 'node:stream';
5
- import { pipeline } from 'node:stream/promises';
6
- import { Command } from 'commander';
7
- import { WjiClient } from '../core/client.js';
8
- import { confirm } from '../core/prompt.js';
9
- import { color, info, isJsonMode, relativeTime, render, success, table, truncate, warn } from '../core/output.js';
10
- import { loadMembers, mapLimit, memberLabel, resolveMember } from '../core/members.js';
11
- import { COLOR_TAGS, canvasesFolders, copyNodes, copyPlacement, copySource, createCanvasFolder, describe, linkedState, listCanvases, loadCanvas, parseVisibility, tagText, updateCanvasFolder, } from './canvas.js';
12
- const WORKERS = 4;
13
- async function loadClass(c, wsId) {
14
- const [{ workspace, members }, canvases] = await Promise.all([loadMembers(c, wsId), listCanvases(c, wsId)]);
15
- const students = members.filter((m) => m.role !== 'owner');
16
- const seats = students.map((member) => ({
17
- member,
18
- canvases: canvases.filter((cv) => cv.ownerUserId === member.userId),
19
- }));
20
- return { workspace, members, canvases, seats };
21
- }
22
- /** `--student` narrows the roster; otherwise every non-owner member. */
23
- function pickSeats(seats, members, tokens) {
24
- if (!tokens?.length)
25
- return seats;
26
- const wanted = new Set(tokens.map((t) => resolveMember(members, t).userId));
27
- const picked = seats.filter((s) => wanted.has(s.member.userId));
28
- const owners = members.filter((m) => m.role === 'owner' && wanted.has(m.userId));
29
- if (owners.length)
30
- warn(`${owners.map(memberLabel).join(', ')} is the teacher (workspace owner), not a student — skipped`);
31
- return picked;
32
- }
33
- function seatCanvas(seat) {
34
- if (seat.canvases.length > 1) {
35
- warn(`${memberLabel(seat.member)} owns ${seat.canvases.length} canvases — using the most recently updated "${[...seat.canvases].sort(byUpdated)[0].name}"`);
36
- }
37
- return [...seat.canvases].sort(byUpdated)[0];
38
- }
39
- const byUpdated = (a, b) => Date.parse(b.updatedAt || '0') - Date.parse(a.updatedAt || '0');
40
- /** Color ids a `--tag` value means on this canvas; a label the canvas lacks matches nothing (no error per student). */
41
- function tagColors(canvas, tags) {
42
- if (!tags?.length)
43
- return undefined;
44
- const colors = new Set();
45
- for (const raw of tags) {
46
- const v = raw.trim().toLowerCase();
47
- for (const t of COLOR_TAGS) {
48
- if (t.id === v || t.alias === v || canvas.tagNames[t.id]?.toLowerCase() === v)
49
- colors.add(t.id);
50
- }
51
- }
52
- return colors;
53
- }
54
- function parseSince(value) {
55
- if (!value)
56
- return undefined;
57
- const m = /^(\d+)\s*(m|h|d)$/i.exec(value.trim());
58
- if (m) {
59
- const unit = { m: 60_000, h: 3_600_000, d: 86_400_000 }[m[2].toLowerCase()];
60
- return Date.now() - Number(m[1]) * unit;
61
- }
62
- const t = Date.parse(value);
63
- if (Number.isNaN(t))
64
- throw new Error('--since takes 30m, 2h, 1d or a date');
65
- return t;
66
- }
67
- function requireStudents(seats, filtered) {
68
- if (seats.length)
69
- return;
70
- if (filtered?.length)
71
- throw new Error('No student matches --student');
72
- throw new Error('No students in this workspace yet. Invite them with `wji ws member invite <emails...>`.');
73
- }
74
- export function classCommand(globals) {
75
- const cls = new Command('class')
76
- .aliases(['classroom'])
77
- .description('Teach with a workspace: one canvas per student, copy work to everyone, check progress and results');
78
- const client = () => new WjiClient(globals());
79
- const workspaceOf = async (c, explicit) => explicit || (await linkedState(c))?.workspaceId || c.requireWorkspace();
80
- cls
81
- .command('roster')
82
- .description('Students (non-owner members) with their role and canvas')
83
- .option('-w, --workspace <id>', 'class workspace (defaults to the linked one, then the profile default)')
84
- .action(async (opts) => {
85
- const c = client();
86
- const wsId = await workspaceOf(c, opts.workspace);
87
- const { workspace, members, seats } = await loadClass(c, wsId);
88
- const teacher = members.filter((m) => m.role === 'owner').map(memberLabel).join(', ');
89
- const rows = seats.map((s) => ({
90
- userId: s.member.userId,
91
- name: s.member.name,
92
- email: s.member.email,
93
- role: s.member.role,
94
- canvases: s.canvases.map(({ folder, ...cv }) => cv),
95
- }));
96
- render({ workspace: { id: wsId, name: workspace.name }, teacher, students: rows }, () => {
97
- info(color.dim(`${workspace.name} (${wsId}) · teacher ${teacher} · ${rows.length} student(s)`));
98
- table(rows, [
99
- { header: 'name', value: (r) => r.name },
100
- { header: 'email', value: (r) => r.email },
101
- { header: 'role', value: (r) => (r.role === 'viewer' ? color.yellow('viewer') : r.role) },
102
- { header: 'canvas', value: (r) => (r.canvases.length ? r.canvases.map((cv) => cv.name).join(', ') : color.red('none')) },
103
- { header: 'visibility', value: (r) => r.canvases.map((cv) => (cv.visibility === 'owner' ? 'private' : 'shared')).join(', ') },
104
- ]);
105
- if (rows.some((r) => r.role === 'viewer'))
106
- info(color.dim('Viewers cannot generate; make them editors with `wji ws member role <name> editor`.'));
107
- if (rows.some((r) => !r.canvases.length))
108
- info(color.dim('Give everyone a canvas with `wji class canvases --create`.'));
109
- });
110
- });
111
- cls
112
- .command('canvases')
113
- .description('Each student\'s own canvas; --create makes the missing ones (named after the student, owned by them)')
114
- .option('-w, --workspace <id>', 'class workspace (defaults to the linked one, then the profile default)')
115
- .option('--create', 'create a canvas for every student who has none')
116
- .option('--visibility <v>', 'owner (private: the student and the teacher) | members (the whole class sees it)', 'owner')
117
- .option('--update', 'also apply --visibility to the students\' existing canvases')
118
- .option('--name <template>', 'canvas name; {name} and {email} are filled in', '{name}')
119
- .option('--student <members...>', 'only these students (name, email or user id)')
120
- .option('--app <appId>', 'InfiniteCanvas app when the workspace has several')
121
- .action(async (opts) => {
122
- const c = client();
123
- const wsId = await workspaceOf(c, opts.workspace);
124
- const visibility = parseVisibility(opts.visibility);
125
- const { members, seats: all } = await loadClass(c, wsId);
126
- const seats = pickSeats(all, members, opts.student);
127
- requireStudents(seats, opts.student);
128
- const created = [];
129
- const updated = [];
130
- if (opts.create && seats.some((s) => !s.canvases.length)) {
131
- const folders = await canvasesFolders(c, wsId);
132
- const target = opts.app ? folders.find((f) => f.appId === opts.app) : folders[0];
133
- if (!target)
134
- throw new Error(`Workspace ${wsId} has no InfiniteCanvas app${opts.app ? ` ${opts.app}` : ''}. Add one with \`wji ws app add canvas\`.`);
135
- for (const seat of seats.filter((s) => !s.canvases.length)) {
136
- const m = seat.member;
137
- const name = opts.name.replace(/\{name\}/g, m.name || m.email || m.userId).replace(/\{email\}/g, m.email);
138
- const raw = await createCanvasFolder(c, target.folderId, name, { ownerUserId: m.userId, visibility });
139
- seat.canvases.push({ id: raw.id, name: raw.name, appId: target.appId, ownerUserId: m.userId, visibility, updatedAt: raw.updatedAt, folder: raw });
140
- created.push({ userId: m.userId, canvasId: raw.id, name: raw.name });
141
- }
142
- }
143
- if (opts.update) {
144
- for (const seat of seats) {
145
- for (const cv of seat.canvases.filter((x) => x.visibility !== visibility && !created.some((k) => k.canvasId === x.id))) {
146
- await updateCanvasFolder(c, cv.id, (env) => {
147
- env.metadata.visibility = visibility;
148
- });
149
- cv.visibility = visibility;
150
- updated.push(cv.id);
151
- }
152
- }
153
- }
154
- const rows = seats.map((s) => ({ student: memberLabel(s.member), userId: s.member.userId, canvases: s.canvases.map(({ folder, ...cv }) => cv) }));
155
- render({ created, updated, students: rows }, () => {
156
- if (created.length)
157
- success(`Created ${created.length} canvas(es)`);
158
- if (updated.length)
159
- success(`Set ${updated.length} canvas(es) to ${visibility === 'owner' ? 'private' : 'shared'}`);
160
- table(rows, [
161
- { header: 'student', value: (r) => r.student },
162
- { header: 'canvas', value: (r) => (r.canvases.length ? r.canvases.map((cv) => cv.name).join(', ') : color.red('none')) },
163
- { header: 'id', value: (r) => color.dim(r.canvases.map((cv) => cv.id).join(', ')) },
164
- { header: 'visibility', value: (r) => r.canvases.map((cv) => (cv.visibility === 'owner' ? 'private' : 'shared')).join(', ') },
165
- ]);
166
- if (!opts.create && rows.some((r) => !r.canvases.length))
167
- info(color.dim('Pass --create to give the others a canvas.'));
168
- });
169
- });
170
- cls
171
- .command('copy [nodeIds...]')
172
- .description('Copy nodes into every student\'s canvas (default: the nodes selected in the linked browser canvas). ' +
173
- 'Connections between them come along; finished media is shared, generate nodes keep their prompt and settings.')
174
- .option('-c, --canvas <id>', 'source canvas (defaults to the linked canvas)')
175
- .option('--selected', 'add the linked browser selection to the node ids given')
176
- .option('--student <members...>', 'only these students (name, email or user id)')
177
- .option('--at <x,y>', 'top-left of the copied group in each canvas; default is right of what is already there')
178
- .option('--same-position', 'keep the original coordinates')
179
- .option('-y, --yes', 'skip confirmation')
180
- .action(async (ids, opts) => {
181
- const c = client();
182
- const { source, nodes } = await copySource(c, ids, opts);
183
- const { members, seats: all } = await loadClass(c, source.workspaceId);
184
- const seats = pickSeats(all, members, opts.student);
185
- requireStudents(seats, opts.student);
186
- const missing = seats.filter((s) => !s.canvases.length);
187
- if (missing.length)
188
- warn(`No canvas yet, skipped: ${missing.map((s) => memberLabel(s.member)).join(', ')} (create them with \`wji class canvases --create\`)`);
189
- const targets = seats
190
- .filter((s) => s.canvases.length)
191
- .map((s) => ({ seat: s, canvas: seatCanvas(s) }))
192
- .filter((t) => t.canvas.id !== source.id);
193
- if (!targets.length)
194
- throw new Error('No student canvas to copy into');
195
- const list = nodes.map((n) => `${n.kind} "${n.name}"`).join(', ');
196
- const question = `Copy ${nodes.length} node(s) (${truncate(list, 120)}) from "${source.name}" into ${targets.length} student canvas(es)?`;
197
- if (!(await confirm(question, opts.yes)))
198
- return info('Aborted');
199
- const placement = copyPlacement(opts);
200
- const results = await mapLimit(targets, WORKERS, async ({ seat, canvas }) => {
201
- try {
202
- const target = await loadCanvas(c, canvas.id);
203
- const r = await copyNodes(c, source, nodes, target, placement);
204
- if (!isJsonMode())
205
- success(`${memberLabel(seat.member)}: ${r.nodes.length} node(s), ${r.edges} connection(s) → ${canvas.name}`);
206
- return { student: memberLabel(seat.member), userId: seat.member.userId, ok: true, ...r };
207
- }
208
- catch (err) {
209
- if (!isJsonMode())
210
- warn(`${memberLabel(seat.member)}: ${err.message}`);
211
- return { student: memberLabel(seat.member), userId: seat.member.userId, ok: false, canvasId: canvas.id, error: err.message };
212
- }
213
- });
214
- const failed = results.filter((r) => !r.ok);
215
- render(results, () => {
216
- const skipped = results.find((r) => r.ok)?.skippedEdges;
217
- if (skipped)
218
- info(color.dim(`${skipped} connection(s) from nodes outside the copy were left out`));
219
- if (failed.length)
220
- warn(`${failed.length} failed — rerun with --student ${failed.map((r) => JSON.stringify(r.student)).join(' ')}`);
221
- else
222
- success(`Copied into ${results.length} canvas(es)`);
223
- });
224
- if (failed.length)
225
- process.exitCode = 1;
226
- });
227
- cls
228
- .command('status')
229
- .description('Progress per student: generate nodes done / running / failed / not started, generations and credits')
230
- .option('-w, --workspace <id>', 'class workspace (defaults to the linked one, then the profile default)')
231
- .option('--tag <tags...>', 'only count nodes with these color tags (label like HW1, or a color)')
232
- .option('-k, --kind <kind>', 'only count this node kind (image | video | audio)')
233
- .option('--student <members...>', 'only these students')
234
- .option('-s, --sort <by>', 'name | recent (last generation first) | progress (fewest done first)', 'name')
235
- .action(async (opts) => {
236
- const c = client();
237
- const wsId = await workspaceOf(c, opts.workspace);
238
- const { members, seats: all } = await loadClass(c, wsId);
239
- const seats = pickSeats(all, members, opts.student);
240
- requireStudents(seats, opts.student);
241
- const usage = await c.get(`/workspace/list/${wsId}/quota-usage`).catch(() => null);
242
- const credits = new Map((usage?.byUserTotal || []).map((u) => [String(u.userId), Number(u.amount) || 0]));
243
- const rows = await mapLimit(seats, WORKERS, async (seat) => {
244
- const base = { student: memberLabel(seat.member), userId: seat.member.userId, email: seat.member.email, credits: credits.get(seat.member.userId) ?? 0 };
245
- if (!seat.canvases.length)
246
- return { ...base, canvas: null, nodes: 0, done: 0, running: 0, failed: 0, todo: 0, generations: 0, lastGeneratedAt: null };
247
- const cvRow = seatCanvas(seat);
248
- const cv = await loadCanvas(c, cvRow.id);
249
- const colors = tagColors(cv, opts.tag);
250
- const nodes = cv.nodes
251
- .filter((n) => !opts.kind || n.kind === opts.kind)
252
- .filter((n) => !colors || colors.has(n.content.colorTag?.colorId))
253
- .map((n) => describe(cv, n));
254
- const gen = nodes.filter((n) => n.mode === 'generate');
255
- const history = await c.getFresh('/ai/assets/history/search', {
256
- workspace: wsId,
257
- scope: 'canvas',
258
- scopeId: cv.id,
259
- page: 1,
260
- limit: 1,
261
- sort: { createdAt: 'desc' },
262
- });
263
- return {
264
- ...base,
265
- canvas: { id: cv.id, name: cv.name },
266
- nodes: nodes.length,
267
- done: gen.filter((n) => n.status === 'done').length,
268
- running: gen.filter((n) => n.status === 'generating' || n.status === 'pending').length,
269
- failed: gen.filter((n) => n.status === 'failed').length,
270
- todo: gen.filter((n) => !n.status).length,
271
- generations: Number(history?.pagination?.total ?? history?.data?.length ?? 0),
272
- lastGeneratedAt: history?.data?.[0]?.createdAt || null,
273
- };
274
- });
275
- const sorters = {
276
- name: (a, b) => a.student.localeCompare(b.student),
277
- recent: (a, b) => Date.parse(b.lastGeneratedAt || '0') - Date.parse(a.lastGeneratedAt || '0'),
278
- progress: (a, b) => a.done - b.done || a.student.localeCompare(b.student),
279
- };
280
- if (!sorters[opts.sort])
281
- throw new Error('--sort must be name, recent or progress');
282
- rows.sort(sorters[opts.sort]);
283
- render(rows, () => {
284
- if (opts.tag)
285
- info(color.dim(`Counting nodes tagged ${opts.tag.join(', ')}`));
286
- table(rows, [
287
- { header: 'student', value: (r) => r.student },
288
- { header: 'canvas', value: (r) => (r.canvas ? truncate(r.canvas.name, 20) : color.red('none')) },
289
- { header: 'nodes', value: (r) => String(r.nodes) },
290
- { header: 'done', value: (r) => (r.done ? color.green(String(r.done)) : '0') },
291
- { header: 'running', value: (r) => (r.running ? color.yellow(String(r.running)) : '') },
292
- { header: 'failed', value: (r) => (r.failed ? color.red(String(r.failed)) : '') },
293
- { header: 'not run', value: (r) => (r.todo ? String(r.todo) : '') },
294
- { header: 'gens', value: (r) => String(r.generations) },
295
- { header: 'last gen', value: (r) => (r.lastGeneratedAt ? relativeTime(r.lastGeneratedAt) : '') },
296
- { header: 'credits', value: (r) => (r.credits ? String(Math.round(r.credits * 10) / 10) : '') },
297
- ]);
298
- if (!usage)
299
- info(color.dim('Credits hidden: this account cannot read the workspace credit usage.'));
300
- });
301
- });
302
- cls
303
- .command('results')
304
- .description('Finished generations across student canvases, newest first; --download saves them into one folder per student')
305
- .option('-w, --workspace <id>', 'class workspace (defaults to the linked one, then the profile default)')
306
- .option('--student <members...>', 'only these students')
307
- .option('--tag <tags...>', 'only nodes with these color tags (label like HW1, or a color)')
308
- .option('-k, --kind <kind>', 'image | video | audio')
309
- .option('-n, --name <regex>', 'only nodes whose name matches (case-insensitive)')
310
- .option('--since <when>', 'finished after: 30m, 2h, 1d or a date')
311
- .option('--all', 'include display nodes (uploaded or copied media), not only generate nodes')
312
- .option('-s, --sort <by>', 'recent | name (student, then newest)', 'recent')
313
- .option('-l, --limit <n>', 'at most this many rows (0 = all)', '0')
314
- .option('-d, --download <dir>', 'save every listed file to <dir>/<student>/')
315
- .action(async (opts) => {
316
- const c = client();
317
- const wsId = await workspaceOf(c, opts.workspace);
318
- const since = parseSince(opts.since);
319
- const nameRe = opts.name ? new RegExp(opts.name, 'i') : undefined;
320
- const { members, seats: all } = await loadClass(c, wsId);
321
- const seats = pickSeats(all, members, opts.student).filter((s) => s.canvases.length);
322
- requireStudents(seats, opts.student);
323
- const perSeat = await mapLimit(seats, WORKERS, async (seat) => {
324
- const cv = await loadCanvas(c, seatCanvas(seat).id);
325
- const colors = tagColors(cv, opts.tag);
326
- return cv.nodes
327
- .filter((n) => ['image', 'video', 'audio'].includes(n.kind))
328
- .filter((n) => !opts.kind || n.kind === opts.kind)
329
- .filter((n) => !colors || colors.has(n.content.colorTag?.colorId))
330
- .filter((n) => !nameRe || nameRe.test(n.name))
331
- .map((n) => ({ n: describe(cv, n), asset: n.file ? cv.assets.get(n.file) : undefined }))
332
- .filter(({ n }) => !!n.url && n.status !== 'generating' && (opts.all || n.mode === 'generate'))
333
- .map(({ n, asset }) => ({
334
- student: memberLabel(seat.member),
335
- userId: seat.member.userId,
336
- canvasId: cv.id,
337
- canvasName: cv.name,
338
- nodeId: n.id,
339
- kind: n.kind,
340
- name: n.name,
341
- tag: n.tag,
342
- model: n.generate?.model,
343
- prompt: n.generate?.prompt,
344
- assetId: n.assetId,
345
- url: n.url,
346
- finishedAt: (asset?.updatedAt || asset?.createdAt || null),
347
- }))
348
- .filter((r) => !since || Date.parse(r.finishedAt || '0') >= since);
349
- });
350
- let rows = perSeat.flat();
351
- const newest = (a, b) => Date.parse(b.finishedAt || '0') - Date.parse(a.finishedAt || '0');
352
- rows.sort(opts.sort === 'name' ? (a, b) => a.student.localeCompare(b.student) || newest(a, b) : newest);
353
- const limit = Number(opts.limit) || 0;
354
- if (limit > 0)
355
- rows = rows.slice(0, limit);
356
- if (opts.download) {
357
- const isDir = await stat(opts.download).then((s) => s.isDirectory()).catch(() => false);
358
- if (!isDir)
359
- await mkdir(opts.download, { recursive: true });
360
- await mapLimit(rows, WORKERS, async (r) => {
361
- const dir = join(opts.download, safeName(r.student));
362
- await mkdir(dir, { recursive: true });
363
- const res = await fetch(r.url);
364
- if (!res.ok || !res.body)
365
- return warn(`${r.student} ${r.name}: download failed (${res.status})`);
366
- const file = join(dir, `${safeName(r.name || r.kind)}-${r.nodeId.slice(-6)}${extensionOf(r.url)}`);
367
- await pipeline(Readable.fromWeb(res.body), createWriteStream(file));
368
- r.path = file;
369
- });
370
- }
371
- render(rows, () => {
372
- if (!rows.length)
373
- return info('No finished results match');
374
- table(rows, [
375
- { header: 'student', value: (r) => r.student },
376
- { header: 'kind', value: (r) => r.kind },
377
- { header: 'name', value: (r) => truncate(r.name, 18) },
378
- { header: 'tag', value: (r) => truncate(tagText(r.tag), 12) },
379
- { header: 'when', value: (r) => relativeTime(r.finishedAt) },
380
- { header: 'model', value: (r) => truncate(r.model || '', 18) },
381
- { header: opts.download ? 'saved' : 'url', value: (r) => truncate(r.path || r.url, 60) },
382
- ]);
383
- if (opts.download)
384
- success(`Saved ${rows.filter((r) => r.path).length} file(s) under ${opts.download}`);
385
- });
386
- });
387
- return cls;
388
- }
389
- function safeName(value) {
390
- return value.replace(/[\\/:*?"<>|\s]+/g, '_').slice(0, 60) || 'untitled';
391
- }
392
- function extensionOf(url) {
393
- const match = /\.([a-z0-9]{2,5})(?:\?|$)/i.exec(url);
394
- return match ? `.${match[1]}` : '';
395
- }
@@ -1,113 +0,0 @@
1
- import { Command } from 'commander';
2
- import { WjiClient } from '../core/client.js';
3
- import { DEFAULT_ENDPOINT, configPath, getProfile, readConfig, updateProfile, writeConfig, } from '../core/config.js';
4
- import { confirm } from '../core/prompt.js';
5
- import { color, detail, info, render, success, table } from '../core/output.js';
6
- const REDACTED_KEYS = new Set(['accessToken', 'refreshToken', 'apiKey']);
7
- function redact(profile) {
8
- const out = {};
9
- for (const [k, v] of Object.entries(profile)) {
10
- out[k] = REDACTED_KEYS.has(k) && typeof v === 'string' ? `${v.slice(0, 8)}…` : v;
11
- }
12
- return out;
13
- }
14
- export function configCommand(globals) {
15
- const config = new Command('config').description('Manage CLI profiles and defaults');
16
- config
17
- .command('show', { isDefault: true })
18
- .description('Show the active profile (secrets redacted)')
19
- .action(() => {
20
- const g = globals();
21
- const { name, profile } = getProfile(g.profile);
22
- const view = redact(profile);
23
- // The endpoint is resolved at request time from flag > env > profile >
24
- // built-in default, so show what will actually be used, not just what is
25
- // stored — an unset profile would otherwise print nothing here.
26
- const client = new WjiClient(g);
27
- const effective = client.endpoint + (profile.endpoint ? '' : ` ${color.dim('(default)')}`);
28
- render({ profile: name, path: configPath(), endpoint: client.endpoint, ...view }, () => {
29
- detail([
30
- ['profile', name],
31
- ['file', configPath()],
32
- ['endpoint', effective],
33
- ...Object.entries(view).filter(([k]) => k !== 'endpoint'),
34
- ['org', client.orgId || 'personal'],
35
- ]);
36
- });
37
- });
38
- config
39
- .command('set <key> <value>')
40
- .description('Set a profile field (endpoint, orgId, workspace, locale, apiKey)')
41
- .action((key, value) => {
42
- const allowed = ['endpoint', 'orgId', 'workspace', 'locale', 'apiKey', 'email', 'name'];
43
- if (!allowed.includes(key)) {
44
- throw new Error(`Unknown key "${key}". Allowed: ${allowed.join(', ')}`);
45
- }
46
- updateProfile(globals().profile, { [key]: value === 'null' ? null : value });
47
- success(`${key} = ${value}`);
48
- });
49
- config
50
- .command('unset <key>')
51
- .description('Remove a profile field')
52
- .action((key) => {
53
- updateProfile(globals().profile, { [key]: undefined });
54
- success(`Cleared ${key}`);
55
- });
56
- config
57
- .command('profiles')
58
- .description('List all profiles')
59
- .action(() => {
60
- const file = readConfig();
61
- // Spread first: a profile's own `name` field is the account display name,
62
- // which must not shadow the profile key.
63
- const rows = Object.entries(file.profiles).map(([key, p]) => ({ ...p, key }));
64
- render({ current: file.current, profiles: rows.map((r) => redact(r)) }, () => table(rows, [
65
- { header: '', value: (r) => (r.key === file.current ? color.green('*') : ' ') },
66
- { header: 'profile', value: (r) => r.key },
67
- { header: 'endpoint', value: (r) => r.endpoint || `${DEFAULT_ENDPOINT} (default)` },
68
- { header: 'account', value: (r) => r.email },
69
- { header: 'org', value: (r) => r.orgId || 'personal' },
70
- ]));
71
- });
72
- config
73
- .command('use <profile>')
74
- .description('Switch the active profile (creates it when new)')
75
- .action((name) => {
76
- const file = readConfig();
77
- if (!file.profiles[name])
78
- file.profiles[name] = {};
79
- file.current = name;
80
- writeConfig(file);
81
- success(`Using profile ${name}`);
82
- });
83
- config
84
- .command('rm <profile>')
85
- .description('Delete a profile')
86
- .option('-y, --yes', 'skip confirmation')
87
- .action(async (name, opts) => {
88
- const file = readConfig();
89
- if (!file.profiles[name])
90
- throw new Error(`No such profile: ${name}`);
91
- const p = file.profiles[name];
92
- // A profile is often the only copy of an sk_ secret — say so before it goes.
93
- const holds = [p.apiKey && 'API key', p.refreshToken && 'login'].filter(Boolean).join(' and ');
94
- const prompt = `Delete profile "${name}"${holds ? ` and the ${holds} stored in it` : ''}?`;
95
- if (!(await confirm(prompt, opts.yes)))
96
- return info('Aborted');
97
- delete file.profiles[name];
98
- if (file.current === name)
99
- file.current = Object.keys(file.profiles)[0] || 'default';
100
- if (!file.profiles[file.current])
101
- file.profiles[file.current] = {};
102
- writeConfig(file);
103
- success(`Deleted profile ${name}`);
104
- info(color.dim(`Active profile is now ${file.current}`));
105
- });
106
- config
107
- .command('path')
108
- .description('Print the config file path')
109
- .action(() => {
110
- process.stdout.write(configPath() + '\n');
111
- });
112
- return config;
113
- }