@vtriv/cli 0.1.1 → 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.
Files changed (3) hide show
  1. package/README.md +237 -30
  2. package/index.ts +310 -93
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # vtriv CLI
2
2
 
3
- Command-line interface for vtriv backend services.
3
+ Command-line interface for vtriv backend services. Uses account-based authentication to manage projects.
4
4
 
5
5
  ## Installation
6
6
 
@@ -16,53 +16,260 @@ bun link
16
16
  vtriv --help
17
17
  ```
18
18
 
19
+ ## Authentication Model
20
+
21
+ The CLI uses a two-tier authentication system:
22
+
23
+ 1. **Account Key** (`vtriv_ak_...`) - Used to create and manage projects
24
+ 2. **API Keys** (`vtriv_sk_...`) - Per-project, used to mint JWTs for service calls
25
+
26
+ Configuration is stored in `~/.config/vtriv/config.json` with `0600` permissions.
27
+
28
+ ### How It Works
29
+
30
+ 1. Run `vtriv config` to set up your account key
31
+ 2. Run `vtriv init <name>` to create a project and API key
32
+ 3. CLI uses the API key to mint short-lived JWTs
33
+ 4. JWTs are cached for 55 minutes (tokens valid for 60)
34
+
35
+ Project names are automatically prefixed with your account slug (e.g., `a1b2c3d4-my-app`), but you work with short names in the CLI.
36
+
19
37
  ## Setup
20
38
 
39
+ ### Configure Account
40
+
21
41
  ```bash
22
- # Initialize configuration
23
- vtriv init
42
+ vtriv config
24
43
  ```
25
44
 
26
- This creates `~/.vtrivrc` with your API gateway URL and bootstrap key.
45
+ You'll be prompted for:
46
+ - Base URL (default: `http://localhost:3000`)
47
+ - Account key (`vtriv_ak_...`)
48
+
49
+ The CLI verifies the account key and fetches your account slug.
27
50
 
28
- ## Quick Start
51
+ ### Create a Project
29
52
 
30
53
  ```bash
31
- # List projects
32
- vtriv auth project:list
54
+ vtriv init my-app
55
+ ```
33
56
 
34
- # Create a project
35
- vtriv auth project:create my-app
57
+ This:
58
+ 1. Creates project `{slug}-my-app` on the server
59
+ 2. Creates an API key for the project
60
+ 3. Saves the profile locally
36
61
 
37
- # Create a user
38
- vtriv auth user:create my-app admin@example.com secretpass
62
+ ### Multiple Projects
39
63
 
40
- # List documents
41
- vtriv db ls my-app posts
64
+ ```bash
65
+ # Create another project
66
+ vtriv init staging --default
42
67
 
43
- # Create a document
44
- echo '{"title":"Hello"}' | vtriv db put my-app posts
68
+ # Use a specific profile
69
+ vtriv --profile my-app db ls posts
70
+ ```
71
+
72
+ ### View Status
45
73
 
46
- # Search
47
- vtriv search query my-app posts "hello world"
74
+ ```bash
75
+ vtriv status
76
+ ```
77
+
78
+ Shows your account, configured profiles, and which is default.
79
+
80
+ ### Manage Projects
81
+
82
+ ```bash
83
+ # List all projects
84
+ vtriv project ls
85
+
86
+ # Delete a project
87
+ vtriv project rm my-app
48
88
  ```
49
89
 
50
90
  ## Commands
