Skip to content

Commit

Permalink
Run commands in vscode terminal with full path to CLI (#146)
Browse files Browse the repository at this point in the history
  • Loading branch information
vcheung-stripe authored Jan 29, 2021
1 parent 5c39d3d commit 873e0f9
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 45 deletions.
4 changes: 3 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ export function activate(this: any, context: ExtensionContext) {

Resource.initialize(context);

const stripeCommands = new Commands(telemetry, new StripeTerminal());
const stripeTerminal = new StripeTerminal(stripeClient);

const stripeCommands = new Commands(telemetry, stripeTerminal);

// Activity bar view
window.createTreeView('stripeDashboardView', {
Expand Down
32 changes: 20 additions & 12 deletions src/stripeClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const fs = require('fs');

export class StripeClient {
telemetry: Telemetry;
cliPath: string | null;
private cliPath: string | null;

constructor(telemetry:Telemetry) {
this.telemetry = telemetry;
Expand Down Expand Up @@ -98,7 +98,25 @@ export class StripeClient {
}
}

async detectInstalled() {
getEvents() {
const events = this.execute('events list');
return events;
}

getResourceById(id: string) {
const resource = this.execute(`get ${id}`);
return resource;
}

async getCLIPath(): Promise<string | null> {
const isInstalled = await this.detectInstalled();
if (!isInstalled) {
return null;
}
return this.cliPath;
}

private async detectInstalled() {
const defaultInstallPath = (() => {
const osType: OSType = getOSType();
switch (osType) {
Expand Down Expand Up @@ -140,16 +158,6 @@ export class StripeClient {
return false;
}

getEvents() {
const events = this.execute('events list');
return events;
}

getResourceById(id: string) {
const resource = this.execute(`get ${id}`);
return resource;
}

private async handleDidChangeConfiguration(e: vscode.ConfigurationChangeEvent) {
const shouldHandleConfigurationChange = e.affectsConfiguration('stripe');
if (shouldHandleConfigurationChange) {
Expand Down
27 changes: 19 additions & 8 deletions src/stripeTerminal.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import * as vscode from 'vscode';
import {OSType, filterAsync, findAsync, getOSType} from './utils';
import {StripeClient} from './stripeClient';
import psList from 'ps-list';

type SupportedStripeCommand = 'events' | 'listen' | 'logs' | 'login' | 'trigger';

export class StripeTerminal {
// eslint-disable-next-line @typescript-eslint/naming-convention
private static KNOWN_LONG_RUNNING_COMMANDS = [
'stripe listen',
'stripe logs tail',
'listen',
'logs tail',
];

private stripeClient: StripeClient;
private terminals: Array<vscode.Terminal>;

constructor() {
constructor(stripeClient: StripeClient) {
this.stripeClient = stripeClient;
this.terminals = [];
vscode.window.onDidCloseTerminal((terminal) => {
terminal.dispose();
Expand All @@ -25,17 +28,22 @@ export class StripeTerminal {
command: SupportedStripeCommand,
args: Array<string> = [],
): Promise<void> {
const cliPath = await this.stripeClient.getCLIPath();
if (!cliPath) {
return;
}

const globalCLIFLags = this.getGlobalCLIFlags();

const commandString = [
'stripe',
cliPath,
command,
...args,
...globalCLIFLags
].join(' ');

const allRunningProcesses = await psList();
const terminal = await this.terminalForCommand(commandString, allRunningProcesses);
const terminal = await this.terminalForCommand(commandString, cliPath, allRunningProcesses);
terminal.sendText(commandString);
terminal.show();
const otherTerminals = this.terminals.filter((t) => t !== terminal);
Expand All @@ -56,8 +64,8 @@ export class StripeTerminal {

private isCommandLongRunning(command: string): boolean {
if (getOSType() === OSType.windows) {
// On Windows we can't get the process command, so always assume terminals running `stripe` are long running
return command.indexOf('stripe') > -1;
// On Windows we can't get the process command, so always assume commands are long-running
return true;
}
return StripeTerminal.KNOWN_LONG_RUNNING_COMMANDS.some((knownCommand) => (
command.indexOf(knownCommand) > -1
Expand Down Expand Up @@ -92,11 +100,14 @@ export class StripeTerminal {

private async terminalForCommand(
command: string,
cliPath: string,
allRunningProcesses: psList.ProcessDescriptor[],
): Promise<vscode.Terminal> {
const isStripeCLICommand = command.startsWith(cliPath);

// If the command is a long-running one, and it's already running in a VS Code terminal,
// we restart it in the same terminal. This does not occur on Windows due to OS limitations.
if (this.isCommandLongRunning(command)) {
if (isStripeCLICommand && this.isCommandLongRunning(command)) {
const terminalWithDesiredCommand = await findAsync(this.terminals, async (t) => {
const runningCommand = await this.getRunningCommand(t, allRunningProcesses);
return runningCommand === command;
Expand Down
30 changes: 13 additions & 17 deletions src/test/suite/stripeClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ suite('stripeClient', () => {
sandbox.restore();
});

suite('detectInstalled', () => {
suite('getCLIPath', () => {
suite('with default CLI install path', () => {
const osPathPairs: [utils.OSType, string][] = [
[utils.OSType.linux, '/usr/local/bin/stripe'],
Expand All @@ -44,21 +44,19 @@ suite('stripeClient', () => {
test('detects installed', async () => {
statStub.returns(Promise.resolve({isFile: () => true})); // the path is a file; CLI found
const stripeClient = new StripeClient(new NoOpTelemetry());
const isInstalled = await stripeClient.detectInstalled();
assert.strictEqual(isInstalled, true);
const cliPath = await stripeClient.getCLIPath();
assert.strictEqual(cliPath, path);
assert.deepStrictEqual(realpathStub.args[0], [path]);
assert.deepStrictEqual(statStub.args[0], [resolvedPath]);
assert.strictEqual(stripeClient.cliPath, path);
});

test('detects not installed', async () => {
statStub.returns(Promise.resolve({isFile: () => false})); // the path is not a file; CLI not found
const stripeClient = new StripeClient(new NoOpTelemetry());
const isInstalled = await stripeClient.detectInstalled();
assert.strictEqual(isInstalled, false);
const cliPath = await stripeClient.getCLIPath();
assert.strictEqual(cliPath, null);
assert.deepStrictEqual(realpathStub.args[0], [path]);
assert.deepStrictEqual(statStub.args[0], [resolvedPath]);
assert.strictEqual(stripeClient.cliPath, null);
});
});
});
Expand All @@ -67,8 +65,8 @@ suite('stripeClient', () => {
sandbox.stub(fs.promises, 'stat').returns(Promise.resolve({isFile: () => false}));
const showErrorMessageSpy = sandbox.stub(vscode.window, 'showErrorMessage');
const stripeClient = new StripeClient(new NoOpTelemetry());
const isInstalled = await stripeClient.detectInstalled();
assert.strictEqual(isInstalled, false);
const cliPath = await stripeClient.getCLIPath();
assert.strictEqual(cliPath, null);
assert.deepStrictEqual(
showErrorMessageSpy.args[0],
[
Expand Down Expand Up @@ -108,21 +106,19 @@ suite('stripeClient', () => {
test('detects installed', async () => {
statStub.returns(Promise.resolve({isFile: () => true})); // the path is a file; CLI found
const stripeClient = new StripeClient(new NoOpTelemetry());
const isInstalled = await stripeClient.detectInstalled();
assert.strictEqual(isInstalled, true);
const cliPath = await stripeClient.getCLIPath();
assert.strictEqual(cliPath, customPath);
assert.deepStrictEqual(realpathStub.args[0], [customPath]);
assert.deepStrictEqual(statStub.args[0], [resolvedPath]);
assert.strictEqual(stripeClient.cliPath, customPath);
});

test('detects not installed', async () => {
statStub.returns(Promise.resolve({isFile: () => false})); // the path is not a file; CLI not found
const stripeClient = new StripeClient(new NoOpTelemetry());
const isInstalled = await stripeClient.detectInstalled();
assert.strictEqual(isInstalled, false);
const cliPath = await stripeClient.getCLIPath();
assert.strictEqual(cliPath, null);
assert.deepStrictEqual(realpathStub.args[0], [customPath]);
assert.deepStrictEqual(statStub.args[0], [resolvedPath]);
assert.strictEqual(stripeClient.cliPath, null);
});
});
});
Expand All @@ -131,8 +127,8 @@ suite('stripeClient', () => {
statStub.returns(Promise.resolve({isFile: () => false}));
const showErrorMessageSpy = sandbox.stub(vscode.window, 'showErrorMessage');
const stripeClient = new StripeClient(new NoOpTelemetry());
const isInstalled = await stripeClient.detectInstalled();
assert.strictEqual(isInstalled, false);
const cliPath = await stripeClient.getCLIPath();
assert.strictEqual(cliPath, null);
assert.deepStrictEqual(
showErrorMessageSpy.args[0],
["You set a custom installation path for the Stripe CLI, but we couldn't find the executable in '/foo/bar/baz'", 'Ok'],
Expand Down
59 changes: 52 additions & 7 deletions src/test/suite/stripeTerminal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,59 @@ suite('stripeTerminal', function() {
sandbox.restore();
});

test('sends command to a new terminal if no terminals exist', async () => {
const sendTextStub = sandbox.stub(terminalStub, 'sendText');
const createTerminalStub = sandbox.stub(vscode.window, 'createTerminal').returns(terminalStub);
[
'/usr/local/bin/stripe',
'/custom/path/to/stripe'
].forEach((path) => {
suite(`when the Stripe CLI is installed at ${path}`, () => {
test(`runs command with ${path}`, async () => {
const sendTextStub = sandbox.stub(terminalStub, 'sendText');
const createTerminalStub = sandbox.stub(vscode.window, 'createTerminal').returns(terminalStub);
const stripeClientStub = <any>{getCLIPath: () => {}};
const getCLIPathStub = sandbox.stub(stripeClientStub, 'getCLIPath').returns(Promise.resolve(path));

const stripeTerminal = new StripeTerminal();
await stripeTerminal.execute('listen', ['--foward-to', 'localhost']);
const stripeTerminal = new StripeTerminal(stripeClientStub);
await stripeTerminal.execute('listen', ['--forward-to', 'localhost']);

assert.strictEqual(createTerminalStub.callCount, 1);
assert.strictEqual(sendTextStub.getCalls()[0].args[0], 'stripe listen --foward-to localhost');
assert.strictEqual(getCLIPathStub.callCount, 1);
assert.strictEqual(createTerminalStub.callCount, 1);
assert.deepStrictEqual(sendTextStub.args[0], [`${path} listen --forward-to localhost`]);
});
});
});

suite('with no Stripe CLI installed', () => {
test('does not run command', async () => {
const sendTextStub = sandbox.stub(terminalStub, 'sendText');
const createTerminalStub = sandbox.stub(vscode.window, 'createTerminal').returns(terminalStub);
const stripeClientStub = <any>{getCLIPath: () => {}};
sandbox.stub(stripeClientStub, 'getCLIPath').returns(null);

const stripeTerminal = new StripeTerminal(stripeClientStub);
await stripeTerminal.execute('listen', ['--forward-to', 'localhost']);

assert.strictEqual(createTerminalStub.callCount, 0);
assert.deepStrictEqual(sendTextStub.callCount, 0);
});
});

suite('if a Stripe terminal already exists', () => {
test('reuses terminal if the command is the same', async () => {
const createTerminalStub = sandbox.stub(vscode.window, 'createTerminal').returns(terminalStub);
const sendTextStub = sandbox.stub(terminalStub, 'sendText');
const stripeClientStub = <any>{getCLIPath: () => {}};
sandbox.stub(stripeClientStub, 'getCLIPath')
.returns(Promise.resolve('/usr/local/bin/stripe'));

const stripeTerminal = new StripeTerminal(stripeClientStub);
await stripeTerminal.execute('listen', ['--forward-to', 'localhost']);

// same command => reuse the same terminal
await stripeTerminal.execute('listen', ['--forward-to', 'localhost']);

assert.strictEqual(createTerminalStub.callCount, 1);
assert.deepStrictEqual(sendTextStub.args[0], ['/usr/local/bin/stripe listen --forward-to localhost']);
assert.deepStrictEqual(sendTextStub.args[1], ['/usr/local/bin/stripe listen --forward-to localhost']);
});
});
});

0 comments on commit 873e0f9

Please sign in to comment.