@agentlink.sh/cli 0.10.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 +345 -0
- package/dist/chunk-DOWH47I5.js +592 -0
- package/dist/chunk-G3HVUCWY.js +62 -0
- package/dist/cloud-N7CILHLD.js +46 -0
- package/dist/index.js +4989 -0
- package/dist/oauth-J4X62CY4.js +199 -0
- package/package.json +32 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
OAUTH_CLIENT_ID,
|
|
4
|
+
OAUTH_CLIENT_SECRET,
|
|
5
|
+
amber,
|
|
6
|
+
blue,
|
|
7
|
+
dim
|
|
8
|
+
} from "./chunk-G3HVUCWY.js";
|
|
9
|
+
|
|
10
|
+
// src/oauth.ts
|
|
11
|
+
import { createHash, randomBytes } from "crypto";
|
|
12
|
+
import { createServer } from "http";
|
|
13
|
+
import { URL } from "url";
|
|
14
|
+
import { execSync } from "child_process";
|
|
15
|
+
var AUTHORIZE_URL = "https://api.supabase.com/v1/oauth/authorize";
|
|
16
|
+
var TOKEN_URL = "https://api.supabase.com/v1/oauth/token";
|
|
17
|
+
var PORT_RANGE_START = 54320;
|
|
18
|
+
var PORT_RANGE_END = 54330;
|
|
19
|
+
var TIMEOUT_MS = 12e4;
|
|
20
|
+
function generateCodeVerifier() {
|
|
21
|
+
return randomBytes(64).toString("base64url");
|
|
22
|
+
}
|
|
23
|
+
function generateCodeChallenge(verifier) {
|
|
24
|
+
return createHash("sha256").update(verifier).digest("base64url");
|
|
25
|
+
}
|
|
26
|
+
function basicAuth() {
|
|
27
|
+
return Buffer.from(`${OAUTH_CLIENT_ID}:${OAUTH_CLIENT_SECRET}`).toString("base64");
|
|
28
|
+
}
|
|
29
|
+
var SUCCESS_HTML = `<!DOCTYPE html>
|
|
30
|
+
<html>
|
|
31
|
+
<head>
|
|
32
|
+
<meta charset="utf-8">
|
|
33
|
+
<title>AgentLink \u2014 Authenticated</title>
|
|
34
|
+
<style>
|
|
35
|
+
body { font-family: system-ui, sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #080c12; color: #e2e8f0; }
|
|
36
|
+
.card { text-align: center; padding: 2rem; }
|
|
37
|
+
h1 { color: #5cb8e4; font-size: 1.5rem; margin-bottom: 0.5rem; }
|
|
38
|
+
p { color: #94a3b8; }
|
|
39
|
+
</style>
|
|
40
|
+
</head>
|
|
41
|
+
<body>
|
|
42
|
+
<div class="card">
|
|
43
|
+
<h1>Authenticated</h1>
|
|
44
|
+
<p>You can close this tab and return to the terminal.</p>
|
|
45
|
+
</div>
|
|
46
|
+
</body>
|
|
47
|
+
</html>`;
|
|
48
|
+
var ERROR_HTML = (msg) => `<!DOCTYPE html>
|
|
49
|
+
<html>
|
|
50
|
+
<head>
|
|
51
|
+
<meta charset="utf-8">
|
|
52
|
+
<title>AgentLink \u2014 Error</title>
|
|
53
|
+
<style>
|
|
54
|
+
body { font-family: system-ui, sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #080c12; color: #e2e8f0; }
|
|
55
|
+
.card { text-align: center; padding: 2rem; }
|
|
56
|
+
h1 { color: #ef4444; font-size: 1.5rem; margin-bottom: 0.5rem; }
|
|
57
|
+
p { color: #94a3b8; }
|
|
58
|
+
</style>
|
|
59
|
+
</head>
|
|
60
|
+
<body>
|
|
61
|
+
<div class="card">
|
|
62
|
+
<h1>Authentication Error</h1>
|
|
63
|
+
<p>${msg}</p>
|
|
64
|
+
</div>
|
|
65
|
+
</body>
|
|
66
|
+
</html>`;
|
|
67
|
+
async function findOpenPort() {
|
|
68
|
+
for (let port = PORT_RANGE_START; port <= PORT_RANGE_END; port++) {
|
|
69
|
+
const available = await new Promise((resolve) => {
|
|
70
|
+
const server = createServer();
|
|
71
|
+
server.once("error", () => resolve(false));
|
|
72
|
+
server.once("listening", () => {
|
|
73
|
+
server.close(() => resolve(true));
|
|
74
|
+
});
|
|
75
|
+
server.listen(port, "127.0.0.1");
|
|
76
|
+
});
|
|
77
|
+
if (available) return port;
|
|
78
|
+
}
|
|
79
|
+
throw new Error(`No open port found in range ${PORT_RANGE_START}-${PORT_RANGE_END}`);
|
|
80
|
+
}
|
|
81
|
+
function openBrowser(url) {
|
|
82
|
+
try {
|
|
83
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
84
|
+
execSync(`${cmd} ${JSON.stringify(url)}`, { stdio: "ignore" });
|
|
85
|
+
} catch {
|
|
86
|
+
console.log(` ${amber("Could not open browser.")} Visit this URL manually:`);
|
|
87
|
+
console.log(` ${dim(url)}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function oauthLogin() {
|
|
91
|
+
const codeVerifier = generateCodeVerifier();
|
|
92
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
93
|
+
const state = randomBytes(16).toString("hex");
|
|
94
|
+
const port = await findOpenPort();
|
|
95
|
+
const redirectUri = `http://localhost:${port}/callback`;
|
|
96
|
+
const authorizeUrl = new URL(AUTHORIZE_URL);
|
|
97
|
+
authorizeUrl.searchParams.set("client_id", OAUTH_CLIENT_ID);
|
|
98
|
+
authorizeUrl.searchParams.set("redirect_uri", redirectUri);
|
|
99
|
+
authorizeUrl.searchParams.set("response_type", "code");
|
|
100
|
+
authorizeUrl.searchParams.set("code_challenge", codeChallenge);
|
|
101
|
+
authorizeUrl.searchParams.set("code_challenge_method", "S256");
|
|
102
|
+
authorizeUrl.searchParams.set("state", state);
|
|
103
|
+
const code = await new Promise((resolve, reject) => {
|
|
104
|
+
const timeout = setTimeout(() => {
|
|
105
|
+
server.close();
|
|
106
|
+
reject(new Error("OAuth login timed out. Try again."));
|
|
107
|
+
}, TIMEOUT_MS);
|
|
108
|
+
const server = createServer((req, res) => {
|
|
109
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
110
|
+
if (url.pathname !== "/callback") {
|
|
111
|
+
res.writeHead(404);
|
|
112
|
+
res.end();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const error = url.searchParams.get("error");
|
|
116
|
+
if (error) {
|
|
117
|
+
const desc = url.searchParams.get("error_description") ?? error;
|
|
118
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
119
|
+
res.end(ERROR_HTML(desc));
|
|
120
|
+
clearTimeout(timeout);
|
|
121
|
+
server.close();
|
|
122
|
+
reject(new Error(`OAuth error: ${desc}`));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const returnedState = url.searchParams.get("state");
|
|
126
|
+
if (returnedState !== state) {
|
|
127
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
128
|
+
res.end(ERROR_HTML("State mismatch \u2014 possible CSRF attack."));
|
|
129
|
+
clearTimeout(timeout);
|
|
130
|
+
server.close();
|
|
131
|
+
reject(new Error("OAuth state mismatch."));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const authCode = url.searchParams.get("code");
|
|
135
|
+
if (!authCode) {
|
|
136
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
137
|
+
res.end(ERROR_HTML("No authorization code received."));
|
|
138
|
+
clearTimeout(timeout);
|
|
139
|
+
server.close();
|
|
140
|
+
reject(new Error("No authorization code in callback."));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
144
|
+
res.end(SUCCESS_HTML);
|
|
145
|
+
clearTimeout(timeout);
|
|
146
|
+
server.close();
|
|
147
|
+
resolve(authCode);
|
|
148
|
+
});
|
|
149
|
+
server.listen(port, "127.0.0.1", () => {
|
|
150
|
+
console.log(` ${blue("\u25CF")} Opening browser for Supabase login...`);
|
|
151
|
+
openBrowser(authorizeUrl.toString());
|
|
152
|
+
console.log(` ${dim("Waiting for authorization...")}`);
|
|
153
|
+
console.log();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
return exchangeCode(code, redirectUri, codeVerifier);
|
|
157
|
+
}
|
|
158
|
+
async function exchangeCode(code, redirectUri, codeVerifier) {
|
|
159
|
+
const res = await fetch(TOKEN_URL, {
|
|
160
|
+
method: "POST",
|
|
161
|
+
headers: {
|
|
162
|
+
Authorization: `Basic ${basicAuth()}`,
|
|
163
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
164
|
+
},
|
|
165
|
+
body: new URLSearchParams({
|
|
166
|
+
grant_type: "authorization_code",
|
|
167
|
+
code,
|
|
168
|
+
redirect_uri: redirectUri,
|
|
169
|
+
code_verifier: codeVerifier
|
|
170
|
+
})
|
|
171
|
+
});
|
|
172
|
+
if (!res.ok) {
|
|
173
|
+
const body = await res.text();
|
|
174
|
+
throw new Error(`Token exchange failed (${res.status}): ${body}`);
|
|
175
|
+
}
|
|
176
|
+
return await res.json();
|
|
177
|
+
}
|
|
178
|
+
async function refreshOAuthToken(refreshToken) {
|
|
179
|
+
const res = await fetch(TOKEN_URL, {
|
|
180
|
+
method: "POST",
|
|
181
|
+
headers: {
|
|
182
|
+
Authorization: `Basic ${basicAuth()}`,
|
|
183
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
184
|
+
},
|
|
185
|
+
body: new URLSearchParams({
|
|
186
|
+
grant_type: "refresh_token",
|
|
187
|
+
refresh_token: refreshToken
|
|
188
|
+
})
|
|
189
|
+
});
|
|
190
|
+
if (!res.ok) {
|
|
191
|
+
const body = await res.text();
|
|
192
|
+
throw new Error(`Token refresh failed (${res.status}): ${body}`);
|
|
193
|
+
}
|
|
194
|
+
return await res.json();
|
|
195
|
+
}
|
|
196
|
+
export {
|
|
197
|
+
oauthLogin,
|
|
198
|
+
refreshOAuthToken
|
|
199
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agentlink.sh/cli",
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"description": "CLI for scaffolding 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
|
+
"commander": "^13",
|
|
20
|
+
"ora": "^9",
|
|
21
|
+
"skills": "1.4.3",
|
|
22
|
+
"@supabase/pg-delta": "1.0.0-alpha.8",
|
|
23
|
+
"supabase": "^2.76.15"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^22",
|
|
27
|
+
"eslint": "^9.39.4",
|
|
28
|
+
"tsup": "^8",
|
|
29
|
+
"typescript": "^5",
|
|
30
|
+
"typescript-eslint": "^8.57.0"
|
|
31
|
+
}
|
|
32
|
+
}
|