@vtriv/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -0
- package/index.ts +937 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# vtriv CLI
|
|
2
|
+
|
|
3
|
+
Command-line interface for vtriv backend services.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Install dependencies
|
|
9
|
+
bun install
|
|
10
|
+
|
|
11
|
+
# Run directly
|
|
12
|
+
bun run index.ts --help
|
|
13
|
+
|
|
14
|
+
# Or link globally
|
|
15
|
+
bun link
|
|
16
|
+
vtriv --help
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Setup
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Initialize configuration
|
|
23
|
+
vtriv init
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
This creates `~/.vtrivrc` with your API gateway URL and bootstrap key.
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# List projects
|
|
32
|
+
vtriv auth project:list
|
|
33
|
+
|
|
34
|
+
# Create a project
|
|
35
|
+
vtriv auth project:create my-app
|
|
36
|
+
|
|
37
|
+
# Create a user
|
|
38
|
+
vtriv auth user:create my-app admin@example.com secretpass
|
|
39
|
+
|
|
40
|
+
# List documents
|
|
41
|
+
vtriv db ls my-app posts
|
|
42
|
+
|
|
43
|
+
# Create a document
|
|
44
|
+
echo '{"title":"Hello"}' | vtriv db put my-app posts
|
|
45
|
+
|
|
46
|
+
# Search
|
|
47
|
+
vtriv search query my-app posts "hello world"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Commands
|
|
51
|
+
|
|
52
|
+
- `vtriv init` - Setup configuration
|
|
53
|
+
- `vtriv auth` - Auth service (projects, users, tokens, API keys)
|
|
54
|
+
- `vtriv db` - Database service (ls, get, put, rm, schema)
|
|
55
|
+
- `vtriv blob` - Blob storage (put, get, rm)
|
|
56
|
+
- `vtriv cron` - Cron jobs (ls, runs, trigger, script:cat)
|
|
57
|
+
- `vtriv ai` - AI service (stats, chat)
|
|
58
|
+
- `vtriv search` - Search service (config, query)
|
|
59
|
+
|
|
60
|
+
## Options
|
|
61
|
+
|
|
62
|
+
- `--json` - Output raw JSON for scripts/agents
|
|
63
|
+
- `--profile <name>` - Use a specific profile
|
|
64
|
+
- `--debug` - Show HTTP curl equivalents
|
|
65
|
+
|
|
66
|
+
## Documentation
|
|
67
|
+
|
|
68
|
+
See [~/grit/skills/vtriv/cli.md](../../grit/skills/vtriv/cli.md) for full documentation.
|
package/index.ts
ADDED
|
@@ -0,0 +1,937 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* vtriv-cli - Command-line interface for vtriv services
|
|
4
|
+
*
|
|
5
|
+
* Gateway-first: single baseUrl with service routing by path prefix
|
|
6
|
+
* Auto-mints JWTs for non-auth services using root key
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { Command } from "commander";
|
|
13
|
+
import chalk from "chalk";
|
|
14
|
+
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// Types
|
|
17
|
+
// =============================================================================
|
|
18
|
+
|
|
19
|
+
interface Profile {
|
|
20
|
+
baseUrl: string;
|
|
21
|
+
rootKey: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface Config {
|
|
25
|
+
default: string;
|
|
26
|
+
profiles: Record<string, Profile>;
|
|
27
|
+
apiKeys?: Record<string, string>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface GlobalOptions {
|
|
31
|
+
json?: boolean;
|
|
32
|
+
profile?: string;
|
|
33
|
+
debug?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// =============================================================================
|
|
37
|
+
// Configuration
|
|
38
|
+
// =============================================================================
|
|
39
|
+
|
|
40
|
+
const CONFIG_PATH = join(homedir(), ".vtrivrc");
|
|
41
|
+
|
|
42
|
+
function loadConfig(): Config | null {
|
|
43
|
+
if (!existsSync(CONFIG_PATH)) return null;
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function saveConfig(config: Config): void {
|
|
52
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getProfile(profileName?: string): Profile {
|
|
56
|
+
const config = loadConfig();
|
|
57
|
+
if (!config) {
|
|
58
|
+
console.error(chalk.red("No config found. Run 'vtriv init' first."));
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
const name = profileName || config.default;
|
|
62
|
+
const profile = config.profiles[name];
|
|
63
|
+
if (!profile) {
|
|
64
|
+
console.error(chalk.red(`Profile '${name}' not found.`));
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
return profile;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// =============================================================================
|
|
71
|
+
// Output Helpers
|
|
72
|
+
// =============================================================================
|
|
73
|
+
|
|
74
|
+
function output(data: unknown, opts: GlobalOptions): void {
|
|
75
|
+
if (opts.json) {
|
|
76
|
+
console.log(JSON.stringify(data, null, 2));
|
|
77
|
+
} else if (Array.isArray(data)) {
|
|
78
|
+
printTable(data);
|
|
79
|
+
} else if (typeof data === "object" && data !== null) {
|
|
80
|
+
for (const [key, value] of Object.entries(data)) {
|
|
81
|
+
console.log(`${chalk.cyan(key)}: ${JSON.stringify(value)}`);
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
console.log(data);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function printTable(rows: Record<string, unknown>[]): void {
|
|
89
|
+
if (rows.length === 0) {
|
|
90
|
+
console.log(chalk.dim("(no results)"));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const keys = Object.keys(rows[0] ?? {});
|
|
94
|
+
const widths = keys.map((k) =>
|
|
95
|
+
Math.max(k.length, ...rows.map((r) => String(r[k] ?? "").length))
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// Header
|
|
99
|
+
const header = keys.map((k, i) => k.padEnd(widths[i] ?? 0)).join(" ");
|
|
100
|
+
console.log(chalk.bold(header));
|
|
101
|
+
console.log(chalk.dim("-".repeat(header.length)));
|
|
102
|
+
|
|
103
|
+
// Rows
|
|
104
|
+
for (const row of rows) {
|
|
105
|
+
const line = keys.map((k, i) => String(row[k] ?? "").padEnd(widths[i] ?? 0)).join(" ");
|
|
106
|
+
console.log(line);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function error(msg: string, opts: GlobalOptions): void {
|
|
111
|
+
if (opts.json) {
|
|
112
|
+
console.error(JSON.stringify({ error: msg }));
|
|
113
|
+
} else {
|
|
114
|
+
console.error(chalk.red(`Error: ${msg}`));
|
|
115
|
+
}
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// =============================================================================
|
|
120
|
+
// HTTP Request Helper
|
|
121
|
+
// =============================================================================
|
|
122
|
+
|
|
123
|
+
// Cache for minted tokens per project
|
|
124
|
+
const tokenCache = new Map<string, { token: string; expires: number }>();
|
|
125
|
+
|
|
126
|
+
async function request(
|
|
127
|
+
service: string,
|
|
128
|
+
path: string,
|
|
129
|
+
options: {
|
|
130
|
+
method?: string;
|
|
131
|
+
body?: unknown;
|
|
132
|
+
project?: string;
|
|
133
|
+
},
|
|
134
|
+
opts: GlobalOptions
|
|
135
|
+
): Promise<unknown> {
|
|
136
|
+
const profile = getProfile(opts.profile);
|
|
137
|
+
const url = `${profile.baseUrl}/${service}${path}`;
|
|
138
|
+
const method = options.method || "GET";
|
|
139
|
+
|
|
140
|
+
const headers: Record<string, string> = {
|
|
141
|
+
"Content-Type": "application/json",
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Auth service uses root key via Bearer header
|
|
145
|
+
if (service === "auth") {
|
|
146
|
+
headers["Authorization"] = `Bearer ${profile.rootKey}`;
|
|
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
|
+
}
|
|
152
|
+
|
|
153
|
+
if (opts.debug) {
|
|
154
|
+
const bodyArg = options.body ? `-d '${JSON.stringify(options.body)}'` : "";
|
|
155
|
+
const authArg = headers["Authorization"]
|
|
156
|
+
? `-H "Authorization: ${headers["Authorization"]}"`
|
|
157
|
+
: "";
|
|
158
|
+
console.error(chalk.dim(`curl -X ${method} "${url}" ${authArg} ${bodyArg}`));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const res = await fetch(url, {
|
|
162
|
+
method,
|
|
163
|
+
headers,
|
|
164
|
+
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const text = await res.text();
|
|
168
|
+
let data: unknown;
|
|
169
|
+
try {
|
|
170
|
+
data = JSON.parse(text);
|
|
171
|
+
} catch {
|
|
172
|
+
data = text;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!res.ok) {
|
|
176
|
+
const errMsg = typeof data === "object" && data !== null && "error" in data
|
|
177
|
+
? (data as { error: string }).error
|
|
178
|
+
: text;
|
|
179
|
+
error(errMsg, opts);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return data;
|
|
183
|
+
}
|
|
184
|
+
|
|
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
|
+
// =============================================================================
|
|
266
|
+
// Program
|
|
267
|
+
// =============================================================================
|
|
268
|
+
|
|
269
|
+
const program = new Command();
|
|
270
|
+
|
|
271
|
+
program
|
|
272
|
+
.name("vtriv")
|
|
273
|
+
.description("CLI for vtriv backend services")
|
|
274
|
+
.version("0.1.0")
|
|
275
|
+
.option("--json", "Output raw JSON")
|
|
276
|
+
.option("--profile <name>", "Use specific profile")
|
|
277
|
+
.option("--debug", "Show HTTP requests");
|
|
278
|
+
|
|
279
|
+
// =============================================================================
|
|
280
|
+
// Init Command
|
|
281
|
+
// =============================================================================
|
|
282
|
+
|
|
283
|
+
program
|
|
284
|
+
.command("init")
|
|
285
|
+
.description("Initialize CLI configuration")
|
|
286
|
+
.action(async () => {
|
|
287
|
+
const existing = loadConfig();
|
|
288
|
+
|
|
289
|
+
console.log(chalk.bold("vtriv CLI Setup\n"));
|
|
290
|
+
|
|
291
|
+
const readline = await import("node:readline");
|
|
292
|
+
const rl = readline.createInterface({
|
|
293
|
+
input: process.stdin,
|
|
294
|
+
output: process.stdout,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const question = (q: string): Promise<string> =>
|
|
298
|
+
new Promise((resolve) => rl.question(q, resolve));
|
|
299
|
+
|
|
300
|
+
const baseUrl = await question(
|
|
301
|
+
`Base URL [${existing?.profiles?.prod?.baseUrl || "https://api.vtriv.com"}]: `
|
|
302
|
+
) || existing?.profiles?.prod?.baseUrl || "https://api.vtriv.com";
|
|
303
|
+
|
|
304
|
+
const rootKey = await question("Root Key (vtriv_rk_...): ");
|
|
305
|
+
|
|
306
|
+
if (!rootKey) {
|
|
307
|
+
console.error(chalk.red("Root key is required."));
|
|
308
|
+
rl.close();
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (!rootKey.startsWith("vtriv_rk_")) {
|
|
313
|
+
console.error(chalk.red("Root key must start with 'vtriv_rk_'."));
|
|
314
|
+
rl.close();
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const config: Config = {
|
|
319
|
+
default: "prod",
|
|
320
|
+
profiles: {
|
|
321
|
+
prod: { baseUrl, rootKey },
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
saveConfig(config);
|
|
326
|
+
console.log(chalk.green(`\nConfig saved to ${CONFIG_PATH}`));
|
|
327
|
+
rl.close();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// =============================================================================
|
|
331
|
+
// Auth Commands
|
|
332
|
+
// =============================================================================
|
|
333
|
+
|
|
334
|
+
const auth = program.command("auth").description("Auth service commands");
|
|
335
|
+
|
|
336
|
+
// Account commands (root key only)
|
|
337
|
+
auth
|
|
338
|
+
.command("account:list")
|
|
339
|
+
.description("List all accounts")
|
|
340
|
+
.action(async () => {
|
|
341
|
+
const opts = program.opts<GlobalOptions>();
|
|
342
|
+
const data = (await request("auth", "/accounts", {}, opts)) as { accounts: unknown[] };
|
|
343
|
+
output(data.accounts, opts);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
auth
|
|
347
|
+
.command("account:create")
|
|
348
|
+
.description("Create a new account")
|
|
349
|
+
.option("--name <name>", "Account name")
|
|
350
|
+
.action(async (cmdOpts: { name?: string }) => {
|
|
351
|
+
const opts = program.opts<GlobalOptions>();
|
|
352
|
+
const body: Record<string, unknown> = {};
|
|
353
|
+
if (cmdOpts.name) body.name = cmdOpts.name;
|
|
354
|
+
const data = await request("auth", "/accounts", { method: "POST", body }, opts);
|
|
355
|
+
output(data, opts);
|
|
356
|
+
if (!opts.json) {
|
|
357
|
+
console.log(chalk.yellow("\nIMPORTANT: Save the account_key above - it cannot be retrieved later!"));
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
auth
|
|
362
|
+
.command("account:get")
|
|
363
|
+
.description("Get account details")
|
|
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
|
+
});
|
|
370
|
+
|
|
371
|
+
auth
|
|
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
|
+
});
|
|
380
|
+
|
|
381
|
+
// Project commands
|
|
382
|
+
auth
|
|
383
|
+
.command("project:list")
|
|
384
|
+
.description("List all projects")
|
|
385
|
+
.action(async () => {
|
|
386
|
+
const opts = program.opts<GlobalOptions>();
|
|
387
|
+
const data = (await request("auth", "/projects", {}, opts)) as { projects: unknown[] };
|
|
388
|
+
output(data.projects, opts);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
auth
|
|
392
|
+
.command("project:create")
|
|
393
|
+
.description("Create a new project")
|
|
394
|
+
.argument("<name>", "Project name")
|
|
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
|
+
});
|
|
400
|
+
|
|
401
|
+
auth
|
|
402
|
+
.command("project:get")
|
|
403
|
+
.description("Get project details")
|
|
404
|
+
.argument("<name>", "Project name")
|
|
405
|
+
.action(async (name: string) => {
|
|
406
|
+
const opts = program.opts<GlobalOptions>();
|
|
407
|
+
const data = await request("auth", `/projects/${name}`, {}, opts);
|
|
408
|
+
output(data, opts);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
auth
|
|
412
|
+
.command("project:delete")
|
|
413
|
+
.description("Delete a project")
|
|
414
|
+
.argument("<name>", "Project name")
|
|
415
|
+
.action(async (name: string) => {
|
|
416
|
+
const opts = program.opts<GlobalOptions>();
|
|
417
|
+
const data = await request("auth", `/projects/${name}`, { method: "DELETE" }, opts);
|
|
418
|
+
output(data, opts);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
auth
|
|
422
|
+
.command("user:list")
|
|
423
|
+
.description("List users in a project")
|
|
424
|
+
.argument("<project>", "Project name")
|
|
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
|
+
});
|
|
430
|
+
|
|
431
|
+
auth
|
|
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
|
+
});
|
|
444
|
+
|
|
445
|
+
auth
|
|
446
|
+
.command("user:delete")
|
|
447
|
+
.description("Delete a user")
|
|
448
|
+
.argument("<project>", "Project name")
|
|
449
|
+
.argument("<id>", "User ID")
|
|
450
|
+
.action(async (project: string, id: string) => {
|
|
451
|
+
const opts = program.opts<GlobalOptions>();
|
|
452
|
+
const data = await request("auth", `/${project}/users/${id}`, { method: "DELETE" }, opts);
|
|
453
|
+
output(data, opts);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
auth
|
|
457
|
+
.command("token:mint")
|
|
458
|
+
.description("Mint a JWT token for a project")
|
|
459
|
+
.argument("<project>", "Project name")
|
|
460
|
+
.action(async (project: string) => {
|
|
461
|
+
const opts = program.opts<GlobalOptions>();
|
|
462
|
+
const token = await getProjectToken(project, opts);
|
|
463
|
+
if (opts.json) {
|
|
464
|
+
console.log(JSON.stringify({ token }));
|
|
465
|
+
} else {
|
|
466
|
+
console.log(token);
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
auth
|
|
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);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
auth
|
|
481
|
+
.command("apikey:create")
|
|
482
|
+
.description("Create an API key")
|
|
483
|
+
.argument("<project>", "Project name")
|
|
484
|
+
.option("--name <name>", "Key name")
|
|
485
|
+
.action(async (project: string, cmdOpts: { name?: string }) => {
|
|
486
|
+
const opts = program.opts<GlobalOptions>();
|
|
487
|
+
const body: Record<string, unknown> = {};
|
|
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
|
+
});
|
|
492
|
+
|
|
493
|
+
auth
|
|
494
|
+
.command("apikey:delete")
|
|
495
|
+
.description("Delete an API key")
|
|
496
|
+
.argument("<project>", "Project name")
|
|
497
|
+
.argument("<id>", "API key ID")
|
|
498
|
+
.action(async (project: string, id: string) => {
|
|
499
|
+
const opts = program.opts<GlobalOptions>();
|
|
500
|
+
const data = await request("auth", `/${project}/api-keys/${id}`, { method: "DELETE" }, opts);
|
|
501
|
+
output(data, opts);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// =============================================================================
|
|
505
|
+
// DB Commands
|
|
506
|
+
// =============================================================================
|
|
507
|
+
|
|
508
|
+
const db = program.command("db").description("Database service commands");
|
|
509
|
+
|
|
510
|
+
db
|
|
511
|
+
.command("ls")
|
|
512
|
+
.description("List documents")
|
|
513
|
+
.argument("<project>", "Project name")
|
|
514
|
+
.argument("<collection>", "Collection name")
|
|
515
|
+
.option("-f, --filter <json>", "Filter criteria (JSON)")
|
|
516
|
+
.option("-l, --limit <n>", "Limit results")
|
|
517
|
+
.option("-s, --sort <field>", "Sort field (prefix with - for desc)")
|
|
518
|
+
.action(async (project: string, collection: string, cmdOpts: { filter?: string; limit?: string; sort?: string }) => {
|
|
519
|
+
const opts = program.opts<GlobalOptions>();
|
|
520
|
+
const params = new URLSearchParams();
|
|
521
|
+
if (cmdOpts.filter) {
|
|
522
|
+
const filter = JSON.parse(cmdOpts.filter);
|
|
523
|
+
for (const [k, v] of Object.entries(filter)) {
|
|
524
|
+
params.set(k, String(v));
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (cmdOpts.limit) params.set("_limit", cmdOpts.limit);
|
|
528
|
+
if (cmdOpts.sort) params.set("_sort", cmdOpts.sort);
|
|
529
|
+
const query = params.toString() ? `?${params.toString()}` : "";
|
|
530
|
+
const data = (await request("db", `/${project}/${collection}${query}`, { project }, opts)) as { data: unknown[] };
|
|
531
|
+
output(data.data, opts);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
db
|
|
535
|
+
.command("get")
|
|
536
|
+
.description("Get a document")
|
|
537
|
+
.argument("<project>", "Project name")
|
|
538
|
+
.argument("<collection>", "Collection name")
|
|
539
|
+
.argument("<id>", "Document ID")
|
|
540
|
+
.action(async (project: string, collection: string, id: string) => {
|
|
541
|
+
const opts = program.opts<GlobalOptions>();
|
|
542
|
+
const data = await request("db", `/${project}/${collection}/${id}`, { project }, opts);
|
|
543
|
+
output(data, opts);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
db
|
|
547
|
+
.command("put")
|
|
548
|
+
.description("Create or update a document (reads JSON from stdin)")
|
|
549
|
+
.argument("<project>", "Project name")
|
|
550
|
+
.argument("<collection>", "Collection name")
|
|
551
|
+
.argument("[id]", "Document ID (for update)")
|
|
552
|
+
.action(async (project: string, collection: string, id?: string) => {
|
|
553
|
+
const opts = program.opts<GlobalOptions>();
|
|
554
|
+
|
|
555
|
+
// Read from stdin
|
|
556
|
+
const chunks: Buffer[] = [];
|
|
557
|
+
for await (const chunk of Bun.stdin.stream()) {
|
|
558
|
+
chunks.push(Buffer.from(chunk));
|
|
559
|
+
}
|
|
560
|
+
const input = Buffer.concat(chunks).toString("utf-8").trim();
|
|
561
|
+
|
|
562
|
+
if (!input) {
|
|
563
|
+
error("No JSON provided on stdin", opts);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
let body: unknown;
|
|
567
|
+
try {
|
|
568
|
+
body = JSON.parse(input);
|
|
569
|
+
} catch {
|
|
570
|
+
error("Invalid JSON on stdin", opts);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (id) {
|
|
574
|
+
// Update (PUT)
|
|
575
|
+
const data = await request("db", `/${project}/${collection}/${id}`, { method: "PUT", body, project }, opts);
|
|
576
|
+
output(data, opts);
|
|
577
|
+
} else {
|
|
578
|
+
// Create (POST)
|
|
579
|
+
const data = await request("db", `/${project}/${collection}`, { method: "POST", body, project }, opts);
|
|
580
|
+
output(data, opts);
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
db
|
|
585
|
+
.command("rm")
|
|
586
|
+
.description("Delete a document")
|
|
587
|
+
.argument("<project>", "Project name")
|
|
588
|
+
.argument("<collection>", "Collection name")
|
|
589
|
+
.argument("<id>", "Document ID")
|
|
590
|
+
.action(async (project: string, collection: string, id: string) => {
|
|
591
|
+
const opts = program.opts<GlobalOptions>();
|
|
592
|
+
const data = await request("db", `/${project}/${collection}/${id}`, { method: "DELETE", project }, opts);
|
|
593
|
+
output(data, opts);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
db
|
|
597
|
+
.command("schema")
|
|
598
|
+
.description("Get or set collection schema")
|
|
599
|
+
.argument("<project>", "Project name")
|
|
600
|
+
.argument("<collection>", "Collection name")
|
|
601
|
+
.option("--set", "Set schema (reads JSON from stdin)")
|
|
602
|
+
.action(async (project: string, collection: string, cmdOpts: { set?: boolean }) => {
|
|
603
|
+
const opts = program.opts<GlobalOptions>();
|
|
604
|
+
|
|
605
|
+
if (cmdOpts.set) {
|
|
606
|
+
const chunks: Buffer[] = [];
|
|
607
|
+
for await (const chunk of Bun.stdin.stream()) {
|
|
608
|
+
chunks.push(Buffer.from(chunk));
|
|
609
|
+
}
|
|
610
|
+
const input = Buffer.concat(chunks).toString("utf-8").trim();
|
|
611
|
+
const body = JSON.parse(input);
|
|
612
|
+
const data = await request("db", `/${project}/${collection}/_schema`, { method: "PUT", body, project }, opts);
|
|
613
|
+
output(data, opts);
|
|
614
|
+
} else {
|
|
615
|
+
const data = await request("db", `/${project}/${collection}/_schema`, { project }, opts);
|
|
616
|
+
output(data, opts);
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
// =============================================================================
|
|
621
|
+
// Blob Commands
|
|
622
|
+
// =============================================================================
|
|
623
|
+
|
|
624
|
+
const blob = program.command("blob").description("Blob storage commands");
|
|
625
|
+
|
|
626
|
+
blob
|
|
627
|
+
.command("put")
|
|
628
|
+
.description("Upload a file")
|
|
629
|
+
.argument("<project>", "Project name")
|
|
630
|
+
.argument("<path>", "Remote path")
|
|
631
|
+
.argument("<file>", "Local file path")
|
|
632
|
+
.action(async (project: string, remotePath: string, localFile: string) => {
|
|
633
|
+
const opts = program.opts<GlobalOptions>();
|
|
634
|
+
const profile = getProfile(opts.profile);
|
|
635
|
+
const token = await getProjectToken(project, opts);
|
|
636
|
+
|
|
637
|
+
const file = Bun.file(localFile);
|
|
638
|
+
if (!(await file.exists())) {
|
|
639
|
+
error(`File not found: ${localFile}`, opts);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const url = `${profile.baseUrl}/blob/${project}/${remotePath}`;
|
|
643
|
+
|
|
644
|
+
if (opts.debug) {
|
|
645
|
+
console.error(chalk.dim(`curl -X PUT "${url}" -T "${localFile}"`));
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const res = await fetch(url, {
|
|
649
|
+
method: "PUT",
|
|
650
|
+
headers: {
|
|
651
|
+
"Authorization": `Bearer ${token}`,
|
|
652
|
+
"Content-Type": file.type || "application/octet-stream",
|
|
653
|
+
},
|
|
654
|
+
body: file,
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
const data = await res.json();
|
|
658
|
+
if (!res.ok) {
|
|
659
|
+
error((data as { error?: string }).error || "Upload failed", opts);
|
|
660
|
+
}
|
|
661
|
+
output(data, opts);
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
blob
|
|
665
|
+
.command("get")
|
|
666
|
+
.description("Download a file (outputs to stdout)")
|
|
667
|
+
.argument("<project>", "Project name")
|
|
668
|
+
.argument("<path>", "Remote path")
|
|
669
|
+
.action(async (project: string, remotePath: string) => {
|
|
670
|
+
const opts = program.opts<GlobalOptions>();
|
|
671
|
+
const profile = getProfile(opts.profile);
|
|
672
|
+
const token = await getProjectToken(project, opts);
|
|
673
|
+
|
|
674
|
+
const url = `${profile.baseUrl}/blob/${project}/${remotePath}`;
|
|
675
|
+
|
|
676
|
+
if (opts.debug) {
|
|
677
|
+
console.error(chalk.dim(`curl "${url}"`));
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const res = await fetch(url, {
|
|
681
|
+
headers: { "Authorization": `Bearer ${token}` },
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
if (!res.ok) {
|
|
685
|
+
const data = await res.json();
|
|
686
|
+
error((data as { error?: string }).error || "Download failed", opts);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Stream to stdout
|
|
690
|
+
const writer = Bun.stdout.writer();
|
|
691
|
+
const reader = res.body?.getReader();
|
|
692
|
+
if (reader) {
|
|
693
|
+
while (true) {
|
|
694
|
+
const { done, value } = await reader.read();
|
|
695
|
+
if (done) break;
|
|
696
|
+
writer.write(value);
|
|
697
|
+
}
|
|
698
|
+
await writer.flush();
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
blob
|
|
703
|
+
.command("rm")
|
|
704
|
+
.description("Delete a file")
|
|
705
|
+
.argument("<project>", "Project name")
|
|
706
|
+
.argument("<path>", "Remote path")
|
|
707
|
+
.action(async (project: string, remotePath: string) => {
|
|
708
|
+
const opts = program.opts<GlobalOptions>();
|
|
709
|
+
const profile = getProfile(opts.profile);
|
|
710
|
+
const token = await getProjectToken(project, opts);
|
|
711
|
+
|
|
712
|
+
const url = `${profile.baseUrl}/blob/${project}/${remotePath}`;
|
|
713
|
+
|
|
714
|
+
const res = await fetch(url, {
|
|
715
|
+
method: "DELETE",
|
|
716
|
+
headers: { "Authorization": `Bearer ${token}` },
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
const data = await res.json();
|
|
720
|
+
if (!res.ok) {
|
|
721
|
+
error((data as { error?: string }).error || "Delete failed", opts);
|
|
722
|
+
}
|
|
723
|
+
output(data, opts);
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
// =============================================================================
|
|
727
|
+
// Cron Commands
|
|
728
|
+
// =============================================================================
|
|
729
|
+
|
|
730
|
+
const cron = program.command("cron").description("Cron service commands");
|
|
731
|
+
|
|
732
|
+
cron
|
|
733
|
+
.command("ls")
|
|
734
|
+
.description("List jobs")
|
|
735
|
+
.argument("<project>", "Project name")
|
|
736
|
+
.action(async (project: string) => {
|
|
737
|
+
const opts = program.opts<GlobalOptions>();
|
|
738
|
+
const data = (await request("cron", `/${project}/jobs`, { project }, opts)) as { jobs: unknown[] };
|
|
739
|
+
output(data.jobs, opts);
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
cron
|
|
743
|
+
.command("runs")
|
|
744
|
+
.description("List run history")
|
|
745
|
+
.argument("<project>", "Project name")
|
|
746
|
+
.option("-l, --limit <n>", "Limit results", "100")
|
|
747
|
+
.option("--job <id>", "Filter by job ID")
|
|
748
|
+
.action(async (project: string, cmdOpts: { limit: string; job?: string }) => {
|
|
749
|
+
const opts = program.opts<GlobalOptions>();
|
|
750
|
+
const params = new URLSearchParams({ limit: cmdOpts.limit });
|
|
751
|
+
if (cmdOpts.job) params.set("job_id", cmdOpts.job);
|
|
752
|
+
const data = (await request("cron", `/${project}/runs?${params}`, { project }, opts)) as { runs: unknown[] };
|
|
753
|
+
output(data.runs, opts);
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
cron
|
|
757
|
+
.command("trigger")
|
|
758
|
+
.description("Manually trigger a job")
|
|
759
|
+
.argument("<project>", "Project name")
|
|
760
|
+
.argument("<job_id>", "Job ID")
|
|
761
|
+
.action(async (project: string, jobId: string) => {
|
|
762
|
+
const opts = program.opts<GlobalOptions>();
|
|
763
|
+
const data = await request("cron", `/${project}/jobs/${jobId}/trigger`, { method: "POST", project }, opts);
|
|
764
|
+
output(data, opts);
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
cron
|
|
768
|
+
.command("script:cat")
|
|
769
|
+
.description("Output script content")
|
|
770
|
+
.argument("<project>", "Project name")
|
|
771
|
+
.argument("<script>", "Script name")
|
|
772
|
+
.action(async (project: string, script: string) => {
|
|
773
|
+
const opts = program.opts<GlobalOptions>();
|
|
774
|
+
const profile = getProfile(opts.profile);
|
|
775
|
+
const token = await getProjectToken(project, opts);
|
|
776
|
+
|
|
777
|
+
const url = `${profile.baseUrl}/cron/${project}/scripts/${script}`;
|
|
778
|
+
const res = await fetch(url, {
|
|
779
|
+
headers: { "Authorization": `Bearer ${token}` },
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
if (!res.ok) {
|
|
783
|
+
const data = await res.json();
|
|
784
|
+
error((data as { error?: string }).error || "Failed to get script", opts);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
console.log(await res.text());
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
// =============================================================================
|
|
791
|
+
// AI Commands
|
|
792
|
+
// =============================================================================
|
|
793
|
+
|
|
794
|
+
const ai = program.command("ai").description("AI service commands");
|
|
795
|
+
|
|
796
|
+
ai
|
|
797
|
+
.command("stats")
|
|
798
|
+
.description("View usage statistics")
|
|
799
|
+
.argument("<project>", "Project name")
|
|
800
|
+
.option("-p, --period <days>", "Period in days", "30")
|
|
801
|
+
.action(async (project: string, cmdOpts: { period: string }) => {
|
|
802
|
+
const opts = program.opts<GlobalOptions>();
|
|
803
|
+
const data = await request("ai", `/usage/stats?period=${cmdOpts.period}d`, { project }, opts);
|
|
804
|
+
output(data, opts);
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
ai
|
|
808
|
+
.command("chat")
|
|
809
|
+
.description("Send a chat completion request")
|
|
810
|
+
.argument("<project>", "Project name")
|
|
811
|
+
.argument("<prompt>", "Chat prompt")
|
|
812
|
+
.option("-m, --model <model>", "Model to use (defaults to project config)")
|
|
813
|
+
.action(async (project: string, prompt: string, cmdOpts: { model?: string }) => {
|
|
814
|
+
const opts = program.opts<GlobalOptions>();
|
|
815
|
+
const profile = getProfile(opts.profile);
|
|
816
|
+
const token = await getProjectToken(project, opts);
|
|
817
|
+
|
|
818
|
+
// Get default model from project config if not specified
|
|
819
|
+
let model = cmdOpts.model;
|
|
820
|
+
if (!model) {
|
|
821
|
+
const configRes = await fetch(`${profile.baseUrl}/ai/config`, {
|
|
822
|
+
headers: { "Authorization": `Bearer ${token}` },
|
|
823
|
+
});
|
|
824
|
+
if (configRes.ok) {
|
|
825
|
+
const config = await configRes.json() as { default_model?: string };
|
|
826
|
+
model = config.default_model || "openai/gpt-4o-mini";
|
|
827
|
+
} else {
|
|
828
|
+
model = "openai/gpt-4o-mini";
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const url = `${profile.baseUrl}/ai/v1/chat/completions`;
|
|
833
|
+
|
|
834
|
+
if (opts.debug) {
|
|
835
|
+
console.error(chalk.dim(`curl -X POST "${url}" ...`));
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const res = await fetch(url, {
|
|
839
|
+
method: "POST",
|
|
840
|
+
headers: {
|
|
841
|
+
"Authorization": `Bearer ${token}`,
|
|
842
|
+
"Content-Type": "application/json",
|
|
843
|
+
},
|
|
844
|
+
body: JSON.stringify({
|
|
845
|
+
model,
|
|
846
|
+
messages: [{ role: "user", content: prompt }],
|
|
847
|
+
}),
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
const data = await res.json();
|
|
851
|
+
if (!res.ok) {
|
|
852
|
+
error((data as { error?: string }).error || "Chat failed", opts);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (opts.json) {
|
|
856
|
+
console.log(JSON.stringify(data, null, 2));
|
|
857
|
+
} else {
|
|
858
|
+
const content = (data as { choices: Array<{ message: { content: string } }> }).choices?.[0]?.message?.content;
|
|
859
|
+
console.log(content || "(no response)");
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
ai
|
|
864
|
+
.command("config")
|
|
865
|
+
.description("Get or set AI config for a project")
|
|
866
|
+
.argument("<project>", "Project name")
|
|
867
|
+
.option("--model <model>", "Set default model")
|
|
868
|
+
.option("--rate-limit <usd>", "Set rate limit in USD")
|
|
869
|
+
.option("--key <key>", "Set custom OpenRouter API key")
|
|
870
|
+
.action(async (project: string, cmdOpts: { model?: string; rateLimit?: string; key?: string }) => {
|
|
871
|
+
const opts = program.opts<GlobalOptions>();
|
|
872
|
+
|
|
873
|
+
// If any options provided, update config
|
|
874
|
+
if (cmdOpts.model || cmdOpts.rateLimit || cmdOpts.key) {
|
|
875
|
+
const body: Record<string, unknown> = {};
|
|
876
|
+
if (cmdOpts.model) body.default_model = cmdOpts.model;
|
|
877
|
+
if (cmdOpts.rateLimit) body.rate_limit_usd = Number.parseFloat(cmdOpts.rateLimit);
|
|
878
|
+
if (cmdOpts.key) body.key = cmdOpts.key;
|
|
879
|
+
|
|
880
|
+
await request("ai", "/config", { method: "PUT", body, project }, opts);
|
|
881
|
+
console.log("Config updated");
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// Always show current config
|
|
885
|
+
const data = await request("ai", "/config", { project }, opts);
|
|
886
|
+
output(data, opts);
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
// =============================================================================
|
|
890
|
+
// Search Commands
|
|
891
|
+
// =============================================================================
|
|
892
|
+
|
|
893
|
+
const search = program.command("search").description("Search service commands");
|
|
894
|
+
|
|
895
|
+
search
|
|
896
|
+
.command("config")
|
|
897
|
+
.description("Get or set index configuration")
|
|
898
|
+
.argument("<project>", "Project name")
|
|
899
|
+
.argument("<index>", "Index name")
|
|
900
|
+
.option("--set", "Set config (reads JSON from stdin)")
|
|
901
|
+
.action(async (project: string, index: string, cmdOpts: { set?: boolean }) => {
|
|
902
|
+
const opts = program.opts<GlobalOptions>();
|
|
903
|
+
|
|
904
|
+
if (cmdOpts.set) {
|
|
905
|
+
const chunks: Buffer[] = [];
|
|
906
|
+
for await (const chunk of Bun.stdin.stream()) {
|
|
907
|
+
chunks.push(Buffer.from(chunk));
|
|
908
|
+
}
|
|
909
|
+
const input = Buffer.concat(chunks).toString("utf-8").trim();
|
|
910
|
+
const body = JSON.parse(input);
|
|
911
|
+
const data = await request("search", `/${project}/${index}/_config`, { method: "PUT", body, project }, opts);
|
|
912
|
+
output(data, opts);
|
|
913
|
+
} else {
|
|
914
|
+
const data = await request("search", `/${project}/${index}/_config`, { project }, opts);
|
|
915
|
+
output(data, opts);
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
search
|
|
920
|
+
.command("query")
|
|
921
|
+
.description("Search an index")
|
|
922
|
+
.argument("<project>", "Project name")
|
|
923
|
+
.argument("<index>", "Index name")
|
|
924
|
+
.argument("<q>", "Search query")
|
|
925
|
+
.option("-l, --limit <n>", "Limit results", "10")
|
|
926
|
+
.action(async (project: string, index: string, q: string, cmdOpts: { limit: string }) => {
|
|
927
|
+
const opts = program.opts<GlobalOptions>();
|
|
928
|
+
const params = new URLSearchParams({ q, limit: cmdOpts.limit });
|
|
929
|
+
const data = (await request("search", `/${project}/${index}/search?${params}`, { project }, opts)) as { results: unknown[] };
|
|
930
|
+
output(data.results, opts);
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
// =============================================================================
|
|
934
|
+
// Parse & Run
|
|
935
|
+
// =============================================================================
|
|
936
|
+
|
|
937
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vtriv/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"module": "index.ts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "CLI for vtriv backend services - auth, db, blob, search, ai, cron",
|
|
7
|
+
"bin": {
|
|
8
|
+
"vtriv": "./index.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.ts"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"cli": "bun run index.ts"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"bun": ">=1.0.0"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/vtriv/vtriv"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"vtriv",
|
|
25
|
+
"cli",
|
|
26
|
+
"bun",
|
|
27
|
+
"auth",
|
|
28
|
+
"database",
|
|
29
|
+
"blob",
|
|
30
|
+
"search",
|
|
31
|
+
"ai"
|
|
32
|
+
],
|
|
33
|
+
"author": "vtriv",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/bun": "latest"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"chalk": "^5.6.2",
|
|
40
|
+
"commander": "^14.0.2"
|
|
41
|
+
}
|
|
42
|
+
}
|