@vtriv/cli 0.1.0 → 0.1.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.
- package/index.ts +371 -390
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* vtriv-cli - Command-line interface for vtriv services
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Each profile represents a project + API key pair.
|
|
6
|
+
* API keys are issued by the platform operator and configured manually.
|
|
7
|
+
* The CLI uses the API key to mint short-lived JWTs for service calls.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { existsSync, readFileSync, writeFileSync, chmodSync } from "node:fs";
|
|
10
11
|
import { homedir } from "node:os";
|
|
11
12
|
import { join } from "node:path";
|
|
12
13
|
import { Command } from "commander";
|
|
@@ -18,13 +19,13 @@ import chalk from "chalk";
|
|
|
18
19
|
|
|
19
20
|
interface Profile {
|
|
20
21
|
baseUrl: string;
|
|
21
|
-
|
|
22
|
+
project: string;
|
|
23
|
+
apiKey: string; // vtriv_sk_*
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
interface Config {
|
|
25
27
|
default: string;
|
|
26
28
|
profiles: Record<string, Profile>;
|
|
27
|
-
apiKeys?: Record<string, string>;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
interface GlobalOptions {
|
|
@@ -49,7 +50,9 @@ function loadConfig(): Config | null {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
function saveConfig(config: Config): void {
|
|
52
|
-
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
53
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
54
|
+
// Ensure permissions are correct even if file existed
|
|
55
|
+
chmodSync(CONFIG_PATH, 0o600);
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
function getProfile(profileName?: string): Profile {
|
|
@@ -61,7 +64,7 @@ function getProfile(profileName?: string): Profile {
|
|
|
61
64
|
const name = profileName || config.default;
|
|
62
65
|
const profile = config.profiles[name];
|
|
63
66
|
if (!profile) {
|
|
64
|
-
console.error(chalk.red(`Profile '${name}' not found.`));
|
|
67
|
+
console.error(chalk.red(`Profile '${name}' not found. Run 'vtriv init' to add it.`));
|
|
65
68
|
process.exit(1);
|
|
66
69
|
}
|
|
67
70
|
return profile;
|
|
@@ -107,7 +110,7 @@ function printTable(rows: Record<string, unknown>[]): void {
|
|
|
107
110
|
}
|
|
108
111
|
}
|
|
109
112
|
|
|
110
|
-
function error(msg: string, opts: GlobalOptions):
|
|
113
|
+
function error(msg: string, opts: GlobalOptions): never {
|
|
111
114
|
if (opts.json) {
|
|
112
115
|
console.error(JSON.stringify({ error: msg }));
|
|
113
116
|
} else {
|
|
@@ -117,45 +120,86 @@ function error(msg: string, opts: GlobalOptions): void {
|
|
|
117
120
|
}
|
|
118
121
|
|
|
119
122
|
// =============================================================================
|
|
120
|
-
//
|
|
123
|
+
// Token Management
|
|
121
124
|
// =============================================================================
|
|
122
125
|
|
|
123
|
-
//
|
|
126
|
+
// In-memory cache for minted tokens (per profile)
|
|
124
127
|
const tokenCache = new Map<string, { token: string; expires: number }>();
|
|
125
128
|
|
|
129
|
+
async function getToken(opts: GlobalOptions, template?: string): Promise<string> {
|
|
130
|
+
const profile = getProfile(opts.profile);
|
|
131
|
+
|
|
132
|
+
// Cache key includes template
|
|
133
|
+
const cacheKey = template ? `${profile.project}:${template}` : profile.project;
|
|
134
|
+
const cached = tokenCache.get(cacheKey);
|
|
135
|
+
if (cached && cached.expires > Date.now()) {
|
|
136
|
+
return cached.token;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Mint a new token using the API key
|
|
140
|
+
const body: Record<string, unknown> = { expires_in: 3600 };
|
|
141
|
+
if (template) {
|
|
142
|
+
body.template = template;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const mintRes = await fetch(`${profile.baseUrl}/auth/${profile.project}/token`, {
|
|
146
|
+
method: "POST",
|
|
147
|
+
headers: {
|
|
148
|
+
Authorization: `Bearer ${profile.apiKey}`,
|
|
149
|
+
"Content-Type": "application/json",
|
|
150
|
+
},
|
|
151
|
+
body: JSON.stringify(body),
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
if (!mintRes.ok) {
|
|
155
|
+
const err = await mintRes.text();
|
|
156
|
+
let msg: string;
|
|
157
|
+
try {
|
|
158
|
+
msg = JSON.parse(err).error || err;
|
|
159
|
+
} catch {
|
|
160
|
+
msg = err;
|
|
161
|
+
}
|
|
162
|
+
error(`Failed to mint token: ${msg}`, opts);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const tokenData = (await mintRes.json()) as { token: string };
|
|
166
|
+
const token = tokenData.token;
|
|
167
|
+
|
|
168
|
+
// Cache for 55 minutes (token valid for 60)
|
|
169
|
+
tokenCache.set(cacheKey, { token, expires: Date.now() + 55 * 60 * 1000 });
|
|
170
|
+
|
|
171
|
+
return token;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// =============================================================================
|
|
175
|
+
// HTTP Request Helper
|
|
176
|
+
// =============================================================================
|
|
177
|
+
|
|
126
178
|
async function request(
|
|
127
179
|
service: string,
|
|
128
180
|
path: string,
|
|
129
181
|
options: {
|
|
130
182
|
method?: string;
|
|
131
183
|
body?: unknown;
|
|
132
|
-
|
|
184
|
+
template?: string;
|
|
133
185
|
},
|
|
134
186
|
opts: GlobalOptions
|
|
135
187
|
): Promise<unknown> {
|
|
136
188
|
const profile = getProfile(opts.profile);
|
|
137
|
-
const url = `${profile.baseUrl}/${service}${path}`;
|
|
189
|
+
const url = `${profile.baseUrl}/${service}/${profile.project}${path}`;
|
|
138
190
|
const method = options.method || "GET";
|
|
139
191
|
|
|
140
192
|
const headers: Record<string, string> = {
|
|
141
193
|
"Content-Type": "application/json",
|
|
142
194
|
};
|
|
143
195
|
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
} else if (options.project) {
|
|
148
|
-
// Other services need a JWT minted via auth
|
|
149
|
-
const token = await getProjectToken(options.project, opts);
|
|
150
|
-
headers["Authorization"] = `Bearer ${token}`;
|
|
151
|
-
}
|
|
196
|
+
// Get a JWT token for the request
|
|
197
|
+
const token = await getToken(opts, options.template);
|
|
198
|
+
headers.Authorization = `Bearer ${token}`;
|
|
152
199
|
|
|
153
200
|
if (opts.debug) {
|
|
154
201
|
const bodyArg = options.body ? `-d '${JSON.stringify(options.body)}'` : "";
|
|
155
|
-
|
|
156
|
-
? `-H "Authorization: ${headers["Authorization"]}"`
|
|
157
|
-
: "";
|
|
158
|
-
console.error(chalk.dim(`curl -X ${method} "${url}" ${authArg} ${bodyArg}`));
|
|
202
|
+
console.error(chalk.dim(`curl -X ${method} "${url}" -H "Authorization: Bearer <token>" ${bodyArg}`));
|
|
159
203
|
}
|
|
160
204
|
|
|
161
205
|
const res = await fetch(url, {
|
|
@@ -173,95 +217,16 @@ async function request(
|
|
|
173
217
|
}
|
|
174
218
|
|
|
175
219
|
if (!res.ok) {
|
|
176
|
-
const errMsg =
|
|
177
|
-
|
|
178
|
-
|
|
220
|
+
const errMsg =
|
|
221
|
+
typeof data === "object" && data !== null && "error" in data
|
|
222
|
+
? (data as { error: string }).error
|
|
223
|
+
: text;
|
|
179
224
|
error(errMsg, opts);
|
|
180
225
|
}
|
|
181
226
|
|
|
182
227
|
return data;
|
|
183
228
|
}
|
|
184
229
|
|
|
185
|
-
async function getProjectToken(project: string, opts: GlobalOptions): Promise<string> {
|
|
186
|
-
const cached = tokenCache.get(project);
|
|
187
|
-
if (cached && cached.expires > Date.now()) {
|
|
188
|
-
return cached.token;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const profile = getProfile(opts.profile);
|
|
192
|
-
|
|
193
|
-
// Get or create a CLI API key
|
|
194
|
-
let cliApiKey = getCachedApiKey(project);
|
|
195
|
-
|
|
196
|
-
if (!cliApiKey) {
|
|
197
|
-
// Create a new API key for CLI use
|
|
198
|
-
const createKeyRes = await fetch(`${profile.baseUrl}/auth/${project}/api-keys`, {
|
|
199
|
-
method: "POST",
|
|
200
|
-
headers: {
|
|
201
|
-
"Authorization": `Bearer ${profile.rootKey}`,
|
|
202
|
-
"Content-Type": "application/json",
|
|
203
|
-
},
|
|
204
|
-
body: JSON.stringify({ name: "vtriv-cli" }),
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
if (!createKeyRes.ok) {
|
|
208
|
-
const err = await createKeyRes.text();
|
|
209
|
-
error(`Failed to create CLI API key: ${err}`, opts);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const keyData = (await createKeyRes.json()) as { api_key: string };
|
|
213
|
-
cliApiKey = keyData.api_key;
|
|
214
|
-
cacheApiKey(project, cliApiKey);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Now mint a token using the API key
|
|
218
|
-
const mintRes = await fetch(`${profile.baseUrl}/auth/${project}/token`, {
|
|
219
|
-
method: "POST",
|
|
220
|
-
headers: {
|
|
221
|
-
"Authorization": `Bearer ${cliApiKey}`,
|
|
222
|
-
"Content-Type": "application/json",
|
|
223
|
-
},
|
|
224
|
-
body: JSON.stringify({ expires_in: 3600 }),
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
if (!mintRes.ok) {
|
|
228
|
-
// API key might be invalid/deleted, clear cache and retry
|
|
229
|
-
clearCachedApiKey(project);
|
|
230
|
-
error(`Failed to mint token. Try again.`, opts);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const tokenData = (await mintRes.json()) as { token: string };
|
|
234
|
-
const token = tokenData.token;
|
|
235
|
-
|
|
236
|
-
// Cache for 55 minutes (token valid for 60)
|
|
237
|
-
tokenCache.set(project, { token, expires: Date.now() + 55 * 60 * 1000 });
|
|
238
|
-
|
|
239
|
-
return token;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// API key cache (stored in config)
|
|
243
|
-
function getCachedApiKey(project: string): string | null {
|
|
244
|
-
const config = loadConfig();
|
|
245
|
-
return config?.apiKeys?.[project] || null;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function cacheApiKey(project: string, apiKey: string): void {
|
|
249
|
-
const config = loadConfig();
|
|
250
|
-
if (!config) return;
|
|
251
|
-
config.apiKeys = config.apiKeys || {};
|
|
252
|
-
config.apiKeys[project] = apiKey;
|
|
253
|
-
saveConfig(config);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function clearCachedApiKey(project: string): void {
|
|
257
|
-
const config = loadConfig();
|
|
258
|
-
if (!config) return;
|
|
259
|
-
if (config.apiKeys) {
|
|
260
|
-
delete config.apiKeys[project];
|
|
261
|
-
saveConfig(config);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
230
|
// =============================================================================
|
|
266
231
|
// Program
|
|
267
232
|
// =============================================================================
|
|
@@ -282,223 +247,234 @@ program
|
|
|
282
247
|
|
|
283
248
|
program
|
|
284
249
|
.command("init")
|
|
285
|
-
.description("
|
|
286
|
-
.
|
|
250
|
+
.description("Add or update a profile")
|
|
251
|
+
.option("--name <name>", "Profile name (defaults to project name)")
|
|
252
|
+
.option("--default", "Set as default profile")
|
|
253
|
+
.action(async (cmdOpts: { name?: string; default?: boolean }) => {
|
|
287
254
|
const existing = loadConfig();
|
|
288
|
-
|
|
255
|
+
|
|
289
256
|
console.log(chalk.bold("vtriv CLI Setup\n"));
|
|
290
|
-
|
|
257
|
+
|
|
291
258
|
const readline = await import("node:readline");
|
|
292
259
|
const rl = readline.createInterface({
|
|
293
260
|
input: process.stdin,
|
|
294
261
|
output: process.stdout,
|
|
295
262
|
});
|
|
296
|
-
|
|
263
|
+
|
|
297
264
|
const question = (q: string): Promise<string> =>
|
|
298
265
|
new Promise((resolve) => rl.question(q, resolve));
|
|
299
|
-
|
|
300
|
-
const baseUrl =
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
266
|
+
|
|
267
|
+
const baseUrl =
|
|
268
|
+
(await question(
|
|
269
|
+
`Base URL [${existing?.profiles?.[existing.default]?.baseUrl || "https://api.vtriv.com"}]: `
|
|
270
|
+
)) ||
|
|
271
|
+
existing?.profiles?.[existing.default]?.baseUrl ||
|
|
272
|
+
"https://api.vtriv.com";
|
|
273
|
+
|
|
274
|
+
const project = await question("Project name: ");
|
|
275
|
+
if (!project) {
|
|
276
|
+
console.error(chalk.red("Project name is required."));
|
|
308
277
|
rl.close();
|
|
309
278
|
process.exit(1);
|
|
310
279
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
280
|
+
|
|
281
|
+
const apiKey = await question("API Key (vtriv_sk_...): ");
|
|
282
|
+
if (!apiKey) {
|
|
283
|
+
console.error(chalk.red("API key is required."));
|
|
314
284
|
rl.close();
|
|
315
285
|
process.exit(1);
|
|
316
286
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
287
|
+
|
|
288
|
+
if (!apiKey.startsWith("vtriv_sk_")) {
|
|
289
|
+
console.error(chalk.red("API key must start with 'vtriv_sk_'."));
|
|
290
|
+
rl.close();
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const profileName = cmdOpts.name || project;
|
|
295
|
+
|
|
296
|
+
const config: Config = existing || { default: profileName, profiles: {} };
|
|
297
|
+
config.profiles[profileName] = { baseUrl, project, apiKey };
|
|
298
|
+
|
|
299
|
+
if (cmdOpts.default || !existing) {
|
|
300
|
+
config.default = profileName;
|
|
301
|
+
}
|
|
302
|
+
|
|
325
303
|
saveConfig(config);
|
|
326
|
-
console.log(chalk.green(`\
|
|
304
|
+
console.log(chalk.green(`\nProfile '${profileName}' saved to ${CONFIG_PATH}`));
|
|
305
|
+
|
|
306
|
+
if (config.default === profileName) {
|
|
307
|
+
console.log(chalk.dim(`Set as default profile.`));
|
|
308
|
+
}
|
|
309
|
+
|
|
327
310
|
rl.close();
|
|
328
311
|
});
|
|
329
312
|
|
|
330
313
|
// =============================================================================
|
|
331
|
-
//
|
|
314
|
+
// Status Command
|
|
332
315
|
// =============================================================================
|
|
333
316
|
|
|
334
|
-
|
|
317
|
+
program
|
|
318
|
+
.command("status")
|
|
319
|
+
.description("Show configured profiles")
|
|
320
|
+
.action(() => {
|
|
321
|
+
const opts = program.opts<GlobalOptions>();
|
|
322
|
+
const config = loadConfig();
|
|
323
|
+
if (!config || Object.keys(config.profiles).length === 0) {
|
|
324
|
+
console.log(chalk.dim("No profiles configured. Run 'vtriv init' to add one."));
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
335
327
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
output(
|
|
328
|
+
const profiles = Object.entries(config.profiles).map(([name, p]) => ({
|
|
329
|
+
name,
|
|
330
|
+
project: p.project,
|
|
331
|
+
baseUrl: p.baseUrl,
|
|
332
|
+
default: name === config.default ? "yes" : "",
|
|
333
|
+
}));
|
|
334
|
+
|
|
335
|
+
output(profiles, opts);
|
|
344
336
|
});
|
|
345
337
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
338
|
+
// =============================================================================
|
|
339
|
+
// Token Command
|
|
340
|
+
// =============================================================================
|
|
341
|
+
|
|
342
|
+
program
|
|
343
|
+
.command("token")
|
|
344
|
+
.description("Mint and display a JWT token")
|
|
345
|
+
.option("-t, --template <name>", "Use a specific template")
|
|
346
|
+
.action(async (cmdOpts: { template?: string }) => {
|
|
351
347
|
const opts = program.opts<GlobalOptions>();
|
|
352
|
-
const
|
|
353
|
-
if (
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
console.log(chalk.yellow("\nIMPORTANT: Save the account_key above - it cannot be retrieved later!"));
|
|
348
|
+
const token = await getToken(opts, cmdOpts.template);
|
|
349
|
+
if (opts.json) {
|
|
350
|
+
console.log(JSON.stringify({ token }));
|
|
351
|
+
} else {
|
|
352
|
+
console.log(token);
|
|
358
353
|
}
|
|
359
354
|
});
|
|
360
355
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
.argument("<id>", "Account ID")
|
|
365
|
-
.action(async (id: string) => {
|
|
366
|
-
const opts = program.opts<GlobalOptions>();
|
|
367
|
-
const data = await request("auth", `/accounts/${id}`, {}, opts);
|
|
368
|
-
output(data, opts);
|
|
369
|
-
});
|
|
356
|
+
// =============================================================================
|
|
357
|
+
// Template Commands
|
|
358
|
+
// =============================================================================
|
|
370
359
|
|
|
371
|
-
|
|
372
|
-
.command("account:delete")
|
|
373
|
-
.description("Delete an account")
|
|
374
|
-
.argument("<id>", "Account ID")
|
|
375
|
-
.action(async (id: string) => {
|
|
376
|
-
const opts = program.opts<GlobalOptions>();
|
|
377
|
-
const data = await request("auth", `/accounts/${id}`, { method: "DELETE" }, opts);
|
|
378
|
-
output(data, opts);
|
|
379
|
-
});
|
|
360
|
+
const template = program.command("template").description("Token template management");
|
|
380
361
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
.
|
|
384
|
-
.description("List all projects")
|
|
362
|
+
template
|
|
363
|
+
.command("ls")
|
|
364
|
+
.description("List all templates")
|
|
385
365
|
.action(async () => {
|
|
386
366
|
const opts = program.opts<GlobalOptions>();
|
|
387
|
-
const
|
|
388
|
-
output(data.projects, opts);
|
|
389
|
-
});
|
|
367
|
+
const profile = getProfile(opts.profile);
|
|
390
368
|
|
|
391
|
-
auth
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
369
|
+
const res = await fetch(`${profile.baseUrl}/auth/${profile.project}/templates`, {
|
|
370
|
+
headers: { Authorization: `Bearer ${profile.apiKey}` },
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
if (!res.ok) {
|
|
374
|
+
const data = await res.json();
|
|
375
|
+
error((data as { error?: string }).error || "Failed to list templates", opts);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const data = (await res.json()) as { templates: Record<string, { claims: Record<string, unknown> }> };
|
|
379
|
+
|
|
380
|
+
if (opts.json) {
|
|
381
|
+
console.log(JSON.stringify(data.templates, null, 2));
|
|
382
|
+
} else {
|
|
383
|
+
const rows = Object.entries(data.templates).map(([name, tmpl]) => ({
|
|
384
|
+
name,
|
|
385
|
+
claims: JSON.stringify(tmpl.claims),
|
|
386
|
+
}));
|
|
387
|
+
printTable(rows);
|
|
388
|
+
}
|
|
399
389
|
});
|
|
400
390
|
|
|
401
|
-
|
|
402
|
-
.command("
|
|
403
|
-
.description("Get
|
|
404
|
-
.argument("<name>", "
|
|
391
|
+
template
|
|
392
|
+
.command("get")
|
|
393
|
+
.description("Get a specific template")
|
|
394
|
+
.argument("<name>", "Template name")
|
|
405
395
|
.action(async (name: string) => {
|
|
406
396
|
const opts = program.opts<GlobalOptions>();
|
|
407
|
-
const
|
|
397
|
+
const profile = getProfile(opts.profile);
|
|
398
|
+
|
|
399
|
+
const res = await fetch(`${profile.baseUrl}/auth/${profile.project}/templates/${name}`, {
|
|
400
|
+
headers: { Authorization: `Bearer ${profile.apiKey}` },
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
if (!res.ok) {
|
|
404
|
+
const data = await res.json();
|
|
405
|
+
error((data as { error?: string }).error || "Failed to get template", opts);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const data = await res.json();
|
|
408
409
|
output(data, opts);
|
|
409
410
|
});
|
|
410
411
|
|
|
411
|
-
|
|
412
|
-
.command("
|
|
413
|
-
.description("
|
|
414
|
-
.argument("<name>", "
|
|
412
|
+
template
|
|
413
|
+
.command("set")
|
|
414
|
+
.description("Create or update a template (reads JSON from stdin)")
|
|
415
|
+
.argument("<name>", "Template name")
|
|
415
416
|
.action(async (name: string) => {
|
|
416
417
|
const opts = program.opts<GlobalOptions>();
|
|
417
|
-
const
|
|
418
|
-
output(data, opts);
|
|
419
|
-
});
|
|
418
|
+
const profile = getProfile(opts.profile);
|
|
420
419
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
const
|
|
427
|
-
const data = (await request("auth", `/${project}/users`, {}, opts)) as { users: unknown[] };
|
|
428
|
-
output(data.users, opts);
|
|
429
|
-
});
|
|
420
|
+
// Read claims JSON from stdin
|
|
421
|
+
const chunks: Buffer[] = [];
|
|
422
|
+
for await (const chunk of Bun.stdin.stream()) {
|
|
423
|
+
chunks.push(Buffer.from(chunk));
|
|
424
|
+
}
|
|
425
|
+
const input = Buffer.concat(chunks).toString("utf-8").trim();
|
|
430
426
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
.argument("<project>", "Project name")
|
|
435
|
-
.argument("<email>", "User email")
|
|
436
|
-
.argument("[password]", "User password")
|
|
437
|
-
.action(async (project: string, email: string, password?: string) => {
|
|
438
|
-
const opts = program.opts<GlobalOptions>();
|
|
439
|
-
const body: Record<string, unknown> = { email };
|
|
440
|
-
if (password) body.password = password;
|
|
441
|
-
const data = await request("auth", `/${project}/users`, { method: "POST", body }, opts);
|
|
442
|
-
output(data, opts);
|
|
443
|
-
});
|
|
427
|
+
if (!input) {
|
|
428
|
+
error("No JSON provided on stdin. Provide claims object, e.g. {\"x-ai\": true}", opts);
|
|
429
|
+
}
|
|
444
430
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
const opts = program.opts<GlobalOptions>();
|
|
452
|
-
const data = await request("auth", `/${project}/users/${id}`, { method: "DELETE" }, opts);
|
|
453
|
-
output(data, opts);
|
|
454
|
-
});
|
|
431
|
+
let claims: Record<string, unknown>;
|
|
432
|
+
try {
|
|
433
|
+
claims = JSON.parse(input);
|
|
434
|
+
} catch {
|
|
435
|
+
error("Invalid JSON on stdin", opts);
|
|
436
|
+
}
|
|
455
437
|
|
|
456
|
-
auth
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
438
|
+
const res = await fetch(`${profile.baseUrl}/auth/${profile.project}/templates/${name}`, {
|
|
439
|
+
method: "PUT",
|
|
440
|
+
headers: {
|
|
441
|
+
Authorization: `Bearer ${profile.apiKey}`,
|
|
442
|
+
"Content-Type": "application/json",
|
|
443
|
+
},
|
|
444
|
+
body: JSON.stringify({ claims }),
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
if (!res.ok) {
|
|
448
|
+
const data = await res.json();
|
|
449
|
+
error((data as { error?: string }).error || "Failed to set template", opts);
|
|
467
450
|
}
|
|
468
|
-
});
|
|
469
451
|
|
|
470
|
-
|
|
471
|
-
.command("apikey:list")
|
|
472
|
-
.description("List API keys for a project")
|
|
473
|
-
.argument("<project>", "Project name")
|
|
474
|
-
.action(async (project: string) => {
|
|
475
|
-
const opts = program.opts<GlobalOptions>();
|
|
476
|
-
const data = (await request("auth", `/${project}/api-keys`, {}, opts)) as { api_keys: unknown[] };
|
|
477
|
-
output(data.api_keys, opts);
|
|
452
|
+
console.log(chalk.green(`Template '${name}' saved.`));
|
|
478
453
|
});
|
|
479
454
|
|
|
480
|
-
|
|
481
|
-
.command("
|
|
482
|
-
.description("
|
|
483
|
-
.argument("<
|
|
484
|
-
.
|
|
485
|
-
.action(async (project: string, cmdOpts: { name?: string }) => {
|
|
455
|
+
template
|
|
456
|
+
.command("rm")
|
|
457
|
+
.description("Delete a template")
|
|
458
|
+
.argument("<name>", "Template name")
|
|
459
|
+
.action(async (name: string) => {
|
|
486
460
|
const opts = program.opts<GlobalOptions>();
|
|
487
|
-
const
|
|
488
|
-
if (cmdOpts.name) body.name = cmdOpts.name;
|
|
489
|
-
const data = await request("auth", `/${project}/api-keys`, { method: "POST", body }, opts);
|
|
490
|
-
output(data, opts);
|
|
491
|
-
});
|
|
461
|
+
const profile = getProfile(opts.profile);
|
|
492
462
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
463
|
+
if (name === "default") {
|
|
464
|
+
error("Cannot delete the default template", opts);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const res = await fetch(`${profile.baseUrl}/auth/${profile.project}/templates/${name}`, {
|
|
468
|
+
method: "DELETE",
|
|
469
|
+
headers: { Authorization: `Bearer ${profile.apiKey}` },
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
if (!res.ok) {
|
|
473
|
+
const data = await res.json();
|
|
474
|
+
error((data as { error?: string }).error || "Failed to delete template", opts);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
console.log(chalk.green(`Template '${name}' deleted.`));
|
|
502
478
|
});
|
|
503
479
|
|
|
504
480
|
// =============================================================================
|
|
@@ -507,15 +483,13 @@ auth
|
|
|
507
483
|
|
|
508
484
|
const db = program.command("db").description("Database service commands");
|
|
509
485
|
|
|
510
|
-
db
|
|
511
|
-
.command("ls")
|
|
486
|
+
db.command("ls")
|
|
512
487
|
.description("List documents")
|
|
513
|
-
.argument("<project>", "Project name")
|
|
514
488
|
.argument("<collection>", "Collection name")
|
|
515
489
|
.option("-f, --filter <json>", "Filter criteria (JSON)")
|
|
516
490
|
.option("-l, --limit <n>", "Limit results")
|
|
517
491
|
.option("-s, --sort <field>", "Sort field (prefix with - for desc)")
|
|
518
|
-
.action(async (
|
|
492
|
+
.action(async (collection: string, cmdOpts: { filter?: string; limit?: string; sort?: string }) => {
|
|
519
493
|
const opts = program.opts<GlobalOptions>();
|
|
520
494
|
const params = new URLSearchParams();
|
|
521
495
|
if (cmdOpts.filter) {
|
|
@@ -527,81 +501,71 @@ db
|
|
|
527
501
|
if (cmdOpts.limit) params.set("_limit", cmdOpts.limit);
|
|
528
502
|
if (cmdOpts.sort) params.set("_sort", cmdOpts.sort);
|
|
529
503
|
const query = params.toString() ? `?${params.toString()}` : "";
|
|
530
|
-
const data = (await request("db", `/${
|
|
504
|
+
const data = (await request("db", `/${collection}${query}`, {}, opts)) as { data: unknown[] };
|
|
531
505
|
output(data.data, opts);
|
|
532
506
|
});
|
|
533
507
|
|
|
534
|
-
db
|
|
535
|
-
.command("get")
|
|
508
|
+
db.command("get")
|
|
536
509
|
.description("Get a document")
|
|
537
|
-
.argument("<project>", "Project name")
|
|
538
510
|
.argument("<collection>", "Collection name")
|
|
539
511
|
.argument("<id>", "Document ID")
|
|
540
|
-
.action(async (
|
|
512
|
+
.action(async (collection: string, id: string) => {
|
|
541
513
|
const opts = program.opts<GlobalOptions>();
|
|
542
|
-
const data = await request("db", `/${
|
|
514
|
+
const data = await request("db", `/${collection}/${id}`, {}, opts);
|
|
543
515
|
output(data, opts);
|
|
544
516
|
});
|
|
545
517
|
|
|
546
|
-
db
|
|
547
|
-
.command("put")
|
|
518
|
+
db.command("put")
|
|
548
519
|
.description("Create or update a document (reads JSON from stdin)")
|
|
549
|
-
.argument("<project>", "Project name")
|
|
550
520
|
.argument("<collection>", "Collection name")
|
|
551
521
|
.argument("[id]", "Document ID (for update)")
|
|
552
|
-
.action(async (
|
|
522
|
+
.action(async (collection: string, id?: string) => {
|
|
553
523
|
const opts = program.opts<GlobalOptions>();
|
|
554
|
-
|
|
524
|
+
|
|
555
525
|
// Read from stdin
|
|
556
526
|
const chunks: Buffer[] = [];
|
|
557
527
|
for await (const chunk of Bun.stdin.stream()) {
|
|
558
528
|
chunks.push(Buffer.from(chunk));
|
|
559
529
|
}
|
|
560
530
|
const input = Buffer.concat(chunks).toString("utf-8").trim();
|
|
561
|
-
|
|
531
|
+
|
|
562
532
|
if (!input) {
|
|
563
533
|
error("No JSON provided on stdin", opts);
|
|
564
534
|
}
|
|
565
|
-
|
|
535
|
+
|
|
566
536
|
let body: unknown;
|
|
567
537
|
try {
|
|
568
538
|
body = JSON.parse(input);
|
|
569
539
|
} catch {
|
|
570
540
|
error("Invalid JSON on stdin", opts);
|
|
571
541
|
}
|
|
572
|
-
|
|
542
|
+
|
|
573
543
|
if (id) {
|
|
574
|
-
|
|
575
|
-
const data = await request("db", `/${project}/${collection}/${id}`, { method: "PUT", body, project }, opts);
|
|
544
|
+
const data = await request("db", `/${collection}/${id}`, { method: "PUT", body }, opts);
|
|
576
545
|
output(data, opts);
|
|
577
546
|
} else {
|
|
578
|
-
|
|
579
|
-
const data = await request("db", `/${project}/${collection}`, { method: "POST", body, project }, opts);
|
|
547
|
+
const data = await request("db", `/${collection}`, { method: "POST", body }, opts);
|
|
580
548
|
output(data, opts);
|
|
581
549
|
}
|
|
582
550
|
});
|
|
583
551
|
|
|
584
|
-
db
|
|
585
|
-
.command("rm")
|
|
552
|
+
db.command("rm")
|
|
586
553
|
.description("Delete a document")
|
|
587
|
-
.argument("<project>", "Project name")
|
|
588
554
|
.argument("<collection>", "Collection name")
|
|
589
555
|
.argument("<id>", "Document ID")
|
|
590
|
-
.action(async (
|
|
556
|
+
.action(async (collection: string, id: string) => {
|
|
591
557
|
const opts = program.opts<GlobalOptions>();
|
|
592
|
-
const data = await request("db", `/${
|
|
558
|
+
const data = await request("db", `/${collection}/${id}`, { method: "DELETE" }, opts);
|
|
593
559
|
output(data, opts);
|
|
594
560
|
});
|
|
595
561
|
|
|
596
|
-
db
|
|
597
|
-
.command("schema")
|
|
562
|
+
db.command("schema")
|
|
598
563
|
.description("Get or set collection schema")
|
|
599
|
-
.argument("<project>", "Project name")
|
|
600
564
|
.argument("<collection>", "Collection name")
|
|
601
565
|
.option("--set", "Set schema (reads JSON from stdin)")
|
|
602
|
-
.action(async (
|
|
566
|
+
.action(async (collection: string, cmdOpts: { set?: boolean }) => {
|
|
603
567
|
const opts = program.opts<GlobalOptions>();
|
|
604
|
-
|
|
568
|
+
|
|
605
569
|
if (cmdOpts.set) {
|
|
606
570
|
const chunks: Buffer[] = [];
|
|
607
571
|
for await (const chunk of Bun.stdin.stream()) {
|
|
@@ -609,10 +573,10 @@ db
|
|
|
609
573
|
}
|
|
610
574
|
const input = Buffer.concat(chunks).toString("utf-8").trim();
|
|
611
575
|
const body = JSON.parse(input);
|
|
612
|
-
const data = await request("db", `/${
|
|
576
|
+
const data = await request("db", `/${collection}/_schema`, { method: "PUT", body }, opts);
|
|
613
577
|
output(data, opts);
|
|
614
578
|
} else {
|
|
615
|
-
const data = await request("db", `/${
|
|
579
|
+
const data = await request("db", `/${collection}/_schema`, {}, opts);
|
|
616
580
|
output(data, opts);
|
|
617
581
|
}
|
|
618
582
|
});
|
|
@@ -626,34 +590,33 @@ const blob = program.command("blob").description("Blob storage commands");
|
|
|
626
590
|
blob
|
|
627
591
|
.command("put")
|
|
628
592
|
.description("Upload a file")
|
|
629
|
-
.argument("<project>", "Project name")
|
|
630
593
|
.argument("<path>", "Remote path")
|
|
631
594
|
.argument("<file>", "Local file path")
|
|
632
|
-
.action(async (
|
|
595
|
+
.action(async (remotePath: string, localFile: string) => {
|
|
633
596
|
const opts = program.opts<GlobalOptions>();
|
|
634
597
|
const profile = getProfile(opts.profile);
|
|
635
|
-
const token = await
|
|
636
|
-
|
|
598
|
+
const token = await getToken(opts);
|
|
599
|
+
|
|
637
600
|
const file = Bun.file(localFile);
|
|
638
601
|
if (!(await file.exists())) {
|
|
639
602
|
error(`File not found: ${localFile}`, opts);
|
|
640
603
|
}
|
|
641
|
-
|
|
642
|
-
const url = `${profile.baseUrl}/blob/${project}/${remotePath}`;
|
|
643
|
-
|
|
604
|
+
|
|
605
|
+
const url = `${profile.baseUrl}/blob/${profile.project}/${remotePath}`;
|
|
606
|
+
|
|
644
607
|
if (opts.debug) {
|
|
645
608
|
console.error(chalk.dim(`curl -X PUT "${url}" -T "${localFile}"`));
|
|
646
609
|
}
|
|
647
|
-
|
|
610
|
+
|
|
648
611
|
const res = await fetch(url, {
|
|
649
612
|
method: "PUT",
|
|
650
613
|
headers: {
|
|
651
|
-
|
|
614
|
+
Authorization: `Bearer ${token}`,
|
|
652
615
|
"Content-Type": file.type || "application/octet-stream",
|
|
653
616
|
},
|
|
654
617
|
body: file,
|
|
655
618
|
});
|
|
656
|
-
|
|
619
|
+
|
|
657
620
|
const data = await res.json();
|
|
658
621
|
if (!res.ok) {
|
|
659
622
|
error((data as { error?: string }).error || "Upload failed", opts);
|
|
@@ -664,28 +627,27 @@ blob
|
|
|
664
627
|
blob
|
|
665
628
|
.command("get")
|
|
666
629
|
.description("Download a file (outputs to stdout)")
|
|
667
|
-
.argument("<project>", "Project name")
|
|
668
630
|
.argument("<path>", "Remote path")
|
|
669
|
-
.action(async (
|
|
631
|
+
.action(async (remotePath: string) => {
|
|
670
632
|
const opts = program.opts<GlobalOptions>();
|
|
671
633
|
const profile = getProfile(opts.profile);
|
|
672
|
-
const token = await
|
|
673
|
-
|
|
674
|
-
const url = `${profile.baseUrl}/blob/${project}/${remotePath}`;
|
|
675
|
-
|
|
634
|
+
const token = await getToken(opts);
|
|
635
|
+
|
|
636
|
+
const url = `${profile.baseUrl}/blob/${profile.project}/${remotePath}`;
|
|
637
|
+
|
|
676
638
|
if (opts.debug) {
|
|
677
639
|
console.error(chalk.dim(`curl "${url}"`));
|
|
678
640
|
}
|
|
679
|
-
|
|
641
|
+
|
|
680
642
|
const res = await fetch(url, {
|
|
681
|
-
headers: {
|
|
643
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
682
644
|
});
|
|
683
|
-
|
|
645
|
+
|
|
684
646
|
if (!res.ok) {
|
|
685
647
|
const data = await res.json();
|
|
686
648
|
error((data as { error?: string }).error || "Download failed", opts);
|
|
687
649
|
}
|
|
688
|
-
|
|
650
|
+
|
|
689
651
|
// Stream to stdout
|
|
690
652
|
const writer = Bun.stdout.writer();
|
|
691
653
|
const reader = res.body?.getReader();
|
|
@@ -702,20 +664,19 @@ blob
|
|
|
702
664
|
blob
|
|
703
665
|
.command("rm")
|
|
704
666
|
.description("Delete a file")
|
|
705
|
-
.argument("<project>", "Project name")
|
|
706
667
|
.argument("<path>", "Remote path")
|
|
707
|
-
.action(async (
|
|
668
|
+
.action(async (remotePath: string) => {
|
|
708
669
|
const opts = program.opts<GlobalOptions>();
|
|
709
670
|
const profile = getProfile(opts.profile);
|
|
710
|
-
const token = await
|
|
711
|
-
|
|
712
|
-
const url = `${profile.baseUrl}/blob/${project}/${remotePath}`;
|
|
713
|
-
|
|
671
|
+
const token = await getToken(opts);
|
|
672
|
+
|
|
673
|
+
const url = `${profile.baseUrl}/blob/${profile.project}/${remotePath}`;
|
|
674
|
+
|
|
714
675
|
const res = await fetch(url, {
|
|
715
676
|
method: "DELETE",
|
|
716
|
-
headers: {
|
|
677
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
717
678
|
});
|
|
718
|
-
|
|
679
|
+
|
|
719
680
|
const data = await res.json();
|
|
720
681
|
if (!res.ok) {
|
|
721
682
|
error((data as { error?: string }).error || "Delete failed", opts);
|
|
@@ -732,58 +693,54 @@ const cron = program.command("cron").description("Cron service commands");
|
|
|
732
693
|
cron
|
|
733
694
|
.command("ls")
|
|
734
695
|
.description("List jobs")
|
|
735
|
-
.
|
|
736
|
-
.action(async (project: string) => {
|
|
696
|
+
.action(async () => {
|
|
737
697
|
const opts = program.opts<GlobalOptions>();
|
|
738
|
-
const data = (await request("cron",
|
|
698
|
+
const data = (await request("cron", "/jobs", {}, opts)) as { jobs: unknown[] };
|
|
739
699
|
output(data.jobs, opts);
|
|
740
700
|
});
|
|
741
701
|
|
|
742
702
|
cron
|
|
743
703
|
.command("runs")
|
|
744
704
|
.description("List run history")
|
|
745
|
-
.argument("<project>", "Project name")
|
|
746
705
|
.option("-l, --limit <n>", "Limit results", "100")
|
|
747
706
|
.option("--job <id>", "Filter by job ID")
|
|
748
|
-
.action(async (
|
|
707
|
+
.action(async (cmdOpts: { limit: string; job?: string }) => {
|
|
749
708
|
const opts = program.opts<GlobalOptions>();
|
|
750
709
|
const params = new URLSearchParams({ limit: cmdOpts.limit });
|
|
751
710
|
if (cmdOpts.job) params.set("job_id", cmdOpts.job);
|
|
752
|
-
const data = (await request("cron",
|
|
711
|
+
const data = (await request("cron", `/runs?${params}`, {}, opts)) as { runs: unknown[] };
|
|
753
712
|
output(data.runs, opts);
|
|
754
713
|
});
|
|
755
714
|
|
|
756
715
|
cron
|
|
757
716
|
.command("trigger")
|
|
758
717
|
.description("Manually trigger a job")
|
|
759
|
-
.argument("<project>", "Project name")
|
|
760
718
|
.argument("<job_id>", "Job ID")
|
|
761
|
-
.action(async (
|
|
719
|
+
.action(async (jobId: string) => {
|
|
762
720
|
const opts = program.opts<GlobalOptions>();
|
|
763
|
-
const data = await request("cron",
|
|
721
|
+
const data = await request("cron", `/jobs/${jobId}/trigger`, { method: "POST" }, opts);
|
|
764
722
|
output(data, opts);
|
|
765
723
|
});
|
|
766
724
|
|
|
767
725
|
cron
|
|
768
726
|
.command("script:cat")
|
|
769
727
|
.description("Output script content")
|
|
770
|
-
.argument("<project>", "Project name")
|
|
771
728
|
.argument("<script>", "Script name")
|
|
772
|
-
.action(async (
|
|
729
|
+
.action(async (script: string) => {
|
|
773
730
|
const opts = program.opts<GlobalOptions>();
|
|
774
731
|
const profile = getProfile(opts.profile);
|
|
775
|
-
const token = await
|
|
776
|
-
|
|
777
|
-
const url = `${profile.baseUrl}/cron/${project}/scripts/${script}`;
|
|
732
|
+
const token = await getToken(opts);
|
|
733
|
+
|
|
734
|
+
const url = `${profile.baseUrl}/cron/${profile.project}/scripts/${script}`;
|
|
778
735
|
const res = await fetch(url, {
|
|
779
|
-
headers: {
|
|
736
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
780
737
|
});
|
|
781
|
-
|
|
738
|
+
|
|
782
739
|
if (!res.ok) {
|
|
783
740
|
const data = await res.json();
|
|
784
741
|
error((data as { error?: string }).error || "Failed to get script", opts);
|
|
785
742
|
}
|
|
786
|
-
|
|
743
|
+
|
|
787
744
|
console.log(await res.text());
|
|
788
745
|
});
|
|
789
746
|
|
|
@@ -793,52 +750,60 @@ cron
|
|
|
793
750
|
|
|
794
751
|
const ai = program.command("ai").description("AI service commands");
|
|
795
752
|
|
|
796
|
-
ai
|
|
797
|
-
.command("stats")
|
|
753
|
+
ai.command("stats")
|
|
798
754
|
.description("View usage statistics")
|
|
799
|
-
.argument("<project>", "Project name")
|
|
800
755
|
.option("-p, --period <days>", "Period in days", "30")
|
|
801
|
-
.action(async (
|
|
756
|
+
.action(async (cmdOpts: { period: string }) => {
|
|
802
757
|
const opts = program.opts<GlobalOptions>();
|
|
803
|
-
const
|
|
804
|
-
|
|
758
|
+
const profile = getProfile(opts.profile);
|
|
759
|
+
const token = await getToken(opts);
|
|
760
|
+
|
|
761
|
+
const url = `${profile.baseUrl}/ai/usage/stats?period=${cmdOpts.period}d`;
|
|
762
|
+
const res = await fetch(url, {
|
|
763
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
if (!res.ok) {
|
|
767
|
+
const data = await res.json();
|
|
768
|
+
error((data as { error?: string }).error || "Failed to get stats", opts);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
output(await res.json(), opts);
|
|
805
772
|
});
|
|
806
773
|
|
|
807
|
-
ai
|
|
808
|
-
.command("chat")
|
|
774
|
+
ai.command("chat")
|
|
809
775
|
.description("Send a chat completion request")
|
|
810
|
-
.argument("<project>", "Project name")
|
|
811
776
|
.argument("<prompt>", "Chat prompt")
|
|
812
777
|
.option("-m, --model <model>", "Model to use (defaults to project config)")
|
|
813
|
-
.action(async (
|
|
778
|
+
.action(async (prompt: string, cmdOpts: { model?: string }) => {
|
|
814
779
|
const opts = program.opts<GlobalOptions>();
|
|
815
780
|
const profile = getProfile(opts.profile);
|
|
816
|
-
const token = await
|
|
817
|
-
|
|
781
|
+
const token = await getToken(opts);
|
|
782
|
+
|
|
818
783
|
// Get default model from project config if not specified
|
|
819
784
|
let model = cmdOpts.model;
|
|
820
785
|
if (!model) {
|
|
821
786
|
const configRes = await fetch(`${profile.baseUrl}/ai/config`, {
|
|
822
|
-
headers: {
|
|
787
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
823
788
|
});
|
|
824
789
|
if (configRes.ok) {
|
|
825
|
-
const config = await configRes.json() as { default_model?: string };
|
|
790
|
+
const config = (await configRes.json()) as { default_model?: string };
|
|
826
791
|
model = config.default_model || "openai/gpt-4o-mini";
|
|
827
792
|
} else {
|
|
828
793
|
model = "openai/gpt-4o-mini";
|
|
829
794
|
}
|
|
830
795
|
}
|
|
831
|
-
|
|
796
|
+
|
|
832
797
|
const url = `${profile.baseUrl}/ai/v1/chat/completions`;
|
|
833
|
-
|
|
798
|
+
|
|
834
799
|
if (opts.debug) {
|
|
835
800
|
console.error(chalk.dim(`curl -X POST "${url}" ...`));
|
|
836
801
|
}
|
|
837
|
-
|
|
802
|
+
|
|
838
803
|
const res = await fetch(url, {
|
|
839
804
|
method: "POST",
|
|
840
805
|
headers: {
|
|
841
|
-
|
|
806
|
+
Authorization: `Bearer ${token}`,
|
|
842
807
|
"Content-Type": "application/json",
|
|
843
808
|
},
|
|
844
809
|
body: JSON.stringify({
|
|
@@ -846,44 +811,60 @@ ai
|
|
|
846
811
|
messages: [{ role: "user", content: prompt }],
|
|
847
812
|
}),
|
|
848
813
|
});
|
|
849
|
-
|
|
814
|
+
|
|
850
815
|
const data = await res.json();
|
|
851
816
|
if (!res.ok) {
|
|
852
817
|
error((data as { error?: string }).error || "Chat failed", opts);
|
|
853
818
|
}
|
|
854
|
-
|
|
819
|
+
|
|
855
820
|
if (opts.json) {
|
|
856
821
|
console.log(JSON.stringify(data, null, 2));
|
|
857
822
|
} else {
|
|
858
|
-
const content = (data as { choices: Array<{ message: { content: string } }> }).choices?.[0]
|
|
823
|
+
const content = (data as { choices: Array<{ message: { content: string } }> }).choices?.[0]
|
|
824
|
+
?.message?.content;
|
|
859
825
|
console.log(content || "(no response)");
|
|
860
826
|
}
|
|
861
827
|
});
|
|
862
828
|
|
|
863
|
-
ai
|
|
864
|
-
.
|
|
865
|
-
.description("Get or set AI config for a project")
|
|
866
|
-
.argument("<project>", "Project name")
|
|
829
|
+
ai.command("config")
|
|
830
|
+
.description("Get or set AI config")
|
|
867
831
|
.option("--model <model>", "Set default model")
|
|
868
832
|
.option("--rate-limit <usd>", "Set rate limit in USD")
|
|
869
833
|
.option("--key <key>", "Set custom OpenRouter API key")
|
|
870
|
-
.action(async (
|
|
834
|
+
.action(async (cmdOpts: { model?: string; rateLimit?: string; key?: string }) => {
|
|
871
835
|
const opts = program.opts<GlobalOptions>();
|
|
872
|
-
|
|
836
|
+
const profile = getProfile(opts.profile);
|
|
837
|
+
const token = await getToken(opts);
|
|
838
|
+
|
|
873
839
|
// If any options provided, update config
|
|
874
840
|
if (cmdOpts.model || cmdOpts.rateLimit || cmdOpts.key) {
|
|
875
841
|
const body: Record<string, unknown> = {};
|
|
876
842
|
if (cmdOpts.model) body.default_model = cmdOpts.model;
|
|
877
843
|
if (cmdOpts.rateLimit) body.rate_limit_usd = Number.parseFloat(cmdOpts.rateLimit);
|
|
878
844
|
if (cmdOpts.key) body.key = cmdOpts.key;
|
|
879
|
-
|
|
880
|
-
await
|
|
845
|
+
|
|
846
|
+
const updateRes = await fetch(`${profile.baseUrl}/ai/config`, {
|
|
847
|
+
method: "PUT",
|
|
848
|
+
headers: {
|
|
849
|
+
Authorization: `Bearer ${token}`,
|
|
850
|
+
"Content-Type": "application/json",
|
|
851
|
+
},
|
|
852
|
+
body: JSON.stringify(body),
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
if (!updateRes.ok) {
|
|
856
|
+
const data = await updateRes.json();
|
|
857
|
+
error((data as { error?: string }).error || "Failed to update config", opts);
|
|
858
|
+
}
|
|
859
|
+
|
|
881
860
|
console.log("Config updated");
|
|
882
861
|
}
|
|
883
|
-
|
|
862
|
+
|
|
884
863
|
// Always show current config
|
|
885
|
-
const
|
|
886
|
-
|
|
864
|
+
const res = await fetch(`${profile.baseUrl}/ai/config`, {
|
|
865
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
866
|
+
});
|
|
867
|
+
output(await res.json(), opts);
|
|
887
868
|
});
|
|
888
869
|
|
|
889
870
|
// =============================================================================
|
|
@@ -895,12 +876,11 @@ const search = program.command("search").description("Search service commands");
|
|
|
895
876
|
search
|
|
896
877
|
.command("config")
|
|
897
878
|
.description("Get or set index configuration")
|
|
898
|
-
.argument("<project>", "Project name")
|
|
899
879
|
.argument("<index>", "Index name")
|
|
900
880
|
.option("--set", "Set config (reads JSON from stdin)")
|
|
901
|
-
.action(async (
|
|
881
|
+
.action(async (index: string, cmdOpts: { set?: boolean }) => {
|
|
902
882
|
const opts = program.opts<GlobalOptions>();
|
|
903
|
-
|
|
883
|
+
|
|
904
884
|
if (cmdOpts.set) {
|
|
905
885
|
const chunks: Buffer[] = [];
|
|
906
886
|
for await (const chunk of Bun.stdin.stream()) {
|
|
@@ -908,10 +888,10 @@ search
|
|
|
908
888
|
}
|
|
909
889
|
const input = Buffer.concat(chunks).toString("utf-8").trim();
|
|
910
890
|
const body = JSON.parse(input);
|
|
911
|
-
const data = await request("search", `/${
|
|
891
|
+
const data = await request("search", `/${index}/_config`, { method: "PUT", body }, opts);
|
|
912
892
|
output(data, opts);
|
|
913
893
|
} else {
|
|
914
|
-
const data = await request("search", `/${
|
|
894
|
+
const data = await request("search", `/${index}/_config`, {}, opts);
|
|
915
895
|
output(data, opts);
|
|
916
896
|
}
|
|
917
897
|
});
|
|
@@ -919,14 +899,15 @@ search
|
|
|
919
899
|
search
|
|
920
900
|
.command("query")
|
|
921
901
|
.description("Search an index")
|
|
922
|
-
.argument("<project>", "Project name")
|
|
923
902
|
.argument("<index>", "Index name")
|
|
924
903
|
.argument("<q>", "Search query")
|
|
925
904
|
.option("-l, --limit <n>", "Limit results", "10")
|
|
926
|
-
.action(async (
|
|
905
|
+
.action(async (index: string, q: string, cmdOpts: { limit: string }) => {
|
|
927
906
|
const opts = program.opts<GlobalOptions>();
|
|
928
907
|
const params = new URLSearchParams({ q, limit: cmdOpts.limit });
|
|
929
|
-
const data = (await request("search", `/${
|
|
908
|
+
const data = (await request("search", `/${index}/search?${params}`, {}, opts)) as {
|
|
909
|
+
results: unknown[];
|
|
910
|
+
};
|
|
930
911
|
output(data.results, opts);
|
|
931
912
|
});
|
|
932
913
|
|