opencode-supabase 0.0.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 +211 -0
- package/index.ts +1 -0
- package/package.json +43 -0
- package/src/server/assets/supabase-logo-wordmark--dark.svg +23 -0
- package/src/server/auth-html.ts +119 -0
- package/src/server/auth.ts +232 -0
- package/src/server/index.ts +13 -0
- package/src/server/store.ts +53 -0
- package/src/server/tools.ts +245 -0
- package/src/shared/api.ts +24 -0
- package/src/shared/broker.ts +179 -0
- package/src/shared/cfg.ts +71 -0
- package/src/shared/oauth.ts +48 -0
- package/src/shared/types.ts +35 -0
- package/src/tui/commands.ts +8 -0
- package/src/tui/dialog.tsx +142 -0
- package/src/tui/index.tsx +14 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { TuiPluginApi } from "@opencode-ai/plugin/tui";
|
|
2
|
+
import { createSignal } from "solid-js";
|
|
3
|
+
|
|
4
|
+
type SupabaseDialogProps = {
|
|
5
|
+
api: TuiPluginApi;
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type OAuthState =
|
|
10
|
+
| { type: "idle" }
|
|
11
|
+
| { type: "authorizing"; url: string }
|
|
12
|
+
| { type: "waiting_callback"; url: string }
|
|
13
|
+
| { type: "success" }
|
|
14
|
+
| { type: "error"; message: string };
|
|
15
|
+
|
|
16
|
+
// API response types
|
|
17
|
+
type ApiError = { message?: string; [key: string]: unknown };
|
|
18
|
+
type ApiResponse<T> = { data?: T; error?: ApiError };
|
|
19
|
+
|
|
20
|
+
type AuthData = {
|
|
21
|
+
url: string;
|
|
22
|
+
instructions: string;
|
|
23
|
+
method: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function SupabaseDialog(props: SupabaseDialogProps) {
|
|
27
|
+
const [state, setState] = createSignal<OAuthState>({ type: "idle" });
|
|
28
|
+
|
|
29
|
+
const startOAuth = async () => {
|
|
30
|
+
try {
|
|
31
|
+
setState({ type: "authorizing", url: "" });
|
|
32
|
+
|
|
33
|
+
// Start OAuth authorization
|
|
34
|
+
const authResponse = (await props.api.client.provider.oauth.authorize({
|
|
35
|
+
providerID: "supabase",
|
|
36
|
+
method: 0,
|
|
37
|
+
})) as unknown as ApiResponse<AuthData>;
|
|
38
|
+
|
|
39
|
+
// Handle the response shape from the plugin API
|
|
40
|
+
if (authResponse.error) {
|
|
41
|
+
throw new Error(authResponse.error.message || "Failed to start OAuth authorization");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const authData = authResponse.data;
|
|
45
|
+
|
|
46
|
+
if (!authData?.url) {
|
|
47
|
+
throw new Error("Invalid OAuth authorization response");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const { url, method } = authData;
|
|
51
|
+
setState({ type: "authorizing", url });
|
|
52
|
+
|
|
53
|
+
// Attempt to open browser automatically
|
|
54
|
+
if (method === "auto") {
|
|
55
|
+
try {
|
|
56
|
+
const open = await import("open");
|
|
57
|
+
await open.default(url);
|
|
58
|
+
} catch {
|
|
59
|
+
// Browser auto-open failed, user can click the URL manually
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
setState({ type: "waiting_callback", url });
|
|
64
|
+
|
|
65
|
+
// Wait for callback
|
|
66
|
+
const callbackResponse = (await props.api.client.provider.oauth.callback({
|
|
67
|
+
providerID: "supabase",
|
|
68
|
+
method: 0,
|
|
69
|
+
})) as unknown as ApiResponse<boolean>;
|
|
70
|
+
|
|
71
|
+
if (callbackResponse.error) {
|
|
72
|
+
throw new Error(callbackResponse.error.message || "OAuth callback failed");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const callbackSucceeded = callbackResponse.data === true;
|
|
76
|
+
|
|
77
|
+
if (callbackSucceeded) {
|
|
78
|
+
setState({ type: "success" });
|
|
79
|
+
props.api.ui.toast({
|
|
80
|
+
variant: "success",
|
|
81
|
+
message: "Connected to Supabase. Management tools coming in next update.",
|
|
82
|
+
});
|
|
83
|
+
props.onClose();
|
|
84
|
+
} else {
|
|
85
|
+
throw new Error("OAuth authorization was denied");
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
const message = error instanceof Error ? error.message : "Authorization failed";
|
|
89
|
+
setState({ type: "error", message });
|
|
90
|
+
props.api.ui.toast({
|
|
91
|
+
variant: "error",
|
|
92
|
+
message: `Supabase authorization failed: ${message}`,
|
|
93
|
+
});
|
|
94
|
+
props.onClose();
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const currentState = state();
|
|
99
|
+
|
|
100
|
+
if (currentState.type === "idle") {
|
|
101
|
+
return props.api.ui.DialogConfirm({
|
|
102
|
+
title: "Connect Supabase",
|
|
103
|
+
message:
|
|
104
|
+
"This will open a browser window to authorize OpenCode to access your Supabase account. Continue?",
|
|
105
|
+
onConfirm: startOAuth,
|
|
106
|
+
onCancel: props.onClose,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (currentState.type === "authorizing") {
|
|
111
|
+
return props.api.ui.DialogAlert({
|
|
112
|
+
title: "Connect Supabase",
|
|
113
|
+
message: currentState.url
|
|
114
|
+
? `Opening browser to authorize Supabase...\n\nIf the browser doesn't open automatically, visit:\n${currentState.url}`
|
|
115
|
+
: "Starting authorization...",
|
|
116
|
+
onConfirm: props.onClose,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (currentState.type === "waiting_callback") {
|
|
121
|
+
return props.api.ui.DialogAlert({
|
|
122
|
+
title: "Connect Supabase",
|
|
123
|
+
message: `Waiting for authorization in your browser...\n\nIf you need to complete authorization manually, visit:\n${currentState.url}`,
|
|
124
|
+
onConfirm: props.onClose,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (currentState.type === "error") {
|
|
129
|
+
return props.api.ui.DialogAlert({
|
|
130
|
+
title: "Authorization Failed",
|
|
131
|
+
message: currentState.message,
|
|
132
|
+
onConfirm: props.onClose,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Success state (should close immediately via onClose)
|
|
137
|
+
return props.api.ui.DialogAlert({
|
|
138
|
+
title: "Connected",
|
|
139
|
+
message: "Successfully connected to Supabase.",
|
|
140
|
+
onConfirm: props.onClose,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { TuiPlugin } from "@opencode-ai/plugin/tui";
|
|
2
|
+
|
|
3
|
+
import { createSupabaseCommand } from "./commands";
|
|
4
|
+
import { SupabaseDialog } from "./dialog";
|
|
5
|
+
|
|
6
|
+
const tui: TuiPlugin = async (api) => {
|
|
7
|
+
api.command.register(() => [
|
|
8
|
+
createSupabaseCommand(() => {
|
|
9
|
+
api.ui.dialog.replace(() => SupabaseDialog({ api, onClose: () => api.ui.dialog.clear() }));
|
|
10
|
+
}),
|
|
11
|
+
]);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default { id: "supabase", tui };
|