opencode-supabase 0.0.3 → 0.0.4-alpha.1
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.
- package/README.md +10 -0
- package/package.json +1 -1
- package/src/server/auth.ts +43 -3
- package/src/server/index.ts +7 -2
- package/src/server/tools.ts +90 -17
- package/src/shared/broker.ts +32 -0
- package/src/shared/log.ts +57 -0
- package/src/tui/dialog.tsx +21 -0
- package/src/tui/index.tsx +6 -1
package/README.md
CHANGED
|
@@ -20,6 +20,16 @@ Launch `opencode` in your project, then run:
|
|
|
20
20
|
|
|
21
21
|
Connect your account and ask your agent about Supabase capabilities.
|
|
22
22
|
|
|
23
|
+
## Debug Logging
|
|
24
|
+
|
|
25
|
+
If you hit auth or tool errors and need logs for an issue, run OpenCode like this and share `opencode-supabase-debug.log`:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
opencode --log-level DEBUG --print-logs 2>opencode-supabase-debug.log
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Without `--print-logs`, OpenCode writes logs to its default log directory, documented as `~/.local/share/opencode/log/` on macOS/Linux and `%USERPROFILE%\.local\share\opencode\log` on Windows.
|
|
32
|
+
|
|
23
33
|
## Available today
|
|
24
34
|
|
|
25
35
|
- **Connect** your Supabase account from OpenCode
|
package/package.json
CHANGED
package/src/server/auth.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type BrokerConfig,
|
|
8
8
|
} from "../shared/broker.ts";
|
|
9
9
|
import { readSupabaseConfig } from "../shared/cfg.ts";
|
|
10
|
+
import type { SupabaseLogger } from "../shared/log.ts";
|
|
10
11
|
import { buildAuthorizeUrl, generatePKCE, generateState } from "../shared/oauth.ts";
|
|
11
12
|
import type { FetchLike, SupabaseTokenResponse } from "../shared/types.ts";
|
|
12
13
|
import { HTML_SUCCESS, htmlError } from "./auth-html.ts";
|
|
@@ -25,6 +26,8 @@ type PendingAuth = {
|
|
|
25
26
|
|
|
26
27
|
type AuthDeps = {
|
|
27
28
|
fetch?: FetchLike;
|
|
29
|
+
logger?: SupabaseLogger;
|
|
30
|
+
setCallbackTimeout?: typeof setTimeout;
|
|
28
31
|
};
|
|
29
32
|
|
|
30
33
|
let server: ReturnType<typeof Bun.serve> | undefined;
|
|
@@ -69,6 +72,10 @@ async function ensureServer(
|
|
|
69
72
|
baseUrl: config.brokerBaseUrl,
|
|
70
73
|
};
|
|
71
74
|
|
|
75
|
+
await deps.logger?.info("supabase callback server started", {
|
|
76
|
+
port,
|
|
77
|
+
});
|
|
78
|
+
|
|
72
79
|
server = Bun.serve({
|
|
73
80
|
port,
|
|
74
81
|
async fetch(req) {
|
|
@@ -78,6 +85,11 @@ async function ensureServer(
|
|
|
78
85
|
}
|
|
79
86
|
|
|
80
87
|
const state = url.searchParams.get("state");
|
|
88
|
+
await deps.logger?.debug("supabase auth callback received", {
|
|
89
|
+
has_state: Boolean(state),
|
|
90
|
+
has_code: Boolean(url.searchParams.get("code")),
|
|
91
|
+
has_error: Boolean(url.searchParams.get("error")),
|
|
92
|
+
});
|
|
81
93
|
if (!state) {
|
|
82
94
|
return new Response(htmlError("Missing required state parameter - potential CSRF attack"), {
|
|
83
95
|
status: 400,
|
|
@@ -98,6 +110,9 @@ async function ensureServer(
|
|
|
98
110
|
if (error) {
|
|
99
111
|
clearTimeout(pending.timeout);
|
|
100
112
|
pendingAuths.delete(state);
|
|
113
|
+
await deps.logger?.error("supabase auth failed", {
|
|
114
|
+
reason: "provider_denied",
|
|
115
|
+
});
|
|
101
116
|
pending.reject(new Error(errorDescription || error));
|
|
102
117
|
return new Response(htmlError(errorDescription || error), {
|
|
103
118
|
headers: { "Content-Type": "text/html" },
|
|
@@ -108,6 +123,9 @@ async function ensureServer(
|
|
|
108
123
|
if (!code) {
|
|
109
124
|
clearTimeout(pending.timeout);
|
|
110
125
|
pendingAuths.delete(state);
|
|
126
|
+
await deps.logger?.error("supabase auth failed", {
|
|
127
|
+
reason: "missing_code",
|
|
128
|
+
});
|
|
111
129
|
pending.reject(new Error("Missing authorization code"));
|
|
112
130
|
return new Response(htmlError("Missing authorization code"), {
|
|
113
131
|
status: 400,
|
|
@@ -127,6 +145,7 @@ async function ensureServer(
|
|
|
127
145
|
code_verifier: pending.codeVerifier,
|
|
128
146
|
},
|
|
129
147
|
deps.fetch,
|
|
148
|
+
deps.logger,
|
|
130
149
|
);
|
|
131
150
|
|
|
132
151
|
const expires = Date.now() + (tokens.expires_in || 3600) * 1000;
|
|
@@ -138,6 +157,10 @@ async function ensureServer(
|
|
|
138
157
|
|
|
139
158
|
pending.resolve({ tokens, expires });
|
|
140
159
|
|
|
160
|
+
await deps.logger?.info("supabase auth completed", {
|
|
161
|
+
status: "success",
|
|
162
|
+
});
|
|
163
|
+
|
|
141
164
|
return new Response(HTML_SUCCESS, {
|
|
142
165
|
headers: { "Content-Type": "text/html" },
|
|
143
166
|
});
|
|
@@ -146,6 +169,11 @@ async function ensureServer(
|
|
|
146
169
|
? `Authorization failed: ${cause.message}`
|
|
147
170
|
: "Authorization failed";
|
|
148
171
|
|
|
172
|
+
await deps.logger?.error("supabase auth failed", {
|
|
173
|
+
status: cause instanceof BrokerClientError ? cause.status : 400,
|
|
174
|
+
broker_error: cause instanceof BrokerClientError,
|
|
175
|
+
});
|
|
176
|
+
|
|
149
177
|
pending.reject(cause instanceof Error ? cause : new Error(String(cause)));
|
|
150
178
|
|
|
151
179
|
return new Response(htmlError(errorMessage), {
|
|
@@ -159,11 +187,20 @@ async function ensureServer(
|
|
|
159
187
|
serverPort = port;
|
|
160
188
|
}
|
|
161
189
|
|
|
162
|
-
function waitForCallback(
|
|
190
|
+
function waitForCallback(
|
|
191
|
+
state: string,
|
|
192
|
+
codeVerifier: string,
|
|
193
|
+
redirectUri: string,
|
|
194
|
+
deps: AuthDeps,
|
|
195
|
+
) {
|
|
163
196
|
return new Promise<{ tokens: SupabaseTokenResponse; expires: number }>((resolve, reject) => {
|
|
164
|
-
const
|
|
197
|
+
const scheduleTimeout = deps.setCallbackTimeout ?? setTimeout;
|
|
198
|
+
const timeout = scheduleTimeout(() => {
|
|
165
199
|
if (!pendingAuths.has(state)) return;
|
|
166
200
|
pendingAuths.delete(state);
|
|
201
|
+
void deps.logger?.error("supabase auth callback timed out", {
|
|
202
|
+
reason: "timeout",
|
|
203
|
+
});
|
|
167
204
|
reject(new Error("OAuth callback timeout - authorization took too long"));
|
|
168
205
|
}, CALLBACK_TIMEOUT_MS);
|
|
169
206
|
|
|
@@ -192,10 +229,13 @@ export function createSupabaseAuth(
|
|
|
192
229
|
label: "Supabase",
|
|
193
230
|
async authorize() {
|
|
194
231
|
await ensureServer(config.oauthPort, config, input, deps);
|
|
232
|
+
await deps.logger?.info("supabase auth started", {
|
|
233
|
+
port: config.oauthPort,
|
|
234
|
+
});
|
|
195
235
|
const pkce = await generatePKCE();
|
|
196
236
|
const state = generateState();
|
|
197
237
|
const redirectUri = callbackUrl(config.oauthPort);
|
|
198
|
-
const callbackPromise = waitForCallback(state, pkce.verifier, redirectUri);
|
|
238
|
+
const callbackPromise = waitForCallback(state, pkce.verifier, redirectUri, deps);
|
|
199
239
|
|
|
200
240
|
return {
|
|
201
241
|
url: buildAuthorizeUrl(config, redirectUri, pkce, state),
|
package/src/server/index.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
2
|
|
|
3
|
+
import { createServerLogWriter, createSupabaseLogger } from "../shared/log.ts";
|
|
3
4
|
import { createSupabaseAuth } from "./auth.ts";
|
|
4
5
|
import { createSupabaseTools } from "./tools.ts";
|
|
5
6
|
|
|
6
7
|
const server: Plugin = async (input, options) => {
|
|
8
|
+
const logger = createSupabaseLogger({
|
|
9
|
+
write: createServerLogWriter(input.client),
|
|
10
|
+
});
|
|
11
|
+
|
|
7
12
|
return {
|
|
8
|
-
auth: createSupabaseAuth(input, options),
|
|
9
|
-
tool: createSupabaseTools(input, options),
|
|
13
|
+
auth: createSupabaseAuth(input, options, { logger }),
|
|
14
|
+
tool: createSupabaseTools(input, options, { logger }),
|
|
10
15
|
};
|
|
11
16
|
};
|
|
12
17
|
|
package/src/server/tools.ts
CHANGED
|
@@ -8,11 +8,13 @@ import {
|
|
|
8
8
|
} from "../shared/broker.ts";
|
|
9
9
|
import { supabaseManagementApiFetch } from "../shared/api.ts";
|
|
10
10
|
import { readSupabaseConfig } from "../shared/cfg.ts";
|
|
11
|
+
import type { SupabaseLogger } from "../shared/log.ts";
|
|
11
12
|
import type { FetchLike } from "../shared/types.ts";
|
|
12
13
|
import { clearSavedAuth, readSavedAuth, writeSavedAuth, type SavedAuth } from "./store.ts";
|
|
13
14
|
|
|
14
15
|
type ToolDeps = {
|
|
15
16
|
fetch?: FetchLike;
|
|
17
|
+
logger?: SupabaseLogger;
|
|
16
18
|
};
|
|
17
19
|
|
|
18
20
|
type HostAuthWriter = {
|
|
@@ -58,40 +60,86 @@ function generateRandomString(length: number) {
|
|
|
58
60
|
.slice(0, length);
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
function sanitizeToolArgs(name: string, args: Record<string, unknown>) {
|
|
64
|
+
const next = { ...args };
|
|
65
|
+
if (name === "supabase_create_project" && typeof next.db_pass === "string") {
|
|
66
|
+
next.db_pass = "[redacted]";
|
|
67
|
+
}
|
|
68
|
+
return next;
|
|
69
|
+
}
|
|
70
|
+
|
|
61
71
|
async function executeSupabaseRequest(
|
|
62
72
|
input: SupabaseToolInput,
|
|
63
73
|
options: PluginOptions | undefined,
|
|
64
74
|
deps: ToolDeps,
|
|
75
|
+
toolName: string,
|
|
76
|
+
context: SupabaseToolContext,
|
|
65
77
|
path: string,
|
|
66
78
|
errorLabel: string,
|
|
67
79
|
init?: RequestInit,
|
|
68
80
|
) {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
const startedAt = Date.now();
|
|
82
|
+
await deps.logger?.info("supabase tool started", {
|
|
83
|
+
tool: toolName,
|
|
84
|
+
sessionID: context.sessionID,
|
|
85
|
+
messageID: context.messageID,
|
|
86
|
+
agent: context.agent,
|
|
87
|
+
});
|
|
88
|
+
try {
|
|
89
|
+
const config = readSupabaseConfig(options);
|
|
90
|
+
const auth = await ensureSupabaseToolAuth(input, options, deps);
|
|
91
|
+
const response = await supabaseManagementApiFetch(
|
|
92
|
+
config,
|
|
93
|
+
auth.access,
|
|
94
|
+
path,
|
|
95
|
+
init,
|
|
96
|
+
deps.fetch,
|
|
97
|
+
);
|
|
78
98
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
99
|
+
await deps.logger?.debug("supabase api response received", {
|
|
100
|
+
tool: toolName,
|
|
101
|
+
path,
|
|
102
|
+
status: response.status,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
const body = await response.text().catch(() => "");
|
|
107
|
+
await deps.logger?.error("supabase tool failed", {
|
|
108
|
+
tool: toolName,
|
|
109
|
+
path,
|
|
110
|
+
status: response.status,
|
|
111
|
+
});
|
|
112
|
+
throw new Error(`Failed to ${errorLabel}: ${response.status} ${body}`.trim());
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const payload = await response.json();
|
|
116
|
+
|
|
117
|
+
await deps.logger?.info("supabase tool completed", {
|
|
118
|
+
tool: toolName,
|
|
119
|
+
duration_ms: Date.now() - startedAt,
|
|
120
|
+
});
|
|
83
121
|
|
|
84
|
-
|
|
122
|
+
return JSON.stringify(payload, null, 2);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
await deps.logger?.error("supabase tool failed", {
|
|
125
|
+
tool: toolName,
|
|
126
|
+
path,
|
|
127
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
128
|
+
});
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
85
131
|
}
|
|
86
132
|
|
|
87
133
|
async function executeSupabaseGet(
|
|
88
134
|
input: SupabaseToolInput,
|
|
89
135
|
options: PluginOptions | undefined,
|
|
90
136
|
deps: ToolDeps,
|
|
137
|
+
toolName: string,
|
|
138
|
+
context: SupabaseToolContext,
|
|
91
139
|
path: string,
|
|
92
140
|
errorLabel: string,
|
|
93
141
|
) {
|
|
94
|
-
return executeSupabaseRequest(input, options, deps, path, errorLabel);
|
|
142
|
+
return executeSupabaseRequest(input, options, deps, toolName, context, path, errorLabel);
|
|
95
143
|
}
|
|
96
144
|
|
|
97
145
|
async function setHostAuth(
|
|
@@ -143,6 +191,7 @@ export async function ensureSupabaseToolAuth(
|
|
|
143
191
|
{ baseUrl: config.brokerBaseUrl },
|
|
144
192
|
{ refresh_token: saved.auth.refresh },
|
|
145
193
|
deps.fetch,
|
|
194
|
+
deps.logger,
|
|
146
195
|
);
|
|
147
196
|
|
|
148
197
|
const nextAuth: SavedAuth = {
|
|
@@ -181,14 +230,30 @@ export function createSupabaseTools(
|
|
|
181
230
|
description: "List all Supabase organizations for the authenticated user.",
|
|
182
231
|
args: {},
|
|
183
232
|
async execute(_args, _context: SupabaseToolContext) {
|
|
184
|
-
return executeSupabaseGet(
|
|
233
|
+
return executeSupabaseGet(
|
|
234
|
+
input,
|
|
235
|
+
options,
|
|
236
|
+
deps,
|
|
237
|
+
"supabase_list_organizations",
|
|
238
|
+
_context,
|
|
239
|
+
"/organizations",
|
|
240
|
+
"list organizations",
|
|
241
|
+
);
|
|
185
242
|
},
|
|
186
243
|
}),
|
|
187
244
|
supabase_list_projects: tool({
|
|
188
245
|
description: "List all Supabase projects for the authenticated user.",
|
|
189
246
|
args: {},
|
|
190
247
|
async execute(_args, _context: SupabaseToolContext) {
|
|
191
|
-
return executeSupabaseGet(
|
|
248
|
+
return executeSupabaseGet(
|
|
249
|
+
input,
|
|
250
|
+
options,
|
|
251
|
+
deps,
|
|
252
|
+
"supabase_list_projects",
|
|
253
|
+
_context,
|
|
254
|
+
"/projects",
|
|
255
|
+
"list projects",
|
|
256
|
+
);
|
|
192
257
|
},
|
|
193
258
|
}),
|
|
194
259
|
supabase_get_project_api_keys: tool({
|
|
@@ -201,6 +266,8 @@ export function createSupabaseTools(
|
|
|
201
266
|
input,
|
|
202
267
|
options,
|
|
203
268
|
deps,
|
|
269
|
+
"supabase_get_project_api_keys",
|
|
270
|
+
_context,
|
|
204
271
|
`/projects/${args.project_ref}/api-keys`,
|
|
205
272
|
"get API keys",
|
|
206
273
|
);
|
|
@@ -215,10 +282,16 @@ export function createSupabaseTools(
|
|
|
215
282
|
db_pass: tool.schema.string().describe("Database password").optional(),
|
|
216
283
|
},
|
|
217
284
|
async execute(args, _context: SupabaseToolContext) {
|
|
285
|
+
await deps.logger?.debug("supabase tool args prepared", {
|
|
286
|
+
tool: "supabase_create_project",
|
|
287
|
+
args: sanitizeToolArgs("supabase_create_project", args),
|
|
288
|
+
});
|
|
218
289
|
return executeSupabaseRequest(
|
|
219
290
|
input,
|
|
220
291
|
options,
|
|
221
292
|
deps,
|
|
293
|
+
"supabase_create_project",
|
|
294
|
+
_context,
|
|
222
295
|
"/projects",
|
|
223
296
|
"create project",
|
|
224
297
|
{
|
package/src/shared/broker.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { FetchLike, SupabaseTokenResponse } from "./types.ts";
|
|
2
|
+
import type { SupabaseLogger } from "./log.ts";
|
|
2
3
|
|
|
3
4
|
export type BrokerConfig = {
|
|
4
5
|
baseUrl: string;
|
|
@@ -95,9 +96,14 @@ async function makeBrokerRequest(
|
|
|
95
96
|
endpoint: string,
|
|
96
97
|
body: unknown,
|
|
97
98
|
fetchImpl: FetchLike,
|
|
99
|
+
logger?: SupabaseLogger,
|
|
98
100
|
): Promise<SupabaseTokenResponse> {
|
|
99
101
|
const url = `${config.baseUrl.replace(/\/$/, "")}${endpoint}`;
|
|
100
102
|
|
|
103
|
+
await logger?.debug("supabase broker request started", {
|
|
104
|
+
endpoint,
|
|
105
|
+
});
|
|
106
|
+
|
|
101
107
|
let response: Response;
|
|
102
108
|
|
|
103
109
|
try {
|
|
@@ -110,6 +116,10 @@ async function makeBrokerRequest(
|
|
|
110
116
|
body: JSON.stringify(body),
|
|
111
117
|
});
|
|
112
118
|
} catch (cause) {
|
|
119
|
+
await logger?.error("supabase broker request failed", {
|
|
120
|
+
endpoint,
|
|
121
|
+
status: 502,
|
|
122
|
+
});
|
|
113
123
|
throw new BrokerClientError({
|
|
114
124
|
code: "upstream_error",
|
|
115
125
|
message: "broker request failed",
|
|
@@ -122,6 +132,10 @@ async function makeBrokerRequest(
|
|
|
122
132
|
try {
|
|
123
133
|
payload = await response.json();
|
|
124
134
|
} catch {
|
|
135
|
+
await logger?.error("supabase broker response invalid", {
|
|
136
|
+
endpoint,
|
|
137
|
+
status: response.status,
|
|
138
|
+
});
|
|
125
139
|
throw new BrokerClientError({
|
|
126
140
|
code: "upstream_error",
|
|
127
141
|
message: "broker returned an invalid response",
|
|
@@ -129,6 +143,11 @@ async function makeBrokerRequest(
|
|
|
129
143
|
});
|
|
130
144
|
}
|
|
131
145
|
|
|
146
|
+
await logger?.debug("supabase broker response received", {
|
|
147
|
+
endpoint,
|
|
148
|
+
status: response.status,
|
|
149
|
+
});
|
|
150
|
+
|
|
132
151
|
if (!response.ok) {
|
|
133
152
|
const errorBody = payload as Record<string, unknown> | undefined;
|
|
134
153
|
const error = errorBody?.error as Record<string, unknown> | undefined;
|
|
@@ -136,6 +155,15 @@ async function makeBrokerRequest(
|
|
|
136
155
|
const code = (error?.code as BrokerErrorCode) || "upstream_error";
|
|
137
156
|
const message = (error?.message as string) || "broker request failed";
|
|
138
157
|
|
|
158
|
+
await logger?.error(
|
|
159
|
+
endpoint === "/exchange" ? "supabase broker exchange failed" : "supabase broker refresh failed",
|
|
160
|
+
{
|
|
161
|
+
endpoint,
|
|
162
|
+
status: response.status,
|
|
163
|
+
code,
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
|
|
139
167
|
throw new BrokerClientError({
|
|
140
168
|
code,
|
|
141
169
|
message,
|
|
@@ -150,6 +178,7 @@ export async function exchangeCodeThroughBroker(
|
|
|
150
178
|
config: BrokerConfig,
|
|
151
179
|
input: ExchangeRequest,
|
|
152
180
|
fetchImpl: FetchLike = fetch,
|
|
181
|
+
logger?: SupabaseLogger,
|
|
153
182
|
): Promise<SupabaseTokenResponse> {
|
|
154
183
|
return makeBrokerRequest(
|
|
155
184
|
config,
|
|
@@ -160,6 +189,7 @@ export async function exchangeCodeThroughBroker(
|
|
|
160
189
|
redirect_uri: input.redirect_uri,
|
|
161
190
|
},
|
|
162
191
|
fetchImpl,
|
|
192
|
+
logger,
|
|
163
193
|
);
|
|
164
194
|
}
|
|
165
195
|
|
|
@@ -167,6 +197,7 @@ export async function refreshTokenThroughBroker(
|
|
|
167
197
|
config: BrokerConfig,
|
|
168
198
|
input: RefreshRequest,
|
|
169
199
|
fetchImpl: FetchLike = fetch,
|
|
200
|
+
logger?: SupabaseLogger,
|
|
170
201
|
): Promise<SupabaseTokenResponse> {
|
|
171
202
|
return makeBrokerRequest(
|
|
172
203
|
config,
|
|
@@ -175,5 +206,6 @@ export async function refreshTokenThroughBroker(
|
|
|
175
206
|
refresh_token: input.refresh_token,
|
|
176
207
|
},
|
|
177
208
|
fetchImpl,
|
|
209
|
+
logger,
|
|
178
210
|
);
|
|
179
211
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export type SupabaseLogLevel = "debug" | "info" | "warn" | "error";
|
|
2
|
+
|
|
3
|
+
export type SupabaseLogger = ReturnType<typeof createSupabaseLogger>;
|
|
4
|
+
|
|
5
|
+
export type LogEntry = {
|
|
6
|
+
service: string;
|
|
7
|
+
level: SupabaseLogLevel;
|
|
8
|
+
message: string;
|
|
9
|
+
extra?: Record<string, unknown>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type LogWriter = (entry: LogEntry) => Promise<unknown>;
|
|
13
|
+
|
|
14
|
+
export function createSupabaseLogger(input: { write: LogWriter }) {
|
|
15
|
+
async function emit(
|
|
16
|
+
level: SupabaseLogLevel,
|
|
17
|
+
message: string,
|
|
18
|
+
extra?: Record<string, unknown>,
|
|
19
|
+
) {
|
|
20
|
+
try {
|
|
21
|
+
const result = await input.write({
|
|
22
|
+
service: "opencode-supabase",
|
|
23
|
+
level,
|
|
24
|
+
message,
|
|
25
|
+
extra,
|
|
26
|
+
});
|
|
27
|
+
if (result && typeof result === "object" && "error" in result) {
|
|
28
|
+
console.error("[opencode-supabase] host log rejected:", (result as { error: unknown }).error);
|
|
29
|
+
}
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error("[opencode-supabase] host log failed:", error instanceof Error ? error.message : error);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
debug(message: string, extra?: Record<string, unknown>) {
|
|
37
|
+
return emit("debug", message, extra);
|
|
38
|
+
},
|
|
39
|
+
info(message: string, extra?: Record<string, unknown>) {
|
|
40
|
+
return emit("info", message, extra);
|
|
41
|
+
},
|
|
42
|
+
warn(message: string, extra?: Record<string, unknown>) {
|
|
43
|
+
return emit("warn", message, extra);
|
|
44
|
+
},
|
|
45
|
+
error(message: string, extra?: Record<string, unknown>) {
|
|
46
|
+
return emit("error", message, extra);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function createServerLogWriter(client: { app: { log: (input: { body: LogEntry }) => Promise<unknown> } }) {
|
|
52
|
+
return (entry: LogEntry) => client.app.log({ body: entry });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function createTuiLogWriter(client: { app: { log: (input: LogEntry, options?: { throwOnError?: boolean }) => Promise<unknown> } }) {
|
|
56
|
+
return (entry: LogEntry) => client.app.log(entry, { throwOnError: true });
|
|
57
|
+
}
|
package/src/tui/dialog.tsx
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { TuiPluginApi } from "@opencode-ai/plugin/tui";
|
|
2
2
|
import { createSignal } from "solid-js";
|
|
3
3
|
|
|
4
|
+
import type { SupabaseLogger } from "../shared/log.ts";
|
|
5
|
+
|
|
4
6
|
type SupabaseDialogProps = {
|
|
5
7
|
api: TuiPluginApi;
|
|
6
8
|
onClose: () => void;
|
|
9
|
+
logger: SupabaseLogger;
|
|
7
10
|
};
|
|
8
11
|
|
|
9
12
|
type OAuthState =
|
|
@@ -28,6 +31,9 @@ export function SupabaseDialog(props: SupabaseDialogProps) {
|
|
|
28
31
|
|
|
29
32
|
const startOAuth = async () => {
|
|
30
33
|
try {
|
|
34
|
+
await props.logger.info("supabase auth started", {
|
|
35
|
+
phase: "authorize",
|
|
36
|
+
});
|
|
31
37
|
setState({ type: "authorizing", url: "" });
|
|
32
38
|
|
|
33
39
|
// Start OAuth authorization
|
|
@@ -50,19 +56,28 @@ export function SupabaseDialog(props: SupabaseDialogProps) {
|
|
|
50
56
|
}
|
|
51
57
|
|
|
52
58
|
const { url, method } = authData;
|
|
59
|
+
const safeUrl = new URL(url);
|
|
53
60
|
setState({ type: "authorizing", url });
|
|
54
61
|
|
|
62
|
+
await props.logger.debug("supabase auth authorize response received", {
|
|
63
|
+
method,
|
|
64
|
+
url_origin: safeUrl.origin,
|
|
65
|
+
url_path: safeUrl.pathname,
|
|
66
|
+
});
|
|
67
|
+
|
|
55
68
|
// Attempt to open browser automatically
|
|
56
69
|
if (method === "auto") {
|
|
57
70
|
try {
|
|
58
71
|
const open = await import("open");
|
|
59
72
|
await open.default(url);
|
|
60
73
|
} catch {
|
|
74
|
+
await props.logger.warn("supabase browser open failed");
|
|
61
75
|
// Browser auto-open failed, user can click the URL manually
|
|
62
76
|
}
|
|
63
77
|
}
|
|
64
78
|
|
|
65
79
|
setState({ type: "waiting_callback", url });
|
|
80
|
+
await props.logger.debug("supabase auth waiting for callback");
|
|
66
81
|
|
|
67
82
|
// Wait for callback
|
|
68
83
|
const callbackResponse = (await props.api.client.provider.oauth.callback({
|
|
@@ -79,6 +94,9 @@ export function SupabaseDialog(props: SupabaseDialogProps) {
|
|
|
79
94
|
const callbackSucceeded = callbackResponse.data === true;
|
|
80
95
|
|
|
81
96
|
if (callbackSucceeded) {
|
|
97
|
+
await props.logger.info("supabase auth completed", {
|
|
98
|
+
status: "success",
|
|
99
|
+
});
|
|
82
100
|
setState({ type: "success" });
|
|
83
101
|
props.api.ui.toast({
|
|
84
102
|
variant: "success",
|
|
@@ -92,6 +110,9 @@ export function SupabaseDialog(props: SupabaseDialogProps) {
|
|
|
92
110
|
} catch (error) {
|
|
93
111
|
const message =
|
|
94
112
|
error instanceof Error ? error.message : "Authorization failed";
|
|
113
|
+
await props.logger.error("supabase auth failed", {
|
|
114
|
+
message,
|
|
115
|
+
});
|
|
95
116
|
setState({ type: "error", message });
|
|
96
117
|
props.api.ui.toast({
|
|
97
118
|
variant: "error",
|
package/src/tui/index.tsx
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import type { TuiPlugin } from "@opencode-ai/plugin/tui";
|
|
2
2
|
|
|
3
|
+
import { createSupabaseLogger, createTuiLogWriter } from "../shared/log.ts";
|
|
3
4
|
import { createSupabaseCommand } from "./commands";
|
|
4
5
|
import { SupabaseDialog } from "./dialog";
|
|
5
6
|
|
|
6
7
|
const tui: TuiPlugin = async (api) => {
|
|
8
|
+
const logger = createSupabaseLogger({
|
|
9
|
+
write: createTuiLogWriter(api.client),
|
|
10
|
+
});
|
|
11
|
+
|
|
7
12
|
api.command.register(() => [
|
|
8
13
|
createSupabaseCommand(() => {
|
|
9
|
-
api.ui.dialog.replace(() => SupabaseDialog({ api, onClose: () => api.ui.dialog.clear() }));
|
|
14
|
+
api.ui.dialog.replace(() => SupabaseDialog({ api, logger, onClose: () => api.ui.dialog.clear() }));
|
|
10
15
|
}),
|
|
11
16
|
]);
|
|
12
17
|
};
|