diff --git a/packages/cli-app/src/exec.js b/packages/cli-app/src/exec.js index 10b54de21..e440566a3 100644 --- a/packages/cli-app/src/exec.js +++ b/packages/cli-app/src/exec.js @@ -182,13 +182,17 @@ export function maybeInjectScreenshotDir(ctx, log) { export const exec = command('exec', { description: 'Start and stop Percy around a supplied command for native apps', - usage: '[options] -- ', + usage: '[options] [--] ', commands: [start, stop, ping], flags: ExecPlugin.default.definition // grouped flags are built-in flags .flags.filter(f => !f.group), + // some environments (e.g. npx PowerShell shims) strip the `--` separator; + // fall back to loose parsing like `percy exec` instead of hard failing + loose: ExecPlugin.default.definition.loose, + percy: { server: true, projectType: 'app', diff --git a/packages/cli-app/test/exec.test.js b/packages/cli-app/test/exec.test.js index b114676af..44b036dc2 100644 --- a/packages/cli-app/test/exec.test.js +++ b/packages/cli-app/test/exec.test.js @@ -1,7 +1,7 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; -import { setupTest } from '@percy/cli-command/test/helpers'; +import { logger, setupTest } from '@percy/cli-command/test/helpers'; import * as ExecPlugin from '@percy/cli-exec'; import { exec, start, stop, ping, @@ -28,12 +28,41 @@ describe('percy app:exec', () => { }); it('does not accept asset discovery options', async () => { - await expectAsync(exec(['--allowed-hostname', 'percy.io'])) - .toBeRejectedWithError("Unknown option '--allowed-hostname'"); await expectAsync(start(['--network-idle-timeout', '500'])) .toBeRejectedWithError("Unknown option '--network-idle-timeout'"); }); + it('degrades unrecognized exec options into the command (loose parsing trade-off)', async () => { + // with loose parsing, exec no longer hard-rejects unknown flags with + // "Unknown option" — a typo like --allowed-hostname falls through to + // command lookup and fails there instead + let { default: which } = await import('which'); + spyOn(which, 'sync').and.returnValue(null); + + await expectAsync(exec(['--allowed-hostname', 'percy.io'])) + .toBeRejectedWithError('Command not found "--allowed-hostname"'); + }); + + it('parses loosely when the command separator is missing', async () => { + // some environments (e.g. npx PowerShell shims) strip the `--` separator; + // the command should warn and run instead of throwing a parse error + expect(exec.definition.loose).toEqual(ExecPlugin.default.definition.loose); + + // stub command resolution so no real binary is looked up or spawned; the + // "Command not found" rejection proves `dotnet` was parsed as the command + // (not rejected as an unexpected argument) before reaching the lookup + let { default: which } = await import('which'); + spyOn(which, 'sync').and.returnValue(null); + + await expectAsync(exec(['dotnet', 'test'])) + .toBeRejectedWithError('Command not found "dotnet"'); + + expect(logger.stderr).toEqual(jasmine.arrayContaining([ + `[percy] ${ExecPlugin.default.definition.loose}`, + '[percy] Error: Command not found "dotnet"' + ])); + }); + describe('maybeInjectMaestroServer', () => { function ctxFor(argv, addr = 'http://localhost:5338') { return { diff --git a/packages/cli-exec/src/exec.js b/packages/cli-exec/src/exec.js index be4850e86..cfa6d81ac 100644 --- a/packages/cli-exec/src/exec.js +++ b/packages/cli-exec/src/exec.js @@ -8,7 +8,7 @@ import { waitForTimeout } from '@percy/client/utils'; export const exec = command('exec', { description: 'Start and stop Percy around a supplied command', - usage: '[options] -- ', + usage: '[options] [--] ', commands: [start, stop, ping, replay], flags: [{