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
6 changes: 5 additions & 1 deletion config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,11 @@ export function actionHandler<
}
this.configSaved = true;

if (this.doNotCreate && !config) {
// `config` is always a truthy wrapper object; `config.config` is the
// existing config file (if any). When a command opted out of file
// creation, skip writing only when there is no file to update — i.e.
// never create a new deno.jsonc as a side effect.
if (this.doNotCreate && !config.config) {
return Promise.resolve();
}

Expand Down
41 changes: 39 additions & 2 deletions deploy/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Command, ValidationError } from "@cliffy/command";
import { green, red, setColorEnabled, yellow } from "@std/fmt/colors";
import {
error,
ExitCode,
renderTemporalTimestamp,
tablePrinter,
writeJsonResult,
Expand Down Expand Up @@ -124,6 +125,10 @@ const logsCommand = new Command<GlobalContext>()
.option("--end <date:string>", "The ending timestamp of the logs", {
depends: ["start"],
})
.option(
"--once",
"Capture currently-available (backfilled) logs, then exit instead of tailing live. Bounded, non-interactive capture for CI/agents; defaults to the last hour, widen with --start.",
)
.example(
"Stream live logs",
"logs --app my-app",
Expand All @@ -132,7 +137,15 @@ const logsCommand = new Command<GlobalContext>()
"View logs from a specific time",
"logs --app my-app --start '2025-01-01T00:00:00Z'",
)
.example(
"Capture current logs then exit (CI/agents)",
"logs --app my-app --once --json --non-interactive",
)
.action(actionHandler(async (config, options) => {
// `logs` is read-only: like the other inspection commands it must not
// create a deno.jsonc as a side effect (which would also print a non-JSON
// "Created configuration file" line on stdout, breaking --json output).
config.noCreate();
const org = await getOrg(options, config, options.org);
const { app } = await getApp(options, config, false, org, options.app);

Expand All @@ -154,21 +167,40 @@ const logsCommand = new Command<GlobalContext>()
const seenIds = new Set();
let onceConnected = false;

// In --once mode we drain the backfill window and exit at the live boundary
// instead of tailing forever. Default that window to the last hour when no
// explicit --start is given, so the bounded capture actually has logs to
// drain — a bare `new Date()` would request an empty window. An explicit
// --start always wins.
const ONCE_LOOKBACK_MS = 60 * 60 * 1000;
const startDate = options.start
? new Date(options.start)
: options.once
? new Date(Date.now() - ONCE_LOOKBACK_MS)
: new Date();

const encoder = new TextEncoder();
const sub = trpcClient.subscription(
"apps.logs",
{
org,
app,
start: (options.start ? new Date(options.start) : new Date())
.toISOString(),
start: startDate.toISOString(),
end: options.end ? new Date(options.end).toISOString() : undefined,
filter: {},
},
{
onData: (data: unknown) => {
const typedData = data as "streaming" | null | LogEntry[];
if (typedData === "streaming") {
// Backfill complete: the server is about to switch to live tailing.
// In --once mode this boundary is our cue to stop — all currently
// available logs have already been flushed synchronously above, so
// close the subscription and exit cleanly.
if (options.once) {
sub.unsubscribe();
Deno.exit(ExitCode.OK);
}
if (!onceConnected && !options.quiet && !options.json) {
console.log("connected, streaming logs...");
}
Expand Down Expand Up @@ -227,6 +259,11 @@ const logsCommand = new Command<GlobalContext>()
},
onStopped: () => {
sub.unsubscribe();
// A server-completed stream (e.g. a bounded --start/--end window)
// in --once mode is a clean, deterministic end-of-capture.
if (options.once) {
Deno.exit(ExitCode.OK);
}
},
},
);
Expand Down
Loading