agentlink-sh 0.26.0
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 +485 -0
- package/dist/chunk-4CW46BPC.js +787 -0
- package/dist/chunk-7NV5CYOF.js +1064 -0
- package/dist/chunk-DM6KG5YU.js +81 -0
- package/dist/chunk-IV5ZSOKF.js +194 -0
- package/dist/chunk-MHI6VJ75.js +27 -0
- package/dist/cloud-ZXVJMV5Q.js +78 -0
- package/dist/constants-PWT7TUWD.js +27 -0
- package/dist/db-DNK3TD5Y.js +31 -0
- package/dist/index.js +7524 -0
- package/dist/oauth-JGWRORJM.js +269 -0
- package/dist/utils-7LT4QSYL.js +27 -0
- package/package.json +35 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
amber,
|
|
4
|
+
blue,
|
|
5
|
+
bold,
|
|
6
|
+
dim
|
|
7
|
+
} from "./chunk-MHI6VJ75.js";
|
|
8
|
+
import {
|
|
9
|
+
OAUTH_CLIENT_ID,
|
|
10
|
+
OAUTH_CLIENT_SECRET
|
|
11
|
+
} from "./chunk-DM6KG5YU.js";
|
|
12
|
+
|
|
13
|
+
// src/oauth.ts
|
|
14
|
+
import { createHash, randomBytes } from "crypto";
|
|
15
|
+
import { createServer } from "http";
|
|
16
|
+
import { URL } from "url";
|
|
17
|
+
import open from "open";
|
|
18
|
+
import { select } from "@inquirer/prompts";
|
|
19
|
+
var theme = {
|
|
20
|
+
prefix: { idle: blue("?"), done: blue("\u2714") },
|
|
21
|
+
style: {
|
|
22
|
+
answer: (text) => amber(text),
|
|
23
|
+
message: (text) => bold(text),
|
|
24
|
+
highlight: (text) => blue(text),
|
|
25
|
+
key: (text) => blue(bold(`<${text}>`))
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var AUTHORIZE_URL = "https://api.supabase.com/v1/oauth/authorize";
|
|
29
|
+
var TOKEN_URL = "https://api.supabase.com/v1/oauth/token";
|
|
30
|
+
var PORT_RANGE_START = 54320;
|
|
31
|
+
var PORT_RANGE_END = 54330;
|
|
32
|
+
var CHECK_IN_MS = 3e4;
|
|
33
|
+
function generateCodeVerifier() {
|
|
34
|
+
return randomBytes(64).toString("base64url");
|
|
35
|
+
}
|
|
36
|
+
function generateCodeChallenge(verifier) {
|
|
37
|
+
return createHash("sha256").update(verifier).digest("base64url");
|
|
38
|
+
}
|
|
39
|
+
function basicAuth() {
|
|
40
|
+
return Buffer.from(`${OAUTH_CLIENT_ID}:${OAUTH_CLIENT_SECRET}`).toString("base64");
|
|
41
|
+
}
|
|
42
|
+
var SUCCESS_HTML = `<!DOCTYPE html>
|
|
43
|
+
<html>
|
|
44
|
+
<head>
|
|
45
|
+
<meta charset="utf-8">
|
|
46
|
+
<title>AgentLink \u2014 Authenticated</title>
|
|
47
|
+
<style>
|
|
48
|
+
body { font-family: system-ui, sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #080c12; color: #e2e8f0; }
|
|
49
|
+
.card { text-align: center; padding: 2rem; }
|
|
50
|
+
h1 { color: #5cb8e4; font-size: 1.5rem; margin-bottom: 0.5rem; }
|
|
51
|
+
p { color: #94a3b8; }
|
|
52
|
+
</style>
|
|
53
|
+
</head>
|
|
54
|
+
<body>
|
|
55
|
+
<div class="card">
|
|
56
|
+
<h1>Authenticated</h1>
|
|
57
|
+
<p>You can close this tab and return to the terminal.</p>
|
|
58
|
+
</div>
|
|
59
|
+
</body>
|
|
60
|
+
</html>`;
|
|
61
|
+
var ERROR_HTML = (msg) => `<!DOCTYPE html>
|
|
62
|
+
<html>
|
|
63
|
+
<head>
|
|
64
|
+
<meta charset="utf-8">
|
|
65
|
+
<title>AgentLink \u2014 Error</title>
|
|
66
|
+
<style>
|
|
67
|
+
body { font-family: system-ui, sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #080c12; color: #e2e8f0; }
|
|
68
|
+
.card { text-align: center; padding: 2rem; }
|
|
69
|
+
h1 { color: #ef4444; font-size: 1.5rem; margin-bottom: 0.5rem; }
|
|
70
|
+
p { color: #94a3b8; }
|
|
71
|
+
</style>
|
|
72
|
+
</head>
|
|
73
|
+
<body>
|
|
74
|
+
<div class="card">
|
|
75
|
+
<h1>Authentication Error</h1>
|
|
76
|
+
<p>${msg}</p>
|
|
77
|
+
</div>
|
|
78
|
+
</body>
|
|
79
|
+
</html>`;
|
|
80
|
+
async function findOpenPort() {
|
|
81
|
+
for (let port = PORT_RANGE_START; port <= PORT_RANGE_END; port++) {
|
|
82
|
+
const available = await new Promise((resolve) => {
|
|
83
|
+
const server = createServer();
|
|
84
|
+
server.once("error", () => resolve(false));
|
|
85
|
+
server.once("listening", () => {
|
|
86
|
+
server.close(() => resolve(true));
|
|
87
|
+
});
|
|
88
|
+
server.listen(port, "127.0.0.1");
|
|
89
|
+
});
|
|
90
|
+
if (available) return port;
|
|
91
|
+
}
|
|
92
|
+
throw new Error(`No open port found in range ${PORT_RANGE_START}-${PORT_RANGE_END}`);
|
|
93
|
+
}
|
|
94
|
+
async function openBrowser(url) {
|
|
95
|
+
try {
|
|
96
|
+
await open(url);
|
|
97
|
+
} catch {
|
|
98
|
+
console.log(` ${amber("Could not open browser.")} Visit this URL manually:`);
|
|
99
|
+
console.log(` ${dim(url)}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function createCallbackServer(port, state) {
|
|
103
|
+
let codeResolve = null;
|
|
104
|
+
let receivedCode = null;
|
|
105
|
+
let done = false;
|
|
106
|
+
const server = createServer((req, res) => {
|
|
107
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
108
|
+
if (url.pathname !== "/callback") {
|
|
109
|
+
res.writeHead(404);
|
|
110
|
+
res.end();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const error = url.searchParams.get("error");
|
|
114
|
+
if (error) {
|
|
115
|
+
const desc = url.searchParams.get("error_description") ?? error;
|
|
116
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
117
|
+
res.end(ERROR_HTML(desc));
|
|
118
|
+
console.log(` ${amber("\u25B2")} OAuth error: ${desc}`);
|
|
119
|
+
done = true;
|
|
120
|
+
codeResolve?.(null);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const returnedState = url.searchParams.get("state");
|
|
124
|
+
if (returnedState !== state) {
|
|
125
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
126
|
+
res.end(ERROR_HTML("State mismatch \u2014 possible CSRF attack."));
|
|
127
|
+
done = true;
|
|
128
|
+
codeResolve?.(null);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const authCode = url.searchParams.get("code");
|
|
132
|
+
if (!authCode) {
|
|
133
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
134
|
+
res.end(ERROR_HTML("No authorization code received."));
|
|
135
|
+
done = true;
|
|
136
|
+
codeResolve?.(null);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
140
|
+
res.end(SUCCESS_HTML);
|
|
141
|
+
receivedCode = authCode;
|
|
142
|
+
done = true;
|
|
143
|
+
codeResolve?.(authCode);
|
|
144
|
+
});
|
|
145
|
+
server.listen(port, "127.0.0.1");
|
|
146
|
+
return {
|
|
147
|
+
/** Wait up to `ms` for the callback. Returns code or null on timeout. Server stays alive. */
|
|
148
|
+
waitForCode(ms) {
|
|
149
|
+
if (receivedCode) return Promise.resolve(receivedCode);
|
|
150
|
+
if (done) return Promise.resolve(null);
|
|
151
|
+
return new Promise((resolve) => {
|
|
152
|
+
codeResolve = resolve;
|
|
153
|
+
const timer = setTimeout(() => {
|
|
154
|
+
codeResolve = null;
|
|
155
|
+
resolve(null);
|
|
156
|
+
}, ms);
|
|
157
|
+
const origResolve = resolve;
|
|
158
|
+
codeResolve = (code) => {
|
|
159
|
+
clearTimeout(timer);
|
|
160
|
+
codeResolve = null;
|
|
161
|
+
origResolve(code);
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
close() {
|
|
166
|
+
server.close();
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
async function oauthLogin(opts = {}) {
|
|
171
|
+
while (true) {
|
|
172
|
+
const codeVerifier = generateCodeVerifier();
|
|
173
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
174
|
+
const state = randomBytes(16).toString("hex");
|
|
175
|
+
const port = await findOpenPort();
|
|
176
|
+
const redirectUri = `http://localhost:${port}/callback`;
|
|
177
|
+
const authorizeUrl = new URL(AUTHORIZE_URL);
|
|
178
|
+
authorizeUrl.searchParams.set("client_id", OAUTH_CLIENT_ID);
|
|
179
|
+
authorizeUrl.searchParams.set("redirect_uri", redirectUri);
|
|
180
|
+
authorizeUrl.searchParams.set("response_type", "code");
|
|
181
|
+
authorizeUrl.searchParams.set("code_challenge", codeChallenge);
|
|
182
|
+
authorizeUrl.searchParams.set("code_challenge_method", "S256");
|
|
183
|
+
authorizeUrl.searchParams.set("state", state);
|
|
184
|
+
if (opts.organizationSlug) {
|
|
185
|
+
authorizeUrl.searchParams.set("organization_slug", opts.organizationSlug);
|
|
186
|
+
}
|
|
187
|
+
console.log(` ${blue("\u25CF")} Opening browser for Supabase login...`);
|
|
188
|
+
await openBrowser(authorizeUrl.toString());
|
|
189
|
+
console.log(` ${dim("Waiting for authorization...")}`);
|
|
190
|
+
console.log();
|
|
191
|
+
const callback = createCallbackServer(port, state);
|
|
192
|
+
try {
|
|
193
|
+
let code = await callback.waitForCode(CHECK_IN_MS);
|
|
194
|
+
if (code) {
|
|
195
|
+
callback.close();
|
|
196
|
+
return exchangeCode(code, redirectUri, codeVerifier);
|
|
197
|
+
}
|
|
198
|
+
while (true) {
|
|
199
|
+
const action = await select({
|
|
200
|
+
message: "No response yet. What would you like to do?",
|
|
201
|
+
theme,
|
|
202
|
+
choices: [
|
|
203
|
+
{ name: "Keep waiting", value: "wait" },
|
|
204
|
+
{ name: "Retry (open browser again)", value: "retry" },
|
|
205
|
+
{ name: "Cancel", value: "cancel" }
|
|
206
|
+
]
|
|
207
|
+
});
|
|
208
|
+
if (action === "cancel") {
|
|
209
|
+
callback.close();
|
|
210
|
+
throw new Error("OAuth login cancelled.");
|
|
211
|
+
}
|
|
212
|
+
if (action === "retry") {
|
|
213
|
+
callback.close();
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
code = await callback.waitForCode(CHECK_IN_MS);
|
|
217
|
+
if (code) {
|
|
218
|
+
callback.close();
|
|
219
|
+
return exchangeCode(code, redirectUri, codeVerifier);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
} catch (err) {
|
|
223
|
+
callback.close();
|
|
224
|
+
throw err;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async function exchangeCode(code, redirectUri, codeVerifier) {
|
|
229
|
+
const res = await fetch(TOKEN_URL, {
|
|
230
|
+
method: "POST",
|
|
231
|
+
headers: {
|
|
232
|
+
Authorization: `Basic ${basicAuth()}`,
|
|
233
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
234
|
+
},
|
|
235
|
+
body: new URLSearchParams({
|
|
236
|
+
grant_type: "authorization_code",
|
|
237
|
+
code,
|
|
238
|
+
redirect_uri: redirectUri,
|
|
239
|
+
code_verifier: codeVerifier
|
|
240
|
+
})
|
|
241
|
+
});
|
|
242
|
+
if (!res.ok) {
|
|
243
|
+
const body = await res.text();
|
|
244
|
+
throw new Error(`Token exchange failed (${res.status}): ${body}`);
|
|
245
|
+
}
|
|
246
|
+
return await res.json();
|
|
247
|
+
}
|
|
248
|
+
async function refreshOAuthToken(refreshToken) {
|
|
249
|
+
const res = await fetch(TOKEN_URL, {
|
|
250
|
+
method: "POST",
|
|
251
|
+
headers: {
|
|
252
|
+
Authorization: `Basic ${basicAuth()}`,
|
|
253
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
254
|
+
},
|
|
255
|
+
body: new URLSearchParams({
|
|
256
|
+
grant_type: "refresh_token",
|
|
257
|
+
refresh_token: refreshToken
|
|
258
|
+
})
|
|
259
|
+
});
|
|
260
|
+
if (!res.ok) {
|
|
261
|
+
const body = await res.text();
|
|
262
|
+
throw new Error(`Token refresh failed (${res.status}): ${body}`);
|
|
263
|
+
}
|
|
264
|
+
return await res.json();
|
|
265
|
+
}
|
|
266
|
+
export {
|
|
267
|
+
oauthLogin,
|
|
268
|
+
refreshOAuthToken
|
|
269
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
assertGitClean,
|
|
4
|
+
checkCommand,
|
|
5
|
+
delay,
|
|
6
|
+
ensureGitignorePattern,
|
|
7
|
+
generateDbPassword,
|
|
8
|
+
initLog,
|
|
9
|
+
listChangedFiles,
|
|
10
|
+
runCommand,
|
|
11
|
+
skillDisplayName,
|
|
12
|
+
slugifyProjectName,
|
|
13
|
+
validateProjectName
|
|
14
|
+
} from "./chunk-IV5ZSOKF.js";
|
|
15
|
+
export {
|
|
16
|
+
assertGitClean,
|
|
17
|
+
checkCommand,
|
|
18
|
+
delay,
|
|
19
|
+
ensureGitignorePattern,
|
|
20
|
+
generateDbPassword,
|
|
21
|
+
initLog,
|
|
22
|
+
listChangedFiles,
|
|
23
|
+
runCommand,
|
|
24
|
+
skillDisplayName,
|
|
25
|
+
slugifyProjectName,
|
|
26
|
+
validateProjectName
|
|
27
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agentlink-sh",
|
|
3
|
+
"version": "0.26.0",
|
|
4
|
+
"description": "CLI for building Supabase apps with AI agents",
|
|
5
|
+
"bin": {
|
|
6
|
+
"agentlink": "dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"type": "module",
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsup",
|
|
14
|
+
"dev": "tsup --watch",
|
|
15
|
+
"lint": "eslint src/"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@inquirer/prompts": "^8.3.0",
|
|
19
|
+
"@supabase/pg-delta": "1.0.0-alpha.14",
|
|
20
|
+
"commander": "^13",
|
|
21
|
+
"open": "^10.2.0",
|
|
22
|
+
"ora": "^9",
|
|
23
|
+
"skills": "1.4.3"
|
|
24
|
+
},
|
|
25
|
+
"optionalDependencies": {
|
|
26
|
+
"supabase": "2.84.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^22",
|
|
30
|
+
"eslint": "^9.39.4",
|
|
31
|
+
"tsup": "^8",
|
|
32
|
+
"typescript": "^5",
|
|
33
|
+
"typescript-eslint": "^8.57.0"
|
|
34
|
+
}
|
|
35
|
+
}
|