diff --git a/package.json b/package.json index 9978436f..28b5a8ec 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ }, "devDependencies": { "@oclif/plugin-command-snapshot": "^5.0.2", - "yarn": "^1.22.22", "@salesforce/cli-plugins-testkit": "^5.1.7", "@salesforce/dev-scripts": "^8.3.0", "@types/sinon": "^21.0.0", diff --git a/scripts/mcp-smoke.cjs b/scripts/mcp-smoke.cjs index 670bb498..952f283f 100644 --- a/scripts/mcp-smoke.cjs +++ b/scripts/mcp-smoke.cjs @@ -27,7 +27,7 @@ const INCLUDE_SETUP = process.env['SMOKE_INCLUDE_SETUP'] === '1'; // ---------------------------------------------------------------------------- const server = spawn('sf', ['provar', 'mcp', 'start', '--allowed-paths', TMP], { stdio: ['pipe', 'pipe', 'inherit'], - shell: true, + shell: process.platform === 'win32', env: { ...process.env, PROVAR_DEV_WHITELIST_KEYS: process.env.PROVAR_DEV_WHITELIST_KEYS || '', diff --git a/src/mcp/licensing/licenseValidator.ts b/src/mcp/licensing/licenseValidator.ts index 421a9c76..2634e2a8 100644 --- a/src/mcp/licensing/licenseValidator.ts +++ b/src/mcp/licensing/licenseValidator.ts @@ -111,6 +111,14 @@ function validateViaIdeDetection(): LicenseValidationResult { ); } + // Warn when the IDE's own ALGAS check is stale (> 7 days) — the IDE may not have been + // opened recently enough to refresh the activation status from the licensing server. + const IDE_FRESHNESS_WARN_MS = 7 * 24 * 60 * 60 * 1000; + if (ideState.lastOnlineCheckMs > 0 && Date.now() - ideState.lastOnlineCheckMs > IDE_FRESHNESS_WARN_MS) { + const ageDays = Math.round((Date.now() - ideState.lastOnlineCheckMs) / (24 * 60 * 60 * 1000)); + log('warn', 'licenseValidator: IDE license online check is stale — open Provar IDE to refresh', { ageDays }); + } + // 3. Valid — write to MCP cache so next start within 2h skips this read const entry: CacheEntry = { keyHash, diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 3762140b..a81320d7 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -5,9 +5,13 @@ * For full license text, see LICENSE.md file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ +import { createRequire } from 'node:module'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; import { log } from './logging/logger.js'; + +const requireJson = createRequire(import.meta.url); +const SERVER_VERSION: string = (requireJson('../../package.json') as { version: string }).version; import { registerProjectInspect } from './tools/projectInspect.js'; import { registerPageObjectGenerate } from './tools/pageObjectGenerate.js'; import { registerPageObjectValidate } from './tools/pageObjectValidate.js'; @@ -33,7 +37,7 @@ export function createProvarMcpServer(config: ServerConfig): McpServer { const server = new McpServer({ name: 'provar-mcp', - version: '1.0.0', + version: SERVER_VERSION, }); // ── Sanity-check tool ──────────────────────────────────────────────────────── @@ -44,7 +48,7 @@ export function createProvarMcpServer(config: ServerConfig): McpServer { message: z.string().optional().default('ping').describe('Optional message to echo back'), }, ({ message }) => { - const result = { pong: message, ts: new Date().toISOString(), server: 'provar-mcp@1.0.0' }; + const result = { pong: message, ts: new Date().toISOString(), server: `provar-mcp@${SERVER_VERSION}` }; return { content: [{ type: 'text' as const, text: JSON.stringify(result) }], structuredContent: result, diff --git a/src/mcp/tools/automationTools.ts b/src/mcp/tools/automationTools.ts index fefdca8e..130b1c49 100644 --- a/src/mcp/tools/automationTools.ts +++ b/src/mcp/tools/automationTools.ts @@ -15,7 +15,7 @@ import { makeError, makeRequestId } from '../schemas/common.js'; import { log } from '../logging/logger.js'; import type { ServerConfig } from '../server.js'; import { assertPathAllowed, PathPolicyError } from '../security/pathPolicy.js'; -import { sfSpawnHelper } from './sfSpawn.js'; +import { sfSpawnHelper, SfNotFoundError } from './sfSpawn.js'; // ── SF CLI discovery ────────────────────────────────────────────────────────── @@ -89,20 +89,6 @@ function resolveSfExecutable(): string | null { return null; } -class SfNotFoundError extends Error { - public readonly code = 'SF_NOT_FOUND'; - public constructor(sfPath?: string) { - const where = sfPath - ? `at explicit path "${sfPath}"` - : 'in PATH or common npm/nvm install locations'; - super( - `sf CLI not found ${where}. ` + - 'Install Salesforce CLI (npm install -g @salesforce/cli) and ensure the install directory is in your PATH, ' + - 'or pass sf_path pointing to the sf executable directly ' + - '(e.g. "~/.nvm/versions/node/v22.0.0/bin/sf").' - ); - } -} function runSfCommand(args: string[], sfPath?: string): SpawnResult { // Use explicit path if provided; otherwise use cached probe result diff --git a/src/mcp/tools/qualityHubTools.ts b/src/mcp/tools/qualityHubTools.ts index 5162af24..223dc7d0 100644 --- a/src/mcp/tools/qualityHubTools.ts +++ b/src/mcp/tools/qualityHubTools.ts @@ -143,7 +143,17 @@ export function registerQualityHubTestRunReport(server: McpServer): void { return { isError: true as const, content: [{ type: 'text' as const, text: JSON.stringify(makeError('QH_REPORT_FAILED', result.stderr || result.stdout, requestId)) }] }; } - const hasFailures = /fail/i.test(result.stdout); + const failureStatuses = new Set(['FAIL', 'FAILED']); + let hasFailures = false; + try { + const parsed = JSON.parse(result.stdout) as { result?: { status?: string } }; + const normalizedStatus = parsed.result?.status?.trim().toUpperCase(); + hasFailures = normalizedStatus !== undefined && failureStatuses.has(normalizedStatus); + } catch { + const statusMatch = result.stdout.match(/"status"\s*:\s*"([^"]+)"/i); + const normalizedStatus = statusMatch?.[1]?.trim().toUpperCase(); + hasFailures = normalizedStatus !== undefined && failureStatuses.has(normalizedStatus); + } const suggestion = hasFailures ? 'Failures detected. Use provar.qualityhub.defect.create with run_id and target_org to automatically create Defect__c records for each failure (syncs to Jira/ADO if configured).' : ''; diff --git a/src/mcp/tools/sfSpawn.ts b/src/mcp/tools/sfSpawn.ts index 47417af3..5b7fd3c3 100644 --- a/src/mcp/tools/sfSpawn.ts +++ b/src/mcp/tools/sfSpawn.ts @@ -19,9 +19,15 @@ export const sfSpawnHelper = { export class SfNotFoundError extends Error { public readonly code = 'SF_NOT_FOUND'; - public constructor() { + public constructor(sfPath?: string) { + const where = sfPath + ? `at explicit path "${sfPath}"` + : 'in PATH or common npm/nvm/volta install locations'; super( - 'sf CLI not found in PATH. Install Salesforce CLI (`npm install -g @salesforce/cli`) and ensure it is in your PATH.' + `sf CLI not found ${where}. ` + + 'Install Salesforce CLI (npm install -g @salesforce/cli) and ensure the install directory is in your PATH, ' + + 'or pass sf_path pointing to the sf executable directly ' + + '(e.g. "~/.nvm/versions/node/v22.0.0/bin/sf").' ); this.name = 'SfNotFoundError'; }