Skip to content

Commit 7a6e13c

Browse files
committed
refactor: array-based error-recovery middleware system in bin.ts
Replace nested executeWithAutoAuth/executeWithSeerTrialPrompt wrappers with a composable ErrorMiddleware type and an array of middlewares. - ErrorMiddleware type: (next, args) => Promise<void> - buildExecutor() composes middlewares innermost-first - Easy to add new middlewares by appending to errorMiddlewares array Also: - Use Object.hasOwn() instead of `in` for TRIAL_NAMES lookup - Fix getDaysRemaining to use same end-of-day UTC as getTrialStatus
1 parent b9647b5 commit 7a6e13c

File tree

2 files changed

+77
-38
lines changed

2 files changed

+77
-38
lines changed

src/bin.ts

Lines changed: 74 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -30,32 +30,38 @@ function handleStreamError(err: NodeJS.ErrnoException): void {
3030
process.stdout.on("error", handleStreamError);
3131
process.stderr.on("error", handleStreamError);
3232

33-
/** Run CLI command with telemetry wrapper */
34-
async function runCommand(args: string[]): Promise<void> {
35-
await withTelemetry(async (span) =>
36-
run(app, args, buildContext(process, span))
37-
);
38-
}
39-
4033
/**
41-
* Execute command with automatic Seer trial prompt.
34+
* Error-recovery middleware for the CLI.
35+
*
36+
* Each middleware wraps command execution and may intercept specific errors
37+
* to perform recovery actions (e.g., login, start trial) then retry.
4238
*
43-
* If the command fails with a trial-eligible SeerError in an interactive TTY,
44-
* checks for available trial, prompts user, starts trial, and retries.
39+
* Middlewares are applied innermost-first: the last middleware in the array
40+
* wraps the outermost layer, so it gets first crack at errors. This means
41+
* auth recovery (outermost) can catch errors from both the command AND
42+
* the trial prompt retry.
4543
*
46-
* Shows a brief context message (not the full error format with URLs) before
47-
* the trial prompt. If the trial isn't available or the user declines, the
48-
* full error is re-thrown so the outer handler in main() displays it normally.
44+
* @param next - The next function in the chain (command or inner middleware)
45+
* @param args - CLI arguments for retry
46+
* @returns A function with the same signature, with error recovery added
47+
*/
48+
type ErrorMiddleware = (
49+
next: (argv: string[]) => Promise<void>,
50+
args: string[]
51+
) => Promise<void>;
52+
53+
/**
54+
* Seer trial prompt middleware.
4955
*
50-
* @throws Re-throws the original error when trial is unavailable or declined
56+
* Catches trial-eligible SeerErrors and offers to start a free trial.
57+
* On success, retries the original command. On failure/decline, re-throws
58+
* so the outer error handler displays the full error with upgrade URL.
5159
*/
52-
async function executeWithSeerTrialPrompt(args: string[]): Promise<void> {
60+
const seerTrialMiddleware: ErrorMiddleware = async (next, args) => {
5361
try {
54-
await runCommand(args);
62+
await next(args);
5563
} catch (err) {
56-
// isTrialEligible handles instanceof SeerError check + reason + orgSlug + TTY
5764
if (isTrialEligible(err)) {
58-
// isTrialEligible narrows err to SeerError with defined orgSlug
5965
const started = await promptAndStartTrial(
6066
// biome-ignore lint/style/noNonNullAssertion: isTrialEligible guarantees orgSlug is defined
6167
err.orgSlug!,
@@ -64,31 +70,25 @@ async function executeWithSeerTrialPrompt(args: string[]): Promise<void> {
6470

6571
if (started) {
6672
process.stderr.write("\nRetrying command...\n\n");
67-
await runCommand(args);
73+
await next(args);
6874
return;
6975
}
70-
71-
// Trial not started (unavailable, declined, or failed) — re-throw
72-
// so the outer error handler in main() displays the full error
73-
// with the upgrade/settings URL
7476
}
7577
throw err;
7678
}
77-
}
79+
};
7880

7981
/**
80-
* Execute command with automatic authentication.
81-
*
82-
* If the command fails due to missing authentication and we're in a TTY,
83-
* automatically run the interactive login flow and retry the command.
82+
* Auto-authentication middleware.
8483
*
85-
* @throws Re-throws any non-authentication errors from the command
84+
* Catches auth errors (not_authenticated, expired) in interactive TTYs
85+
* and runs the login flow. On success, retries through the full middleware
86+
* chain so inner middlewares (e.g., trial prompt) also apply to the retry.
8687
*/
87-
async function executeWithAutoAuth(args: string[]): Promise<void> {
88+
const autoAuthMiddleware: ErrorMiddleware = async (next, args) => {
8889
try {
89-
await executeWithSeerTrialPrompt(args);
90+
await next(args);
9091
} catch (err) {
91-
// Auto-login for auth errors in interactive TTY environments
9292
// Use isatty(0) for reliable stdin TTY detection (process.stdin.isTTY can be undefined in Bun)
9393
// Errors can opt-out via skipAutoAuth (e.g., auth status command)
9494
if (
@@ -111,21 +111,58 @@ async function executeWithAutoAuth(args: string[]): Promise<void> {
111111

112112
if (loginSuccess) {
113113
process.stderr.write("\nRetrying command...\n\n");
114-
await executeWithSeerTrialPrompt(args);
114+
await next(args);
115115
return;
116116
}
117117

118-
// Login failed or was cancelled - set exit code and return
119-
// (don't call process.exit() directly to allow finally blocks to run)
118+
// Login failed or was cancelled
120119
process.exitCode = 1;
121120
return;
122121
}
123122

124-
// Re-throw non-auth errors to be handled by main
125123
throw err;
126124
}
125+
};
126+
127+
/**
128+
* Error-recovery middlewares applied around command execution.
129+
*
130+
* Order matters: applied innermost-first, so the last entry wraps the
131+
* outermost layer. Auth middleware is outermost so it catches errors
132+
* from both the command and any inner middleware retries.
133+
*
134+
* To add a new middleware, append it to this array.
135+
*/
136+
const errorMiddlewares: ErrorMiddleware[] = [
137+
seerTrialMiddleware,
138+
autoAuthMiddleware,
139+
];
140+
141+
/** Run CLI command with telemetry wrapper */
142+
async function runCommand(args: string[]): Promise<void> {
143+
await withTelemetry(async (span) =>
144+
run(app, args, buildContext(process, span))
145+
);
146+
}
147+
148+
/**
149+
* Build the command executor by composing error-recovery middlewares.
150+
*
151+
* Wraps `runCommand` with each middleware in order (innermost-first),
152+
* producing a single function that handles all error recovery.
153+
*/
154+
function buildExecutor(): (args: string[]) => Promise<void> {
155+
let executor = runCommand;
156+
for (const mw of errorMiddlewares) {
157+
const next = executor;
158+
executor = (args) => mw(next, args);
159+
}
160+
return executor;
127161
}
128162

163+
/** Command executor with all error-recovery middlewares applied */
164+
const executeCommand = buildExecutor();
165+
129166
async function main(): Promise<void> {
130167
// Clean up old binary from previous Windows upgrade (no-op if file doesn't exist)
131168
startCleanupOldBinary();
@@ -148,7 +185,7 @@ async function main(): Promise<void> {
148185
}
149186

150187
try {
151-
await executeWithAutoAuth(args);
188+
await executeCommand(args);
152189
} catch (err) {
153190
process.stderr.write(`${error("Error:")} ${formatError(err)}\n`);
154191
process.exitCode = getExitCode(err);

src/lib/trials.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ export function getDaysRemaining(trial: ProductTrial): number | null {
165165
}
166166

167167
const end = new Date(trial.endDate);
168+
// Match getTrialStatus: treat endDate as end-of-day UTC
169+
end.setUTCHours(23, 59, 59, 999);
168170
const now = new Date();
169171
const diffMs = end.getTime() - now.getTime();
170172
const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
@@ -187,5 +189,5 @@ export function getValidTrialNames(): string[] {
187189
* @returns true if it's a valid trial name
188190
*/
189191
export function isTrialName(value: string): boolean {
190-
return value in TRIAL_NAMES;
192+
return Object.hasOwn(TRIAL_NAMES, value);
191193
}

0 commit comments

Comments
 (0)