@wji.ai/cli 0.1.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.
- package/README.md +242 -0
- package/dist/commands/agent.js +274 -0
- package/dist/commands/asset.js +241 -0
- package/dist/commands/auth.js +216 -0
- package/dist/commands/canvas.js +915 -0
- package/dist/commands/config.js +113 -0
- package/dist/commands/fsnode.js +252 -0
- package/dist/commands/gen.js +366 -0
- package/dist/commands/model.js +134 -0
- package/dist/commands/org.js +85 -0
- package/dist/commands/preset.js +176 -0
- package/dist/commands/profile.js +129 -0
- package/dist/commands/skill.js +272 -0
- package/dist/commands/workspace.js +305 -0
- package/dist/core/client.js +252 -0
- package/dist/core/config.js +95 -0
- package/dist/core/output.js +143 -0
- package/dist/core/prompt.js +118 -0
- package/dist/core/zip.js +87 -0
- package/dist/index.js +94 -0
- package/package.json +49 -0
- package/skill/wji/SKILL.md +398 -0
- package/skill/wji/reference/canvas-recipes.md +179 -0
- package/skill/wji/reference/commands.md +381 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { createWriteStream } from 'node:fs';
|
|
2
|
+
import { stat } from 'node:fs/promises';
|
|
3
|
+
import { basename, join } from 'node:path';
|
|
4
|
+
import { Readable } from 'node:stream';
|
|
5
|
+
import { pipeline } from 'node:stream/promises';
|
|
6
|
+
import { Command } from 'commander';
|
|
7
|
+
import { ASSET_BATCH_MAX_IDS, WjiClient, isObjectId } from '../core/client.js';
|
|
8
|
+
import { confirm } from '../core/prompt.js';
|
|
9
|
+
import { color, detail, info, isJsonMode, printJson, relativeTime, render, success, table, truncate, warn } from '../core/output.js';
|
|
10
|
+
const STATE = { 0: 'pending', 1: 'ready', [-1]: 'cancelled' };
|
|
11
|
+
/** The server rejects the whole batch if any id is malformed — fail fast and say which. */
|
|
12
|
+
function assertAssetIds(ids) {
|
|
13
|
+
const bad = ids.filter((id) => !isObjectId(id));
|
|
14
|
+
if (bad.length)
|
|
15
|
+
throw new Error(`Not asset ids (expected 24-char ObjectIds): ${bad.join(', ')}`);
|
|
16
|
+
}
|
|
17
|
+
function humanSize(bytes) {
|
|
18
|
+
if (bytes == null)
|
|
19
|
+
return null;
|
|
20
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
21
|
+
let value = bytes;
|
|
22
|
+
let unit = 0;
|
|
23
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
24
|
+
value /= 1024;
|
|
25
|
+
unit++;
|
|
26
|
+
}
|
|
27
|
+
return `${value.toFixed(unit === 0 ? 0 : 1)}${units[unit]}`;
|
|
28
|
+
}
|
|
29
|
+
export function assetCommand(globals) {
|
|
30
|
+
const asset = new Command('asset').description('Upload and manage assets');
|
|
31
|
+
asset
|
|
32
|
+
.command('upload <file...>')
|
|
33
|
+
.description('Upload one or more local files')
|
|
34
|
+
.option('-w, --workspace <id>', 'attach to a workspace')
|
|
35
|
+
.option('--store <id>', 'vector store id')
|
|
36
|
+
.option('--vectorize', 'index the file for retrieval')
|
|
37
|
+
.option('--width <n>', 'source width hint')
|
|
38
|
+
.option('--height <n>', 'source height hint')
|
|
39
|
+
.option('--duration <n>', 'media duration hint in seconds')
|
|
40
|
+
.action(async (files, opts) => {
|
|
41
|
+
const client = new WjiClient(globals());
|
|
42
|
+
const workspace = client.workspace(opts.workspace);
|
|
43
|
+
const uploaded = [];
|
|
44
|
+
for (const file of files) {
|
|
45
|
+
const size = (await stat(file)).size;
|
|
46
|
+
info(color.dim(`Uploading ${basename(file)} (${humanSize(size)})…`));
|
|
47
|
+
const result = await client.upload('/assets/v2/upload', file, {
|
|
48
|
+
workspace,
|
|
49
|
+
store: opts.store,
|
|
50
|
+
vectorize: opts.vectorize ? 'true' : undefined,
|
|
51
|
+
width: opts.width,
|
|
52
|
+
height: opts.height,
|
|
53
|
+
duration: opts.duration,
|
|
54
|
+
});
|
|
55
|
+
uploaded.push(result);
|
|
56
|
+
if (!isJsonMode())
|
|
57
|
+
success(`${basename(file)} → ${result.id}`);
|
|
58
|
+
}
|
|
59
|
+
if (isJsonMode())
|
|
60
|
+
render(files.length === 1 ? uploaded[0] : uploaded, () => { });
|
|
61
|
+
});
|
|
62
|
+
asset
|
|
63
|
+
.command('import <url>')
|
|
64
|
+
.description('Import an asset from a URL')
|
|
65
|
+
.option('-w, --workspace <id>', 'attach to a workspace')
|
|
66
|
+
.option('-n, --name <name>', 'asset name')
|
|
67
|
+
.option('--type <type>', 'asset type override')
|
|
68
|
+
.option('--raw', 'use /assets/import-from-url instead of create-from-url')
|
|
69
|
+
.action(async (url, opts) => {
|
|
70
|
+
const client = new WjiClient(globals());
|
|
71
|
+
const created = opts.raw
|
|
72
|
+
? await client.post('/assets/import-from-url', { url })
|
|
73
|
+
: await client.post('/assets/create-from-url', {
|
|
74
|
+
url,
|
|
75
|
+
workspace: client.workspace(opts.workspace),
|
|
76
|
+
name: opts.name,
|
|
77
|
+
type: opts.type,
|
|
78
|
+
});
|
|
79
|
+
render(created, (a) => success(`Imported ${a.id}`));
|
|
80
|
+
});
|
|
81
|
+
asset
|
|
82
|
+
.command('list')
|
|
83
|
+
.description('Search your assets')
|
|
84
|
+
.option('--store <id>', 'filter by vector store')
|
|
85
|
+
.option('--page <n>', 'page number', '1')
|
|
86
|
+
.option('--limit <n>', 'page size', '20')
|
|
87
|
+
.action(async (opts) => {
|
|
88
|
+
const client = new WjiClient(globals());
|
|
89
|
+
const result = await client.get('/assets/search', {
|
|
90
|
+
store: opts.store,
|
|
91
|
+
page: Number(opts.page),
|
|
92
|
+
limit: Number(opts.limit),
|
|
93
|
+
});
|
|
94
|
+
render(result, (r) => {
|
|
95
|
+
table(r.data || [], [
|
|
96
|
+
{ header: 'id', value: (a) => a.id },
|
|
97
|
+
{ header: 'type', value: (a) => a.type },
|
|
98
|
+
{ header: 'name', value: (a) => truncate(a.name, 30) },
|
|
99
|
+
{ header: 'size', value: (a) => humanSize(a.size) },
|
|
100
|
+
{ header: 'state', value: (a) => STATE[a.status] ?? a.status },
|
|
101
|
+
{ header: 'created', value: (a) => relativeTime(a.createdAt) },
|
|
102
|
+
]);
|
|
103
|
+
info(color.dim(`${r.pagination?.total ?? 0} total`));
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
asset
|
|
107
|
+
.command('get <id>')
|
|
108
|
+
.description('Show one asset')
|
|
109
|
+
.action(async (id) => {
|
|
110
|
+
const client = new WjiClient(globals());
|
|
111
|
+
const a = await client.get(`/assets/list/${id}`);
|
|
112
|
+
render(a, () => printAsset(a));
|
|
113
|
+
});
|
|
114
|
+
asset
|
|
115
|
+
.command('status <ids...>')
|
|
116
|
+
.description(`Check processing state of several assets (sent ${ASSET_BATCH_MAX_IDS} per request)`)
|
|
117
|
+
.action(async (ids) => {
|
|
118
|
+
assertAssetIds(ids);
|
|
119
|
+
const unique = [...new Set(ids)];
|
|
120
|
+
const client = new WjiClient(globals());
|
|
121
|
+
const rows = await client.checkAssetStatus(unique);
|
|
122
|
+
render(rows, (list) => {
|
|
123
|
+
table(list || [], [
|
|
124
|
+
{ header: 'id', value: (a) => a.id },
|
|
125
|
+
{ header: 'state', value: (a) => STATE[a.status] ?? a.status },
|
|
126
|
+
{ header: 'error', value: (a) => truncate(a.error?.message || a.error, 40) },
|
|
127
|
+
{ header: 'url', value: (a) => truncate(a.url, 50) },
|
|
128
|
+
]);
|
|
129
|
+
// The server silently drops ids you may not read instead of erroring.
|
|
130
|
+
const missing = unique.length - (list?.length ?? 0);
|
|
131
|
+
if (missing > 0) {
|
|
132
|
+
warn(`${missing} id(s) not returned: not found, or neither yours nor in a workspace you belong to`);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
asset
|
|
137
|
+
.command('update <id>')
|
|
138
|
+
.description('Rename or retag an asset')
|
|
139
|
+
.option('-n, --name <name>', 'new name')
|
|
140
|
+
.option('--tags <tag...>', 'replace tags')
|
|
141
|
+
.action(async (id, opts) => {
|
|
142
|
+
const client = new WjiClient(globals());
|
|
143
|
+
const updated = await client.patch(`/assets/list/${id}`, {
|
|
144
|
+
name: opts.name,
|
|
145
|
+
tags: opts.tags,
|
|
146
|
+
});
|
|
147
|
+
render(updated, () => success(`Updated ${id}`));
|
|
148
|
+
});
|
|
149
|
+
asset
|
|
150
|
+
.command('rm <id>')
|
|
151
|
+
.description('Delete an asset')
|
|
152
|
+
.option('-y, --yes', 'skip confirmation')
|
|
153
|
+
.action(async (id, opts) => {
|
|
154
|
+
if (!(await confirm(`Delete asset ${id}?`, opts.yes)))
|
|
155
|
+
return info('Aborted');
|
|
156
|
+
const client = new WjiClient(globals());
|
|
157
|
+
const result = await client.delete(`/assets/list/${id}`);
|
|
158
|
+
render(result, () => success(`Deleted ${id}`));
|
|
159
|
+
});
|
|
160
|
+
asset
|
|
161
|
+
.command('download <ids...>')
|
|
162
|
+
.description('Download assets to disk (a directory keeps each asset name plus its file extension)')
|
|
163
|
+
.option('-o, --out <path>', 'output file (one id) or directory', '.')
|
|
164
|
+
.action(async (ids, opts) => {
|
|
165
|
+
const client = new WjiClient(globals());
|
|
166
|
+
const isDir = await stat(opts.out).then((s) => s.isDirectory()).catch(() => false);
|
|
167
|
+
if (ids.length > 1 && !isDir)
|
|
168
|
+
throw new Error('With several ids, --out must be an existing directory');
|
|
169
|
+
const saved = [];
|
|
170
|
+
for (const id of ids) {
|
|
171
|
+
const a = await client.get(`/assets/list/${id}`);
|
|
172
|
+
if (!a?.url)
|
|
173
|
+
throw new Error(`Asset ${id} has no URL yet (state: ${STATE[a?.status] ?? a?.status})`);
|
|
174
|
+
const res = await fetch(a.url);
|
|
175
|
+
if (!res.ok || !res.body)
|
|
176
|
+
throw new Error(`Download failed: ${res.status} ${res.statusText}`);
|
|
177
|
+
let target = opts.out;
|
|
178
|
+
if (isDir) {
|
|
179
|
+
// Asset names are titles ("boat"), not file names: keep the extension so
|
|
180
|
+
// image viewers and agents can open the file.
|
|
181
|
+
const base = String(a.name || id).replace(/[\\/]/g, '_');
|
|
182
|
+
target = join(opts.out, /\.[a-z0-9]{2,5}$/i.test(base) ? base : `${base}${extensionOf(a.url)}`);
|
|
183
|
+
}
|
|
184
|
+
await pipeline(Readable.fromWeb(res.body), createWriteStream(target));
|
|
185
|
+
saved.push({ id, path: target });
|
|
186
|
+
if (!isJsonMode())
|
|
187
|
+
success(`Saved ${target}`);
|
|
188
|
+
}
|
|
189
|
+
if (isJsonMode())
|
|
190
|
+
printJson(ids.length === 1 ? saved[0] : saved);
|
|
191
|
+
});
|
|
192
|
+
asset
|
|
193
|
+
.command('resolve <url>')
|
|
194
|
+
.description('Find an existing asset by its URL')
|
|
195
|
+
.option('-w, --workspace <id>', 'workspace scope')
|
|
196
|
+
.action(async (url, opts) => {
|
|
197
|
+
const client = new WjiClient(globals());
|
|
198
|
+
const a = await client.get('/assets/resolve-by-url', {
|
|
199
|
+
url,
|
|
200
|
+
workspace: client.workspace(opts.workspace),
|
|
201
|
+
});
|
|
202
|
+
render(a, (found) => (found ? printAsset(found) : info('No matching asset')));
|
|
203
|
+
});
|
|
204
|
+
asset
|
|
205
|
+
.command('summary <id...>')
|
|
206
|
+
.description(`Generate AI summaries for your own assets (at most ${ASSET_BATCH_MAX_IDS} per call)`)
|
|
207
|
+
.action(async (ids) => {
|
|
208
|
+
assertAssetIds(ids);
|
|
209
|
+
// Not batched automatically: every id is a separate vectorize + LLM job.
|
|
210
|
+
if (ids.length > ASSET_BATCH_MAX_IDS) {
|
|
211
|
+
throw new Error(`asset summary takes at most ${ASSET_BATCH_MAX_IDS} ids per call`);
|
|
212
|
+
}
|
|
213
|
+
const client = new WjiClient(globals());
|
|
214
|
+
const result = ids.length === 1
|
|
215
|
+
? await client.post(`/assets/list/${ids[0]}/summary`)
|
|
216
|
+
: await client.post('/assets/summary/batch', { ids });
|
|
217
|
+
render(result, (r) => detail(Object.entries(r || {})));
|
|
218
|
+
});
|
|
219
|
+
return asset;
|
|
220
|
+
}
|
|
221
|
+
function extensionOf(url) {
|
|
222
|
+
const match = /\.([a-z0-9]{2,5})(?:\?|$)/i.exec(url);
|
|
223
|
+
return match ? `.${match[1]}` : '';
|
|
224
|
+
}
|
|
225
|
+
function printAsset(a) {
|
|
226
|
+
detail([
|
|
227
|
+
['id', a.id],
|
|
228
|
+
['name', a.name],
|
|
229
|
+
['type', a.type],
|
|
230
|
+
['mimetype', a.mimetype],
|
|
231
|
+
['state', STATE[a.status] ?? a.status],
|
|
232
|
+
['size', humanSize(a.size)],
|
|
233
|
+
['aspectRatio', a.aspectRatio],
|
|
234
|
+
['duration', a.duration],
|
|
235
|
+
['url', a.url],
|
|
236
|
+
['thumbnail', a.thumbnail],
|
|
237
|
+
['tags', (a.tags || []).join(', ')],
|
|
238
|
+
['error', a.error?.message || a.error],
|
|
239
|
+
['createdAt', a.createdAt],
|
|
240
|
+
]);
|
|
241
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { ApiError, WjiClient } from '../core/client.js';
|
|
3
|
+
import { getProfile, updateProfile } from '../core/config.js';
|
|
4
|
+
import { askSecret, ask, confirm } from '../core/prompt.js';
|
|
5
|
+
import { color, detail, info, isJsonMode, printJson, relativeTime, render, success, table, warn } from '../core/output.js';
|
|
6
|
+
export function authCommand(globals) {
|
|
7
|
+
const auth = new Command('auth').description('Sign in and manage credentials');
|
|
8
|
+
auth
|
|
9
|
+
.command('login')
|
|
10
|
+
.description('Sign in with email + password and store the tokens')
|
|
11
|
+
.option('-e, --email <email>', 'account email')
|
|
12
|
+
.option('-p, --password <password>', 'account password (prompted when omitted)')
|
|
13
|
+
.action(async (opts) => {
|
|
14
|
+
const g = globals();
|
|
15
|
+
const client = new WjiClient(g);
|
|
16
|
+
const email = opts.email || (await ask('Email: '));
|
|
17
|
+
const password = opts.password || (await askSecret('Password: '));
|
|
18
|
+
const result = await client.request('POST', '/auth/signin', {
|
|
19
|
+
body: { email, password },
|
|
20
|
+
anonymous: true,
|
|
21
|
+
});
|
|
22
|
+
updateProfile(g.profile, {
|
|
23
|
+
endpoint: client.endpoint,
|
|
24
|
+
accessToken: result.tokens.accessToken,
|
|
25
|
+
refreshToken: result.tokens.refreshToken,
|
|
26
|
+
userId: result.user.id,
|
|
27
|
+
email: result.user.email,
|
|
28
|
+
name: result.user.name,
|
|
29
|
+
});
|
|
30
|
+
if (isJsonMode())
|
|
31
|
+
return printJson(result);
|
|
32
|
+
success(`Signed in as ${result.user.name || result.user.email} (profile: ${client.profileName})`);
|
|
33
|
+
info(color.dim(`Endpoint ${client.endpoint} · balance ${result.user.balance ?? '-'}`));
|
|
34
|
+
});
|
|
35
|
+
auth
|
|
36
|
+
.command('signup')
|
|
37
|
+
.description('Register a new account (needs an email verification code)')
|
|
38
|
+
.requiredOption('-e, --email <email>', 'account email')
|
|
39
|
+
.requiredOption('-n, --name <name>', 'display name')
|
|
40
|
+
.requiredOption('-c, --code <code>', 'email verification code')
|
|
41
|
+
.option('-p, --password <password>', 'password (prompted when omitted)')
|
|
42
|
+
.action(async (opts) => {
|
|
43
|
+
const g = globals();
|
|
44
|
+
const client = new WjiClient(g);
|
|
45
|
+
const password = opts.password || (await askSecret('Password: '));
|
|
46
|
+
const result = await client.request('POST', '/auth/signup', {
|
|
47
|
+
body: { email: opts.email, name: opts.name, code: opts.code, password },
|
|
48
|
+
anonymous: true,
|
|
49
|
+
});
|
|
50
|
+
updateProfile(g.profile, {
|
|
51
|
+
endpoint: client.endpoint,
|
|
52
|
+
accessToken: result.tokens.accessToken,
|
|
53
|
+
refreshToken: result.tokens.refreshToken,
|
|
54
|
+
userId: result.user.id,
|
|
55
|
+
email: result.user.email,
|
|
56
|
+
name: result.user.name,
|
|
57
|
+
});
|
|
58
|
+
render(result, () => success(`Registered and signed in as ${result.user.email}`));
|
|
59
|
+
});
|
|
60
|
+
auth
|
|
61
|
+
.command('token')
|
|
62
|
+
.description('Print the active bearer token (use for scripting)')
|
|
63
|
+
.action(() => {
|
|
64
|
+
const client = new WjiClient(globals());
|
|
65
|
+
process.stdout.write(client.requireToken() + '\n');
|
|
66
|
+
});
|
|
67
|
+
auth
|
|
68
|
+
.command('whoami')
|
|
69
|
+
.description('Show the signed-in account')
|
|
70
|
+
.action(async () => {
|
|
71
|
+
const client = new WjiClient(globals());
|
|
72
|
+
const me = await client.get('/user/me');
|
|
73
|
+
render(me, (u) => detail([
|
|
74
|
+
['id', u.id],
|
|
75
|
+
['name', u.name],
|
|
76
|
+
['email', u.email],
|
|
77
|
+
['balance', u.balance],
|
|
78
|
+
['vip', u.vip?.level],
|
|
79
|
+
['endpoint', client.endpoint],
|
|
80
|
+
['org', client.orgId || 'personal'],
|
|
81
|
+
['profile', client.profileName],
|
|
82
|
+
['credential', client.usingApiKey ? 'api key' : 'jwt'],
|
|
83
|
+
]));
|
|
84
|
+
});
|
|
85
|
+
auth
|
|
86
|
+
.command('refresh')
|
|
87
|
+
.description('Exchange the stored refresh token for a fresh access token')
|
|
88
|
+
.action(async () => {
|
|
89
|
+
const client = new WjiClient(globals());
|
|
90
|
+
// Goes through the same locked path as the automatic refresh, so a manual
|
|
91
|
+
// refresh can't race a parallel command into replaying the old token.
|
|
92
|
+
const profile = await client.refreshSession();
|
|
93
|
+
render({ accessToken: profile.accessToken, refreshToken: profile.refreshToken }, () => success('Access token refreshed'));
|
|
94
|
+
});
|
|
95
|
+
auth
|
|
96
|
+
.command('logout')
|
|
97
|
+
.description('Clear stored credentials for the current profile')
|
|
98
|
+
.action(async () => {
|
|
99
|
+
const g = globals();
|
|
100
|
+
const { profile } = getProfile(g.profile);
|
|
101
|
+
if (profile.refreshToken && profile.accessToken) {
|
|
102
|
+
// POST with the token in the body — `GET /auth/signout?rt=` is deprecated
|
|
103
|
+
// and put the refresh token into access logs.
|
|
104
|
+
const client = new WjiClient(g);
|
|
105
|
+
const signout = (refreshToken) => client.request('POST', '/auth/signout', { body: { refreshToken }, noRetry: true });
|
|
106
|
+
try {
|
|
107
|
+
try {
|
|
108
|
+
await signout(profile.refreshToken);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
// An expired access token: refreshing rotates the refresh token, so the
|
|
112
|
+
// one to revoke is the new one — revoking the old one would be a no-op.
|
|
113
|
+
if (!(err instanceof ApiError && err.status === 401))
|
|
114
|
+
throw err;
|
|
115
|
+
const rotated = await client.refreshSession();
|
|
116
|
+
if (rotated.refreshToken)
|
|
117
|
+
await signout(rotated.refreshToken);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Server-side signout is best effort; local credentials still go.
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
updateProfile(g.profile, {
|
|
125
|
+
accessToken: undefined,
|
|
126
|
+
refreshToken: undefined,
|
|
127
|
+
apiKey: undefined,
|
|
128
|
+
userId: undefined,
|
|
129
|
+
email: undefined,
|
|
130
|
+
name: undefined,
|
|
131
|
+
});
|
|
132
|
+
success('Signed out');
|
|
133
|
+
});
|
|
134
|
+
// ── API keys ───────────────────────────────────────────────────────────────
|
|
135
|
+
const key = auth.command('key').description('Manage long-lived sk_ API keys');
|
|
136
|
+
key
|
|
137
|
+
.command('list')
|
|
138
|
+
.description('List your API keys')
|
|
139
|
+
.option('--limit <n>', 'page size', '20')
|
|
140
|
+
.action(async (opts) => {
|
|
141
|
+
const client = new WjiClient(globals());
|
|
142
|
+
const result = await client.get('/api-key/search', { limit: Number(opts.limit) });
|
|
143
|
+
render(result, (r) => table(r.data || [], [
|
|
144
|
+
{ header: 'id', value: (k) => k.id },
|
|
145
|
+
{ header: 'name', value: (k) => k.name },
|
|
146
|
+
{ header: 'prefix', value: (k) => k.keyPrefix },
|
|
147
|
+
{ header: 'revoked', value: (k) => (k.revokedAt ? 'yes' : '') },
|
|
148
|
+
{ header: 'last used', value: (k) => relativeTime(k.lastUsedAt) },
|
|
149
|
+
{ header: 'created', value: (k) => relativeTime(k.createdAt) },
|
|
150
|
+
]));
|
|
151
|
+
});
|
|
152
|
+
key
|
|
153
|
+
.command('create <name>')
|
|
154
|
+
.description('Create an API key; the secret is shown once')
|
|
155
|
+
.option('--expires <iso>', 'expiry date, e.g. 2027-01-01')
|
|
156
|
+
.option('--save', 'store the new key in this profile and use it for future calls')
|
|
157
|
+
.action(async (name, opts) => {
|
|
158
|
+
const g = globals();
|
|
159
|
+
const client = new WjiClient(g);
|
|
160
|
+
// The endpoint answers { apiKey, secret }; the plaintext secret is only ever
|
|
161
|
+
// returned here.
|
|
162
|
+
const created = await client.post('/api-key/create', {
|
|
163
|
+
name,
|
|
164
|
+
expiresAt: opts.expires ? new Date(opts.expires).toISOString() : undefined,
|
|
165
|
+
});
|
|
166
|
+
const secret = created.secret;
|
|
167
|
+
if (opts.save && secret)
|
|
168
|
+
updateProfile(g.profile, { apiKey: secret });
|
|
169
|
+
if (isJsonMode())
|
|
170
|
+
return printJson(created);
|
|
171
|
+
success(`Created API key "${name}" (${created.apiKey?.id ?? ''})`);
|
|
172
|
+
if (secret) {
|
|
173
|
+
process.stdout.write(secret + '\n');
|
|
174
|
+
info(color.dim('Store this now — it is not retrievable later.'));
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
warn('The server did not return a secret.');
|
|
178
|
+
}
|
|
179
|
+
if (opts.save)
|
|
180
|
+
info(color.dim(`Saved to profile ${client.profileName}`));
|
|
181
|
+
});
|
|
182
|
+
key
|
|
183
|
+
.command('revoke <id>')
|
|
184
|
+
.description('Revoke an API key')
|
|
185
|
+
.option('-y, --yes', 'skip confirmation')
|
|
186
|
+
.action(async (id, opts) => {
|
|
187
|
+
const prompt = `Revoke API key ${id}? Anything still using it stops working immediately.`;
|
|
188
|
+
if (!(await confirm(prompt, opts.yes)))
|
|
189
|
+
return info('Aborted');
|
|
190
|
+
const client = new WjiClient(globals());
|
|
191
|
+
const result = await client.post(`/api-key/list/${id}/revoke`);
|
|
192
|
+
render(result, () => success(`Revoked ${id}`));
|
|
193
|
+
});
|
|
194
|
+
key
|
|
195
|
+
.command('rm <id>')
|
|
196
|
+
.description('Delete an API key')
|
|
197
|
+
.option('-y, --yes', 'skip confirmation')
|
|
198
|
+
.action(async (id, opts) => {
|
|
199
|
+
if (!(await confirm(`Delete API key ${id}?`, opts.yes)))
|
|
200
|
+
return info('Aborted');
|
|
201
|
+
const client = new WjiClient(globals());
|
|
202
|
+
const result = await client.delete(`/api-key/list/${id}`);
|
|
203
|
+
render(result, () => success(`Deleted ${id}`));
|
|
204
|
+
});
|
|
205
|
+
key
|
|
206
|
+
.command('use <secret>')
|
|
207
|
+
.description('Store an existing sk_ key in this profile')
|
|
208
|
+
.action((secret) => {
|
|
209
|
+
const g = globals();
|
|
210
|
+
if (!secret.startsWith('sk_'))
|
|
211
|
+
throw new Error('An API key must start with sk_');
|
|
212
|
+
updateProfile(g.profile, { apiKey: secret });
|
|
213
|
+
success('API key saved');
|
|
214
|
+
});
|
|
215
|
+
return auth;
|
|
216
|
+
}
|