1import {
2 AuthProvidersManageScope,
3 AuthAccessWriteScope,
4 EnvironmentId,
5 HubDirectory,
6 HubError,
7 type HubManagementRequest,
8 AuthOrchestrationReadScope,
9 AuthOrchestrationOperateScope,
10 ArchiveStatus,
11 ArchiveError,
12 type ArchiveManagementRequest,
13 ThreadId,
14 ProviderInstanceId,
15 SharedAccountError,
16 SharedAccountId,
17 SharedProfilesStatus,
18 type SharedProfilesRequest,
19} from "@t3tools/contracts";
20import * as Console from "effect/Console";
21import * as Effect from "effect/Effect";
22import * as Layer from "effect/Layer";
23import * as Option from "effect/Option";
24import * as References from "effect/References";
25import * as Schema from "effect/Schema";
26import { Argument, Command, Flag, GlobalFlag } from "effect/cli";
27import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http";
28import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts";
29import * as ServerConfig from "../config.ts";
30import { readPersistedServerRuntimeState } from "../serverRuntimeState.ts";
31import { authLocationFlags, resolveCliAuthConfig, type CliAuthLocationFlags } from "./config.ts";
32
33const encodeStatus = Schema.encodeEffect(
34 Schema.fromJsonString(Schema.Union([SharedProfilesStatus, ArchiveStatus, HubDirectory])),
35);
36
37const run = (
38 flags: CliAuthLocationFlags,
39 request: SharedProfilesRequest | ArchiveManagementRequest | HubManagementRequest,
40) =>
41 Effect.gen(function* () {
42 const config = yield* resolveCliAuthConfig(flags, yield* GlobalFlag.LogLevel);
43 const state = yield* readPersistedServerRuntimeState(config.serverRuntimeStatePath);
44 if (Option.isNone(state)) return yield* new SharedAccountError({ reason: "connection" });
45 return yield* Effect.gen(function* () {
46 const auth = yield* EnvironmentAuth.EnvironmentAuth;
47 const http = yield* HttpClient.HttpClient;
48 const [endpoint, scopes] = request.type.startsWith("hub.")
49 ? (["/api/hub/manage", [AuthAccessWriteScope]] as const)
50 : request.type.startsWith("archive.")
51 ? ([
52 "/api/archive/manage",
53 [AuthOrchestrationReadScope, AuthOrchestrationOperateScope],
54 ] as const)
55 : (["/api/shared/profiles", [AuthProvidersManageScope]] as const);
56 const result = yield* Effect.acquireUseRelease(
57 auth.issueSession({
58 scopes,
59 label: "t3 shared cli",
60 }),
61 (session) =>
62 HttpClientRequest.post(`${state.value.origin}${endpoint}`).pipe(
63 HttpClientRequest.setHeader("authorization", `Bearer ${session.token}`),
64 HttpClientRequest.bodyJson(request),
65 Effect.flatMap(http.execute),
66 Effect.flatMap((response) =>
67 response.status === 200
68 ? HttpClientResponse.schemaBodyJson(
69 Schema.Union([SharedProfilesStatus, ArchiveStatus, HubDirectory]),
70 )(response)
71 : HttpClientResponse.schemaBodyJson(
72 Schema.Union([SharedAccountError, ArchiveError, HubError]),
73 )(response).pipe(Effect.flatMap(Effect.fail)),
74 ),
75 Effect.timeout("30 seconds"),
76 ),
77 (session) => auth.revokeSession(session.sessionId).pipe(Effect.ignore({ log: true })),
78 );
79 yield* Console.log(yield* encodeStatus(result));
80 }).pipe(
81 Effect.scoped,
82 Effect.provide(
83 Layer.mergeAll(
84 EnvironmentAuth.layerRuntime.pipe(
85 Layer.provide(ServerConfig.layer(config)),
86 Layer.provide(Layer.succeed(References.MinimumLogLevel, "Error")),
87 ),
88 FetchHttpClient.layer,
89 ),
90 ),
91 );
92 });
93
94const archiveStatus = Command.make("status", authLocationFlags).pipe(
95 Command.withHandler((flags) => run(flags, { type: "archive.status" })),
96);
97const archiveRetry = Command.make("retry", {
98 ...authLocationFlags,
99 threadId: Argument.String("thread-id").pipe(Argument.withSchema(ThreadId), Argument.optional),
100}).pipe(
101 Command.withDescription("Retry transcript uploads that stopped after failures."),
102 Command.withHandler((flags) =>
103 run(flags, {
104 type: "archive.retry",
105 ...(Option.isSome(flags.threadId) ? { threadId: flags.threadId.value } : {}),
106 }),
107 ),
108);
109const archive = Command.make("archive").pipe(
110 Command.withSubcommands([archiveStatus, archiveRetry]),
111);
112const status = Command.make("status", authLocationFlags).pipe(
113 Command.withDescription("List shared accounts on this host."),
114 Command.withHandler((flags) => run(flags, { type: "status" })),
115);
116const sync = Command.make("sync", authLocationFlags).pipe(
117 Command.withDescription("Refresh shared profiles from the shared server."),
118 Command.withHandler((flags) => run(flags, { type: "sync" })),
119);
120const connect = Command.make("connect", {
121 ...authLocationFlags,
122 url: Argument.String("url"),
123 tokenFile: Flag.String("token-file").pipe(
124 Flag.withDescription("Private file containing this host’s shared-server bearer token."),
125 ),
126}).pipe(
127 Command.withDescription("Connect this host to a private T3 server."),
128 Command.withHandler((flags) =>
129 run(flags, { type: "connect", url: flags.url, tokenFile: flags.tokenFile }),
130 ),
131);
132const disconnect = Command.make("disconnect", authLocationFlags).pipe(
133 Command.withDescription("Disconnect and disable this host’s shared profiles."),
134 Command.withHandler((flags) => run(flags, { type: "disconnect" })),
135);
136const importAccount = Command.make("import", {
137 ...authLocationFlags,
138 instanceId: Argument.String("instance-id").pipe(Argument.withSchema(ProviderInstanceId)),
139 tokenFile: Flag.String("token-file").pipe(
140 Flag.withDescription("Private file containing a Claude setup token."),
141 Flag.optional,
142 ),
143 replaceId: Flag.String("replace").pipe(
144 Flag.withSchema(SharedAccountId),
145 Flag.withDescription("Replace the credentials of an existing shared account."),
146 Flag.optional,
147 ),
148}).pipe(
149 Command.withDescription("Copy a signed-in profile into managed shared accounts."),
150 Command.withHandler((flags) =>
151 run(flags, {
152 type: "import",
153 instanceId: flags.instanceId,
154 ...(Option.isSome(flags.tokenFile) ? { tokenFile: flags.tokenFile.value } : {}),
155 ...(Option.isSome(flags.replaceId) ? { replaceId: flags.replaceId.value } : {}),
156 }),
157 ),
158);
159const remove = Command.make("remove", {
160 ...authLocationFlags,
161 id: Argument.String("account-id").pipe(Argument.withSchema(SharedAccountId)),
162}).pipe(
163 Command.withDescription("Remove a shared account from every connected host."),
164 Command.withHandler((flags) => run(flags, { type: "remove", id: flags.id })),
165);
166
167const hub = Command.make("hub").pipe(
168 Command.withDescription("Manage machines reached through this server."),
169 Command.withSubcommands([
170 Command.make("configure", {
171 ...authLocationFlags,
172 publicUrl: Argument.String("public-url"),
173 }).pipe(
174 Command.withHandler((flags) =>
175 run(flags, { type: "hub.configure", publicUrl: flags.publicUrl }),
176 ),
177 ),
178 Command.make("status", authLocationFlags).pipe(
179 Command.withHandler((flags) => run(flags, { type: "hub.status" })),
180 ),
181 Command.make("enroll", {
182 ...authLocationFlags,
183 url: Argument.String("private-machine-url"),
184 environmentId: Argument.String("environment-id").pipe(Argument.withSchema(EnvironmentId)),
185 tokenFile: Flag.String("token-file"),
186 }).pipe(
187 Command.withDescription("Enroll a machine using its private administrative bearer grant."),
188 Command.withHandler((flags) =>
189 run(flags, {
190 type: "hub.enroll",
191 url: flags.url,
192 environmentId: flags.environmentId,
193 tokenFile: flags.tokenFile,
194 }),
195 ),
196 ),
197 Command.make("remove", {
198 ...authLocationFlags,
199 environmentId: Argument.String("environment-id").pipe(Argument.withSchema(EnvironmentId)),
200 }).pipe(
201 Command.withHandler((flags) =>
202 run(flags, { type: "hub.remove", environmentId: flags.environmentId }),
203 ),
204 ),
205 ]),
206);
207
208export const sharedCommand = Command.make("shared").pipe(
209 Command.withDescription("Manage shared provider accounts."),
210 Command.withSubcommands([status, connect, disconnect, importAccount, sync, remove, archive, hub]),
211);