deploy.ts168 lines · main
1import { diff, type Change, type SchemaDef } from '@briven/schema';
2
3import { apiCall, ApiCallError } from '../api-client.js';
4import { discoverFunctions, loadProjectSchema } from '../bundler.js';
5import { readCredentials } from '../config.js';
6import { readProjectConfig } from '../project-config.js';
7import { banner, blankLine, error as printError, step, success } from '../output.js';
8
9interface DeploymentResponse {
10 deployment: {
11 id: string;
12 status: 'pending' | 'running' | 'succeeded' | 'failed' | 'cancelled';
13 createdAt: string;
14 };
15}
16
17interface CurrentSchemaResponse {
18 deploymentId: string | null;
19 snapshot: SchemaDef | null;
20}
21
22interface Args {
23 confirmDestructive: boolean;
24 dryRun: boolean;
25}
26
27function parse(argv: readonly string[]): Args {
28 const out: Args = { confirmDestructive: false, dryRun: false };
29 for (const arg of argv) {
30 if (arg === '--confirm-destructive') out.confirmDestructive = true;
31 else if (arg === '--dry-run') out.dryRun = true;
32 }
33 return out;
34}
35
36export async function runDeploy(argv: readonly string[]): Promise<number> {
37 const args = parse(argv);
38 const local = await readProjectConfig();
39 if (!local) {
40 printError('no briven.json in this directory.');
41 step('run: briven init');
42 return 1;
43 }
44
45 const targetId = local.projectId;
46 if (!targetId) {
47 printError('briven.json has no projectId — link this directory first.');
48 step('run: briven link');
49 return 1;
50 }
51
52 const creds = await readCredentials();
53 const cred = creds.projects[targetId];
54 if (!cred) {
55 printError(`no stored credentials for ${targetId}.`);
56 step('run: briven login --project <id> --key <brk_...>');
57 return 1;
58 }
59
60 banner('deploy');
61 step(`project ${targetId}`);
62 step(`origin ${cred.apiOrigin}`);
63
64 step('loading briven/schema.ts');
65 let nextSchema: SchemaDef | null;
66 try {
67 nextSchema = await loadProjectSchema(process.cwd());
68 } catch (err) {
69 printError(err instanceof Error ? err.message : 'failed to load schema');
70 return 1;
71 }
72 if (!nextSchema) {
73 printError('briven/schema.ts not found — run `briven init` first.');
74 return 1;
75 }
76
77 step('discovering briven/functions');
78 const functions = await discoverFunctions(process.cwd());
79
80 step('fetching current deployed schema');
81 let current: CurrentSchemaResponse;
82 try {
83 current = await apiCall<CurrentSchemaResponse>(`/v1/projects/${targetId}/schema/current`, {
84 apiOrigin: cred.apiOrigin,
85 apiKey: cred.apiKey,
86 });
87 } catch (err) {
88 if (err instanceof ApiCallError) {
89 printError(`server rejected: ${err.code} (${err.status})`);
90 } else {
91 printError(err instanceof Error ? err.message : 'unknown error');
92 }
93 return 1;
94 }
95
96 const result = diff(current.snapshot, nextSchema);
97 blankLine();
98 if (result.changes.length === 0) {
99 step('no schema changes');
100 } else {
101 step(`schema changes (${result.changes.length}):`);
102 for (const c of result.changes) {
103 step(` ${formatChange(c)}`);
104 }
105 }
106 step(`functions: ${functions.count}`);
107
108 if (result.destructive && !args.confirmDestructive) {
109 blankLine();
110 printError(
111 'destructive changes detected (drop table / drop column). Re-run with --confirm-destructive.',
112 );
113 return 1;
114 }
115
116 if (args.dryRun) {
117 blankLine();
118 success('dry run — nothing sent to the server');
119 return 0;
120 }
121
122 const summary = summarise(result.changes);
123
124 step('creating deployment...');
125 try {
126 const res = await apiCall<DeploymentResponse>(`/v1/projects/${targetId}/deployments`, {
127 method: 'POST',
128 apiOrigin: cred.apiOrigin,
129 apiKey: cred.apiKey,
130 body: {
131 schemaDiffSummary: summary,
132 schemaSnapshot: nextSchema,
133 functionCount: functions.count,
134 functionNames: functions.names,
135 bundle: functions.bundle,
136 },
137 });
138 blankLine();
139 success(`deployment ${res.deployment.id} · ${res.deployment.status}`);
140 return 0;
141 } catch (err) {
142 if (err instanceof ApiCallError) {
143 printError(`deploy failed: ${err.code} (${err.status})`);
144 } else {
145 printError(err instanceof Error ? err.message : 'unknown error');
146 }
147 return 1;
148 }
149}
150
151function formatChange(c: Change): string {
152 switch (c.kind) {
153 case 'create_table':
154 return `+ table ${c.table}`;
155 case 'drop_table':
156 return `- table ${c.table}`;
157 case 'add_column':
158 return `+ ${c.table}.${c.column}`;
159 case 'drop_column':
160 return `- ${c.table}.${c.column}`;
161 }
162}
163
164function summarise(changes: readonly Change[]): Record<string, number> {
165 const counts: Record<string, number> = {};
166 for (const c of changes) counts[c.kind] = (counts[c.kind] ?? 0) + 1;
167 return counts;
168}