osdy-pi 0.1.8 → 1.2.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 +209 -161
- package/bin/osdy-pi.mjs +127 -0
- package/extensions/osdy-pi/account-profiles.test.ts +517 -0
- package/extensions/osdy-pi/account-profiles.ts +581 -0
- package/extensions/osdy-pi/animation.ts +47 -5
- package/extensions/osdy-pi/constants.ts +282 -103
- package/extensions/osdy-pi/diff-panel.ts +78 -24
- package/extensions/osdy-pi/editor-settings.test.ts +163 -0
- package/extensions/osdy-pi/editor-settings.ts +112 -0
- package/extensions/osdy-pi/metrics.ts +8 -135
- package/extensions/osdy-pi/plugin-events.ts +26 -0
- package/extensions/osdy-pi/profile-label.ts +29 -0
- package/extensions/osdy-pi/runtime-helpers.test.ts +26 -0
- package/extensions/osdy-pi/runtime-helpers.ts +87 -44
- package/extensions/osdy-pi/runtime.test.ts +62 -0
- package/extensions/osdy-pi/runtime.ts +256 -64
- package/extensions/osdy-pi/types.ts +33 -10
- package/extensions/osdy-pi/ui.test.ts +51 -0
- package/extensions/osdy-pi/ui.ts +298 -114
- package/extensions/osdy-pi/utils.ts +43 -1
- package/extensions/osdy-pi/working-tree.ts +25 -6
- package/package.json +22 -5
- package/scripts/osdy-pi-account-profiles.mjs +501 -0
- package/themes/osdy-pi-catppuccin-frappe.json +94 -0
- package/themes/osdy-pi-catppuccin-latte.json +94 -0
- package/themes/osdy-pi-catppuccin-macchiato.json +94 -0
- package/themes/osdy-pi-catppuccin-mocha.json +94 -0
- package/themes/osdy-pi-dark.json +0 -6
- package/themes/osdy-pi-dracula.json +83 -0
- package/themes/osdy-pi-kanagawa-dragon.json +91 -0
- package/themes/osdy-pi-kanagawa-lotus.json +92 -0
- package/themes/osdy-pi-kanagawa-wave.json +91 -0
- package/themes/osdy-pi-lucent-orange.json +80 -0
- package/themes/osdy-pi-matrix.json +83 -0
- package/themes/osdy-pi-new.json +91 -0
- package/themes/osdy-pi-sexy.json +84 -0
- package/themes/osdy-pi-tokyo-night.json +83 -0
- package/themes/osdy-pi-light.json +0 -84
- package/themes/osdy-pi-simple.json +0 -83
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
import { spawn as spawnChild } from "node:child_process";
|
|
2
|
+
import type { ChildProcess } from "node:child_process";
|
|
3
|
+
import { readdir } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
7
|
+
import type {
|
|
8
|
+
ExtensionCommandContext,
|
|
9
|
+
ExtensionContext,
|
|
10
|
+
} from "@earendil-works/pi-coding-agent";
|
|
11
|
+
|
|
12
|
+
const PROFILE_NAME = /^[a-z0-9](?:[a-z0-9-]{0,62})$/;
|
|
13
|
+
const RESERVED_PROFILE_NAMES = new Set(["default", "profiles", "auth.json"]);
|
|
14
|
+
|
|
15
|
+
export type AccountContext = Pick<
|
|
16
|
+
ExtensionCommandContext,
|
|
17
|
+
"isIdle" | "waitForIdle" | "shutdown"
|
|
18
|
+
> & {
|
|
19
|
+
sessionManager: Pick<
|
|
20
|
+
ExtensionCommandContext["sessionManager"],
|
|
21
|
+
"getSessionFile"
|
|
22
|
+
>;
|
|
23
|
+
ui: Pick<ExtensionContext["ui"], "notify" | "select" | "input">;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export interface AccountHandoffDependencies {
|
|
27
|
+
spawn(command: string, args: string[]): Promise<void>;
|
|
28
|
+
launcher?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AccountManagementDependencies {
|
|
32
|
+
profiles(): Promise<string[]>;
|
|
33
|
+
run(args: string[]): Promise<{ code: number; stdout: string; stderr: string }>;
|
|
34
|
+
activeProfile?: string | undefined;
|
|
35
|
+
handoff?: (profile: string) => Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type LauncherMessage =
|
|
39
|
+
| { type: "osdy-pi-ready" }
|
|
40
|
+
| { type: "osdy-pi-error"; message: string };
|
|
41
|
+
|
|
42
|
+
type ScheduleTimeout = (callback: () => void, delay: number) => NodeJS.Timeout;
|
|
43
|
+
|
|
44
|
+
const LAUNCHER_READY_TIMEOUT_MS = 10_000;
|
|
45
|
+
|
|
46
|
+
function isProfileName(value: string): boolean {
|
|
47
|
+
return PROFILE_NAME.test(value) && !RESERVED_PROFILE_NAMES.has(value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sharedAgentDir(env: NodeJS.ProcessEnv): string {
|
|
51
|
+
return resolve(
|
|
52
|
+
env.OSDY_PI_SHARED_AGENT_DIR ??
|
|
53
|
+
env.PI_CODING_AGENT_DIR ??
|
|
54
|
+
join(homedir(), ".pi", "agent"),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function availableProfiles(baseDir: string): Promise<string[]> {
|
|
59
|
+
try {
|
|
60
|
+
const entries = await readdir(join(baseDir, "osdy-pi", "profiles"), {
|
|
61
|
+
withFileTypes: true,
|
|
62
|
+
});
|
|
63
|
+
const profiles: string[] = [];
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
if (entry.isDirectory() && isProfileName(entry.name))
|
|
66
|
+
profiles.push(entry.name);
|
|
67
|
+
}
|
|
68
|
+
return profiles.sort(compareStrings);
|
|
69
|
+
} catch (error: unknown) {
|
|
70
|
+
if (isErrorCode(error, "ENOENT")) return [];
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isErrorCode(error: unknown, code: string): boolean {
|
|
76
|
+
return (
|
|
77
|
+
typeof error === "object" &&
|
|
78
|
+
error !== null &&
|
|
79
|
+
"code" in error &&
|
|
80
|
+
error.code === code
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function compareStrings(left: string, right: string): number {
|
|
85
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function handoffToAccount(
|
|
89
|
+
ctx: AccountContext,
|
|
90
|
+
profile: string,
|
|
91
|
+
dependencies: AccountHandoffDependencies,
|
|
92
|
+
): Promise<void> {
|
|
93
|
+
if (!isProfileName(profile)) {
|
|
94
|
+
ctx.ui.notify(
|
|
95
|
+
"Cannot switch accounts: the selected profile name is invalid.",
|
|
96
|
+
"warning",
|
|
97
|
+
);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const sessionPath = ctx.sessionManager.getSessionFile();
|
|
101
|
+
if (!sessionPath || !isAbsolute(sessionPath)) {
|
|
102
|
+
ctx.ui.notify(
|
|
103
|
+
"Cannot switch accounts: the current session has not been saved yet.",
|
|
104
|
+
"warning",
|
|
105
|
+
);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (!ctx.isIdle()) await ctx.waitForIdle();
|
|
109
|
+
const launcher = dependencies.launcher ?? "osdy-pi";
|
|
110
|
+
try {
|
|
111
|
+
await dependencies.spawn(launcher, [
|
|
112
|
+
"account",
|
|
113
|
+
"use",
|
|
114
|
+
profile,
|
|
115
|
+
"--",
|
|
116
|
+
"--session",
|
|
117
|
+
sessionPath,
|
|
118
|
+
]);
|
|
119
|
+
} catch {
|
|
120
|
+
ctx.ui.notify(
|
|
121
|
+
"Cannot switch accounts: unable to start the replacement Pi process.",
|
|
122
|
+
"warning",
|
|
123
|
+
);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
ctx.shutdown();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function isLauncherMessage(message: unknown): message is LauncherMessage {
|
|
130
|
+
if (typeof message !== "object" || message === null || !("type" in message))
|
|
131
|
+
return false;
|
|
132
|
+
if (message.type === "osdy-pi-ready") return true;
|
|
133
|
+
return (
|
|
134
|
+
message.type === "osdy-pi-error" &&
|
|
135
|
+
"message" in message &&
|
|
136
|
+
typeof message.message === "string"
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function waitForLauncherReady(
|
|
141
|
+
child: ChildProcess,
|
|
142
|
+
timeoutMs = LAUNCHER_READY_TIMEOUT_MS,
|
|
143
|
+
scheduleTimeout: ScheduleTimeout = setTimeout,
|
|
144
|
+
): Promise<void> {
|
|
145
|
+
return new Promise((resolveReady, rejectReady) => {
|
|
146
|
+
let settled = false;
|
|
147
|
+
const cleanup = (): void => {
|
|
148
|
+
clearTimeout(timeout);
|
|
149
|
+
child.removeListener("error", onError);
|
|
150
|
+
child.removeListener("exit", onExit);
|
|
151
|
+
child.removeListener("message", onMessage);
|
|
152
|
+
};
|
|
153
|
+
const fail = (error: Error): void => {
|
|
154
|
+
if (settled) return;
|
|
155
|
+
settled = true;
|
|
156
|
+
cleanup();
|
|
157
|
+
rejectReady(error);
|
|
158
|
+
};
|
|
159
|
+
const onError = (error: Error): void => fail(error);
|
|
160
|
+
const onExit = (): void =>
|
|
161
|
+
fail(new Error("Replacement Pi exited before confirming readiness."));
|
|
162
|
+
const onMessage = (message: unknown): void => {
|
|
163
|
+
if (!isLauncherMessage(message)) return;
|
|
164
|
+
if (message.type === "osdy-pi-error") {
|
|
165
|
+
fail(new Error(message.message));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (settled) return;
|
|
169
|
+
settled = true;
|
|
170
|
+
cleanup();
|
|
171
|
+
child.disconnect();
|
|
172
|
+
child.unref();
|
|
173
|
+
resolveReady();
|
|
174
|
+
};
|
|
175
|
+
const timeout = scheduleTimeout(
|
|
176
|
+
() =>
|
|
177
|
+
fail(new Error("Replacement Pi timed out before confirming readiness.")),
|
|
178
|
+
timeoutMs,
|
|
179
|
+
);
|
|
180
|
+
child.once("error", onError);
|
|
181
|
+
child.once("exit", onExit);
|
|
182
|
+
child.on("message", onMessage);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function bundledLauncherPath(): string {
|
|
187
|
+
return fileURLToPath(new URL("../../bin/osdy-pi.mjs", import.meta.url));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function validProfileName(value: string): boolean {
|
|
191
|
+
return isProfileName(value);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function managementError(result: { code: number; stderr: string }): string {
|
|
195
|
+
return (
|
|
196
|
+
result.stderr
|
|
197
|
+
.replaceAll(/[\r\n]+/g, " ")
|
|
198
|
+
.trim()
|
|
199
|
+
.slice(0, 180) || `command exited ${result.code}`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export type DefaultAccountState =
|
|
204
|
+
| { status: "valid"; profile: string }
|
|
205
|
+
| { status: "unset" }
|
|
206
|
+
| { status: "invalid" }
|
|
207
|
+
| { status: "error"; message: string };
|
|
208
|
+
|
|
209
|
+
export function parseDefaultAccountResult(result: {
|
|
210
|
+
code: number;
|
|
211
|
+
stdout: string;
|
|
212
|
+
stderr: string;
|
|
213
|
+
}): DefaultAccountState {
|
|
214
|
+
if (result.code !== 0)
|
|
215
|
+
return { status: "error", message: managementError(result) };
|
|
216
|
+
const output = result.stdout.trim();
|
|
217
|
+
if (output === "No default account.") return { status: "unset" };
|
|
218
|
+
if (output === "Default account metadata is invalid; no account selected.")
|
|
219
|
+
return { status: "invalid" };
|
|
220
|
+
if (isProfileName(output)) return { status: "valid", profile: output };
|
|
221
|
+
return {
|
|
222
|
+
status: "error",
|
|
223
|
+
message: `unexpected default account output: ${output.replaceAll(/[\r\n]+/g, " ").slice(0, 180) || "(empty)"}`,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function runManagementCommand(
|
|
228
|
+
ctx: AccountContext,
|
|
229
|
+
dependencies: AccountManagementDependencies,
|
|
230
|
+
args: string[],
|
|
231
|
+
): Promise<{ code: number; stdout: string; stderr: string } | undefined> {
|
|
232
|
+
try {
|
|
233
|
+
return await dependencies.run(args);
|
|
234
|
+
} catch {
|
|
235
|
+
ctx.ui.notify(
|
|
236
|
+
"Account operation failed before it could complete.",
|
|
237
|
+
"warning",
|
|
238
|
+
);
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function markedProfiles(
|
|
244
|
+
profiles: string[],
|
|
245
|
+
activeProfile: string | undefined,
|
|
246
|
+
defaultProfile: string | undefined,
|
|
247
|
+
): string[] {
|
|
248
|
+
return profiles.map(
|
|
249
|
+
(profile) =>
|
|
250
|
+
`${profile}${profile === activeProfile ? " (active)" : ""}${profile === defaultProfile ? " (default)" : ""}`,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function currentDefault(
|
|
255
|
+
dependencies: AccountManagementDependencies,
|
|
256
|
+
): Promise<DefaultAccountState> {
|
|
257
|
+
let result: { code: number; stdout: string; stderr: string };
|
|
258
|
+
try {
|
|
259
|
+
result = await dependencies.run(["account", "default"]);
|
|
260
|
+
} catch {
|
|
261
|
+
return { status: "error", message: "account command could not be started" };
|
|
262
|
+
}
|
|
263
|
+
return parseDefaultAccountResult(result);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export async function manageAccountProfile(
|
|
267
|
+
ctx: AccountContext,
|
|
268
|
+
mode: "rename" | "remove",
|
|
269
|
+
dependencies: AccountManagementDependencies,
|
|
270
|
+
): Promise<void> {
|
|
271
|
+
let profiles: string[];
|
|
272
|
+
try {
|
|
273
|
+
profiles = await dependencies.profiles();
|
|
274
|
+
} catch {
|
|
275
|
+
ctx.ui.notify("Could not list account profiles.", "warning");
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (profiles.length === 0) {
|
|
279
|
+
ctx.ui.notify("No account profiles are available.", "warning");
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const selected = await ctx.ui.select(
|
|
283
|
+
mode === "rename"
|
|
284
|
+
? "Rename OpenAI account"
|
|
285
|
+
: "Remove OpenAI account permanently",
|
|
286
|
+
markedProfiles(profiles, dependencies.activeProfile, undefined),
|
|
287
|
+
);
|
|
288
|
+
if (selected === undefined) return;
|
|
289
|
+
const activeMarker = selected.indexOf(" (");
|
|
290
|
+
const target =
|
|
291
|
+
activeMarker === -1 ? selected : selected.slice(0, activeMarker);
|
|
292
|
+
if (!profiles.includes(target)) return;
|
|
293
|
+
if (target === dependencies.activeProfile) {
|
|
294
|
+
ctx.ui.notify(
|
|
295
|
+
"Cannot change the active profile. Switch to another profile first.",
|
|
296
|
+
"warning",
|
|
297
|
+
);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (mode === "rename") {
|
|
301
|
+
const newName = await ctx.ui.input("New profile name");
|
|
302
|
+
if (newName === undefined) return;
|
|
303
|
+
if (!validProfileName(newName)) {
|
|
304
|
+
ctx.ui.notify(
|
|
305
|
+
"Profile names use lowercase letters, numbers, and hyphens only.",
|
|
306
|
+
"warning",
|
|
307
|
+
);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const result = await runManagementCommand(ctx, dependencies, [
|
|
311
|
+
"account",
|
|
312
|
+
"rename",
|
|
313
|
+
target,
|
|
314
|
+
newName,
|
|
315
|
+
]);
|
|
316
|
+
if (result)
|
|
317
|
+
ctx.ui.notify(
|
|
318
|
+
result.code === 0
|
|
319
|
+
? `Account profile renamed to ${newName}.`
|
|
320
|
+
: `Rename failed: ${managementError(result)}`,
|
|
321
|
+
result.code === 0 ? "info" : "warning",
|
|
322
|
+
);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const defaultState = await currentDefault(dependencies);
|
|
326
|
+
if (defaultState.status === "invalid" || defaultState.status === "error") {
|
|
327
|
+
ctx.ui.notify(
|
|
328
|
+
"Cannot safely remove an account while default metadata is unavailable; clear or fix the default account first.",
|
|
329
|
+
"warning",
|
|
330
|
+
);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
let replacement: string | undefined;
|
|
334
|
+
if (defaultState.status === "valid" && defaultState.profile === target) {
|
|
335
|
+
replacement = await ctx.ui.select(
|
|
336
|
+
"Choose a new default account",
|
|
337
|
+
profiles.filter((profile) => profile !== target),
|
|
338
|
+
);
|
|
339
|
+
if (replacement === undefined) return;
|
|
340
|
+
}
|
|
341
|
+
const confirmation = await ctx.ui.input(
|
|
342
|
+
`Type ${target} to permanently remove it`,
|
|
343
|
+
);
|
|
344
|
+
if (confirmation === undefined) return;
|
|
345
|
+
if (confirmation !== target) {
|
|
346
|
+
ctx.ui.notify(
|
|
347
|
+
"Removal cancelled: confirmation did not match the profile name.",
|
|
348
|
+
"warning",
|
|
349
|
+
);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const args = ["account", "remove", target, "--confirm", target];
|
|
353
|
+
if (replacement !== undefined) args.push("--replacement", replacement);
|
|
354
|
+
const result = await runManagementCommand(ctx, dependencies, args);
|
|
355
|
+
if (result)
|
|
356
|
+
ctx.ui.notify(
|
|
357
|
+
result.code === 0
|
|
358
|
+
? `Account profile ${target} permanently removed.`
|
|
359
|
+
: `Removal failed: ${managementError(result)}`,
|
|
360
|
+
result.code === 0 ? "info" : "warning",
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export async function manageAccountProfiles(
|
|
365
|
+
ctx: AccountContext,
|
|
366
|
+
dependencies: AccountManagementDependencies,
|
|
367
|
+
): Promise<void> {
|
|
368
|
+
for (;;) {
|
|
369
|
+
let profiles: string[];
|
|
370
|
+
try {
|
|
371
|
+
profiles = await dependencies.profiles();
|
|
372
|
+
} catch {
|
|
373
|
+
ctx.ui.notify("Could not list account profiles.", "warning");
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const defaultState = await currentDefault(dependencies);
|
|
377
|
+
const defaultProfile =
|
|
378
|
+
defaultState.status === "valid" ? defaultState.profile : undefined;
|
|
379
|
+
const action = await ctx.ui.select("OpenAI account manager", [
|
|
380
|
+
"Switch",
|
|
381
|
+
"Add",
|
|
382
|
+
"Default",
|
|
383
|
+
"Rename",
|
|
384
|
+
"Remove",
|
|
385
|
+
"Account info",
|
|
386
|
+
"Cancel",
|
|
387
|
+
]);
|
|
388
|
+
if (action === undefined || action === "Cancel") return;
|
|
389
|
+
if (action === "Account info") {
|
|
390
|
+
const defaultNote =
|
|
391
|
+
defaultState.status === "invalid"
|
|
392
|
+
? "\nDefault account metadata is invalid; clear or fix the default account."
|
|
393
|
+
: defaultState.status === "error"
|
|
394
|
+
? "\nDefault account unavailable."
|
|
395
|
+
: "";
|
|
396
|
+
ctx.ui.notify(
|
|
397
|
+
`Accounts:\n${markedProfiles(profiles, dependencies.activeProfile, defaultProfile).join("\n") || "No account profiles."}${defaultNote}`,
|
|
398
|
+
"info",
|
|
399
|
+
);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (action === "Add") {
|
|
403
|
+
const name = await ctx.ui.input("New profile name");
|
|
404
|
+
if (name === undefined) continue;
|
|
405
|
+
if (!validProfileName(name)) {
|
|
406
|
+
ctx.ui.notify(
|
|
407
|
+
"Profile names use lowercase letters, numbers, and hyphens only.",
|
|
408
|
+
"warning",
|
|
409
|
+
);
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
const result = await runManagementCommand(ctx, dependencies, [
|
|
413
|
+
"account",
|
|
414
|
+
"create",
|
|
415
|
+
name,
|
|
416
|
+
]);
|
|
417
|
+
if (result)
|
|
418
|
+
ctx.ui.notify(
|
|
419
|
+
result.code === 0
|
|
420
|
+
? "Account profile created. Select it later via Switch, then run /login and select ChatGPT Plus/Pro (Codex)."
|
|
421
|
+
: `Create failed: ${managementError(result)}`,
|
|
422
|
+
result.code === 0 ? "info" : "warning",
|
|
423
|
+
);
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (action === "Default") {
|
|
427
|
+
const choice = await ctx.ui.select("Default account", [
|
|
428
|
+
"Show current",
|
|
429
|
+
"Set default",
|
|
430
|
+
"Clear default",
|
|
431
|
+
"Back",
|
|
432
|
+
]);
|
|
433
|
+
if (choice === "Set default") {
|
|
434
|
+
const profile = await ctx.ui.select("Choose default account", profiles);
|
|
435
|
+
if (profile !== undefined) {
|
|
436
|
+
const result = await runManagementCommand(ctx, dependencies, [
|
|
437
|
+
"account",
|
|
438
|
+
"default",
|
|
439
|
+
profile,
|
|
440
|
+
]);
|
|
441
|
+
if (result)
|
|
442
|
+
ctx.ui.notify(
|
|
443
|
+
result.code === 0
|
|
444
|
+
? `Default account set to ${profile}.`
|
|
445
|
+
: `Default failed: ${managementError(result)}`,
|
|
446
|
+
result.code === 0 ? "info" : "warning",
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
} else if (choice === "Clear default") {
|
|
450
|
+
const result = await runManagementCommand(ctx, dependencies, [
|
|
451
|
+
"account",
|
|
452
|
+
"default",
|
|
453
|
+
"--clear",
|
|
454
|
+
]);
|
|
455
|
+
if (result)
|
|
456
|
+
ctx.ui.notify(
|
|
457
|
+
result.code === 0
|
|
458
|
+
? "Default account cleared."
|
|
459
|
+
: `Default failed: ${managementError(result)}`,
|
|
460
|
+
result.code === 0 ? "info" : "warning",
|
|
461
|
+
);
|
|
462
|
+
} else if (choice === "Show current") {
|
|
463
|
+
if (defaultState.status === "valid")
|
|
464
|
+
ctx.ui.notify(`Default account: ${defaultState.profile}`, "info");
|
|
465
|
+
else if (defaultState.status === "unset")
|
|
466
|
+
ctx.ui.notify("No default account.", "info");
|
|
467
|
+
else if (defaultState.status === "invalid")
|
|
468
|
+
ctx.ui.notify(
|
|
469
|
+
"Default account metadata is invalid; no account selected.",
|
|
470
|
+
"warning",
|
|
471
|
+
);
|
|
472
|
+
else
|
|
473
|
+
ctx.ui.notify(
|
|
474
|
+
"Default account is unavailable; clear or fix the default account before continuing.",
|
|
475
|
+
"warning",
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
if (action === "Rename" || action === "Remove") {
|
|
481
|
+
await manageAccountProfile(
|
|
482
|
+
ctx,
|
|
483
|
+
action.toLowerCase() as "rename" | "remove",
|
|
484
|
+
dependencies,
|
|
485
|
+
);
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
if (profiles.length === 0) {
|
|
489
|
+
ctx.ui.notify("No account profiles are available.", "warning");
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
const target = await ctx.ui.select(
|
|
493
|
+
"Switch OpenAI account",
|
|
494
|
+
markedProfiles(
|
|
495
|
+
profiles,
|
|
496
|
+
dependencies.activeProfile,
|
|
497
|
+
defaultProfile ?? undefined,
|
|
498
|
+
),
|
|
499
|
+
);
|
|
500
|
+
if (target === undefined) continue;
|
|
501
|
+
const marker = target.indexOf(" (");
|
|
502
|
+
const profile = marker === -1 ? target : target.slice(0, marker);
|
|
503
|
+
if (!profiles.includes(profile)) continue;
|
|
504
|
+
if (profile === dependencies.activeProfile) {
|
|
505
|
+
ctx.ui.notify("That account is already active.", "info");
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
if (dependencies.handoff) await dependencies.handoff(profile);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function runBundledCommand(
|
|
514
|
+
args: string[],
|
|
515
|
+
): Promise<{ code: number; stdout: string; stderr: string }> {
|
|
516
|
+
return new Promise((resolveResult, rejectResult) => {
|
|
517
|
+
const child = spawnChild(process.execPath, [bundledLauncherPath(), ...args], {
|
|
518
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
519
|
+
});
|
|
520
|
+
let stdout = "";
|
|
521
|
+
let stderr = "";
|
|
522
|
+
const append = (current: string, chunk: Buffer): string =>
|
|
523
|
+
(current + chunk.toString("utf8")).slice(-1024);
|
|
524
|
+
child.stdout?.on("data", (chunk: Buffer) => {
|
|
525
|
+
stdout = append(stdout, chunk);
|
|
526
|
+
});
|
|
527
|
+
child.stderr?.on("data", (chunk: Buffer) => {
|
|
528
|
+
stderr = append(stderr, chunk);
|
|
529
|
+
});
|
|
530
|
+
child.once("error", rejectResult);
|
|
531
|
+
child.once("exit", (code) =>
|
|
532
|
+
resolveResult({ code: code ?? 1, stdout, stderr }),
|
|
533
|
+
);
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export function registerAccountProfilesCommand(pi: {
|
|
538
|
+
registerCommand(
|
|
539
|
+
name: string,
|
|
540
|
+
options: {
|
|
541
|
+
description: string;
|
|
542
|
+
handler: (args: string, ctx: ExtensionCommandContext) => Promise<void>;
|
|
543
|
+
},
|
|
544
|
+
): void;
|
|
545
|
+
}): void {
|
|
546
|
+
pi.registerCommand("osdy-account", {
|
|
547
|
+
description: "Restart Pi with another isolated OpenAI account profile.",
|
|
548
|
+
handler: async (args, ctx) => {
|
|
549
|
+
const mode = args.trim();
|
|
550
|
+
if ((ctx as ExtensionCommandContext & { hasUI?: boolean }).hasUI === false) {
|
|
551
|
+
ctx.ui.notify("Account manager requires an interactive Pi UI.", "warning");
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
const dependencies: AccountManagementDependencies = {
|
|
555
|
+
profiles: () => availableProfiles(sharedAgentDir(process.env)),
|
|
556
|
+
run: runBundledCommand,
|
|
557
|
+
activeProfile: process.env.OSDY_PI_PROFILE_NAME,
|
|
558
|
+
handoff: (profile) =>
|
|
559
|
+
handoffToAccount(ctx, profile, {
|
|
560
|
+
launcher: bundledLauncherPath(),
|
|
561
|
+
spawn: (command, launchArgs) =>
|
|
562
|
+
waitForLauncherReady(
|
|
563
|
+
spawnChild(process.execPath, [command, ...launchArgs], {
|
|
564
|
+
detached: true,
|
|
565
|
+
stdio: ["ignore", "ignore", "ignore", "ipc"],
|
|
566
|
+
}),
|
|
567
|
+
),
|
|
568
|
+
}),
|
|
569
|
+
};
|
|
570
|
+
if (mode === "rename" || mode === "remove") {
|
|
571
|
+
await manageAccountProfile(ctx, mode, dependencies);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
if (mode) {
|
|
575
|
+
ctx.ui.notify("Usage: /osdy-account [rename|remove]", "warning");
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
await manageAccountProfiles(ctx, dependencies);
|
|
579
|
+
},
|
|
580
|
+
});
|
|
581
|
+
}
|
|
@@ -36,6 +36,33 @@ function getAnimatedAsciiColor(
|
|
|
36
36
|
return baseColor;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
const RAW_HEX_COLOR = /^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/;
|
|
40
|
+
|
|
41
|
+
const GLOW_HIGHLIGHT_TONES = {
|
|
42
|
+
p: "c",
|
|
43
|
+
c: "v",
|
|
44
|
+
v: "p",
|
|
45
|
+
} as const;
|
|
46
|
+
|
|
47
|
+
const GLOW_TRAIL_TONES = {
|
|
48
|
+
p: "v",
|
|
49
|
+
c: "p",
|
|
50
|
+
v: "c",
|
|
51
|
+
} as const;
|
|
52
|
+
|
|
53
|
+
function colorAsciiCharacter(
|
|
54
|
+
theme: SimpleTheme,
|
|
55
|
+
color: string,
|
|
56
|
+
character: string,
|
|
57
|
+
): string {
|
|
58
|
+
const hexMatch = RAW_HEX_COLOR.exec(color);
|
|
59
|
+
if (!hexMatch) return theme.fg(color, character);
|
|
60
|
+
const red = Number.parseInt(hexMatch[1] ?? "", 16);
|
|
61
|
+
const green = Number.parseInt(hexMatch[2] ?? "", 16);
|
|
62
|
+
const blue = Number.parseInt(hexMatch[3] ?? "", 16);
|
|
63
|
+
return `\u001B[38;2;${red};${green};${blue}m${character}\u001B[39m`;
|
|
64
|
+
}
|
|
65
|
+
|
|
39
66
|
export function animateAsciiLine(
|
|
40
67
|
line: string,
|
|
41
68
|
lineIndex: number,
|
|
@@ -68,10 +95,11 @@ export function animateAsciiLineWithToneMap(
|
|
|
68
95
|
palette: MascotTonePalette,
|
|
69
96
|
style: AsciiAnimationStyle,
|
|
70
97
|
): string {
|
|
98
|
+
const toneCharacters = Array.from(toneMap);
|
|
71
99
|
return Array.from(line)
|
|
72
100
|
.map((char, charIndex) => {
|
|
73
101
|
if (char === " ") return char;
|
|
74
|
-
const tone =
|
|
102
|
+
const tone = toneCharacters[charIndex];
|
|
75
103
|
if (
|
|
76
104
|
tone !== "b" &&
|
|
77
105
|
tone !== "h" &&
|
|
@@ -85,12 +113,26 @@ export function animateAsciiLineWithToneMap(
|
|
|
85
113
|
return char;
|
|
86
114
|
}
|
|
87
115
|
const baseColor = palette[tone];
|
|
88
|
-
if (style === "static")
|
|
116
|
+
if (style === "static") {
|
|
117
|
+
return colorAsciiCharacter(theme, baseColor, char);
|
|
118
|
+
}
|
|
89
119
|
const wave = positiveModulo(charIndex + lineIndex * 2 - frame * 5, 44);
|
|
90
|
-
|
|
91
|
-
|
|
120
|
+
if (tone === "p" || tone === "c" || tone === "v") {
|
|
121
|
+
return colorAsciiCharacter(
|
|
122
|
+
theme,
|
|
123
|
+
getAnimatedAsciiColor(
|
|
124
|
+
wave,
|
|
125
|
+
baseColor,
|
|
126
|
+
palette[GLOW_HIGHLIGHT_TONES[tone]],
|
|
127
|
+
palette[GLOW_TRAIL_TONES[tone]],
|
|
128
|
+
),
|
|
129
|
+
char,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
const highlightColor = tone === "b" || tone === "h" ? palette.h : palette.l;
|
|
92
133
|
const trailColor = tone === "d" ? palette.m : palette.b;
|
|
93
|
-
return
|
|
134
|
+
return colorAsciiCharacter(
|
|
135
|
+
theme,
|
|
94
136
|
getAnimatedAsciiColor(wave, baseColor, highlightColor, trailColor),
|
|
95
137
|
char,
|
|
96
138
|
);
|