51
91
 
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)
92
+ ### Global Options
93
+
94
+ | Option | Description |
95
+ |--------|-------------|
96
+ | `--json` | Output raw JSON (for scripts/agents) |
97
+ | `--profile <name>` | Use a specific profile |
98
+ | `--debug` | Show HTTP curl equivalents |
99
+
100
+ ### Token Commands
101
+
102
+ ```bash
103
+ # Mint and display a JWT
104
+ vtriv token
105
+
106
+ # Mint with specific template
107
+ vtriv token --template ai-enabled
108
+ ```
109
+
110
+ ### Template Commands
111
+
112
+ Manage token templates for your project.
113
+
114
+ ```bash
115
+ # List all templates
116
+ vtriv template ls
117
+
118
+ # Get a specific template
119
+ vtriv template get default
120
+
121
+ # Create/update a template (reads JSON from stdin)
122
+ vtriv template set ai-enabled <<< '{"x-db": true, "x-blob": true, "x-ai": true}'
123
+
124
+ # Delete a template (cannot delete 'default')
125
+ vtriv template rm ai-enabled
126
+ ```
127
+
128
+ ### Database Commands
129
+
130
+ ```bash
131
+ # List documents
132
+ vtriv db ls posts
133
+ vtriv db ls posts --filter '{"status":"published"}' --limit 10 --sort -_created_at
134
+
135
+ # Get a document
136
+ vtriv db get posts abc-123
137
+
138
+ # Create a document (reads JSON from stdin)
139
+ echo '{"title":"Hello","body":"World"}' | vtriv db put posts
140
+
141
+ # Update a document
142
+ echo '{"title":"Updated"}' | vtriv db put posts abc-123
143
+
144
+ # Delete a document
145
+ vtriv db rm posts abc-123
146
+
147
+ # Get/set collection schema
148
+ vtriv db schema posts
149
+ echo '{"indexes":["status"],"unique":["slug"]}' | vtriv db schema posts --set
150
+ ```
151
+
152
+ ### Blob Commands
153
+
154
+ ```bash
155
+ # Upload a file
156
+ vtriv blob put images/photo.jpg ./local-photo.jpg
157
+
158
+ # Download a file (outputs to stdout)
159
+ vtriv blob get images/photo.jpg > downloaded.jpg
160
+
161
+ # Delete a file
162
+ vtriv blob rm images/photo.jpg
163
+ ```
164
+
165
+ ### Cron Commands
166
+
167
+ ```bash
168
+ # List jobs
169
+ vtriv cron ls
170
+
171
+ # View run history
172
+ vtriv cron runs
173
+ vtriv cron runs --limit 50 --job daily-backup
174
+
175
+ # Manually trigger a job
176
+ vtriv cron trigger daily-backup
177
+
178
+ # View a script
179
+ vtriv cron script:cat backup.ts
180
+ ```
181
+
182
+ ### AI Commands
183
+
184
+ ```bash
185
+ # Simple chat
186
+ vtriv ai chat "What is the capital of France?"
187
+
188
+ # With specific model
189
+ vtriv ai chat "Explain quantum computing" --model anthropic/claude-3.5-sonnet
190
+
191
+ # View usage statistics
192
+ vtriv ai stats
193
+ vtriv ai stats --period 7
194
+
195
+ # Get/set AI config
196
+ vtriv ai config
197
+ vtriv ai config --model openai/gpt-4o --rate-limit 10.0
198
+ ```
199
+
200
+ ### Search Commands
201
+
202
+ ```bash
203
+ # Get index configuration
204
+ vtriv search config posts
205
+
206
+ # Set index configuration (reads JSON from stdin)
207
+ echo '{"model":"openai/text-embedding-3-small","dimensions":1536,"input_fields":["title","body"],"store_fields":["id","title","slug"]}' | vtriv search config posts --set
208
+
209
+ # Search an index
210
+ vtriv search query posts "semantic search" --limit 20
211
+ ```
212
+
213
+ ## Output Formats
214
+
215
+ By default, the CLI formats output as tables:
216
+
217
+ ```
218
+ $ vtriv db ls posts
219
+ id title status
220
+ -----------------------------------------
221
+ abc-123 Hello World published
222
+ def-456 Draft Post draft
223
+ ```
224
+
225
+ Use `--json` for machine-readable output:
226
+
227
+ ```bash
228
+ $ vtriv db ls posts --json
229
+ [
230
+ {"id": "abc-123", "title": "Hello World", "status": "published"},
231
+ {"id": "def-456", "title": "Draft Post", "status": "draft"}
232
+ ]
233
+ ```
234
+
235
+ ## Debugging
236
+
237
+ Use `--debug` to see the curl equivalent of each request:
238
+
239
+ ```bash
240
+ $ vtriv --debug db ls posts
241
+ curl -X GET "http://localhost:3000/db/a1b2c3d4-my-app/posts" -H "Authorization: Bearer <token>"
242
+ ```
243
+
244
+ ## Configuration File
245
+
246
+ The config file at `~/.config/vtriv/config.json`:
247
+
248
+ ```json
249
+ {
250
+ "baseUrl": "http://localhost:3000",
251
+ "accountKey": "vtriv_ak_...",
252
+ "accountSlug": "a1b2c3d4",
253
+ "default": "my-app",
254
+ "profiles": {
255
+ "my-app": {
256
+ "apiKey": "vtriv_sk_..."
257
+ },
258
+ "staging": {
259
+ "apiKey": "vtriv_sk_..."
260
+ }
261
+ }
262
+ }
263
+ ```
264
+
265
+ ## Migration from Old Config
59
266
 
