@blinkdotnew/cli 0.3.7 → 0.4.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/package.json CHANGED
@@ -1,12 +1,38 @@
1
1
  {
2
2
  "name": "@blinkdotnew/cli",
3
- "version": "0.3.7",
4
- "description": "Blink platform CLI — deploy apps, manage databases, generate AI content",
3
+ "version": "0.4.0",
4
+ "description": "Blink CLI — full-stack cloud infrastructure from your terminal. Deploy, database, auth, storage, backend, domains, and more.",
5
5
  "bin": {
6
6
  "blink": "dist/cli.js"
7
7
  },
8
8
  "main": "./dist/cli.js",
9
9
  "type": "module",
10
+ "keywords": [
11
+ "blink",
12
+ "cli",
13
+ "deploy",
14
+ "hosting",
15
+ "backend",
16
+ "database",
17
+ "auth",
18
+ "storage",
19
+ "domains",
20
+ "ai",
21
+ "full-stack",
22
+ "infrastructure",
23
+ "cloud",
24
+ "vibe-coding",
25
+ "cursor",
26
+ "claude-code",
27
+ "codex"
28
+ ],
29
+ "homepage": "https://blink.new/docs/cli",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/blink-new/blink-sdk",
33
+ "directory": "packages/cli"
34
+ },
35
+ "license": "MIT",
10
36
  "scripts": {
11
37
  "build": "tsup",
12
38
  "dev": "node --loader tsx src/cli.ts",
package/src/cli.ts CHANGED
@@ -17,6 +17,18 @@ import { registerProjectCommands } from './commands/project.js'
17
17
  import { registerAuthCommands } from './commands/auth.js'
18
18
  import { registerAgentCommands } from './commands/agent.js'
19
19
  import { registerSecretsCommands } from './commands/secrets.js'
20
+ import { registerAuthConfigCommands } from './commands/auth-config.js'
21
+ import { registerDomainsCommands } from './commands/domains.js'
22
+ import { registerHostingCommands } from './commands/hosting.js'
23
+ import { registerSecurityCommands } from './commands/security.js'
24
+ import { registerEnvCommands } from './commands/env.js'
25
+ import { registerBackendCommands } from './commands/backend.js'
26
+ import { registerFunctionsCommands } from './commands/functions.js'
27
+ import { registerInitCommands } from './commands/init.js'
28
+ import { registerWorkspaceCommands } from './commands/workspace.js'
29
+ import { registerVersionCommands } from './commands/versions.js'
30
+ import { registerBillingCommands } from './commands/billing.js'
31
+ import { registerTokenCommands } from './commands/tokens.js'
20
32
 
21
33
  const require = createRequire(import.meta.url)
22
34
  const pkg = require('../package.json') as { version: string }
@@ -109,6 +121,70 @@ LinkedIn (dedicated commands for the LinkedIn connector):
109
121
  $ blink linkedin unlike "urn:li:ugcPost:123" Unlike a post
110
122
  Link LinkedIn at: blink.new/claw (Agent Integrations tab)
111
123
 
124
+ Environment Variables (project secrets):
125
+ $ blink env list List env var names
126
+ $ blink env set STRIPE_KEY sk_live_xxx Set an env var
127
+ $ blink env delete OLD_KEY Remove an env var
128
+ $ blink env push .env Bulk import from .env file
129
+ $ blink env pull > .env.local Export as KEY=VALUE
130
+
131
+ Backend (Hono server on CF Workers):
132
+ $ blink backend deploy ./backend Deploy backend/ to Cloudflare Workers
133
+ $ blink backend status Check backend status + URL
134
+ $ blink backend logs Tail backend request logs
135
+ $ blink backend delete --yes Remove backend
136
+
137
+ Auth Config (Blink Auth providers):
138
+ $ blink auth-config get Show auth configuration
139
+ $ blink auth-config set --provider google --enabled true
140
+ $ blink auth-config byoc-set --provider google --client-id X --client-secret Y
141
+
142
+ Domains:
143
+ $ blink domains list List custom domains
144
+ $ blink domains add myapp.com Add a custom domain
145
+ $ blink domains verify <domain_id> Verify DNS
146
+ $ blink domains search "myapp" Search available domains
147
+ $ blink domains purchase myapp.com Buy a domain
148
+
149
+ Hosting:
150
+ $ blink hosting status Check hosting state + URLs
151
+ $ blink hosting activate Activate production hosting
152
+ $ blink hosting deactivate --yes Deactivate hosting
153
+
154
+ Security & CORS:
155
+ $ blink security get Show per-module auth policy
156
+ $ blink security set --module db --require-auth true
157
+ $ blink cors get Show allowed CORS origins
158
+ $ blink cors set --origins https://example.com
159
+
160
+ Functions (legacy edge functions):
161
+ $ blink functions list List edge functions
162
+ $ blink functions logs index View function logs
163
+ $ blink functions delete old-fn --yes Delete a function
164
+
165
+ Versions:
166
+ $ blink versions list List saved versions
167
+ $ blink versions save --message "v1.0" Save a snapshot
168
+ $ blink versions restore ver_xxx --yes Restore to a version
169
+
170
+ Workspace:
171
+ $ blink workspace list List workspaces
172
+ $ blink workspace create "My Team" Create workspace
173
+ $ blink workspace members List members + roles
174
+
175
+ Billing:
176
+ $ blink credits Check credit balance
177
+ $ blink usage Usage breakdown
178
+
179
+ Tokens (Personal Access Tokens):
180
+ $ blink tokens list List PATs
181
+ $ blink tokens create --name "CI" Create PAT (shown once!)
182
+ $ blink tokens revoke <id> --yes Revoke a PAT
183
+
184
+ Init:
185
+ $ blink init Create project + link to current dir
186
+ $ blink init --name "My App" Create with custom name
187
+
112
188
  Agents (Claw — zero config on Fly machines, BLINK_AGENT_ID is already set):
113
189
  $ blink agent list List all agents in workspace
114
190
  $ blink agent status Show current agent details
@@ -118,7 +194,6 @@ Secrets (encrypted agent vault — values never shown):
118
194
  $ blink secrets list List secret key names
119
195
  $ blink secrets set GITHUB_TOKEN ghp_xxx Add/update a secret
120
196
  $ blink secrets delete OLD_KEY Remove a secret
121
- $ blink secrets set --agent clw_other KEY v Set secret on another agent (agent manager pattern)
122
197
 
123
198
  Tip — check full context (agent + project + auth):
124
199
  $ blink status
@@ -152,6 +227,18 @@ registerConnectorCommands(program)
152
227
  registerLinkedInCommands(program)
153
228
  registerPhoneCommands(program)
154
229
  registerSmsCommands(program)
230
+ registerAuthConfigCommands(program)
231
+ registerDomainsCommands(program)
232
+ registerHostingCommands(program)
233
+ registerSecurityCommands(program)
234
+ registerEnvCommands(program)
235
+ registerBackendCommands(program)
236
+ registerFunctionsCommands(program)
237
+ registerInitCommands(program)
238
+ registerWorkspaceCommands(program)
239
+ registerVersionCommands(program)
240
+ registerBillingCommands(program)
241
+ registerTokenCommands(program)
155
242
 
156
243
  program.command('use <project_id>')
157
244
  .description('Set active project for this shell session (alternative to blink link)')
@@ -0,0 +1,129 @@
1
+ import { Command } from 'commander'
2
+ import { appRequest } from '../lib/api-app.js'
3
+ import { requireToken } from '../lib/auth.js'
4
+ import { requireProjectId } from '../lib/project.js'
5
+ import { printJson, isJsonMode, withSpinner, printSuccess, printKv } from '../lib/output.js'
6
+ import chalk from 'chalk'
7
+
8
+ function printAuthConfig(auth: { mode?: string; providers?: Record<string, { enabled?: boolean }> }) {
9
+ printKv('Mode', auth.mode ?? 'redirect')
10
+ console.log()
11
+ console.log(chalk.bold('Providers:'))
12
+ for (const [name, cfg] of Object.entries(auth.providers ?? {})) {
13
+ const status = cfg.enabled ? chalk.green('enabled') : chalk.dim('disabled')
14
+ console.log(` ${name.padEnd(12)} ${status}`)
15
+ }
16
+ }
17
+
18
+ export function registerAuthConfigCommands(program: Command) {
19
+ const authConfig = program.command('auth-config')
20
+ .description('Configure Blink Auth — providers, mode, and BYOC credentials')
21
+ .addHelpText('after', `
22
+ Manage authentication settings for your Blink project.
23
+ Configure OAuth providers (Google, GitHub, Microsoft, Apple, Email)
24
+ and set managed/headless mode.
25
+
26
+ Examples:
27
+ $ blink auth-config get Show current auth config
28
+ $ blink auth-config set --provider google --enabled true
29
+ $ blink auth-config set --mode managed
30
+ $ blink auth-config byoc-set --provider google --client-id xxx --client-secret yyy
31
+ $ blink auth-config byoc-remove --provider google
32
+ `)
33
+
34
+ authConfig.command('get [project_id]')
35
+ .description('Show current auth configuration')
36
+ .addHelpText('after', `
37
+ Examples:
38
+ $ blink auth-config get Show auth config for linked project
39
+ $ blink auth-config get proj_xxx Show auth config for specific project
40
+ $ blink auth-config get --json Machine-readable output
41
+ `)
42
+ .action(async (projectArg: string | undefined) => {
43
+ requireToken()
44
+ const projectId = requireProjectId(projectArg)
45
+ const result = await withSpinner('Loading auth config...', () =>
46
+ appRequest(`/api/projects/${projectId}/auth`)
47
+ )
48
+ if (isJsonMode()) return printJson(result)
49
+ const auth = result?.auth ?? result
50
+ printAuthConfig(auth)
51
+ })
52
+
53
+ authConfig.command('set [project_id]')
54
+ .description('Update auth provider or mode')
55
+ .option('--provider <name>', 'Provider to update (google/github/microsoft/apple/email)')
56
+ .option('--enabled <bool>', 'Enable or disable provider (true/false)')
57
+ .option('--mode <mode>', 'Auth mode (managed/headless)')
58
+ .addHelpText('after', `
59
+ Examples:
60
+ $ blink auth-config set --provider google --enabled true
61
+ $ blink auth-config set --provider email --enabled false
62
+ $ blink auth-config set --mode managed
63
+ $ blink auth-config set proj_xxx --provider github --enabled true
64
+ `)
65
+ .action(async (projectArg: string | undefined, opts) => {
66
+ requireToken()
67
+ const projectId = requireProjectId(projectArg)
68
+ const current = await appRequest(`/api/projects/${projectId}/auth`)
69
+ const auth = current?.auth ?? current ?? {}
70
+ if (opts.mode) auth.mode = opts.mode
71
+ if (opts.provider && opts.enabled !== undefined) {
72
+ if (!auth.providers) auth.providers = {}
73
+ const existing = auth.providers[opts.provider]
74
+ const providerConfig = typeof existing === 'object' && existing ? existing : {}
75
+ auth.providers[opts.provider] = { ...providerConfig, enabled: opts.enabled === 'true' }
76
+ }
77
+ const result = await withSpinner('Updating auth config...', () =>
78
+ appRequest(`/api/projects/${projectId}/auth`, { method: 'PUT', body: auth })
79
+ )
80
+ if (isJsonMode()) return printJson(result)
81
+ printSuccess('Auth config updated')
82
+ })
83
+
84
+ registerByocCommands(authConfig)
85
+ }
86
+
87
+ function registerByocCommands(authConfig: Command) {
88
+ authConfig.command('byoc-set [project_id]')
89
+ .description('Set BYOC (Bring Your Own Credentials) for an OAuth provider')
90
+ .requiredOption('--provider <name>', 'Provider (google/github/microsoft/apple)')
91
+ .requiredOption('--client-id <id>', 'OAuth client ID')
92
+ .requiredOption('--client-secret <secret>', 'OAuth client secret')
93
+ .addHelpText('after', `
94
+ Examples:
95
+ $ blink auth-config byoc-set --provider google --client-id xxx --client-secret yyy
96
+ $ blink auth-config byoc-set proj_xxx --provider github --client-id xxx --client-secret yyy
97
+ `)
98
+ .action(async (projectArg: string | undefined, opts) => {
99
+ requireToken()
100
+ const projectId = requireProjectId(projectArg)
101
+ const body = { provider: opts.provider, client_id: opts.clientId, client_secret: opts.clientSecret }
102
+ await withSpinner('Setting BYOC credentials...', () =>
103
+ appRequest(`/api/projects/${projectId}/auth/byoc`, { body })
104
+ )
105
+ if (isJsonMode()) return printJson({ status: 'ok', provider: opts.provider })
106
+ printSuccess(`BYOC credentials set for ${opts.provider}`)
107
+ })
108
+
109
+ authConfig.command('byoc-remove [project_id]')
110
+ .description('Remove BYOC credentials for an OAuth provider')
111
+ .requiredOption('--provider <name>', 'Provider (google/github/microsoft/apple)')
112
+ .addHelpText('after', `
113
+ Examples:
114
+ $ blink auth-config byoc-remove --provider google
115
+ $ blink auth-config byoc-remove proj_xxx --provider github
116
+ `)
117
+ .action(async (projectArg: string | undefined, opts) => {
118
+ requireToken()
119
+ const projectId = requireProjectId(projectArg)
120
+ await withSpinner('Removing BYOC credentials...', () =>
121
+ appRequest(`/api/projects/${projectId}/auth/byoc`, {
122
+ method: 'DELETE',
123
+ body: { provider: opts.provider },
124
+ })
125
+ )
126
+ if (isJsonMode()) return printJson({ status: 'ok', provider: opts.provider })
127
+ printSuccess(`BYOC credentials removed for ${opts.provider}`)
128
+ })
129
+ }
@@ -0,0 +1,175 @@
1
+ import { Command } from 'commander'
2
+ import { appRequest } from '../lib/api-app.js'
3
+ import { requireToken } from '../lib/auth.js'
4
+ import { requireProjectId } from '../lib/project.js'
5
+ import { printJson, printSuccess, printKv, printUrl, isJsonMode, withSpinner, printError } from '../lib/output.js'
6
+ import { readdirSync, readFileSync, statSync } from 'node:fs'
7
+ import { join, relative } from 'node:path'
8
+ import chalk from 'chalk'
9
+
10
+ const EXCLUDED_FILES = new Set(['package-lock.json', 'bun.lockb', 'yarn.lock', 'pnpm-lock.yaml'])
11
+
12
+ function collectBackendFiles(dir: string): Array<{ path: string; source_code: string }> {
13
+ const files: Array<{ path: string; source_code: string }> = []
14
+ function walk(current: string) {
15
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
16
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue
17
+ const fullPath = join(current, entry.name)
18
+ if (entry.isDirectory()) { walk(fullPath); continue }
19
+ if (EXCLUDED_FILES.has(entry.name)) continue
20
+ const ext = entry.name.split('.').pop()?.toLowerCase() ?? ''
21
+ if (!['ts', 'js', 'tsx', 'jsx', 'json'].includes(ext)) continue
22
+ files.push({
23
+ path: join('backend', relative(dir, fullPath)),
24
+ source_code: readFileSync(fullPath, 'utf-8'),
25
+ })
26
+ }
27
+ }
28
+ walk(dir)
29
+ return files
30
+ }
31
+
32
+ function resolveProjectAndDir(arg1?: string, arg2?: string): { projectId: string; dir: string } {
33
+ if (arg2) return { projectId: requireProjectId(arg1), dir: arg2 }
34
+ if (arg1?.startsWith('proj_')) return { projectId: requireProjectId(arg1), dir: 'backend' }
35
+ return { projectId: requireProjectId(), dir: arg1 ?? 'backend' }
36
+ }
37
+
38
+ export function registerBackendCommands(program: Command) {
39
+ const backend = program.command('backend')
40
+ .description('Manage backend (CF Workers for Platforms) for your project')
41
+ .addHelpText('after', `
42
+ Deploy, monitor, and manage your project's backend worker.
43
+ The backend is a Hono server running on Cloudflare Workers for Platforms.
44
+
45
+ Examples:
46
+ $ blink backend deploy Deploy backend/ folder
47
+ $ blink backend deploy ./server Deploy from custom dir
48
+ $ blink backend status Check deployment status
49
+ $ blink backend logs View recent logs
50
+ $ blink backend delete --yes Remove backend worker
51
+ `)
52
+
53
+ registerBackendDeploy(backend)
54
+ registerBackendStatus(backend)
55
+ registerBackendLogs(backend)
56
+ registerBackendDelete(backend)
57
+ }
58
+
59
+ function registerBackendDeploy(backend: Command) {
60
+ backend.command('deploy [project_id] [dir]')
61
+ .description('Deploy backend files to CF Workers for Platforms')
62
+ .addHelpText('after', `
63
+ Reads all .ts/.js/.json files from the backend/ directory (or specified dir),
64
+ bundles and deploys them as a Cloudflare Worker.
65
+
66
+ Examples:
67
+ $ blink backend deploy Deploy backend/ for linked project
68
+ $ blink backend deploy ./server Deploy from custom directory
69
+ $ blink backend deploy proj_xxx Deploy backend/ for specific project
70
+ $ blink backend deploy proj_xxx ./api Deploy ./api for specific project
71
+ `)
72
+ .action(async (arg1?: string, arg2?: string) => {
73
+ requireToken()
74
+ const { projectId, dir } = resolveProjectAndDir(arg1, arg2)
75
+ if (!statSync(dir, { throwIfNoEntry: false })?.isDirectory()) {
76
+ printError(`Directory not found: ${dir}`, `Create a backend/ folder with index.ts`)
77
+ process.exit(1)
78
+ }
79
+ const files = collectBackendFiles(dir)
80
+ if (!files.length) {
81
+ printError(`No .ts/.js files found in ${dir}`)
82
+ process.exit(1)
83
+ }
84
+ if (!isJsonMode()) console.log(chalk.dim(` ${files.length} file${files.length === 1 ? '' : 's'} in ${dir}`))
85
+ const result = await withSpinner('Deploying backend...', () =>
86
+ appRequest(`/api/v1/projects/${projectId}/backend/deploy`, { body: { files } })
87
+ )
88
+ if (isJsonMode()) return printJson(result)
89
+ printSuccess('Backend deployed')
90
+ if (result?.url) printUrl('URL', result.url)
91
+ printKv('Project', projectId)
92
+ })
93
+ }
94
+
95
+ function registerBackendStatus(backend: Command) {
96
+ backend.command('status [project_id]')
97
+ .description('Check backend deployment status')
98
+ .addHelpText('after', `
99
+ Examples:
100
+ $ blink backend status
101
+ $ blink backend status proj_xxx
102
+ $ blink backend status --json
103
+ `)
104
+ .action(async (projectArg?: string) => {
105
+ requireToken()
106
+ const projectId = requireProjectId(projectArg)
107
+ const result = await withSpinner('Checking status...', () =>
108
+ appRequest(`/api/v1/projects/${projectId}/backend/status`)
109
+ )
110
+ if (isJsonMode()) return printJson(result)
111
+ printKv('Project', projectId)
112
+ printKv('Enabled', result?.enabled ? 'yes' : 'no')
113
+ printKv('Tier', result?.tier ?? '-')
114
+ printKv('Requests', String(result?.requests_this_month ?? 0))
115
+ if (result?.enabled) printUrl('URL', `https://${projectId.slice(-8)}.backend.blink.new`)
116
+ })
117
+ }
118
+
119
+ function registerBackendLogs(backend: Command) {
120
+ backend.command('logs [project_id]')
121
+ .description('View recent backend logs')
122
+ .option('--minutes <n>', 'Minutes of logs to fetch', '30')
123
+ .option('--slug <name>', 'Function slug', 'index')
124
+ .addHelpText('after', `
125
+ Examples:
126
+ $ blink backend logs Last 30 min of logs
127
+ $ blink backend logs --minutes 60 Last hour
128
+ $ blink backend logs proj_xxx --minutes 5
129
+ $ blink backend logs --json
130
+ `)
131
+ .action(async (projectArg: string | undefined, opts) => {
132
+ requireToken()
133
+ const projectId = requireProjectId(projectArg)
134
+ const slug = opts.slug ?? 'index'
135
+ const since = new Date(Date.now() - parseInt(opts.minutes ?? '30') * 60 * 1000).toISOString()
136
+ const result = await withSpinner('Fetching logs...', () =>
137
+ appRequest(`/api/v1/projects/${projectId}/functions/${slug}/logs?since=${since}`)
138
+ )
139
+ if (isJsonMode()) return printJson(result)
140
+ const logs: Array<{ timestamp?: string; method?: string; path?: string; status_code?: number; latency_ms?: number; error?: string }> = result?.logs ?? result ?? []
141
+ if (!logs.length) { console.log(chalk.dim('(no logs)')); return }
142
+ for (const log of logs) {
143
+ const ts = log.timestamp ? chalk.dim(new Date(log.timestamp).toLocaleTimeString()) + ' ' : ''
144
+ const err = log.error ? chalk.red(` ${log.error}`) : ''
145
+ console.log(`${ts}${log.method ?? ''} ${log.path ?? ''} ${log.status_code ?? ''} ${log.latency_ms ?? ''}ms${err}`)
146
+ }
147
+ })
148
+ }
149
+
150
+ function registerBackendDelete(backend: Command) {
151
+ backend.command('delete [project_id]')
152
+ .description('Delete the backend worker')
153
+ .option('--yes', 'Skip confirmation')
154
+ .addHelpText('after', `
155
+ Examples:
156
+ $ blink backend delete --yes
157
+ $ blink backend delete proj_xxx --yes
158
+ `)
159
+ .action(async (projectArg: string | undefined, opts) => {
160
+ requireToken()
161
+ const projectId = requireProjectId(projectArg)
162
+ const skipConfirm = opts.yes || process.argv.includes('--yes') || process.argv.includes('-y')
163
+ if (!skipConfirm && !isJsonMode() && process.stdout.isTTY) {
164
+ const { confirm } = await import('@clack/prompts')
165
+ const ok = await confirm({ message: `Delete backend for project ${projectId}? This cannot be undone.` })
166
+ if (!ok) { console.log('Cancelled.'); return }
167
+ }
168
+ // TODO: needs /api/v1/ route with PAT auth (Phase 1a)
169
+ await withSpinner('Deleting backend...', () =>
170
+ appRequest(`/api/project/${projectId}/backend`, { method: 'DELETE' })
171
+ )
172
+ if (isJsonMode()) return printJson({ status: 'ok', project_id: projectId })
173
+ printSuccess(`Backend deleted for ${projectId}`)
174
+ })
175
+ }
@@ -0,0 +1,56 @@
1
+ import { Command } from 'commander'
2
+ import { appRequest } from '../lib/api-app.js'
3
+ import { requireToken } from '../lib/auth.js'
4
+ import { printJson, printKv, isJsonMode, withSpinner } from '../lib/output.js'
5
+ import chalk from 'chalk'
6
+
7
+ function printUsage(data: { total?: number; breakdown?: Array<{ category: string; amount: number }> }) {
8
+ console.log()
9
+ printKv('Total', `${data.total ?? 0} credits used`)
10
+ const breakdown = data.breakdown ?? []
11
+ if (breakdown.length) {
12
+ console.log()
13
+ console.log(chalk.bold('Breakdown:'))
14
+ for (const item of breakdown) printKv(` ${item.category}`, `${item.amount}`)
15
+ }
16
+ console.log()
17
+ }
18
+
19
+ export function registerBillingCommands(program: Command) {
20
+ program.command('credits')
21
+ .description('Check your credit balance and tier')
22
+ .addHelpText('after', `
23
+ Examples:
24
+ $ blink credits
25
+ $ blink credits --json
26
+ `)
27
+ .action(async () => {
28
+ requireToken()
29
+ const result = await withSpinner('Loading usage...', () =>
30
+ appRequest('/api/usage/summary?period=month')
31
+ )
32
+ if (isJsonMode()) return printJson(result)
33
+ const data = result ?? {}
34
+ console.log()
35
+ printKv('Used', `${data.total ?? 0} credits this month`)
36
+ console.log()
37
+ })
38
+
39
+ program.command('usage')
40
+ .description('Show usage summary for the current billing period')
41
+ .option('--period <granularity>', 'Period granularity: hour, day, week, month', 'day')
42
+ .addHelpText('after', `
43
+ Examples:
44
+ $ blink usage
45
+ $ blink usage --period month
46
+ $ blink usage --json
47
+ `)
48
+ .action(async (opts) => {
49
+ requireToken()
50
+ const result = await withSpinner('Loading usage...', () =>
51
+ appRequest(`/api/usage/summary?period=${opts.period}`)
52
+ )
53
+ if (isJsonMode()) return printJson(result)
54
+ printUsage(result)
55
+ })
56
+ }