t3code-cli 0.1.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.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +70 -0
  3. package/dist/bin.js +17842 -0
  4. package/dist/index.js +2 -0
  5. package/dist/runtime-DU_hs0MM.js +61631 -0
  6. package/package.json +59 -0
  7. package/src/application/error.ts +4 -0
  8. package/src/application/layer.ts +20 -0
  9. package/src/application/model-selection.ts +109 -0
  10. package/src/application/models.ts +26 -0
  11. package/src/application/project-commands.ts +28 -0
  12. package/src/application/projects.ts +50 -0
  13. package/src/application/service.ts +93 -0
  14. package/src/application/shell-sequence.ts +29 -0
  15. package/src/application/thread-commands.ts +89 -0
  16. package/src/application/thread-wait.ts +86 -0
  17. package/src/application/threads.ts +172 -0
  18. package/src/auth/error.ts +42 -0
  19. package/src/auth/layer.ts +114 -0
  20. package/src/auth/local.ts +241 -0
  21. package/src/auth/pairing.ts +54 -0
  22. package/src/auth/schema.ts +55 -0
  23. package/src/auth/service.ts +16 -0
  24. package/src/auth/transport.ts +132 -0
  25. package/src/auth/type.ts +31 -0
  26. package/src/bin.ts +47 -0
  27. package/src/cli/app.ts +18 -0
  28. package/src/cli/auth-format.ts +43 -0
  29. package/src/cli/auth.ts +99 -0
  30. package/src/cli/error.ts +16 -0
  31. package/src/cli/input/error.ts +11 -0
  32. package/src/cli/input/layer.ts +31 -0
  33. package/src/cli/input/service.ts +11 -0
  34. package/src/cli/message-input.ts +25 -0
  35. package/src/cli/model-format.ts +18 -0
  36. package/src/cli/model-options.ts +56 -0
  37. package/src/cli/models.ts +45 -0
  38. package/src/cli/output/error.ts +11 -0
  39. package/src/cli/output/layer.ts +53 -0
  40. package/src/cli/output/service.ts +15 -0
  41. package/src/cli/output-format.ts +41 -0
  42. package/src/cli/project-format.ts +11 -0
  43. package/src/cli/projects.ts +62 -0
  44. package/src/cli/thread-format.ts +74 -0
  45. package/src/cli/threads/archive.ts +28 -0
  46. package/src/cli/threads/list.ts +29 -0
  47. package/src/cli/threads/messages.ts +36 -0
  48. package/src/cli/threads/send.ts +91 -0
  49. package/src/cli/threads/start.ts +136 -0
  50. package/src/cli/threads/wait.ts +35 -0
  51. package/src/cli/threads.ts +22 -0
  52. package/src/cli/wait-events.ts +112 -0
  53. package/src/config/error.ts +21 -0
  54. package/src/config/layer.ts +103 -0
  55. package/src/config/service.ts +24 -0
  56. package/src/config/url.ts +55 -0
  57. package/src/domain/command-schema.ts +82 -0
  58. package/src/domain/error.ts +46 -0
  59. package/src/domain/helpers.ts +23 -0
  60. package/src/domain/model-config.ts +40 -0
  61. package/src/domain/schema.ts +162 -0
  62. package/src/domain/thread-lifecycle.ts +97 -0
  63. package/src/environment/layer.ts +12 -0
  64. package/src/environment/service.ts +13 -0
  65. package/src/index.ts +18 -0
  66. package/src/orchestration/layer.ts +193 -0
  67. package/src/orchestration/service.ts +39 -0
  68. package/src/protocol/schema.ts +105 -0
  69. package/src/rpc/error.ts +31 -0
  70. package/src/rpc/layer.ts +99 -0
  71. package/src/rpc/service.ts +16 -0
  72. package/src/runtime.ts +28 -0
  73. package/src/version/layer.ts +25 -0
  74. package/src/version/service.ts +8 -0