60
- ## Options
267
+ If you have an old `~/.vtrivrc` file, you'll need to:
268
+ 1. Run `vtriv config` with your account key
269
+ 2. Run `vtriv init <name>` for each project
61
270
 
62
- - `--json` - Output raw JSON for scripts/agents
63
- - `--profile <name>` - Use a specific profile
64
- - `--debug` - Show HTTP curl equivalents
271
+ The old config format is not compatible with the new account-based system.
65
272
 
66
- ## Documentation
273
+ ## License
67
274
 
68
- See [~/grit/skills/vtriv/cli.md](../../grit/skills/vtriv/cli.md) for full documentation.
275
+ Private - Part of vtriv meta-project
package/index.ts CHANGED
@@ -2,14 +2,17 @@
2
2
  /**
3
3
  * vtriv-cli - Command-line interface for vtriv services
4
4
  *
5
- * Each profile represents a project + API key pair.
6
- * API keys are issued by the platform operator and configured manually.
7
- * The CLI uses the API key to mint short-lived JWTs for service calls.
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.
8
11
  */
9
12
 
10
- import { existsSync, readFileSync, writeFileSync, chmodSync } from "node:fs";
13
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from "node:fs";
11
14
  import { homedir } from "node:os";
12
- import { join } from "node:path";
15
+ import { join, dirname } from "node:path";
13
16
  import { Command } from "commander";
14
17
  import chalk from "chalk";
15
18
 
@@ -18,13 +21,14 @@ import chalk from "chalk";
18
21
  // =============================================================================
19
22
 
20
23
  interface Profile {
21
- baseUrl: string;
22
- project: string;
23
24
  apiKey: string; // vtriv_sk_*
24
25
  }
25
26
 
26
27
  interface Config {
27
- default: string;
28
+ baseUrl: string;
29
+ accountKey?: string; // vtriv_ak_*
30
+ accountSlug?: string;
31
+ default?: string;
28
32
  profiles: Record<string, Profile>;
29
33
  }
30
34
 
@@ -38,36 +42,55 @@ interface GlobalOptions {
38
42
  // Configuration
39
43
  // =============================================================================
40
44
 
41
- const CONFIG_PATH = join(homedir(), ".vtrivrc");
45
+ function getConfigPath(): string {
46
+ const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
47
+ return join(xdgConfig, "vtriv", "config.json");
48
+ }
42
49
 
43
50
  function loadConfig(): Config | null {
44
- if (!existsSync(CONFIG_PATH)) return null;
51
+ const configPath = getConfigPath();
52
+ if (!existsSync(configPath)) return null;
45
53
  try {
46
- return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
54
+ return JSON.parse(readFileSync(configPath, "utf-8"));
47
55
  } catch {
48
56
  return null;
49
57
  }
50
58
  }
51
59
 
52
60
  function saveConfig(config: Config): void {
53
- writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
54
- // Ensure permissions are correct even if file existed
55
- chmodSync(CONFIG_PATH, 0o600);
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);
56
68
  }
57
69
 
58
- function getProfile(profileName?: string): Profile {
70
+ function getConfig(): Config {
59
71
  const config = loadConfig();
60
72
  if (!config) {
61
- console.error(chalk.red("No config found. Run 'vtriv init' first."));
73
+ console.error(chalk.red("No config found. Run 'vtriv config' first."));
62
74
  process.exit(1);
63
75
  }
64
- const name = profileName || config.default;
65
- const profile = config.profiles[name];
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."));
84
+ process.exit(1);
85
+ }
86
+ const profile = config.profiles[shortName];
66
87
  if (!profile) {
67
- console.error(chalk.red(`Profile '${name}' not found. Run 'vtriv init' to add it.`));
88
+ console.error(chalk.red(`Profile '${shortName}' not found. Run 'vtriv init ${shortName}' to create it.`));
68
89
  process.exit(1);
69
90
  }
70
- return profile;
91
+ // Expand short name to full project name
92
+ const fullProjectName = config.accountSlug ? `${config.accountSlug}-${shortName}` : shortName;
93
+ return { config, profile, fullProjectName, shortName };
71
94
  }
