Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
366 changes: 299 additions & 67 deletions src/cli-entry.ts

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion src/cli/commands/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
loadActionInterface,
} from "../schemas/actions/index";
import { EnvelopeWriter } from "../services/envelope-writer";

// ---------------------------------------------------------------------------
// Colocated next_actions
// ---------------------------------------------------------------------------
Expand Down
1 change: 0 additions & 1 deletion src/cli/commands/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import {
} from "../schemas/api/index";
import { CliConfig } from "../services/cli-config";
import { EnvelopeWriter } from "../services/envelope-writer";

const VALID_METHODS: readonly HttpMethod[] = [
"GET",
"POST",
Expand Down
1 change: 0 additions & 1 deletion src/cli/commands/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ import {
import { protectPayload, truncateList } from "../agent/truncation";
import type { NextAction } from "../agent/types";
import { EnvelopeWriter } from "../services/envelope-writer";

// ---------------------------------------------------------------------------
// Helpers (pure, no global state)
// ---------------------------------------------------------------------------
Expand Down
1 change: 0 additions & 1 deletion src/cli/commands/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
import { envGetEffect } from "../../core/environment";
import type { NextAction } from "../agent/types";
import { EnvelopeWriter } from "../services/envelope-writer";

// ---------------------------------------------------------------------------
// Colocated next_actions
// ---------------------------------------------------------------------------
Expand Down
1 change: 0 additions & 1 deletion src/cli/commands/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
} from "../../core/environment";
import type { NextAction } from "../agent/types";
import { EnvelopeWriter } from "../services/envelope-writer";

// ---------------------------------------------------------------------------
// Colocated next_actions
// ---------------------------------------------------------------------------
Expand Down
1 change: 0 additions & 1 deletion src/cli/commands/webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { webhookEventsEffect } from "../../core/webhooks";
import { truncateList } from "../agent/truncation";
import type { NextAction } from "../agent/types";
import { EnvelopeWriter } from "../services/envelope-writer";

// ---------------------------------------------------------------------------
// Colocated next_actions
// ---------------------------------------------------------------------------
Expand Down
4 changes: 4 additions & 0 deletions src/cli/services/cli-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ import type { Environment } from "../../core/environment";
* --env) so that every layer in the dependency graph can access them without
* mutable module-level state.
*/
export type OutputFormat = "json" | "plaintext";

export interface CliConfigShape {
readonly prettyPrint: boolean;
readonly verbosity: number; // 0 = silent, 1 = basic, 2 = full
readonly environmentOverride: Environment | null;
readonly outputFormat: OutputFormat;
}

export class CliConfig extends Context.Tag("CliConfig")<
Expand All @@ -25,6 +28,7 @@ export const defaultCliConfig: CliConfigShape = {
prettyPrint: false,
verbosity: 0,
environmentOverride: null,
outputFormat: "json",
};

/** Build a layer from parsed global flags. */
Expand Down
124 changes: 105 additions & 19 deletions src/cli/services/envelope-writer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,76 @@ import type {
} from "../agent/types";
import { CliConfig } from "./cli-config";

// ---------------------------------------------------------------------------
// Plaintext renderer
// ---------------------------------------------------------------------------

function renderValue(value: unknown, indent = 0): string {
const pad = " ".repeat(indent);
if (value === null || value === undefined) return "";
if (typeof value !== "object") return String(value);
if (Array.isArray(value)) {
if (value.length === 0) return "(none)";
return value
.map((item) => {
const rendered = renderValue(item, indent);
return `${pad}- ${rendered.trimStart()}`;
})
.join("\n");
}
return Object.entries(value as Record<string, unknown>)
.filter(([, v]) => v !== undefined && v !== null)
.map(([k, v]) => {
const label = k.replace(/_/g, " ");
if (typeof v === "object" && v !== null) {
const inner = renderValue(v, indent + 1);
if (!inner) return null;
return `${pad}${label}:\n${inner}`;
}
return `${pad}${label}: ${v}`;
})
.filter(Boolean)
.join("\n");
}

