link.ts129 lines · main
| 1 | import { createInterface } from 'node:readline'; |
| 2 | |
| 3 | import { apiCall, ApiCallError } from '../api-client.js'; |
| 4 | import { readCredentials } from '../config.js'; |
| 5 | import { readProjectConfig, writeProjectConfig } from '../project-config.js'; |
| 6 | import { |
| 7 | banner, |
| 8 | blankLine, |
| 9 | error as printError, |
| 10 | link as printLink, |
| 11 | step, |
| 12 | success, |
| 13 | } from '../output.js'; |
| 14 | |
| 15 | interface ParsedArgs { |
| 16 | projectId?: string; |
| 17 | } |
| 18 | |
| 19 | function parse(argv: readonly string[]): ParsedArgs { |
| 20 | const out: ParsedArgs = {}; |
| 21 | for (let i = 0; i < argv.length; i += 1) { |
| 22 | const arg = argv[i]; |
| 23 | if (arg === '--project' && argv[i + 1]) { |
| 24 | out.projectId = argv[++i]; |
| 25 | } else if (arg && arg.startsWith('--project=')) { |
| 26 | out.projectId = arg.slice('--project='.length); |
| 27 | } |
| 28 | } |
| 29 | return out; |
| 30 | } |
| 31 | |
| 32 | export async function runLink(argv: readonly string[]): Promise<number> { |
| 33 | if (argv.includes('--help') || argv.includes('-h')) { |
| 34 | printHelp(); |
| 35 | return 0; |
| 36 | } |
| 37 | |
| 38 | const args = parse(argv); |
| 39 | const local = await readProjectConfig(); |
| 40 | if (!local) { |
| 41 | printError('no briven.json in this directory.'); |
| 42 | step('run: briven init'); |
| 43 | return 1; |
| 44 | } |
| 45 | |
| 46 | const creds = await readCredentials(); |
| 47 | const known = Object.values(creds.projects); |
| 48 | |
| 49 | if (known.length === 0) { |
| 50 | printError('no logged-in projects.'); |
| 51 | step('run: briven connect && briven projects use <p_...>'); |
| 52 | step(' or: briven login --project <p_...> --key <brk_...>'); |
| 53 | return 1; |
| 54 | } |
| 55 | |
| 56 | let projectId: string; |
| 57 | |
| 58 | if (args.projectId) { |
| 59 | if (!creds.projects[args.projectId]) { |
| 60 | printError(`no credentials for ${args.projectId}.`); |
| 61 | step(`run: briven login --project ${args.projectId} --key <brk_...>`); |
| 62 | return 1; |
| 63 | } |
| 64 | projectId = args.projectId; |
| 65 | } else if (known.length === 1) { |
| 66 | projectId = known[0]!.projectId; |
| 67 | } else { |
| 68 | banner('link'); |
| 69 | blankLine(); |
| 70 | step('available projects:'); |
| 71 | known.forEach((p, i) => { |
| 72 | step(` ${i + 1}. ${p.projectId} ····${p.suffix}`); |
| 73 | }); |
| 74 | blankLine(); |
| 75 | const pick = await promptLine(` · pick [1-${known.length}]: `); |
| 76 | const idx = Number.parseInt(pick.trim(), 10) - 1; |
| 77 | if (Number.isNaN(idx) || idx < 0 || idx >= known.length) { |
| 78 | printError('invalid selection'); |
| 79 | return 1; |
| 80 | } |
| 81 | projectId = known[idx]!.projectId; |
| 82 | } |
| 83 | |
| 84 | const cred = creds.projects[projectId]!; |
| 85 | banner(`link ${projectId}`); |
| 86 | step(`verifying via ${cred.apiOrigin}`); |
| 87 | |
| 88 | try { |
| 89 | await apiCall(`/v1/projects/${projectId}/info`, { |
| 90 | apiOrigin: cred.apiOrigin, |
| 91 | apiKey: cred.apiKey, |
| 92 | }); |
| 93 | } catch (err) { |
| 94 | if (err instanceof ApiCallError) { |
| 95 | printError(`server rejected the credentials: ${err.code} (${err.status})`); |
| 96 | } else { |
| 97 | printError(err instanceof Error ? err.message : 'unknown error'); |
| 98 | } |
| 99 | return 1; |
| 100 | } |
| 101 | |
| 102 | await writeProjectConfig({ ...local, projectId }); |
| 103 | blankLine(); |
| 104 | success(`linked ${local.name} → ${projectId}`); |
| 105 | step('next: briven deploy | briven dev'); |
| 106 | printLink('https://docs.briven.tech/cli'); |
| 107 | return 0; |
| 108 | } |
| 109 | |
| 110 | function printHelp(): void { |
| 111 | banner('link'); |
| 112 | blankLine(); |
| 113 | step('briven link pick from logged-in projects'); |
| 114 | step('briven link --project p_abc123 link to a specific project'); |
| 115 | blankLine(); |
| 116 | step('writes the project id into briven.json.'); |
| 117 | step('requires a local project key first: briven projects use <p_...> or briven login.'); |
| 118 | } |
| 119 | |
| 120 | async function promptLine(prompt: string): Promise<string> { |
| 121 | process.stdout.write(prompt); |
| 122 | return new Promise((resolvePromise) => { |
| 123 | const rl = createInterface({ input: process.stdin, output: process.stdout }); |
| 124 | rl.once('line', (line) => { |
| 125 | rl.close(); |
| 126 | resolvePromise(line); |
| 127 | }); |
| 128 | }); |
| 129 | } |