Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c3e4245
docs: clarify @beta tag is required for MCP support
mrdailey99 Apr 13, 2026
17de177
fixed auth login bug that failed to return to cli
mrdailey99 Apr 15, 2026
1f975fb
chore: bump version to 1.5.0-beta.5
mrdailey99 Apr 15, 2026
9376fbc
fix(auth): address PR review comments on openBrowser
mrdailey99 Apr 15, 2026
fc43ff8
Merge pull request #118 from ProvarTesting/feature/auth-and-quality-h…
mrdailey99 Apr 15, 2026
41dc2de
fix(mcp): use shell:true for .cmd/.bat executables on Windows
mrdailey99 Apr 15, 2026
3a533e7
fix(auth): trim and validate stored api key in resolveApiKey
mrdailey99 Apr 15, 2026
6a10625
feat(mcp): implement 7 test-loop improvements from real usage feedback
mrdailey99 Apr 15, 2026
57812b1
fix(auth): address PR #119 review comments on loginFlow + bump to beta.6
mrdailey99 Apr 15, 2026
31e948e
fix(auth): guard against non-string api_key in resolveApiKey
mrdailey99 Apr 15, 2026
b8f3c87
fix(mcp): address PR 121 review comments
mrdailey99 Apr 15, 2026
410dcc0
fix(windows-sf-spawn): address Copilot PR review comments
mrdailey99 Apr 15, 2026
568fb7c
fix(lint): rename _sfPlatform, fix JSDoc indent, fix string quotes
mrdailey99 Apr 15, 2026
7dfee1a
docs: note wireit caching gotcha and direct mocha command in CLAUDE.md
mrdailey99 Apr 15, 2026
9704976
Merge pull request #121 from ProvarTesting/feature/test-creation-loop…
mrdailey99 Apr 15, 2026
b230cd6
Merge pull request #120 from ProvarTesting/fix/qh-validate-auth-fallback
mrdailey99 Apr 15, 2026
e1c3905
fix validate via api bug
mrdailey99 Apr 15, 2026
e6c4b9b
Merge branch 'develop' of https://github.com/ProvarTesting/provardx-c…
mrdailey99 Apr 15, 2026
09bb3ae
fix: resolve merge conflict with develop in automationTools.test.ts
mrdailey99 Apr 15, 2026
51f52be
Merge pull request #122 from ProvarTesting/fix/windows-sf-cmd-spawn
mrdailey99 Apr 15, 2026
632c561
feat: add provar.qualityhub.examples.retrieve and provar.org.describe…
mrdailey99 Apr 17, 2026
72650c2
chore: merge develop — keep phase2 corpus retrieve tests
mrdailey99 Apr 17, 2026
4fe68be
chore: bump version to 1.5.0-beta.7
mrdailey99 Apr 17, 2026
f8737fe
docs: add corpus retrieval pilot scenarios to mcp-pilot-guide
mrdailey99 Apr 17, 2026
65d4867
fix: truncate generic error message in log to 200 chars
mrdailey99 Apr 17, 2026
cbccb64
docs: update provar.org.describe to reference Salesforce Hosted MCP (…
mrdailey99 Apr 17, 2026
58f1ca9
refactor: remove provar.org.describe tool — superseded by Salesforce …
mrdailey99 Apr 17, 2026
ff87107
Merge pull request #123 from ProvarTesting/feature/phase2-corpus-retr…
mrdailey99 Apr 17, 2026
ea7d071
docs(mcp): add GitHub Copilot, Cursor, and Agentforce Vibes client se…
mrdailey99 Apr 17, 2026
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Validation runs in two modes: **local only** (structural rules, no key required)
**Requires:** Provar Automation IDE installed with an activated license.

```sh
# 1. Install the plugin (if not already installed)
# 1. Install the plugin — @beta is required for MCP support
sf plugins install @provartesting/provardx-cli@beta

# 2. (Optional) Authenticate for full 170+ rule validation
Expand Down
2 changes: 1 addition & 1 deletion docs/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ The Provar DX CLI ships with a built-in **Model Context Protocol (MCP) server**
## Quick start

```sh
# 1. Install the plugin
# 1. Install the plugin — @beta is required for MCP support
sf plugins install @provartesting/provardx-cli@beta

# 2. (Optional) Authenticate for full 170+ rule validation
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@provartesting/provardx-cli",
"description": "A plugin for the Salesforce CLI to orchestrate testing activities and report quality metrics to Provar Quality Hub",
"version": "1.5.0-beta.4",
"version": "1.5.0-beta.5",
"license": "BSD-3-Clause",
"plugins": [
"@provartesting/provardx-plugins-automation",
Expand Down
41 changes: 31 additions & 10 deletions src/services/auth/loginFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import crypto from 'node:crypto';
import http from 'node:http';
import https from 'node:https';
import { execFile } from 'node:child_process';
import { spawn, type ChildProcess } from 'node:child_process';
import { URL } from 'node:url';

// All three ports must be pre-registered in the Cognito App Client.
Expand Down Expand Up @@ -76,21 +76,35 @@ function isPortFree(port: number): Promise<boolean> {
* Open a URL in the system browser. The URL is passed as an argument — not
* interpolated into a shell string — to avoid command injection.
*/
export function openBrowser(url: string): void {
switch (process.platform) {
/**
* Return the platform-specific command and argument list for opening a URL
* in the system browser. Exported so tests can assert the correct command is
* chosen for each platform without actually spawning a process.
*/
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two adjacent JSDoc blocks here; the first one (“Open a URL in the system browser…”) is now orphaned and no longer documents any symbol. This can confuse generated docs and makes it unclear which function the injection-safety note applies to. Consider merging the two blocks or moving the injection-safety text onto the openBrowser JSDoc and keeping only one JSDoc block per export.

Copilot uses AI. Check for mistakes.
export function getBrowserCommand(url: string, platform: NodeJS.Platform = process.platform): { cmd: string; args: string[] } {
switch (platform) {
case 'darwin':
execFile('open', [url]);
break;
return { cmd: 'open', args: [url] };
case 'win32':
// Pass the URL via $args[0] so it is never interpolated into the -Command
// string — avoids quote-breaking and injection risk from special characters.
execFile('powershell.exe', ['-NoProfile', '-Command', 'Start-Process $args[0]', '-args', url]);
break;
return { cmd: 'powershell.exe', args: ['-NoProfile', '-Command', 'Start-Process $args[0]', '-args', url] };
default:
execFile('xdg-open', [url]);
return { cmd: 'xdg-open', args: [url] };
}
}

export function openBrowser(url: string): void {
// detached:true + stdio:'ignore' + unref() is the standard Node.js pattern for
// fire-and-forget child processes — the event loop will not wait for them to exit.
const { cmd, args } = getBrowserCommand(url);
const child: ChildProcess = spawn(cmd, args, { detached: true, stdio: 'ignore' });
// Suppress unhandled-error crashes if the browser executable is not found.
// The login URL is already printed to the terminal so the user can open it manually.
child.on('error', () => { /* intentional no-op */ });
child.unref();
}

// ── Localhost callback server ─────────────────────────────────────────────────

/**
Expand All @@ -107,26 +121,33 @@ export function listenForCallback(port: number, expectedState?: string): Promise
const callbackState = parsed.searchParams.get('state');

if (expectedState && callbackState !== expectedState) {
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8', Connection: 'close' });
res.end(
'<html><body style="font-family:sans-serif;padding:2rem;max-width:480px">' +
'<h2 style="color:#c23934">Authentication failed</h2>' +
'<p>Invalid state parameter — possible CSRF attack. Please try again.</p>' +
'</body></html>'
);
server.close();
server.closeAllConnections?.();
reject(new Error('OAuth callback state mismatch — possible CSRF. Try again.'));
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same Node-version concern here: server.closeAllConnections?.() won’t run on Node 18.0/18.1 even though the package supports those versions, so a lingering keep-alive socket could still prevent the process from exiting. Consider adding a socket-tracking fallback when closeAllConnections is undefined so shutdown is reliable across the full supported Node range.

Copilot uses AI. Check for mistakes.
return;
}

res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
// 'Connection: close' tells the browser to close the TCP connection after
// this response so server.close() has no lingering keep-alive sockets to
// wait for, allowing the Node.js event loop to exit promptly.
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', Connection: 'close' });
res.end(
'<html><body style="font-family:sans-serif;padding:2rem;max-width:480px">' +
'<h2 style="color:#0070d2">Authentication complete</h2>' +
'<p>You can close this tab and return to the terminal.</p>' +
'</body></html>'
);
server.close();
// Destroy any sockets that are still open (e.g. a browser that ignores
// the Connection:close header). Requires Node 18.2+.
server.closeAllConnections?.();
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

server.closeAllConnections?.() only exists on Node >=18.2, but package.json allows Node 18.0+. On Node 18.0/18.1, if a browser keeps the socket open despite Connection: close, server.close() can still keep the event loop alive. If you need this to work reliably on the full supported Node range, consider tracking active sockets via server.on('connection', ...) and destroying them on shutdown as a fallback when closeAllConnections is unavailable.

Copilot uses AI. Check for mistakes.

if (code) {
resolve(code);
Expand Down
40 changes: 40 additions & 0 deletions test/unit/commands/provar/auth/login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { describe, it, beforeEach, afterEach } from 'mocha';
import sinon from 'sinon';
import {
generatePkce,
getBrowserCommand,
loginFlowClient,
type CognitoTokens,
CALLBACK_PORTS,
Expand Down Expand Up @@ -59,6 +60,45 @@ function restoreHome(): void {
fs.rmSync(tempDir, { recursive: true, force: true });
}

// ── getBrowserCommand ─────────────────────────────────────────────────────────

describe('getBrowserCommand', () => {
const url = 'https://example.com/login?code=abc&state=xyz';

it('uses "open" on macOS', () => {
const { cmd, args } = getBrowserCommand(url, 'darwin');
assert.equal(cmd, 'open');
assert.deepEqual(args, [url]);
});

it('uses powershell.exe with Start-Process on Windows', () => {
const { cmd, args } = getBrowserCommand(url, 'win32');
assert.equal(cmd, 'powershell.exe');
assert.ok(args.includes('-NoProfile'), 'should pass -NoProfile');
assert.ok(args.includes('Start-Process $args[0]') || args.join(' ').includes('Start-Process'), 'should use Start-Process');
// URL is passed as a separate arg — never interpolated into the command string
assert.equal(args[args.length - 1], url, 'URL must be the last argument');
});

it('uses "xdg-open" on Linux', () => {
const { cmd, args } = getBrowserCommand(url, 'linux');
assert.equal(cmd, 'xdg-open');
assert.deepEqual(args, [url]);
});

it('uses "xdg-open" for unknown platforms', () => {
const { cmd } = getBrowserCommand(url, 'freebsd');
assert.equal(cmd, 'xdg-open');
});

it('includes the full URL in args for all platforms', () => {
for (const platform of ['darwin', 'win32', 'linux'] as NodeJS.Platform[]) {
const { args } = getBrowserCommand(url, platform);
assert.ok(args.includes(url), `URL must appear in args for platform ${platform}`);
}
});
});

// ── generatePkce ──────────────────────────────────────────────────────────────

describe('generatePkce', () => {
Expand Down
Loading