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.
- package/LICENSE +21 -0
- package/README.md +70 -0
- package/dist/bin.js +17842 -0
- package/dist/index.js +2 -0
- package/dist/runtime-DU_hs0MM.js +61631 -0
- package/package.json +59 -0
- package/src/application/error.ts +4 -0
- package/src/application/layer.ts +20 -0
- package/src/application/model-selection.ts +109 -0
- package/src/application/models.ts +26 -0
- package/src/application/project-commands.ts +28 -0
- package/src/application/projects.ts +50 -0
- package/src/application/service.ts +93 -0
- package/src/application/shell-sequence.ts +29 -0
- package/src/application/thread-commands.ts +89 -0
- package/src/application/thread-wait.ts +86 -0
- package/src/application/threads.ts +172 -0
- package/src/auth/error.ts +42 -0
- package/src/auth/layer.ts +114 -0
- package/src/auth/local.ts +241 -0
- package/src/auth/pairing.ts +54 -0
- package/src/auth/schema.ts +55 -0
- package/src/auth/service.ts +16 -0
- package/src/auth/transport.ts +132 -0
- package/src/auth/type.ts +31 -0
- package/src/bin.ts +47 -0
- package/src/cli/app.ts +18 -0
- package/src/cli/auth-format.ts +43 -0
- package/src/cli/auth.ts +99 -0
- package/src/cli/error.ts +16 -0
- package/src/cli/input/error.ts +11 -0
- package/src/cli/input/layer.ts +31 -0
- package/src/cli/input/service.ts +11 -0
- package/src/cli/message-input.ts +25 -0
- package/src/cli/model-format.ts +18 -0
- package/src/cli/model-options.ts +56 -0
- package/src/cli/models.ts +45 -0
- package/src/cli/output/error.ts +11 -0
- package/src/cli/output/layer.ts +53 -0
- package/src/cli/output/service.ts +15 -0
- package/src/cli/output-format.ts +41 -0
- package/src/cli/project-format.ts +11 -0
- package/src/cli/projects.ts +62 -0
- package/src/cli/thread-format.ts +74 -0
- package/src/cli/threads/archive.ts +28 -0
- package/src/cli/threads/list.ts +29 -0
- package/src/cli/threads/messages.ts +36 -0
- package/src/cli/threads/send.ts +91 -0
- package/src/cli/threads/start.ts +136 -0
- package/src/cli/threads/wait.ts +35 -0
- package/src/cli/threads.ts +22 -0
- package/src/cli/wait-events.ts +112 -0
- package/src/config/error.ts +21 -0
- package/src/config/layer.ts +103 -0
- package/src/config/service.ts +24 -0
- package/src/config/url.ts +55 -0
- package/src/domain/command-schema.ts +82 -0
- package/src/domain/error.ts +46 -0
- package/src/domain/helpers.ts +23 -0
- package/src/domain/model-config.ts +40 -0
- package/src/domain/schema.ts +162 -0
- package/src/domain/thread-lifecycle.ts +97 -0
- package/src/environment/layer.ts +12 -0
- package/src/environment/service.ts +13 -0
- package/src/index.ts +18 -0
- package/src/orchestration/layer.ts +193 -0
- package/src/orchestration/service.ts +39 -0
- package/src/protocol/schema.ts +105 -0
- package/src/rpc/error.ts +31 -0
- package/src/rpc/layer.ts +99 -0
- package/src/rpc/service.ts +16 -0
- package/src/runtime.ts +28 -0
- package/src/version/layer.ts +25 -0
- package/src/version/service.ts +8 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import { HttpClient, HttpClientError, HttpClientRequest } from "effect/unstable/http";
|
|
3
|
+
|
|
4
|
+
import type { ResolvedConfig } from "../config/service.ts";
|
|
5
|
+
import { AuthTransportError } from "./error.ts";
|
|
6
|
+
import {
|
|
7
|
+
decodeAuthBearerBootstrapResult,
|
|
8
|
+
decodeAuthSessionState,
|
|
9
|
+
decodeAuthWebSocketTokenResult,
|
|
10
|
+
} from "./schema.ts";
|
|
11
|
+
|
|
12
|
+
export const makeAuthTransport = Effect.fn("makeAuthTransport")(function* () {
|
|
13
|
+
const client = HttpClient.filterStatusOk(yield* HttpClient.HttpClient);
|
|
14
|
+
|
|
15
|
+
const bootstrapBearer = Effect.fn("AuthTransport.bootstrapBearer")(function* (input: {
|
|
16
|
+
readonly baseUrl: string;
|
|
17
|
+
readonly credential: string;
|
|
18
|
+
}) {
|
|
19
|
+
const request = HttpClientRequest.post(input.baseUrl).pipe(
|
|
20
|
+
HttpClientRequest.appendUrl("/api/auth/bootstrap/bearer"),
|
|
21
|
+
HttpClientRequest.acceptJson,
|
|
22
|
+
HttpClientRequest.bodyJsonUnsafe({ credential: input.credential }),
|
|
23
|
+
);
|
|
24
|
+
const response = yield* client.execute(request).pipe(
|
|
25
|
+
Effect.catchTags({
|
|
26
|
+
HttpClientError: (error) =>
|
|
27
|
+
Effect.fail(
|
|
28
|
+
new AuthTransportError({
|
|
29
|
+
message: "auth request failed",
|
|
30
|
+
cause: HttpClientError.HttpClientErrorSchema.fromHttpClientError(error),
|
|
31
|
+
}),
|
|
32
|
+
),
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
return yield* response.json.pipe(
|
|
36
|
+
Effect.flatMap(decodeAuthBearerBootstrapResult),
|
|
37
|
+
Effect.catchTags({
|
|
38
|
+
HttpClientError: (error) =>
|
|
39
|
+
Effect.fail(
|
|
40
|
+
new AuthTransportError({
|
|
41
|
+
message: "auth request failed",
|
|
42
|
+
cause: HttpClientError.HttpClientErrorSchema.fromHttpClientError(error),
|
|
43
|
+
}),
|
|
44
|
+
),
|
|
45
|
+
SchemaError: (error) =>
|
|
46
|
+
Effect.fail(
|
|
47
|
+
new AuthTransportError({ message: "auth response decode failed", cause: error }),
|
|
48
|
+
),
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const getSession = Effect.fn("AuthTransport.getSession")(function* (config: ResolvedConfig) {
|
|
54
|
+
const request = authenticatedRequest(config, "/api/auth/session", "get");
|
|
55
|
+
const response = yield* client.execute(request).pipe(
|
|
56
|
+
Effect.catchTags({
|
|
57
|
+
HttpClientError: (error) =>
|
|
58
|
+
Effect.fail(
|
|
59
|
+
new AuthTransportError({
|
|
60
|
+
message: "auth request failed",
|
|
61
|
+
cause: HttpClientError.HttpClientErrorSchema.fromHttpClientError(error),
|
|
62
|
+
}),
|
|
63
|
+
),
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
return yield* response.json.pipe(
|
|
67
|
+
Effect.flatMap(decodeAuthSessionState),
|
|
68
|
+
Effect.catchTags({
|
|
69
|
+
HttpClientError: (error) =>
|
|
70
|
+
Effect.fail(
|
|
71
|
+
new AuthTransportError({
|
|
72
|
+
message: "auth request failed",
|
|
73
|
+
cause: HttpClientError.HttpClientErrorSchema.fromHttpClientError(error),
|
|
74
|
+
}),
|
|
75
|
+
),
|
|
76
|
+
SchemaError: (error) =>
|
|
77
|
+
Effect.fail(
|
|
78
|
+
new AuthTransportError({ message: "auth response decode failed", cause: error }),
|
|
79
|
+
),
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const issueWebSocketToken = Effect.fn("AuthTransport.issueWebSocketToken")(function* (
|
|
85
|
+
config: ResolvedConfig,
|
|
86
|
+
) {
|
|
87
|
+
const request = authenticatedRequest(config, "/api/auth/ws-token", "post");
|
|
88
|
+
const response = yield* client.execute(request).pipe(
|
|
89
|
+
Effect.catchTags({
|
|
90
|
+
HttpClientError: (error) =>
|
|
91
|
+
Effect.fail(
|
|
92
|
+
new AuthTransportError({
|
|
93
|
+
message: "auth request failed",
|
|
94
|
+
cause: HttpClientError.HttpClientErrorSchema.fromHttpClientError(error),
|
|
95
|
+
}),
|
|
96
|
+
),
|
|
97
|
+
}),
|
|
98
|
+
);
|
|
99
|
+
return yield* response.json.pipe(
|
|
100
|
+
Effect.flatMap(decodeAuthWebSocketTokenResult),
|
|
101
|
+
Effect.catchTags({
|
|
102
|
+
HttpClientError: (error) =>
|
|
103
|
+
Effect.fail(
|
|
104
|
+
new AuthTransportError({
|
|
105
|
+
message: "auth request failed",
|
|
106
|
+
cause: HttpClientError.HttpClientErrorSchema.fromHttpClientError(error),
|
|
107
|
+
}),
|
|
108
|
+
),
|
|
109
|
+
SchemaError: (error) =>
|
|
110
|
+
Effect.fail(
|
|
111
|
+
new AuthTransportError({ message: "auth response decode failed", cause: error }),
|
|
112
|
+
),
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
bootstrapBearer,
|
|
119
|
+
getSession,
|
|
120
|
+
issueWebSocketToken,
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
function authenticatedRequest(config: ResolvedConfig, path: string, method: "get" | "post") {
|
|
125
|
+
const request =
|
|
126
|
+
method === "get" ? HttpClientRequest.get(config.url) : HttpClientRequest.post(config.url);
|
|
127
|
+
return request.pipe(
|
|
128
|
+
HttpClientRequest.appendUrl(path),
|
|
129
|
+
HttpClientRequest.acceptJson,
|
|
130
|
+
HttpClientRequest.bearerToken(config.token),
|
|
131
|
+
);
|
|
132
|
+
}
|
package/src/auth/type.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { AuthBearerBootstrapResult } from "./schema.ts";
|
|
2
|
+
|
|
3
|
+
export type AuthSessionRole = AuthBearerBootstrapResult["role"];
|
|
4
|
+
|
|
5
|
+
export type PairingUrl = {
|
|
6
|
+
readonly baseUrl: string;
|
|
7
|
+
readonly credential: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type PairResult = {
|
|
11
|
+
readonly url: string;
|
|
12
|
+
readonly role: AuthBearerBootstrapResult["role"];
|
|
13
|
+
readonly expiresAt: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type LocalAuthInput = {
|
|
17
|
+
readonly baseDir?: string;
|
|
18
|
+
readonly t3Command?: string;
|
|
19
|
+
readonly origin?: string;
|
|
20
|
+
readonly role: AuthSessionRole;
|
|
21
|
+
readonly label: string;
|
|
22
|
+
readonly subject: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type LocalAuthResult = {
|
|
26
|
+
readonly url: string;
|
|
27
|
+
readonly role: AuthSessionRole;
|
|
28
|
+
readonly expiresAt: string;
|
|
29
|
+
readonly source: "local";
|
|
30
|
+
readonly baseDir: string;
|
|
31
|
+
};
|
package/src/bin.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Layer from "effect/Layer";
|
|
4
|
+
import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
|
|
5
|
+
import * as NodeServices from "@effect/platform-node/NodeServices";
|
|
6
|
+
import { Command } from "effect/unstable/cli";
|
|
7
|
+
|
|
8
|
+
import { createCliCommand } from "./cli/app.ts";
|
|
9
|
+
import { NodeEnvironmentLive } from "./environment/layer.ts";
|
|
10
|
+
import { T3InputLive } from "./cli/input/layer.ts";
|
|
11
|
+
import { T3OutputLive } from "./cli/output/layer.ts";
|
|
12
|
+
import { T3Output } from "./cli/output/service.ts";
|
|
13
|
+
import { AppLayer } from "./runtime.ts";
|
|
14
|
+
import { T3VersionBundledLive, T3VersionPackageJsonLive } from "./version/layer.ts";
|
|
15
|
+
import { T3Version } from "./version/service.ts";
|
|
16
|
+
|
|
17
|
+
declare const T3CLI_VERSION: string | undefined;
|
|
18
|
+
|
|
19
|
+
const VersionLive =
|
|
20
|
+
typeof T3CLI_VERSION === "string" ? T3VersionBundledLive : T3VersionPackageJsonLive;
|
|
21
|
+
|
|
22
|
+
const PlatformLayer = Layer.mergeAll(NodeServices.layer, NodeEnvironmentLive);
|
|
23
|
+
|
|
24
|
+
const CliLayer = Layer.mergeAll(
|
|
25
|
+
AppLayer.pipe(Layer.provide(PlatformLayer)),
|
|
26
|
+
NodeServices.layer,
|
|
27
|
+
NodeEnvironmentLive,
|
|
28
|
+
T3InputLive.pipe(Layer.provide(NodeServices.layer)),
|
|
29
|
+
T3OutputLive.pipe(Layer.provide(NodeServices.layer)),
|
|
30
|
+
VersionLive.pipe(Layer.provide(NodeServices.layer)),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const program = Effect.gen(function* () {
|
|
34
|
+
const version = yield* T3Version;
|
|
35
|
+
return yield* Command.run(createCliCommand(), { version: version.version });
|
|
36
|
+
}).pipe(
|
|
37
|
+
Effect.tapError((error) =>
|
|
38
|
+
Effect.gen(function* () {
|
|
39
|
+
const output = yield* T3Output;
|
|
40
|
+
yield* output.writeStderr(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
41
|
+
}),
|
|
42
|
+
),
|
|
43
|
+
Effect.scoped,
|
|
44
|
+
Effect.provide(CliLayer),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
NodeRuntime.runMain(program, { disableErrorReporting: true });
|
package/src/cli/app.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Command } from "effect/unstable/cli";
|
|
2
|
+
|
|
3
|
+
import { createAuthCommand } from "./auth.ts";
|
|
4
|
+
import { createModelsCommand } from "./models.ts";
|
|
5
|
+
import { createProjectsCommand } from "./projects.ts";
|
|
6
|
+
import { createThreadsCommand } from "./threads.ts";
|
|
7
|
+
|
|
8
|
+
export function createCliCommand() {
|
|
9
|
+
return Command.make("t3cli").pipe(
|
|
10
|
+
Command.withDescription("non-interactive cli for running t3code server"),
|
|
11
|
+
Command.withSubcommands([
|
|
12
|
+
createAuthCommand(),
|
|
13
|
+
createModelsCommand(),
|
|
14
|
+
createProjectsCommand(),
|
|
15
|
+
createThreadsCommand(),
|
|
16
|
+
]),
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { AuthSessionState } from "../auth/schema.ts";
|
|
2
|
+
import type { ResolvedConfig } from "../config/service.ts";
|
|
3
|
+
import type { LocalAuthResult, PairResult } from "../auth/type.ts";
|
|
4
|
+
|
|
5
|
+
export function formatAuthPaired(result: PairResult) {
|
|
6
|
+
return `paired: ${result.url}\nrole: ${result.role}\nexpires: ${result.expiresAt}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function formatAuthLocalHuman(result: LocalAuthResult) {
|
|
10
|
+
return [
|
|
11
|
+
`paired: ${result.url}`,
|
|
12
|
+
`role: ${result.role}`,
|
|
13
|
+
`expires: ${result.expiresAt}`,
|
|
14
|
+
`baseDir: ${result.baseDir}`,
|
|
15
|
+
].join("\n");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function formatAuthLocalJson(result: LocalAuthResult) {
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function formatAuthStatusHuman(input: {
|
|
23
|
+
readonly config: ResolvedConfig;
|
|
24
|
+
readonly result: AuthSessionState;
|
|
25
|
+
}) {
|
|
26
|
+
return [
|
|
27
|
+
`url: ${input.config.url}`,
|
|
28
|
+
`authenticated: ${input.result.authenticated ? "yes" : "no"}`,
|
|
29
|
+
...(input.result.role !== undefined ? [`role: ${input.result.role}`] : []),
|
|
30
|
+
...(input.result.expiresAt !== undefined ? [`expires: ${input.result.expiresAt}`] : []),
|
|
31
|
+
].join("\n");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function formatAuthStatusJson(input: {
|
|
35
|
+
readonly config: ResolvedConfig;
|
|
36
|
+
readonly result: AuthSessionState;
|
|
37
|
+
}) {
|
|
38
|
+
return {
|
|
39
|
+
...input.result,
|
|
40
|
+
url: input.config.url,
|
|
41
|
+
source: input.config.source,
|
|
42
|
+
};
|
|
43
|
+
}
|
package/src/cli/auth.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Option from "effect/Option";
|
|
3
|
+
import { Argument, Command, Flag } from "effect/unstable/cli";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
formatAuthLocalHuman,
|
|
7
|
+
formatAuthLocalJson,
|
|
8
|
+
formatAuthPaired,
|
|
9
|
+
formatAuthStatusHuman,
|
|
10
|
+
formatAuthStatusJson,
|
|
11
|
+
} from "./auth-format.ts";
|
|
12
|
+
import { T3Auth } from "../auth/service.ts";
|
|
13
|
+
import { T3Config } from "../config/service.ts";
|
|
14
|
+
import { Environment } from "../environment/service.ts";
|
|
15
|
+
import { humanJsonFormatChoices, resolveOutputFormat } from "./output-format.ts";
|
|
16
|
+
import { T3Output } from "./output/service.ts";
|
|
17
|
+
|
|
18
|
+
export function createAuthCommand() {
|
|
19
|
+
return Command.make("auth").pipe(
|
|
20
|
+
Command.withDescription("auth commands"),
|
|
21
|
+
Command.withSubcommands([pairCommand, localCommand, statusCommand]),
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const pairCommand = Command.make(
|
|
26
|
+
"pair",
|
|
27
|
+
{
|
|
28
|
+
url: Argument.string("url"),
|
|
29
|
+
format: Flag.choice("format", humanJsonFormatChoices).pipe(Flag.withDefault("auto")),
|
|
30
|
+
},
|
|
31
|
+
({ url, format }) =>
|
|
32
|
+
Effect.gen(function* () {
|
|
33
|
+
const auth = yield* T3Auth;
|
|
34
|
+
const environment = yield* Environment;
|
|
35
|
+
const output = yield* T3Output;
|
|
36
|
+
const resolvedFormat = resolveOutputFormat(format, environment, "json");
|
|
37
|
+
const result = yield* auth.pair(url);
|
|
38
|
+
if (resolvedFormat === "json") {
|
|
39
|
+
yield* output.printJson(result);
|
|
40
|
+
} else {
|
|
41
|
+
yield* output.printInfo(formatAuthPaired(result));
|
|
42
|
+
}
|
|
43
|
+
}),
|
|
44
|
+
).pipe(Command.withDescription("pair with t3code server"));
|
|
45
|
+
|
|
46
|
+
const localCommand = Command.make(
|
|
47
|
+
"local",
|
|
48
|
+
{
|
|
49
|
+
baseDir: Flag.string("base-dir").pipe(Flag.optional),
|
|
50
|
+
t3Command: Flag.string("t3-command").pipe(Flag.optional),
|
|
51
|
+
origin: Flag.string("origin").pipe(Flag.optional),
|
|
52
|
+
role: Flag.choice("role", ["owner", "client"] as const).pipe(Flag.withDefault("owner")),
|
|
53
|
+
label: Flag.string("label").pipe(Flag.withDefault("t3cli")),
|
|
54
|
+
subject: Flag.string("subject").pipe(Flag.withDefault("t3cli-local")),
|
|
55
|
+
format: Flag.choice("format", humanJsonFormatChoices).pipe(Flag.withDefault("auto")),
|
|
56
|
+
},
|
|
57
|
+
({ baseDir, t3Command, origin, role, label, subject, format }) =>
|
|
58
|
+
Effect.gen(function* () {
|
|
59
|
+
const auth = yield* T3Auth;
|
|
60
|
+
const environment = yield* Environment;
|
|
61
|
+
const output = yield* T3Output;
|
|
62
|
+
const resolvedFormat = resolveOutputFormat(format, environment, "json");
|
|
63
|
+
const result = yield* auth.local({
|
|
64
|
+
role,
|
|
65
|
+
label,
|
|
66
|
+
subject,
|
|
67
|
+
...(Option.isSome(t3Command) ? { t3Command: t3Command.value } : {}),
|
|
68
|
+
...(Option.isSome(baseDir) ? { baseDir: baseDir.value } : {}),
|
|
69
|
+
...(Option.isSome(origin) ? { origin: origin.value } : {}),
|
|
70
|
+
});
|
|
71
|
+
if (resolvedFormat === "json") {
|
|
72
|
+
yield* output.printJson(formatAuthLocalJson(result));
|
|
73
|
+
} else {
|
|
74
|
+
yield* output.printInfo(formatAuthLocalHuman(result));
|
|
75
|
+
}
|
|
76
|
+
}),
|
|
77
|
+
).pipe(Command.withDescription("authenticate with local t3code installation"));
|
|
78
|
+
|
|
79
|
+
const statusCommand = Command.make(
|
|
80
|
+
"status",
|
|
81
|
+
{
|
|
82
|
+
format: Flag.choice("format", humanJsonFormatChoices).pipe(Flag.withDefault("auto")),
|
|
83
|
+
},
|
|
84
|
+
({ format }) =>
|
|
85
|
+
Effect.gen(function* () {
|
|
86
|
+
const configService = yield* T3Config;
|
|
87
|
+
const auth = yield* T3Auth;
|
|
88
|
+
const environment = yield* Environment;
|
|
89
|
+
const output = yield* T3Output;
|
|
90
|
+
const resolvedFormat = resolveOutputFormat(format, environment, "json");
|
|
91
|
+
const config = yield* configService.resolve();
|
|
92
|
+
const result = yield* auth.status();
|
|
93
|
+
if (resolvedFormat === "json") {
|
|
94
|
+
yield* output.printJson(formatAuthStatusJson({ config, result }));
|
|
95
|
+
} else {
|
|
96
|
+
yield* output.printInfo(formatAuthStatusHuman({ config, result }));
|
|
97
|
+
}
|
|
98
|
+
}),
|
|
99
|
+
).pipe(Command.withDescription("show auth status"));
|
package/src/cli/error.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
|
|
3
|
+
export class MessageInputError extends Schema.TaggedErrorClass<MessageInputError>()(
|
|
4
|
+
"MessageInputError",
|
|
5
|
+
{
|
|
6
|
+
message: Schema.String,
|
|
7
|
+
},
|
|
8
|
+
) {}
|
|
9
|
+
|
|
10
|
+
export class InvalidLimitError extends Schema.TaggedErrorClass<InvalidLimitError>()(
|
|
11
|
+
"InvalidLimitError",
|
|
12
|
+
{
|
|
13
|
+
message: Schema.String,
|
|
14
|
+
value: Schema.String,
|
|
15
|
+
},
|
|
16
|
+
) {}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
|
|
3
|
+
const PlatformErrorCauseSchema = Schema.Struct({
|
|
4
|
+
_tag: Schema.Literal("PlatformError"),
|
|
5
|
+
message: Schema.String,
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export class InputError extends Schema.TaggedErrorClass<InputError>()("InputError", {
|
|
9
|
+
message: Schema.String,
|
|
10
|
+
cause: Schema.optionalKey(PlatformErrorCauseSchema),
|
|
11
|
+
}) {}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Layer from "effect/Layer";
|
|
3
|
+
import * as Stdio from "effect/Stdio";
|
|
4
|
+
import * as Stream from "effect/Stream";
|
|
5
|
+
|
|
6
|
+
import { InputError } from "./error.ts";
|
|
7
|
+
import { T3Input } from "./service.ts";
|
|
8
|
+
|
|
9
|
+
export const makeT3Input = Effect.fn("makeT3Input")(function* () {
|
|
10
|
+
const stdio = yield* Stdio.Stdio;
|
|
11
|
+
|
|
12
|
+
const readStdin = Effect.fn("T3InputLive.readStdin")(function* () {
|
|
13
|
+
return yield* stdio.stdin.pipe(
|
|
14
|
+
Stream.decodeText(),
|
|
15
|
+
Stream.runFold(
|
|
16
|
+
() => "",
|
|
17
|
+
(acc, chunk) => acc + chunk,
|
|
18
|
+
),
|
|
19
|
+
Effect.catchTags({
|
|
20
|
+
PlatformError: (error) =>
|
|
21
|
+
Effect.fail(new InputError({ message: "failed to read stdin", cause: error })),
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
readStdin,
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const T3InputLive = Layer.effect(T3Input, makeT3Input());
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import type * as Effect from "effect/Effect";
|
|
3
|
+
|
|
4
|
+
import type { InputError } from "./error.ts";
|
|
5
|
+
|
|
6
|
+
export class T3Input extends Context.Service<
|
|
7
|
+
T3Input,
|
|
8
|
+
{
|
|
9
|
+
readonly readStdin: () => Effect.Effect<string, InputError>;
|
|
10
|
+
}
|
|
11
|
+
>()("t3cli/T3Input") {}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
|
|
3
|
+
import { MessageInputError } from "./error.ts";
|
|
4
|
+
import type { InputError } from "./input/error.ts";
|
|
5
|
+
|
|
6
|
+
export function readInitialMessage(input: {
|
|
7
|
+
readonly message: string | undefined;
|
|
8
|
+
readonly fromStdin: boolean;
|
|
9
|
+
readonly readStdin: () => Effect.Effect<string, InputError>;
|
|
10
|
+
}) {
|
|
11
|
+
if (input.message !== undefined && input.message.length > 0 && input.fromStdin) {
|
|
12
|
+
return Effect.fail(
|
|
13
|
+
new MessageInputError({ message: "pass message argument or --stdin, not both" }),
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
if (input.fromStdin) {
|
|
17
|
+
return input.readStdin();
|
|
18
|
+
}
|
|
19
|
+
if (input.message === undefined || input.message.length === 0) {
|
|
20
|
+
return Effect.fail(
|
|
21
|
+
new MessageInputError({ message: "message required unless --stdin is used" }),
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
return Effect.succeed(input.message);
|
|
25
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ServerProvider } from "../domain/schema.ts";
|
|
2
|
+
|
|
3
|
+
export function formatModelsHuman(providers: ReadonlyArray<ServerProvider>) {
|
|
4
|
+
if (providers.length === 0) {
|
|
5
|
+
return "no models found\n";
|
|
6
|
+
}
|
|
7
|
+
return providers
|
|
8
|
+
.map((provider) => {
|
|
9
|
+
const models = provider.models;
|
|
10
|
+
const displayName = provider.displayName ?? provider.instanceId;
|
|
11
|
+
const header = `${displayName} (${provider.instanceId}) - ${provider.status}`;
|
|
12
|
+
if (models.length === 0) {
|
|
13
|
+
return `${header}\n no models`;
|
|
14
|
+
}
|
|
15
|
+
return [header, ...models.map((model) => ` ${model.slug} - ${model.name}`)].join("\n");
|
|
16
|
+
})
|
|
17
|
+
.join("\n\n");
|
|
18
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as Option from "effect/Option";
|
|
2
|
+
|
|
3
|
+
type ModelOptionValue = string | boolean;
|
|
4
|
+
|
|
5
|
+
export type ModelOption = {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly value: ModelOptionValue;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function buildModelOptions(input: {
|
|
11
|
+
readonly option: Option.Option<Record<string, string>>;
|
|
12
|
+
readonly reasoningEffort: Option.Option<string>;
|
|
13
|
+
readonly effort: Option.Option<string>;
|
|
14
|
+
readonly fastMode: Option.Option<boolean>;
|
|
15
|
+
readonly thinking: Option.Option<boolean>;
|
|
16
|
+
}): ReadonlyArray<ModelOption> {
|
|
17
|
+
const options = new Map<string, ModelOption>();
|
|
18
|
+
const optionRecord = Option.getOrUndefined(input.option);
|
|
19
|
+
if (optionRecord !== undefined) {
|
|
20
|
+
for (const [id, value] of Object.entries(optionRecord)) {
|
|
21
|
+
options.set(id, { id, value: parseModelOptionValue(value) });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
setStringOption(options, "reasoningEffort", Option.getOrUndefined(input.reasoningEffort));
|
|
25
|
+
setStringOption(options, "effort", Option.getOrUndefined(input.effort));
|
|
26
|
+
setBooleanOption(options, "fastMode", Option.getOrUndefined(input.fastMode));
|
|
27
|
+
setBooleanOption(options, "thinking", Option.getOrUndefined(input.thinking));
|
|
28
|
+
return [...options.values()];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseModelOptionValue(value: string): ModelOptionValue {
|
|
32
|
+
const normalized = value.trim().toLowerCase();
|
|
33
|
+
if (normalized === "true") {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
if (normalized === "false") {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function setStringOption(options: Map<string, ModelOption>, id: string, value: string | undefined) {
|
|
43
|
+
if (value !== undefined && value.length > 0) {
|
|
44
|
+
options.set(id, { id, value });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function setBooleanOption(
|
|
49
|
+
options: Map<string, ModelOption>,
|
|
50
|
+
id: string,
|
|
51
|
+
value: boolean | undefined,
|
|
52
|
+
) {
|
|
53
|
+
if (value !== undefined) {
|
|
54
|
+
options.set(id, { id, value });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Option from "effect/Option";
|
|
3
|
+
import { Command, Flag } from "effect/unstable/cli";
|
|
4
|
+
|
|
5
|
+
import { T3Application } from "../application/service.ts";
|
|
6
|
+
import { Environment } from "../environment/service.ts";
|
|
7
|
+
import { formatModelsHuman } from "./model-format.ts";
|
|
8
|
+
import { humanJsonFormatChoices, resolveOutputFormat } from "./output-format.ts";
|
|
9
|
+
import { T3Output } from "./output/service.ts";
|
|
10
|
+
|
|
11
|
+
export function createModelsCommand() {
|
|
12
|
+
return Command.make("models").pipe(
|
|
13
|
+
Command.withDescription("model commands"),
|
|
14
|
+
Command.withSubcommands([listCommand]),
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const listCommand = Command.make(
|
|
19
|
+
"list",
|
|
20
|
+
{
|
|
21
|
+
all: Flag.boolean("all"),
|
|
22
|
+
provider: Flag.string("provider").pipe(Flag.optional),
|
|
23
|
+
format: Flag.choice("format", humanJsonFormatChoices).pipe(Flag.withDefault("auto")),
|
|
24
|
+
},
|
|
25
|
+
({ all, provider, format }) =>
|
|
26
|
+
Effect.gen(function* () {
|
|
27
|
+
const application = yield* T3Application;
|
|
28
|
+
const environment = yield* Environment;
|
|
29
|
+
const output = yield* T3Output;
|
|
30
|
+
const providerValue = Option.getOrUndefined(provider);
|
|
31
|
+
const resolvedFormat = resolveOutputFormat(format, environment, "json");
|
|
32
|
+
const providers = yield* application.listModels({
|
|
33
|
+
all,
|
|
34
|
+
...(providerValue !== undefined && providerValue.length > 0
|
|
35
|
+
? { provider: providerValue }
|
|
36
|
+
: {}),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (resolvedFormat === "json") {
|
|
40
|
+
yield* output.printJson(providers);
|
|
41
|
+
} else {
|
|
42
|
+
yield* output.writeStdout(formatModelsHuman(providers));
|
|
43
|
+
}
|
|
44
|
+
}),
|
|
45
|
+
).pipe(Command.withDescription("list provider models"));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
|
|
3
|
+
const PlatformErrorCauseSchema = Schema.Struct({
|
|
4
|
+
_tag: Schema.Literal("PlatformError"),
|
|
5
|
+
message: Schema.String,
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export class OutputError extends Schema.TaggedErrorClass<OutputError>()("OutputError", {
|
|
9
|
+
message: Schema.String,
|
|
10
|
+
cause: Schema.optionalKey(PlatformErrorCauseSchema),
|
|
11
|
+
}) {}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Layer from "effect/Layer";
|
|
3
|
+
import * as Stdio from "effect/Stdio";
|
|
4
|
+
import * as Stream from "effect/Stream";
|
|
5
|
+
|
|
6
|
+
import { OutputError } from "./error.ts";
|
|
7
|
+
import { T3Output } from "./service.ts";
|
|
8
|
+
|
|
9
|
+
export const makeT3Output = Effect.fn("makeT3Output")(function* () {
|
|
10
|
+
const stdio = yield* Stdio.Stdio;
|
|
11
|
+
|
|
12
|
+
const writeStdout = Effect.fn("T3OutputLive.writeStdout")(function* (text: string) {
|
|
13
|
+
yield* Stream.succeed(text).pipe(
|
|
14
|
+
Stream.run(stdio.stdout()),
|
|
15
|
+
Effect.catchTags({
|
|
16
|
+
PlatformError: (error) =>
|
|
17
|
+
Effect.fail(new OutputError({ message: "failed to write output", cause: error })),
|
|
18
|
+
}),
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const writeStderr = Effect.fn("T3OutputLive.writeStderr")(function* (text: string) {
|
|
23
|
+
yield* Stream.succeed(text).pipe(
|
|
24
|
+
Stream.run(stdio.stderr()),
|
|
25
|
+
Effect.catchTags({
|
|
26
|
+
PlatformError: (error) =>
|
|
27
|
+
Effect.fail(new OutputError({ message: "failed to write output", cause: error })),
|
|
28
|
+
}),
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const printJson = Effect.fn("T3OutputLive.printJson")(function* (value: unknown) {
|
|
33
|
+
yield* writeStdout(`${JSON.stringify(value, null, 2)}\n`);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const printNdjson = Effect.fn("T3OutputLive.printNdjson")(function* (value: unknown) {
|
|
37
|
+
yield* writeStdout(`${JSON.stringify(value)}\n`);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const printInfo = Effect.fn("T3OutputLive.printInfo")(function* (message: string) {
|
|
41
|
+
yield* writeStdout(`${message}\n`);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
writeStdout,
|
|
46
|
+
writeStderr,
|
|
47
|
+
printJson,
|
|
48
|
+
printNdjson,
|
|
49
|
+
printInfo,
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const T3OutputLive = Layer.effect(T3Output, makeT3Output());
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import type * as Effect from "effect/Effect";
|
|
3
|
+
|
|
4
|
+
import type { OutputError } from "./error.ts";
|
|
5
|
+
|
|
6
|
+
export class T3Output extends Context.Service<
|
|
7
|
+
T3Output,
|
|
8
|
+
{
|
|
9
|
+
readonly writeStdout: (text: string) => Effect.Effect<void, OutputError>;
|
|
10
|
+
readonly writeStderr: (text: string) => Effect.Effect<void, OutputError>;
|
|
11
|
+
readonly printJson: (value: unknown) => Effect.Effect<void, OutputError>;
|
|
12
|
+
readonly printNdjson: (value: unknown) => Effect.Effect<void, OutputError>;
|
|
13
|
+
readonly printInfo: (message: string) => Effect.Effect<void, OutputError>;
|
|
14
|
+
}
|
|
15
|
+
>()("t3cli/T3Output") {}
|