@blinkdotnew/cli 0.3.7 → 0.4.1

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.
@@ -0,0 +1,211 @@
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, createTable } from '../lib/output.js'
6
+ import chalk from 'chalk'
7
+
8
+ function printDomainRow(d: { id?: string; domain: string; verified?: boolean; dns_type?: string }) {
9
+ const status = d.verified ? chalk.green('verified') : chalk.yellow('pending')
10
+ return [d.id ?? '-', d.domain, status, d.dns_type ?? '-']
11
+ }
12
+
13
+ export function registerDomainsCommands(program: Command) {
14
+ const domains = program.command('domains')
15
+ .description('Custom domain management — add, verify, remove, purchase, and connect')
16
+ .addHelpText('after', `
17
+ Manage custom domains for your Blink projects.
18
+ Search and purchase domains directly, or connect existing ones.
19
+
20
+ Examples:
21
+ $ blink domains list List domains on linked project
22
+ $ blink domains add example.com Add a custom domain
23
+ $ blink domains verify dom_xxx Trigger DNS verification
24
+ $ blink domains remove dom_xxx --yes Remove a domain
25
+ $ blink domains search cool-app Search available domains
26
+ $ blink domains purchase cool-app.com Purchase a domain
27
+ $ blink domains my List your purchased domains
28
+ $ blink domains connect example.com Connect a purchased domain to project
29
+ `)
30
+
31
+ domains.command('list [project_id]')
32
+ .description('List all domains on a project')
33
+ .addHelpText('after', `
34
+ Examples:
35
+ $ blink domains list List domains for linked project
36
+ $ blink domains list proj_xxx List domains for specific project
37
+ $ blink domains list --json Machine-readable output
38
+ `)
39
+ .action(async (projectArg: string | undefined) => {
40
+ requireToken()
41
+ const projectId = requireProjectId(projectArg)
42
+ const result = await withSpinner('Loading domains...', () =>
43
+ appRequest(`/api/project/${projectId}/domains`)
44
+ )
45
+ if (isJsonMode()) return printJson(result)
46
+ const items: Array<{ id?: string; domain: string; verified?: boolean; dns_type?: string }> = result?.domains ?? result ?? []
47
+ if (!items.length) { console.log(chalk.dim('(no domains — use `blink domains add <domain>`)'));return }
48
+ const table = createTable(['ID', 'Domain', 'Status', 'DNS'])
49
+ for (const d of items) table.push(printDomainRow(d))
50
+ console.log(table.toString())
51
+ })
52
+
53
+ domains.command('add <domain> [project_id]')
54
+ .description('Add a custom domain to a project')
55
+ .addHelpText('after', `
56
+ Examples:
57
+ $ blink domains add example.com Add to linked project
58
+ $ blink domains add example.com proj_xxx Add to specific project
59
+ `)
60
+ .action(async (domain: string, projectArg: string | undefined) => {
61
+ requireToken()
62
+ const projectId = requireProjectId(projectArg)
63
+ const result = await withSpinner(`Adding ${domain}...`, () =>
64
+ appRequest(`/api/project/${projectId}/domains`, { body: { domain } })
65
+ )
66
+ if (isJsonMode()) return printJson(result)
67
+ printSuccess(`Domain ${domain} added`)
68
+ const domainId = result?.domain?.id ?? result?.id
69
+ if (domainId) printKv('Domain ID', domainId)
70
+ console.log(chalk.dim('Run `blink domains verify ' + (domainId ?? '<domain_id>') + '` after configuring DNS'))
71
+ })
72
+
73
+ registerDomainActions(domains)
74
+ registerGlobalDomainCommands(domains)
75
+ }
76
+
77
+ function registerDomainActions(domains: Command) {
78
+ domains.command('verify <domain_id> [project_id]')
79
+ .description('Trigger DNS verification for a domain')
80
+ .addHelpText('after', `
81
+ Examples:
82
+ $ blink domains verify dom_xxx
83
+ $ blink domains verify dom_xxx proj_xxx
84
+ `)
85
+ .action(async (domainId: string, projectArg: string | undefined) => {
86
+ requireToken()
87
+ const projectId = requireProjectId(projectArg)
88
+ const result = await withSpinner('Verifying DNS...', () =>
89
+ appRequest(`/api/project/${projectId}/domains/${domainId}`, { method: 'POST' })
90
+ )
91
+ if (isJsonMode()) return printJson(result)
92
+ const verified = result?.hostname_status === 'active'
93
+ if (verified) printSuccess('Domain verified')
94
+ else console.log(chalk.yellow('!') + ' DNS not yet propagated — try again in a few minutes')
95
+ })
96
+
97
+ domains.command('remove <domain_id> [project_id]')
98
+ .description('Remove a domain from a project')
99
+ .option('--yes', 'Skip confirmation')
100
+ .addHelpText('after', `
101
+ Examples:
102
+ $ blink domains remove dom_xxx --yes
103
+ $ blink domains remove dom_xxx proj_xxx
104
+ `)
105
+ .action(async (domainId: string, projectArg: string | undefined, opts) => {
106
+ requireToken()
107
+ const projectId = requireProjectId(projectArg)
108
+ if (!shouldSkipConfirm(opts)) {
109
+ const { confirm } = await import('@clack/prompts')
110
+ const ok = await confirm({ message: `Remove domain ${domainId}?` })
111
+ if (!ok) { console.log('Cancelled.'); return }
112
+ }
113
+ await withSpinner('Removing domain...', () =>
114
+ appRequest(`/api/project/${projectId}/domains/${domainId}`, { method: 'DELETE' })
115
+ )
116
+ if (isJsonMode()) return printJson({ status: 'ok', domain_id: domainId })
117
+ printSuccess('Domain removed')
118
+ })
119
+ }
120
+
121
+ function registerGlobalDomainCommands(domains: Command) {
122
+ domains.command('search <query>')
123
+ .description('Search available domains for purchase (no auth needed)')
124
+ .addHelpText('after', `
125
+ Examples:
126
+ $ blink domains search cool-app
127
+ $ blink domains search my-startup --json
128
+ `)
129
+ .action(async (query: string) => {
130
+ const result = await withSpinner('Searching domains...', () =>
131
+ appRequest(`/api/domains/search?q=${encodeURIComponent(query)}`)
132
+ )
133
+ if (isJsonMode()) return printJson(result)
134
+ const items: Array<{ domain: string; available?: boolean; price?: string }> = result?.results ?? result ?? []
135
+ if (!items.length) { console.log(chalk.dim('No results')); return }
136
+ const table = createTable(['Domain', 'Available', 'Price'])
137
+ for (const d of items) {
138
+ const avail = d.available ? chalk.green('yes') : chalk.red('no')
139
+ table.push([d.domain, avail, d.price ?? '-'])
140
+ }
141
+ console.log(table.toString())
142
+ })
143
+
144
+ // TODO: Purchase requires a multi-step flow (contact info, payment confirmation).
145
+ // Currently only sends domain + period — will fail without full contact details.
146
+ domains.command('purchase <domain>')
147
+ .description('Purchase a domain')
148
+ .option('--period <years>', 'Registration period in years', '1')
149
+ .addHelpText('after', `
150
+ Examples:
151
+ $ blink domains purchase cool-app.com
152
+ $ blink domains purchase cool-app.com --period 2
153
+ $ blink domains purchase cool-app.com --json
154
+ `)
155
+ .option('--yes', 'Skip confirmation')
156
+ .action(async (domain: string, opts) => {
157
+ requireToken()
158
+ if (!shouldSkipConfirm(opts)) {
159
+ const { confirm } = await import('@clack/prompts')
160
+ const ok = await confirm({ message: `Purchase ${domain} for ${opts.period} year(s)? This will charge your account.` })
161
+ if (!ok) { console.log('Cancelled.'); return }
162
+ }
163
+ const result = await withSpinner(`Purchasing ${domain}...`, () =>
164
+ appRequest('/api/domains/purchase', { body: { domain, period: Number(opts.period) } })
165
+ )
166
+ if (isJsonMode()) return printJson(result)
167
+ printSuccess(`Domain ${domain} purchased`)
168
+ if (result?.domain_id) printKv('Domain ID', result.domain_id)
169
+ })
170
+
171
+ domains.command('connect <domain> [project_id]')
172
+ .description('Connect a purchased domain to a project')
173
+ .addHelpText('after', `
174
+ Examples:
175
+ $ blink domains connect example.com Connect to linked project
176
+ $ blink domains connect example.com proj_xxx Connect to specific project
177
+ `)
178
+ .action(async (domain: string, projectArg: string | undefined) => {
179
+ requireToken()
180
+ const projectId = requireProjectId(projectArg)
181
+ const result = await withSpinner(`Connecting ${domain}...`, () =>
182
+ appRequest('/api/domains/connect', { body: { domainId: domain, projectId } })
183
+ )
184
+ if (isJsonMode()) return printJson(result)
185
+ printSuccess(`${domain} connected to ${projectId}`)
186
+ })
187
+
188
+ domains.command('my')
189
+ .description('List your purchased domains')
190
+ .addHelpText('after', `
191
+ Examples:
192
+ $ blink domains my
193
+ $ blink domains my --json
194
+ `)
195
+ .action(async () => {
196
+ requireToken()
197
+ const result = await withSpinner('Loading your domains...', () =>
198
+ appRequest('/api/domains/my')
199
+ )
200
+ if (isJsonMode()) return printJson(result)
201
+ const items: Array<{ domain: string; status?: string; expiration_date?: string }> = result?.domains ?? result ?? []
202
+ if (!items.length) { console.log(chalk.dim('(no purchased domains)')); return }
203
+ const table = createTable(['Domain', 'Status', 'Expires'])
204
+ for (const d of items) table.push([d.domain, d.status ?? '-', d.expiration_date ?? '-'])
205
+ console.log(table.toString())
206
+ })
207
+ }
208
+
209
+ function shouldSkipConfirm(opts: { yes?: boolean }) {
210
+ return opts.yes || process.argv.includes('--yes') || process.argv.includes('-y') || isJsonMode() || !process.stdout.isTTY
211
+ }
@@ -0,0 +1,215 @@
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, isJsonMode, withSpinner, createTable, printError } from '../lib/output.js'
6
+ import { readFileSync, existsSync } from 'node:fs'
7
+ import chalk from 'chalk'
8
+
9
+ async function resolveSecretId(projectId: string, keyName: string): Promise<string> {
10
+ const result = await appRequest(`/api/projects/${projectId}/secrets`)
11
+ const secrets: Array<{ id: string; key: string }> = result?.secrets ?? result ?? []
12
+ const match = secrets.find(s => s.key === keyName)
13
+ if (!match) {
14
+ printError(`Env var "${keyName}" not found in project ${projectId}`)
15
+ process.exit(1)
16
+ }
17
+ return match.id
18
+ }
19
+
20
+ function parseEnvFile(filePath: string): Array<{ key: string; value: string }> {
21
+ if (!existsSync(filePath)) {
22
+ printError(`File not found: ${filePath}`)
23
+ process.exit(1)
24
+ }
25
+ const content = readFileSync(filePath, 'utf-8')
26
+ const entries: Array<{ key: string; value: string }> = []
27
+ for (const line of content.split('\n')) {
28
+ const trimmed = line.trim()
29
+ if (!trimmed || trimmed.startsWith('#')) continue
30
+ const eqIdx = trimmed.indexOf('=')
31
+ if (eqIdx === -1) continue
32
+ const key = trimmed.slice(0, eqIdx).trim()
33
+ let value = trimmed.slice(eqIdx + 1).trim()
34
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
35
+ value = value.slice(1, -1)
36
+ }
37
+ if (key) entries.push({ key, value })
38
+ }
39
+ return entries
40
+ }
41
+
42
+ export function registerEnvCommands(program: Command) {
43
+ const env = program.command('env')
44
+ .description('Manage project environment variables (secrets)')
45
+ .addHelpText('after', `
46
+ Environment variables are encrypted key-value pairs for your project.
47
+ They are injected at runtime in backend functions and edge workers.
48
+ Use "env pull > .env.local" to export as a file.
49
+
50
+ Note: For Claw agent secrets, use "blink secrets" instead.
51
+
52
+ Examples:
53
+ $ blink env list List env vars
54
+ $ blink env set DATABASE_URL postgres://...
55
+ $ blink env delete OLD_KEY
56
+ $ blink env push .env Bulk import from .env file
57
+ $ blink env pull > .env.local Export all vars to stdout
58
+ `)
59
+
60
+ registerEnvList(env)
61
+ registerEnvSet(env)
62
+ registerEnvDelete(env)
63
+ registerEnvPush(env)
64
+ registerEnvPull(env)
65
+ }
66
+
67
+ function registerEnvList(env: Command) {
68
+ env.command('list [project_id]')
69
+ .description('List environment variables')
70
+ .addHelpText('after', `
71
+ Examples:
72
+ $ blink env list List for linked project
73
+ $ blink env list proj_xxx List for specific project
74
+ $ blink env list --json Machine-readable output
75
+ `)
76
+ .action(async (projectArg: string | undefined) => {
77
+ requireToken()
78
+ const projectId = requireProjectId(projectArg)
79
+ const result = await withSpinner('Loading env vars...', () =>
80
+ appRequest(`/api/projects/${projectId}/secrets`)
81
+ )
82
+ if (isJsonMode()) return printJson(result)
83
+ const secrets: Array<{ key: string; value?: string }> = result?.secrets ?? result ?? []
84
+ if (!secrets.length) {
85
+ console.log(chalk.dim('(no env vars — use `blink env set KEY value`)'))
86
+ return
87
+ }
88
+ const table = createTable(['Key', 'Value'])
89
+ for (const s of secrets) table.push([chalk.bold(s.key), s.value ?? ''])
90
+ console.log(table.toString())
91
+ console.log(chalk.dim(`\n${secrets.length} variable${secrets.length === 1 ? '' : 's'}`))
92
+ })
93
+ }
94
+
95
+ function registerEnvSet(env: Command) {
96
+ env.command('set <key> <value>')
97
+ .description('Create or update an environment variable')
98
+ .option('--project <id>', 'Project ID (defaults to linked project)')
99
+ .addHelpText('after', `
100
+ Examples:
101
+ $ blink env set DATABASE_URL postgres://user:pass@host/db
102
+ $ blink env set STRIPE_KEY sk_live_xxx
103
+ $ blink env set API_SECRET mysecret --project proj_xxx
104
+ `)
105
+ .action(async (key: string, value: string, opts) => {
106
+ requireToken()
107
+ const projectId = requireProjectId(opts.project)
108
+ await withSpinner(`Setting ${key}...`, () =>
109
+ appRequest(`/api/projects/${projectId}/secrets`, {
110
+ method: 'POST',
111
+ body: { key, value },
112
+ })
113
+ )
114
+ if (isJsonMode()) return printJson({ status: 'ok', key, project_id: projectId })
115
+ printSuccess(`${key} saved to project ${projectId}`)
116
+ })
117
+ }
118
+
119
+ function registerEnvDelete(env: Command) {
120
+ env.command('delete <key>')
121
+ .description('Delete an environment variable')
122
+ .option('--project <id>', 'Project ID (defaults to linked project)')
123
+ .option('--yes', 'Skip confirmation')
124
+ .addHelpText('after', `
125
+ Examples:
126
+ $ blink env delete OLD_KEY
127
+ $ blink env delete OLD_KEY --yes
128
+ $ blink env delete OLD_KEY --project proj_xxx
129
+ `)
130
+ .action(async (key: string, opts) => {
131
+ requireToken()
132
+ const projectId = requireProjectId(opts.project)
133
+ const skipConfirm = opts.yes || process.argv.includes('--yes') || process.argv.includes('-y')
134
+ if (!skipConfirm && !isJsonMode() && process.stdout.isTTY) {
135
+ const { confirm } = await import('@clack/prompts')
136
+ const ok = await confirm({ message: `Delete env var "${key}" from project ${projectId}?` })
137
+ if (!ok) { console.log('Cancelled.'); return }
138
+ }
139
+ const secretId = await resolveSecretId(projectId, key)
140
+ await withSpinner(`Deleting ${key}...`, () =>
141
+ appRequest(`/api/projects/${projectId}/secrets/${secretId}`, { method: 'DELETE' })
142
+ )
143
+ if (isJsonMode()) return printJson({ status: 'ok', key, project_id: projectId })
144
+ printSuccess(`Deleted ${key}`)
145
+ })
146
+ }
147
+
148
+ function registerEnvPush(env: Command) {
149
+ env.command('push <file>')
150
+ .description('Bulk import environment variables from a .env file')
151
+ .option('--project <id>', 'Project ID (defaults to linked project)')
152
+ .option('--yes', 'Skip confirmation')
153
+ .addHelpText('after', `
154
+ Reads KEY=VALUE pairs from the file and upserts each one.
155
+ Lines starting with # and blank lines are skipped.
156
+
157
+ Examples:
158
+ $ blink env push .env
159
+ $ blink env push .env.production --project proj_xxx
160
+ $ blink env push .env --yes
161
+ `)
162
+ .action(async (file: string, opts) => {
163
+ requireToken()
164
+ const projectId = requireProjectId(opts.project)
165
+ const entries = parseEnvFile(file)
166
+ if (!entries.length) {
167
+ printError(`No KEY=VALUE pairs found in ${file}`)
168
+ process.exit(1)
169
+ }
170
+ const skipConfirm = opts.yes || process.argv.includes('--yes') || process.argv.includes('-y') || isJsonMode() || !process.stdout.isTTY
171
+ if (!skipConfirm) {
172
+ console.log(chalk.dim(` Will upsert ${entries.length} vars: ${entries.map(e => e.key).join(', ')}`))
173
+ const { confirm } = await import('@clack/prompts')
174
+ const ok = await confirm({ message: `Push ${entries.length} env vars to project ${projectId}?` })
175
+ if (!ok) { console.log('Cancelled.'); return }
176
+ }
177
+ await withSpinner(`Pushing ${entries.length} vars from ${file}...`, async () => {
178
+ for (const { key, value } of entries) {
179
+ await appRequest(`/api/projects/${projectId}/secrets`, {
180
+ method: 'POST',
181
+ body: { key, value },
182
+ })
183
+ }
184
+ })
185
+ if (isJsonMode()) return printJson({ status: 'ok', count: entries.length, project_id: projectId })
186
+ printSuccess(`Pushed ${entries.length} env var${entries.length === 1 ? '' : 's'} to ${projectId}`)
187
+ })
188
+ }
189
+
190
+ function registerEnvPull(env: Command) {
191
+ env.command('pull [project_id]')
192
+ .description('Export all environment variables as KEY=VALUE to stdout')
193
+ .addHelpText('after', `
194
+ Prints all env vars as KEY=VALUE lines to stdout.
195
+ Pipe to a file to create a local .env:
196
+
197
+ Examples:
198
+ $ blink env pull > .env.local
199
+ $ blink env pull proj_xxx > .env.production
200
+ $ blink env pull --json
201
+ `)
202
+ .action(async (projectArg: string | undefined) => {
203
+ requireToken()
204
+ const projectId = requireProjectId(projectArg)
205
+ const result = await appRequest(`/api/projects/${projectId}/secrets`)
206
+ const secrets: Array<{ key: string; value?: string }> = result?.secrets ?? result ?? []
207
+ if (isJsonMode()) return printJson(secrets)
208
+ if (process.stdout.isTTY) {
209
+ process.stderr.write(chalk.yellow('⚠ Printing secret values to terminal. Pipe to file: blink env pull > .env.local\n'))
210
+ }
211
+ for (const s of secrets) {
212
+ process.stdout.write(`${s.key}=${s.value ?? ''}\n`)
213
+ }
214
+ })
215
+ }
@@ -0,0 +1,136 @@
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, isJsonMode, withSpinner, createTable } from '../lib/output.js'
6
+ import chalk from 'chalk'
7
+
8
+ const toDate = (ts: any) => new Date(typeof ts === 'number' && ts < 1e11 ? ts * 1000 : ts)
9
+
10
+ export function registerFunctionsCommands(program: Command) {
11
+ const fns = program.command('functions')
12
+ .description('Manage edge functions (legacy) for your project')
13
+ .addHelpText('after', `
14
+ List, inspect, and manage edge functions deployed to your project.
15
+ For the newer backend worker (CF Workers for Platforms), use "blink backend".
16
+
17
+ Examples:
18
+ $ blink functions list List all functions
19
+ $ blink functions get index Get function details
20
+ $ blink functions logs index View function logs
21
+ $ blink functions delete old-fn --yes Delete a function
22
+ `)
23
+
24
+ registerFnList(fns)
25
+ registerFnGet(fns)
26
+ registerFnDelete(fns)
27
+ registerFnLogs(fns)
28
+ }
29
+
30
+ function registerFnList(fns: Command) {
31
+ fns.command('list [project_id]')
32
+ .description('List all edge functions')
33
+ .addHelpText('after', `
34
+ Examples:
35
+ $ blink functions list
36
+ $ blink functions list proj_xxx
37
+ $ blink functions list --json
38
+ `)
39
+ .action(async (projectArg?: string) => {
40
+ requireToken()
41
+ const projectId = requireProjectId(projectArg)
42
+ const result = await withSpinner('Loading functions...', () =>
43
+ appRequest(`/api/v1/projects/${projectId}/functions`)
44
+ )
45
+ const functions: Array<{ slug: string; status?: string; created_at?: string }> = result?.functions ?? result ?? []
46
+ if (isJsonMode()) return printJson(functions)
47
+ if (!functions.length) { console.log(chalk.dim('(no functions)')); return }
48
+ const table = createTable(['Slug', 'Status', 'Created'])
49
+ for (const fn of functions) {
50
+ table.push([fn.slug, fn.status ?? '-', fn.created_at ? toDate(fn.created_at).toLocaleDateString() : '-'])
51
+ }
52
+ console.log(table.toString())
53
+ })
54
+ }
55
+
56
+ function registerFnGet(fns: Command) {
57
+ fns.command('get <slug> [project_id]')
58
+ .description('Get details of a specific function')
59
+ .addHelpText('after', `
60
+ Examples:
61
+ $ blink functions get index
62
+ $ blink functions get my-api proj_xxx
63
+ $ blink functions get index --json
64
+ `)
65
+ .action(async (slug: string, projectArg?: string) => {
66
+ requireToken()
67
+ const projectId = requireProjectId(projectArg)
68
+ const result = await withSpinner(`Loading ${slug}...`, () =>
69
+ appRequest(`/api/v1/projects/${projectId}/functions/${slug}`)
70
+ )
71
+ if (isJsonMode()) return printJson(result)
72
+ const fn = result?.function ?? result
73
+ console.log(chalk.bold('Slug: ') + (fn?.slug ?? slug))
74
+ if (fn?.status) console.log(chalk.bold('Status: ') + fn.status)
75
+ if (fn?.url) console.log(chalk.bold('URL: ') + chalk.cyan(fn.url))
76
+ if (fn?.created_at) console.log(chalk.bold('Created: ') + toDate(fn.created_at).toLocaleString())
77
+ })
78
+ }
79
+
80
+ function registerFnDelete(fns: Command) {
81
+ fns.command('delete <slug> [project_id]')
82
+ .description('Delete an edge function')
83
+ .option('--yes', 'Skip confirmation')
84
+ .addHelpText('after', `
85
+ Examples:
86
+ $ blink functions delete old-fn --yes
87
+ $ blink functions delete my-api proj_xxx --yes
88
+ `)
89
+ .action(async (slug: string, projectArg: string | undefined, opts) => {
90
+ requireToken()
91
+ const projectId = requireProjectId(projectArg)
92
+ const skipConfirm = opts.yes || process.argv.includes('--yes') || process.argv.includes('-y')
93
+ if (!skipConfirm && !isJsonMode() && process.stdout.isTTY) {
94
+ const { confirm } = await import('@clack/prompts')
95
+ const ok = await confirm({ message: `Delete function "${slug}" from project ${projectId}?` })
96
+ if (!ok) { console.log('Cancelled.'); return }
97
+ }
98
+ await withSpinner(`Deleting ${slug}...`, () =>
99
+ appRequest(`/api/v1/projects/${projectId}/functions/${slug}`, { method: 'DELETE' })
100
+ )
101
+ if (isJsonMode()) return printJson({ status: 'ok', slug, project_id: projectId })
102
+ printSuccess(`Deleted function ${slug}`)
103
+ })
104
+ }
105
+
106
+ function registerFnLogs(fns: Command) {
107
+ fns.command('logs <slug> [project_id]')
108
+ .description('View logs for a specific function')
109
+ .option('--minutes <n>', 'Minutes of logs to fetch', '30')
110
+ .option('--limit <n>', 'Max number of log entries')
111
+ .addHelpText('after', `
112
+ Examples:
113
+ $ blink functions logs index Last 30 min
114
+ $ blink functions logs my-api --minutes 60 Last hour
115
+ $ blink functions logs index proj_xxx --limit 100
116
+ $ blink functions logs index --json
117
+ `)
118
+ .action(async (slug: string, projectArg: string | undefined, opts) => {
119
+ requireToken()
120
+ const projectId = requireProjectId(projectArg)
121
+ const since = new Date(Date.now() - parseInt(opts.minutes ?? '30') * 60 * 1000).toISOString()
122
+ const params = new URLSearchParams({ since })
123
+ if (opts.limit) params.set('limit', opts.limit)
124
+ const result = await withSpinner('Fetching logs...', () =>
125
+ appRequest(`/api/v1/projects/${projectId}/functions/${slug}/logs?${params}`)
126
+ )
127
+ if (isJsonMode()) return printJson(result)
128
+ const logs: Array<{ timestamp?: string; message?: string; level?: string }> = result?.logs ?? result ?? []
129
+ if (!logs.length) { console.log(chalk.dim('(no logs)')); return }
130
+ for (const log of logs) {
131
+ const ts = log.timestamp ? chalk.dim(new Date(log.timestamp).toLocaleTimeString()) + ' ' : ''
132
+ const level = log.level ? chalk.yellow(`[${log.level}] `) : ''
133
+ console.log(`${ts}${level}${log.message ?? JSON.stringify(log)}`)
134
+ }
135
+ })
136
+ }
@@ -0,0 +1,117 @@
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
+ const STATE_COLORS: Record<string, (s: string) => string> = {
9
+ active: chalk.green,
10
+ inactive: chalk.dim,
11
+ grace: chalk.yellow,
12
+ offline: chalk.red,
13
+ }
14
+
15
+ function printHostingStatus(data: Record<string, unknown>) {
16
+ const state = String(data.status ?? data.state ?? 'unknown')
17
+ const colorFn = STATE_COLORS[state] ?? chalk.white
18
+ printKv('State', colorFn(state))
19
+ if (data.hosting_tier) printKv('Tier', String(data.hosting_tier))
20
+ if (data.hosting_prod_url) printKv('URL', String(data.hosting_prod_url))
21
+ }
22
+
23
+ export function registerHostingCommands(program: Command) {
24
+ const hosting = program.command('hosting')
25
+ .description('Hosting management — activate, deactivate, and check status')
26
+ .addHelpText('after', `
27
+ Manage hosting for your Blink project.
28
+ Hosting gives your project a live URL on blinkpowered.com or a custom domain.
29
+
30
+ Examples:
31
+ $ blink hosting status Check hosting state and URLs
32
+ $ blink hosting activate Activate hosting
33
+ $ blink hosting deactivate --yes Deactivate hosting
34
+ $ blink hosting reactivate Reactivate after deactivation
35
+ `)
36
+
37
+ hosting.command('status [project_id]')
38
+ .description('Show hosting state, tier, and URLs')
39
+ .addHelpText('after', `
40
+ Examples:
41
+ $ blink hosting status Status for linked project
42
+ $ blink hosting status proj_xxx Status for specific project
43
+ $ blink hosting status --json Machine-readable output
44
+ `)
45
+ .action(async (projectArg: string | undefined) => {
46
+ requireToken()
47
+ const projectId = requireProjectId(projectArg)
48
+ const result = await withSpinner('Loading hosting status...', () =>
49
+ appRequest(`/api/project/${projectId}/hosting/status`)
50
+ )
51
+ if (isJsonMode()) return printJson(result)
52
+ printHostingStatus(result)
53
+ })
54
+
55
+ hosting.command('activate [project_id]')
56
+ .description('Activate hosting for a project')
57
+ .addHelpText('after', `
58
+ Examples:
59
+ $ blink hosting activate
60
+ $ blink hosting activate proj_xxx
61
+ `)
62
+ .action(async (projectArg: string | undefined) => {
63
+ requireToken()
64
+ const projectId = requireProjectId(projectArg)
65
+ const result = await withSpinner('Activating hosting...', () =>
66
+ appRequest(`/api/project/${projectId}/hosting/activate`, { method: 'POST' })
67
+ )
68
+ if (isJsonMode()) return printJson(result)
69
+ printSuccess('Hosting activated')
70
+ if (result?.hosting_prod_url) printKv('URL', result.hosting_prod_url)
71
+ })
72
+
73
+ hosting.command('deactivate [project_id]')
74
+ .description('Deactivate hosting for a project')
75
+ .option('--yes', 'Skip confirmation')
76
+ .addHelpText('after', `
77
+ Examples:
78
+ $ blink hosting deactivate --yes
79
+ $ blink hosting deactivate proj_xxx
80
+ `)
81
+ .action(async (projectArg: string | undefined, opts) => {
82
+ requireToken()
83
+ const projectId = requireProjectId(projectArg)
84
+ if (!shouldSkipConfirm(opts)) {
85
+ const { confirm } = await import('@clack/prompts')
86
+ const ok = await confirm({ message: 'Deactivate hosting? Your site will go offline.' })
87
+ if (!ok) { console.log('Cancelled.'); return }
88
+ }
89
+ await withSpinner('Deactivating hosting...', () =>
90
+ appRequest(`/api/project/${projectId}/hosting/deactivate`, { method: 'POST' })
91
+ )
92
+ if (isJsonMode()) return printJson({ status: 'ok', project_id: projectId })
93
+ printSuccess('Hosting deactivated')
94
+ })
95
+
96
+ hosting.command('reactivate [project_id]')
97
+ .description('Reactivate hosting after deactivation')
98
+ .addHelpText('after', `
99
+ Examples:
100
+ $ blink hosting reactivate
101
+ $ blink hosting reactivate proj_xxx
102
+ `)
103
+ .action(async (projectArg: string | undefined) => {
104
+ requireToken()
105
+ const projectId = requireProjectId(projectArg)
106
+ const result = await withSpinner('Reactivating hosting...', () =>
107
+ appRequest(`/api/project/${projectId}/hosting/reactivate`, { method: 'POST' })
108
+ )
109
+ if (isJsonMode()) return printJson(result)
110
+ printSuccess('Hosting reactivated')
111
+ if (result?.hosting_prod_url) printKv('URL', result.hosting_prod_url)
112
+ })
113
+ }
114
+
115
+ function shouldSkipConfirm(opts: { yes?: boolean }) {
116
+ return opts.yes || process.argv.includes('--yes') || process.argv.includes('-y') || isJsonMode() || !process.stdout.isTTY
117
+ }