@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/README.md +152 -321
- package/dist/cli.js +1536 -78
- package/package.json +28 -2
- package/src/cli.ts +88 -1
- package/src/commands/auth-config.ts +129 -0
- package/src/commands/backend.ts +175 -0
- package/src/commands/billing.ts +56 -0
- package/src/commands/domains.ts +211 -0
- package/src/commands/env.ts +215 -0
- package/src/commands/functions.ts +136 -0
- package/src/commands/hosting.ts +117 -0
- package/src/commands/init.ts +77 -0
- package/src/commands/security.ts +136 -0
- package/src/commands/tokens.ts +98 -0
- package/src/commands/versions.ts +95 -0
- package/src/commands/workspace.ts +130 -0
- package/src/lib/api-app.ts +7 -1
- package/src/lib/api-resources.ts +3 -0
- package/src/lib/auth.ts +6 -1
- package/src/lib/config.ts +2 -2
- package/src/lib/project.ts +7 -1
|
@@ -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
|
+
}
|