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,31 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Prompt from "effect/unstable/cli/Prompt";
|
|
3
|
+
|
|
4
|
+
import { DestructiveConfirmationRequiredError } from "./error.ts";
|
|
5
|
+
import { isInteractiveHumanTerminal } from "./output-format.ts";
|
|
6
|
+
import type { EnvironmentShape } from "../environment/service.ts";
|
|
7
|
+
|
|
8
|
+
export const requireDestructiveConfirmation = Effect.fn("requireDestructiveConfirmation")(
|
|
9
|
+
function* (input: {
|
|
10
|
+
readonly message: string;
|
|
11
|
+
readonly yes: boolean;
|
|
12
|
+
readonly environment: EnvironmentShape;
|
|
13
|
+
}) {
|
|
14
|
+
if (input.yes) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (!isInteractiveHumanTerminal(input.environment)) {
|
|
18
|
+
yield* Effect.fail(
|
|
19
|
+
new DestructiveConfirmationRequiredError({
|
|
20
|
+
message: "destructive action requires --yes in non-interactive mode",
|
|
21
|
+
}),
|
|
22
|
+
);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const confirmed = yield* Prompt.run(Prompt.confirm({ message: input.message, initial: false }));
|
|
26
|
+
if (confirmed) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
yield* Effect.fail(new DestructiveConfirmationRequiredError({ message: "aborted" }));
|
|
30
|
+
},
|
|
31
|
+
);
|
package/src/cli/error.ts
CHANGED
|
@@ -22,11 +22,20 @@ export class MissingThreadError extends Schema.TaggedErrorClass<MissingThreadErr
|
|
|
22
22
|
},
|
|
23
23
|
) {}
|
|
24
24
|
|
|
25
|
-
export class
|
|
26
|
-
|
|
25
|
+
export class SelfActionError extends Schema.TaggedErrorClass<SelfActionError>()("SelfActionError", {
|
|
26
|
+
message: Schema.String,
|
|
27
|
+
threadId: Schema.String,
|
|
28
|
+
}) {}
|
|
29
|
+
|
|
30
|
+
export class DestructiveConfirmationRequiredError extends Schema.TaggedErrorClass<DestructiveConfirmationRequiredError>()(
|
|
31
|
+
"DestructiveConfirmationRequiredError",
|
|
32
|
+
{ message: Schema.String },
|
|
33
|
+
) {}
|
|
34
|
+
|
|
35
|
+
export class InvalidFlagCombinationError extends Schema.TaggedErrorClass<InvalidFlagCombinationError>()(
|
|
36
|
+
"InvalidFlagCombinationError",
|
|
27
37
|
{
|
|
28
38
|
message: Schema.String,
|
|
29
|
-
threadId: Schema.String,
|
|
30
39
|
},
|
|
31
40
|
) {}
|
|
32
41
|
|
|
@@ -36,3 +45,17 @@ export class MissingRequestError extends Schema.TaggedErrorClass<MissingRequestE
|
|
|
36
45
|
message: Schema.String,
|
|
37
46
|
},
|
|
38
47
|
) {}
|
|
48
|
+
|
|
49
|
+
export class MissingUpdateFieldsError extends Schema.TaggedErrorClass<MissingUpdateFieldsError>()(
|
|
50
|
+
"MissingUpdateFieldsError",
|
|
51
|
+
{
|
|
52
|
+
message: Schema.String,
|
|
53
|
+
},
|
|
54
|
+
) {}
|
|
55
|
+
|
|
56
|
+
export class ConflictingUpdateFlagsError extends Schema.TaggedErrorClass<ConflictingUpdateFlagsError>()(
|
|
57
|
+
"ConflictingUpdateFlagsError",
|
|
58
|
+
{
|
|
59
|
+
message: Schema.String,
|
|
60
|
+
},
|
|
61
|
+
) {}
|
package/src/cli/flags.ts
CHANGED
|
@@ -30,6 +30,21 @@ export const projectPathFlag = Flag.string("path").pipe(
|
|
|
30
30
|
Flag.withDefault("."),
|
|
31
31
|
);
|
|
32
32
|
|
|
33
|
+
export const yesFlag = Flag.boolean("yes").pipe(
|
|
34
|
+
Flag.withDescription("Skip interactive confirmation"),
|
|
35
|
+
Flag.withDefault(false),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
export const forceFlag = Flag.boolean("force").pipe(
|
|
39
|
+
Flag.withDescription("Delete non-empty project (cascade thread deletes)"),
|
|
40
|
+
Flag.withDefault(false),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
export const selfActionForceFlag = Flag.boolean("force").pipe(
|
|
44
|
+
Flag.withDescription("Confirm an action targeting the calling agent thread"),
|
|
45
|
+
Flag.withAlias("f"),
|
|
46
|
+
);
|
|
47
|
+
|
|
33
48
|
export const formatFlag = Flag.choice("format", humanJsonFormatChoices).pipe(
|
|
34
49
|
Flag.withDefault("auto"),
|
|
35
50
|
);
|
package/src/cli/input/layer.ts
CHANGED
|
@@ -23,8 +23,28 @@ export const makeT3Input = Effect.fn("makeT3Input")(function* () {
|
|
|
23
23
|
);
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
+
const readStdinBinary = Effect.fn("T3InputLive.readStdinBinary")(function* () {
|
|
27
|
+
return yield* stdio.stdin.pipe(
|
|
28
|
+
Stream.runFold(
|
|
29
|
+
() => new Uint8Array(0),
|
|
30
|
+
(acc, chunk) => {
|
|
31
|
+
const merged = new Uint8Array(acc.length + chunk.length);
|
|
32
|
+
merged.set(acc);
|
|
33
|
+
merged.set(chunk, acc.length);
|
|
34
|
+
return merged;
|
|
35
|
+
},
|
|
36
|
+
),
|
|
37
|
+
Effect.map((bytes) => Buffer.from(bytes).toString("latin1")),
|
|
38
|
+
Effect.catchTags({
|
|
39
|
+
PlatformError: (error) =>
|
|
40
|
+
Effect.fail(new InputError({ message: "failed to read stdin", cause: error })),
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
26
45
|
return {
|
|
27
46
|
readStdin,
|
|
47
|
+
readStdinBinary,
|
|
28
48
|
};
|
|
29
49
|
});
|
|
30
50
|
|
package/src/cli/input/service.ts
CHANGED
package/src/cli/output-format.ts
CHANGED
|
@@ -25,13 +25,17 @@ export function canRenderLiveTerminal(environment: EnvironmentShape) {
|
|
|
25
25
|
);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
function
|
|
28
|
+
export function isInteractiveHumanTerminal(environment: EnvironmentShape) {
|
|
29
29
|
return (
|
|
30
30
|
environment.stdoutIsTTY && !isAgentEnvironment(environment) && environment.env.TERM !== "dumb"
|
|
31
31
|
);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
function
|
|
34
|
+
function isHumanTerminal(environment: EnvironmentShape) {
|
|
35
|
+
return isInteractiveHumanTerminal(environment);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isAgentEnvironment(environment: EnvironmentShape) {
|
|
35
39
|
return (
|
|
36
40
|
environment.env.CI !== undefined ||
|
|
37
41
|
environment.env.CODEX_CI !== undefined ||
|
|
@@ -9,3 +9,10 @@ export function formatProjectsHuman(projects: ReadonlyArray<OrchestrationProject
|
|
|
9
9
|
export function formatProjectAddedHuman(project: OrchestrationProjectShell) {
|
|
10
10
|
return `project added: ${project.title}\nid: ${project.id}\npath: ${project.workspaceRoot}`;
|
|
11
11
|
}
|
|
12
|
+
|
|
13
|
+
export function formatProjectDeletedHuman(input: {
|
|
14
|
+
readonly projectId: string;
|
|
15
|
+
readonly dispatch: { readonly sequence: number };
|
|
16
|
+
}) {
|
|
17
|
+
return `project deleted: ${input.projectId}\nsequence: ${input.dispatch.sequence}`;
|
|
18
|
+
}
|
package/src/cli/project.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { Command, Flag } from "effect/unstable/cli";
|
|
|
4
4
|
|
|
5
5
|
import { formatFlag, projectPathFlag } from "./flags.ts";
|
|
6
6
|
import { formatProjectAddedHuman, formatProjectsHuman } from "./project-format.ts";
|
|
7
|
+
import { deleteProjectCommand } from "./projects/delete.ts";
|
|
7
8
|
import { T3Application } from "../application/service.ts";
|
|
8
9
|
import { Environment } from "../environment/service.ts";
|
|
9
10
|
import { resolveOutputFormat } from "./output-format.ts";
|
|
@@ -12,7 +13,7 @@ import { T3Output } from "./output/service.ts";
|
|
|
12
13
|
export function createProjectCommand() {
|
|
13
14
|
return Command.make("project").pipe(
|
|
14
15
|
Command.withDescription("project commands"),
|
|
15
|
-
Command.withSubcommands([listCommand, addCommand]),
|
|
16
|
+
Command.withSubcommands([listCommand, addCommand, deleteProjectCommand]),
|
|
16
17
|
);
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import { Command } from "effect/unstable/cli";
|
|
3
|
+
|
|
4
|
+
import { requireDestructiveConfirmation } from "../confirm.ts";
|
|
5
|
+
import { forceFlag, formatFlag, projectFlag, yesFlag } from "../flags.ts";
|
|
6
|
+
import { formatProjectDeletedHuman } from "../project-format.ts";
|
|
7
|
+
import { requireCommandProjectRef } from "../require.ts";
|
|
8
|
+
import { T3Application } from "../../application/service.ts";
|
|
9
|
+
import { Environment } from "../../environment/service.ts";
|
|
10
|
+
import { resolveOutputFormat } from "../output-format.ts";
|
|
11
|
+
import { T3Output } from "../output/service.ts";
|
|
12
|
+
|
|
13
|
+
export const deleteProjectCommand = Command.make(
|
|
14
|
+
"delete",
|
|
15
|
+
{
|
|
16
|
+
project: projectFlag,
|
|
17
|
+
force: forceFlag,
|
|
18
|
+
yes: yesFlag,
|
|
19
|
+
format: formatFlag,
|
|
20
|
+
},
|
|
21
|
+
({ project, force, yes, format }) =>
|
|
22
|
+
Effect.gen(function* () {
|
|
23
|
+
const application = yield* T3Application;
|
|
24
|
+
const environment = yield* Environment;
|
|
25
|
+
const output = yield* T3Output;
|
|
26
|
+
const projectRef = yield* requireCommandProjectRef({
|
|
27
|
+
project,
|
|
28
|
+
env: environment.env,
|
|
29
|
+
cwd: environment.cwd,
|
|
30
|
+
});
|
|
31
|
+
const resolvedProject = yield* application.resolveProject(projectRef);
|
|
32
|
+
yield* requireDestructiveConfirmation({
|
|
33
|
+
message: `Delete project ${resolvedProject.title} (${resolvedProject.id})?`,
|
|
34
|
+
yes,
|
|
35
|
+
environment,
|
|
36
|
+
});
|
|
37
|
+
const resolvedFormat = resolveOutputFormat(format, environment, "json");
|
|
38
|
+
const result = yield* application.deleteProject({
|
|
39
|
+
projectId: resolvedProject.id,
|
|
40
|
+
...(force ? { force: true } : {}),
|
|
41
|
+
});
|
|
42
|
+
if (resolvedFormat === "json") {
|
|
43
|
+
return yield* output.printJson({
|
|
44
|
+
projectId: result.projectId,
|
|
45
|
+
dispatch: result.dispatch,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return yield* output.printInfo(formatProjectDeletedHuman(result));
|
|
49
|
+
}),
|
|
50
|
+
).pipe(Command.withDescription("delete project"));
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
|
|
3
|
+
import { SelfActionError } from "./error.ts";
|
|
4
|
+
import { isAgentEnvironment } from "./output-format.ts";
|
|
5
|
+
import type { EnvironmentShape } from "../environment/service.ts";
|
|
6
|
+
|
|
7
|
+
export const requireSelfActionConfirmation = Effect.fn("requireSelfActionConfirmation")(
|
|
8
|
+
function* (input: {
|
|
9
|
+
readonly threadId: string;
|
|
10
|
+
readonly force: boolean;
|
|
11
|
+
readonly environment: EnvironmentShape;
|
|
12
|
+
readonly action: string;
|
|
13
|
+
}) {
|
|
14
|
+
if (input.force) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (!isAgentEnvironment(input.environment)) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const callerThreadId = input.environment.env.T3CODE_THREAD_ID;
|
|
21
|
+
if (callerThreadId === undefined || callerThreadId.length === 0) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (input.threadId !== callerThreadId) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
yield* Effect.fail(
|
|
28
|
+
new SelfActionError({
|
|
29
|
+
threadId: input.threadId,
|
|
30
|
+
message: `cannot ${input.action} thread ${input.threadId}: command targets the calling thread. Pass --force to confirm.`,
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
},
|
|
34
|
+
);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import { Argument, Command } from "effect/unstable/cli";
|
|
3
|
+
|
|
4
|
+
import { T3Application } from "../../application/service.ts";
|
|
5
|
+
import { Environment } from "../../environment/service.ts";
|
|
6
|
+
import { threadFlag } from "../flags.ts";
|
|
7
|
+
import { requireCommandThreadId } from "./scope.ts";
|
|
8
|
+
import { runAttachedTerminalSession, toTerminalAttachTarget } from "./shared.ts";
|
|
9
|
+
|
|
10
|
+
export const attachTerminalCommand = Command.make(
|
|
11
|
+
"attach",
|
|
12
|
+
{
|
|
13
|
+
thread: threadFlag,
|
|
14
|
+
terminalId: Argument.string("terminal-id"),
|
|
15
|
+
},
|
|
16
|
+
({ thread, terminalId }) =>
|
|
17
|
+
Effect.gen(function* () {
|
|
18
|
+
const application = yield* T3Application;
|
|
19
|
+
const environment = yield* Environment;
|
|
20
|
+
const threadId = yield* requireCommandThreadId({
|
|
21
|
+
thread,
|
|
22
|
+
env: environment.env,
|
|
23
|
+
});
|
|
24
|
+
const terminal = yield* application.getTerminal({
|
|
25
|
+
threadId,
|
|
26
|
+
terminalId,
|
|
27
|
+
});
|
|
28
|
+
yield* runAttachedTerminalSession({
|
|
29
|
+
terminal: toTerminalAttachTarget(terminal),
|
|
30
|
+
});
|
|
31
|
+
}),
|
|
32
|
+
).pipe(Command.withDescription("attach to a terminal"));
|
|
@@ -0,0 +1,101 @@
|
|
|
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 { assert, describe, it } from "@effect/vitest";
|
|
10
|
+
import { Command } from "effect/unstable/cli";
|
|
11
|
+
import { fromPartial } from "@total-typescript/shoehorn";
|
|
12
|
+
|
|
13
|
+
import { T3Application } from "../../application/service.ts";
|
|
14
|
+
import { T3Config } from "../../config/service.ts";
|
|
15
|
+
import { NodeEnvironmentLive } from "../../environment/layer.ts";
|
|
16
|
+
import { InvalidFlagCombinationError } from "../error.ts";
|
|
17
|
+
import { T3Input } from "../input/service.ts";
|
|
18
|
+
import { T3Output } from "../output/service.ts";
|
|
19
|
+
import { readTerminalCommand } from "./read.ts";
|
|
20
|
+
import { writeTerminalCommand } from "./write.ts";
|
|
21
|
+
|
|
22
|
+
const testLayer = Layer.mergeAll(
|
|
23
|
+
Layer.succeed(
|
|
24
|
+
T3Application,
|
|
25
|
+
fromPartial({
|
|
26
|
+
getTerminal: () => Effect.die("getTerminal should not be called"),
|
|
27
|
+
attachTerminal: () => Effect.die("attachTerminal should not be called"),
|
|
28
|
+
writeTerminal: () => Effect.die("writeTerminal should not be called"),
|
|
29
|
+
}),
|
|
30
|
+
),
|
|
31
|
+
Layer.succeed(
|
|
32
|
+
T3Config,
|
|
33
|
+
fromPartial({
|
|
34
|
+
resolve: () =>
|
|
35
|
+
Effect.succeed({
|
|
36
|
+
url: "ws://localhost",
|
|
37
|
+
token: "token",
|
|
38
|
+
source: "config",
|
|
39
|
+
local: true,
|
|
40
|
+
}),
|
|
41
|
+
}),
|
|
42
|
+
),
|
|
43
|
+
Layer.succeed(T3Output, {
|
|
44
|
+
writeStdout: () => Effect.void,
|
|
45
|
+
writeStderr: () => Effect.void,
|
|
46
|
+
printJson: () => Effect.void,
|
|
47
|
+
printNdjson: () => Effect.void,
|
|
48
|
+
printInfo: () => Effect.void,
|
|
49
|
+
}),
|
|
50
|
+
Layer.succeed(T3Input, {
|
|
51
|
+
readStdin: () => Effect.die("readStdin should not be called"),
|
|
52
|
+
readStdinBinary: () => Effect.die("readStdinBinary should not be called"),
|
|
53
|
+
}),
|
|
54
|
+
NodeServices.layer,
|
|
55
|
+
NodeEnvironmentLive,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
describe("readTerminalCommand", () => {
|
|
59
|
+
it.layer(testLayer)("readTerminalCommand", (t) => {
|
|
60
|
+
t.effect("rejects --from-sequence without --follow", () =>
|
|
61
|
+
Effect.gen(function* () {
|
|
62
|
+
const run = Command.runWith(readTerminalCommand, { version: "0.0.0-test" });
|
|
63
|
+
const exit = yield* run(["--thread", "thread-1", "term-1", "--from-sequence", "1"]).pipe(
|
|
64
|
+
Effect.exit,
|
|
65
|
+
);
|
|
66
|
+
assert.isTrue(Exit.isFailure(exit));
|
|
67
|
+
if (!Exit.isFailure(exit)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const error = Cause.findErrorOption(exit.cause);
|
|
71
|
+
assert.isTrue(Option.isSome(error));
|
|
72
|
+
if (Option.isSome(error)) {
|
|
73
|
+
assert.instanceOf(error.value, InvalidFlagCombinationError);
|
|
74
|
+
assert.equal(error.value.message, "--from-sequence requires --follow");
|
|
75
|
+
}
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("writeTerminalCommand", () => {
|
|
82
|
+
it.layer(testLayer)("writeTerminalCommand", (t) => {
|
|
83
|
+
t.effect("rejects multiple payload sources", () =>
|
|
84
|
+
Effect.gen(function* () {
|
|
85
|
+
const run = Command.runWith(writeTerminalCommand, { version: "0.0.0-test" });
|
|
86
|
+
const exit = yield* run(["--thread", "thread-1", "term-1", "hello", "--stdin"]).pipe(
|
|
87
|
+
Effect.exit,
|
|
88
|
+
);
|
|
89
|
+
assert.isTrue(Exit.isFailure(exit));
|
|
90
|
+
if (!Exit.isFailure(exit)) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const error = Cause.findErrorOption(exit.cause);
|
|
94
|
+
assert.isTrue(Option.isSome(error));
|
|
95
|
+
if (Option.isSome(error)) {
|
|
96
|
+
assert.instanceOf(error.value, InvalidFlagCombinationError);
|
|
97
|
+
}
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
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 { formatTerminalCreatedHuman } from "../terminal-format.ts";
|
|
6
|
+
import { T3Application } from "../../application/service.ts";
|
|
7
|
+
import { Environment } from "../../environment/service.ts";
|
|
8
|
+
import { formatFlag, threadFlag } from "../flags.ts";
|
|
9
|
+
import { resolveOutputFormat } from "../output-format.ts";
|
|
10
|
+
import { T3Output } from "../output/service.ts";
|
|
11
|
+
import { requireCommandThreadId } from "./scope.ts";
|
|
12
|
+
import { runAttachedTerminalSession, snapshotToTerminalAttachTarget } from "./shared.ts";
|
|
13
|
+
|
|
14
|
+
export const createTerminalCommand = Command.make(
|
|
15
|
+
"create",
|
|
16
|
+
{
|
|
17
|
+
thread: threadFlag,
|
|
18
|
+
command: Argument.string("command").pipe(Argument.optional),
|
|
19
|
+
id: Flag.string("id").pipe(Flag.optional),
|
|
20
|
+
attach: Flag.boolean("attach"),
|
|
21
|
+
format: formatFlag,
|
|
22
|
+
},
|
|
23
|
+
({ thread, command, id, attach, format }) =>
|
|
24
|
+
Effect.gen(function* () {
|
|
25
|
+
const application = yield* T3Application;
|
|
26
|
+
const environment = yield* Environment;
|
|
27
|
+
const output = yield* T3Output;
|
|
28
|
+
const threadId = yield* requireCommandThreadId({
|
|
29
|
+
thread,
|
|
30
|
+
env: environment.env,
|
|
31
|
+
});
|
|
32
|
+
const terminalId = Option.getOrUndefined(id);
|
|
33
|
+
const commandValue = Option.getOrUndefined(command);
|
|
34
|
+
const snapshot = yield* application.createTerminal({
|
|
35
|
+
threadId,
|
|
36
|
+
...(terminalId !== undefined ? { terminalId } : {}),
|
|
37
|
+
...(commandValue !== undefined ? { command: commandValue } : {}),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (attach) {
|
|
41
|
+
yield* runAttachedTerminalSession({
|
|
42
|
+
terminal: snapshotToTerminalAttachTarget(snapshot),
|
|
43
|
+
});
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const resolvedFormat = resolveOutputFormat(format, environment, "json");
|
|
48
|
+
if (resolvedFormat === "json") {
|
|
49
|
+
yield* output.printJson(snapshot);
|
|
50
|
+
} else {
|
|
51
|
+
yield* output.printInfo(formatTerminalCreatedHuman(snapshot));
|
|
52
|
+
}
|
|
53
|
+
}),
|
|
54
|
+
).pipe(Command.withDescription("create a terminal for a thread"));
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import { Argument, Command, Flag } from "effect/unstable/cli";
|
|
3
|
+
|
|
4
|
+
import { requireDestructiveConfirmation } from "../confirm.ts";
|
|
5
|
+
import { formatTerminalDestroyedHuman } from "../terminal-format.ts";
|
|
6
|
+
import { T3Application } from "../../application/service.ts";
|
|
7
|
+
import { Environment } from "../../environment/service.ts";
|
|
8
|
+
import { formatFlag, threadFlag, yesFlag } from "../flags.ts";
|
|
9
|
+
import { resolveOutputFormat } from "../output-format.ts";
|
|
10
|
+
import { T3Output } from "../output/service.ts";
|
|
11
|
+
import { requireCommandThreadId } from "./scope.ts";
|
|
12
|
+
|
|
13
|
+
export const destroyTerminalCommand = Command.make(
|
|
14
|
+
"destroy",
|
|
15
|
+
{
|
|
16
|
+
thread: threadFlag,
|
|
17
|
+
terminalId: Argument.string("terminal-id"),
|
|
18
|
+
quiet: Flag.boolean("quiet"),
|
|
19
|
+
yes: yesFlag,
|
|
20
|
+
format: formatFlag,
|
|
21
|
+
},
|
|
22
|
+
({ thread, terminalId, quiet, yes, format }) =>
|
|
23
|
+
Effect.gen(function* () {
|
|
24
|
+
const application = yield* T3Application;
|
|
25
|
+
const environment = yield* Environment;
|
|
26
|
+
const output = yield* T3Output;
|
|
27
|
+
const threadId = yield* requireCommandThreadId({
|
|
28
|
+
thread,
|
|
29
|
+
env: environment.env,
|
|
30
|
+
});
|
|
31
|
+
yield* requireDestructiveConfirmation({
|
|
32
|
+
message: `Destroy terminal ${terminalId} in thread ${threadId} and delete its history?`,
|
|
33
|
+
yes,
|
|
34
|
+
environment,
|
|
35
|
+
});
|
|
36
|
+
const terminal = {
|
|
37
|
+
threadId,
|
|
38
|
+
terminalId,
|
|
39
|
+
};
|
|
40
|
+
yield* application.destroyTerminal(terminal);
|
|
41
|
+
if (quiet) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const resolvedFormat = resolveOutputFormat(format, environment, "json");
|
|
45
|
+
if (resolvedFormat === "json") {
|
|
46
|
+
yield* output.printJson({
|
|
47
|
+
threadId: terminal.threadId,
|
|
48
|
+
terminalId: terminal.terminalId,
|
|
49
|
+
destroyed: true,
|
|
50
|
+
});
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
yield* output.printInfo(
|
|
54
|
+
formatTerminalDestroyedHuman({
|
|
55
|
+
terminalId,
|
|
56
|
+
threadId,
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
}),
|
|
60
|
+
).pipe(Command.withDescription("destroy a terminal and its history"));
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import "vite-plus/test/config";
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vite-plus/test";
|
|
4
|
+
|
|
5
|
+
import { decodeBase64Payload, decodeHexPayload } from "./encoding.ts";
|
|
6
|
+
import { filterAttachStreamEvent } from "./shared.ts";
|
|
7
|
+
|
|
8
|
+
describe("terminal encoding", () => {
|
|
9
|
+
it("preserves non-utf8 bytes in hex payloads", () => {
|
|
10
|
+
expect(decodeHexPayload("ff")).toBe("\xff");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("preserves non-utf8 bytes in base64 payloads", () => {
|
|
14
|
+
expect(decodeBase64Payload("/w==")).toBe("\xff");
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("filterAttachStreamEvent", () => {
|
|
19
|
+
const snapshotEvent = {
|
|
20
|
+
type: "snapshot" as const,
|
|
21
|
+
snapshot: {
|
|
22
|
+
threadId: "thread-1",
|
|
23
|
+
terminalId: "term-1",
|
|
24
|
+
cwd: "/tmp",
|
|
25
|
+
worktreePath: null,
|
|
26
|
+
status: "running" as const,
|
|
27
|
+
pid: 1,
|
|
28
|
+
history: "hello",
|
|
29
|
+
exitCode: null,
|
|
30
|
+
exitSignal: null,
|
|
31
|
+
label: "shell",
|
|
32
|
+
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
33
|
+
sequence: 3,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
it("keeps snapshot history when --history is set with --from-sequence", () => {
|
|
38
|
+
const filtered = filterAttachStreamEvent(snapshotEvent, {
|
|
39
|
+
includeHistory: true,
|
|
40
|
+
fromSequence: 5,
|
|
41
|
+
});
|
|
42
|
+
expect(filtered?.type).toBe("snapshot");
|
|
43
|
+
if (filtered?.type === "snapshot") {
|
|
44
|
+
expect(filtered.snapshot.history).toBe("hello");
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("filters sequenced events inclusively from --from-sequence", () => {
|
|
49
|
+
const outputEvent = {
|
|
50
|
+
type: "output" as const,
|
|
51
|
+
threadId: "thread-1",
|
|
52
|
+
terminalId: "term-1",
|
|
53
|
+
sequence: 5,
|
|
54
|
+
data: "x",
|
|
55
|
+
};
|
|
56
|
+
expect(
|
|
57
|
+
filterAttachStreamEvent(outputEvent, { includeHistory: false, fromSequence: 5 }),
|
|
58
|
+
).toEqual(outputEvent);
|
|
59
|
+
expect(
|
|
60
|
+
filterAttachStreamEvent(outputEvent, { includeHistory: false, fromSequence: 6 }),
|
|
61
|
+
).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function bytesToLatin1String(bytes: Uint8Array): string {
|
|
2
|
+
return Buffer.from(bytes).toString("latin1");
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function decodeHexPayload(value: string): string | undefined {
|
|
6
|
+
const normalized = value.trim();
|
|
7
|
+
if (
|
|
8
|
+
normalized.length === 0 ||
|
|
9
|
+
normalized.length % 2 !== 0 ||
|
|
10
|
+
!/^[0-9a-fA-F]+$/.test(normalized)
|
|
11
|
+
) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
return Buffer.from(normalized, "hex").toString("latin1");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function decodeBase64Payload(value: string): string | undefined {
|
|
18
|
+
const normalized = value.trim();
|
|
19
|
+
if (normalized.length === 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(normalized)) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
return Buffer.from(normalized, "base64").toString("latin1");
|
|
23
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
|
|
3
|
+
export class TerminalCliError extends Schema.TaggedErrorClass<TerminalCliError>()(
|
|
4
|
+
"TerminalCliError",
|
|
5
|
+
{
|
|
6
|
+
message: Schema.String,
|
|
7
|
+
threadId: Schema.optionalKey(Schema.String),
|
|
8
|
+
terminalId: Schema.optionalKey(Schema.String),
|
|
9
|
+
},
|
|
10
|
+
) {}
|
|
11
|
+
|
|
12
|
+
export class TerminalIoError extends Schema.TaggedErrorClass<TerminalIoError>()("TerminalIoError", {
|
|
13
|
+
message: Schema.String,
|
|
14
|
+
}) {}
|