@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.
@@ -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
+ }
@@ -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 { errMsg = JSON.parse(errText).error ?? errMsg } catch { /* use default */ }
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') ?? ''
@@ -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
- console.error('Error: Not authenticated. Run `blink login --interactive` or set BLINK_API_KEY env var.')
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') {
@@ -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
- console.error('Error: No project linked. Run `blink link <project_id>` or use `blink use <project_id>`.')
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