@vtriv/cli 0.1.0 → 0.1.2
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 +237 -30
- package/index.ts +601 -403
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* vtriv-cli - Command-line interface for vtriv services
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Configuration hierarchy:
|
|
6
|
+
* 1. Account key (vtriv_ak_*) - set via `vtriv config`
|
|
7
|
+
* 2. Project profiles - created via `vtriv init <name>`
|
|
8
|
+
*
|
|
9
|
+
* The CLI auto-creates projects prefixed with account slug,
|
|
10
|
+
* and translates short names to full names transparently.
|
|
7
11
|
*/
|
|
8
12
|
|
|
9
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from "node:fs";
|
|
10
14
|
import { homedir } from "node:os";
|
|
11
|
-
import { join } from "node:path";
|
|
15
|
+
import { join, dirname } from "node:path";
|
|
12
16
|
import { Command } from "commander";
|
|
13
17
|
import chalk from "chalk";
|
|
14
18
|
|
|
@@ -17,14 +21,15 @@ import chalk from "chalk";
|
|
|
17
21
|
// =============================================================================
|
|
18
22
|
|
|
19
23
|
interface Profile {
|
|
20
|
-
|
|
21
|
-
rootKey: string;
|
|
24
|
+
apiKey: string; // vtriv_sk_*
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
interface Config {
|
|
25
|
-
|
|
28
|
+
baseUrl: string;
|
|
29
|
+
accountKey?: string; // vtriv_ak_*
|
|
30
|
+
accountSlug?: string;
|
|
31
|
+
default?: string;
|
|
26
32
|
profiles: Record<string, Profile>;
|
|
27
|
-
apiKeys?: Record<string, string>;
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
interface GlobalOptions {
|
|
@@ -37,34 +42,55 @@ interface GlobalOptions {
|
|
|
37
42
|
// Configuration
|
|
38
43
|
// =============================================================================
|
|
39
44
|
|
|
40
|
-
|
|
45
|
+
function getConfigPath(): string {
|
|
46
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
47
|
+
return join(xdgConfig, "vtriv", "config.json");
|
|
48
|
+
}
|
|
41
49
|
|
|
42
50
|
function loadConfig(): Config | null {
|
|
43
|
-
|
|
51
|
+
const configPath = getConfigPath();
|
|
52
|
+
if (!existsSync(configPath)) return null;
|
|
44
53
|
try {
|
|
45
|
-
return JSON.parse(readFileSync(
|
|
54
|
+
return JSON.parse(readFileSync(configPath, "utf-8"));
|
|
46
55
|
} catch {
|
|
47
56
|
return null;
|
|
48
57
|
}
|
|
49
58
|
}
|
|
50
59
|
|
|
51
60
|
function saveConfig(config: Config): void {
|
|
52
|
-
|
|
61
|
+
const configPath = getConfigPath();
|
|
62
|
+
const configDir = dirname(configPath);
|
|
63
|
+
if (!existsSync(configDir)) {
|
|
64
|
+
mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
65
|
+
}
|
|
66
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
67
|
+
chmodSync(configPath, 0o600);
|
|
53
68
|
}
|
|
54
69
|
|
|
55
|
-
function
|
|
70
|
+
function getConfig(): Config {
|
|
56
71
|
const config = loadConfig();
|
|
57
72
|
if (!config) {
|
|
58
|
-
console.error(chalk.red("No config found. Run 'vtriv
|
|
73
|
+
console.error(chalk.red("No config found. Run 'vtriv config' first."));
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
return config;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getProfile(profileName?: string): { config: Config; profile: Profile; fullProjectName: string; shortName: string } {
|
|
80
|
+
const config = getConfig();
|
|
81
|
+
const shortName = profileName || config.default;
|
|
82
|
+
if (!shortName) {
|
|
83
|
+
console.error(chalk.red("No default profile. Run 'vtriv init <name>' to create one."));
|
|
59
84
|
process.exit(1);
|
|
60
85
|
}
|
|
61
|
-
const
|
|
62
|
-
const profile = config.profiles[name];
|
|
86
|
+
const profile = config.profiles[shortName];
|
|
63
87
|
if (!profile) {
|
|
64
|
-
console.error(chalk.red(`Profile '${
|
|
88
|
+
console.error(chalk.red(`Profile '${shortName}' not found. Run 'vtriv init ${shortName}' to create it.`));
|
|
65
89
|
process.exit(1);
|
|
66
90
|
}
|
|
67
|
-
|
|
91
|
+
// Expand short name to full project name
|
|
92
|
+
const fullProjectName = config.accountSlug ? `${config.accountSlug}-${shortName}` : shortName;
|
|
93
|
+
return { config, profile, fullProjectName, shortName };
|
|
68
94
|
}
|
|
69
95
|
|
|
70
96
|
// =============================================================================
|
|
@@ -107,7 +133,7 @@ function printTable(rows: Record<string, unknown>[]): void {
|
|
|
107
133
|
}
|
|
108
134
|
}
|
|
109
135
|
|
|
110
|
-
function error(msg: string, opts: GlobalOptions):
|
|
136
|
+
function error(msg: string, opts: GlobalOptions): never {
|
|
111
137
|
if (opts.json) {
|
|
112
138
|
console.error(JSON.stringify({ error: msg }));
|
|
113
139
|
} else {
|
|
@@ -117,45 +143,86 @@ function error(msg: string, opts: GlobalOptions): void {
|
|
|
117
143
|
}
|
|
118
144
|
|
|
119
145
|
// =============================================================================
|
|
120
|
-
//
|
|
146
|
+
// Token Management
|
|
121
147
|
// =============================================================================
|
|
122
148
|
|
|
123
|
-
//
|
|
149
|
+
// In-memory cache for minted tokens (per profile)
|
|
124
150
|
const tokenCache = new Map<string, { token: string; expires: number }>();
|
|
125
151
|
|
|
152
|
+
async function getToken(opts: GlobalOptions, template?: string): Promise<string> {
|
|
153
|
+
const { config, profile, fullProjectName } = getProfile(opts.profile);
|
|
154
|
+
|
|
155
|
+
// Cache key includes template
|
|
156
|
+
const cacheKey = template ? `${fullProjectName}:${template}` : fullProjectName;
|
|
157
|
+
const cached = tokenCache.get(cacheKey);
|
|
158
|
+
if (cached && cached.expires > Date.now()) {
|
|
159
|
+
return cached.token;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Mint a new token using the API key
|
|
163
|
+
const body: Record<string, unknown> = { expires_in: 3600 };
|
|
164
|
+
if (template) {
|
|
165
|
+
body.template = template;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const mintRes = await fetch(`${config.baseUrl}/auth/${fullProjectName}/token`, {
|
|
169
|
+
method: "POST",
|
|
170
|
+
headers: {
|
|
171
|
+
Authorization: `Bearer ${profile.apiKey}`,
|
|
172
|
+
"Content-Type": "application/json",
|
|
173
|
+
},
|
|
174
|
+
body: JSON.stringify(body),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (!mintRes.ok) {
|
|
178
|
+
const err = await mintRes.text();
|
|
179
|
+
let msg: string;
|
|
180
|
+
try {
|
|
181
|
+
msg = JSON.parse(err).error || err;
|
|
182
|
+
} catch {
|
|
183
|
+
msg = err;
|
|
184
|
+
}
|
|
185
|
+
error(`Failed to mint token: ${msg}`, opts);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const tokenData = (await mintRes.json()) as { token: string };
|
|
189
|
+
const token = tokenData.token;
|
|
190
|
+
|
|
191
|
+
// Cache for 55 minutes (token valid for 60)
|
|
192
|
+
tokenCache.set(cacheKey, { token, expires: Date.now() + 55 * 60 * 1000 });
|
|
193
|
+
|
|
194
|
+
return token;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// =============================================================================
|
|
198
|
+
// HTTP Request Helper
|
|
199
|
+
// =============================================================================
|
|
200
|
+
|
|
126
201
|
async function request(
|
|
127
202
|
service: string,
|
|
128
203
|
path: string,
|
|
129
204
|
options: {
|
|
130
205
|
method?: string;
|
|
131
206
|
body?: unknown;
|
|
132
|
-
|
|
207
|
+
template?: string;
|
|
133
208
|
},
|
|
134
209
|
opts: GlobalOptions
|
|
135
210
|
): Promise<unknown> {
|
|
136
|
-
const
|
|
137
|
-
const url = `${
|
|
211
|
+
const { config, fullProjectName } = getProfile(opts.profile);
|
|
212
|
+
const url = `${config.baseUrl}/${service}/${fullProjectName}${path}`;
|
|
138
213
|
const method = options.method || "GET";
|
|
139
214
|
|
|
140
215
|
const headers: Record<string, string> = {
|
|
141
216
|
"Content-Type": "application/json",
|
|
142
217
|
};
|
|
143
218
|
|
|
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
|
-
}
|
|
219
|
+
// Get a JWT token for the request
|
|
220
|
+
const token = await getToken(opts, options.template);
|
|
221
|
+
headers.Authorization = `Bearer ${token}`;
|
|
152
222
|
|
|
153
223
|
if (opts.debug) {
|
|
154
224
|
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}`));
|
|
225
|
+
console.error(chalk.dim(`curl -X ${method} "${url}" -H "Authorization: Bearer <token>" ${bodyArg}`));
|
|
159
226
|
}
|
|
160
227
|
|
|
161
228
|
const res = await fetch(url, {
|
|
@@ -173,95 +240,16 @@ async function request(
|
|
|
173
240
|
}
|
|
174
241
|
|
|
175
242
|
if (!res.ok) {
|
|
176
|
-
const errMsg =
|
|
177
|
-
|
|
178
|
-
|
|
243
|
+
const errMsg =
|
|
244
|
+
typeof data === "object" && data !== null && "error" in data
|
|
245
|
+
? (data as { error: string }).error
|
|
246
|
+
: text;
|
|
179
247
|
error(errMsg, opts);
|
|
180
248
|
}
|
|
181
249
|
|
|
182
250
|
return data;
|
|
183
251
|
}
|
|
184
252
|
|
|
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
253
|
// =============================================================================
|
|
266
254
|
// Program
|
|
267
255
|
// =============================================================================
|
|
@@ -277,189 +265,304 @@ program
|
|
|
277
265
|
.option("--debug", "Show HTTP requests");
|
|
278
266
|
|
|
279
267
|
// =============================================================================
|
|
280
|
-
//
|
|
268
|
+
// Config Command
|
|
281
269
|
// =============================================================================
|
|
282
270
|
|
|
283
271
|
program
|
|
284
|
-
.command("
|
|
285
|
-
.description("
|
|
272
|
+
.command("config")
|
|
273
|
+
.description("Configure account key and base URL")
|
|
286
274
|
.action(async () => {
|
|
287
275
|
const existing = loadConfig();
|
|
288
|
-
|
|
289
|
-
console.log(chalk.bold("vtriv
|
|
290
|
-
|
|
276
|
+
|
|
277
|
+
console.log(chalk.bold("vtriv Account Configuration\n"));
|
|
278
|
+
|
|
291
279
|
const readline = await import("node:readline");
|
|
292
280
|
const rl = readline.createInterface({
|
|
293
281
|
input: process.stdin,
|
|
294
282
|
output: process.stdout,
|
|
295
283
|
});
|
|
296
|
-
|
|
284
|
+
|
|
297
285
|
const question = (q: string): Promise<string> =>
|
|
298
286
|
new Promise((resolve) => rl.question(q, resolve));
|
|
299
|
-
|
|
300
|
-
const baseUrl =
|
|
301
|
-
`Base URL [${existing?.
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if (!
|
|
307
|
-
console.error(chalk.red("
|
|
287
|
+
|
|
288
|
+
const baseUrl =
|
|
289
|
+
(await question(`Base URL [${existing?.baseUrl || "http://localhost:3000"}]: `)) ||
|
|
290
|
+
existing?.baseUrl ||
|
|
291
|
+
"http://localhost:3000";
|
|
292
|
+
|
|
293
|
+
const accountKey = await question("Account Key (vtriv_ak_...): ");
|
|
294
|
+
if (!accountKey) {
|
|
295
|
+
console.error(chalk.red("Account key is required."));
|
|
308
296
|
rl.close();
|
|
309
297
|
process.exit(1);
|
|
310
298
|
}
|
|
311
|
-
|
|
312
|
-
if (!
|
|
313
|
-
console.error(chalk.red("
|
|
299
|
+
|
|
300
|
+
if (!accountKey.startsWith("vtriv_ak_")) {
|
|
301
|
+
console.error(chalk.red("Account key must start with 'vtriv_ak_'."));
|
|
314
302
|
rl.close();
|
|
315
303
|
process.exit(1);
|
|
316
304
|
}
|
|
317
|
-
|
|
305
|
+
|
|
306
|
+
rl.close();
|
|
307
|
+
|
|
308
|
+
// Fetch account info to get slug
|
|
309
|
+
console.log(chalk.dim("\nVerifying account key..."));
|
|
310
|
+
const res = await fetch(`${baseUrl}/auth/accounts/me`, {
|
|
311
|
+
headers: { Authorization: `Bearer ${accountKey}` },
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
if (!res.ok) {
|
|
315
|
+
const err = await res.text();
|
|
316
|
+
let msg: string;
|
|
317
|
+
try {
|
|
318
|
+
msg = JSON.parse(err).error || err;
|
|
319
|
+
} catch {
|
|
320
|
+
msg = err;
|
|
321
|
+
}
|
|
322
|
+
console.error(chalk.red(`Failed to verify account: ${msg}`));
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const accountInfo = (await res.json()) as { id: string; slug: string; name: string | null };
|
|
327
|
+
|
|
318
328
|
const config: Config = {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
329
|
+
baseUrl,
|
|
330
|
+
accountKey,
|
|
331
|
+
accountSlug: accountInfo.slug,
|
|
332
|
+
default: existing?.default,
|
|
333
|
+
profiles: existing?.profiles || {},
|
|
323
334
|
};
|
|
324
|
-
|
|
335
|
+
|
|
325
336
|
saveConfig(config);
|
|
326
|
-
console.log(chalk.green(`\
|
|
327
|
-
|
|
337
|
+
console.log(chalk.green(`\nAccount configured!`));
|
|
338
|
+
console.log(chalk.dim(` Slug: ${accountInfo.slug}`));
|
|
339
|
+
console.log(chalk.dim(` Name: ${accountInfo.name || "(none)"}`));
|
|
340
|
+
console.log(chalk.dim(` Config: ${getConfigPath()}`));
|
|
328
341
|
});
|
|
329
342
|
|
|
330
343
|
// =============================================================================
|
|
331
|
-
//
|
|
344
|
+
// Init Command
|
|
332
345
|
// =============================================================================
|
|
333
346
|
|
|
334
|
-
|
|
347
|
+
program
|
|
348
|
+
.command("init")
|
|
349
|
+
.description("Create a project and configure profile")
|
|
350
|
+
.argument("<name>", "Project name (short name, will be prefixed with account slug)")
|
|
351
|
+
.option("--default", "Set as default profile")
|
|
352
|
+
.action(async (name: string, cmdOpts: { default?: boolean }) => {
|
|
353
|
+
const config = loadConfig();
|
|
354
|
+
|
|
355
|
+
if (!config?.accountKey || !config?.accountSlug) {
|
|
356
|
+
console.error(chalk.red("No account configured. Run 'vtriv config' first."));
|
|
357
|
+
process.exit(1);
|
|
358
|
+
}
|
|
335
359
|
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const opts = program.opts<GlobalOptions>();
|
|
342
|
-
const data = (await request("auth", "/accounts", {}, opts)) as { accounts: unknown[] };
|
|
343
|
-
output(data.accounts, opts);
|
|
344
|
-
});
|
|
360
|
+
// Validate project name
|
|
361
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
362
|
+
console.error(chalk.red("Invalid project name. Use alphanumeric, dash, or underscore only."));
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
345
365
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
366
|
+
const fullProjectName = `${config.accountSlug}-${name}`;
|
|
367
|
+
|
|
368
|
+
// Try to create project
|
|
369
|
+
console.log(chalk.dim(`Creating project ${fullProjectName}...`));
|
|
370
|
+
const createRes = await fetch(`${config.baseUrl}/auth/projects`, {
|
|
371
|
+
method: "POST",
|
|
372
|
+
headers: {
|
|
373
|
+
Authorization: `Bearer ${config.accountKey}`,
|
|
374
|
+
"Content-Type": "application/json",
|
|
375
|
+
},
|
|
376
|
+
body: JSON.stringify({ name }),
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
let projectExists = false;
|
|
380
|
+
if (!createRes.ok) {
|
|
381
|
+
const err = (await createRes.json()) as { error?: string };
|
|
382
|
+
if (err.error === "Project already exists") {
|
|
383
|
+
projectExists = true;
|
|
384
|
+
console.log(chalk.yellow(`Project ${fullProjectName} already exists.`));
|
|
385
|
+
} else {
|
|
386
|
+
console.error(chalk.red(`Failed to create project: ${err.error}`));
|
|
387
|
+
process.exit(1);
|
|
388
|
+
}
|
|
389
|
+
} else {
|
|
390
|
+
console.log(chalk.green(`Created project ${fullProjectName}`));
|
|
358
391
|
}
|
|
359
|
-
});
|
|
360
392
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
393
|
+
// Create or fetch API key
|
|
394
|
+
console.log(chalk.dim("Creating API key..."));
|
|
395
|
+
const keyRes = await fetch(`${config.baseUrl}/auth/${fullProjectName}/api-keys`, {
|
|
396
|
+
method: "POST",
|
|
397
|
+
headers: {
|
|
398
|
+
Authorization: `Bearer ${config.accountKey}`,
|
|
399
|
+
"Content-Type": "application/json",
|
|
400
|
+
},
|
|
401
|
+
body: JSON.stringify({ name: "CLI" }),
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
if (!keyRes.ok) {
|
|
405
|
+
const err = (await keyRes.json()) as { error?: string };
|
|
406
|
+
console.error(chalk.red(`Failed to create API key: ${err.error}`));
|
|
407
|
+
process.exit(1);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const keyData = (await keyRes.json()) as { api_key: string };
|
|
411
|
+
|
|
412
|
+
// Save profile
|
|
413
|
+
config.profiles[name] = { apiKey: keyData.api_key };
|
|
414
|
+
if (cmdOpts.default || !config.default) {
|
|
415
|
+
config.default = name;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
saveConfig(config);
|
|
419
|
+
console.log(chalk.green(`\nProfile '${name}' saved.`));
|
|
420
|
+
if (config.default === name) {
|
|
421
|
+
console.log(chalk.dim("Set as default profile."));
|
|
422
|
+
}
|
|
423
|
+
if (projectExists) {
|
|
424
|
+
console.log(chalk.dim("Note: A new API key was created for the existing project."));
|
|
425
|
+
}
|
|
369
426
|
});
|
|
370
427
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
428
|
+
// =============================================================================
|
|
429
|
+
// Status Command
|
|
430
|
+
// =============================================================================
|
|
431
|
+
|
|
432
|
+
program
|
|
433
|
+
.command("status")
|
|
434
|
+
.description("Show configuration and profiles")
|
|
435
|
+
.action(() => {
|
|
376
436
|
const opts = program.opts<GlobalOptions>();
|
|
377
|
-
const
|
|
378
|
-
|
|
437
|
+
const config = loadConfig();
|
|
438
|
+
if (!config) {
|
|
439
|
+
console.log(chalk.dim("No config found. Run 'vtriv config' first."));
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (opts.json) {
|
|
444
|
+
// Don't expose the actual keys
|
|
445
|
+
const safeConfig = {
|
|
446
|
+
baseUrl: config.baseUrl,
|
|
447
|
+
accountSlug: config.accountSlug,
|
|
448
|
+
hasAccountKey: !!config.accountKey,
|
|
449
|
+
default: config.default,
|
|
450
|
+
profiles: Object.keys(config.profiles),
|
|
451
|
+
};
|
|
452
|
+
console.log(JSON.stringify(safeConfig, null, 2));
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
console.log(chalk.bold("Configuration"));
|
|
457
|
+
console.log(` Base URL: ${config.baseUrl}`);
|
|
458
|
+
console.log(` Account: ${config.accountSlug || chalk.dim("(not configured)")}`);
|
|
459
|
+
console.log(` Default: ${config.default || chalk.dim("(none)")}`);
|
|
460
|
+
console.log();
|
|
461
|
+
|
|
462
|
+
if (Object.keys(config.profiles).length === 0) {
|
|
463
|
+
console.log(chalk.dim("No profiles. Run 'vtriv init <name>' to create one."));
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
console.log(chalk.bold("Profiles"));
|
|
468
|
+
for (const [name] of Object.entries(config.profiles)) {
|
|
469
|
+
const fullName = config.accountSlug ? `${config.accountSlug}-${name}` : name;
|
|
470
|
+
const isDefault = name === config.default ? chalk.green(" (default)") : "";
|
|
471
|
+
console.log(` ${name}${isDefault}`);
|
|
472
|
+
console.log(chalk.dim(` → ${fullName}`));
|
|
473
|
+
}
|
|
379
474
|
});
|
|
380
475
|
|
|
381
|
-
//
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
476
|
+
// =============================================================================
|
|
477
|
+
// Project Commands
|
|
478
|
+
// =============================================================================
|
|
479
|
+
|
|
480
|
+
const project = program.command("project").description("Project management");
|
|
481
|
+
|
|
482
|
+
project
|
|
483
|
+
.command("ls")
|
|
484
|
+
.description("List projects")
|
|
385
485
|
.action(async () => {
|
|
386
486
|
const opts = program.opts<GlobalOptions>();
|
|
387
|
-
const
|
|
388
|
-
output(data.projects, opts);
|
|
389
|
-
});
|
|
487
|
+
const config = getConfig();
|
|
390
488
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
.action(async (name: string) => {
|
|
396
|
-
const opts = program.opts<GlobalOptions>();
|
|
397
|
-
const data = await request("auth", "/projects", { method: "POST", body: { name } }, opts);
|
|
398
|
-
output(data, opts);
|
|
399
|
-
});
|
|
489
|
+
if (!config.accountKey) {
|
|
490
|
+
console.error(chalk.red("No account configured. Run 'vtriv config' first."));
|
|
491
|
+
process.exit(1);
|
|
492
|
+
}
|
|
400
493
|
|
|
401
|
-
auth
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
494
|
+
const res = await fetch(`${config.baseUrl}/auth/projects`, {
|
|
495
|
+
headers: { Authorization: `Bearer ${config.accountKey}` },
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
if (!res.ok) {
|
|
499
|
+
const err = (await res.json()) as { error?: string };
|
|
500
|
+
error(err.error || "Failed to list projects", opts);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const data = (await res.json()) as { projects: Array<{ name: string; short_name: string; created_at: string }> };
|
|
504
|
+
|
|
505
|
+
if (opts.json) {
|
|
506
|
+
console.log(JSON.stringify(data.projects, null, 2));
|
|
507
|
+
} else {
|
|
508
|
+
const rows = data.projects.map((p) => ({
|
|
509
|
+
name: p.short_name,
|
|
510
|
+
full_name: p.name,
|
|
511
|
+
created: p.created_at.split("T")[0],
|
|
512
|
+
profile: config.profiles[p.short_name] ? "✓" : "",
|
|
513
|
+
}));
|
|
514
|
+
printTable(rows);
|
|
515
|
+
}
|
|
409
516
|
});
|
|
410
517
|
|
|
411
|
-
|
|
412
|
-
.command("
|
|
518
|
+
project
|
|
519
|
+
.command("rm")
|
|
413
520
|
.description("Delete a project")
|
|
414
|
-
.argument("<name>", "Project name")
|
|
521
|
+
.argument("<name>", "Project name (short name)")
|
|
415
522
|
.action(async (name: string) => {
|
|
416
523
|
const opts = program.opts<GlobalOptions>();
|
|
417
|
-
const
|
|
418
|
-
output(data, opts);
|
|
419
|
-
});
|
|
524
|
+
const config = getConfig();
|
|
420
525
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
.action(async (project: string) => {
|
|
426
|
-
const opts = program.opts<GlobalOptions>();
|
|
427
|
-
const data = (await request("auth", `/${project}/users`, {}, opts)) as { users: unknown[] };
|
|
428
|
-
output(data.users, opts);
|
|
429
|
-
});
|
|
526
|
+
if (!config.accountKey || !config.accountSlug) {
|
|
527
|
+
console.error(chalk.red("No account configured. Run 'vtriv config' first."));
|
|
528
|
+
process.exit(1);
|
|
529
|
+
}
|
|
430
530
|
|
|
431
|
-
|
|
432
|
-
.command("user:create")
|
|
433
|
-
.description("Create a user")
|
|
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
|
-
});
|
|
531
|
+
const fullName = `${config.accountSlug}-${name}`;
|
|
444
532
|
|
|
445
|
-
auth
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
533
|
+
const res = await fetch(`${config.baseUrl}/auth/projects/${fullName}`, {
|
|
534
|
+
method: "DELETE",
|
|
535
|
+
headers: { Authorization: `Bearer ${config.accountKey}` },
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
if (!res.ok) {
|
|
539
|
+
const err = (await res.json()) as { error?: string };
|
|
540
|
+
error(err.error || "Failed to delete project", opts);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Remove from local profiles
|
|
544
|
+
if (config.profiles[name]) {
|
|
545
|
+
delete config.profiles[name];
|
|
546
|
+
if (config.default === name) {
|
|
547
|
+
config.default = Object.keys(config.profiles)[0];
|
|
548
|
+
}
|
|
549
|
+
saveConfig(config);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
console.log(chalk.green(`Deleted project ${fullName}`));
|
|
454
553
|
});
|
|
455
554
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
555
|
+
// =============================================================================
|
|
556
|
+
// Token Command
|
|
557
|
+
// =============================================================================
|
|
558
|
+
|
|
559
|
+
program
|
|
560
|
+
.command("token")
|
|
561
|
+
.description("Mint and display a JWT token")
|
|
562
|
+
.option("-t, --template <name>", "Use a specific template")
|
|
563
|
+
.action(async (cmdOpts: { template?: string }) => {
|
|
461
564
|
const opts = program.opts<GlobalOptions>();
|
|
462
|
-
const token = await
|
|
565
|
+
const token = await getToken(opts, cmdOpts.template);
|
|
463
566
|
if (opts.json) {
|
|
464
567
|
console.log(JSON.stringify({ token }));
|
|
465
568
|
} else {
|
|
@@ -467,38 +570,128 @@ auth
|
|
|
467
570
|
}
|
|
468
571
|
});
|
|
469
572
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
573
|
+
// =============================================================================
|
|
574
|
+
// Template Commands
|
|
575
|
+
// =============================================================================
|
|
576
|
+
|
|
577
|
+
const template = program.command("template").description("Token template management");
|
|
578
|
+
|
|
579
|
+
template
|
|
580
|
+
.command("ls")
|
|
581
|
+
.description("List all templates")
|
|
582
|
+
.action(async () => {
|
|
475
583
|
const opts = program.opts<GlobalOptions>();
|
|
476
|
-
const
|
|
477
|
-
|
|
584
|
+
const { config, profile, fullProjectName } = getProfile(opts.profile);
|
|
585
|
+
|
|
586
|
+
const res = await fetch(`${config.baseUrl}/auth/${fullProjectName}/templates`, {
|
|
587
|
+
headers: { Authorization: `Bearer ${profile.apiKey}` },
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
if (!res.ok) {
|
|
591
|
+
const data = await res.json();
|
|
592
|
+
error((data as { error?: string }).error || "Failed to list templates", opts);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const data = (await res.json()) as { templates: Record<string, { claims: Record<string, unknown> }> };
|
|
596
|
+
|
|
597
|
+
if (opts.json) {
|
|
598
|
+
console.log(JSON.stringify(data.templates, null, 2));
|
|
599
|
+
} else {
|
|
600
|
+
const rows = Object.entries(data.templates).map(([name, tmpl]) => ({
|
|
601
|
+
name,
|
|
602
|
+
claims: JSON.stringify(tmpl.claims),
|
|
603
|
+
}));
|
|
604
|
+
printTable(rows);
|
|
605
|
+
}
|
|
478
606
|
});
|
|
479
607
|
|
|
480
|
-
|
|
481
|
-
.command("
|
|
482
|
-
.description("
|
|
483
|
-
.argument("<
|
|
484
|
-
.
|
|
485
|
-
.action(async (project: string, cmdOpts: { name?: string }) => {
|
|
608
|
+
template
|
|
609
|
+
.command("get")
|
|
610
|
+
.description("Get a specific template")
|
|
611
|
+
.argument("<name>", "Template name")
|
|
612
|
+
.action(async (name: string) => {
|
|
486
613
|
const opts = program.opts<GlobalOptions>();
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
const
|
|
614
|
+
const { config, profile, fullProjectName } = getProfile(opts.profile);
|
|
615
|
+
|
|
616
|
+
const res = await fetch(`${config.baseUrl}/auth/${fullProjectName}/templates/${name}`, {
|
|
617
|
+
headers: { Authorization: `Bearer ${profile.apiKey}` },
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
if (!res.ok) {
|
|
621
|
+
const data = await res.json();
|
|
622
|
+
error((data as { error?: string }).error || "Failed to get template", opts);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const data = await res.json();
|
|
490
626
|
output(data, opts);
|
|
491
627
|
});
|
|
492
628
|
|
|
493
|
-
|
|
494
|
-
.command("
|
|
495
|
-
.description("
|
|
496
|
-
.argument("<
|
|
497
|
-
.
|
|
498
|
-
.action(async (project: string, id: string) => {
|
|
629
|
+
template
|
|
630
|
+
.command("set")
|
|
631
|
+
.description("Create or update a template (reads JSON from stdin)")
|
|
632
|
+
.argument("<name>", "Template name")
|
|
633
|
+
.action(async (name: string) => {
|
|
499
634
|
const opts = program.opts<GlobalOptions>();
|
|
500
|
-
const
|
|
501
|
-
|
|
635
|
+
const { config, profile, fullProjectName } = getProfile(opts.profile);
|
|
636
|
+
|
|
637
|
+
// Read claims JSON from stdin
|
|
638
|
+
const chunks: Buffer[] = [];
|
|
639
|
+
for await (const chunk of Bun.stdin.stream()) {
|
|
640
|
+
chunks.push(Buffer.from(chunk));
|
|
641
|
+
}
|
|
642
|
+
const input = Buffer.concat(chunks).toString("utf-8").trim();
|
|
643
|
+
|
|
644
|
+
if (!input) {
|
|
645
|
+
error("No JSON provided on stdin. Provide claims object, e.g. {\"x-ai\": true}", opts);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
let claims: Record<string, unknown>;
|
|
649
|
+
try {
|
|
650
|
+
claims = JSON.parse(input);
|
|
651
|
+
} catch {
|
|
652
|
+
error("Invalid JSON on stdin", opts);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const res = await fetch(`${config.baseUrl}/auth/${fullProjectName}/templates/${name}`, {
|
|
656
|
+
method: "PUT",
|
|
657
|
+
headers: {
|
|
658
|
+
Authorization: `Bearer ${profile.apiKey}`,
|
|
659
|
+
"Content-Type": "application/json",
|
|
660
|
+
},
|
|
661
|
+
body: JSON.stringify({ claims }),
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
if (!res.ok) {
|
|
665
|
+
const data = await res.json();
|
|
666
|
+
error((data as { error?: string }).error || "Failed to set template", opts);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
console.log(chalk.green(`Template '${name}' saved.`));
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
template
|
|
673
|
+
.command("rm")
|
|
674
|
+
.description("Delete a template")
|
|
675
|
+
.argument("<name>", "Template name")
|
|
676
|
+
.action(async (name: string) => {
|
|
677
|
+
const opts = program.opts<GlobalOptions>();
|
|
678
|
+
const { config, profile, fullProjectName } = getProfile(opts.profile);
|
|
679
|
+
|
|
680
|
+
if (name === "default") {
|
|
681
|
+
error("Cannot delete the default template", opts);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const res = await fetch(`${config.baseUrl}/auth/${fullProjectName}/templates/${name}`, {
|
|
685
|
+
method: "DELETE",
|
|
686
|
+
headers: { Authorization: `Bearer ${profile.apiKey}` },
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
if (!res.ok) {
|
|
690
|
+
const data = await res.json();
|
|
691
|
+
error((data as { error?: string }).error || "Failed to delete template", opts);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
console.log(chalk.green(`Template '${name}' deleted.`));
|
|
502
695
|
});
|
|
503
696
|
|
|
504
697
|
// =============================================================================
|
|
@@ -507,15 +700,13 @@ auth
|
|
|
507
700
|
|
|
508
701
|
const db = program.command("db").description("Database service commands");
|
|
509
702
|
|
|
510
|
-
db
|
|
511
|
-
.command("ls")
|
|
703
|
+
db.command("ls")
|
|
512
704
|
.description("List documents")
|
|
513
|
-
.argument("<project>", "Project name")
|
|
514
705
|
.argument("<collection>", "Collection name")
|
|
515
706
|
.option("-f, --filter <json>", "Filter criteria (JSON)")
|
|
516
707
|
.option("-l, --limit <n>", "Limit results")
|
|
517
708
|
.option("-s, --sort <field>", "Sort field (prefix with - for desc)")
|
|
518
|
-
.action(async (
|
|
709
|
+
.action(async (collection: string, cmdOpts: { filter?: string; limit?: string; sort?: string }) => {
|
|
519
710
|
const opts = program.opts<GlobalOptions>();
|
|
520
711
|
const params = new URLSearchParams();
|
|
521
712
|
if (cmdOpts.filter) {
|
|
@@ -527,81 +718,71 @@ db
|
|
|
527
718
|
if (cmdOpts.limit) params.set("_limit", cmdOpts.limit);
|
|
528
719
|
if (cmdOpts.sort) params.set("_sort", cmdOpts.sort);
|
|
529
720
|
const query = params.toString() ? `?${params.toString()}` : "";
|
|
530
|
-
const data = (await request("db", `/${
|
|
721
|
+
const data = (await request("db", `/${collection}${query}`, {}, opts)) as { data: unknown[] };
|
|
531
722
|
output(data.data, opts);
|
|
532
723
|
});
|
|
533
724
|
|
|
534
|
-
db
|
|
535
|
-
.command("get")
|
|
725
|
+
db.command("get")
|
|
536
726
|
.description("Get a document")
|
|
537
|
-
.argument("<project>", "Project name")
|
|
538
727
|
.argument("<collection>", "Collection name")
|
|
539
728
|
.argument("<id>", "Document ID")
|
|
540
|
-
.action(async (
|
|
729
|
+
.action(async (collection: string, id: string) => {
|
|
541
730
|
const opts = program.opts<GlobalOptions>();
|
|
542
|
-
const data = await request("db", `/${
|
|
731
|
+
const data = await request("db", `/${collection}/${id}`, {}, opts);
|
|
543
732
|
output(data, opts);
|
|
544
733
|
});
|
|
545
734
|
|
|
546
|
-
db
|
|
547
|
-
.command("put")
|
|
735
|
+
db.command("put")
|
|
548
736
|
.description("Create or update a document (reads JSON from stdin)")
|
|
549
|
-
.argument("<project>", "Project name")
|
|
550
737
|
.argument("<collection>", "Collection name")
|
|
551
738
|
.argument("[id]", "Document ID (for update)")
|
|
552
|
-
.action(async (
|
|
739
|
+
.action(async (collection: string, id?: string) => {
|
|
553
740
|
const opts = program.opts<GlobalOptions>();
|
|
554
|
-
|
|
741
|
+
|
|
555
742
|
// Read from stdin
|
|
556
743
|
const chunks: Buffer[] = [];
|
|
557
744
|
for await (const chunk of Bun.stdin.stream()) {
|
|
558
745
|
chunks.push(Buffer.from(chunk));
|
|
559
746
|
}
|
|
560
747
|
const input = Buffer.concat(chunks).toString("utf-8").trim();
|
|
561
|
-
|
|
748
|
+
|
|
562
749
|
if (!input) {
|
|
563
750
|
error("No JSON provided on stdin", opts);
|
|
564
751
|
}
|
|
565
|
-
|
|
752
|
+
|
|
566
753
|
let body: unknown;
|
|
567
754
|
try {
|
|
568
755
|
body = JSON.parse(input);
|
|
569
756
|
} catch {
|
|
570
757
|
error("Invalid JSON on stdin", opts);
|
|
571
758
|
}
|
|
572
|
-
|
|
759
|
+
|
|
573
760
|
if (id) {
|
|
574
|
-
|
|
575
|
-
const data = await request("db", `/${project}/${collection}/${id}`, { method: "PUT", body, project }, opts);
|
|
761
|
+
const data = await request("db", `/${collection}/${id}`, { method: "PUT", body }, opts);
|
|
576
762
|
output(data, opts);
|
|
577
763
|
} else {
|
|
578
|
-
|
|
579
|
-
const data = await request("db", `/${project}/${collection}`, { method: "POST", body, project }, opts);
|
|
764
|
+
const data = await request("db", `/${collection}`, { method: "POST", body }, opts);
|
|
580
765
|
output(data, opts);
|
|
581
766
|
}
|
|
582
767
|
});
|
|
583
768
|
|
|
584
|
-
db
|
|
585
|
-
.command("rm")
|
|
769
|
+
db.command("rm")
|
|
586
770
|
.description("Delete a document")
|
|
587
|
-
.argument("<project>", "Project name")
|
|
588
771
|
.argument("<collection>", "Collection name")
|
|
589
772
|
.argument("<id>", "Document ID")
|
|
590
|
-
.action(async (
|
|
773
|
+
.action(async (collection: string, id: string) => {
|
|
591
774
|
const opts = program.opts<GlobalOptions>();
|
|
592
|
-
const data = await request("db", `/${
|
|
775
|
+
const data = await request("db", `/${collection}/${id}`, { method: "DELETE" }, opts);
|
|
593
776
|
output(data, opts);
|
|
594
777
|
});
|
|
595
778
|
|
|
596
|
-
db
|
|
597
|
-
.command("schema")
|
|
779
|
+
db.command("schema")
|
|
598
780
|
.description("Get or set collection schema")
|
|
599
|
-
.argument("<project>", "Project name")
|
|
600
781
|
.argument("<collection>", "Collection name")
|
|
601
782
|
.option("--set", "Set schema (reads JSON from stdin)")
|
|
602
|
-
.action(async (
|
|
783
|
+
.action(async (collection: string, cmdOpts: { set?: boolean }) => {
|
|
603
784
|
const opts = program.opts<GlobalOptions>();
|
|
604
|
-
|
|
785
|
+
|
|
605
786
|
if (cmdOpts.set) {
|
|
606
787
|
const chunks: Buffer[] = [];
|
|
607
788
|
for await (const chunk of Bun.stdin.stream()) {
|
|
@@ -609,10 +790,10 @@ db
|
|
|
609
790
|
}
|
|
610
791
|
const input = Buffer.concat(chunks).toString("utf-8").trim();
|
|
611
792
|
const body = JSON.parse(input);
|
|
612
|
-
const data = await request("db", `/${
|
|
793
|
+
const data = await request("db", `/${collection}/_schema`, { method: "PUT", body }, opts);
|
|
613
794
|
output(data, opts);
|
|
614
795
|
} else {
|
|
615
|
-
const data = await request("db", `/${
|
|
796
|
+
const data = await request("db", `/${collection}/_schema`, {}, opts);
|
|
616
797
|
output(data, opts);
|
|
617
798
|
}
|
|
618
799
|
});
|
|
@@ -626,34 +807,33 @@ const blob = program.command("blob").description("Blob storage commands");
|
|
|
626
807
|
blob
|
|
627
808
|
.command("put")
|
|
628
809
|
.description("Upload a file")
|
|
629
|
-
.argument("<project>", "Project name")
|
|
630
810
|
.argument("<path>", "Remote path")
|
|
631
811
|
.argument("<file>", "Local file path")
|
|
632
|
-
.action(async (
|
|
812
|
+
.action(async (remotePath: string, localFile: string) => {
|
|
633
813
|
const opts = program.opts<GlobalOptions>();
|
|
634
|
-
const
|
|
635
|
-
const token = await
|
|
636
|
-
|
|
814
|
+
const { config, fullProjectName } = getProfile(opts.profile);
|
|
815
|
+
const token = await getToken(opts);
|
|
816
|
+
|
|
637
817
|
const file = Bun.file(localFile);
|
|
638
818
|
if (!(await file.exists())) {
|
|
639
819
|
error(`File not found: ${localFile}`, opts);
|
|
640
820
|
}
|
|
641
|
-
|
|
642
|
-
const url = `${
|
|
643
|
-
|
|
821
|
+
|
|
822
|
+
const url = `${config.baseUrl}/blob/${fullProjectName}/${remotePath}`;
|
|
823
|
+
|
|
644
824
|
if (opts.debug) {
|
|
645
825
|
console.error(chalk.dim(`curl -X PUT "${url}" -T "${localFile}"`));
|
|
646
826
|
}
|
|
647
|
-
|
|
827
|
+
|
|
648
828
|
const res = await fetch(url, {
|
|
649
829
|
method: "PUT",
|
|
650
830
|
headers: {
|
|
651
|
-
|
|
831
|
+
Authorization: `Bearer ${token}`,
|
|
652
832
|
"Content-Type": file.type || "application/octet-stream",
|
|
653
833
|
},
|
|
654
834
|
body: file,
|
|
655
835
|
});
|
|
656
|
-
|
|
836
|
+
|
|
657
837
|
const data = await res.json();
|
|
658
838
|
if (!res.ok) {
|
|
659
839
|
error((data as { error?: string }).error || "Upload failed", opts);
|
|
@@ -664,28 +844,27 @@ blob
|
|
|
664
844
|
blob
|
|
665
845
|
.command("get")
|
|
666
846
|
.description("Download a file (outputs to stdout)")
|
|
667
|
-
.argument("<project>", "Project name")
|
|
668
847
|
.argument("<path>", "Remote path")
|
|
669
|
-
.action(async (
|
|
848
|
+
.action(async (remotePath: string) => {
|
|
670
849
|
const opts = program.opts<GlobalOptions>();
|
|
671
|
-
const
|
|
672
|
-
const token = await
|
|
673
|
-
|
|
674
|
-
const url = `${
|
|
675
|
-
|
|
850
|
+
const { config, fullProjectName } = getProfile(opts.profile);
|
|
851
|
+
const token = await getToken(opts);
|
|
852
|
+
|
|
853
|
+
const url = `${config.baseUrl}/blob/${fullProjectName}/${remotePath}`;
|
|
854
|
+
|
|
676
855
|
if (opts.debug) {
|
|
677
856
|
console.error(chalk.dim(`curl "${url}"`));
|
|
678
857
|
}
|
|
679
|
-
|
|
858
|
+
|
|
680
859
|
const res = await fetch(url, {
|
|
681
|
-
headers: {
|
|
860
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
682
861
|
});
|
|
683
|
-
|
|
862
|
+
|
|
684
863
|
if (!res.ok) {
|
|
685
864
|
const data = await res.json();
|
|
686
865
|
error((data as { error?: string }).error || "Download failed", opts);
|
|
687
866
|
}
|
|
688
|
-
|
|
867
|
+
|
|
689
868
|
// Stream to stdout
|
|
690
869
|
const writer = Bun.stdout.writer();
|
|
691
870
|
const reader = res.body?.getReader();
|
|
@@ -702,20 +881,19 @@ blob
|
|
|
702
881
|
blob
|
|
703
882
|
.command("rm")
|
|
704
883
|
.description("Delete a file")
|
|
705
|
-
.argument("<project>", "Project name")
|
|
706
884
|
.argument("<path>", "Remote path")
|
|
707
|
-
.action(async (
|
|
885
|
+
.action(async (remotePath: string) => {
|
|
708
886
|
const opts = program.opts<GlobalOptions>();
|
|
709
|
-
const
|
|
710
|
-
const token = await
|
|
711
|
-
|
|
712
|
-
const url = `${
|
|
713
|
-
|
|
887
|
+
const { config, fullProjectName } = getProfile(opts.profile);
|
|
888
|
+
const token = await getToken(opts);
|
|
889
|
+
|
|
890
|
+
const url = `${config.baseUrl}/blob/${fullProjectName}/${remotePath}`;
|
|
891
|
+
|
|
714
892
|
const res = await fetch(url, {
|
|
715
893
|
method: "DELETE",
|
|
716
|
-
headers: {
|
|
894
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
717
895
|
});
|
|
718
|
-
|
|
896
|
+
|
|
719
897
|
const data = await res.json();
|
|
720
898
|
if (!res.ok) {
|
|
721
899
|
error((data as { error?: string }).error || "Delete failed", opts);
|
|
@@ -732,58 +910,54 @@ const cron = program.command("cron").description("Cron service commands");
|
|
|
732
910
|
cron
|
|
733
911
|
.command("ls")
|
|
734
912
|
.description("List jobs")
|
|
735
|
-
.
|
|
736
|
-
.action(async (project: string) => {
|
|
913
|
+
.action(async () => {
|
|
737
914
|
const opts = program.opts<GlobalOptions>();
|
|
738
|
-
const data = (await request("cron",
|
|
915
|
+
const data = (await request("cron", "/jobs", {}, opts)) as { jobs: unknown[] };
|
|
739
916
|
output(data.jobs, opts);
|
|
740
917
|
});
|
|
741
918
|
|
|
742
919
|
cron
|
|
743
920
|
.command("runs")
|
|
744
921
|
.description("List run history")
|
|
745
|
-
.argument("<project>", "Project name")
|
|
746
922
|
.option("-l, --limit <n>", "Limit results", "100")
|
|
747
923
|
.option("--job <id>", "Filter by job ID")
|
|
748
|
-
.action(async (
|
|
924
|
+
.action(async (cmdOpts: { limit: string; job?: string }) => {
|
|
749
925
|
const opts = program.opts<GlobalOptions>();
|
|
750
926
|
const params = new URLSearchParams({ limit: cmdOpts.limit });
|
|
751
927
|
if (cmdOpts.job) params.set("job_id", cmdOpts.job);
|
|
752
|
-
const data = (await request("cron",
|
|
928
|
+
const data = (await request("cron", `/runs?${params}`, {}, opts)) as { runs: unknown[] };
|
|
753
929
|
output(data.runs, opts);
|
|
754
930
|
});
|
|
755
931
|
|
|
756
932
|
cron
|
|
757
933
|
.command("trigger")
|
|
758
934
|
.description("Manually trigger a job")
|
|
759
|
-
.argument("<project>", "Project name")
|
|
760
935
|
.argument("<job_id>", "Job ID")
|
|
761
|
-
.action(async (
|
|
936
|
+
.action(async (jobId: string) => {
|
|
762
937
|
const opts = program.opts<GlobalOptions>();
|
|
763
|
-
const data = await request("cron",
|
|
938
|
+
const data = await request("cron", `/jobs/${jobId}/trigger`, { method: "POST" }, opts);
|
|
764
939
|
output(data, opts);
|
|
765
940
|
});
|
|
766
941
|
|
|
767
942
|
cron
|
|
768
943
|
.command("script:cat")
|
|
769
944
|
.description("Output script content")
|
|
770
|
-
.argument("<project>", "Project name")
|
|
771
945
|
.argument("<script>", "Script name")
|
|
772
|
-
.action(async (
|
|
946
|
+
.action(async (script: string) => {
|
|
773
947
|
const opts = program.opts<GlobalOptions>();
|
|
774
|
-
const
|
|
775
|
-
const token = await
|
|
776
|
-
|
|
777
|
-
const url = `${
|
|
948
|
+
const { config, fullProjectName } = getProfile(opts.profile);
|
|
949
|
+
const token = await getToken(opts);
|
|
950
|
+
|
|
951
|
+
const url = `${config.baseUrl}/cron/${fullProjectName}/scripts/${script}`;
|
|
778
952
|
const res = await fetch(url, {
|
|
779
|
-
headers: {
|
|
953
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
780
954
|
});
|
|
781
|
-
|
|
955
|
+
|
|
782
956
|
if (!res.ok) {
|
|
783
957
|
const data = await res.json();
|
|
784
958
|
error((data as { error?: string }).error || "Failed to get script", opts);
|
|
785
959
|
}
|
|
786
|
-
|
|
960
|
+
|
|
787
961
|
console.log(await res.text());
|
|
788
962
|
});
|
|
789
963
|
|
|
@@ -793,52 +967,60 @@ cron
|
|
|
793
967
|
|
|
794
968
|
const ai = program.command("ai").description("AI service commands");
|
|
795
969
|
|
|
796
|
-
ai
|
|
797
|
-
.command("stats")
|
|
970
|
+
ai.command("stats")
|
|
798
971
|
.description("View usage statistics")
|
|
799
|
-
.argument("<project>", "Project name")
|
|
800
972
|
.option("-p, --period <days>", "Period in days", "30")
|
|
801
|
-
.action(async (
|
|
973
|
+
.action(async (cmdOpts: { period: string }) => {
|
|
802
974
|
const opts = program.opts<GlobalOptions>();
|
|
803
|
-
const
|
|
804
|
-
|
|
975
|
+
const { config } = getProfile(opts.profile);
|
|
976
|
+
const token = await getToken(opts);
|
|
977
|
+
|
|
978
|
+
const url = `${config.baseUrl}/ai/usage/stats?period=${cmdOpts.period}d`;
|
|
979
|
+
const res = await fetch(url, {
|
|
980
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
if (!res.ok) {
|
|
984
|
+
const data = await res.json();
|
|
985
|
+
error((data as { error?: string }).error || "Failed to get stats", opts);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
output(await res.json(), opts);
|
|
805
989
|
});
|
|
806
990
|
|
|
807
|
-
ai
|
|
808
|
-
.command("chat")
|
|
991
|
+
ai.command("chat")
|
|
809
992
|
.description("Send a chat completion request")
|
|
810
|
-
.argument("<project>", "Project name")
|
|
811
993
|
.argument("<prompt>", "Chat prompt")
|
|
812
994
|
.option("-m, --model <model>", "Model to use (defaults to project config)")
|
|
813
|
-
.action(async (
|
|
995
|
+
.action(async (prompt: string, cmdOpts: { model?: string }) => {
|
|
814
996
|
const opts = program.opts<GlobalOptions>();
|
|
815
|
-
const
|
|
816
|
-
const token = await
|
|
817
|
-
|
|
997
|
+
const { config } = getProfile(opts.profile);
|
|
998
|
+
const token = await getToken(opts);
|
|
999
|
+
|
|
818
1000
|
// Get default model from project config if not specified
|
|
819
1001
|
let model = cmdOpts.model;
|
|
820
1002
|
if (!model) {
|
|
821
|
-
const configRes = await fetch(`${
|
|
822
|
-
headers: {
|
|
1003
|
+
const configRes = await fetch(`${config.baseUrl}/ai/config`, {
|
|
1004
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
823
1005
|
});
|
|
824
1006
|
if (configRes.ok) {
|
|
825
|
-
const
|
|
826
|
-
model =
|
|
1007
|
+
const aiConfig = (await configRes.json()) as { default_model?: string };
|
|
1008
|
+
model = aiConfig.default_model || "openai/gpt-4o-mini";
|
|
827
1009
|
} else {
|
|
828
1010
|
model = "openai/gpt-4o-mini";
|
|
829
1011
|
}
|
|
830
1012
|
}
|
|
831
|
-
|
|
832
|
-
const url = `${
|
|
833
|
-
|
|
1013
|
+
|
|
1014
|
+
const url = `${config.baseUrl}/ai/v1/chat/completions`;
|
|
1015
|
+
|
|
834
1016
|
if (opts.debug) {
|
|
835
1017
|
console.error(chalk.dim(`curl -X POST "${url}" ...`));
|
|
836
1018
|
}
|
|
837
|
-
|
|
1019
|
+
|
|
838
1020
|
const res = await fetch(url, {
|
|
839
1021
|
method: "POST",
|
|
840
1022
|
headers: {
|
|
841
|
-
|
|
1023
|
+
Authorization: `Bearer ${token}`,
|
|
842
1024
|
"Content-Type": "application/json",
|
|
843
1025
|
},
|
|
844
1026
|
body: JSON.stringify({
|
|
@@ -846,44 +1028,60 @@ ai
|
|
|
846
1028
|
messages: [{ role: "user", content: prompt }],
|
|
847
1029
|
}),
|
|
848
1030
|
});
|
|
849
|
-
|
|
1031
|
+
|
|
850
1032
|
const data = await res.json();
|
|
851
1033
|
if (!res.ok) {
|
|
852
1034
|
error((data as { error?: string }).error || "Chat failed", opts);
|
|
853
1035
|
}
|
|
854
|
-
|
|
1036
|
+
|
|
855
1037
|
if (opts.json) {
|
|
856
1038
|
console.log(JSON.stringify(data, null, 2));
|
|
857
1039
|
} else {
|
|
858
|
-
const content = (data as { choices: Array<{ message: { content: string } }> }).choices?.[0]
|
|
1040
|
+
const content = (data as { choices: Array<{ message: { content: string } }> }).choices?.[0]
|
|
1041
|
+
?.message?.content;
|
|
859
1042
|
console.log(content || "(no response)");
|
|
860
1043
|
}
|
|
861
1044
|
});
|
|
862
1045
|
|
|
863
|
-
ai
|
|
864
|
-
.
|
|
865
|
-
.description("Get or set AI config for a project")
|
|
866
|
-
.argument("<project>", "Project name")
|
|
1046
|
+
ai.command("config")
|
|
1047
|
+
.description("Get or set AI config")
|
|
867
1048
|
.option("--model <model>", "Set default model")
|
|
868
1049
|
.option("--rate-limit <usd>", "Set rate limit in USD")
|
|
869
1050
|
.option("--key <key>", "Set custom OpenRouter API key")
|
|
870
|
-
.action(async (
|
|
1051
|
+
.action(async (cmdOpts: { model?: string; rateLimit?: string; key?: string }) => {
|
|
871
1052
|
const opts = program.opts<GlobalOptions>();
|
|
872
|
-
|
|
1053
|
+
const { config } = getProfile(opts.profile);
|
|
1054
|
+
const token = await getToken(opts);
|
|
1055
|
+
|
|
873
1056
|
// If any options provided, update config
|
|
874
1057
|
if (cmdOpts.model || cmdOpts.rateLimit || cmdOpts.key) {
|
|
875
1058
|
const body: Record<string, unknown> = {};
|
|
876
1059
|
if (cmdOpts.model) body.default_model = cmdOpts.model;
|
|
877
1060
|
if (cmdOpts.rateLimit) body.rate_limit_usd = Number.parseFloat(cmdOpts.rateLimit);
|
|
878
1061
|
if (cmdOpts.key) body.key = cmdOpts.key;
|
|
879
|
-
|
|
880
|
-
await
|
|
1062
|
+
|
|
1063
|
+
const updateRes = await fetch(`${config.baseUrl}/ai/config`, {
|
|
1064
|
+
method: "PUT",
|
|
1065
|
+
headers: {
|
|
1066
|
+
Authorization: `Bearer ${token}`,
|
|
1067
|
+
"Content-Type": "application/json",
|
|
1068
|
+
},
|
|
1069
|
+
body: JSON.stringify(body),
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
if (!updateRes.ok) {
|
|
1073
|
+
const data = await updateRes.json();
|
|
1074
|
+
error((data as { error?: string }).error || "Failed to update config", opts);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
881
1077
|
console.log("Config updated");
|
|
882
1078
|
}
|
|
883
|
-
|
|
1079
|
+
|
|
884
1080
|
// Always show current config
|
|
885
|
-
const
|
|
886
|
-
|
|
1081
|
+
const res = await fetch(`${config.baseUrl}/ai/config`, {
|
|
1082
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
1083
|
+
});
|
|
1084
|
+
output(await res.json(), opts);
|
|
887
1085
|
});
|
|
888
1086
|
|
|
889
1087
|
// =============================================================================
|
|
@@ -895,12 +1093,11 @@ const search = program.command("search").description("Search service commands");
|
|
|
895
1093
|
search
|
|
896
1094
|
.command("config")
|
|
897
1095
|
.description("Get or set index configuration")
|
|
898
|
-
.argument("<project>", "Project name")
|
|
899
1096
|
.argument("<index>", "Index name")
|
|
900
1097
|
.option("--set", "Set config (reads JSON from stdin)")
|
|
901
|
-
.action(async (
|
|
1098
|
+
.action(async (index: string, cmdOpts: { set?: boolean }) => {
|
|
902
1099
|
const opts = program.opts<GlobalOptions>();
|
|
903
|
-
|
|
1100
|
+
|
|
904
1101
|
if (cmdOpts.set) {
|
|
905
1102
|
const chunks: Buffer[] = [];
|
|
906
1103
|
for await (const chunk of Bun.stdin.stream()) {
|
|
@@ -908,10 +1105,10 @@ search
|
|
|
908
1105
|
}
|
|
909
1106
|
const input = Buffer.concat(chunks).toString("utf-8").trim();
|
|
910
1107
|
const body = JSON.parse(input);
|
|
911
|
-
const data = await request("search", `/${
|
|
1108
|
+
const data = await request("search", `/${index}/_config`, { method: "PUT", body }, opts);
|
|
912
1109
|
output(data, opts);
|
|
913
1110
|
} else {
|
|
914
|
-
const data = await request("search", `/${
|
|
1111
|
+
const data = await request("search", `/${index}/_config`, {}, opts);
|
|
915
1112
|
output(data, opts);
|
|
916
1113
|
}
|
|
917
1114
|
});
|
|
@@ -919,14 +1116,15 @@ search
|
|
|
919
1116
|
search
|
|
920
1117
|
.command("query")
|
|
921
1118
|
.description("Search an index")
|
|
922
|
-
.argument("<project>", "Project name")
|
|
923
1119
|
.argument("<index>", "Index name")
|
|
924
1120
|
.argument("<q>", "Search query")
|
|
925
1121
|
.option("-l, --limit <n>", "Limit results", "10")
|
|
926
|
-
.action(async (
|
|
1122
|
+
.action(async (index: string, q: string, cmdOpts: { limit: string }) => {
|
|
927
1123
|
const opts = program.opts<GlobalOptions>();
|
|
928
1124
|
const params = new URLSearchParams({ q, limit: cmdOpts.limit });
|
|
929
|
-
const data = (await request("search", `/${
|
|
1125
|
+
const data = (await request("search", `/${index}/search?${params}`, {}, opts)) as {
|
|
1126
|
+
results: unknown[];
|
|
1127
|
+
};
|
|
930
1128
|
output(data.results, opts);
|
|
931
1129
|
});
|
|
932
1130
|
|