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.
Files changed (124) hide show
  1. package/README.md +33 -1
  2. package/dist/application.js +2 -2
  3. package/dist/auth.js +1 -1
  4. package/dist/bin.js +1566 -351
  5. package/dist/cli.js +1 -1
  6. package/dist/config.js +1 -1
  7. package/dist/connection.js +1 -11
  8. package/dist/index.js +1 -1
  9. package/dist/layout.js +1 -1
  10. package/dist/node.js +2 -0
  11. package/dist/orchestration.js +2 -2
  12. package/dist/rpc.js +2 -2
  13. package/dist/runtime.js +1 -1
  14. package/dist/scope.js +1 -1
  15. package/dist/shared.js +1740 -767
  16. package/dist/src/application/index.d.ts +3 -2
  17. package/dist/src/application/layer.d.ts +82 -841
  18. package/dist/src/application/model-selection.d.ts +15 -0
  19. package/dist/src/application/models.d.ts +1 -87
  20. package/dist/src/application/project-commands.d.ts +9 -0
  21. package/dist/src/application/projects.d.ts +13 -135
  22. package/dist/src/application/service.d.ts +84 -5
  23. package/dist/src/application/shell-sequence.d.ts +2 -12
  24. package/dist/src/application/terminals.d.ts +66 -0
  25. package/dist/src/application/thread-commands.d.ts +47 -0
  26. package/dist/src/application/thread-update.d.ts +8 -0
  27. package/dist/src/application/thread-wait.d.ts +3 -14
  28. package/dist/src/application/threads.d.ts +21 -50
  29. package/dist/src/cli/flags.d.ts +3 -0
  30. package/dist/src/cli/output-format.d.ts +2 -0
  31. package/dist/src/domain/error.d.ts +14 -1
  32. package/dist/src/domain/helpers.d.ts +0 -9
  33. package/dist/src/domain/model-config.d.ts +29 -3
  34. package/dist/src/domain/thread-lifecycle.d.ts +2 -10
  35. package/dist/src/node/index.d.ts +2 -0
  36. package/dist/src/orchestration/index.d.ts +1 -0
  37. package/dist/src/orchestration/layer.d.ts +177 -1032
  38. package/dist/src/orchestration/service.d.ts +1 -0
  39. package/dist/src/rpc/error.d.ts +4 -2
  40. package/dist/src/rpc/index.d.ts +5 -0
  41. package/dist/src/rpc/layer.d.ts +1097 -499
  42. package/dist/src/rpc/operation.d.ts +5752 -0
  43. package/dist/src/rpc/ws-group.d.ts +327 -147
  44. package/dist/src/runtime/layer.d.ts +1 -1
  45. package/dist/t3tools.js +2 -2
  46. package/dist/upstream-t3code/packages/contracts/src/environmentHttp.d.ts +10 -33
  47. package/dist/upstream-t3code/packages/contracts/src/ipc.d.ts +0 -6
  48. package/dist/upstream-t3code/packages/contracts/src/orchestration.d.ts +1 -657
  49. package/dist/upstream-t3code/packages/contracts/src/provider.d.ts +0 -14
  50. package/dist/upstream-t3code/packages/contracts/src/providerRuntime.d.ts +1 -175
  51. package/dist/upstream-t3code/packages/contracts/src/rpc.d.ts +0 -492
  52. package/dist/upstream-t3code/packages/contracts/src/server.d.ts +0 -45
  53. package/dist/upstream-t3code/packages/contracts/src/settings.d.ts +0 -21
  54. package/dist/upstream-t3code/packages/contracts/src/terminal.d.ts +3 -6
  55. package/package.json +6 -2
  56. package/src/application/index.ts +28 -2
  57. package/src/application/layer.ts +34 -5
  58. package/src/application/model-selection.ts +68 -2
  59. package/src/application/models.ts +14 -14
  60. package/src/application/project-commands.ts +15 -0
  61. package/src/application/projects.ts +43 -13
  62. package/src/application/service.ts +196 -77
  63. package/src/application/shell-sequence.ts +5 -7
  64. package/src/application/terminals.ts +207 -0
  65. package/src/application/thread-commands.test.ts +43 -0
  66. package/src/application/thread-commands.ts +81 -0
  67. package/src/application/thread-update.ts +72 -0
  68. package/src/application/thread-wait.ts +46 -47
  69. package/src/application/threads.test.ts +166 -0
  70. package/src/application/threads.ts +123 -40
  71. package/src/bin.ts +2 -0
  72. package/src/cli/app.ts +2 -0
  73. package/src/cli/confirm.ts +31 -0
  74. package/src/cli/error.ts +26 -3
  75. package/src/cli/flags.ts +15 -0
  76. package/src/cli/input/layer.ts +20 -0
  77. package/src/cli/input/service.ts +1 -0
  78. package/src/cli/output-format.ts +6 -2
  79. package/src/cli/project-format.ts +7 -0
  80. package/src/cli/project.ts +2 -1
  81. package/src/cli/projects/delete.ts +50 -0
  82. package/src/cli/self-action.ts +34 -0
  83. package/src/cli/terminal/attach.ts +32 -0
  84. package/src/cli/terminal/commands.test.ts +101 -0
  85. package/src/cli/terminal/create.ts +54 -0
  86. package/src/cli/terminal/destroy.ts +60 -0
  87. package/src/cli/terminal/encoding.test.ts +63 -0
  88. package/src/cli/terminal/encoding.ts +23 -0
  89. package/src/cli/terminal/error.ts +14 -0
  90. package/src/cli/terminal/io-node-layer.ts +82 -0
  91. package/src/cli/terminal/io-service.ts +25 -0
  92. package/src/cli/terminal/list.ts +35 -0
  93. package/src/cli/terminal/read.ts +102 -0
  94. package/src/cli/terminal/scope.ts +30 -0
  95. package/src/cli/terminal/shared.ts +250 -0
  96. package/src/cli/terminal/stream.ts +64 -0
  97. package/src/cli/terminal/wait.test.ts +146 -0
  98. package/src/cli/terminal/wait.ts +222 -0
  99. package/src/cli/terminal/write.ts +132 -0
  100. package/src/cli/terminal-format.ts +167 -0
  101. package/src/cli/terminal.ts +26 -0
  102. package/src/cli/thread-format.test.ts +32 -0
  103. package/src/cli/thread-format.ts +8 -1
  104. package/src/cli/thread.ts +8 -0
  105. package/src/cli/threads/archive.ts +11 -18
  106. package/src/cli/threads/delete.ts +61 -0
  107. package/src/cli/threads/interrupt.ts +52 -0
  108. package/src/cli/threads/list.test.ts +69 -0
  109. package/src/cli/threads/list.ts +15 -3
  110. package/src/cli/threads/send.ts +22 -2
  111. package/src/cli/threads/unarchive.ts +44 -0
  112. package/src/cli/threads/update.ts +150 -0
  113. package/src/domain/error.ts +20 -1
  114. package/src/domain/model-config.ts +4 -0
  115. package/src/domain/thread-lifecycle.ts +14 -0
  116. package/src/node/index.ts +2 -0
  117. package/src/orchestration/index.ts +1 -0
  118. package/src/orchestration/layer.ts +33 -85
  119. package/src/orchestration/service.ts +4 -0
  120. package/src/rpc/error.ts +10 -0
  121. package/src/rpc/index.ts +5 -0
  122. package/src/rpc/operation.ts +83 -0
  123. package/src/rpc/ws-group.ts +24 -0
  124. 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
+ });
@@ -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, Flag } from "effect/unstable/cli";
3
+ import { Command } from "effect/unstable/cli";
4
4
 
5
- import { formatFlag, threadFlag } from "../flags.ts";
6
- import { MissingThreadError, SelfArchiveError } from "../error.ts";
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: Flag.boolean("force").pipe(Flag.withAlias("f")),
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
- const envThreadId = environment.env.T3CODE_THREAD_ID;
37
- if (
38
- envThreadId !== undefined &&
39
- envThreadId.length > 0 &&
40
- threadId === envThreadId &&
41
- !force
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") {