| 1 | import { |
| 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"; |
| 20 | import * as Console from "effect/Console"; |
| 21 | import * as Effect from "effect/Effect"; |
| 22 | import * as Layer from "effect/Layer"; |
| 23 | import * as Option from "effect/Option"; |
| 24 | import * as References from "effect/References"; |
| 25 | import * as Schema from "effect/Schema"; |
| 26 | import { Argument, Command, Flag, GlobalFlag } from "effect/cli"; |
| 27 | import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http"; |
| 28 | import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts"; |
| 29 | import * as ServerConfig from "../config.ts"; |
| 30 | import { readPersistedServerRuntimeState } from "../serverRuntimeState.ts"; |
| 31 | import { authLocationFlags, resolveCliAuthConfig, type CliAuthLocationFlags } from "./config.ts"; |
| 32 | |
| 33 | const encodeStatus = Schema.encodeEffect( |
| 34 | Schema.fromJsonString(Schema.Union([SharedProfilesStatus, ArchiveStatus, HubDirectory])), |
| 35 | ); |
| 36 | |
| 37 | const 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 | |
| 94 | const archiveStatus = Command.make("status", authLocationFlags).pipe( |
| 95 | Command.withHandler((flags) => run(flags, { type: "archive.status" })), |
| 96 | ); |
| 97 | const 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 | ); |
| 109 | const archive = Command.make("archive").pipe( |
| 110 | Command.withSubcommands([archiveStatus, archiveRetry]), |
| 111 | ); |
| 112 | const status = Command.make("status", authLocationFlags).pipe( |
| 113 | Command.withDescription("List shared accounts on this host."), |
| 114 | Command.withHandler((flags) => run(flags, { type: "status" })), |
| 115 | ); |
| 116 | const 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 | ); |
| 120 | const 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 | ); |
| 132 | const 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 | ); |
| 136 | const 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 | ); |
| 159 | const 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 | |
| 167 | const 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 | |
| 208 | export 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 | ); |