function renderSuccessPlaintext(result: unknown): string {
return renderValue(result);
}

function renderErrorPlaintext(
error: { message: string; code: string },
fix: string,
): string {
return `Error: ${error.message}\nFix: ${fix}`;
}

function renderStreamEventPlaintext(event: StreamEvent): string {
switch (event.type) {
case "start":
return `Running ${event.command}...`;
case "step": {
const prefix =
event.status === "completed"
? "[ok]"
: event.status === "failed"
? "[FAILED]"
: "[ .. ]";
const suffix =
event.status === "failed" && event.message ? `: ${event.message}` : "";
return `${prefix} ${event.name}${suffix}`;
}
case "progress": {
const pct = event.percent !== undefined ? ` ${event.percent}%` : "";
const msg = event.message ? ` ${event.message}` : "";
return `${event.name}:${pct}${msg}`.trim();
}
case "result":
return renderSuccessPlaintext(event.result);
case "error":
return renderErrorPlaintext(event.error, event.fix);
}
}

// ---------------------------------------------------------------------------
// Service interface
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -80,7 +150,9 @@ export const EnvelopeWriterLive: Layer.Layer<EnvelopeWriter, never, CliConfig> =
const config = yield* CliConfig;
const written = yield* Ref.make(false);

function serialize(value: unknown): string {
const plaintext = config.outputFormat === "plaintext";

function serializeJson(value: unknown): string {
return config.prettyPrint
? JSON.stringify(value, null, 2)
: JSON.stringify(value);
Expand All @@ -106,7 +178,11 @@ export const EnvelopeWriterLive: Layer.Layer<EnvelopeWriter, never, CliConfig> =
next_actions: nextActions,
};
if (!alreadyWritten) {
yield* writeLine(serialize(envelope));
yield* writeLine(
plaintext
? renderSuccessPlaintext(result)
: serializeJson(envelope),
);
yield* Ref.set(written, true);
}
return envelope;
Expand All @@ -128,7 +204,11 @@ export const EnvelopeWriterLive: Layer.Layer<EnvelopeWriter, never, CliConfig> =
next_actions: nextActions,
};
if (!alreadyWritten) {
yield* writeLine(serialize(envelope));
yield* writeLine(
plaintext
? renderErrorPlaintext(error, fix)
: serializeJson(envelope),
);
yield* Ref.set(written, true);
yield* Effect.sync(() => {
process.exitCode = 1;
Expand All @@ -138,7 +218,9 @@ export const EnvelopeWriterLive: Layer.Layer<EnvelopeWriter, never, CliConfig> =
});

const emitStreamEvent = (event: StreamEvent): Effect.Effect<void> =>
writeLine(serialize(event));
writeLine(
plaintext ? renderStreamEventPlaintext(event) : serializeJson(event),
);

const emitStreamResult = <T>(
command: string,
Expand All @@ -147,13 +229,15 @@ export const EnvelopeWriterLive: Layer.Layer<EnvelopeWriter, never, CliConfig> =
): Effect.Effect<void> =>
Effect.gen(function* () {
yield* writeLine(
serialize({
type: "result" as const,
ok: true,
command,
result,
next_actions: nextActions,
}),
plaintext
? renderSuccessPlaintext(result)
: serializeJson({
type: "result" as const,
ok: true,
command,
result,
next_actions: nextActions,
}),
);
yield* Ref.set(written, true);
});
Expand All @@ -166,14 +250,16 @@ export const EnvelopeWriterLive: Layer.Layer<EnvelopeWriter, never, CliConfig> =
): Effect.Effect<void> =>
Effect.gen(function* () {
yield* writeLine(
serialize({
type: "error" as const,
ok: false,
command,
error,
fix,
next_actions: nextActions,
}),
plaintext
? renderErrorPlaintext(error, fix)
: serializeJson({
type: "error" as const,
ok: false,
command,
error,
fix,
next_actions: nextActions,
}),
);
yield* Ref.set(written, true);
yield* Effect.sync(() => {
Expand Down
Loading