1import * as Console from "effect/Console";
2import * as Effect from "effect/Effect";
3import * as Option from "effect/Option";
4import * as Redacted from "effect/Redacted";
5import * as Profiles from "../../../Alchemist/routes/profile.ts";
6import * as CliKit from "../../../Cli/CliKit/index.ts";
7import { confirmOrDecline } from "../confirm.ts";
8import { isPromptCancellation } from "../errors.ts";
9
10export const profileTui = Effect.promise(
11 () => import("../../../Cli/components/view/Profile.tsx"),
12);
13
14export type EditAction = "add" | "reconfigure" | "remove";
15export interface EditOutcome {
16 readonly provider: string;
17 readonly action: EditAction;
18 readonly outcome: "done" | "skipped";
19}
20
21export const showProfileFlow = Effect.fn(function* (options: {
22 profileName: string;
23 activeProfile: string;
24 envFile: Option.Option<string>;
25 main: string;
26}) {
27 const profile = yield* Profiles.get({
28 name: options.profileName,
29 includeProviderStatus: true,
30 entrypoint: options.main,
31 envFile: Option.getOrUndefined(options.envFile),
32 });
33 yield* Console.log(
34 [
35 `${profile.active ? "*" : "-"} ${profile.name}`,
36 ...profile.providers.map((provider) =>
37 [
38 ` ${provider.name} (${provider.method}) — ${provider.status}`,
39 ...provider.details.map(({ key, value }) => ` ${key}: ${value}`),
40 ...(provider.diagnostic
41 ? [` ${provider.diagnostic.message}`]
42 : []),
43 ].join("\n"),
44 ),
45 ].join("\n"),
46 );
47});
48
49export const renameProfileFlow = Effect.fn(function* (
50 name: string,
51 suppliedNewName: string | undefined,
52) {
53 const newName = (
54 suppliedNewName ??
55 (yield* CliKit.accessors.prompt.text({
56 message: `Rename profile '${name}' to`,
57 placeholder: `${name}-new`,
58 validate: (value) =>
59 value.trim().length > 0 ? undefined : "Profile name is required",
60 }))
61 ).trim();
62 yield* Profiles.rename({ name, newName });
63 yield* CliKit.accessors.output.success(
64 `Renamed profile '${name}' to '${newName}'.`,
65 );
66 return newName;
67});
68
69export const deleteProfileFlow = Effect.fn(function* (options: {
70 name: string;
71 envFile: Option.Option<string>;
72 main: string;
73 yes: boolean;
74}) {
75 // Existence check only — skip the provider-status probe (it imports the
76 // stack entrypoint and reaches provider APIs).
77 const exists = yield* Profiles.get({
78 name: options.name,
79 includeProviderStatus: false,
80 }).pipe(Effect.option);
81 if (Option.isNone(exists)) {
82 yield* Console.log(
83 `Profile ${options.name}: Not found. Nothing was deleted.`,
84 );
85 return false;
86 }
87 // Declining short-circuits with exit code 1; the boolean only reports the
88 // not-found early return above to the caller.
89 yield* confirmOrDecline({
90 yes: options.yes,
91 message: `Delete profile '${options.name}' and all its stored credentials? This cannot be undone.`,
92 confirmLabel: "Delete",
93 cancelLabel: "Cancel",
94 });
95 yield* Profiles.deleteProfile({ name: options.name });
96 yield* CliKit.accessors.output.success(
97 `Deleted profile '${options.name}' and its credentials.`,
98 );
99 return true;
100});
101
102export const editProfileFlow = Effect.fn(function* (options: {
103 selectedProfile: string;
104 add: ReadonlyArray<string>;
105 reconfigure: ReadonlyArray<string>;
106 remove: ReadonlyArray<string>;
107 envFile: Option.Option<string>;
108 main: string;
109 configureInput?: {
110 method?: string;
111 values: Record<string, string>;
112 };
113}) {
114 let plan: Array<{ provider: string; action: EditAction }> = [
115 ...options.add.map((provider) => ({ provider, action: "add" as const })),
116 ...options.reconfigure.map((provider) => ({
117 provider,
118 action: "reconfigure" as const,
119 })),
120 ...options.remove.map((provider) => ({
121 provider,
122 action: "remove" as const,
123 })),
124 ];
125
126 if (plan.length === 0) {
127 // Only connected names/methods are needed for the cycle prompt; the
128 // status probe would import the entrypoint and reach provider APIs.
129 const profile = yield* Profiles.get({
130 name: options.selectedProfile,
131 includeProviderStatus: false,
132 });
133 const available = yield* Profiles.providers({
134 profile: options.selectedProfile,
135 entrypoint: options.main,
136 envFile: Option.getOrUndefined(options.envFile),
137 });
138 const connected = new Map(
139 profile.providers.map((item) => [item.name, item]),
140 );
141 const names = [
142 ...new Set([...connected.keys(), ...available.map(({ name }) => name)]),
143 ].sort();
144 const prompt = yield* CliKit.CliKit;
145 const { editStateStyle } = yield* profileTui;
146 const glyphs = CliKit.glyphsFor(prompt.terminal.unicode);
147 const state = (
148 key: keyof typeof editStateStyle,
149 value: { provider: string; action: EditAction } | null,
150 ) => ({
151 value,
152 icon: glyphs[editStateStyle[key].icon],
153 label: editStateStyle[key].label,
154 variant: editStateStyle[key].variant,
155 });
156 const choices = names.map((provider) =>
157 connected.has(provider)
158 ? {
159 label: provider,
160 description: connected.get(provider)!.method,
161 states: [
162 state("keep", null),
163 state("reconfigure", { provider, action: "reconfigure" }),
164 state("remove", { provider, action: "remove" }),
165 ],
166 }
167 : {
168 label: provider,
169 states: [
170 state("skip", null),
171 state("add", { provider, action: "add" }),
172 ],
173 },
174 );
175 plan = (yield* prompt.prompt.cycle({
176 message: `Manage accounts in profile '${options.selectedProfile}'`,
177 options: choices,
178 requireChange: true,
179 })).filter((item) => item !== null);
180 }
181
182 const outcomes: EditOutcome[] = [];
183 for (const step of plan) {
184 const run =
185 step.action === "remove"
186 ? Profiles.removeProvider({
187 profile: options.selectedProfile,
188 provider: step.provider,
189 entrypoint: options.main,
190 envFile: Option.getOrUndefined(options.envFile),
191 })
192 : Profiles.configure({
193 profile: options.selectedProfile,
194 provider: step.provider,
195 entrypoint: options.main,
196 envFile: Option.getOrUndefined(options.envFile),
197 action: step.action,
198 method: options.configureInput?.method,
199 values:
200 options.configureInput === undefined
201 ? undefined
202 : Object.fromEntries(
203 Object.entries(options.configureInput.values).map(
204 ([key, value]) => [key, Redacted.make(value)],
205 ),
206 ),
207 }).pipe(Effect.asVoid);
208 const result = yield* Effect.result(run);
209 if (result._tag === "Success") {
210 outcomes.push({ ...step, outcome: "done" });
211 } else if (isPromptCancellation(result.failure)) {
212 outcomes.push({ ...step, outcome: "skipped" });
213 } else {
214 return yield* Effect.fail(result.failure);
215 }
216 }
217 return outcomes;
218});