t3code-cli 0.9.1 → 0.11.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/README.md +52 -14
- package/dist/application.js +2 -2
- package/dist/auth.js +1 -1
- package/dist/bin.js +1556 -341
- package/dist/cli.js +1 -1
- package/dist/config.js +1 -1
- package/dist/connection.js +1 -11
- package/dist/index.js +1 -1
- package/dist/layout.js +1 -1
- package/dist/node.js +2 -0
- package/dist/orchestration.js +2 -2
- package/dist/rpc.js +2 -2
- package/dist/runtime.js +1 -1
- package/dist/scope.js +1 -1
- package/dist/shared.js +1734 -756
- package/dist/src/application/index.d.ts +3 -2
- package/dist/src/application/layer.d.ts +82 -841
- package/dist/src/application/model-selection.d.ts +15 -0
- package/dist/src/application/models.d.ts +1 -87
- package/dist/src/application/project-commands.d.ts +9 -0
- package/dist/src/application/projects.d.ts +13 -135
- package/dist/src/application/service.d.ts +84 -5
- package/dist/src/application/shell-sequence.d.ts +2 -12
- package/dist/src/application/terminals.d.ts +66 -0
- package/dist/src/application/thread-commands.d.ts +47 -0
- package/dist/src/application/thread-update.d.ts +8 -0
- package/dist/src/application/thread-wait.d.ts +3 -14
- package/dist/src/application/threads.d.ts +21 -50
- package/dist/src/cli/flags.d.ts +3 -0
- package/dist/src/cli/output-format.d.ts +2 -0
- package/dist/src/domain/error.d.ts +14 -1
- package/dist/src/domain/helpers.d.ts +0 -9
- package/dist/src/domain/model-config.d.ts +29 -3
- package/dist/src/domain/thread-lifecycle.d.ts +2 -10
- package/dist/src/node/index.d.ts +2 -0
- package/dist/src/orchestration/index.d.ts +1 -0
- package/dist/src/orchestration/layer.d.ts +177 -1032
- package/dist/src/orchestration/service.d.ts +1 -0
- package/dist/src/rpc/error.d.ts +4 -2
- package/dist/src/rpc/index.d.ts +5 -0
- package/dist/src/rpc/layer.d.ts +1097 -499
- package/dist/src/rpc/operation.d.ts +5752 -0
- package/dist/src/rpc/ws-group.d.ts +327 -147
- package/dist/src/runtime/layer.d.ts +1 -1
- package/dist/t3tools.js +2 -2
- package/dist/upstream-t3code/packages/contracts/src/environmentHttp.d.ts +10 -33
- package/dist/upstream-t3code/packages/contracts/src/ipc.d.ts +0 -6
- package/dist/upstream-t3code/packages/contracts/src/orchestration.d.ts +1 -657
- package/dist/upstream-t3code/packages/contracts/src/provider.d.ts +0 -14
- package/dist/upstream-t3code/packages/contracts/src/providerRuntime.d.ts +1 -175
- package/dist/upstream-t3code/packages/contracts/src/rpc.d.ts +0 -492
- package/dist/upstream-t3code/packages/contracts/src/server.d.ts +0 -45
- package/dist/upstream-t3code/packages/contracts/src/settings.d.ts +0 -21
- package/dist/upstream-t3code/packages/contracts/src/terminal.d.ts +3 -6
- package/package.json +6 -2
- package/src/application/index.ts +28 -2
- package/src/application/layer.ts +34 -5
- package/src/application/model-selection.ts +68 -2
- package/src/application/models.ts +14 -14
- package/src/application/project-commands.ts +15 -0
- package/src/application/projects.ts +43 -13
- package/src/application/service.ts +196 -77
- package/src/application/shell-sequence.ts +5 -7
- package/src/application/terminals.ts +207 -0
- package/src/application/thread-commands.test.ts +43 -0
- package/src/application/thread-commands.ts +81 -0
- package/src/application/thread-update.ts +72 -0
- package/src/application/thread-wait.ts +46 -47
- package/src/application/threads.test.ts +166 -0
- package/src/application/threads.ts +123 -40
- package/src/bin.ts +2 -0
- package/src/cli/app.ts +14 -0
- package/src/cli/confirm.ts +31 -0
- package/src/cli/error.ts +26 -3
- package/src/cli/flags.ts +15 -0
- package/src/cli/input/layer.ts +20 -0
- package/src/cli/input/service.ts +1 -0
- package/src/cli/output-format.ts +6 -2
- package/src/cli/project-format.ts +7 -0
- package/src/cli/project.ts +2 -1
- package/src/cli/projects/delete.ts +50 -0
- package/src/cli/self-action.ts +34 -0
- package/src/cli/terminal/attach.ts +32 -0
- package/src/cli/terminal/commands.test.ts +101 -0
- package/src/cli/terminal/create.ts +54 -0
- package/src/cli/terminal/destroy.ts +60 -0
- package/src/cli/terminal/encoding.test.ts +63 -0
- package/src/cli/terminal/encoding.ts +23 -0
- package/src/cli/terminal/error.ts +14 -0
- package/src/cli/terminal/io-node-layer.ts +82 -0
- package/src/cli/terminal/io-service.ts +25 -0
- package/src/cli/terminal/list.ts +35 -0
- package/src/cli/terminal/read.ts +102 -0
- package/src/cli/terminal/scope.ts +30 -0
- package/src/cli/terminal/shared.ts +250 -0
- package/src/cli/terminal/stream.ts +64 -0
- package/src/cli/terminal/wait.test.ts +146 -0
- package/src/cli/terminal/wait.ts +222 -0
- package/src/cli/terminal/write.ts +132 -0
- package/src/cli/terminal-format.ts +167 -0
- package/src/cli/terminal.ts +26 -0
- package/src/cli/thread-format.test.ts +32 -0
- package/src/cli/thread-format.ts +8 -1
- package/src/cli/thread.ts +9 -13
- package/src/cli/threads/archive.ts +11 -18
- package/src/cli/threads/delete.ts +61 -0
- package/src/cli/threads/interrupt.ts +52 -0
- package/src/cli/threads/list.test.ts +69 -0
- package/src/cli/threads/list.ts +15 -3
- package/src/cli/threads/messages.ts +3 -3
- package/src/cli/threads/send.ts +22 -2
- package/src/cli/threads/unarchive.ts +44 -0
- package/src/cli/threads/update.ts +150 -0
- package/src/domain/error.ts +20 -1
- package/src/domain/model-config.ts +4 -0
- package/src/domain/thread-lifecycle.test.ts +35 -0
- package/src/domain/thread-lifecycle.ts +29 -6
- package/src/node/index.ts +2 -0
- package/src/orchestration/index.ts +1 -0
- package/src/orchestration/layer.ts +33 -85
- package/src/orchestration/service.ts +4 -0
- package/src/rpc/error.ts +10 -0
- package/src/rpc/index.ts +5 -0
- package/src/rpc/operation.ts +83 -0
- package/src/rpc/ws-group.ts +24 -0
- package/src/runtime/layer.ts +7 -2
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import "vite-plus/test/config";
|
|
2
|
+
|
|
3
|
+
import * as Cause from "effect/Cause";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as Exit from "effect/Exit";
|
|
6
|
+
import * as Layer from "effect/Layer";
|
|
7
|
+
import * as NodeServices from "@effect/platform-node/NodeServices";
|
|
8
|
+
import * as Option from "effect/Option";
|
|
9
|
+
import * as Stream from "effect/Stream";
|
|
10
|
+
import { assert, describe, expect, it } from "@effect/vitest";
|
|
11
|
+
import { Command } from "effect/unstable/cli";
|
|
12
|
+
import { fromPartial } from "@total-typescript/shoehorn";
|
|
13
|
+
|
|
14
|
+
import { T3Application } from "../../application/service.ts";
|
|
15
|
+
import { T3Config } from "../../config/service.ts";
|
|
16
|
+
import { NodeEnvironmentLive } from "../../environment/layer.ts";
|
|
17
|
+
import { T3Output } from "../output/service.ts";
|
|
18
|
+
import { TerminalCliError } from "./error.ts";
|
|
19
|
+
import { resolveMetadataWait, waitTerminalCommand } from "./wait.ts";
|
|
20
|
+
|
|
21
|
+
describe("resolveMetadataWait", () => {
|
|
22
|
+
it("resolves closed immediately when terminal is missing from snapshot", () => {
|
|
23
|
+
const resolution = resolveMetadataWait(
|
|
24
|
+
{ type: "snapshot", terminals: [] },
|
|
25
|
+
"closed",
|
|
26
|
+
"thread-1",
|
|
27
|
+
"term-1",
|
|
28
|
+
);
|
|
29
|
+
expect(resolution).toEqual({
|
|
30
|
+
kind: "result",
|
|
31
|
+
value: {
|
|
32
|
+
threadId: "thread-1",
|
|
33
|
+
terminalId: "term-1",
|
|
34
|
+
target: "closed",
|
|
35
|
+
status: "closed",
|
|
36
|
+
alreadySatisfied: true,
|
|
37
|
+
missingTreatedAsClosed: true,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("uses the same exited error when terminal is missing from snapshot or removed", () => {
|
|
43
|
+
const missing = resolveMetadataWait(
|
|
44
|
+
{ type: "snapshot", terminals: [] },
|
|
45
|
+
"exited",
|
|
46
|
+
"thread-1",
|
|
47
|
+
"term-1",
|
|
48
|
+
);
|
|
49
|
+
const removed = resolveMetadataWait(
|
|
50
|
+
{ type: "remove", threadId: "thread-1", terminalId: "term-1" },
|
|
51
|
+
"exited",
|
|
52
|
+
"thread-1",
|
|
53
|
+
"term-1",
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
expect(missing?.kind).toBe("fail");
|
|
57
|
+
expect(removed?.kind).toBe("fail");
|
|
58
|
+
if (missing?.kind === "fail" && removed?.kind === "fail") {
|
|
59
|
+
expect(missing.error.message).toBe(removed.error.message);
|
|
60
|
+
expect(missing.error).toBeInstanceOf(TerminalCliError);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("waitTerminalCommand", () => {
|
|
66
|
+
const testLayer = Layer.mergeAll(
|
|
67
|
+
Layer.succeed(
|
|
68
|
+
T3Application,
|
|
69
|
+
fromPartial({
|
|
70
|
+
getTerminal: () => Effect.die("getTerminal should not be called"),
|
|
71
|
+
watchTerminalMetadata: () =>
|
|
72
|
+
Stream.make({
|
|
73
|
+
type: "snapshot" as const,
|
|
74
|
+
terminals: [],
|
|
75
|
+
}),
|
|
76
|
+
}),
|
|
77
|
+
),
|
|
78
|
+
Layer.succeed(
|
|
79
|
+
T3Config,
|
|
80
|
+
fromPartial({
|
|
81
|
+
resolve: () =>
|
|
82
|
+
Effect.succeed({
|
|
83
|
+
url: "ws://localhost",
|
|
84
|
+
token: "token",
|
|
85
|
+
source: "config",
|
|
86
|
+
local: true,
|
|
87
|
+
}),
|
|
88
|
+
}),
|
|
89
|
+
),
|
|
90
|
+
Layer.succeed(T3Output, {
|
|
91
|
+
writeStdout: () => Effect.void,
|
|
92
|
+
writeStderr: () => Effect.void,
|
|
93
|
+
printJson: () => Effect.void,
|
|
94
|
+
printNdjson: () => Effect.void,
|
|
95
|
+
printInfo: () => Effect.void,
|
|
96
|
+
}),
|
|
97
|
+
NodeServices.layer,
|
|
98
|
+
NodeEnvironmentLive,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
it.layer(testLayer)("waitTerminalCommand", (t) => {
|
|
102
|
+
t.effect("does not require getTerminal before waiting for closed", () =>
|
|
103
|
+
Effect.gen(function* () {
|
|
104
|
+
const run = Command.runWith(waitTerminalCommand, { version: "0.0.0-test" });
|
|
105
|
+
const exit = yield* run([
|
|
106
|
+
"--thread",
|
|
107
|
+
"thread-1",
|
|
108
|
+
"term-1",
|
|
109
|
+
"--for",
|
|
110
|
+
"closed",
|
|
111
|
+
"--format",
|
|
112
|
+
"json",
|
|
113
|
+
]).pipe(Effect.exit);
|
|
114
|
+
assert.isTrue(Exit.isSuccess(exit));
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
t.effect("uses exited error without getTerminal when terminal is already missing", () =>
|
|
119
|
+
Effect.gen(function* () {
|
|
120
|
+
const run = Command.runWith(waitTerminalCommand, { version: "0.0.0-test" });
|
|
121
|
+
const exit = yield* run([
|
|
122
|
+
"--thread",
|
|
123
|
+
"thread-1",
|
|
124
|
+
"term-1",
|
|
125
|
+
"--for",
|
|
126
|
+
"exited",
|
|
127
|
+
"--format",
|
|
128
|
+
"json",
|
|
129
|
+
]).pipe(Effect.exit);
|
|
130
|
+
assert.isTrue(Exit.isFailure(exit));
|
|
131
|
+
if (!Exit.isFailure(exit)) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const error = Cause.findErrorOption(exit.cause);
|
|
135
|
+
assert.isTrue(Option.isSome(error));
|
|
136
|
+
if (Option.isSome(error)) {
|
|
137
|
+
assert.instanceOf(error.value, TerminalCliError);
|
|
138
|
+
assert.equal(
|
|
139
|
+
error.value.message,
|
|
140
|
+
"terminal closed before an exited event was observed: term-1",
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Option from "effect/Option";
|
|
3
|
+
import * as Stream from "effect/Stream";
|
|
4
|
+
import type { TerminalMetadataStreamEvent, TerminalSummary } from "#t3tools/contracts";
|
|
5
|
+
import { Argument, Command, Flag } from "effect/unstable/cli";
|
|
6
|
+
|
|
7
|
+
import { T3Application } from "../../application/service.ts";
|
|
8
|
+
import { Environment } from "../../environment/service.ts";
|
|
9
|
+
import { formatFlag, threadFlag } from "../flags.ts";
|
|
10
|
+
import { resolveOutputFormat } from "../output-format.ts";
|
|
11
|
+
import { T3Output } from "../output/service.ts";
|
|
12
|
+
import { TerminalCliError } from "./error.ts";
|
|
13
|
+
import { requireCommandThreadId } from "./scope.ts";
|
|
14
|
+
|
|
15
|
+
const terminalWaitTargetChoices = ["exited", "closed", "ended"] as const;
|
|
16
|
+
type TerminalWaitTarget = (typeof terminalWaitTargetChoices)[number];
|
|
17
|
+
|
|
18
|
+
type WaitResult =
|
|
19
|
+
| {
|
|
20
|
+
readonly threadId: string;
|
|
21
|
+
readonly terminalId: string;
|
|
22
|
+
readonly target: TerminalWaitTarget;
|
|
23
|
+
readonly status: string;
|
|
24
|
+
readonly exitCode?: number | null;
|
|
25
|
+
readonly exitSignal?: number | null;
|
|
26
|
+
readonly updatedAt?: string;
|
|
27
|
+
readonly sequence?: number;
|
|
28
|
+
readonly alreadySatisfied: boolean;
|
|
29
|
+
readonly missingTreatedAsClosed?: true;
|
|
30
|
+
}
|
|
31
|
+
| {
|
|
32
|
+
readonly threadId: string;
|
|
33
|
+
readonly terminalId: string;
|
|
34
|
+
readonly target: TerminalWaitTarget;
|
|
35
|
+
readonly status: "exited" | "closed";
|
|
36
|
+
readonly exitCode?: number | null;
|
|
37
|
+
readonly exitSignal?: number | null;
|
|
38
|
+
readonly sequence?: number;
|
|
39
|
+
readonly alreadySatisfied: false;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type WaitResolution =
|
|
43
|
+
| { readonly kind: "result"; readonly value: WaitResult }
|
|
44
|
+
| { readonly kind: "fail"; readonly error: TerminalCliError };
|
|
45
|
+
|
|
46
|
+
export const waitTerminalCommand = Command.make(
|
|
47
|
+
"wait",
|
|
48
|
+
{
|
|
49
|
+
thread: threadFlag,
|
|
50
|
+
terminalId: Argument.string("terminal-id"),
|
|
51
|
+
target: Flag.choice("for", terminalWaitTargetChoices).pipe(Flag.withDefault("exited")),
|
|
52
|
+
format: formatFlag,
|
|
53
|
+
},
|
|
54
|
+
({ thread, terminalId, target, format }) =>
|
|
55
|
+
Effect.gen(function* () {
|
|
56
|
+
const application = yield* T3Application;
|
|
57
|
+
const environment = yield* Environment;
|
|
58
|
+
const output = yield* T3Output;
|
|
59
|
+
const threadId = yield* requireCommandThreadId({
|
|
60
|
+
thread,
|
|
61
|
+
env: environment.env,
|
|
62
|
+
});
|
|
63
|
+
const resolvedFormat = resolveOutputFormat(format, environment, "json");
|
|
64
|
+
|
|
65
|
+
const resolution = yield* application.watchTerminalMetadata().pipe(
|
|
66
|
+
Stream.map((event) => resolveMetadataWait(event, target, threadId, terminalId)),
|
|
67
|
+
Stream.filter((value): value is WaitResolution => value !== null),
|
|
68
|
+
Stream.runHead,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if (Option.isNone(resolution)) {
|
|
72
|
+
yield* Effect.fail(
|
|
73
|
+
new TerminalCliError({
|
|
74
|
+
message: "terminal wait stream ended unexpectedly",
|
|
75
|
+
threadId,
|
|
76
|
+
terminalId,
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
} else if (resolution.value.kind === "fail") {
|
|
80
|
+
yield* Effect.fail(resolution.value.error);
|
|
81
|
+
} else {
|
|
82
|
+
const result = resolution.value.value;
|
|
83
|
+
if (resolvedFormat === "json") {
|
|
84
|
+
yield* output.printJson(result);
|
|
85
|
+
} else {
|
|
86
|
+
yield* output.printInfo(formatWaitHuman(result));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}),
|
|
90
|
+
).pipe(Command.withDescription("wait for terminal lifecycle events"));
|
|
91
|
+
|
|
92
|
+
function terminalClosedBeforeExitError(threadId: string, terminalId: string) {
|
|
93
|
+
return new TerminalCliError({
|
|
94
|
+
message: `terminal closed before an exited event was observed: ${terminalId}`,
|
|
95
|
+
threadId,
|
|
96
|
+
terminalId,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function resolveMetadataWait(
|
|
101
|
+
event: TerminalMetadataStreamEvent,
|
|
102
|
+
target: TerminalWaitTarget,
|
|
103
|
+
threadId: string,
|
|
104
|
+
terminalId: string,
|
|
105
|
+
): WaitResolution | null {
|
|
106
|
+
if (event.type === "snapshot") {
|
|
107
|
+
const terminal = event.terminals.find(
|
|
108
|
+
(candidate) => candidate.threadId === threadId && candidate.terminalId === terminalId,
|
|
109
|
+
);
|
|
110
|
+
if (terminal !== undefined) {
|
|
111
|
+
if (targetSatisfiedBySummary(target, terminal)) {
|
|
112
|
+
return {
|
|
113
|
+
kind: "result",
|
|
114
|
+
value: {
|
|
115
|
+
threadId: terminal.threadId,
|
|
116
|
+
terminalId: terminal.terminalId,
|
|
117
|
+
target,
|
|
118
|
+
status: terminal.status,
|
|
119
|
+
exitCode: terminal.exitCode,
|
|
120
|
+
exitSignal: terminal.exitSignal,
|
|
121
|
+
updatedAt: terminal.updatedAt,
|
|
122
|
+
alreadySatisfied: true,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
if (target === "closed" || target === "ended") {
|
|
129
|
+
return {
|
|
130
|
+
kind: "result",
|
|
131
|
+
value: {
|
|
132
|
+
threadId,
|
|
133
|
+
terminalId,
|
|
134
|
+
target,
|
|
135
|
+
status: "closed",
|
|
136
|
+
alreadySatisfied: true,
|
|
137
|
+
missingTreatedAsClosed: true,
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
kind: "fail",
|
|
143
|
+
error: terminalClosedBeforeExitError(threadId, terminalId),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (event.type === "upsert") {
|
|
148
|
+
if (event.terminal.threadId !== threadId || event.terminal.terminalId !== terminalId) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
if ((target === "exited" || target === "ended") && event.terminal.status === "exited") {
|
|
152
|
+
return {
|
|
153
|
+
kind: "result",
|
|
154
|
+
value: {
|
|
155
|
+
threadId,
|
|
156
|
+
terminalId,
|
|
157
|
+
target,
|
|
158
|
+
status: "exited",
|
|
159
|
+
exitCode: event.terminal.exitCode,
|
|
160
|
+
exitSignal: event.terminal.exitSignal,
|
|
161
|
+
alreadySatisfied: false,
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (event.threadId !== threadId || event.terminalId !== terminalId) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (target === "exited") {
|
|
173
|
+
return {
|
|
174
|
+
kind: "fail",
|
|
175
|
+
error: terminalClosedBeforeExitError(threadId, terminalId),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (target === "closed" || target === "ended") {
|
|
180
|
+
return {
|
|
181
|
+
kind: "result",
|
|
182
|
+
value: {
|
|
183
|
+
threadId,
|
|
184
|
+
terminalId,
|
|
185
|
+
target,
|
|
186
|
+
status: "closed",
|
|
187
|
+
alreadySatisfied: false,
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function targetSatisfiedBySummary(target: TerminalWaitTarget, terminal: TerminalSummary) {
|
|
196
|
+
if (target === "exited") {
|
|
197
|
+
return terminal.status === "exited";
|
|
198
|
+
}
|
|
199
|
+
if (target === "closed") {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
return terminal.status === "exited";
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function formatWaitHuman(input: {
|
|
206
|
+
readonly threadId: string;
|
|
207
|
+
readonly terminalId: string;
|
|
208
|
+
readonly status: string;
|
|
209
|
+
readonly alreadySatisfied: boolean;
|
|
210
|
+
readonly exitCode?: number | null;
|
|
211
|
+
readonly exitSignal?: number | null;
|
|
212
|
+
}) {
|
|
213
|
+
const details = [
|
|
214
|
+
typeof input.exitCode === "number" ? `code ${input.exitCode}` : null,
|
|
215
|
+
typeof input.exitSignal === "number" ? `signal ${input.exitSignal}` : null,
|
|
216
|
+
]
|
|
217
|
+
.filter((value): value is string => value !== null)
|
|
218
|
+
.join(", ");
|
|
219
|
+
return details.length > 0
|
|
220
|
+
? `terminal ${input.status}: ${input.terminalId} (${input.threadId}) ${details}`
|
|
221
|
+
: `terminal ${input.status}: ${input.terminalId} (${input.threadId})`;
|
|
222
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
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 { formatTerminalWrittenHuman } from "../terminal-format.ts";
|
|
6
|
+
import { T3Application } from "../../application/service.ts";
|
|
7
|
+
import { Environment } from "../../environment/service.ts";
|
|
8
|
+
import { InvalidFlagCombinationError } from "../error.ts";
|
|
9
|
+
import { T3Input } from "../input/service.ts";
|
|
10
|
+
import { formatFlag, threadFlag } from "../flags.ts";
|
|
11
|
+
import { resolveOutputFormat } from "../output-format.ts";
|
|
12
|
+
import { T3Output } from "../output/service.ts";
|
|
13
|
+
import { decodeBase64Payload, decodeHexPayload } from "./encoding.ts";
|
|
14
|
+
import { TerminalCliError } from "./error.ts";
|
|
15
|
+
import { requireCommandThreadId } from "./scope.ts";
|
|
16
|
+
|
|
17
|
+
export const writeTerminalCommand = Command.make(
|
|
18
|
+
"write",
|
|
19
|
+
{
|
|
20
|
+
thread: threadFlag,
|
|
21
|
+
terminalId: Argument.string("terminal-id"),
|
|
22
|
+
data: Argument.string("data").pipe(Argument.optional),
|
|
23
|
+
stdin: Flag.boolean("stdin"),
|
|
24
|
+
hex: Flag.string("hex").pipe(Flag.optional),
|
|
25
|
+
base64: Flag.string("base64").pipe(Flag.optional),
|
|
26
|
+
quiet: Flag.boolean("quiet"),
|
|
27
|
+
format: formatFlag,
|
|
28
|
+
},
|
|
29
|
+
({ thread, terminalId, data, stdin, hex, base64, quiet, format }) =>
|
|
30
|
+
Effect.gen(function* () {
|
|
31
|
+
const output = yield* T3Output;
|
|
32
|
+
const inputService = yield* T3Input;
|
|
33
|
+
const application = yield* T3Application;
|
|
34
|
+
const environment = yield* Environment;
|
|
35
|
+
const threadId = yield* requireCommandThreadId({
|
|
36
|
+
thread,
|
|
37
|
+
env: environment.env,
|
|
38
|
+
});
|
|
39
|
+
const argumentData = Option.getOrUndefined(data);
|
|
40
|
+
const hexData = Option.getOrUndefined(hex);
|
|
41
|
+
const base64Data = Option.getOrUndefined(base64);
|
|
42
|
+
const inputCount = [
|
|
43
|
+
stdin,
|
|
44
|
+
argumentData !== undefined,
|
|
45
|
+
hexData !== undefined,
|
|
46
|
+
base64Data !== undefined,
|
|
47
|
+
].filter(Boolean).length;
|
|
48
|
+
|
|
49
|
+
if (inputCount !== 1) {
|
|
50
|
+
yield* Effect.fail(
|
|
51
|
+
new InvalidFlagCombinationError({
|
|
52
|
+
message: "provide exactly one of <data>, --stdin, --hex <data>, or --base64 <data>",
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const payload =
|
|
58
|
+
hexData !== undefined
|
|
59
|
+
? yield* decodeHexPayloadOrFail(hexData, threadId, terminalId)
|
|
60
|
+
: base64Data !== undefined
|
|
61
|
+
? yield* decodeBase64PayloadOrFail(base64Data, threadId, terminalId)
|
|
62
|
+
: stdin
|
|
63
|
+
? yield* inputService.readStdinBinary()
|
|
64
|
+
: argumentData!;
|
|
65
|
+
if (payload.length === 0) {
|
|
66
|
+
yield* Effect.fail(
|
|
67
|
+
new TerminalCliError({
|
|
68
|
+
message: "terminal write payload is empty",
|
|
69
|
+
threadId,
|
|
70
|
+
terminalId,
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
yield* application.writeTerminal({
|
|
76
|
+
terminal: {
|
|
77
|
+
threadId,
|
|
78
|
+
terminalId,
|
|
79
|
+
},
|
|
80
|
+
data: payload,
|
|
81
|
+
});
|
|
82
|
+
if (quiet) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const bytes = Buffer.byteLength(payload, "latin1");
|
|
86
|
+
const resolvedFormat = resolveOutputFormat(format, environment, "json");
|
|
87
|
+
if (resolvedFormat === "json") {
|
|
88
|
+
yield* output.printJson({
|
|
89
|
+
threadId,
|
|
90
|
+
terminalId,
|
|
91
|
+
bytes,
|
|
92
|
+
encoding: hexData !== undefined ? "hex" : base64Data !== undefined ? "base64" : "latin1",
|
|
93
|
+
});
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
yield* output.printInfo(
|
|
97
|
+
formatTerminalWrittenHuman({
|
|
98
|
+
terminalId,
|
|
99
|
+
threadId,
|
|
100
|
+
bytes,
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
}),
|
|
104
|
+
).pipe(Command.withDescription("write raw data to a terminal"));
|
|
105
|
+
|
|
106
|
+
function decodeHexPayloadOrFail(value: string, threadId: string, terminalId: string) {
|
|
107
|
+
const decoded = decodeHexPayload(value);
|
|
108
|
+
if (decoded === undefined) {
|
|
109
|
+
return Effect.fail(
|
|
110
|
+
new TerminalCliError({
|
|
111
|
+
message: "invalid hex payload",
|
|
112
|
+
threadId,
|
|
113
|
+
terminalId,
|
|
114
|
+
}),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
return Effect.succeed(decoded);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function decodeBase64PayloadOrFail(value: string, threadId: string, terminalId: string) {
|
|
121
|
+
const decoded = decodeBase64Payload(value);
|
|
122
|
+
if (decoded === undefined) {
|
|
123
|
+
return Effect.fail(
|
|
124
|
+
new TerminalCliError({
|
|
125
|
+
message: "invalid base64 payload",
|
|
126
|
+
threadId,
|
|
127
|
+
terminalId,
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
return Effect.succeed(decoded);
|
|
132
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type { TerminalSessionSnapshot, TerminalSummary } from "#t3tools/contracts";
|
|
2
|
+
|
|
3
|
+
type TerminalRow = {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly status: string;
|
|
6
|
+
readonly label: string;
|
|
7
|
+
readonly cwd: string;
|
|
8
|
+
readonly updated: string;
|
|
9
|
+
readonly process: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function formatTerminalListHuman(terminals: ReadonlyArray<TerminalSummary>) {
|
|
13
|
+
if (terminals.length === 0) {
|
|
14
|
+
return "no terminals\n";
|
|
15
|
+
}
|
|
16
|
+
return `${formatTerminalTable(terminals.map(toSummaryRow))}\n`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function formatTerminalCreatedHuman(snapshot: TerminalSessionSnapshot) {
|
|
20
|
+
return `terminal created\n${formatTerminalTable([toSnapshotRow(snapshot)])}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function formatTerminalWrittenHuman(input: {
|
|
24
|
+
readonly terminalId: string;
|
|
25
|
+
readonly threadId: string;
|
|
26
|
+
readonly bytes: number;
|
|
27
|
+
}) {
|
|
28
|
+
return `wrote ${input.bytes} bytes to ${input.terminalId} (${input.threadId})`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function formatTerminalDestroyedHuman(input: {
|
|
32
|
+
readonly terminalId: string;
|
|
33
|
+
readonly threadId: string;
|
|
34
|
+
}) {
|
|
35
|
+
return `destroyed ${input.terminalId} (${input.threadId})`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function formatTerminalTable(rows: ReadonlyArray<TerminalRow>) {
|
|
39
|
+
const headers = {
|
|
40
|
+
id: "id",
|
|
41
|
+
status: "status",
|
|
42
|
+
label: "label",
|
|
43
|
+
cwd: "cwd",
|
|
44
|
+
updated: "updated",
|
|
45
|
+
process: "process",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const widths = {
|
|
49
|
+
id: columnWidth(
|
|
50
|
+
headers.id,
|
|
51
|
+
rows.map((row) => row.id),
|
|
52
|
+
44,
|
|
53
|
+
),
|
|
54
|
+
status: columnWidth(
|
|
55
|
+
headers.status,
|
|
56
|
+
rows.map((row) => row.status),
|
|
57
|
+
8,
|
|
58
|
+
),
|
|
59
|
+
label: columnWidth(
|
|
60
|
+
headers.label,
|
|
61
|
+
rows.map((row) => row.label),
|
|
62
|
+
24,
|
|
63
|
+
),
|
|
64
|
+
cwd: columnWidth(
|
|
65
|
+
headers.cwd,
|
|
66
|
+
rows.map((row) => row.cwd),
|
|
67
|
+
40,
|
|
68
|
+
),
|
|
69
|
+
updated: columnWidth(
|
|
70
|
+
headers.updated,
|
|
71
|
+
rows.map((row) => row.updated),
|
|
72
|
+
24,
|
|
73
|
+
),
|
|
74
|
+
process: columnWidth(
|
|
75
|
+
headers.process,
|
|
76
|
+
rows.map((row) => row.process),
|
|
77
|
+
20,
|
|
78
|
+
),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const header = [
|
|
82
|
+
pad(headers.id, widths.id),
|
|
83
|
+
pad(headers.status, widths.status),
|
|
84
|
+
pad(headers.label, widths.label),
|
|
85
|
+
pad(headers.cwd, widths.cwd),
|
|
86
|
+
pad(headers.updated, widths.updated),
|
|
87
|
+
pad(headers.process, widths.process),
|
|
88
|
+
].join(" ");
|
|
89
|
+
|
|
90
|
+
const divider = [
|
|
91
|
+
"-".repeat(widths.id),
|
|
92
|
+
"-".repeat(widths.status),
|
|
93
|
+
"-".repeat(widths.label),
|
|
94
|
+
"-".repeat(widths.cwd),
|
|
95
|
+
"-".repeat(widths.updated),
|
|
96
|
+
"-".repeat(widths.process),
|
|
97
|
+
].join(" ");
|
|
98
|
+
|
|
99
|
+
const body = rows
|
|
100
|
+
.map((row) =>
|
|
101
|
+
[
|
|
102
|
+
pad(row.id, widths.id),
|
|
103
|
+
pad(row.status, widths.status),
|
|
104
|
+
pad(row.label, widths.label),
|
|
105
|
+
pad(row.cwd, widths.cwd),
|
|
106
|
+
pad(row.updated, widths.updated),
|
|
107
|
+
pad(row.process, widths.process),
|
|
108
|
+
].join(" "),
|
|
109
|
+
)
|
|
110
|
+
.join("\n");
|
|
111
|
+
|
|
112
|
+
return `${header}\n${divider}\n${body}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function toSummaryRow(terminal: TerminalSummary): TerminalRow {
|
|
116
|
+
return {
|
|
117
|
+
id: terminal.terminalId,
|
|
118
|
+
status: terminal.status,
|
|
119
|
+
label: terminal.label,
|
|
120
|
+
cwd: terminal.cwd,
|
|
121
|
+
updated: terminal.updatedAt,
|
|
122
|
+
process: formatProcessInfo(terminal),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function toSnapshotRow(snapshot: TerminalSessionSnapshot): TerminalRow {
|
|
127
|
+
return {
|
|
128
|
+
id: snapshot.terminalId,
|
|
129
|
+
status: snapshot.status,
|
|
130
|
+
label: snapshot.label,
|
|
131
|
+
cwd: snapshot.cwd,
|
|
132
|
+
updated: snapshot.updatedAt,
|
|
133
|
+
process: formatProcessInfo(snapshot),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function formatProcessInfo(input: {
|
|
138
|
+
readonly pid: number | null;
|
|
139
|
+
readonly exitCode: number | null;
|
|
140
|
+
readonly exitSignal: number | null;
|
|
141
|
+
}) {
|
|
142
|
+
const parts = [
|
|
143
|
+
typeof input.pid === "number" ? `pid ${input.pid}` : null,
|
|
144
|
+
typeof input.exitCode === "number" ? `exit ${input.exitCode}` : null,
|
|
145
|
+
typeof input.exitSignal === "number" ? `sig ${input.exitSignal}` : null,
|
|
146
|
+
].filter((part): part is string => part !== null);
|
|
147
|
+
return parts.length > 0 ? parts.join(", ") : "-";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function columnWidth(header: string, values: ReadonlyArray<string>, maxWidth: number) {
|
|
151
|
+
const widest = values.reduce((width, value) => Math.max(width, value.length), header.length);
|
|
152
|
+
return Math.min(widest, maxWidth);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function pad(value: string, width: number) {
|
|
156
|
+
return truncate(value, width).padEnd(width, " ");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function truncate(value: string, width: number) {
|
|
160
|
+
if (value.length <= width) {
|
|
161
|
+
return value;
|
|
162
|
+
}
|
|
163
|
+
if (width <= 3) {
|
|
164
|
+
return value.slice(0, width);
|
|
165
|
+
}
|
|
166
|
+
return `${value.slice(0, width - 3)}...`;
|
|
167
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Command } from "effect/unstable/cli";
|
|
2
|
+
|
|
3
|
+
import { attachTerminalCommand } from "./terminal/attach.ts";
|
|
4
|
+
import { createTerminalCommand } from "./terminal/create.ts";
|
|
5
|
+
import { destroyTerminalCommand } from "./terminal/destroy.ts";
|
|
6
|
+
import { listTerminalCommand } from "./terminal/list.ts";
|
|
7
|
+
import { readTerminalCommand } from "./terminal/read.ts";
|
|
8
|
+
import { streamTerminalCommand } from "./terminal/stream.ts";
|
|
9
|
+
import { waitTerminalCommand } from "./terminal/wait.ts";
|
|
10
|
+
import { writeTerminalCommand } from "./terminal/write.ts";
|
|
11
|
+
|
|
12
|
+
export function createTerminalCommandGroup() {
|
|
13
|
+
return Command.make("terminal").pipe(
|
|
14
|
+
Command.withDescription("terminal commands"),
|
|
15
|
+
Command.withSubcommands([
|
|
16
|
+
listTerminalCommand,
|
|
17
|
+
createTerminalCommand,
|
|
18
|
+
attachTerminalCommand,
|
|
19
|
+
readTerminalCommand,
|
|
20
|
+
streamTerminalCommand,
|
|
21
|
+
waitTerminalCommand,
|
|
22
|
+
writeTerminalCommand,
|
|
23
|
+
destroyTerminalCommand,
|
|
24
|
+
]),
|
|
25
|
+
);
|
|
26
|
+
}
|