@vtriv/cli 0.1.1 → 0.1.3

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 +240 -30
  2. package/index.ts +341 -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,263 @@ 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
+ # Reset templates to server defaults (useful after server updates)
128
+ vtriv template reset
129
+ ```
130
+
131
+ ### Database Commands
132
+
133
+ ```bash
134
+ # List documents
135
+ vtriv db ls posts
136
+ vtriv db ls posts --filter '{"status":"published"}' --limit 10 --sort -_created_at
137
+
138
+ # Get a document
139
+ vtriv db get posts abc-123
140
+
141
+ # Create a document (reads JSON from stdin)
142
+ echo '{"title":"Hello","body":"World"}' | vtriv db put posts
143
+
144
+ # Update a document
145
+ echo '{"title":"Updated"}' | vtriv db put posts abc-123
146
+
147
+ # Delete a document
148
+ vtriv db rm posts abc-123
149
+
150
+ # Get/set collection schema
151
+ vtriv db schema posts
152
+ echo '{"indexes":["status"],"unique":["slug"]}' | vtriv db schema posts --set
153
+ ```
154
+
155
+ ### Blob Commands
156
+
157
+ ```bash
158
+ # Upload a file
159
+ vtriv blob put images/photo.jpg ./local-photo.jpg
160
+
161
+ # Download a file (outputs to stdout)
162
+ vtriv blob get images/photo.jpg > downloaded.jpg
163
+
164
+ # Delete a file
165
+ vtriv blob rm images/photo.jpg
166
+ ```
167
+
168
+ ### Cron Commands
169
+
170
+ ```bash
171
+ # List jobs
172
+ vtriv cron ls
173
+
174
+ # View run history
175
+ vtriv cron runs
176
+ vtriv cron runs --limit 50 --job daily-backup
177
+
178
+ # Manually trigger a job
179
+ vtriv cron trigger daily-backup
180
+
181
+ # View a script
182
+ vtriv cron script:cat backup.ts
183
+ ```
184
+
185
+ ### AI Commands
186
+
187
+ ```bash
188
+ # Simple chat
189
+ vtriv ai chat "What is the capital of France?"
190
+
191
+ # With specific model
192
+ vtriv ai chat "Explain quantum computing" --model anthropic/claude-3.5-sonnet
193
+
194
+ # View usage statistics
195
+ vtriv ai stats
196
+ vtriv ai stats --period 7
197
+
198
+ # Get/set AI config
199
+ vtriv ai config
200
+ vtriv ai config --model openai/gpt-4o --rate-limit 10.0
201
+ ```
202
+
203
+ ### Search Commands
204
+
205
+ ```bash
206
+ # Get index configuration
207
+ vtriv search config posts
208
+
209
+ # Set index configuration (reads JSON from stdin)
210
+ echo '{"model":"openai/text-embedding-3-small","dimensions":1536,"input_fields":["title","body"],"store_fields":["id","title","slug"]}' | vtriv search config posts --set
211
+
212
+ # Search an index
213
+ vtriv search query posts "semantic search" --limit 20
214
+ ```
215
+
216
+ ## Output Formats
217
+
218
+ By default, the CLI formats output as tables:
219
+
220
+ ```
221
+ $ vtriv db ls posts
222
+ id title status
223
+ -----------------------------------------
224
+ abc-123 Hello World published
225
+ def-456 Draft Post draft
226
+ ```
227
+
228
+ Use `--json` for machine-readable output:
229
+
230
+ ```bash
231
+ $ vtriv db ls posts --json
232
+ [
233
+ {"id": "abc-123", "title": "Hello World", "status": "published"},
234
+ {"id": "def-456", "title": "Draft Post", "status": "draft"}
235
+ ]
236
+ ```
237
+
238
+ ## Debugging
239
+
240
+ Use `--debug` to see the curl equivalent of each request:
241
+
242
+ ```bash
243
+ $ vtriv --debug db ls posts
244
+ curl -X GET "http://localhost:3000/db/a1b2c3d4-my-app/posts" -H "Authorization: Bearer <token>"
245
+ ```
246
+
247
+ ## Configuration File
248
+
249
+ The config file at `~/.config/vtriv/config.json`:
250
+
251
+ ```json
252
+ {
253
+ "baseUrl": "http://localhost:3000",
254
+ "accountKey": "vtriv_ak_...",
255
+ "accountSlug": "a1b2c3d4",
256
+ "default": "my-app",
257
+ "profiles": {
258
+ "my-app": {
259
+ "apiKey": "vtriv_sk_..."
260
+ },
261
+ "staging": {
262
+ "apiKey": "vtriv_sk_..."
263
+ }
264
+ }
265
+ }
266
+ ```
267
+
268
+ ## Migration from Old Config
59
269
 
60
- ## Options
270
+ If you have an old `~/.vtrivrc` file, you'll need to:
271
+ 1. Run `vtriv config` with your account key
272
+ 2. Run `vtriv init <name>` for each project
61
273
 
62
- - `--json` - Output raw JSON for scripts/agents
63
- - `--profile <name>` - Use a specific profile
64
- - `--debug` - Show HTTP curl equivalents
274
+ The old config format is not compatible with the new account-based system.
65
275
 
66
- ## Documentation
276
+ ## License
67
277
 
68
- See [~/grit/skills/vtriv/cli.md](../../grit/skills/vtriv/cli.md) for full documentation.
278
+ 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."));
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."));
62
84
  process.exit(1);
63
85
  }
64
- const name = profileName || config.default;
65
- const profile = config.profiles[name];
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
+ }
365
+
366
+ const fullProjectName = `${config.accountSlug}-${name}`;
295
367
 
296
- const config: Config = existing || { default: profileName, profiles: {} };
297
- config.profiles[profileName] = { baseUrl, project, apiKey };
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
+ });
298
378
 
299
- if (cmdOpts.default || !existing) {
300
- config.default = profileName;
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."));
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."));
325
464
  return;
326
465
  }
327
466
 
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
- }));
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
  });
@@ -477,6 +694,37 @@ template
477
694
  console.log(chalk.green(`Template '${name}' deleted.`));
478
695
  });
479
696
 
697
+ template
698
+ .command("reset")
699
+ .description("Reset templates to server defaults")
700
+ .action(async () => {
701
+ const opts = program.opts<GlobalOptions>();
702
+ const { config, profile, fullProjectName } = getProfile(opts.profile);
703
+
704
+ const res = await fetch(`${config.baseUrl}/auth/${fullProjectName}/templates/_reset`, {
705
+ method: "POST",
706
+ headers: { Authorization: `Bearer ${profile.apiKey}` },
707
+ });
708
+
709
+ if (!res.ok) {
710
+ const data = await res.json();
711
+ error((data as { error?: string }).error || "Failed to reset templates", opts);
712
+ }
713
+
714
+ const data = (await res.json()) as { templates: Record<string, { claims: Record<string, unknown> }> };
715
+
716
+ if (opts.json) {
717
+ console.log(JSON.stringify(data.templates, null, 2));
718
+ } else {
719
+ console.log(chalk.green("Templates reset to defaults:"));
720
+ const rows = Object.entries(data.templates).map(([name, tmpl]) => ({
721
+ name,
722
+ claims: JSON.stringify(tmpl.claims),
723
+ }));
724
+ printTable(rows);
725
+ }
726
+ });
727
+
480
728
  // =============================================================================
481
729
  // DB Commands
482
730
  // =============================================================================
@@ -594,7 +842,7 @@ blob
594
842
  .argument("<file>", "Local file path")
595
843
  .action(async (remotePath: string, localFile: string) => {
596
844
  const opts = program.opts<GlobalOptions>();
597
- const profile = getProfile(opts.profile);
845
+ const { config, fullProjectName } = getProfile(opts.profile);
598
846
  const token = await getToken(opts);
599
847
 
600
848
  const file = Bun.file(localFile);
@@ -602,7 +850,7 @@ blob
602
850
  error(`File not found: ${localFile}`, opts);
603
851
  }
604
852
 
605
- const url = `${profile.baseUrl}/blob/${profile.project}/${remotePath}`;
853
+ const url = `${config.baseUrl}/blob/${fullProjectName}/${remotePath}`;
606
854
 
607
855
  if (opts.debug) {
608
856
  console.error(chalk.dim(`curl -X PUT "${url}" -T "${localFile}"`));
@@ -630,10 +878,10 @@ blob
630
878
  .argument("<path>", "Remote path")
631
879
  .action(async (remotePath: string) => {
632
880
  const opts = program.opts<GlobalOptions>();
633
- const profile = getProfile(opts.profile);
881
+ const { config, fullProjectName } = getProfile(opts.profile);
634
882
  const token = await getToken(opts);
635
883
 
636
- const url = `${profile.baseUrl}/blob/${profile.project}/${remotePath}`;
884
+ const url = `${config.baseUrl}/blob/${fullProjectName}/${remotePath}`;
637
885
 
638
886
  if (opts.debug) {
639
887
  console.error(chalk.dim(`curl "${url}"`));
@@ -667,10 +915,10 @@ blob
667
915
  .argument("<path>", "Remote path")
668
916
  .action(async (remotePath: string) => {
669
917
  const opts = program.opts<GlobalOptions>();
670
- const profile = getProfile(opts.profile);
918
+ const { config, fullProjectName } = getProfile(opts.profile);
671
919
  const token = await getToken(opts);
672
920
 
673
- const url = `${profile.baseUrl}/blob/${profile.project}/${remotePath}`;
921
+ const url = `${config.baseUrl}/blob/${fullProjectName}/${remotePath}`;
674
922
 
675
923
  const res = await fetch(url, {
676
924
  method: "DELETE",
@@ -728,10 +976,10 @@ cron
728
976
  .argument("<script>", "Script name")
729
977
  .action(async (script: string) => {
730
978
  const opts = program.opts<GlobalOptions>();
731
- const profile = getProfile(opts.profile);
979
+ const { config, fullProjectName } = getProfile(opts.profile);
732
980
  const token = await getToken(opts);
733
981
 
734
- const url = `${profile.baseUrl}/cron/${profile.project}/scripts/${script}`;
982
+ const url = `${config.baseUrl}/cron/${fullProjectName}/scripts/${script}`;
735
983
  const res = await fetch(url, {
736
984
  headers: { Authorization: `Bearer ${token}` },
737
985
  });
@@ -755,10 +1003,10 @@ ai.command("stats")
755
1003
  .option("-p, --period <days>", "Period in days", "30")
756
1004
  .action(async (cmdOpts: { period: string }) => {
757
1005
  const opts = program.opts<GlobalOptions>();
758
- const profile = getProfile(opts.profile);
1006
+ const { config } = getProfile(opts.profile);
759
1007
  const token = await getToken(opts);
760
1008
 
761
- const url = `${profile.baseUrl}/ai/usage/stats?period=${cmdOpts.period}d`;
1009
+ const url = `${config.baseUrl}/ai/usage/stats?period=${cmdOpts.period}d`;
762
1010
  const res = await fetch(url, {
763
1011
  headers: { Authorization: `Bearer ${token}` },
764
1012
  });
@@ -777,24 +1025,24 @@ ai.command("chat")
777
1025
  .option("-m, --model <model>", "Model to use (defaults to project config)")
778
1026
  .action(async (prompt: string, cmdOpts: { model?: string }) => {
779
1027
  const opts = program.opts<GlobalOptions>();
780
- const profile = getProfile(opts.profile);
1028
+ const { config } = getProfile(opts.profile);
781
1029
  const token = await getToken(opts);
782
1030
 
783
1031
  // Get default model from project config if not specified
784
1032
  let model = cmdOpts.model;
785
1033
  if (!model) {
786
- const configRes = await fetch(`${profile.baseUrl}/ai/config`, {
1034
+ const configRes = await fetch(`${config.baseUrl}/ai/config`, {
787
1035
  headers: { Authorization: `Bearer ${token}` },
788
1036
  });
789
1037
  if (configRes.ok) {
790
- const config = (await configRes.json()) as { default_model?: string };
791
- model = config.default_model || "openai/gpt-4o-mini";
1038
+ const aiConfig = (await configRes.json()) as { default_model?: string };
1039
+ model = aiConfig.default_model || "openai/gpt-4o-mini";
792
1040
  } else {
793
1041
  model = "openai/gpt-4o-mini";
794
1042
  }
795
1043
  }
796
1044
 
797
- const url = `${profile.baseUrl}/ai/v1/chat/completions`;
1045
+ const url = `${config.baseUrl}/ai/v1/chat/completions`;
798
1046
 
799
1047
  if (opts.debug) {
800
1048
  console.error(chalk.dim(`curl -X POST "${url}" ...`));
@@ -833,7 +1081,7 @@ ai.command("config")
833
1081
  .option("--key <key>", "Set custom OpenRouter API key")
834
1082
  .action(async (cmdOpts: { model?: string; rateLimit?: string; key?: string }) => {
835
1083
  const opts = program.opts<GlobalOptions>();
836
- const profile = getProfile(opts.profile);
1084
+ const { config } = getProfile(opts.profile);
837
1085
  const token = await getToken(opts);
838
1086
 
839
1087
  // If any options provided, update config
@@ -843,7 +1091,7 @@ ai.command("config")
843
1091
  if (cmdOpts.rateLimit) body.rate_limit_usd = Number.parseFloat(cmdOpts.rateLimit);
844
1092
  if (cmdOpts.key) body.key = cmdOpts.key;
845
1093
 
846
- const updateRes = await fetch(`${profile.baseUrl}/ai/config`, {
1094
+ const updateRes = await fetch(`${config.baseUrl}/ai/config`, {
847
1095
  method: "PUT",
848
1096
  headers: {
849
1097
  Authorization: `Bearer ${token}`,
@@ -861,7 +1109,7 @@ ai.command("config")
861
1109
  }
862
1110
 
863
1111
  // Always show current config
864
- const res = await fetch(`${profile.baseUrl}/ai/config`, {
1112
+ const res = await fetch(`${config.baseUrl}/ai/config`, {
865
1113
  headers: { Authorization: `Bearer ${token}` },
866
1114
  });
867
1115
  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.3",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "description": "CLI for vtriv backend services - auth, db, blob, search, ai, cron",