t3code-cli 0.10.0 → 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 +33 -1
- package/dist/application.js +2 -2
- package/dist/auth.js +1 -1
- package/dist/bin.js +1566 -351
- 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 +1740 -767
- 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 +2 -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 +8 -0
- 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/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.ts +14 -0
- 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,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
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import "vite-plus/test/config";
|
|
2
|
+
|
|
3
|
+
import { assert, describe, it } from "@effect/vitest";
|
|
4
|
+
import { fromPartial } from "@total-typescript/shoehorn";
|
|
5
|
+
import type { OrchestrationThreadShell } from "#t3tools/contracts";
|
|
6
|
+
|
|
7
|
+
import { formatThreadsHuman } from "./thread-format.ts";
|
|
8
|
+
|
|
9
|
+
describe("formatThreadsHuman", () => {
|
|
10
|
+
it("appends (archived) for archived threads", () => {
|
|
11
|
+
const threads: OrchestrationThreadShell[] = [
|
|
12
|
+
fromPartial({
|
|
13
|
+
id: "active-1",
|
|
14
|
+
title: "Active thread",
|
|
15
|
+
archivedAt: null,
|
|
16
|
+
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
17
|
+
session: null,
|
|
18
|
+
}),
|
|
19
|
+
fromPartial({
|
|
20
|
+
id: "archived-1",
|
|
21
|
+
title: "Archived thread",
|
|
22
|
+
archivedAt: "2026-01-02T00:00:00.000Z",
|
|
23
|
+
updatedAt: "2026-01-02T00:00:00.000Z",
|
|
24
|
+
session: null,
|
|
25
|
+
}),
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const output = formatThreadsHuman(threads);
|
|
29
|
+
assert.include(output, "- Active thread\n");
|
|
30
|
+
assert.include(output, "- Archived thread (archived)\n");
|
|
31
|
+
});
|
|
32
|
+
});
|
package/src/cli/thread-format.ts
CHANGED
|
@@ -53,11 +53,18 @@ export function formatThreadsHuman(threads: ReadonlyArray<OrchestrationThreadShe
|
|
|
53
53
|
return threads
|
|
54
54
|
.map(
|
|
55
55
|
(thread) =>
|
|
56
|
-
`- ${thread.title}\n id: ${thread.id}\n status: ${threadStatus(thread)}\n updated: ${thread.updatedAt}\n`,
|
|
56
|
+
`- ${thread.title}${thread.archivedAt !== null ? " (archived)" : ""}\n id: ${thread.id}\n status: ${threadStatus(thread)}\n updated: ${thread.updatedAt}\n`,
|
|
57
57
|
)
|
|
58
58
|
.join("");
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
export function formatThreadDeletedHuman(input: {
|
|
62
|
+
readonly threadId: string;
|
|
63
|
+
readonly dispatch: { readonly sequence: number };
|
|
64
|
+
}) {
|
|
65
|
+
return `thread deleted: ${input.threadId}\nsequence: ${input.dispatch.sequence}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
61
68
|
export function formatThreadStartedHuman(input: {
|
|
62
69
|
readonly thread: OrchestrationThread;
|
|
63
70
|
readonly sequence: number;
|
package/src/cli/thread.ts
CHANGED
|
@@ -3,7 +3,11 @@ import { Command } from "effect/unstable/cli";
|
|
|
3
3
|
import { approveThreadCommand } from "./threads/approve.ts";
|
|
4
4
|
import { archiveThreadCommand } from "./threads/archive.ts";
|
|
5
5
|
import { callbackThreadCommand } from "./threads/callback.ts";
|
|
6
|
+
import { deleteThreadCommand } from "./threads/delete.ts";
|
|
7
|
+
import { interruptThreadCommand } from "./threads/interrupt.ts";
|
|
6
8
|
import { respondThreadCommand } from "./threads/respond.ts";
|
|
9
|
+
import { unarchiveThreadCommand } from "./threads/unarchive.ts";
|
|
10
|
+
import { updateThreadCommand } from "./threads/update.ts";
|
|
7
11
|
|
|
8
12
|
export function createThreadCommand() {
|
|
9
13
|
return Command.make("thread").pipe(
|
|
@@ -12,6 +16,10 @@ export function createThreadCommand() {
|
|
|
12
16
|
approveThreadCommand,
|
|
13
17
|
respondThreadCommand,
|
|
14
18
|
archiveThreadCommand,
|
|
19
|
+
interruptThreadCommand,
|
|
20
|
+
unarchiveThreadCommand,
|
|
21
|
+
updateThreadCommand,
|
|
22
|
+
deleteThreadCommand,
|
|
15
23
|
callbackThreadCommand,
|
|
16
24
|
]),
|
|
17
25
|
);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import * as Effect from "effect/Effect";
|
|
2
2
|
import * as Option from "effect/Option";
|
|
3
|
-
import { Command
|
|
3
|
+
import { Command } from "effect/unstable/cli";
|
|
4
4
|
|
|
5
|
-
import { formatFlag, threadFlag } from "../flags.ts";
|
|
6
|
-
import { MissingThreadError
|
|
5
|
+
import { formatFlag, selfActionForceFlag, threadFlag } from "../flags.ts";
|
|
6
|
+
import { MissingThreadError } from "../error.ts";
|
|
7
|
+
import { requireSelfActionConfirmation } from "../self-action.ts";
|
|
7
8
|
import { resolveThreadId } from "../../scope/index.ts";
|
|
8
9
|
import { T3Application } from "../../application/service.ts";
|
|
9
10
|
import { Environment } from "../../environment/service.ts";
|
|
@@ -14,7 +15,7 @@ export const archiveThreadCommand = Command.make(
|
|
|
14
15
|
"archive",
|
|
15
16
|
{
|
|
16
17
|
thread: threadFlag,
|
|
17
|
-
force:
|
|
18
|
+
force: selfActionForceFlag,
|
|
18
19
|
format: formatFlag,
|
|
19
20
|
},
|
|
20
21
|
({ thread, force, format }) =>
|
|
@@ -33,20 +34,12 @@ export const archiveThreadCommand = Command.make(
|
|
|
33
34
|
}),
|
|
34
35
|
);
|
|
35
36
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
) {
|
|
43
|
-
return yield* Effect.fail(
|
|
44
|
-
new SelfArchiveError({
|
|
45
|
-
threadId,
|
|
46
|
-
message: `cannot archive thread ${threadId}: matches T3CODE_THREAD_ID (are you trying to archive yourself?). Use --force to override.`,
|
|
47
|
-
}),
|
|
48
|
-
);
|
|
49
|
-
}
|
|
37
|
+
yield* requireSelfActionConfirmation({
|
|
38
|
+
threadId,
|
|
39
|
+
force,
|
|
40
|
+
environment,
|
|
41
|
+
action: "archive",
|
|
42
|
+
});
|
|
50
43
|
const resolvedFormat = resolveOutputFormat(format, environment, "json");
|
|
51
44
|
const dispatch = yield* application.archiveThread(threadId);
|
|
52
45
|
if (resolvedFormat === "json") {
|