Skip to content

Commit a8b24be

Browse files
feat(cli): verbose flag outputs stack traces and error codes (#96)
1 parent 17ac805 commit a8b24be

2 files changed

Lines changed: 135 additions & 3 deletions

File tree

cli/src/lib/command.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,42 @@ export default abstract class EverywhereBaseCommand extends Command {
1515
}),
1616
verbose: Flags.boolean({
1717
char: 'v',
18-
description: 'Show detailed output.',
18+
description: 'Show detailed output including stack traces and error codes.',
1919
}),
2020
};
2121

22+
protected get isVerbose(): boolean {
23+
return this._verbose;
24+
}
25+
2226
protected get pluginDir(): string {
23-
// Resolved during run() after flags are parsed
2427
return this._pluginDir;
2528
}
2629

30+
private _verbose = false;
2731
private _pluginDir = process.cwd();
2832

33+
override async init(): Promise<void> {
34+
await super.init();
35+
const { flags } = await this.parse(this.constructor as typeof EverywhereBaseCommand);
36+
this._verbose = flags.verbose ?? false;
37+
}
38+
39+
override async catch(error: Error & { code?: string; exitCode?: number }): Promise<void> {
40+
if (this._verbose) {
41+
const timestamp = new Date().toISOString();
42+
const stack = error.stack ?? `Error: ${error.message}`;
43+
this.warn(`[${timestamp}]\n${stack}`);
44+
if (error.code) {
45+
this.warn(` code: ${error.code}`);
46+
}
47+
if (error.exitCode !== undefined) {
48+
this.warn(` exit: ${error.exitCode}`);
49+
}
50+
}
51+
return super.catch(error);
52+
}
53+
2954
protected async parsePluginDir(): Promise<string> {
3055
const { flags } = await this.parse(this.constructor as typeof EverywhereBaseCommand);
3156
const dir = flags['plugin-dir'];

cli/tests/lib/command.test.ts

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1-
import { describe, it, expect } from 'vitest';
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
22
import EverywhereBaseCommand from '../../src/lib/command.js';
33

4+
class TestCommand extends EverywhereBaseCommand {
5+
async run(): Promise<void> {}
6+
7+
setVerbose(value: boolean): void {
8+
(this as any)._verbose = value;
9+
}
10+
}
11+
412
describe('EverywhereBaseCommand', () => {
513
describe('baseFlags', () => {
614
it('defines a plugin-dir flag', () => {
@@ -19,4 +27,103 @@ describe('EverywhereBaseCommand', () => {
1927
expect(EverywhereBaseCommand.baseFlags['verbose'].char).toBe('v');
2028
});
2129
});
30+
31+
describe('isVerbose', () => {
32+
describe('when verbose has not been set', () => {
33+
it('returns false', () => {
34+
const cmd = new TestCommand([], {} as any);
35+
expect((cmd as any).isVerbose).toBe(false);
36+
});
37+
});
38+
39+
describe('when verbose has been set', () => {
40+
it('returns true', () => {
41+
const cmd = new TestCommand([], {} as any);
42+
cmd.setVerbose(true);
43+
expect((cmd as any).isVerbose).toBe(true);
44+
});
45+
});
46+
});
47+
48+
describe('catch()', () => {
49+
let cmd: TestCommand;
50+
let warnSpy: ReturnType<typeof vi.spyOn>;
51+
52+
beforeEach(() => {
53+
cmd = new TestCommand([], {} as any);
54+
warnSpy = vi.spyOn(cmd, 'warn').mockImplementation(() => {});
55+
});
56+
57+
describe('when verbose is not set', () => {
58+
it('does not output the stack trace', async () => {
59+
const error = new Error('oops');
60+
error.stack = 'Error: oops\n at src/build.ts:42:5';
61+
try {
62+
await cmd.catch(error);
63+
} catch {
64+
/* oclif re-throws */
65+
}
66+
expect(warnSpy).not.toHaveBeenCalledWith(expect.stringContaining('at src/build.ts'));
67+
});
68+
69+
it('does not output a timestamp', async () => {
70+
const error = new Error('oops');
71+
try {
72+
await cmd.catch(error);
73+
} catch {
74+
/* oclif re-throws */
75+
}
76+
expect(warnSpy).not.toHaveBeenCalledWith(expect.stringMatching(/\d{4}-\d{2}-\d{2}T/));
77+
});
78+
});
79+
80+
describe('when verbose is set', () => {
81+
beforeEach(() => {
82+
cmd.setVerbose(true);
83+
});
84+
85+
it('outputs the stack trace', async () => {
86+
const error = new Error('oops');
87+
error.stack = 'Error: oops\n at src/build.ts:42:5';
88+
try {
89+
await cmd.catch(error);
90+
} catch {
91+
/* oclif re-throws */
92+
}
93+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('at src/build.ts'));
94+
});
95+
96+
it('outputs a timestamp', async () => {
97+
const error = new Error('oops');
98+
try {
99+
await cmd.catch(error);
100+
} catch {
101+
/* oclif re-throws */
102+
}
103+
expect(warnSpy).toHaveBeenCalledWith(expect.stringMatching(/\d{4}-\d{2}-\d{2}T/));
104+
});
105+
106+
it('outputs the error code when present', async () => {
107+
const error = Object.assign(new Error('oops'), { code: 'ENOENT' });
108+
try {
109+
await cmd.catch(error);
110+
} catch {
111+
/* oclif re-throws */
112+
}
113+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('ENOENT'));
114+
});
115+
116+
it('does not output the error cause', async () => {
117+
const cause = { token: 'eyJ0b2tlbg.secret.sig' };
118+
const error = Object.assign(new Error('Request failed'), { cause });
119+
try {
120+
await cmd.catch(error);
121+
} catch {
122+
/* oclif re-throws */
123+
}
124+
const allWarnArgs = warnSpy.mock.calls.flat().join('');
125+
expect(allWarnArgs).not.toContain('eyJ0b2tlbg');
126+
});
127+
});
128+
});
22129
});

0 commit comments

Comments
 (0)