@harars/opencode-switch-openai-auth-plugin 0.1.0 → 0.1.2
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 +4 -1
- package/dist/highlights-eq9cgrbb.scm +604 -0
- package/dist/highlights-ghv9g403.scm +205 -0
- package/dist/highlights-hk7bwhj4.scm +284 -0
- package/dist/highlights-r812a2qc.scm +150 -0
- package/dist/highlights-x6tmsnaa.scm +115 -0
- package/dist/index.js +922 -0
- package/dist/injections-73j83es3.scm +27 -0
- package/dist/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
- package/dist/tree-sitter-markdown-411r6y9b.wasm +0 -0
- package/dist/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
- package/dist/tree-sitter-typescript-zxjzwt75.wasm +0 -0
- package/dist/tree-sitter-zig-e78zbjpm.wasm +0 -0
- package/dist/tui.js +922 -0
- package/package.json +6 -5
- package/src/format.ts +0 -27
- package/src/index.ts +0 -1
- package/src/login.tsx +0 -297
- package/src/parse.ts +0 -73
- package/src/paths.ts +0 -27
- package/src/store.ts +0 -244
- package/src/tui.tsx +0 -194
- package/src/types.ts +0 -71
package/dist/tui.js
ADDED
|
@@ -0,0 +1,922 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/tui.tsx
|
|
3
|
+
import { createComponent as _$createComponent2 } from "@opentui/solid";
|
|
4
|
+
import { insertNode as _$insertNode2 } from "@opentui/solid";
|
|
5
|
+
import { insert as _$insert2 } from "@opentui/solid";
|
|
6
|
+
import { setProp as _$setProp2 } from "@opentui/solid";
|
|
7
|
+
import { createElement as _$createElement2 } from "@opentui/solid";
|
|
8
|
+
|
|
9
|
+
// src/format.ts
|
|
10
|
+
function short(id) {
|
|
11
|
+
if (!id)
|
|
12
|
+
return;
|
|
13
|
+
if (id.length <= 12)
|
|
14
|
+
return id;
|
|
15
|
+
return `${id.slice(0, 6)}...${id.slice(-4)}`;
|
|
16
|
+
}
|
|
17
|
+
function accountTitle(account) {
|
|
18
|
+
return account.email || account.accountId || account.id;
|
|
19
|
+
}
|
|
20
|
+
function accountDescription(account, current) {
|
|
21
|
+
return short(account.accountId);
|
|
22
|
+
}
|
|
23
|
+
function accountFooter(current) {
|
|
24
|
+
return current ? "Current" : undefined;
|
|
25
|
+
}
|
|
26
|
+
function loginTitle() {
|
|
27
|
+
return "login";
|
|
28
|
+
}
|
|
29
|
+
function logoutTitle(has) {
|
|
30
|
+
return has ? "logout" : "logout unavailable";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/login.tsx
|
|
34
|
+
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
35
|
+
import { effect as _$effect } from "@opentui/solid";
|
|
36
|
+
import { createTextNode as _$createTextNode } from "@opentui/solid";
|
|
37
|
+
import { insertNode as _$insertNode } from "@opentui/solid";
|
|
38
|
+
import { insert as _$insert } from "@opentui/solid";
|
|
39
|
+
import { setProp as _$setProp } from "@opentui/solid";
|
|
40
|
+
import { createElement as _$createElement } from "@opentui/solid";
|
|
41
|
+
import { TextAttributes } from "@opentui/core";
|
|
42
|
+
import { useKeyboard } from "@opentui/solid";
|
|
43
|
+
|
|
44
|
+
// src/login-helpers.ts
|
|
45
|
+
async function clip(text) {
|
|
46
|
+
let copied = false;
|
|
47
|
+
if (process.stdout.isTTY) {
|
|
48
|
+
const base64 = Buffer.from(text).toString("base64");
|
|
49
|
+
const osc52 = `\x1B]52;c;${base64}\x07`;
|
|
50
|
+
const seq = process.env.TMUX || process.env.STY ? `\x1BPtmux;\x1B${osc52}\x1B\\` : osc52;
|
|
51
|
+
process.stdout.write(seq);
|
|
52
|
+
copied = true;
|
|
53
|
+
}
|
|
54
|
+
const cmds = process.platform === "darwin" ? [["pbcopy"]] : process.platform === "win32" ? [["clip"]] : [["wl-copy"], ["xclip", "-selection", "clipboard"], ["xsel", "--clipboard", "--input"]];
|
|
55
|
+
try {
|
|
56
|
+
await Promise.any(cmds.map(async (cmd) => {
|
|
57
|
+
const proc = Bun.spawn({
|
|
58
|
+
cmd,
|
|
59
|
+
stdin: "pipe",
|
|
60
|
+
stdout: "ignore",
|
|
61
|
+
stderr: "ignore"
|
|
62
|
+
});
|
|
63
|
+
await proc.stdin.write(new TextEncoder().encode(text));
|
|
64
|
+
proc.stdin.end();
|
|
65
|
+
const code = await proc.exited;
|
|
66
|
+
if (code !== 0)
|
|
67
|
+
throw new Error("copy failed");
|
|
68
|
+
}));
|
|
69
|
+
return true;
|
|
70
|
+
} catch {
|
|
71
|
+
return copied;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function target(authz) {
|
|
75
|
+
return authz.instructions.match(/[A-Z0-9]{4}-[A-Z0-9]{4,5}/)?.[0] ?? authz.url;
|
|
76
|
+
}
|
|
77
|
+
async function runOAuthCallback(callback, input) {
|
|
78
|
+
try {
|
|
79
|
+
const res = await callback(input);
|
|
80
|
+
return !res.error;
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/store.ts
|
|
87
|
+
import fs from "fs/promises";
|
|
88
|
+
import path2 from "path";
|
|
89
|
+
|
|
90
|
+
// src/paths.ts
|
|
91
|
+
import os from "os";
|
|
92
|
+
import path from "path";
|
|
93
|
+
function dataPath() {
|
|
94
|
+
if (process.env.OPENCODE_TEST_HOME) {
|
|
95
|
+
return path.join(process.env.OPENCODE_TEST_HOME, ".local", "share", "opencode");
|
|
96
|
+
}
|
|
97
|
+
if (process.env.XDG_DATA_HOME)
|
|
98
|
+
return path.join(process.env.XDG_DATA_HOME, "opencode");
|
|
99
|
+
if (process.platform === "darwin")
|
|
100
|
+
return path.join(os.homedir(), "Library", "Application Support", "opencode");
|
|
101
|
+
if (process.platform === "win32") {
|
|
102
|
+
const root = process.env.LOCALAPPDATA || process.env.APPDATA;
|
|
103
|
+
if (root)
|
|
104
|
+
return path.join(root, "opencode");
|
|
105
|
+
}
|
|
106
|
+
return path.join(os.homedir(), ".local", "share", "opencode");
|
|
107
|
+
}
|
|
108
|
+
function authPath() {
|
|
109
|
+
return path.join(dataPath(), "auth.json");
|
|
110
|
+
}
|
|
111
|
+
function storeDir() {
|
|
112
|
+
return path.join(dataPath(), "auth-switch");
|
|
113
|
+
}
|
|
114
|
+
function storePath() {
|
|
115
|
+
return path.join(storeDir(), "accounts.json");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/parse.ts
|
|
119
|
+
import { createHash } from "crypto";
|
|
120
|
+
function text(v) {
|
|
121
|
+
return typeof v === "string" && v.trim() ? v : undefined;
|
|
122
|
+
}
|
|
123
|
+
function obj(v) {
|
|
124
|
+
return v && typeof v === "object" ? v : undefined;
|
|
125
|
+
}
|
|
126
|
+
function parseJwtClaims(token) {
|
|
127
|
+
const part = token.split(".")[1];
|
|
128
|
+
if (!part)
|
|
129
|
+
return {};
|
|
130
|
+
try {
|
|
131
|
+
return JSON.parse(Buffer.from(part, "base64url").toString("utf8"));
|
|
132
|
+
} catch {
|
|
133
|
+
return {};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function extractEmail(auth) {
|
|
137
|
+
const claims = parseJwtClaims(auth.access);
|
|
138
|
+
const direct = text(claims["https://api.openai.com/profile.email"]);
|
|
139
|
+
if (direct)
|
|
140
|
+
return direct;
|
|
141
|
+
return text(obj(claims["https://api.openai.com/profile"])?.email);
|
|
142
|
+
}
|
|
143
|
+
function extractPlan(auth) {
|
|
144
|
+
return text(parseJwtClaims(auth.access)["https://api.openai.com/auth.chatgpt_plan_type"]);
|
|
145
|
+
}
|
|
146
|
+
function extractAccountId(auth) {
|
|
147
|
+
if (text(auth.accountId))
|
|
148
|
+
return text(auth.accountId);
|
|
149
|
+
const claims = parseJwtClaims(auth.access);
|
|
150
|
+
const direct = text(claims.chatgpt_account_id);
|
|
151
|
+
if (direct)
|
|
152
|
+
return direct;
|
|
153
|
+
const namespaced = text(claims["https://api.openai.com/auth.chatgpt_account_id"]);
|
|
154
|
+
if (namespaced)
|
|
155
|
+
return namespaced;
|
|
156
|
+
const orgs = claims.organizations;
|
|
157
|
+
if (!Array.isArray(orgs) || !orgs.length)
|
|
158
|
+
return;
|
|
159
|
+
return text(obj(orgs[0])?.id);
|
|
160
|
+
}
|
|
161
|
+
function fallback(refresh) {
|
|
162
|
+
return `fallback:${createHash("sha256").update(refresh).digest("hex").slice(0, 16)}`;
|
|
163
|
+
}
|
|
164
|
+
function key(auth) {
|
|
165
|
+
return fallback(auth.refresh);
|
|
166
|
+
}
|
|
167
|
+
function entryFromAuth(auth, now = Date.now()) {
|
|
168
|
+
return {
|
|
169
|
+
key: key(auth),
|
|
170
|
+
email: extractEmail(auth),
|
|
171
|
+
accountId: extractAccountId(auth),
|
|
172
|
+
plan: extractPlan(auth),
|
|
173
|
+
savedAt: now,
|
|
174
|
+
auth: clean(auth)
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function clean(auth) {
|
|
178
|
+
return {
|
|
179
|
+
type: "oauth",
|
|
180
|
+
refresh: auth.refresh,
|
|
181
|
+
access: auth.access,
|
|
182
|
+
expires: auth.expires,
|
|
183
|
+
...text(auth.accountId) ? { accountId: auth.accountId } : {},
|
|
184
|
+
...text(auth.enterpriseUrl) ? { enterpriseUrl: auth.enterpriseUrl } : {}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/store.ts
|
|
189
|
+
function obj2(v) {
|
|
190
|
+
return v && typeof v === "object" ? v : undefined;
|
|
191
|
+
}
|
|
192
|
+
function text2(v) {
|
|
193
|
+
return typeof v === "string" && v.trim() ? v : undefined;
|
|
194
|
+
}
|
|
195
|
+
function num(v) {
|
|
196
|
+
return typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
197
|
+
}
|
|
198
|
+
function validAuth(v) {
|
|
199
|
+
const item = obj2(v);
|
|
200
|
+
if (!item)
|
|
201
|
+
return false;
|
|
202
|
+
if (item.type !== "oauth")
|
|
203
|
+
return false;
|
|
204
|
+
if (!text2(item.refresh) || !text2(item.access) || num(item.expires) === undefined)
|
|
205
|
+
return false;
|
|
206
|
+
if (item.accountId !== undefined && !text2(item.accountId))
|
|
207
|
+
return false;
|
|
208
|
+
if (item.enterpriseUrl !== undefined && !text2(item.enterpriseUrl))
|
|
209
|
+
return false;
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
function validEntry(id, v) {
|
|
213
|
+
const item = obj2(v);
|
|
214
|
+
if (!item || item.key !== id)
|
|
215
|
+
return false;
|
|
216
|
+
if (item.email !== undefined && !text2(item.email))
|
|
217
|
+
return false;
|
|
218
|
+
if (item.accountId !== undefined && !text2(item.accountId))
|
|
219
|
+
return false;
|
|
220
|
+
if (item.plan !== undefined && !text2(item.plan))
|
|
221
|
+
return false;
|
|
222
|
+
if (num(item.savedAt) === undefined)
|
|
223
|
+
return false;
|
|
224
|
+
if (item.lastUsedAt !== undefined && num(item.lastUsedAt) === undefined)
|
|
225
|
+
return false;
|
|
226
|
+
return validAuth(item.auth);
|
|
227
|
+
}
|
|
228
|
+
async function readJson(file) {
|
|
229
|
+
const data = Bun.file(file);
|
|
230
|
+
if (!await data.exists())
|
|
231
|
+
return;
|
|
232
|
+
const raw = (await data.text()).trim();
|
|
233
|
+
if (!raw)
|
|
234
|
+
return;
|
|
235
|
+
return JSON.parse(raw);
|
|
236
|
+
}
|
|
237
|
+
async function writeJson(file, value) {
|
|
238
|
+
await fs.mkdir(storeDir(), { recursive: true });
|
|
239
|
+
await Bun.write(file, `${JSON.stringify(value, null, 2)}
|
|
240
|
+
`);
|
|
241
|
+
}
|
|
242
|
+
function hydrateAccount(id, item) {
|
|
243
|
+
return {
|
|
244
|
+
...item,
|
|
245
|
+
id,
|
|
246
|
+
email: item.email || extractEmail(item.auth),
|
|
247
|
+
accountId: item.accountId || extractAccountId(item.auth)
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function normalizeEntries(entries) {
|
|
251
|
+
const normalized = {};
|
|
252
|
+
let changed = false;
|
|
253
|
+
for (const [id, value] of Object.entries(entries)) {
|
|
254
|
+
if (!validEntry(id, value)) {
|
|
255
|
+
const item = obj2(value);
|
|
256
|
+
if (!item || !validAuth(item.auth)) {
|
|
257
|
+
changed = true;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const auth = clean(item.auth);
|
|
261
|
+
const nextKey2 = fallback(auth.refresh);
|
|
262
|
+
normalized[nextKey2] = {
|
|
263
|
+
key: nextKey2,
|
|
264
|
+
email: text2(item.email) || extractEmail(auth),
|
|
265
|
+
accountId: text2(item.accountId) || extractAccountId(auth),
|
|
266
|
+
plan: text2(item.plan),
|
|
267
|
+
savedAt: num(item.savedAt) ?? Date.now(),
|
|
268
|
+
...num(item.lastUsedAt) !== undefined ? { lastUsedAt: num(item.lastUsedAt) } : {},
|
|
269
|
+
auth
|
|
270
|
+
};
|
|
271
|
+
changed = true;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const nextKey = fallback(value.auth.refresh);
|
|
275
|
+
if (nextKey !== id || value.key !== nextKey)
|
|
276
|
+
changed = true;
|
|
277
|
+
normalized[nextKey] = {
|
|
278
|
+
...value,
|
|
279
|
+
key: nextKey,
|
|
280
|
+
auth: clean(value.auth)
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
return { normalized, changed };
|
|
284
|
+
}
|
|
285
|
+
async function readCurrentAuth() {
|
|
286
|
+
const raw = await readJson(authPath());
|
|
287
|
+
const data = obj2(raw);
|
|
288
|
+
if (!data)
|
|
289
|
+
return;
|
|
290
|
+
const auth = data.openai;
|
|
291
|
+
if (!validAuth(auth))
|
|
292
|
+
return;
|
|
293
|
+
return clean(auth);
|
|
294
|
+
}
|
|
295
|
+
async function writeCurrentOpenAI(auth) {
|
|
296
|
+
const raw = await readJson(authPath()) ?? {};
|
|
297
|
+
const next = obj2(raw) ?? {};
|
|
298
|
+
next.openai = clean(auth);
|
|
299
|
+
await fs.mkdir(path2.dirname(authPath()), { recursive: true });
|
|
300
|
+
await Bun.write(authPath(), `${JSON.stringify(next, null, 2)}
|
|
301
|
+
`);
|
|
302
|
+
}
|
|
303
|
+
async function readStore() {
|
|
304
|
+
let raw;
|
|
305
|
+
try {
|
|
306
|
+
raw = await readJson(storePath());
|
|
307
|
+
} catch {
|
|
308
|
+
return { ok: false, reason: "malformed" };
|
|
309
|
+
}
|
|
310
|
+
if (raw === undefined)
|
|
311
|
+
return { ok: false, reason: "missing" };
|
|
312
|
+
const root = obj2(raw);
|
|
313
|
+
const map = obj2(root?.openai);
|
|
314
|
+
if (!root || root.version !== 1 || !map)
|
|
315
|
+
return { ok: false, reason: "malformed" };
|
|
316
|
+
const { normalized, changed } = normalizeEntries(map);
|
|
317
|
+
if (changed) {
|
|
318
|
+
await writeStore({ version: 1, openai: normalized });
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
ok: true,
|
|
322
|
+
store: {
|
|
323
|
+
version: 1,
|
|
324
|
+
openai: normalized
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
async function writeStore(store) {
|
|
329
|
+
await writeJson(storePath(), store);
|
|
330
|
+
}
|
|
331
|
+
async function pruneInvalidAccounts() {
|
|
332
|
+
const load = await readStore();
|
|
333
|
+
if (!load.ok)
|
|
334
|
+
return load;
|
|
335
|
+
const next = Object.fromEntries(Object.entries(load.store.openai).filter(([id, item]) => validEntry(id, item)));
|
|
336
|
+
if (Object.keys(next).length !== Object.keys(load.store.openai).length) {
|
|
337
|
+
await writeStore({ version: 1, openai: next });
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
ok: true,
|
|
341
|
+
accounts: Object.entries(next).map(([id, item]) => hydrateAccount(id, item))
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function match(list, auth) {
|
|
345
|
+
return list.find((item) => {
|
|
346
|
+
if (item.auth.refresh === auth.refresh)
|
|
347
|
+
return true;
|
|
348
|
+
return item.id === fallback(auth.refresh);
|
|
349
|
+
})?.id;
|
|
350
|
+
}
|
|
351
|
+
async function upsertSavedAccount(auth) {
|
|
352
|
+
const base = entryFromAuth(auth);
|
|
353
|
+
const load = await readStore();
|
|
354
|
+
if (!load.ok && load.reason === "malformed") {
|
|
355
|
+
throw new Error("account store malformed");
|
|
356
|
+
}
|
|
357
|
+
const store = load.ok ? load.store : { version: 1, openai: {} };
|
|
358
|
+
const list = Object.entries(store.openai);
|
|
359
|
+
const match2 = list.find(([id, item]) => {
|
|
360
|
+
if (!validEntry(id, item))
|
|
361
|
+
return false;
|
|
362
|
+
return item.auth.refresh === auth.refresh || id === base.key;
|
|
363
|
+
});
|
|
364
|
+
const prev = match2?.[1];
|
|
365
|
+
const next = {
|
|
366
|
+
...base,
|
|
367
|
+
savedAt: prev?.savedAt ?? base.savedAt,
|
|
368
|
+
lastUsedAt: prev?.lastUsedAt
|
|
369
|
+
};
|
|
370
|
+
const out = Object.fromEntries(list.filter(([id]) => id !== match2?.[0]).filter(([id, item]) => validEntry(id, item)));
|
|
371
|
+
out[next.key] = next;
|
|
372
|
+
await writeStore({ version: 1, openai: out });
|
|
373
|
+
return { ...next, id: next.key };
|
|
374
|
+
}
|
|
375
|
+
function sort(list, cur) {
|
|
376
|
+
return [...list].sort((a, b) => {
|
|
377
|
+
const ac = a.id === cur ? 1 : 0;
|
|
378
|
+
const bc = b.id === cur ? 1 : 0;
|
|
379
|
+
if (ac !== bc)
|
|
380
|
+
return bc - ac;
|
|
381
|
+
if ((b.lastUsedAt ?? 0) !== (a.lastUsedAt ?? 0))
|
|
382
|
+
return (b.lastUsedAt ?? 0) - (a.lastUsedAt ?? 0);
|
|
383
|
+
if (b.savedAt !== a.savedAt)
|
|
384
|
+
return b.savedAt - a.savedAt;
|
|
385
|
+
return (a.email || a.accountId || a.id).localeCompare(b.email || b.accountId || b.id);
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
async function listAccounts() {
|
|
389
|
+
const load = await pruneInvalidAccounts();
|
|
390
|
+
if (!load.ok)
|
|
391
|
+
return load;
|
|
392
|
+
const auth = await readCurrentAuth();
|
|
393
|
+
const cur = auth ? match(load.accounts, auth) ?? fallback(auth.refresh) : undefined;
|
|
394
|
+
return { ok: true, accounts: sort(load.accounts, cur), current: cur };
|
|
395
|
+
}
|
|
396
|
+
async function switchAccount(id) {
|
|
397
|
+
const load = await readStore();
|
|
398
|
+
if (!load.ok)
|
|
399
|
+
throw new Error("account store unavailable");
|
|
400
|
+
const item = load.store.openai[id];
|
|
401
|
+
if (!validEntry(id, item))
|
|
402
|
+
throw new Error("saved account not found");
|
|
403
|
+
await writeCurrentOpenAI(item.auth);
|
|
404
|
+
await writeStore({
|
|
405
|
+
version: 1,
|
|
406
|
+
openai: {
|
|
407
|
+
...load.store.openai,
|
|
408
|
+
[id]: {
|
|
409
|
+
...item,
|
|
410
|
+
lastUsedAt: Date.now()
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
async function deleteSavedAccount(id) {
|
|
416
|
+
const load = await readStore();
|
|
417
|
+
if (!load.ok)
|
|
418
|
+
throw new Error("account store unavailable");
|
|
419
|
+
if (!(id in load.store.openai))
|
|
420
|
+
throw new Error("saved account not found");
|
|
421
|
+
const next = { ...load.store.openai };
|
|
422
|
+
delete next[id];
|
|
423
|
+
await writeStore({ version: 1, openai: next });
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// src/login.tsx
|
|
427
|
+
function visible(prompt, values) {
|
|
428
|
+
if (!prompt.when)
|
|
429
|
+
return true;
|
|
430
|
+
const cur = values[prompt.when.key];
|
|
431
|
+
if (prompt.when.op === "eq")
|
|
432
|
+
return cur === prompt.when.value;
|
|
433
|
+
return cur !== prompt.when.value;
|
|
434
|
+
}
|
|
435
|
+
function same(prev, next) {
|
|
436
|
+
if (!prev || !next)
|
|
437
|
+
return false;
|
|
438
|
+
return prev.refresh === next.refresh;
|
|
439
|
+
}
|
|
440
|
+
function bind(api, authz) {
|
|
441
|
+
useKeyboard((evt) => {
|
|
442
|
+
if (evt.name !== "c" || evt.ctrl || evt.meta)
|
|
443
|
+
return;
|
|
444
|
+
evt.preventDefault();
|
|
445
|
+
evt.stopPropagation();
|
|
446
|
+
clip(target(authz)).then((ok) => api.ui.toast({
|
|
447
|
+
variant: ok ? "info" : "warning",
|
|
448
|
+
message: ok ? "Copied to clipboard" : "Failed to copy to clipboard"
|
|
449
|
+
}));
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
function WaitView(props) {
|
|
453
|
+
bind(props.api, props.authz);
|
|
454
|
+
return (() => {
|
|
455
|
+
var _el$ = _$createElement("box"), _el$2 = _$createElement("text"), _el$3 = _$createElement("text"), _el$4 = _$createElement("text"), _el$5 = _$createElement("text"), _el$7 = _$createElement("text");
|
|
456
|
+
_$insertNode(_el$, _el$2);
|
|
457
|
+
_$insertNode(_el$, _el$3);
|
|
458
|
+
_$insertNode(_el$, _el$4);
|
|
459
|
+
_$insertNode(_el$, _el$5);
|
|
460
|
+
_$insertNode(_el$, _el$7);
|
|
461
|
+
_$setProp(_el$, "paddingLeft", 2);
|
|
462
|
+
_$setProp(_el$, "paddingRight", 2);
|
|
463
|
+
_$setProp(_el$, "gap", 1);
|
|
464
|
+
_$setProp(_el$, "paddingBottom", 1);
|
|
465
|
+
_$insert(_el$2, () => props.title);
|
|
466
|
+
_$insert(_el$3, () => props.authz.instructions);
|
|
467
|
+
_$insert(_el$4, () => props.authz.url);
|
|
468
|
+
_$insertNode(_el$5, _$createTextNode(`Waiting for authorization...`));
|
|
469
|
+
_$insertNode(_el$7, _$createTextNode(`Press c to copy the link`));
|
|
470
|
+
_$effect((_$p) => _$setProp(_el$2, "attributes", TextAttributes.BOLD, _$p));
|
|
471
|
+
return _el$;
|
|
472
|
+
})();
|
|
473
|
+
}
|
|
474
|
+
function CodePrompt(props) {
|
|
475
|
+
bind(props.api, props.authz);
|
|
476
|
+
return _$createComponent(props.api.ui.DialogPrompt, {
|
|
477
|
+
get title() {
|
|
478
|
+
return props.title;
|
|
479
|
+
},
|
|
480
|
+
placeholder: "Authorization code",
|
|
481
|
+
get onConfirm() {
|
|
482
|
+
return props.onConfirm;
|
|
483
|
+
},
|
|
484
|
+
get onCancel() {
|
|
485
|
+
return props.onCancel;
|
|
486
|
+
},
|
|
487
|
+
description: () => (() => {
|
|
488
|
+
var _el$9 = _$createElement("box"), _el$0 = _$createElement("text"), _el$1 = _$createElement("text"), _el$10 = _$createElement("text");
|
|
489
|
+
_$insertNode(_el$9, _el$0);
|
|
490
|
+
_$insertNode(_el$9, _el$1);
|
|
491
|
+
_$insertNode(_el$9, _el$10);
|
|
492
|
+
_$setProp(_el$9, "gap", 1);
|
|
493
|
+
_$insert(_el$0, () => props.authz.instructions);
|
|
494
|
+
_$insert(_el$1, () => props.authz.url);
|
|
495
|
+
_$insertNode(_el$10, _$createTextNode(`Press c to copy the code`));
|
|
496
|
+
return _el$9;
|
|
497
|
+
})()
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
function wait(api, title, authz, run) {
|
|
501
|
+
api.ui.dialog.replace(() => _$createComponent(WaitView, {
|
|
502
|
+
api,
|
|
503
|
+
title,
|
|
504
|
+
authz
|
|
505
|
+
}));
|
|
506
|
+
run();
|
|
507
|
+
}
|
|
508
|
+
async function choose(api, methods) {
|
|
509
|
+
if (methods.length === 1)
|
|
510
|
+
return 0;
|
|
511
|
+
return await new Promise((resolve) => {
|
|
512
|
+
api.ui.dialog.replace(() => _$createComponent(api.ui.DialogSelect, {
|
|
513
|
+
title: "Select auth method",
|
|
514
|
+
get options() {
|
|
515
|
+
return methods.map((item, index) => ({
|
|
516
|
+
title: item.method.label,
|
|
517
|
+
value: index
|
|
518
|
+
}));
|
|
519
|
+
},
|
|
520
|
+
onSelect: (item) => resolve(item.value)
|
|
521
|
+
}), () => resolve(null));
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
async function ask(api, title, prompt) {
|
|
525
|
+
if (prompt.type === "text") {
|
|
526
|
+
return await new Promise((resolve) => {
|
|
527
|
+
api.ui.dialog.replace(() => (() => {
|
|
528
|
+
var _el$12 = _$createElement("box");
|
|
529
|
+
_$setProp(_el$12, "paddingLeft", 2);
|
|
530
|
+
_$setProp(_el$12, "paddingRight", 2);
|
|
531
|
+
_$setProp(_el$12, "paddingTop", 2);
|
|
532
|
+
_$setProp(_el$12, "paddingBottom", 2);
|
|
533
|
+
_$insert(_el$12, _$createComponent(api.ui.DialogPrompt, {
|
|
534
|
+
title,
|
|
535
|
+
get placeholder() {
|
|
536
|
+
return prompt.placeholder;
|
|
537
|
+
},
|
|
538
|
+
onConfirm: (value) => resolve(value),
|
|
539
|
+
onCancel: () => resolve(null),
|
|
540
|
+
description: () => (() => {
|
|
541
|
+
var _el$13 = _$createElement("text");
|
|
542
|
+
_$insert(_el$13, () => prompt.message);
|
|
543
|
+
return _el$13;
|
|
544
|
+
})()
|
|
545
|
+
}));
|
|
546
|
+
return _el$12;
|
|
547
|
+
})(), () => resolve(null));
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
return await new Promise((resolve) => {
|
|
551
|
+
api.ui.dialog.replace(() => (() => {
|
|
552
|
+
var _el$14 = _$createElement("box");
|
|
553
|
+
_$setProp(_el$14, "paddingLeft", 2);
|
|
554
|
+
_$setProp(_el$14, "paddingRight", 2);
|
|
555
|
+
_$setProp(_el$14, "paddingTop", 2);
|
|
556
|
+
_$setProp(_el$14, "paddingBottom", 2);
|
|
557
|
+
_$insert(_el$14, _$createComponent(api.ui.DialogSelect, {
|
|
558
|
+
get title() {
|
|
559
|
+
return prompt.message;
|
|
560
|
+
},
|
|
561
|
+
get options() {
|
|
562
|
+
return (prompt.options ?? []).map((item) => ({
|
|
563
|
+
title: item.label,
|
|
564
|
+
value: item.value,
|
|
565
|
+
description: item.hint
|
|
566
|
+
}));
|
|
567
|
+
},
|
|
568
|
+
onSelect: (item) => resolve(item.value)
|
|
569
|
+
}));
|
|
570
|
+
return _el$14;
|
|
571
|
+
})(), () => resolve(null));
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
async function prompts(api, title, method) {
|
|
575
|
+
const values = {};
|
|
576
|
+
for (const prompt of method.prompts ?? []) {
|
|
577
|
+
if (!visible(prompt, values))
|
|
578
|
+
continue;
|
|
579
|
+
const value = await ask(api, title, prompt);
|
|
580
|
+
if (value == null)
|
|
581
|
+
return;
|
|
582
|
+
values[prompt.key] = value;
|
|
583
|
+
}
|
|
584
|
+
return values;
|
|
585
|
+
}
|
|
586
|
+
async function save(api, prev) {
|
|
587
|
+
let activeUpdated = false;
|
|
588
|
+
try {
|
|
589
|
+
const client = api.client;
|
|
590
|
+
if (typeof api.client.instance.dispose === "function") {
|
|
591
|
+
await api.client.instance.dispose();
|
|
592
|
+
}
|
|
593
|
+
if (typeof client.sync?.bootstrap === "function") {
|
|
594
|
+
await client.sync.bootstrap();
|
|
595
|
+
}
|
|
596
|
+
const auth = await readCurrentAuth();
|
|
597
|
+
if (!auth) {
|
|
598
|
+
api.ui.toast({
|
|
599
|
+
variant: "error",
|
|
600
|
+
message: "Login completed, but saved account could not be loaded"
|
|
601
|
+
});
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
activeUpdated = true;
|
|
605
|
+
if (same(prev, auth)) {
|
|
606
|
+
api.ui.toast({
|
|
607
|
+
variant: "warning",
|
|
608
|
+
message: "Login completed, but OpenAI account did not change"
|
|
609
|
+
});
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
await upsertSavedAccount(auth);
|
|
613
|
+
return true;
|
|
614
|
+
} catch {
|
|
615
|
+
api.ui.toast({
|
|
616
|
+
variant: "error",
|
|
617
|
+
message: activeUpdated ? "Login completed, but saving the account failed" : "Login completed, but account sync failed"
|
|
618
|
+
});
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
async function code(api, index, method, authz) {
|
|
623
|
+
const prev = await readCurrentAuth();
|
|
624
|
+
const ok = await new Promise((resolve) => {
|
|
625
|
+
api.ui.dialog.replace(() => _$createComponent(CodePrompt, {
|
|
626
|
+
api,
|
|
627
|
+
get title() {
|
|
628
|
+
return method.label;
|
|
629
|
+
},
|
|
630
|
+
authz,
|
|
631
|
+
onConfirm: async (value) => {
|
|
632
|
+
resolve(await runOAuthCallback(api.client.provider.oauth.callback, {
|
|
633
|
+
providerID: "openai",
|
|
634
|
+
method: index,
|
|
635
|
+
code: value
|
|
636
|
+
}));
|
|
637
|
+
},
|
|
638
|
+
onCancel: () => resolve(false)
|
|
639
|
+
}), () => resolve(false));
|
|
640
|
+
});
|
|
641
|
+
if (!ok) {
|
|
642
|
+
api.ui.toast({
|
|
643
|
+
variant: "error",
|
|
644
|
+
message: "Login failed"
|
|
645
|
+
});
|
|
646
|
+
return false;
|
|
647
|
+
}
|
|
648
|
+
return save(api, prev);
|
|
649
|
+
}
|
|
650
|
+
async function auto(api, index, method, authz) {
|
|
651
|
+
const prev = await readCurrentAuth();
|
|
652
|
+
const ok = await new Promise((resolve) => {
|
|
653
|
+
wait(api, method.label, authz, async () => {
|
|
654
|
+
resolve(await runOAuthCallback(api.client.provider.oauth.callback, {
|
|
655
|
+
providerID: "openai",
|
|
656
|
+
method: index
|
|
657
|
+
}));
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
if (!ok) {
|
|
661
|
+
api.ui.toast({
|
|
662
|
+
variant: "error",
|
|
663
|
+
message: "Login failed"
|
|
664
|
+
});
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
return save(api, prev);
|
|
668
|
+
}
|
|
669
|
+
async function hasLogin(api) {
|
|
670
|
+
try {
|
|
671
|
+
const res = await api.client.provider.auth();
|
|
672
|
+
const methods = res.data?.openai ?? [];
|
|
673
|
+
return methods.some((item) => item.type === "oauth");
|
|
674
|
+
} catch {
|
|
675
|
+
return false;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
async function loginOpenAI(api) {
|
|
679
|
+
try {
|
|
680
|
+
const auth = await api.client.provider.auth();
|
|
681
|
+
const methods = (auth.data?.openai ?? []).map((method2, index2) => ({
|
|
682
|
+
method: method2,
|
|
683
|
+
index: index2
|
|
684
|
+
})).filter((item) => item.method.type === "oauth");
|
|
685
|
+
if (!methods.length) {
|
|
686
|
+
api.ui.toast({
|
|
687
|
+
variant: "error",
|
|
688
|
+
message: "OpenAI OAuth login unavailable"
|
|
689
|
+
});
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
const index = await choose(api, methods);
|
|
693
|
+
if (index == null)
|
|
694
|
+
return false;
|
|
695
|
+
const picked = methods[index];
|
|
696
|
+
const method = picked.method;
|
|
697
|
+
const inputs = await prompts(api, method.label, method);
|
|
698
|
+
if (method.prompts?.length && !inputs)
|
|
699
|
+
return false;
|
|
700
|
+
const authz = await api.client.provider.oauth.authorize({
|
|
701
|
+
providerID: "openai",
|
|
702
|
+
method: picked.index,
|
|
703
|
+
inputs
|
|
704
|
+
});
|
|
705
|
+
if (authz.error || !authz.data) {
|
|
706
|
+
api.ui.toast({
|
|
707
|
+
variant: "error",
|
|
708
|
+
message: "Login failed"
|
|
709
|
+
});
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
if (authz.data.method === "code")
|
|
713
|
+
return code(api, picked.index, method, authz.data);
|
|
714
|
+
if (authz.data.method === "auto")
|
|
715
|
+
return auto(api, picked.index, method, authz.data);
|
|
716
|
+
api.ui.toast({
|
|
717
|
+
variant: "error",
|
|
718
|
+
message: "Unsupported auth method"
|
|
719
|
+
});
|
|
720
|
+
return false;
|
|
721
|
+
} catch {
|
|
722
|
+
api.ui.toast({
|
|
723
|
+
variant: "error",
|
|
724
|
+
message: "Login failed"
|
|
725
|
+
});
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// src/tui.tsx
|
|
731
|
+
var seq = 0;
|
|
732
|
+
function frame(text3) {
|
|
733
|
+
return (() => {
|
|
734
|
+
var _el$ = _$createElement2("box"), _el$2 = _$createElement2("text");
|
|
735
|
+
_$insertNode2(_el$, _el$2);
|
|
736
|
+
_$setProp2(_el$, "paddingLeft", 2);
|
|
737
|
+
_$setProp2(_el$, "paddingRight", 2);
|
|
738
|
+
_$setProp2(_el$, "paddingTop", 2);
|
|
739
|
+
_$setProp2(_el$, "paddingBottom", 2);
|
|
740
|
+
_$insert2(_el$2, text3);
|
|
741
|
+
return _el$;
|
|
742
|
+
})();
|
|
743
|
+
}
|
|
744
|
+
async function pick(api, list) {
|
|
745
|
+
return await new Promise((resolve) => {
|
|
746
|
+
api.ui.dialog.replace(() => _$createComponent2(api.ui.DialogSelect, {
|
|
747
|
+
title: "Remove saved account",
|
|
748
|
+
get options() {
|
|
749
|
+
return list.map((item) => ({
|
|
750
|
+
title: accountTitle(item),
|
|
751
|
+
value: item.id,
|
|
752
|
+
category: accountDescription(item, false)
|
|
753
|
+
}));
|
|
754
|
+
},
|
|
755
|
+
onSelect: (item) => resolve(item.value)
|
|
756
|
+
}), () => resolve(null));
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
async function confirm(api, title, message) {
|
|
760
|
+
return await new Promise((resolve) => {
|
|
761
|
+
api.ui.dialog.replace(() => _$createComponent2(api.ui.DialogConfirm, {
|
|
762
|
+
title,
|
|
763
|
+
message,
|
|
764
|
+
onConfirm: () => resolve(true),
|
|
765
|
+
onCancel: () => resolve(false)
|
|
766
|
+
}), () => resolve(false));
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
async function remove(api, list) {
|
|
770
|
+
const id = await pick(api, list);
|
|
771
|
+
if (!id)
|
|
772
|
+
return;
|
|
773
|
+
const item = list.find((row) => row.id === id);
|
|
774
|
+
if (!item)
|
|
775
|
+
return;
|
|
776
|
+
const ok = await confirm(api, "Remove saved account", `Remove ${accountTitle(item)} from saved accounts?`);
|
|
777
|
+
if (!ok)
|
|
778
|
+
return;
|
|
779
|
+
try {
|
|
780
|
+
await deleteSavedAccount(id);
|
|
781
|
+
api.ui.toast({
|
|
782
|
+
variant: "success",
|
|
783
|
+
message: "Saved account removed"
|
|
784
|
+
});
|
|
785
|
+
} catch {
|
|
786
|
+
api.ui.toast({
|
|
787
|
+
variant: "error",
|
|
788
|
+
message: "Failed to remove saved account"
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
function options(list, current, canLogin) {
|
|
793
|
+
const rows = list.map((item) => ({
|
|
794
|
+
title: accountTitle(item),
|
|
795
|
+
value: {
|
|
796
|
+
type: "account",
|
|
797
|
+
id: item.id
|
|
798
|
+
},
|
|
799
|
+
description: accountDescription(item, item.id === current),
|
|
800
|
+
footer: accountFooter(item.id === current),
|
|
801
|
+
category: "Accounts"
|
|
802
|
+
}));
|
|
803
|
+
return [...canLogin ? [{
|
|
804
|
+
title: loginTitle(),
|
|
805
|
+
value: {
|
|
806
|
+
type: "login"
|
|
807
|
+
}
|
|
808
|
+
}] : [], ...rows, ...list.length ? [{
|
|
809
|
+
title: logoutTitle(true),
|
|
810
|
+
value: {
|
|
811
|
+
type: "logout"
|
|
812
|
+
},
|
|
813
|
+
description: "Remove a saved account",
|
|
814
|
+
category: "Actions"
|
|
815
|
+
}] : []];
|
|
816
|
+
}
|
|
817
|
+
async function open(api) {
|
|
818
|
+
const id = ++seq;
|
|
819
|
+
api.ui.dialog.setSize("large");
|
|
820
|
+
api.ui.dialog.replace(() => frame("Loading accounts..."));
|
|
821
|
+
const [store, canLogin] = await Promise.all([listAccounts(), hasLogin(api)]);
|
|
822
|
+
if (id !== seq)
|
|
823
|
+
return;
|
|
824
|
+
if (!store.ok && store.reason === "malformed") {
|
|
825
|
+
api.ui.toast({
|
|
826
|
+
variant: "error",
|
|
827
|
+
message: "Failed to load account store"
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
const accounts = store.ok ? store.accounts : [];
|
|
831
|
+
const current = store.ok ? store.current : undefined;
|
|
832
|
+
const rows = options(accounts, current, canLogin);
|
|
833
|
+
if (!rows.length) {
|
|
834
|
+
api.ui.dialog.replace(() => frame("OpenAI login unavailable and no saved accounts found"));
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
if (!accounts.length && canLogin) {
|
|
838
|
+
api.ui.dialog.replace(() => _$createComponent2(api.ui.DialogSelect, {
|
|
839
|
+
title: "Switch OpenAI account",
|
|
840
|
+
options: rows,
|
|
841
|
+
placeholder: "Search",
|
|
842
|
+
onSelect: (item) => void act(api, item.value, accounts)
|
|
843
|
+
}));
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
api.ui.dialog.replace(() => _$createComponent2(api.ui.DialogSelect, {
|
|
847
|
+
title: "Switch OpenAI account",
|
|
848
|
+
options: rows,
|
|
849
|
+
placeholder: "Search",
|
|
850
|
+
current: current ? {
|
|
851
|
+
type: "account",
|
|
852
|
+
id: current
|
|
853
|
+
} : undefined,
|
|
854
|
+
onSelect: (item) => void act(api, item.value, accounts)
|
|
855
|
+
}));
|
|
856
|
+
}
|
|
857
|
+
async function act(api, action, list) {
|
|
858
|
+
if (action.type === "login") {
|
|
859
|
+
const ok = await loginOpenAI(api);
|
|
860
|
+
if (ok) {
|
|
861
|
+
api.ui.toast({
|
|
862
|
+
variant: "success",
|
|
863
|
+
message: "Account saved"
|
|
864
|
+
});
|
|
865
|
+
await open(api);
|
|
866
|
+
}
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
if (action.type === "logout") {
|
|
870
|
+
await remove(api, list);
|
|
871
|
+
await open(api);
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
try {
|
|
875
|
+
await switchAccount(action.id);
|
|
876
|
+
} catch {
|
|
877
|
+
api.ui.toast({
|
|
878
|
+
variant: "error",
|
|
879
|
+
message: "Failed to switch account"
|
|
880
|
+
});
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
try {
|
|
884
|
+
const client = api.client;
|
|
885
|
+
if (typeof api.client.instance.dispose === "function") {
|
|
886
|
+
await api.client.instance.dispose();
|
|
887
|
+
}
|
|
888
|
+
if (typeof client.sync?.bootstrap === "function") {
|
|
889
|
+
await client.sync.bootstrap();
|
|
890
|
+
}
|
|
891
|
+
api.ui.toast({
|
|
892
|
+
variant: "success",
|
|
893
|
+
message: "Switched OpenAI account"
|
|
894
|
+
});
|
|
895
|
+
api.ui.dialog.clear();
|
|
896
|
+
} catch {
|
|
897
|
+
api.ui.toast({
|
|
898
|
+
variant: "warning",
|
|
899
|
+
message: "Account file was switched, but session refresh failed. Restart OpenCode to apply it."
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
var tui = async (api) => {
|
|
904
|
+
api.command.register(() => [{
|
|
905
|
+
title: "Switch OpenAI account",
|
|
906
|
+
value: "openai.switch",
|
|
907
|
+
description: "Login, switch, or remove saved OpenAI accounts",
|
|
908
|
+
slash: {
|
|
909
|
+
name: "switch"
|
|
910
|
+
},
|
|
911
|
+
onSelect: () => {
|
|
912
|
+
open(api);
|
|
913
|
+
}
|
|
914
|
+
}]);
|
|
915
|
+
};
|
|
916
|
+
var tui_default = {
|
|
917
|
+
id: "harars.switch-auth",
|
|
918
|
+
tui
|
|
919
|
+
};
|
|
920
|
+
export {
|
|
921
|
+
tui_default as default
|
|
922
|
+
};
|