invoke.ts156 lines · main
1import { apiCall, ApiCallError } from '../api-client.js';
2import { readCredentials } from '../config.js';
3import { readProjectConfig } from '../project-config.js';
4import { banner, blankLine, error as printError, step, success } from '../output.js';
5
6interface InvokeResponse {
7 ok: boolean;
8 value?: unknown;
9 code?: string;
10 message?: string;
11 durationMs?: number;
12 deploymentId?: string;
13 touchedTables?: string[];
14}
15
16interface Args {
17 functionName: string | null;
18 body: unknown;
19 raw: boolean;
20 bodyError: string | null;
21}
22
23function parse(argv: readonly string[]): Args {
24 const out: Args = { functionName: null, body: null, raw: false, bodyError: null };
25 let bodySources = 0;
26 for (let i = 0; i < argv.length; i++) {
27 const arg = argv[i]!;
28 if (arg === '--body' && argv[i + 1]) {
29 bodySources++;
30 const raw = argv[++i]!;
31 try {
32 out.body = JSON.parse(raw);
33 } catch {
34 out.bodyError = `--body is not valid json: ${raw.slice(0, 60)}${raw.length > 60 ? '…' : ''}`;
35 }
36 } else if (arg === '--body-file' && argv[i + 1]) {
37 bodySources++;
38 out.bodyError = `__BODY_FILE__:${argv[++i]!}`; // marker; resolved async below
39 } else if (arg === '--raw') {
40 out.raw = true;
41 } else if (arg === '--help' || arg === '-h') {
42 out.functionName = '__HELP__';
43 } else if (!arg.startsWith('--') && out.functionName === null) {
44 out.functionName = arg;
45 }
46 }
47 if (bodySources > 1 && !out.bodyError?.startsWith('__BODY_FILE__')) {
48 // Only flag conflict if --body parsed; --body-file is handled below.
49 out.bodyError = '--body and --body-file cannot be combined';
50 }
51 return out;
52}
53
54function printUsage(): void {
55 banner('invoke');
56 blankLine();
57 step('usage: briven invoke <function-name> [--body <json>] [--body-file <path>] [--raw]');
58 step(' --body <json> inline JSON request body (default: null)');
59 step(' --body-file <path> read JSON body from a file');
60 step(' --raw print only the function return value (unwrapped)');
61}
62
63export async function runInvoke(argv: readonly string[]): Promise<number> {
64 const args = parse(argv);
65
66 if (args.functionName === '__HELP__') {
67 printUsage();
68 return 0;
69 }
70 if (!args.functionName) {
71 printUsage();
72 return 1;
73 }
74
75 // Resolve --body-file lazily so we don't import fs unless needed.
76 if (args.bodyError?.startsWith('__BODY_FILE__:')) {
77 const path = args.bodyError.slice('__BODY_FILE__:'.length);
78 args.bodyError = null;
79 try {
80 const { readFile } = await import('node:fs/promises');
81 const text = await readFile(path, 'utf8');
82 args.body = JSON.parse(text);
83 } catch (err) {
84 printError(`could not read --body-file: ${err instanceof Error ? err.message : 'unknown'}`);
85 return 1;
86 }
87 }
88 if (args.bodyError) {
89 printError(args.bodyError);
90 return 1;
91 }
92
93 const local = await readProjectConfig();
94 const file = await readCredentials();
95 const targetId = local?.projectId ?? file.default;
96 if (!targetId) {
97 printError('no linked project found.');
98 step('run: briven login --project <p_...> --key <brk_...>');
99 return 1;
100 }
101
102 const cred = file.projects[targetId];
103 if (!cred) {
104 printError(`no credentials stored for ${targetId}`);
105 step('run: briven login --project <id> --key <brk_...>');
106 return 1;
107 }
108
109 banner('invoke');
110 step(`project ${targetId}`);
111 step(`function ${args.functionName}`);
112 step(`origin ${cred.apiOrigin}`);
113
114 let res: InvokeResponse;
115 try {
116 res = await apiCall<InvokeResponse>(
117 `/v1/projects/${targetId}/functions/${encodeURIComponent(args.functionName)}`,
118 {
119 apiOrigin: cred.apiOrigin,
120 apiKey: cred.apiKey,
121 method: 'POST',
122 body: args.body ?? {},
123 },
124 );
125 } catch (err) {
126 blankLine();
127 if (err instanceof ApiCallError) {
128 printError(`${err.code} (${err.status}): ${err.message}`);
129 } else {
130 printError(err instanceof Error ? err.message : 'unknown error');
131 }
132 return 1;
133 }
134
135 blankLine();
136 if (args.raw) {
137 // Unwrapped: just the function's return value, raw JSON. Lets callers
138 // pipe through jq without needing to peel `.value`.
139 process.stdout.write(`${JSON.stringify(res.ok ? res.value : { code: res.code, message: res.message }, null, 2)}\n`);
140 return res.ok ? 0 : 1;
141 }
142
143 if (res.ok) {
144 if (typeof res.durationMs === 'number') step(`took ${res.durationMs}ms`);
145 if (res.touchedTables && res.touchedTables.length > 0) {
146 step(`tables ${res.touchedTables.join(', ')}`);
147 }
148 success('returned:');
149 process.stdout.write(`${JSON.stringify(res.value, null, 2)}\n`);
150 return 0;
151 }
152
153 printError(`${res.code ?? 'unknown_error'}: ${res.message ?? 'no message'}`);
154 if (typeof res.durationMs === 'number') step(`took ${res.durationMs}ms`);
155 return 1;
156}