@@ -0,0 +1,172 @@
1
+ import * as Crypto from "effect/Crypto";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Path from "effect/Path";
4
+
5
+ import { Environment } from "../environment/service.ts";
6
+ import { T3Orchestration } from "../orchestration/service.ts";
7
+ import { ThreadSessionError } from "../domain/error.ts";
8
+ import { resolveProject } from "../domain/helpers.ts";
9
+ import { type StartThreadInput } from "./service.ts";
10
+ import type { SendThreadInput } from "./service.ts";
11
+ import { mergeModelOptions } from "./model-selection.ts";
12
+ import {
13
+ makeThreadArchiveCommand,
14
+ makeThreadStartCommands,
15
+ makeThreadTurnContinueCommand,
16
+ } from "./thread-commands.ts";
17
+ import {
18
+ waitForThread as waitForThreadUntilComplete,
19
+ watchThread as watchThreadEvents,
20
+ } from "./thread-wait.ts";
21
+ import { waitForShellSequence } from "./shell-sequence.ts";
22
+
23
+ export const makeThreadApplication = Effect.fn("makeThreadApplication")(function* () {
24
+ const orchestration = yield* T3Orchestration;
25
+ const crypto = yield* Crypto.Crypto;
26
+ const path = yield* Path.Path;
27
+ const environment = yield* Environment;
28
+ const listThreads = Effect.fn("T3ApplicationLive.listThreads")(function* (projectRef: string) {
29
+ const snapshot = yield* orchestration.getShellSnapshot();
30
+ const project = resolveProject(snapshot, projectRef, path, environment.cwd);
31
+ return {
32
+ project,
33
+ threads: snapshot.threads.filter((thread) => thread.projectId === project.id),
34
+ };
35
+ });
36
+ const getThreadMessages = Effect.fn("T3ApplicationLive.getThreadMessages")(function* (
37
+ threadId: string,
38
+ ) {
39
+ return yield* orchestration.getThreadSnapshot(threadId);
40
+ });
41
+ const archiveThread = Effect.fn("T3ApplicationLive.archiveThread")(function* (threadId: string) {
42
+ const command = yield* makeThreadArchiveCommand(threadId).pipe(
43
+ Effect.provideService(Crypto.Crypto, crypto),
44
+ );
45
+ return yield* orchestration.dispatch(command);
46
+ });
47
+ const startThread = Effect.fn("T3ApplicationLive.startThread")(function* (
48
+ startInput: StartThreadInput,
49
+ policy?: {
50
+ readonly until: "dispatch" | "visible" | "complete";
51
+ },
52
+ ) {
53
+ const snapshot = yield* orchestration.getShellSnapshot();
54
+ const project = resolveProject(snapshot, startInput.projectRef, path, environment.cwd);
55
+ const serverConfig = yield* orchestration.getServerConfig();
56
+ const commands = yield* makeThreadStartCommands({
57
+ start: startInput,
58
+ project,
59
+ serverConfig,
60
+ }).pipe(Effect.provideService(Crypto.Crypto, crypto));
61
+ const createDispatch = yield* orchestration.dispatch(commands.createCommand);
62
+ yield* waitForShellSequence({
63
+ orchestration,
64
+ sequence: createDispatch.sequence,
65
+ });
66
+ const dispatch = yield* orchestration.dispatch(commands.turnCommand);
67
+ const threadId = commands.threadId;
68
+ const until = policy?.until ?? "dispatch";
69
+ if (until === "dispatch") {
70
+ return { dispatch, project, threadId };
71
+ }
72
+ yield* waitForShellSequence({
73
+ orchestration,
74
+ sequence: dispatch.sequence,
75
+ });
76
+ if (until === "visible") {
77
+ const thread = yield* Effect.scoped(
78
+ Effect.gen(function* () {
79
+ const opened = yield* orchestration.openThread(threadId);
80
+ return opened.snapshot;
81
+ }),
82
+ );
83
+ return { dispatch, project, threadId, thread };
84
+ }
85
+ const thread = yield* waitForThreadUntilComplete({
86
+ orchestration,
87
+ threadId,
88
+ });
89
+ yield* failIfThreadError(thread);
90
+ return { dispatch, project, threadId, thread };
91
+ });
92
+ const sendThread = Effect.fn("T3ApplicationLive.sendThread")(function* (
93
+ input: SendThreadInput,
94
+ policy?: {
95
+ readonly until: "dispatch" | "visible" | "complete";
96
+ },
97
+ ) {
98
+ const modelSelection =
99
+ input.options !== undefined && input.options.length > 0
100
+ ? mergeModelOptions(
101
+ (yield* orchestration.getThreadSnapshot(input.threadId)).modelSelection,
102
+ input.options,
103
+ )
104
+ : undefined;
105
+ const command = yield* makeThreadTurnContinueCommand({
106
+ ...input,
107
+ ...(modelSelection !== undefined ? { modelSelection } : {}),
108
+ }).pipe(Effect.provideService(Crypto.Crypto, crypto));
109
+ const dispatch = yield* orchestration.dispatch(command);
110
+ const until = policy?.until ?? "dispatch";
111
+ if (until === "dispatch") {
112
+ return { dispatch, threadId: input.threadId };
113
+ }
114
+ yield* waitForShellSequence({
115
+ orchestration,
116
+ sequence: dispatch.sequence,
117
+ });
118
+ if (until === "visible") {
119
+ const thread = yield* Effect.scoped(
120
+ Effect.gen(function* () {
121
+ const opened = yield* orchestration.openThread(input.threadId);
122
+ return opened.snapshot;
123
+ }),
124
+ );
125
+ return { dispatch, threadId: input.threadId, thread };
126
+ }
127
+ const thread = yield* waitForThreadUntilComplete({
128
+ orchestration,
129
+ threadId: input.threadId,
130
+ });
131
+ yield* failIfThreadError(thread);
132
+ return { dispatch, threadId: input.threadId, thread };
133
+ });
134
+ const watchThread = (threadId: string) =>
135
+ watchThreadEvents({
136
+ orchestration,
137
+ threadId,
138
+ });
139
+ const waitForThread = Effect.fn("T3ApplicationLive.waitForThread")(function* (threadId: string) {
140
+ const thread = yield* waitForThreadUntilComplete({
141
+ orchestration,
142
+ threadId,
143
+ });
144
+ yield* failIfThreadError(thread);
145
+ return thread;
146
+ });
147
+
148
+ return {
149
+ archiveThread,
150
+ listThreads,
151
+ getThreadMessages,
152
+ sendThread,
153
+ startThread,
154
+ watchThread,
155
+ waitForThread,
156
+ };
157
+ });
158
+
159
+ function failIfThreadError(thread: {
160
+ readonly id: string;
161
+ readonly session: { readonly status: string; readonly lastError: string | null } | null;
162
+ }) {
163
+ if (thread.session?.status !== "error") {
164
+ return Effect.void;
165
+ }
166
+ return Effect.fail(
167
+ new ThreadSessionError({
168
+ threadId: thread.id,
169
+ message: thread.session.lastError ?? "thread ended with error",
170
+ }),
171
+ );
172
+ }
@@ -0,0 +1,42 @@
1
+ import * as Cause from "effect/Cause";
2
+ import * as Schema from "effect/Schema";
3
+ import { PlatformError } from "effect/PlatformError";
4
+ import { HttpClientError } from "effect/unstable/http";
5
+
6
+ import { ConfigError, UrlError } from "../config/error.ts";
7
+
8
+ export class AuthPairingUrlError extends Schema.TaggedErrorClass<AuthPairingUrlError>()(
9
+ "AuthPairingUrlError",
10
+ {
11
+ message: Schema.String,
12
+ cause: Schema.optionalKey(Schema.instanceOf(Cause.IllegalArgumentError)),
13
+ },
14
+ ) {}
15
+
16
+ export class AuthConfigError extends Schema.TaggedErrorClass<AuthConfigError>()("AuthConfigError", {
17
+ message: Schema.String,
18
+ cause: Schema.optionalKey(Schema.Union([ConfigError, UrlError])),
19
+ }) {}
20
+
21
+ export class AuthTransportError extends Schema.TaggedErrorClass<AuthTransportError>()(
22
+ "AuthTransportError",
23
+ {
24
+ message: Schema.String,
25
+ cause: Schema.optionalKey(
26
+ Schema.Union([HttpClientError.HttpClientErrorSchema, Schema.instanceOf(Schema.SchemaError)]),
27
+ ),
28
+ },
29
+ ) {}
30
+
31
+ const AuthLocalErrorCauseSchema = Schema.Union([
32
+ Schema.instanceOf(PlatformError),
33
+ Schema.instanceOf(Schema.SchemaError),
34
+ UrlError,
35
+ ]);
36
+
37
+ export class AuthLocalError extends Schema.TaggedErrorClass<AuthLocalError>()("AuthLocalError", {
38
+ message: Schema.String,
39
+ cause: Schema.optionalKey(AuthLocalErrorCauseSchema),
40
+ }) {}
41
+
42
+ export type AuthError = AuthPairingUrlError | AuthConfigError | AuthTransportError | AuthLocalError;
@@ -0,0 +1,114 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as FileSystem from "effect/FileSystem";
3
+ import * as Layer from "effect/Layer";
4
+ import * as Path from "effect/Path";
5
+ import * as Scope from "effect/Scope";
6
+ import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner";
7
+
8
+ import { T3Config } from "../config/service.ts";
9
+ import { Environment } from "../environment/service.ts";
10
+ import { AuthConfigError } from "./error.ts";
11
+ import { issueLocalSession, resolveLocalOrigin } from "./local.ts";
12
+ import { parsePairingUrl } from "./pairing.ts";
13
+ import { T3Auth } from "./service.ts";
14
+ import { makeAuthTransport } from "./transport.ts";
15
+ import type { LocalAuthInput } from "./type.ts";
16
+
17
+ export const makeT3Auth = Effect.fn("makeT3Auth")(function* () {
18
+ const config = yield* T3Config;
19
+ const transport = yield* makeAuthTransport();
20
+ const spawner = yield* ChildProcessSpawner;
21
+ const fs = yield* FileSystem.FileSystem;
22
+ const path = yield* Path.Path;
23
+ const environment = yield* Environment;
24
+ const scope = yield* Scope.Scope;
25
+
26
+ const pair = Effect.fn("T3AuthLive.pair")(function* (pairingUrl: string) {
27
+ const parsed = yield* parsePairingUrl(pairingUrl);
28
+ const result = yield* transport.bootstrapBearer(parsed);
29
+ const existing = yield* config.readStored().pipe(
30
+ Effect.catchTags({
31
+ ConfigError: (error) =>
32
+ Effect.fail(new AuthConfigError({ message: "auth config failed", cause: error })),
33
+ }),
34
+ );
35
+ yield* config
36
+ .writeStored({ ...existing, url: parsed.baseUrl, token: result.sessionToken })
37
+ .pipe(
38
+ Effect.catchTags({
39
+ ConfigError: (error) =>
40
+ Effect.fail(new AuthConfigError({ message: "auth config failed", cause: error })),
41
+ }),
42
+ );
43
+ return { url: parsed.baseUrl, role: result.role, expiresAt: result.expiresAt };
44
+ });
45
+
46
+ const local = Effect.fn("T3AuthLive.local")(function* (input: LocalAuthInput) {
47
+ const issued = yield* issueLocalSession(input).pipe(
48
+ Effect.provideService(ChildProcessSpawner, spawner),
49
+ Effect.provideService(FileSystem.FileSystem, fs),
50
+ Effect.provideService(Path.Path, path),
51
+ Effect.provideService(Environment, environment),
52
+ Effect.provideService(Scope.Scope, scope),
53
+ );
54
+ const url = yield* resolveLocalOrigin({
55
+ baseDir: issued.baseDir,
56
+ ...(input.origin !== undefined ? { origin: input.origin } : {}),
57
+ }).pipe(
58
+ Effect.provideService(FileSystem.FileSystem, fs),
59
+ Effect.provideService(Path.Path, path),
60
+ );
61
+ const existing = yield* config.readStored().pipe(
62
+ Effect.catchTags({
63
+ ConfigError: (error) =>
64
+ Effect.fail(new AuthConfigError({ message: "auth config failed", cause: error })),
65
+ }),
66
+ );
67
+ yield* config.writeStored({ ...existing, url, token: issued.session.token }).pipe(
68
+ Effect.catchTags({
69
+ ConfigError: (error) =>
70
+ Effect.fail(new AuthConfigError({ message: "auth config failed", cause: error })),
71
+ }),
72
+ );
73
+ return {
74
+ url,
75
+ role: issued.session.role,
76
+ expiresAt: issued.session.expiresAt,
77
+ source: "local" as const,
78
+ baseDir: issued.baseDir,
79
+ };
80
+ });
81
+
82
+ const status = Effect.fn("T3AuthLive.status")(function* () {
83
+ const resolved = yield* config.resolve().pipe(
84
+ Effect.catchTags({
85
+ ConfigError: (error) =>
86
+ Effect.fail(new AuthConfigError({ message: "auth config failed", cause: error })),
87
+ UrlError: (error) =>
88
+ Effect.fail(new AuthConfigError({ message: "auth config failed", cause: error })),
89
+ }),
90
+ );
91
+ return yield* transport.getSession(resolved);
92
+ });
93
+
94
+ const issueWebSocketToken = Effect.fn("T3AuthLive.issueWebSocketToken")(function* () {
95
+ const resolved = yield* config.resolve().pipe(
96
+ Effect.catchTags({
97
+ ConfigError: (error) =>
98
+ Effect.fail(new AuthConfigError({ message: "auth config failed", cause: error })),
99
+ UrlError: (error) =>
100
+ Effect.fail(new AuthConfigError({ message: "auth config failed", cause: error })),
101
+ }),
102
+ );
103
+ return yield* transport.issueWebSocketToken(resolved);
104
+ });
105
+
106
+ return {
107
+ pair,
108
+ local,
109
+ status,
110
+ issueWebSocketToken,
111
+ };
112
+ });
113
+
114
+ export const T3AuthLive = Layer.effect(T3Auth, makeT3Auth());
@@ -0,0 +1,241 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as FileSystem from "effect/FileSystem";
3
+ import * as Path from "effect/Path";
4
+ import * as Stream from "effect/Stream";
5
+ import * as ChildProcess from "effect/unstable/process/ChildProcess";
6
+ import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner";
7
+
8
+ import { normalizeHttpBaseUrl } from "../config/url.ts";
9
+ import { Environment, type EnvironmentShape } from "../environment/service.ts";
10
+ import { AuthLocalError } from "./error.ts";
11
+ import {
12
+ decodeAuthLocalRuntimeStateFromJson,
13
+ decodeAuthLocalSessionIssueResultFromJson,
14
+ type AuthLocalSessionIssueResult,
15
+ } from "./schema.ts";
16
+ import type { LocalAuthInput } from "./type.ts";
17
+
18
+ export const issueLocalSession = Effect.fn("issueLocalSession")(function* (
19
+ input: Pick<LocalAuthInput, "baseDir" | "t3Command" | "role" | "label" | "subject">,
20
+ ) {
21
+ const environment = yield* Environment;
22
+ const baseDir = yield* resolveLocalBaseDir(input.baseDir, environment);
23
+ if (input.label.length === 0) {
24
+ return yield* Effect.fail(new AuthLocalError({ message: "local auth label cannot be empty" }));
25
+ }
26
+ if (input.subject.length === 0) {
27
+ return yield* Effect.fail(
28
+ new AuthLocalError({ message: "local auth subject cannot be empty" }),
29
+ );
30
+ }
31
+
32
+ const args = [
33
+ "auth",
34
+ "session",
35
+ "issue",
36
+ "--base-dir",
37
+ baseDir,
38
+ "--json",
39
+ "--role",
40
+ input.role,
41
+ "--label",
42
+ input.label,
43
+ "--subject",
44
+ input.subject,
45
+ ];
46
+ const command = resolveLocalT3Command(input.t3Command, environment);
47
+ const output = yield* runLocalT3Command(command, args);
48
+ const parsed = yield* parseSessionIssueOutput(output.stdout);
49
+ return { baseDir, session: parsed } as const;
50
+ });
51
+
52
+ export const resolveLocalOrigin = Effect.fn("resolveLocalOrigin")(function* (input: {
53
+ readonly baseDir: string;
54
+ readonly origin?: string;
55
+ }) {
56
+ if (input.origin !== undefined) {
57
+ return yield* normalizeLocalOrigin(input.origin);
58
+ }
59
+
60
+ const path = yield* Path.Path;
61
+ const runtimeStatePath = path.join(input.baseDir, "userdata", "server-runtime.json");
62
+ const fs = yield* FileSystem.FileSystem;
63
+ const raw = yield* fs.readFileString(runtimeStatePath).pipe(
64
+ Effect.mapError(
65
+ (error) =>
66
+ new AuthLocalError({
67
+ message: `local runtime state not found: ${runtimeStatePath}. Make sure T3 Code is running with Network access enabled, or pass --origin manually.`,
68
+ cause: error,
69
+ }),
70
+ ),
71
+ );
72
+ const state = yield* decodeAuthLocalRuntimeStateFromJson(raw).pipe(
73
+ Effect.mapError(
74
+ (error) =>
75
+ new AuthLocalError({ message: "local runtime state has invalid shape", cause: error }),
76
+ ),
77
+ );
78
+ return yield* normalizeLocalOrigin(state.origin);
79
+ });
80
+
81
+ function parseSessionIssueOutput(
82
+ stdout: string,
83
+ ): Effect.Effect<AuthLocalSessionIssueResult, AuthLocalError> {
84
+ return decodeAuthLocalSessionIssueResultFromJson(stdout).pipe(
85
+ Effect.mapError(
86
+ (error) => new AuthLocalError({ message: "local auth returned invalid shape", cause: error }),
87
+ ),
88
+ );
89
+ }
90
+
91
+ function normalizeLocalOrigin(origin: string) {
92
+ return normalizeHttpBaseUrl(origin).pipe(
93
+ Effect.mapError((error) => new AuthLocalError({ message: error.message, cause: error })),
94
+ );
95
+ }
96
+
97
+ type LocalT3Command = {
98
+ readonly command: string;
99
+ readonly argsPrefix: ReadonlyArray<string>;
100
+ readonly env?: Readonly<Record<string, string>>;
101
+ };
102
+
103
+ const defaultLocalT3Command: LocalT3Command = {
104
+ command: "t3",
105
+ argsPrefix: [],
106
+ };
107
+
108
+ const macOsAppNames = ["T3 Code (Dev)", "T3 Code (Alpha)", "T3 Code (Nightly)", "T3 Code"] as const;
109
+
110
+ function resolveLocalT3Command(
111
+ input: string | undefined,
112
+ environment: EnvironmentShape,
113
+ ): LocalT3Command {
114
+ const envCommand = environment.env["T3CLI_T3_COMMAND"];
115
+ const exactCommand =
116
+ input ?? (envCommand !== undefined && envCommand.length > 0 ? envCommand : undefined);
117
+ if (exactCommand !== undefined && exactCommand.length > 0) {
118
+ return { command: exactCommand, argsPrefix: [] };
119
+ }
120
+ return defaultLocalT3Command;
121
+ }
122
+
123
+ const resolveMacOsAppT3Command = Effect.fn("resolveMacOsAppT3Command")(function* () {
124
+ const fs = yield* FileSystem.FileSystem;
125
+ for (const appName of macOsAppNames) {
126
+ const appPath = `/Applications/${appName}.app`;
127
+ const command = `${appPath}/Contents/MacOS/${appName}`;
128
+ if (yield* fs.exists(command)) {
129
+ return {
130
+ command,
131
+ argsPrefix: [`${appPath}/Contents/Resources/app.asar/apps/server/dist/bin.mjs`],
132
+ env: { ELECTRON_RUN_AS_NODE: "1" },
133
+ } satisfies LocalT3Command;
134
+ }
135
+ }
136
+ return undefined;
137
+ });
138
+
139
+ const runLocalT3Command = Effect.fn("runLocalT3Command")(function* (
140
+ command: LocalT3Command,
141
+ args: ReadonlyArray<string>,
142
+ ) {
143
+ return yield* runLocalT3CommandOnce(command, args).pipe(
144
+ Effect.catchTag("PlatformError", (error) =>
145
+ command === defaultLocalT3Command && process.platform === "darwin"
146
+ ? Effect.gen(function* () {
147
+ const fallback = yield* resolveMacOsAppT3Command();
148
+ if (fallback === undefined) {
149
+ return yield* Effect.fail(error);
150
+ }
151
+ return yield* runLocalT3CommandOnce(fallback, args);
152
+ })
153
+ : Effect.fail(error),
154
+ ),
155
+ Effect.catchTag("PlatformError", (error) =>
156
+ Effect.fail(
157
+ new AuthLocalError({
158
+ message: `local auth failed: ${error.message}`,
159
+ cause: error,
160
+ }),
161
+ ),
162
+ ),
163
+ );
164
+ });
165
+
166
+ const runLocalT3CommandOnce = Effect.fn("runLocalT3CommandOnce")(function* (
167
+ command: LocalT3Command,
168
+ args: ReadonlyArray<string>,
169
+ ) {
170
+ const spawner = yield* ChildProcessSpawner;
171
+ const processCommand = ChildProcess.make(command.command, [...command.argsPrefix, ...args]).pipe(
172
+ command.env === undefined ? (self) => self : ChildProcess.setEnv(command.env),
173
+ );
174
+ const child = yield* spawner.spawn(processCommand);
175
+ const [stdout, stderr, exitCode] = yield* Effect.all(
176
+ [
177
+ collectProcessOutput(child.stdout),
178
+ collectProcessOutput(child.stderr),
179
+ child.exitCode.pipe(Effect.map(Number)),
180
+ ],
181
+ { concurrency: "unbounded" },
182
+ ).pipe(
183
+ Effect.mapError(
184
+ (error) =>
185
+ new AuthLocalError({
186
+ message: `local auth failed: ${error.message}`,
187
+ cause: error,
188
+ }),
189
+ ),
190
+ );
191
+
192
+ if (exitCode !== 0) {
193
+ return yield* Effect.fail(
194
+ new AuthLocalError({
195
+ message: `local auth failed: ${formatCommandFailure(stderr, stdout, exitCode)}`,
196
+ }),
197
+ );
198
+ }
199
+
200
+ return { stdout, stderr };
201
+ });
202
+
203
+ const collectProcessOutput = <E>(stream: Stream.Stream<Uint8Array, E>): Effect.Effect<string, E> =>
204
+ stream.pipe(
205
+ Stream.decodeText(),
206
+ Stream.runFold(
207
+ () => "",
208
+ (acc, chunk) => acc + chunk,
209
+ ),
210
+ );
211
+
212
+ const resolveLocalBaseDir = Effect.fn("resolveLocalBaseDir")(function* (
213
+ input: string | undefined,
214
+ environment: EnvironmentShape,
215
+ ) {
216
+ const path = yield* Path.Path;
217
+ const envBaseDir = environment.env["T3CODE_HOME"];
218
+ const raw = input ?? envBaseDir;
219
+ if (raw === undefined || raw.length === 0) {
220
+ return path.join(environment.homeDir, ".t3");
221
+ }
222
+ if (raw === "~") {
223
+ return environment.homeDir;
224
+ }
225
+ if (raw.startsWith("~/") || raw.startsWith("~\\")) {
226
+ return path.join(environment.homeDir, raw.slice(2));
227
+ }
228
+ return path.resolve(environment.cwd, raw);
229
+ });
230
+
231
+ function formatCommandFailure(stderr: string, stdout: string, exitCode: number) {
232
+ const stderrDetail = stderr.trim();
233
+ if (stderrDetail.length > 0) {
234
+ return stderrDetail;
235
+ }
236
+ const stdoutDetail = stdout.trim();
237
+ if (stdoutDetail.length > 0) {
238
+ return stdoutDetail;
239
+ }
240
+ return `t3 exited with code ${exitCode}`;
241
+ }
@@ -0,0 +1,54 @@
1
+ import * as Effect from "effect/Effect";
2
+ import { Url } from "effect/unstable/http";
3
+
4
+ import { AuthPairingUrlError } from "./error.ts";
5
+ import type { PairingUrl } from "./type.ts";
6
+
7
+ export function parsePairingUrl(value: string): Effect.Effect<PairingUrl, AuthPairingUrlError> {
8
+ return Effect.gen(function* () {
9
+ const url = yield* parseUrl(value);
10
+ const token = yield* readPairingToken(url);
11
+ if (token.length === 0) {
12
+ return yield* Effect.fail(new AuthPairingUrlError({ message: "pairing url missing token" }));
13
+ }
14
+
15
+ const hostedHost = url.searchParams.get("host")?.trim();
16
+ const baseUrl = normalizeBaseUrl(
17
+ yield* parseUrl(hostedHost !== undefined && hostedHost.length > 0 ? hostedHost : url.origin),
18
+ );
19
+ return { baseUrl, credential: token };
20
+ });
21
+ }
22
+
23
+ function parseUrl(value: string): Effect.Effect<URL, AuthPairingUrlError> {
24
+ return Effect.fromResult(Url.fromString(value)).pipe(
25
+ Effect.catchTags({
26
+ IllegalArgumentError: (error) =>
27
+ Effect.fail(new AuthPairingUrlError({ message: "invalid pairing url", cause: error })),
28
+ }),
29
+ );
30
+ }
31
+
32
+ function normalizeBaseUrl(url: URL) {
33
+ return Url.mutate(url, (current) => {
34
+ current.hash = "";
35
+ current.search = "";
36
+ current.pathname = "";
37
+ })
38
+ .toString()
39
+ .replace(/\/$/, "");
40
+ }
41
+
42
+ function readPairingToken(url: URL): Effect.Effect<string, AuthPairingUrlError> {
43
+ return Effect.gen(function* () {
44
+ const hash = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash;
45
+ const hashUrl = yield* parseUrl(`http://t3.local/?${hash}`);
46
+ const hashToken = hashUrl.searchParams.get("token")?.trim();
47
+ const token = url.searchParams.get("token")?.trim();
48
+ return hashToken !== undefined && hashToken.length > 0
49
+ ? hashToken
50
+ : token !== undefined && token.length > 0
51
+ ? token
52
+ : "";
53
+ });
54
+ }
@@ -0,0 +1,55 @@
1
+ import * as Schema from "effect/Schema";
2
+
3
+ export const AuthBearerBootstrapResultSchema = Schema.Struct({
4
+ authenticated: Schema.Literal(true),
5
+ role: Schema.Literals(["owner", "client"]),
6
+ sessionMethod: Schema.Literal("bearer-session-token"),
7
+ expiresAt: Schema.String,
8
+ sessionToken: Schema.String,
9
+ });
10
+ export type AuthBearerBootstrapResult = typeof AuthBearerBootstrapResultSchema.Type;
11
+
12
+ export const AuthSessionStateSchema = Schema.Struct({
13
+ authenticated: Schema.Boolean,
14
+ role: Schema.optionalKey(Schema.Literals(["owner", "client"])),
15
+ sessionMethod: Schema.optionalKey(Schema.String),
16
+ expiresAt: Schema.optionalKey(Schema.String),
17
+ });
18
+ export type AuthSessionState = typeof AuthSessionStateSchema.Type;
19
+
20
+ export const AuthWebSocketTokenResultSchema = Schema.Struct({
21
+ token: Schema.String,
22
+ expiresAt: Schema.String,
23
+ });
24
+ export type AuthWebSocketTokenResult = typeof AuthWebSocketTokenResultSchema.Type;
25
+
26
+ export const AuthLocalSessionIssueResultSchema = Schema.Struct({
27
+ token: Schema.String,
28
+ role: Schema.Literals(["owner", "client"]),
29
+ expiresAt: Schema.String,
30
+ });
31
+ export type AuthLocalSessionIssueResult = typeof AuthLocalSessionIssueResultSchema.Type;
32
+
33
+ export const AuthLocalRuntimeStateSchema = Schema.Struct({
34
+ version: Schema.Literal(1),
35
+ origin: Schema.String,
36
+ });
37
+ export type AuthLocalRuntimeState = typeof AuthLocalRuntimeStateSchema.Type;
38
+
39
+ export const decodeAuthBearerBootstrapResult = Schema.decodeUnknownEffect(
40
+ AuthBearerBootstrapResultSchema,
41
+ );
42
+ export const decodeAuthSessionState = Schema.decodeUnknownEffect(AuthSessionStateSchema);
43
+ export const decodeAuthWebSocketTokenResult = Schema.decodeUnknownEffect(
44
+ AuthWebSocketTokenResultSchema,
45
+ );
46
+ export const decodeAuthLocalSessionIssueResult = Schema.decodeUnknownEffect(
47
+ AuthLocalSessionIssueResultSchema,
48
+ );
49
+ export const decodeAuthLocalRuntimeState = Schema.decodeUnknownEffect(AuthLocalRuntimeStateSchema);
50
+ export const decodeAuthLocalSessionIssueResultFromJson = Schema.decodeUnknownEffect(
51
+ Schema.fromJsonString(AuthLocalSessionIssueResultSchema),
52
+ );
53
+ export const decodeAuthLocalRuntimeStateFromJson = Schema.decodeUnknownEffect(
54
+ Schema.fromJsonString(AuthLocalRuntimeStateSchema),
55
+ );
@@ -0,0 +1,16 @@
1
+ import * as Context from "effect/Context";
2
+ import type * as Effect from "effect/Effect";
3
+
4
+ import type { AuthSessionState, AuthWebSocketTokenResult } from "./schema.ts";
5
+ import type { AuthError } from "./error.ts";
6
+ import type { LocalAuthInput, LocalAuthResult, PairResult } from "./type.ts";
7
+
8
+ export class T3Auth extends Context.Service<
9
+ T3Auth,
10
+ {
11
+ readonly pair: (value: string) => Effect.Effect<PairResult, AuthError>;
12
+ readonly local: (input: LocalAuthInput) => Effect.Effect<LocalAuthResult, AuthError>;
13
+ readonly status: () => Effect.Effect<AuthSessionState, AuthError>;
14
+ readonly issueWebSocketToken: () => Effect.Effect<AuthWebSocketTokenResult, AuthError>;
15
+ }
16
+ >()("t3cli/T3Auth") {}