72
95
 
73
96
  // =============================================================================
@@ -127,10 +150,10 @@ function error(msg: string, opts: GlobalOptions): never {
127
150
  const tokenCache = new Map<string, { token: string; expires: number }>();
128
151
 
129
152
  async function getToken(opts: GlobalOptions, template?: string): Promise<string> {
130
- const profile = getProfile(opts.profile);
153
+ const { config, profile, fullProjectName } = getProfile(opts.profile);
131
154
 
132
155
  // Cache key includes template
133
- const cacheKey = template ? `${profile.project}:${template}` : profile.project;
156
+ const cacheKey = template ? `${fullProjectName}:${template}` : fullProjectName;
134
157
  const cached = tokenCache.get(cacheKey);
135
158
  if (cached && cached.expires > Date.now()) {
136
159
  return cached.token;
@@ -142,7 +165,7 @@ async function getToken(opts: GlobalOptions, template?: string): Promise<string>
142
165
  body.template = template;
143
166
  }
144
167
 
145
- const mintRes = await fetch(`${profile.baseUrl}/auth/${profile.project}/token`, {
168
+ const mintRes = await fetch(`${config.baseUrl}/auth/${fullProjectName}/token`, {
146
169
  method: "POST",
147
170
  headers: {
148
171
  Authorization: `Bearer ${profile.apiKey}`,
@@ -185,8 +208,8 @@ async function request(
185
208
  },
186
209
  opts: GlobalOptions
187
210
  ): Promise<unknown> {
188
- const profile = getProfile(opts.profile);
189
- const url = `${profile.baseUrl}/${service}/${profile.project}${path}`;
211
+ const { config, fullProjectName } = getProfile(opts.profile);
212
+ const url = `${config.baseUrl}/${service}/${fullProjectName}${path}`;
190
213
  const method = options.method || "GET";
191
214
 
192
215
  const headers: Record<string, string> = {
@@ -242,18 +265,16 @@ program
242
265
  .option("--debug", "Show HTTP requests");
243
266
 
244
267
  // =============================================================================
245
- // Init Command
268
+ // Config Command
246
269
  // =============================================================================
247
270
 
248
271
  program
249
- .command("init")
250
- .description("Add or update a profile")
251
- .option("--name <name>", "Profile name (defaults to project name)")
252
- .option("--default", "Set as default profile")
253
- .action(async (cmdOpts: { name?: string; default?: boolean }) => {
272
+ .command("config")
273
+ .description("Configure account key and base URL")
274
+ .action(async () => {
254
275
  const existing = loadConfig();
255
276
 
256
- console.log(chalk.bold("vtriv CLI Setup\n"));
277
+ console.log(chalk.bold("vtriv Account Configuration\n"));
257
278
 
258
279
  const readline = await import("node:readline");
259
280
  const rl = readline.createInterface({
@@ -265,49 +286,143 @@ program
265
286
  new Promise((resolve) => rl.question(q, resolve));
266
287
 
267
288
  const baseUrl =
268
- (await question(
269
- `Base URL [${existing?.profiles?.[existing.default]?.baseUrl || "https://api.vtriv.com"}]: `
270
- )) ||
271
- existing?.profiles?.[existing.default]?.baseUrl ||
272
- "https://api.vtriv.com";
273
-
274
- const project = await question("Project name: ");
275
- if (!project) {
276
- console.error(chalk.red("Project name is required."));
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."));
277
296
  rl.close();
278
297
  process.exit(1);
279
298
  }
280
299
 
281
- const apiKey = await question("API Key (vtriv_sk_...): ");
282
- if (!apiKey) {
283
- console.error(chalk.red("API key is required."));
300
+ if (!accountKey.startsWith("vtriv_ak_")) {
301
+ console.error(chalk.red("Account key must start with 'vtriv_ak_'."));
284
302
  rl.close();
285
303
  process.exit(1);
286
304
  }
287
305
 
288
- if (!apiKey.startsWith("vtriv_sk_")) {
289
- console.error(chalk.red("API key must start with 'vtriv_sk_'."));
290
- rl.close();
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
+
328
+ const config: Config = {
329
+ baseUrl,
330
+ accountKey,
331
+ accountSlug: accountInfo.slug,
332
+ default: existing?.default,
333
+ profiles: existing?.profiles || {},
334
+ };
335
+
336
+ saveConfig(config);
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()}`));
341
+ });
342
+
343
+ // =============================================================================
344
+ // Init Command
345
+ // =============================================================================
346
+
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."));
291
357
  process.exit(1);
292
358
  }
293
359
 
294
- const profileName = cmdOpts.name || project;
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
+ }
295
365
 
296
- const config: Config = existing || { default: profileName, profiles: {} };
297
- config.profiles[profileName] = { baseUrl, project, apiKey };
366
+ const fullProjectName = `${config.accountSlug}-${name}`;
298
367
 
299
- if (cmdOpts.default || !existing) {
300
- config.default = profileName;
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}`));
301
391
  }
302
392
 
303
- saveConfig(config);
304
- console.log(chalk.green(`\nProfile '${profileName}' saved to ${CONFIG_PATH}`));
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
+ });
305
403
 
306
- if (config.default === profileName) {
307
- console.log(chalk.dim(`Set as default profile.`));
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);
308
408
  }
309
409
 
310
- rl.close();
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
+ }
311
426
  });
312
427
 
313
428
  // =============================================================================
@@ -316,23 +431,125 @@ program
316
431
 
317
432
  program
318
433
  .command("status")
319
- .description("Show configured profiles")
434
+ .description("Show configuration and profiles")
320
435
  .action(() => {
321
436
  const opts = program.opts<GlobalOptions>();
322
437
  const config = loadConfig();
323
- if (!config || Object.keys(config.profiles).length === 0) {
324
- console.log(chalk.dim("No profiles configured. Run 'vtriv init' to add one."));
438
+ if (!config) {
439
+ console.log(chalk.dim("No config found. Run 'vtriv config' first."));
325
440
  return;
326
441
  }
327
442
 
328
- const profiles = Object.entries(config.profiles).map(([name, p]) => ({
329
- name,
330
- project: p.project,
331
- baseUrl: p.baseUrl,
332
- default: name === config.default ? "yes" : "",
333
- }));
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
+ }
474
+ });
475
+
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")
485
+ .action(async () => {
486
+ const opts = program.opts<GlobalOptions>();
487
+ const config = getConfig();
488
+
489
+ if (!config.accountKey) {
490
+ console.error(chalk.red("No account configured. Run 'vtriv config' first."));
491
+ process.exit(1);
492
+ }
493
+
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
+ }
516
+ });
517
+
518
+ project
519
+ .command("rm")
520
+ .description("Delete a project")
521
+ .argument("<name>", "Project name (short name)")
522
+ .action(async (name: string) => {
523
+ const opts = program.opts<GlobalOptions>();
524
+ const config = getConfig();
525
+
526
+ if (!config.accountKey || !config.accountSlug) {
527
+ console.error(chalk.red("No account configured. Run 'vtriv config' first."));
528
+ process.exit(1);
529
+ }
530
+
531
+ const fullName = `${config.accountSlug}-${name}`;
532
+
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
+ }
334
551
 
335
- output(profiles, opts);
552
+ console.log(chalk.green(`Deleted project ${fullName}`));
336
553
  });
337
554
 
338
555
  // =============================================================================
@@ -364,9 +581,9 @@ template
364
581
  .description("List all templates")
365
582
  .action(async () => {
366
583
  const opts = program.opts<GlobalOptions>();
367
- const profile = getProfile(opts.profile);
584
+ const { config, profile, fullProjectName } = getProfile(opts.profile);
368
585
 
369
- const res = await fetch(`${profile.baseUrl}/auth/${profile.project}/templates`, {
586
+ const res = await fetch(`${config.baseUrl}/auth/${fullProjectName}/templates`, {
370
587
  headers: { Authorization: `Bearer ${profile.apiKey}` },
371
588
  });
372
589
 
@@ -394,9 +611,9 @@ template
394
611
  .argument("<name>", "Template name")
395
612
  .action(async (name: string) => {
396
613
  const opts = program.opts<GlobalOptions>();
397
- const profile = getProfile(opts.profile);
614
+ const { config, profile, fullProjectName } = getProfile(opts.profile);
398
615
 
399
- const res = await fetch(`${profile.baseUrl}/auth/${profile.project}/templates/${name}`, {
616
+ const res = await fetch(`${config.baseUrl}/auth/${fullProjectName}/templates/${name}`, {
400
617
  headers: { Authorization: `Bearer ${profile.apiKey}` },
401
618
  });
402
619
 
@@ -415,7 +632,7 @@ template
415
632
  .argument("<name>", "Template name")
416
633
  .action(async (name: string) => {
417
634
  const opts = program.opts<GlobalOptions>();
418
- const profile = getProfile(opts.profile);
635
+ const { config, profile, fullProjectName } = getProfile(opts.profile);
419
636
 
420
637
  // Read claims JSON from stdin
421
638
  const chunks: Buffer[] = [];
@@ -435,7 +652,7 @@ template
435
652
  error("Invalid JSON on stdin", opts);
436
653
  }
437
654
 
438
- const res = await fetch(`${profile.baseUrl}/auth/${profile.project}/templates/${name}`, {
655
+ const res = await fetch(`${config.baseUrl}/auth/${fullProjectName}/templates/${name}`, {
439
656
  method: "PUT",
440
657
  headers: {
441
658
  Authorization: `Bearer ${profile.apiKey}`,
@@ -458,13 +675,13 @@ template
458
675
  .argument("<name>", "Template name")
459
676
  .action(async (name: string) => {
460
677
  const opts = program.opts<GlobalOptions>();
461
- const profile = getProfile(opts.profile);
678
+ const { config, profile, fullProjectName } = getProfile(opts.profile);
462
679
 
463
680
  if (name === "default") {
464
681
  error("Cannot delete the default template", opts);
465
682
  }
466
683
 
467
- const res = await fetch(`${profile.baseUrl}/auth/${profile.project}/templates/${name}`, {
684
+ const res = await fetch(`${config.baseUrl}/auth/${fullProjectName}/templates/${name}`, {
468
685
  method: "DELETE",
469
686
  headers: { Authorization: `Bearer ${profile.apiKey}` },
470
687
  });
@@ -594,7 +811,7 @@ blob
594
811
  .argument("<file>", "Local file path")
595
812
  .action(async (remotePath: string, localFile: string) => {
596
813
  const opts = program.opts<GlobalOptions>();
597
- const profile = getProfile(opts.profile);
814
+ const { config, fullProjectName } = getProfile(opts.profile);
598
815
  const token = await getToken(opts);
599
816
 
600
817
  const file = Bun.file(localFile);
@@ -602,7 +819,7 @@ blob
602
819
  error(`File not found: ${localFile}`, opts);
603
820
  }
604
821
 
605
- const url = `${profile.baseUrl}/blob/${profile.project}/${remotePath}`;
822
+ const url = `${config.baseUrl}/blob/${fullProjectName}/${remotePath}`;
606
823
 
607
824
  if (opts.debug) {
608
825
  console.error(chalk.dim(`curl -X PUT "${url}" -T "${localFile}"`));
@@ -630,10 +847,10 @@ blob
630
847
  .argument("<path>", "Remote path")
631
848
  .action(async (remotePath: string) => {
632
849
  const opts = program.opts<GlobalOptions>();
633
- const profile = getProfile(opts.profile);
850
+ const { config, fullProjectName } = getProfile(opts.profile);
634
851
  const token = await getToken(opts);
635
852
 
636
- const url = `${profile.baseUrl}/blob/${profile.project}/${remotePath}`;
853
+ const url = `${config.baseUrl}/blob/${fullProjectName}/${remotePath}`;
637
854
 
638
855
  if (opts.debug) {
639
856
  console.error(chalk.dim(`curl "${url}"`));
@@ -667,10 +884,10 @@ blob
667
884
  .argument("<path>", "Remote path")
668
885
  .action(async (remotePath: string) => {
669
886
  const opts = program.opts<GlobalOptions>();
670
- const profile = getProfile(opts.profile);
887
+ const { config, fullProjectName } = getProfile(opts.profile);
671
888
  const token = await getToken(opts);
672
889
 
673
- const url = `${profile.baseUrl}/blob/${profile.project}/${remotePath}`;
890
+ const url = `${config.baseUrl}/blob/${fullProjectName}/${remotePath}`;
674
891
 
675
892
  const res = await fetch(url, {
676
893
  method: "DELETE",
@@ -728,10 +945,10 @@ cron
728
945
  .argument("<script>", "Script name")
729
946
  .action(async (script: string) => {
730
947
  const opts = program.opts<GlobalOptions>();
731
- const profile = getProfile(opts.profile);
948
+ const { config, fullProjectName } = getProfile(opts.profile);
732
949
  const token = await getToken(opts);
733
950
 
734
- const url = `${profile.baseUrl}/cron/${profile.project}/scripts/${script}`;
951
+ const url = `${config.baseUrl}/cron/${fullProjectName}/scripts/${script}`;
735
952
  const res = await fetch(url, {
736
953
  headers: { Authorization: `Bearer ${token}` },
737
954
  });
@@ -755,10 +972,10 @@ ai.command("stats")
755
972
  .option("-p, --period <days>", "Period in days", "30")
756
973
  .action(async (cmdOpts: { period: string }) => {
757
974
  const opts = program.opts<GlobalOptions>();
758
- const profile = getProfile(opts.profile);
975
+ const { config } = getProfile(opts.profile);
759
976
  const token = await getToken(opts);
760
977
 
761
- const url = `${profile.baseUrl}/ai/usage/stats?period=${cmdOpts.period}d`;
978
+ const url = `${config.baseUrl}/ai/usage/stats?period=${cmdOpts.period}d`;
762
979
  const res = await fetch(url, {
763
980
  headers: { Authorization: `Bearer ${token}` },
764
981
  });
@@ -777,24 +994,24 @@ ai.command("chat")
777
994
  .option("-m, --model <model>", "Model to use (defaults to project config)")
778
995
  .action(async (prompt: string, cmdOpts: { model?: string }) => {
779
996
  const opts = program.opts<GlobalOptions>();
780
- const profile = getProfile(opts.profile);
997
+ const { config } = getProfile(opts.profile);
781
998
  const token = await getToken(opts);
782
999
 
783
1000
  // Get default model from project config if not specified
784
1001
  let model = cmdOpts.model;
785
1002
  if (!model) {
786
- const configRes = await fetch(`${profile.baseUrl}/ai/config`, {
1003
+ const configRes = await fetch(`${config.baseUrl}/ai/config`, {
787
1004
  headers: { Authorization: `Bearer ${token}` },
788
1005
  });
789
1006
  if (configRes.ok) {
790
- const config = (await configRes.json()) as { default_model?: string };
791
- model = config.default_model || "openai/gpt-4o-mini";
1007
+ const aiConfig = (await configRes.json()) as { default_model?: string };
1008
+ model = aiConfig.default_model || "openai/gpt-4o-mini";
792
1009
  } else {
793
1010
  model = "openai/gpt-4o-mini";
794
1011
  }
795
1012
  }
796
1013
 
797
- const url = `${profile.baseUrl}/ai/v1/chat/completions`;
1014
+ const url = `${config.baseUrl}/ai/v1/chat/completions`;
798
1015
 
799
1016
  if (opts.debug) {
800
1017
  console.error(chalk.dim(`curl -X POST "${url}" ...`));
@@ -833,7 +1050,7 @@ ai.command("config")
833
1050
  .option("--key <key>", "Set custom OpenRouter API key")
834
1051
  .action(async (cmdOpts: { model?: string; rateLimit?: string; key?: string }) => {
835
1052
  const opts = program.opts<GlobalOptions>();
836
- const profile = getProfile(opts.profile);
1053
+ const { config } = getProfile(opts.profile);
837
1054
  const token = await getToken(opts);
838
1055
 
839
1056
  // If any options provided, update config
@@ -843,7 +1060,7 @@ ai.command("config")
843
1060
  if (cmdOpts.rateLimit) body.rate_limit_usd = Number.parseFloat(cmdOpts.rateLimit);
844
1061
  if (cmdOpts.key) body.key = cmdOpts.key;
845
1062
 
846
- const updateRes = await fetch(`${profile.baseUrl}/ai/config`, {
1063
+ const updateRes = await fetch(`${config.baseUrl}/ai/config`, {
847
1064
  method: "PUT",
848
1065
  headers: {
849
1066
  Authorization: `Bearer ${token}`,
@@ -861,7 +1078,7 @@ ai.command("config")
861
1078
  }
862
1079
 
863
1080
  // Always show current config
864
- const res = await fetch(`${profile.baseUrl}/ai/config`, {
1081
+ const res = await fetch(`${config.baseUrl}/ai/config`, {
865
1082
  headers: { Authorization: `Bearer ${token}` },
866
1083
  });
867
1084
  output(await res.json(), opts);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vtriv/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "description": "CLI for vtriv backend services - auth, db, blob, search, ai, cron",