@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,77 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { appRequest } from '../lib/api-app.js'
|
|
3
|
+
import { requireToken } from '../lib/auth.js'
|
|
4
|
+
import { writeProjectConfig } from '../lib/project.js'
|
|
5
|
+
import { printJson, printSuccess, printKv, isJsonMode, withSpinner, printError } from '../lib/output.js'
|
|
6
|
+
import { basename } from 'node:path'
|
|
7
|
+
import chalk from 'chalk'
|
|
8
|
+
|
|
9
|
+
export function registerInitCommands(program: Command) {
|
|
10
|
+
program.command('init')
|
|
11
|
+
.description('Initialize a new Blink project and link it to the current directory')
|
|
12
|
+
.option('--name <name>', 'Project name (defaults to current directory name)')
|
|
13
|
+
.option('--from <project_id>', 'Create a new project named after an existing one')
|
|
14
|
+
.addHelpText('after', `
|
|
15
|
+
Creates a new Blink project and writes .blink/project.json in the current directory.
|
|
16
|
+
After init, all commands work without specifying a project_id.
|
|
17
|
+
|
|
18
|
+
Examples:
|
|
19
|
+
$ blink init Create project named after current dir
|
|
20
|
+
$ blink init --name "My SaaS App" Create with custom name
|
|
21
|
+
$ blink init --from proj_xxx Clone from existing project
|
|
22
|
+
$ blink init --json Machine-readable output
|
|
23
|
+
|
|
24
|
+
After init:
|
|
25
|
+
$ blink deploy ./dist --prod
|
|
26
|
+
$ blink db query "SELECT * FROM users"
|
|
27
|
+
$ blink env set DATABASE_URL postgres://...
|
|
28
|
+
`)
|
|
29
|
+
.action(async (opts) => {
|
|
30
|
+
requireToken()
|
|
31
|
+
if (opts.from) {
|
|
32
|
+
await initFromExisting(opts.from)
|
|
33
|
+
} else {
|
|
34
|
+
await initNew(opts.name)
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function initNew(nameOpt?: string) {
|
|
40
|
+
const name = nameOpt ?? basename(process.cwd())
|
|
41
|
+
const result = await withSpinner(`Creating project "${name}"...`, () =>
|
|
42
|
+
appRequest('/api/projects/create', { method: 'POST', body: { prompt: name } })
|
|
43
|
+
)
|
|
44
|
+
const proj = result?.project ?? result
|
|
45
|
+
if (!proj?.id) {
|
|
46
|
+
printError('Failed to create project — no ID returned')
|
|
47
|
+
process.exit(1)
|
|
48
|
+
}
|
|
49
|
+
writeProjectConfig({ projectId: proj.id })
|
|
50
|
+
if (isJsonMode()) return printJson({ project_id: proj.id, name: proj.name ?? name })
|
|
51
|
+
printSuccess(`Project created and linked`)
|
|
52
|
+
printKv('ID', proj.id)
|
|
53
|
+
printKv('Name', proj.name ?? name)
|
|
54
|
+
console.log(chalk.dim('\n Run `blink deploy ./dist --prod` to deploy'))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function initFromExisting(sourceId: string) {
|
|
58
|
+
const source = await withSpinner('Loading source project...', () =>
|
|
59
|
+
appRequest(`/api/projects/${sourceId}`)
|
|
60
|
+
)
|
|
61
|
+
const sourceName = source?.project?.name ?? source?.name ?? sourceId
|
|
62
|
+
const name = `${sourceName} (copy)`
|
|
63
|
+
const result = await withSpinner(`Creating project "${name}"...`, () =>
|
|
64
|
+
appRequest('/api/projects/create', { method: 'POST', body: { prompt: name } })
|
|
65
|
+
)
|
|
66
|
+
const proj = result?.project ?? result
|
|
67
|
+
if (!proj?.id) {
|
|
68
|
+
printError('Failed to create project — no ID returned')
|
|
69
|
+
process.exit(1)
|
|
70
|
+
}
|
|
71
|
+
writeProjectConfig({ projectId: proj.id })
|
|
72
|
+
if (isJsonMode()) return printJson({ project_id: proj.id, name: proj.name ?? name, from: sourceId })
|
|
73
|
+
printSuccess(`Project created from ${sourceId} and linked`)
|
|
74
|
+
printKv('ID', proj.id)
|
|
75
|
+
printKv('Name', proj.name ?? name)
|
|
76
|
+
printKv('From', sourceId)
|
|
77
|
+
}
|
|
@@ -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, isJsonMode, withSpinner, printSuccess, createTable } from '../lib/output.js'
|
|
6
|
+
import chalk from 'chalk'
|
|
7
|
+
|
|
8
|
+
function printSecurityPolicy(data: { modules?: Record<string, { require_auth?: boolean }> }) {
|
|
9
|
+
const modules = data.modules ?? data
|
|
10
|
+
const table = createTable(['Module', 'Require Auth'])
|
|
11
|
+
for (const [name, cfg] of Object.entries(modules as Record<string, { require_auth?: boolean }>)) {
|
|
12
|
+
const status = cfg.require_auth ? chalk.green('yes') : chalk.dim('no')
|
|
13
|
+
table.push([name, status])
|
|
14
|
+
}
|
|
15
|
+
console.log(table.toString())
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function printCorsOrigins(origins: string[]) {
|
|
19
|
+
if (!origins.length) {
|
|
20
|
+
console.log(chalk.dim('(no CORS origins configured — all origins allowed)'))
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
for (const o of origins) console.log(` ${o}`)
|
|
24
|
+
console.log(chalk.dim(`\n${origins.length} origin${origins.length === 1 ? '' : 's'}`))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function registerSecurityCommands(program: Command) {
|
|
28
|
+
registerSecurityGroup(program)
|
|
29
|
+
registerCorsGroup(program)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function registerSecurityGroup(program: Command) {
|
|
33
|
+
const security = program.command('security')
|
|
34
|
+
.description('Security policy — require auth per module (db/ai/storage/realtime/rag)')
|
|
35
|
+
.addHelpText('after', `
|
|
36
|
+
Control which backend modules require user authentication.
|
|
37
|
+
When require_auth is true, unauthenticated requests are rejected.
|
|
38
|
+
|
|
39
|
+
Examples:
|
|
40
|
+
$ blink security get Show security policy
|
|
41
|
+
$ blink security set --module db --require-auth true
|
|
42
|
+
$ blink security set --module ai --require-auth false
|
|
43
|
+
`)
|
|
44
|
+
|
|
45
|
+
security.command('get [project_id]')
|
|
46
|
+
.description('Show current security policy')
|
|
47
|
+
.addHelpText('after', `
|
|
48
|
+
Examples:
|
|
49
|
+
$ blink security get Show policy for linked project
|
|
50
|
+
$ blink security get proj_xxx Show policy for specific project
|
|
51
|
+
$ blink security get --json Machine-readable output
|
|
52
|
+
`)
|
|
53
|
+
.action(async (projectArg: string | undefined) => {
|
|
54
|
+
requireToken()
|
|
55
|
+
const projectId = requireProjectId(projectArg)
|
|
56
|
+
const result = await withSpinner('Loading security policy...', () =>
|
|
57
|
+
appRequest(`/api/project/${projectId}/security`)
|
|
58
|
+
)
|
|
59
|
+
if (isJsonMode()) return printJson(result)
|
|
60
|
+
printSecurityPolicy(result?.policy ?? result)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
security.command('set [project_id]')
|
|
64
|
+
.description('Update security policy for a module')
|
|
65
|
+
.requiredOption('--module <name>', 'Module (db/ai/storage/realtime/rag)')
|
|
66
|
+
.requiredOption('--require-auth <bool>', 'Require auth (true/false)')
|
|
67
|
+
.addHelpText('after', `
|
|
68
|
+
Examples:
|
|
69
|
+
$ blink security set --module db --require-auth true
|
|
70
|
+
$ blink security set --module ai --require-auth false
|
|
71
|
+
$ blink security set proj_xxx --module storage --require-auth true
|
|
72
|
+
`)
|
|
73
|
+
.action(async (projectArg: string | undefined, opts) => {
|
|
74
|
+
requireToken()
|
|
75
|
+
const projectId = requireProjectId(projectArg)
|
|
76
|
+
const requireAuth = opts.requireAuth === 'true'
|
|
77
|
+
const body = { policy: { modules: { [opts.module]: { require_auth: requireAuth } } } }
|
|
78
|
+
await withSpinner('Updating security policy...', () =>
|
|
79
|
+
appRequest(`/api/project/${projectId}/security`, { method: 'PUT', body })
|
|
80
|
+
)
|
|
81
|
+
if (isJsonMode()) return printJson({ status: 'ok', module: opts.module, require_auth: requireAuth })
|
|
82
|
+
printSuccess(`${opts.module}: require_auth = ${requireAuth}`)
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function registerCorsGroup(program: Command) {
|
|
87
|
+
const cors = program.command('cors')
|
|
88
|
+
.description('CORS origin management — control which origins can access your project APIs')
|
|
89
|
+
.addHelpText('after', `
|
|
90
|
+
Manage allowed CORS origins for your project.
|
|
91
|
+
When no origins are configured, all origins are allowed.
|
|
92
|
+
|
|
93
|
+
Examples:
|
|
94
|
+
$ blink cors get Show allowed origins
|
|
95
|
+
$ blink cors set --origins https://example.com https://app.example.com
|
|
96
|
+
`)
|
|
97
|
+
|
|
98
|
+
cors.command('get [project_id]')
|
|
99
|
+
.description('Show allowed CORS origins')
|
|
100
|
+
.addHelpText('after', `
|
|
101
|
+
Examples:
|
|
102
|
+
$ blink cors get Show origins for linked project
|
|
103
|
+
$ blink cors get proj_xxx Show origins for specific project
|
|
104
|
+
$ blink cors get --json Machine-readable output
|
|
105
|
+
`)
|
|
106
|
+
.action(async (projectArg: string | undefined) => {
|
|
107
|
+
requireToken()
|
|
108
|
+
const projectId = requireProjectId(projectArg)
|
|
109
|
+
const result = await withSpinner('Loading CORS config...', () =>
|
|
110
|
+
appRequest(`/api/project/${projectId}/cors`)
|
|
111
|
+
)
|
|
112
|
+
if (isJsonMode()) return printJson(result)
|
|
113
|
+
const origins: string[] = result?.custom_origins ?? result?.origins ?? []
|
|
114
|
+
printCorsOrigins(origins)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
cors.command('set [project_id]')
|
|
118
|
+
.description('Set allowed CORS origins')
|
|
119
|
+
.requiredOption('--origins <urls...>', 'Allowed origins (space-separated)')
|
|
120
|
+
.addHelpText('after', `
|
|
121
|
+
Examples:
|
|
122
|
+
$ blink cors set --origins https://example.com
|
|
123
|
+
$ blink cors set --origins https://example.com https://app.example.com
|
|
124
|
+
$ blink cors set proj_xxx --origins https://example.com
|
|
125
|
+
`)
|
|
126
|
+
.action(async (projectArg: string | undefined, opts) => {
|
|
127
|
+
requireToken()
|
|
128
|
+
const projectId = requireProjectId(projectArg)
|
|
129
|
+
const origins: string[] = opts.origins
|
|
130
|
+
await withSpinner('Updating CORS origins...', () =>
|
|
131
|
+
appRequest(`/api/project/${projectId}/cors`, { method: 'PUT', body: { custom_origins: origins } })
|
|
132
|
+
)
|
|
133
|
+
if (isJsonMode()) return printJson({ status: 'ok', origins })
|
|
134
|
+
printSuccess(`CORS origins set: ${origins.join(', ')}`)
|
|
135
|
+
})
|
|
136
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { appRequest } from '../lib/api-app.js'
|
|
3
|
+
import { requireToken } from '../lib/auth.js'
|
|
4
|
+
import { printJson, isJsonMode, withSpinner, createTable, printSuccess } from '../lib/output.js'
|
|
5
|
+
import chalk from 'chalk'
|
|
6
|
+
|
|
7
|
+
function printTokenTable(tokens: Array<{ id: string; name: string; key_prefix: string; created_at: string; last_used_at: string }>) {
|
|
8
|
+
const table = createTable(['ID', 'Name', 'Prefix', 'Created', 'Last Used'])
|
|
9
|
+
for (const t of tokens) {
|
|
10
|
+
table.push([
|
|
11
|
+
t.id,
|
|
12
|
+
t.name ?? '-',
|
|
13
|
+
t.key_prefix ?? '-',
|
|
14
|
+
t.created_at ? new Date(t.created_at).toLocaleDateString() : '-',
|
|
15
|
+
t.last_used_at ? new Date(t.last_used_at).toLocaleDateString() : 'never',
|
|
16
|
+
])
|
|
17
|
+
}
|
|
18
|
+
console.log(table.toString())
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function printNewToken(token: { id: string; key: string; name: string }) {
|
|
22
|
+
console.log()
|
|
23
|
+
printSuccess(`Token created: ${token.name}`)
|
|
24
|
+
console.log()
|
|
25
|
+
console.log(chalk.bold(' Token: ') + chalk.green(token.key))
|
|
26
|
+
console.log()
|
|
27
|
+
console.log(chalk.yellow(' ⚠ Save this token now — it will not be shown again!'))
|
|
28
|
+
console.log(chalk.dim(' Use as: export BLINK_API_KEY=' + token.key))
|
|
29
|
+
console.log()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function registerTokenCommands(program: Command) {
|
|
33
|
+
const tokens = program.command('tokens')
|
|
34
|
+
.description('Manage personal access tokens (API keys)')
|
|
35
|
+
.addHelpText('after', `
|
|
36
|
+
Examples:
|
|
37
|
+
$ blink tokens list
|
|
38
|
+
$ blink tokens create --name "CI deploy key"
|
|
39
|
+
$ blink tokens revoke tok_xxx --yes
|
|
40
|
+
`)
|
|
41
|
+
|
|
42
|
+
tokens.command('list')
|
|
43
|
+
.description('List all personal access tokens')
|
|
44
|
+
.addHelpText('after', `
|
|
45
|
+
Examples:
|
|
46
|
+
$ blink tokens list
|
|
47
|
+
$ blink tokens list --json
|
|
48
|
+
`)
|
|
49
|
+
.action(async () => {
|
|
50
|
+
requireToken()
|
|
51
|
+
const result = await withSpinner('Loading tokens...', () => appRequest('/api/tokens'))
|
|
52
|
+
const tokensList = result?.tokens ?? result ?? []
|
|
53
|
+
if (isJsonMode()) return printJson(tokensList)
|
|
54
|
+
if (!tokensList.length) return console.log(chalk.dim('No tokens found. Create one with `blink tokens create --name "my key"`.'))
|
|
55
|
+
printTokenTable(tokensList)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
tokens.command('create')
|
|
59
|
+
.description('Create a new personal access token')
|
|
60
|
+
.requiredOption('--name <name>', 'Name for the token (e.g. "CI deploy key")')
|
|
61
|
+
.addHelpText('after', `
|
|
62
|
+
Examples:
|
|
63
|
+
$ blink tokens create --name "CI deploy key"
|
|
64
|
+
$ blink tokens create --name "staging" --json
|
|
65
|
+
`)
|
|
66
|
+
.action(async (opts) => {
|
|
67
|
+
requireToken()
|
|
68
|
+
const result = await withSpinner('Creating token...', () =>
|
|
69
|
+
appRequest('/api/tokens', { method: 'POST', body: { name: opts.name } })
|
|
70
|
+
)
|
|
71
|
+
if (isJsonMode()) return printJson(result)
|
|
72
|
+
const token = result?.token ?? result
|
|
73
|
+
printNewToken(token)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
tokens.command('revoke <token_id>')
|
|
77
|
+
.description('Revoke (delete) a personal access token')
|
|
78
|
+
.option('--yes', 'Skip confirmation')
|
|
79
|
+
.addHelpText('after', `
|
|
80
|
+
Examples:
|
|
81
|
+
$ blink tokens revoke tok_xxx
|
|
82
|
+
$ blink tokens revoke tok_xxx --yes
|
|
83
|
+
`)
|
|
84
|
+
.action(async (tokenId: string, opts) => {
|
|
85
|
+
requireToken()
|
|
86
|
+
const skipConfirm = opts.yes || process.argv.includes('--yes') || process.argv.includes('-y')
|
|
87
|
+
if (!skipConfirm && !isJsonMode() && process.stdout.isTTY) {
|
|
88
|
+
const { confirm } = await import('@clack/prompts')
|
|
89
|
+
const ok = await confirm({ message: `Revoke token ${tokenId}? This cannot be undone.` })
|
|
90
|
+
if (!ok) { console.log('Cancelled.'); return }
|
|
91
|
+
}
|
|
92
|
+
await withSpinner('Revoking token...', () =>
|
|
93
|
+
appRequest(`/api/tokens/${tokenId}`, { method: 'DELETE' })
|
|
94
|
+
)
|
|
95
|
+
if (isJsonMode()) return printJson({ status: 'ok', token_id: tokenId })
|
|
96
|
+
printSuccess(`Revoked token ${tokenId}`)
|
|
97
|
+
})
|
|
98
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
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, createTable, printSuccess } from '../lib/output.js'
|
|
6
|
+
import chalk from 'chalk'
|
|
7
|
+
|
|
8
|
+
function printVersionTable(versions: Array<{ id: string; description?: string; message?: string; created_at: string }>) {
|
|
9
|
+
const table = createTable(['ID', 'Description', 'Created'])
|
|
10
|
+
for (const v of versions) {
|
|
11
|
+
table.push([v.id, v.description ?? v.message ?? '-', new Date(v.created_at).toLocaleString()])
|
|
12
|
+
}
|
|
13
|
+
console.log(table.toString())
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function registerVersionCommands(program: Command) {
|
|
17
|
+
const versions = program.command('versions')
|
|
18
|
+
.description('Manage project version snapshots')
|
|
19
|
+
.addHelpText('after', `
|
|
20
|
+
Examples:
|
|
21
|
+
$ blink versions list
|
|
22
|
+
$ blink versions save --message "before refactor"
|
|
23
|
+
$ blink versions restore ver_xxx
|
|
24
|
+
`)
|
|
25
|
+
|
|
26
|
+
versions.command('list [project_id]')
|
|
27
|
+
.description('List saved versions for a project')
|
|
28
|
+
.addHelpText('after', `
|
|
29
|
+
Examples:
|
|
30
|
+
$ blink versions list
|
|
31
|
+
$ blink versions list proj_xxx
|
|
32
|
+
$ blink versions list --json
|
|
33
|
+
`)
|
|
34
|
+
.action(async (projectIdArg: string | undefined) => {
|
|
35
|
+
requireToken()
|
|
36
|
+
const projectId = requireProjectId(projectIdArg)
|
|
37
|
+
const result = await withSpinner('Loading versions...', () =>
|
|
38
|
+
appRequest(`/api/versions?projectId=${projectId}`)
|
|
39
|
+
)
|
|
40
|
+
const versionsList = result?.versions ?? result ?? []
|
|
41
|
+
if (isJsonMode()) return printJson(versionsList)
|
|
42
|
+
if (!versionsList.length) return console.log(chalk.dim('No versions saved yet.'))
|
|
43
|
+
printVersionTable(versionsList)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
versions.command('save [project_id]')
|
|
47
|
+
.description('Save a version snapshot of the current project state')
|
|
48
|
+
.requiredOption('--message <msg>', 'Version description (required)')
|
|
49
|
+
.addHelpText('after', `
|
|
50
|
+
Examples:
|
|
51
|
+
$ blink versions save --message "stable v1"
|
|
52
|
+
$ blink versions save proj_xxx --message "pre-deploy"
|
|
53
|
+
$ blink versions save --json
|
|
54
|
+
`)
|
|
55
|
+
.action(async (projectIdArg: string | undefined, opts) => {
|
|
56
|
+
requireToken()
|
|
57
|
+
const projectId = requireProjectId(projectIdArg)
|
|
58
|
+
const result = await withSpinner('Saving version...', () =>
|
|
59
|
+
appRequest('/api/save-version', {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
body: { projectId, description: opts.message },
|
|
62
|
+
})
|
|
63
|
+
)
|
|
64
|
+
if (isJsonMode()) return printJson(result)
|
|
65
|
+
const ver = result?.version ?? result
|
|
66
|
+
printSuccess(`Version saved: ${ver?.id ?? 'ok'}`)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
versions.command('restore <version_id> [project_id]')
|
|
70
|
+
.description('Restore a project to a saved version')
|
|
71
|
+
.option('--yes', 'Skip confirmation')
|
|
72
|
+
.addHelpText('after', `
|
|
73
|
+
Examples:
|
|
74
|
+
$ blink versions restore ver_xxx
|
|
75
|
+
$ blink versions restore ver_xxx proj_xxx --yes
|
|
76
|
+
`)
|
|
77
|
+
.action(async (versionId: string, projectIdArg: string | undefined, opts) => {
|
|
78
|
+
requireToken()
|
|
79
|
+
const projectId = requireProjectId(projectIdArg)
|
|
80
|
+
const skipConfirm = opts.yes || process.argv.includes('--yes') || process.argv.includes('-y')
|
|
81
|
+
if (!skipConfirm && !isJsonMode() && process.stdout.isTTY) {
|
|
82
|
+
const { confirm } = await import('@clack/prompts')
|
|
83
|
+
const ok = await confirm({ message: `Restore project ${projectId} to version ${versionId}?` })
|
|
84
|
+
if (!ok) { console.log('Cancelled.'); return }
|
|
85
|
+
}
|
|
86
|
+
const result = await withSpinner('Restoring version...', () =>
|
|
87
|
+
appRequest('/api/versions/restore', {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
body: { projectId, identifier: versionId },
|
|
90
|
+
})
|
|
91
|
+
)
|
|
92
|
+
if (isJsonMode()) return printJson(result)
|
|
93
|
+
printSuccess(`Restored to version ${versionId}`)
|
|
94
|
+
})
|
|
95
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { appRequest } from '../lib/api-app.js'
|
|
3
|
+
import { requireToken } from '../lib/auth.js'
|
|
4
|
+
import { printJson, isJsonMode, withSpinner, createTable, printSuccess, printError } from '../lib/output.js'
|
|
5
|
+
import chalk from 'chalk'
|
|
6
|
+
|
|
7
|
+
function printWorkspaceTable(workspaces: Array<{ id: string; name: string; slug: string; tier: string; role: string }>) {
|
|
8
|
+
const table = createTable(['ID', 'Name', 'Slug', 'Tier', 'Role'])
|
|
9
|
+
for (const w of workspaces) table.push([w.id, w.name, w.slug ?? '-', w.tier ?? '-', w.role ?? '-'])
|
|
10
|
+
console.log(table.toString())
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function printMemberTable(members: Array<{ name: string; email: string; role: string }>) {
|
|
14
|
+
const table = createTable(['Name', 'Email', 'Role'])
|
|
15
|
+
for (const m of members) table.push([m.name ?? '-', m.email, m.role ?? '-'])
|
|
16
|
+
console.log(table.toString())
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function registerWorkspaceCommands(program: Command) {
|
|
20
|
+
const workspace = program.command('workspace')
|
|
21
|
+
.description('Manage workspaces, members, and invitations')
|
|
22
|
+
.addHelpText('after', `
|
|
23
|
+
Examples:
|
|
24
|
+
$ blink workspace list
|
|
25
|
+
$ blink workspace create "My Team"
|
|
26
|
+
$ blink workspace switch wsp_xxx
|
|
27
|
+
$ blink workspace members wsp_xxx
|
|
28
|
+
$ blink workspace invite user@example.com wsp_xxx --role admin
|
|
29
|
+
`)
|
|
30
|
+
|
|
31
|
+
workspace.command('list')
|
|
32
|
+
.description('List all workspaces you belong to')
|
|
33
|
+
.addHelpText('after', `
|
|
34
|
+
Examples:
|
|
35
|
+
$ blink workspace list
|
|
36
|
+
$ blink workspace list --json
|
|
37
|
+
`)
|
|
38
|
+
.action(async () => {
|
|
39
|
+
requireToken()
|
|
40
|
+
const result = await withSpinner('Loading workspaces...', () => appRequest('/api/workspaces'))
|
|
41
|
+
const workspaces = result?.workspaces ?? result ?? []
|
|
42
|
+
if (isJsonMode()) return printJson(workspaces)
|
|
43
|
+
if (!workspaces.length) return console.log(chalk.dim('No workspaces found.'))
|
|
44
|
+
printWorkspaceTable(workspaces)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
workspace.command('create <name>')
|
|
48
|
+
.description('Create a new workspace')
|
|
49
|
+
.addHelpText('after', `
|
|
50
|
+
Examples:
|
|
51
|
+
$ blink workspace create "My Team"
|
|
52
|
+
$ blink workspace create "Acme Corp" --json
|
|
53
|
+
`)
|
|
54
|
+
.action(async (name: string) => {
|
|
55
|
+
requireToken()
|
|
56
|
+
const result = await withSpinner(`Creating "${name}"...`, () =>
|
|
57
|
+
appRequest('/api/workspaces', { method: 'POST', body: { name } })
|
|
58
|
+
)
|
|
59
|
+
if (isJsonMode()) return printJson(result)
|
|
60
|
+
const ws = result?.workspace ?? result
|
|
61
|
+
printSuccess(`Created workspace: ${ws.name} (${ws.id})`)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
workspace.command('switch <workspace_id>')
|
|
65
|
+
.description('Switch your active workspace')
|
|
66
|
+
.addHelpText('after', `
|
|
67
|
+
Examples:
|
|
68
|
+
$ blink workspace switch wsp_xxx
|
|
69
|
+
`)
|
|
70
|
+
.action(async (workspaceId: string) => {
|
|
71
|
+
requireToken()
|
|
72
|
+
await withSpinner('Switching workspace...', () =>
|
|
73
|
+
appRequest('/api/workspaces/switch', { method: 'POST', body: { workspace_id: workspaceId } })
|
|
74
|
+
)
|
|
75
|
+
if (isJsonMode()) return printJson({ status: 'ok', workspace_id: workspaceId })
|
|
76
|
+
printSuccess(`Switched to workspace ${workspaceId}`)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
workspace.command('members [workspace_id]')
|
|
80
|
+
.description('List members of a workspace')
|
|
81
|
+
.option('--workspace <id>', 'Workspace ID')
|
|
82
|
+
.addHelpText('after', `
|
|
83
|
+
Examples:
|
|
84
|
+
$ blink workspace members wsp_xxx
|
|
85
|
+
$ blink workspace members --workspace wsp_xxx
|
|
86
|
+
$ blink workspace members wsp_xxx --json
|
|
87
|
+
`)
|
|
88
|
+
.action(async (workspaceIdArg: string | undefined, opts) => {
|
|
89
|
+
requireToken()
|
|
90
|
+
const workspaceId = workspaceIdArg ?? opts.workspace
|
|
91
|
+
if (!workspaceId) {
|
|
92
|
+
printError('Workspace ID required', 'blink workspace members wsp_xxx')
|
|
93
|
+
process.exit(1)
|
|
94
|
+
}
|
|
95
|
+
const result = await withSpinner('Loading members...', () =>
|
|
96
|
+
appRequest(`/api/workspaces/${workspaceId}/members`)
|
|
97
|
+
)
|
|
98
|
+
const members = result?.members ?? result ?? []
|
|
99
|
+
if (isJsonMode()) return printJson(members)
|
|
100
|
+
if (!members.length) return console.log(chalk.dim('No members found.'))
|
|
101
|
+
printMemberTable(members)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
workspace.command('invite <email> [workspace_id]')
|
|
105
|
+
.description('Invite a member to a workspace')
|
|
106
|
+
.option('--workspace <id>', 'Workspace ID')
|
|
107
|
+
.option('--role <role>', 'Role: admin, member, viewer', 'member')
|
|
108
|
+
.addHelpText('after', `
|
|
109
|
+
Examples:
|
|
110
|
+
$ blink workspace invite user@example.com wsp_xxx
|
|
111
|
+
$ blink workspace invite user@example.com --workspace wsp_xxx --role admin
|
|
112
|
+
$ blink workspace invite user@example.com wsp_xxx --role viewer
|
|
113
|
+
`)
|
|
114
|
+
.action(async (email: string, workspaceIdArg: string | undefined, opts) => {
|
|
115
|
+
requireToken()
|
|
116
|
+
const workspaceId = workspaceIdArg ?? opts.workspace
|
|
117
|
+
if (!workspaceId) {
|
|
118
|
+
printError('Workspace ID required', 'blink workspace invite user@example.com wsp_xxx')
|
|
119
|
+
process.exit(1)
|
|
120
|
+
}
|
|
121
|
+
const result = await withSpinner(`Inviting ${email}...`, () =>
|
|
122
|
+
appRequest(`/api/workspaces/${workspaceId}/invites`, {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
body: { emails: [email], role: opts.role },
|
|
125
|
+
})
|
|
126
|
+
)
|
|
127
|
+
if (isJsonMode()) return printJson(result)
|
|
128
|
+
printSuccess(`Invited ${email} as ${opts.role} to ${workspaceId}`)
|
|
129
|
+
})
|
|
130
|
+
}
|
package/src/lib/api-app.ts
CHANGED
|
@@ -30,7 +30,13 @@ export async function appRequest(path: string, opts: RequestOptions = {}) {
|
|
|
30
30
|
if (!res.ok) {
|
|
31
31
|
const errText = await res.text()
|
|
32
32
|
let errMsg = `HTTP ${res.status}`
|
|
33
|
-
try {
|
|
33
|
+
try {
|
|
34
|
+
const parsed = JSON.parse(errText)
|
|
35
|
+
errMsg = parsed.error ?? parsed.message ?? errMsg
|
|
36
|
+
} catch { /* use default */ }
|
|
37
|
+
if (res.status === 401) errMsg += ' — check your API key (blink login --interactive)'
|
|
38
|
+
if (res.status === 403) errMsg += ' — check project permissions or workspace tier'
|
|
39
|
+
if (res.status === 404) errMsg += ' — resource not found (check project ID)'
|
|
34
40
|
throw new Error(errMsg)
|
|
35
41
|
}
|
|
36
42
|
const ct = res.headers.get('content-type') ?? ''
|
package/src/lib/api-resources.ts
CHANGED
|
@@ -43,6 +43,9 @@ export async function resourcesRequest(path: string, opts: RequestOptions = {})
|
|
|
43
43
|
else if (parsed.message) errMsg = parsed.message
|
|
44
44
|
else if (err) errMsg = JSON.stringify(err)
|
|
45
45
|
} catch { /* use default */ }
|
|
46
|
+
if (res.status === 401) errMsg += ' — check your API key (blink login --interactive)'
|
|
47
|
+
if (res.status === 403) errMsg += ' — check project permissions or workspace tier'
|
|
48
|
+
if (res.status === 404) errMsg += ' — resource not found (check project ID)'
|
|
46
49
|
throw new Error(errMsg)
|
|
47
50
|
}
|
|
48
51
|
const ct = res.headers.get('content-type') ?? ''
|
package/src/lib/auth.ts
CHANGED
|
@@ -21,7 +21,12 @@ export function resolveToken(): string | undefined {
|
|
|
21
21
|
export function requireToken(): string {
|
|
22
22
|
const token = resolveToken()
|
|
23
23
|
if (!token) {
|
|
24
|
-
|
|
24
|
+
process.stderr.write(
|
|
25
|
+
'Error: Not authenticated.\n' +
|
|
26
|
+
' Get your API key at: blink.new/settings?tab=api-keys (starts with blnk_ak_)\n' +
|
|
27
|
+
' Then: blink login --interactive Save to ~/.config/blink/config.toml\n' +
|
|
28
|
+
' or: export BLINK_API_KEY=blnk_ak_... Set env var (CI/agents)\n'
|
|
29
|
+
)
|
|
25
30
|
process.exit(1)
|
|
26
31
|
}
|
|
27
32
|
return token
|
package/src/lib/config.ts
CHANGED
|
@@ -44,10 +44,10 @@ export function readConfig(profile = 'default'): BlinkConfig | undefined {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
export function writeConfig(data: BlinkConfig, profile = 'default') {
|
|
47
|
-
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true })
|
|
47
|
+
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 })
|
|
48
48
|
const existing = existsSync(CONFIG_FILE) ? parseToml(readFileSync(CONFIG_FILE, 'utf-8')) : {}
|
|
49
49
|
existing[profile] = { ...existing[profile], ...data }
|
|
50
|
-
writeFileSync(CONFIG_FILE, serializeToml(existing))
|
|
50
|
+
writeFileSync(CONFIG_FILE, serializeToml(existing), { mode: 0o600 })
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
export function clearConfig(profile = 'default') {
|
package/src/lib/project.ts
CHANGED
|
@@ -32,7 +32,13 @@ export function resolveProjectId(explicitId?: string): string | undefined {
|
|
|
32
32
|
export function requireProjectId(explicitId?: string): string {
|
|
33
33
|
const id = resolveProjectId(explicitId)
|
|
34
34
|
if (!id) {
|
|
35
|
-
|
|
35
|
+
process.stderr.write(
|
|
36
|
+
'Error: No project context.\n' +
|
|
37
|
+
' 1. blink link <project_id> Link to current directory\n' +
|
|
38
|
+
' 2. export BLINK_ACTIVE_PROJECT=proj_xxx Set env var (CI/agents)\n' +
|
|
39
|
+
' 3. Pass project_id as argument e.g. blink db query proj_xxx "SELECT 1"\n' +
|
|
40
|
+
" Don't have a project yet? Run: blink init\n"
|
|
41
|
+
)
|
|
36
42
|
process.exit(1)
|
|
37
43
|
}
|
|
38
44
|
return id
|