link.ts129 lines · main
1import { createInterface } from 'node:readline';
2
3import { apiCall, ApiCallError } from '../api-client.js';
4import { readCredentials } from '../config.js';
5import { readProjectConfig, writeProjectConfig } from '../project-config.js';
6import {
7 banner,
8 blankLine,
9 error as printError,
10 link as printLink,
11 step,
12 success,
13} from '../output.js';
14
15interface ParsedArgs {
16 projectId?: string;
17}
18
19function 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
32export 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
110function 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
120async 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}