diff --git a/.github/workflows/CIRelease_Tagging.yml b/.github/workflows/CIRelease_Tagging.yml index 4693b3e1..ec24487a 100644 --- a/.github/workflows/CIRelease_Tagging.yml +++ b/.github/workflows/CIRelease_Tagging.yml @@ -12,8 +12,8 @@ jobs: runs-on: ubuntu-latest if: github.event.pull_request.merged == true steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 with: node-version: 20 - name: Install Dependencies diff --git a/.github/workflows/CI_Execution.yml b/.github/workflows/CI_Execution.yml index 3d1558ad..1e146a01 100644 --- a/.github/workflows/CI_Execution.yml +++ b/.github/workflows/CI_Execution.yml @@ -19,6 +19,24 @@ on: # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: + quality-analysis: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: mrdailey99/QualityOrchestrator@v1.0.0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + test-dir: 'test' + framework: 'auto' + generate-stubs: 'false' + fail-on-high: 'false' + provardx-ci-execution: strategy: matrix: @@ -26,12 +44,12 @@ jobs: nodeversion: [20] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 with: node-version: ${{ matrix.nodeversion }} - name: 'Cache node_modules' - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ matrix.os == 'windows-latest' && 'C:\\Users\\runneradmin\\AppData\\Roaming\\npm-cache' || '~/.npm' }} key: ${{ runner.os }}-node-v${{ matrix.nodeversion }}-${{ hashFiles('**/package.json') }} @@ -62,7 +80,7 @@ jobs: - name: Check for target branch in Utils repo id: check_branch - uses: actions/github-script@v6 + uses: actions/github-script@v9 with: script: | const branch = process.env.BRANCH_NAME; @@ -80,7 +98,7 @@ jobs: return branchExists; - name: Check out Utils repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: repository: ProvarTesting/provardx-plugins-utils path: utils @@ -107,7 +125,7 @@ jobs: PROVAR_DEV_WHITELIST_KEYS: ${{ secrets.PROVAR_DEV_WHITELIST_KEYS }} run: node scripts/mcp-smoke.cjs - name: Check out Regression repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: repository: ProvarTesting/provar-manager-regression path: ProvarRegression @@ -127,7 +145,7 @@ jobs: yarn run test:nuts - name: Archive NUTS results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: nuts-report-${{ matrix.os }} path: mochawesome-report diff --git a/.github/workflows/DeployManual.yml b/.github/workflows/DeployManual.yml index 5612e182..1d83ec7c 100644 --- a/.github/workflows/DeployManual.yml +++ b/.github/workflows/DeployManual.yml @@ -14,11 +14,11 @@ jobs: contents: read steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Setup Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: 20 registry-url: 'https://registry.npmjs.org' diff --git a/.github/workflows/UnpublishManual.yml b/.github/workflows/UnpublishManual.yml index e8a52bd7..1f212c1b 100644 --- a/.github/workflows/UnpublishManual.yml +++ b/.github/workflows/UnpublishManual.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Setup Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: 20 registry-url: 'https://registry.npmjs.org' diff --git a/docs/NITROX_COMPONENT_CATALOG.md b/docs/NITROX_COMPONENT_CATALOG.md new file mode 100644 index 00000000..46a5d554 --- /dev/null +++ b/docs/NITROX_COMPONENT_CATALOG.md @@ -0,0 +1,2001 @@ +# NitroX Component Package Catalog + +Shipped base NitroX (Hybrid Model) component packages. +Use as a reference when generating new NitroX components — match naming conventions, +type strings, tagNames, interaction titles, and attribute names from these shipped packages. + +--- + +## common (v1.8.21) + +Package includes definitions for Generic Fact Component +**Requires Provar:** >=2.10.2 + +### Components + +#### Generic Component + +- **name:** `json::/com/provar/common/GenericComponent` +- **type:** `container/genericComponent` +- **tagName:** `*` +- **interactions:** `Clear`, `Set`, `Check`, `Uncheck`, `Click` +- **attributes:** `Class`, `Visible`, `Disabled`, `Name`, `Label`, `Type`, `Checked`, `Required`, `Read only`, `Max length`, `Min length`, `Href`, `Value` + +--- + +## experienceCloud (v1.0.7) + +Package includes definitions for Experience Cloud FACT elements +**Requires Provar:** >=2.10.2 + +### Components + +#### Community Leaderboard Item + +- **name:** `json::/com/provar/experienceCloud/CommunityLeaderboardItem` +- **type:** `container` +- **tagName:** `div` +- **interactions:** `Click` +- **attributes:** `Class` +- **child elements:** 2 + +#### CheckBox + +- **name:** `json::/com/provar/experienceCloud/ExpCloudCheckBox` +- **type:** `Checkbox` +- **tagName:** `div` +- **interactions:** `Check`, `Uncheck` +- **attributes:** `Class`, `Value` + +#### Lookup List + +- **name:** `json::/com/provar/experienceCloud/ExpCloudLookupList` +- **type:** `container` +- **tagName:** `a` +- **interactions:** `Click` +- **attributes:** `Name`, `Type`, `Text`, `Class`, `Href` + +#### PickList + +- **name:** `json::/com/provar/experienceCloud/ExpCloudPickList` +- **type:** `container` +- **tagName:** `div` +- **interactions:** `Set`, `Set By Index` +- **attributes:** `Class`, `Value` + +#### Rich Text Area + +- **name:** `json::/com/provar/experienceCloud/ExpCloudRichTextArea` +- **type:** `container` +- **tagName:** `div` +- **interactions:** `Clear`, `Set` +- **attributes:** `Class`, `Value` + +#### Profile Menu + +- **name:** `json::/com/provar/experienceCloud/UserProfileMenu` +- **type:** `container` +- **tagName:** `community_user-user-profile-menu` +- **interactions:** `Set`, `Set By Index`, `Click` +- **attributes:** `Class`, `Username`, `Is guest user`, `Login button text`, `Menu Items` + +--- + +## html5 (v1.8.11) + +Package includes definitions for HTML5 FACT elements +**Requires Provar:** >=2.10.2 + +### Components + +#### HTML5 Anchor Object + +- **name:** `json::/com/provar/html5/AnchorObject` +- **type:** `container` +- **tagName:** `a` +- **interactions:** `Click` +- **attributes:** `Name`, `Visible`, `Type`, `Text`, `Class`, `Href` + +#### HTML5 Button + +- **name:** `json::/com/provar/html5/Button` +- **type:** `container` +- **tagName:** `button` +- **interactions:** `Click` +- **attributes:** `Disabled`, `Visible`, `Text Content`, `Inner Text`, `Label`, `Name`, `Type`, `Title`, `Aria Label` + +#### HTML5 Button Input + +- **name:** `json::/com/provar/html5/ButtonInput` +- **type:** `container` +- **tagName:** `input` +- **interactions:** `Click` +- **attributes:** `Disabled`, `Visible`, `Validation Message`, `Name`, `Type` + +#### HTML5 Checkbox Input + +- **name:** `json::/com/provar/html5/CheckboxInput` +- **type:** `container` +- **tagName:** `input` +- **interactions:** `Check`, `Uncheck` +- **attributes:** `Disabled`, `Name`, `Visible`, `Type`, `Checked`, `Required`, `Read only` + +#### HTML5 Color Input + +- **name:** `json::/com/provar/html5/ColorInput` +- **type:** `container` +- **tagName:** `input` +- **interactions:** `Set` +- **attributes:** `Disabled`, `Visible`, `Name`, `Type`, `Value` + +#### HTML5 Date Input + +- **name:** `json::/com/provar/html5/DateInput` +- **type:** `container` +- **tagName:** `input` +- **interactions:** `Set`, `Set Date` +- **attributes:** `Disabled`, `Visible`, `Name`, `Pattern`, `Placeholder`, `Read only`, `Required`, `Type`, `Value`, `Min`, `Max`, `Validation Message`, `Class` + +#### HTML5 Datetime-local Input + +- **name:** `json::/com/provar/html5/DatetimeLocalInput` +- **type:** `container` +- **tagName:** `input` +- **interactions:** `Set`, `Set Date Time` +- **attributes:** `Disabled`, `Visible`, `Name`, `Pattern`, `Class`, `Placeholder`, `Read only`, `Required`, `Type`, `Validation Message`, `Value`, `Max`, `Min` + +#### HTML5 Div + +- **name:** `json::/com/provar/html5/Division` +- **type:** `container` +- **tagName:** `div` +- **interactions:** `Click` +- **attributes:** `Class`, `Visible`, `Value` + +#### HTML5 Email Input + +- **name:** `json::/com/provar/html5/EmailInput` +- **type:** `container` +- **tagName:** `input` +- **interactions:** `Clear`, `Append`, `Set` +- **attributes:** `Disabled`, `Visible`, `Validation Message`, `Max length`, `Name`, `Pattern`, `Placeholder`, `Read only`, `Required`, `Type`, `Value`, `Class` + +#### HTML5 File Input + +- **name:** `json::/com/provar/html5/FileInput` +- **type:** `container` +- **tagName:** `input` +- **interactions:** `Upload Files` +- **attributes:** `Disabled`, `Visible`, `Name`, `Type`, `Value` + +#### HTML5 Footer + +- **name:** `json::/com/provar/html5/Footer` +- **type:** `container` +- **tagName:** `footer` +- **attributes:** `Class`, `Value` + +#### HTML5 Form + +- **name:** `json::/com/provar/html5/Form` +- **type:** `container` +- **tagName:** `form` +- **attributes:** `Class`, `Name`, `Id`, `Value` + +#### HTML5 Header + +- **name:** `json::/com/provar/html5/Header` +- **type:** `container` +- **tagName:** `header` +- **attributes:** `Class`, `Value` + +#### HTML5 Heading1 + +- **name:** `json::/com/provar/html5/Header1` +- **type:** `container` +- **tagName:** `h1` +- **attributes:** `Class`, `Visible`, `Value` + +#### HTML5 Heading2 + +- **name:** `json::/com/provar/html5/Header2` +- **type:** `container` +- **tagName:** `h2` +- **attributes:** `Class`, `Visible`, `Value` + +#### HTML5 Heading3 + +- **name:** `json::/com/provar/html5/Header3` +- **type:** `container` +- **tagName:** `h3` +- **attributes:** `Class`, `Visible`, `Value` + +#### HTML5 Heading4 + +- **name:** `json::/com/provar/html5/Header4` +- **type:** `container` +- **tagName:** `h4` +- **attributes:** `Class`, `Visible`, `Value` + +#### HTML5 Heading5 + +- **name:** `json::/com/provar/html5/Header5` +- **type:** `container` +- **tagName:** `h5` +- **attributes:** `Class`, `Visible`, `Value` + +#### HTML5 Heading6 + +- **name:** `json::/com/provar/html5/Header6` +- **type:** `container` +- **tagName:** `h6` +- **attributes:** `Class`, `Visible`, `Value` + +#### HTML5 Iframe + +- **name:** `json::/com/provar/html5/IFrame` +- **type:** `iframe` +- **tagName:** `iframe` +- **attributes:** `Id`, `Name` + +#### HTML5 Image + +- **name:** `json::/com/provar/html5/Image` +- **type:** `container` +- **tagName:** `img` +- **attributes:** `Name`, `Visible`, `Source`, `Width`, `Alternate`, `Class` + +#### HTML5 Image Input + +- **name:** `json::/com/provar/html5/ImageInput` +- **type:** `container` +- **tagName:** `input` +- **interactions:** `Click` +- **attributes:** `Disabled`, `Visible`, `Name`, `Type`, `Value`, `Source`, `Width` + +#### HTML5 Label + +- **name:** `json::/com/provar/html5/Label` +- **type:** `container` +- **tagName:** `label` +- **attributes:** `Class`, `Visible`, `Value` + +#### HTML5 Legend + +- **name:** `json::/com/provar/html5/Legend` +- **type:** `container` +- **tagName:** `legend` +- **attributes:** `Class`, `Visible`, `Value` + +#### HTML5 List Item + +- **name:** `json::/com/provar/html5/ListItem` +- **type:** `container` +- **tagName:** `li` +- **attributes:** `Visible`, `Class` + +#### HTML5 Main + +- **name:** `json::/com/provar/html5/Main` +- **type:** `container` +- **tagName:** `main` +- **attributes:** `Class`, `Value` + +#### HTML5 Number Input + +- **name:** `json::/com/provar/html5/NumberInput` +- **type:** `container` +- **tagName:** `input` +- **interactions:** `Clear`, `Append`, `Set` +- **attributes:** `Disabled`, `Visible`, `Validation Message`, `Min`, `Max`, `Name`, `Placeholder`, `Read only`, `Required`, `Type`, `Value`, `Class` + +#### HTML5 P + +- **name:** `json::/com/provar/html5/Paragraph` +- **type:** `container` +- **tagName:** `p` +- **attributes:** `Class`, `Visible`, `Text content`, `Value` + +#### HTML5 Password Input + +- **name:** `json::/com/provar/html5/PasswordInput` +- **type:** `container` +- **tagName:** `input` +- **interactions:** `Clear`, `Append`, `Set` +- **attributes:** `Disabled`, `Visible`, `Validation Message`, `Max length`, `Name`, `Pattern`, `Placeholder`, `Read only`, `Required`, `Type`, `Value`, `Class` + +#### HTML5 Radio + +- **name:** `json::/com/provar/html5/RadioInput` +- **type:** `container` +- **tagName:** `input` +- **interactions:** `Check` +- **attributes:** `Disabled`, `Visible`, `Name`, `Type`, `Checked`, `Required` + +#### HTML5 Range Input + +- **name:** `json::/com/provar/html5/RangeInput` +- **type:** `container` +- **tagName:** `input` +- **interactions:** `Set`, `Set Range Value` +- **attributes:** `Disabled`, `Visible`, `Validation Message`, `Name`, `Type`, `Value`, `Max`, `Min`, `Step` + +#### HTML5 Reset + +- **name:** `json::/com/provar/html5/ResetInput` +- **type:** `container` +- **tagName:** `input` +- **interactions:** `Click` +- **attributes:** `Disabled`, `Visible`, `Validation Message`, `Name`, `Type`, `Class`, `Value` + +#### HTML5 Search Input + +- **name:** `json::/com/provar/html5/SearchInput` +- **type:** `container` +- **tagName:** `input` +- **interactions:** `Set` +- **attributes:** `Disabled`, `Visible`, `Validation Message`, `Max length`, `Name`, `Pattern`, `Placeholder`, `Read only`, `Required`, `Type`, `Value`, `Class` + +#### HTML5 Section + +- **name:** `json::/com/provar/html5/Section` +- **type:** `container` +- **tagName:** `section` +- **attributes:** `Class`, `Value` + +#### HTML5 Select + +- **name:** `json::/com/provar/html5/Select` +- **type:** `container` +- **tagName:** `select` +- **interactions:** `Set`, `Set By Index` +- **attributes:** `Class`, `Visible`, `Value`, `Selected Index`, `Disabled`, `Name`, `Required` + +#### HTML5 Span + +- **name:** `json::/com/provar/html5/Span` +- **type:** `container` +- **tagName:** `span` +- **interactions:** `Click` +- **attributes:** `Class`, `Visible`, `Value` + +#### HTML5 Submit + +- **name:** `json::/com/provar/html5/SubmitInput` +- **type:** `container` +- **tagName:** `input` +- **interactions:** `Click` +- **attributes:** `Disabled`, `Visible`, `Name`, `Validation Message`, `Type`, `Class`, `Value` + +#### HTML5 Telephone Input + +- **name:** `json::/com/provar/html5/TelephoneInput` +- **type:** `container` +- **tagName:** `input` +- **interactions:** `Clear`, `Append`, `Set` +- **attributes:** `Disabled`, `Visible`, `Min length`, `Max length`, `Name`, `Pattern`, `Placeholder`, `Read only`, `Required`, `Validation Message`, `Type`, `Value`, `Class` + +#### HTML5 Textarea + +- **name:** `json::/com/provar/html5/Textarea` +- **type:** `container` +- **tagName:** `textarea` +- **interactions:** `Clear`, `Append`, `Set` +- **attributes:** `Disabled`, `Visible`, `Text Content`, `innerText`, `Name`, `Type`, `Min length`, `Max length`, `Rows`, `Columns`, `Placeholder`, `Read only`, `Required`, `Value`, `Class` + +#### HTML5 Text Input + +- **name:** `json::/com/provar/html5/TextInput` +- **type:** `container` +- **tagName:** `input` +- **interactions:** `Clear`, `Append`, `Set` +- **attributes:** `Disabled`, `Visible`, `Validation Message`, `Min length`, `Max length`, `Name`, `Pattern`, `Placeholder`, `Read only`, `Required`, `Type`, `Value`, `Class` + +#### HTML5 Time Input + +- **name:** `json::/com/provar/html5/TimeInput` +- **type:** `container` +- **tagName:** `input` +- **interactions:** `Set` +- **attributes:** `Disabled`, `Visible`, `Name`, `Validation Message`, `Read only`, `Required`, `Type`, `Value`, `Max`, `Min`, `Class` + +#### HTML5 Unordered List + +- **name:** `json::/com/provar/html5/UnorderedList` +- **type:** `container` +- **tagName:** `ul` +- **attributes:** `Visible`, `Class` + +#### HTML5 Url Input + +- **name:** `json::/com/provar/html5/UrlInput` +- **type:** `container` +- **tagName:** `input` +- **interactions:** `Set` +- **attributes:** `Disabled`, `Visible`, `Max length`, `Name`, `Pattern`, `Placeholder`, `Read only`, `Required`, `Type`, `Value`, `Class` + +--- + +## msdynamics (v1.0.3) + +Package includes definitions for Microsoft Dynamics +**Requires Provar:** >=2.10.2 + +### Components + +#### MS Dynamics Abstract Field Section Item + +- **name:** `json::/nitroXPackages/ms-dynamics/AbstractFieldSectionItem` +- **type:** `container` +- **tagName:** `div` +- **interactions:** `Click` +- **attributes:** `Field Name`, `Label`, `Value`, `Required`, `Read Only`, `Message` + +#### MS Dynamics Attach File Button + +- **name:** `json::/nitroXPackages/ms-dynamics/AttachFileButton` +- **type:** `container` +- **tagName:** `button` +- **interactions:** `Activate`, `Attach File` +- **attributes:** `Label` + +#### MS Dynamics Currency Field Section Item + +- **name:** `json::/nitroXPackages/ms-dynamics/CurrencyFieldSectionItem` +- **type:** `container` +- **tagName:** `div` +- **interactions:** `Set` + +#### MS Dynamics Date/Time Field Section Item + +- **name:** `json::/nitroXPackages/ms-dynamics/DateTimeFieldSectionItem` +- **type:** `container` +- **tagName:** `div` +- **interactions:** `Set`, `Clear` + +#### MS Dynamics Decimal Number Field Section Item + +- **name:** `json::/nitroXPackages/ms-dynamics/DecimalNumberFieldSectionItem` +- **type:** `container` +- **tagName:** `div` +- **interactions:** `Set` + +#### MS Dynamics Dialog Button + +- **name:** `json::/nitroXPackages/ms-dynamics/DialogButton` +- **type:** `container` +- **tagName:** `button` +- **interactions:** `Click` +- **attributes:** `The buttons's label` + +#### MS Dynamics Email Field Section Item + +- **name:** `json::/nitroXPackages/ms-dynamics/EmailFieldSectionItem` +- **type:** `container` +- **tagName:** `div` +- **interactions:** `Set` + +#### MS Dynamics Flip Switch Field Section Item + +- **name:** `json::/nitroXPackages/ms-dynamics/FlipSwitchFieldSectionItem` +- **type:** `container` +- **tagName:** `div` +- **interactions:** `Toggle`, `Check`, `Unheck` +- **attributes:** `Switch Value`, `Switch Label` + +#### MS Dynamics Flyout Menu item + +- **name:** `json::/nitroXPackages/ms-dynamics/FlyoutMenuItem` +- **type:** `container` +- **tagName:** `button,li` +- **interactions:** `Click`, `Locate` +- **attributes:** `Label` + +#### MS Dynamics Form Command + +- **name:** `json::/nitroXPackages/ms-dynamics/FormCommand` +- **type:** `container` +- **tagName:** `button` +- **interactions:** `Click`, `Locate` +- **attributes:** `Label` + +#### MS Dynamics Grid Command + +- **name:** `json::/nitroXPackages/ms-dynamics/GridCommand` +- **type:** `container` +- **tagName:** `button` +- **interactions:** `Click`, `Locate` +- **attributes:** `The command's label.` + +#### MS Dynamics Grid Filter + +- **name:** `json::/nitroXPackages/ms-dynamics/GridFilter` +- **type:** `container` +- **tagName:** `div` +- **interactions:** `Set` +- **attributes:** `Filter Type`, `Value` + +#### MS Dynamics Header Fields Flyout + +- **name:** `json::/nitroXPackages/ms-dynamics/HeaderFieldsFlyout` +- **type:** `container` +- **tagName:** `button` +- **interactions:** `Activate` + +#### MS Dynamics Qualify Lead Dialog + +- **name:** `json::/nitroXPackages/ms-dynamics/LeadQualifyDialog` +- **type:** `container` +- **tagName:** `div` + +#### MS Grid Control + +- **name:** `json::/nitroXPackages/ms-dynamics/LegacyGrid` +- **type:** `table` +- **tagName:** `div` +- **attributes:** `Entity Display Name`, `Entity Type`, `Columns`, `Column Labels`, `ColumnsFields`, `Row Values` + +#### Grid Column + +- **name:** `json::/nitroXPackages/ms-dynamics/LegacyGridColumn` +- **type:** `abstract` +- **tagName:** `div` +- **interactions:** `Click` +- **attributes:** `Column Name`, `Label`, `Value`, `Column Type`, `Cell Type` + +#### Text Column + +- **name:** `json::/nitroXPackages/ms-dynamics/LegacyGridTextColumn` +- **type:** `column` + +#### MS Dynamics Navigation Bar + +- **name:** `json::/nitroXPackages/ms-dynamics/NavigationBar` +- **type:** `container` +- **tagName:** `div` +- **interactions:** `Locate` + +#### MS Dynamics Option Set Field Section Item + +- **name:** `json::/nitroXPackages/ms-dynamics/OptionSetFieldSectionItem` +- **type:** `container` +- **tagName:** `div` +- **interactions:** `Set` + +#### MS Dynamics Page Section + +- **name:** `json::/nitroXPackages/ms-dynamics/PageSection` +- **type:** `container` +- **tagName:** `section` +- **attributes:** `Title` + +#### MS Dynamics Phone Number Field Section Item + +- **name:** `json::/nitroXPackages/ms-dynamics/PhoneNumberFieldSectionItem` +- **type:** `container` +- **tagName:** `div` +- **interactions:** `Set` + +#### MS Dynamics Confirm Dialog + +- **name:** `json::/nitroXPackages/ms-dynamics/PowerAppsConfirmDialog` +- **type:** `container` +- **tagName:** `div` +- **interactions:** `Confirm`, `Cancel` +- **attributes:** `Title`, `Subtitle`, `Message text`, `Confirm button label`, `Cancel button label` + +#### MS Power Apps Grid Control + +- **name:** `json::/nitroXPackages/ms-dynamics/PowerAppsGrid` +- **type:** `table` +- **tagName:** `div` +- **attributes:** `Entity Display Name`, `Entity Type`, `Columns`, `Column Labels`, `ColumnsFields`, `ROW Number`, `CheckBox Column`, `Row Values` + +#### Checkbox Column + +- **name:** `json::/nitroXPackages/ms-dynamics/PowerAppsGridCheckboxColumn` +- **type:** `column` +- **tagName:** `div` +- **interactions:** `Check`, `Uncheck`, `Select All Rows`, `Un-Select All Rows` + +#### MS Power Apps Grid Column + +- **name:** `json::/nitroXPackages/ms-dynamics/PowerAppsGridColumn` +- **type:** `abstract` +- **tagName:** `div,span` +- **interactions:** `Click` +- **attributes:** `Column Name`, `Data Type`, `Data Format`, `Label`, `Value`, `Column Type`, `Cell Type` + +#### Currency Column + +- **name:** `json::/nitroXPackages/ms-dynamics/PowerAppsGridCurrencyColumn` +- **type:** `column` + +#### Date Column + +- **name:** `json::/nitroXPackages/ms-dynamics/PowerAppsGridDateColumn` +- **type:** `column` + +#### DateTime Column + +- **name:** `json::/nitroXPackages/ms-dynamics/PowerAppsGridDateTimeColumn` +- **type:** `column` + +#### Decimal Column + +- **name:** `json::/nitroXPackages/ms-dynamics/PowerAppsGridDecimalColumn` +- **type:** `column` + +#### Email Column + +- **name:** `json::/nitroXPackages/ms-dynamics/PowerAppsGridEmailColumn` +- **type:** `column` + +#### Integer Column + +- **name:** `json::/nitroXPackages/ms-dynamics/PowerAppsGridIntegerColumn` +- **type:** `column` + +#### Lookup Column + +- **name:** `json::/nitroXPackages/ms-dynamics/PowerAppsGridLookupColumn` +- **type:** `column` + +#### OptionSet Column + +- **name:** `json::/nitroXPackages/ms-dynamics/PowerAppsGridOptionSetColumn` +- **type:** `column` + +#### Phone Column + +- **name:** `json::/nitroXPackages/ms-dynamics/PowerAppsGridPhoneColumn` +- **type:** `column` + +#### Text Column + +- **name:** `json::/nitroXPackages/ms-dynamics/PowerAppsGridTextColumn` +- **type:** `column` + +#### MS Dynamics Process Bread Crumb Stage + +- **name:** `json::/nitroXPackages/ms-dynamics/ProcessBreadCrumbStage` +- **type:** `container` +- **tagName:** `button` +- **interactions:** `Activate` +- **attributes:** `Stage Title`, `Stage's GUID'` + +#### MS Dynamics Quick Create Button + +- **name:** `json::/nitroXPackages/ms-dynamics/QuickCreateButton` +- **type:** `container` +- **tagName:** `button` +- **interactions:** `Click`, `Click and Confirm` +- **attributes:** `Label` + +#### MS Dynamics Quick Create Menu Item + +- **name:** `json::/nitroXPackages/ms-dynamics/QuickCreateMenuItem` +- **type:** `container` +- **tagName:** `button` +- **interactions:** `Click`, `Locate` +- **attributes:** `Label` + +#### MS Dynamics Related Entity Menu Item + +- **name:** `json::/nitroXPackages/ms-dynamics/RelatedEntityMenuItem` +- **type:** `container` +- **tagName:** `div` +- **interactions:** `Click`, `Locate` +- **attributes:** `The item's label` + +#### MS Dynamics Related Entity Tab + +- **name:** `json::/nitroXPackages/ms-dynamics/RelatedEntityTabPanel` +- **type:** `container` +- **tagName:** `li,div` +- **interactions:** `Activate`, `Locate` +- **attributes:** `Title`, `Selected`, `Tab Name` + +#### MS Dynamics Rich Text Editor + +- **name:** `json::/nitroXPackages/ms-dynamics/RichTextEditor` +- **type:** `container` +- **tagName:** `div` +- **interactions:** `Set Text`, `Clear`, `Insert Text` +- **attributes:** `Label`, `Value` + +#### MS Dynamics Selection Tree Field Section Item + +- **name:** `json::/nitroXPackages/ms-dynamics/SelectionTreeFieldSectionItem` +- **type:** `container` +- **tagName:** `div` +- **interactions:** `Set`, `Clear` + +#### MS Dynamics Simple Lookup Field Section Item + +- **name:** `json::/nitroXPackages/ms-dynamics/SimpleLookupFieldSectionItem` +- **type:** `container` +- **tagName:** `div` +- **interactions:** `Set`, `Clear`, `New` + +#### MS Dynamics Site Map Group Area + +- **name:** `json::/nitroXPackages/ms-dynamics/SiteMapAreaGroup` +- **type:** `container` +- **tagName:** `lix` +- **attributes:** `Label` + +#### MS Dynamics Site Map Entity + +- **name:** `json::/nitroXPackages/ms-dynamics/SiteMapEntity` +- **type:** `container` +- **tagName:** `li` +- **interactions:** `Click` +- **attributes:** `Label` + +#### MS Dynamics Site Map Group Area + +- **name:** `json::/nitroXPackages/ms-dynamics/SiteMapEntityAreaGroup` +- **type:** `container` +- **tagName:** `li` +- **interactions:** `Click` +- **attributes:** `Label` + +#### MS Dynamics Site Pinned Group + +- **name:** `json::/nitroXPackages/ms-dynamics/SiteMapEntityPinnedGroup` +- **type:** `container` +- **tagName:** `li,div` +- **interactions:** `Activate` +- **attributes:** `Label` + +#### MS Dynamics Site Recent Group + +- **name:** `json::/nitroXPackages/ms-dynamics/SiteMapEntityRecentGroup` +- **type:** `container` +- **tagName:** `li,div` +- **interactions:** `Activate` +- **attributes:** `Label` + +#### MS Dynamics Tab + +- **name:** `json::/nitroXPackages/ms-dynamics/TabPanel` +- **type:** `container` +- **tagName:** `li,div` +- **interactions:** `Activate`, `Locate` +- **attributes:** `Title`, `Accessibility Label`, `Selected`, `Tab Name` + +#### MS Dynamics Text Field Section Item + +- **name:** `json::/nitroXPackages/ms-dynamics/TextFieldSectionItem` +- **type:** `container` +- **tagName:** `div` +- **interactions:** `Set` + +#### MS Dynamics Whole Number Field Section Item + +- **name:** `json::/nitroXPackages/ms-dynamics/WholeNumberFieldSectionItem` +- **type:** `container` +- **tagName:** `div` +- **interactions:** `Set` + +#### Ms Dynamics Column + +- **name:** `json::/nitroXPackages/ms-dynamics/WorkListColumn` +- **type:** `Column` +- **tagName:** `xxx` +- **interactions:** `Click` +- **attributes:** `Column Name`, `Label`, `Value`, `Column Type`, `Cell Type` + +#### MS Work List + +- **name:** `json::/nitroXPackages/ms-dynamics/WorkListTable` +- **type:** `table` +- **tagName:** `div` +- **attributes:** `Columns`, `Column Key`, `ColumnsData`, `Row Values`, `Row Count`, `CheckBox Column` + +#### Text Column + +- **name:** `json::/nitroXPackages/ms-dynamics/WorkListTextColumn` +- **type:** `column` +- **tagName:** `div` + +--- + +## omnistudio (v1.0.0) + +Package includes definitions for Omnistudio FACT elements +**Requires Provar:** >=2.10.2 + +### Components + +#### Omnistudio Select + +- **name:** `json::/com/provar/omnistudio/OmnistudioSelect` +- **type:** `container` +- **tagName:** `omnistudio-omniscript-select` +- **interactions:** `Set` +- **attributes:** `Data Omni Key`, `Visible`, `Disabled`, `Options`, `Label`, `Message`, `Name`, `Placeholder`, `Value Label`, `Read only`, `Required`, `Options`, `Options List`, `Value` +- **child elements:** 2 + +--- + +## runtimeOmnistudio (v1.0.0) + +Package includes definitions for Runtime Omnistudio FACT elements +**Requires Provar:** >=2.10.2 + +### Components + +#### Runtime Omnistudio Radio + +- **name:** `json::/com/provar/runtimeOmnistudio/RuntimeOmnistudioRadio` +- **type:** `container` +- **tagName:** `runtime_omnistudio_omniscript-omniscript-radio` +- **interactions:** `Set` +- **attributes:** `Data Omni Key`, `Visible`, `Disabled`, `Label`, `Name`, `Required`, `Value`, `Type` + +--- + +## salesforce-lwc (v1.9.23) + +Package includes definitions for Salesforce Lightning Web Components FACT elements +**Requires Provar:** >=2.10.2 + +### Components + +#### CheckBox Column + +- **name:** `json::/com/provar/salesforce/lwc/CheckBoxColumn` +- **type:** `container` +- **tagName:** `*` +- **interactions:** `Check`, `Uncheck`, `Select All Rows`, `Un-Select All Rows` + +#### Currency Column + +- **name:** `json::/com/provar/salesforce/lwc/CurrencyColumn` +- **type:** `container` +- **tagName:** `*` +- **attributes:** `Format style` + +#### Date Column + +- **name:** `json::/com/provar/salesforce/lwc/DateColumn` +- **type:** `container` +- **tagName:** `*` +- **interactions:** ` Inline Edit setDate` +- **attributes:** `Day`, `Era`, `Hour`, `Hour12`, `Minute`, `Month`, `Second`, `Weekday`, `Year` + +#### Lightning Accordion + +- **name:** `json::/com/provar/salesforce/lwc/LightningAccordion` +- **type:** `container` +- **tagName:** `lightning-accordion` + +#### Lightning Accordion Section + +- **name:** `json::/com/provar/salesforce/lwc/LightningAccordionSection` +- **type:** `container` +- **tagName:** `lightning-accordion-section` +- **interactions:** `Activate`, `Expand Section`, `Collapse Section` +- **attributes:** `Label`, `Name`, `Visible` +- **child elements:** 1 + +#### Lightning Address Input + +- **name:** `json::/com/provar/salesforce/lwc/LightningAddressInput` +- **type:** `container` +- **tagName:** `lightning-input-address` +- **interactions:** `Set`, `Set Street`, `Set City`, `Set Province`, `Set Country`, `Set Postal Code` +- **attributes:** `Address label`, `Address lookup placeholder`, `City`, `City label`, `City placeholder`, `Country`, `Country disabled`, `Country label`, `Country options`, `Country placeholder`, `Disabled`, `Field level help`, `Postal code`, `Postal code label`, `Postal code placeholder`, `Province`, `Province label`, `Province options`, `Province placeholder`, `Read only`, `Required`, `Show address lookup`, `Street`, `Street label`, `Street placeholder`, `Visible` +- **child elements:** 14 + +#### Lightning Button + +- **name:** `json::/com/provar/salesforce/lwc/LightningButton` +- **type:** `container` +- **tagName:** `lightning-button` +- **interactions:** `Click` +- **attributes:** `Icon name`, `Label`, `Name`, `Title`, `Class`, `Variant`, `Disabled`, `Visible` + +#### Lightning Button Group + +- **name:** `json::/com/provar/salesforce/lwc/LightningButtonGroup` +- **type:** `container` +- **tagName:** `lightning-button-group` + +#### Lightning Button Icon + +- **name:** `json::/com/provar/salesforce/lwc/LightningButtonIcon` +- **type:** `container` +- **tagName:** `lightning-button-icon` +- **interactions:** `Click` +- **attributes:** `Icon name`, `Alternative Text`, `Tooltip`, `Name`, `Title`, `Class`, `Disabled`, `Variant`, `Size`, `Visible` + +#### Lightning Button Icon Stateful + +- **name:** `json::/com/provar/salesforce/lwc/LightningButtonIconStateful` +- **type:** `container` +- **tagName:** `lightning-button-icon-stateful` +- **interactions:** `Toggle On`, `Toggle Off`, `Toggle` +- **attributes:** `Name`, `Title`, `Icon name`, `Alternative Text`, `Size`, `Disabled`, `Class`, `Selected`, `Variant`, `Visible` + +#### Lightning Button Menu + +- **name:** `json::/com/provar/salesforce/lwc/LightningButtonMenu` +- **type:** `container` +- **tagName:** `lightning-button-menu` +- **interactions:** `Click`, `Activate` +- **attributes:** `Value`, `Label`, `Tooltip`, `Access key`, `Title`, `Icon name`, `Icon Size`, `Visible` + +#### Lightning Button Stateful + +- **name:** `json::/com/provar/salesforce/lwc/LightningButtonStateful` +- **type:** `container` +- **tagName:** `lightning-button-stateful` +- **interactions:** `Toggle On`, `Toggle Off`, `Toggle` +- **attributes:** `Icon name when hover`, `Icon name when off`, `Icon name when on`, `Label when hover`, `Label when off`, `Label when on`, `Disabled`, `Class`, `Selected`, `Variant`, `Visible` + +#### Lightning Card + +- **name:** `json::/com/provar/salesforce/lwc/LightningCard` +- **type:** `container` +- **tagName:** `lightning-card` +- **attributes:** `Title`, `Icon name`, `Class`, `Visible` +- **child elements:** 1 + +#### Lightning Checkbox Button Input + +- **name:** `json::/com/provar/salesforce/lwc/LightningCheckboxButtonInput` +- **type:** `container` +- **tagName:** `lightning-input` +- **interactions:** `Check`, `Uncheck`, `Toggle` +- **attributes:** `Name`, `Label`, `Disabled`, `Read only`, `Class`, `Required`, `Checked`, `Type`, `Message`, `Visible` +- **child elements:** 2 + +#### Lightning Checkbox Group + +- **name:** `json::/com/provar/salesforce/lwc/LightningCheckboxGroup` +- **type:** `container` +- **tagName:** `lightning-checkbox-group` +- **interactions:** `Check`, `Uncheck` +- **attributes:** `Name`, `Label`, `Disabled`, `Class`, `Required`, `Message when value missing`, `Visible` +- **child elements:** 2 + +#### Lightning Checkbox Input + +- **name:** `json::/com/provar/salesforce/lwc/LightningCheckboxInput` +- **type:** `container` +- **tagName:** `lightning-input` +- **interactions:** `Check`, `Uncheck`, `Toggle`, `Set` +- **attributes:** `Name`, `Label`, `Disabled`, `Class`, `Required`, `Checked`, `Type`, `Visible` +- **child elements:** 2 + +#### Lightning Column + +- **name:** `json::/com/provar/salesforce/lwc/LightningColumn` +- **type:** `Column` +- **tagName:** `*` +- **interactions:** `Sort Column`, `Wrap Text`, ` Inline Edit set`, `Append Inline Edit`, `Clear Inline Edit`, `Clip Text` +- **attributes:** `Column Key`, `Label`, `Value`, `Column Type`, `Cell Type` + +#### Lightning Combobox + +- **name:** `json::/com/provar/salesforce/lwc/LightningCombobox` +- **type:** `container` +- **tagName:** `lightning-combobox` +- **interactions:** `Set` +- **attributes:** `Disabled`, `Field level help`, `Label`, `Options`, `Options List`, `Message`, `Name`, `Placeholder`, `Read only`, `Required`, `Value Label`, `Value`, `Visible` +- **child elements:** 2 + +#### Lightning Datatable + +- **name:** `json::/com/provar/Table/LightningDatatable` +- **type:** `table` +- **tagName:** `lightning-datatable` +- **attributes:** `Columns`, `ColumnsFields`, `ROW Number`, `CheckBox Column` + +#### Lightning Date Input + +- **name:** `json::/com/provar/salesforce/lwc/LightningDateInput` +- **type:** `container` +- **tagName:** `lightning-input` +- **interactions:** `Clear`, `Set`, `Set Date`, `Set Today` +- **attributes:** `Name`, `Label`, `Disabled`, `Read only`, `Class`, `Required`, `Min`, `Max`, `Value`, `Formatted Value`, `Placeholder`, `Type`, `Message`, `Visible` +- **child elements:** 2 + +#### Lightning Date Time Input + +- **name:** `json::/com/provar/salesforce/lwc/LightningDateTimeInput` +- **type:** `container` +- **tagName:** `lightning-input` +- **interactions:** `Clear`, `Set`, `Set Now`, `Set Date and Time`, `Set Date`, `Set Time` +- **attributes:** `Name`, `Label`, `Disabled`, `Visible`, `Read only`, `Class`, `Required`, `Min`, `Max`, `Value`, `Type`, `Timezone`, `Message`, `Formatted Time Value`, `Formatted Date Value` +- **child elements:** 6 + +#### Lightning Dual Listbox + +- **name:** `json::/com/provar/salesforce/lwc/LightningDualListbox` +- **type:** `container` +- **tagName:** `lightning-dual-listbox` +- **interactions:** `Set` +- **attributes:** `Add button label`, `Visible`, `Disable reordering`, `Disabled`, `Down button label`, `Field level help`, `Label`, `Max`, `Message when range overflow`, `Message when range underflow`, `Message when value missing`, `Min`, `Name`, `Remove button label`, `Required`, `Selected label`, `Source label`, `Up button label`, `Value`, `Required options` +- **child elements:** 6 + +#### Lightning Email Input + +- **name:** `json::/com/provar/salesforce/lwc/LightningEmailInput` +- **type:** `container` +- **tagName:** `lightning-input` +- **interactions:** `Clear`, `Set` +- **attributes:** `Name`, `Visible`, `Label`, `Disabled`, `Read only`, `Class`, `Required`, `Multiple`, `Value`, `Placeholder`, `Type`, `Message` +- **child elements:** 2 + +#### Lightning File Input + +- **name:** `json::/com/provar/salesforce/lwc/LightningFileInput` +- **type:** `container` +- **tagName:** `lightning-input` +- **interactions:** `Upload Files` +- **attributes:** `Accepted File Types`, `Visible`, `Disabled`, `Label`, `Multiple Files Allowed`, `Name`, `Message` + +#### Lightning File Upload + +- **name:** `json::/com/provar/salesforce/lwc/LightningFileUpload` +- **type:** `container` +- **tagName:** `lightning-file-upload` +- **interactions:** `Upload Files` +- **attributes:** `Accepted File Types`, `Visible`, `Disabled`, `Label`, `Multiple Files Allowed`, `Name`, `Message` + +#### Lightning Formatted Address + +- **name:** `json::/com/provar/salesforce/lwc/LightningFormattedAddress` +- **type:** `container` +- **tagName:** `lightning-formatted-address` +- **interactions:** `Click` +- **attributes:** `City`, `Visible`, `Country`, `Disabled`, `Latitude`, `Longitude`, `Postal Code`, `Province`, `Show static map`, `Street` + +#### Lightning Formatted Date Time + +- **name:** `json::/com/provar/salesforce/lwc/LightningFormattedDateTime` +- **type:** `container` +- **tagName:** `lightning-formatted-date-time` +- **attributes:** `Day`, `Visible`, `Era`, `Hour`, `Hour12`, `Minute`, `Month`, `Second`, `Value`, `Weekday`, `Year` + +#### Lightning Formatted Email + +- **name:** `json::/com/provar/salesforce/lwc/LightningFormattedEmail` +- **type:** `container` +- **tagName:** `lightning-formatted-email` +- **interactions:** `Click` +- **attributes:** `Value`, `Visible`, `Label`, `Hide-icon` + +#### Lightning Formatted Lookup + +- **name:** `json::/com/provar/salesforce/lwc/LightningFormattedLookup` +- **type:** `container` +- **tagName:** `lightning-formatted-lookup` +- **interactions:** `Click` +- **attributes:** `Visible`, `Display value` + +#### Lightning Formatted Name + +- **name:** `json::/com/provar/salesforce/lwc/LightningFormattedName` +- **type:** `container` +- **tagName:** `lightning-formatted-name` +- **attributes:** `Salutation`, `Visible`, `First Name`, `Middle Name`, `Last Name`, `Suffix`, `Informal Name`, `Format`, `Inner Text` + +#### Lightning Formatted Number + +- **name:** `json::/com/provar/salesforce/lwc/LightningFormattedNumber` +- **type:** `container` +- **tagName:** `lightning-formatted-number` +- **attributes:** `Value`, `Visible`, `Inner text`, `Format style`, `Currency code`, `Currency display as`, `Maximum fraction digits`, `Maximum significant digits`, `Minimum fraction digits`, `Minimum integer digits`, `Minimum significant digits` + +#### Lightning Formatted Phone + +- **name:** `json::/com/provar/salesforce/lwc/LightningFormattedPhone` +- **type:** `container` +- **tagName:** `lightning-formatted-phone` +- **attributes:** `Value`, `Visible`, `Disabled` + +#### Lightning Formatted Rich Text + +- **name:** `json::/com/provar/salesforce/lwc/LightningFormattedRichText` +- **type:** `container` +- **tagName:** `lightning-formatted-rich-text` +- **attributes:** `Value`, `Visible`, `Text Content`, `innerText` + +#### Lightning Formatted Text + +- **name:** `json::/com/provar/salesforce/lwc/LightningFormattedText` +- **type:** `container` +- **tagName:** `lightning-formatted-text` +- **interactions:** `Click` +- **attributes:** `Value`, `Visible`, `Linkify` + +#### Lightning Formatted Url + +- **name:** `json::/com/provar/salesforce/lwc/LightningFormattedUrl` +- **type:** `container` +- **tagName:** `lightning-formatted-url` +- **interactions:** `Click` +- **attributes:** `Value`, `Visible`, `Label`, `Tooltip` + +#### Lightning Grouped Combobox + +- **name:** `json::/com/provar/salesforce/lwc/LightningGroupedCombobox` +- **type:** `container` +- **tagName:** `lightning-grouped-combobox` +- **interactions:** `Set` +- **attributes:** `Disabled`, `Visible`, `Field level help`, `Label`, `Message`, `Name`, `Placeholder`, `Read only`, `Required`, `Value Label`, `Value` +- **child elements:** 2 + +#### Lightning Helptext + +- **name:** `json::/com/provar/salesforce/lwc/LightningHelptext` +- **type:** `container` +- **tagName:** `lightning-helptext` +- **interactions:** `Hover` +- **attributes:** `Content`, `Visible`, `Icon name` + +#### Lightning Icon + +- **name:** `json::/com/provar/salesforce/lwc/LightningIcon` +- **type:** `container` +- **tagName:** `lightning-icon` +- **interactions:** `Click` +- **attributes:** `Icon name`, `Visible`, `Title`, `Class`, `Size` + +#### Lightning Input Field + +- **name:** `json::/com/provar/salesforce/lwc/LightningInputField` +- **type:** `container` +- **tagName:** `lightning-input-field` +- **attributes:** `Field name` + +#### Lightning Layout + +- **name:** `json::/com/provar/salesforce/lwc/LightningLayout` +- **type:** `container` +- **tagName:** `lightning-layout` + +#### Lightning Layout Item + +- **name:** `json::/com/provar/salesforce/lwc/LightningLayoutItem` +- **type:** `container` +- **tagName:** `lightning-layout-item` + +#### Lightning Location Input + +- **name:** `json::/com/provar/salesforce/lwc/LightningLocationInput` +- **type:** `container` +- **tagName:** `lightning-input-location` +- **interactions:** `Set`, `Set Latitude`, `Set Longitude` +- **attributes:** `Label`, `Visible`, `Disabled`, `Read only`, `Class`, `Required`, `Field level help`, `Latitude`, `Longitude` +- **child elements:** 5 + +#### Lightning Lookup + +- **name:** `json::/com/provar/salesforce/lwc/LightningLookup` +- **type:** `container` +- **tagName:** `lightning-lookup` +- **interactions:** `Set`, `Show All Results` +- **attributes:** `Field name`, `Class`, `Visible`, `Disabled`, `Label`, `Required`, `Value`, `Text Value` + +#### Lightning Lookup Address + +- **name:** `json::/com/provar/salesforce/lwc/LightningLookupAddress` +- **type:** `container` +- **tagName:** `lightning-lookup-address` +- **interactions:** `Enter and Select` +- **attributes:** `Disabled`, `Visible`, `Placeholder` + +#### Lightning Menu Item + +- **name:** `json::/com/provar/salesforce/lwc/LightningMenuItem` +- **type:** `container` +- **tagName:** `lightning-menu-item` +- **interactions:** `Click`, `Deactivate` +- **attributes:** `Checked`, `Visible`, `Disabled`, `Download`, `Href`, `Icon name`, `Label`, `Value` + +#### Lightning Number Input + +- **name:** `json::/com/provar/salesforce/lwc/LightningNumberInput` +- **type:** `container` +- **tagName:** `lightning-input` +- **interactions:** `Clear`, `Set` +- **attributes:** `Name`, `Visible`, `Label`, `Disabled`, `Read only`, `Class`, `Required`, `Value`, `Formatted Value`, `Placeholder`, `Type`, `Message` +- **child elements:** 2 + +#### Lightning Output Field + +- **name:** `json::/com/provar/salesforce/lwc/LightningOutputField` +- **type:** `container` +- **tagName:** `lightning-output-field` +- **attributes:** `Field class`, `Field name` +- **child elements:** 4 + +#### Lightning Password Input + +- **name:** `json::/com/provar/salesforce/lwc/LightningPasswordInput` +- **type:** `container` +- **tagName:** `lightning-input` +- **interactions:** `Clear`, `Set` +- **attributes:** `Name`, `Visible`, `Label`, `Disabled`, `Read only`, `Class`, `Required`, `Value`, `Placeholder`, `Type`, `Pattern`, `Message` +- **child elements:** 2 + +#### Lightning PickList + +- **name:** `json::/com/provar/salesforce/lwc/LightningPicklist` +- **type:** `container` +- **tagName:** `lightning-picklist` +- **interactions:** `Set`, `Set By Index` +- **attributes:** `Disabled`, `Visible`, `Label`, `Name`, `Read only`, `Required`, `Value`, `Options List`, `Options`, `Message` +- **child elements:** 2 + +#### Lightning Pill + +- **name:** `json::/com/provar/salesforce/lwc/LightningPill` +- **type:** `container` +- **tagName:** `lightning-pill` +- **interactions:** `Click`, `Remove` +- **attributes:** `Name`, `Visible`, `Label`, `Href`, `Has error`, `Role` + +#### Lightning Progress Indicator + +- **name:** `json::/com/provar/salesforce/lwc/LightningProgressIndicator` +- **type:** `container` +- **tagName:** `lightning-progress-indicator` +- **attributes:** `Has error`, `Current step`, `Variant`, `Type` + +#### Lightning Progress Step + +- **name:** `json::/com/provar/salesforce/lwc/LightningProgressStep` +- **type:** `container` +- **tagName:** `lightning-progress-step` +- **interactions:** `Select Step` +- **attributes:** `Label`, `Visible`, `Value` + +#### Lightning Radio Group + +- **name:** `json::/com/provar/salesforce/lwc/LightningRadioGroup` +- **type:** `container` +- **tagName:** `lightning-radio-group` +- **interactions:** `Check` +- **attributes:** `Name`, `Visible`, `Label`, `Disabled`, `Class`, `Required`, `Message when value missing` +- **child elements:** 2 + +#### Lightning Record Edit Form + +- **name:** `json::/com/provar/salesforce/lwc/LightningRecordEditForm` +- **type:** `container` +- **tagName:** `lightning-record-edit-form` +- **attributes:** `Object API name`, `Record id`, `Record type id` + +#### Lightning Record Form + +- **name:** `json::/com/provar/salesforce/lwc/LightningRecordForm` +- **type:** `container` +- **tagName:** `lightning-record-form` + +#### Lightning Record Picker + +- **name:** `json::/com/provar/salesforce/lwc/LightningRecordPicker` +- **type:** `container` +- **tagName:** `lightning-record-picker` +- **interactions:** `Set` +- **attributes:** `Disabled`, `Visible`, `Label`, `Name`, `Read only`, `Required`, `Value`, `Message` +- **child elements:** 2 + +#### Lightning Record View Form + +- **name:** `json::/com/provar/salesforce/lwc/LightningRecordViewForm` +- **type:** `container` +- **tagName:** `lightning-record-view-form` + +#### Lightning Rich Text Input + +- **name:** `json::/com/provar/salesforce/lwc/LightningRichTextInput` +- **type:** `container` +- **tagName:** `lightning-input-rich-text` +- **interactions:** `Clear`, `Set` +- **attributes:** `Label`, `Visible`, `Disabled`, `Class`, `Required`, `Value`, `Placeholder` +- **child elements:** 2 + +#### Lightning Search Input + +- **name:** `json::/com/provar/salesforce/lwc/LightningSearchInput` +- **type:** `container` +- **tagName:** `lightning-input` +- **interactions:** `Clear`, `Set` +- **attributes:** `Name`, `Visible`, `Label`, `Disabled`, `Read only`, `Class`, `Required`, `Value`, `Placeholder`, `Type` +- **child elements:** 2 + +#### Lightning Select + +- **name:** `json::/com/provar/salesforce/lwc/LightningSelect` +- **type:** `container` +- **tagName:** `lightning-select` +- **interactions:** `Set` +- **attributes:** `Disabled`, `Visible`, `Label`, `Name`, `Read only`, `Required`, `Value`, `Message` +- **child elements:** 2 + +#### Lightning Slider + +- **name:** `json::/com/provar/salesforce/lwc/LightningSlider` +- **type:** `container` +- **tagName:** `lightning-slider` +- **interactions:** `Set Slider Value` +- **attributes:** `Label`, `Visible`, `Disabled`, `Min`, `Max`, `Step`, `Value` +- **child elements:** 3 + +#### Lightning Tab + +- **name:** `json::/com/provar/salesforce/lwc/LightningTab` +- **type:** `container` +- **tagName:** `lightning-tab` +- **interactions:** `Activate`, `Select Tab` +- **attributes:** `End icon name`, `Visible`, `Icon name`, `Title`, `Label`, `Show error indicator` + +#### Lightning Tabset + +- **name:** `json::/com/provar/salesforce/lwc/LightningTabset` +- **type:** `container` +- **tagName:** `lightning-tabset` +- **interactions:** `Set` +- **attributes:** `Active tab value`, `Visible`, `Title` + +#### Lightning Telephone Input + +- **name:** `json::/com/provar/salesforce/lwc/LightningTelephoneInput` +- **type:** `container` +- **tagName:** `lightning-input` +- **interactions:** `Clear`, `Set` +- **attributes:** `Name`, `Visible`, `Label`, `Disabled`, `Read only`, `Class`, `Required`, `Value`, `Placeholder`, `Type`, `Message` +- **child elements:** 2 + +#### Lightning Text Area + +- **name:** `json::/com/provar/salesforce/lwc/LightningTextArea` +- **type:** `container` +- **tagName:** `lightning-textarea` +- **interactions:** `Clear`, `Set`, `Set Text` +- **attributes:** `Name`, `Visible`, `Label`, `Disabled`, `Read only`, `Class`, `Required`, `Value`, `Placeholder`, `Message`, `Max length` +- **child elements:** 2 + +#### Lightning Text Input + +- **name:** `json::/com/provar/salesforce/lwc/LightningTextInput` +- **type:** `container` +- **tagName:** `lightning-input` +- **interactions:** `Clear`, `Append`, `Set` +- **attributes:** `Name`, `Data-id`, `Message when value missing`, `Message when pattern mismatch`, `Label`, `Disabled`, `Read only`, `Class`, `Required`, `Message`, `Value`, `Placeholder`, `Type`, `Visible` +- **child elements:** 2 + +#### Lightning Tile + +- **name:** `json::/com/provar/salesforce/lwc/LightningTile` +- **type:** `container` +- **tagName:** `lightning-tile` +- **interactions:** `Click` +- **attributes:** `Label`, `Visible`, `Actions`, `Link` + +#### Lightning Time Input + +- **name:** `json::/com/provar/salesforce/lwc/LightningTimeInput` +- **type:** `container` +- **tagName:** `lightning-input` +- **interactions:** `Clear`, `Set` +- **attributes:** `Name`, `Label`, `Disabled`, `Read only`, `Class`, `Required`, `Placeholder`, `Value`, `Formatted Value`, `Min`, `Max`, `Type`, `Visible`, `Message` +- **child elements:** 2 + +#### Lightning Toggle Input + +- **name:** `json::/com/provar/salesforce/lwc/LightningToggleInput` +- **type:** `container` +- **tagName:** `lightning-input` +- **interactions:** `Check`, `Uncheck`, `Set` +- **attributes:** `Name`, `Visible`, `Label`, `Disabled`, `Read only`, `Class`, `Required`, `Checked`, `Type`, `Message` +- **child elements:** 2 + +#### Lightning Url Input + +- **name:** `json::/com/provar/salesforce/lwc/LightningUrlInput` +- **type:** `container` +- **tagName:** `lightning-input` +- **interactions:** `Clear`, `Set` +- **attributes:** `Name`, `Visible`, `Label`, `Disabled`, `Read only`, `Class`, `Required`, `Value`, `Placeholder`, `Type`, `Message` +- **child elements:** 2 + +#### Lightning Vertical Navigation + +- **name:** `json::/com/provar/salesforce/lwc/LightningVerticalNavigation` +- **type:** `container` +- **tagName:** `lightning-vertical-navigation` +- **attributes:** `Selected item` + +#### Lightning Vertical Navigation Item + +- **name:** `json::/com/provar/salesforce/lwc/LightningVerticalNavigationItem` +- **type:** `container` +- **tagName:** `lightning-vertical-navigation-item` +- **interactions:** `Click` +- **attributes:** `Label`, `Visible`, `Name` + +#### Lightning Vertical Navigation Item Badge + +- **name:** `json::/com/provar/salesforce/lwc/LightningVerticalNavigationItemBadge` +- **type:** `container` +- **tagName:** `lightning-vertical-navigation-item-badge` +- **interactions:** `Click` +- **attributes:** `Label`, `Visible`, `Name`, `Badge count` + +#### Lightning Vertical Navigation Item Icon + +- **name:** `json::/com/provar/salesforce/lwc/LightningVerticalNavigationItemIcon` +- **type:** `container` +- **tagName:** `lightning-vertical-navigation-item-icon` +- **interactions:** `Click` +- **attributes:** `Label`, `Visible`, `Name`, `Icon name` + +#### Lightning Vertical Navigation Overflow + +- **name:** `json::/com/provar/salesforce/lwc/LightningVerticalNavigationOverflow` +- **type:** `container` +- **tagName:** `lightning-vertical-navigation-overflow` +- **interactions:** `Click` +- **attributes:** `Text`, `Visible`, `Label` + +#### Lightning Vertical Navigation Section + +- **name:** `json::/com/provar/salesforce/lwc/LightningVerticalNavigationSection` +- **type:** `container` +- **tagName:** `lightning-vertical-navigation-section` +- **attributes:** `Visible`, `Label` + +#### Number Column + +- **name:** `json::/com/provar/salesforce/lwc/NumberColumn` +- **type:** `container` +- **tagName:** `*` +- **attributes:** `Format style`, `Currency code`, `Currency display as`, `Maximum fraction digits`, `Maximum significant digits`, `Minimum fraction digits`, `Minimum integer digits`, `Minimum significant digits` + +#### Phone column + +- **name:** `json::/com/provar/salesforce/lwc/PhoneColumn` +- **type:** `container` +- **tagName:** `*` +- **attributes:** `Disabled` + +#### Text Column + +- **name:** `json::/com/provar/salesforce/lwc/TextColumn` +- **type:** `container` +- **tagName:** `*` +- **attributes:** `Linkify` + +#### Url Column + +- **name:** `json::/com/provar/salesforce/lwc/UrlColumn` +- **type:** `container` +- **tagName:** `*` +- **interactions:** `Click` +- **attributes:** `Tooltip` + +--- + +## screenflow (v1.4.14) + +Package includes definitions for Fact Component +**Requires Provar:** >=2.9.0 + +### Components + +#### Screen Flow Address + +- **name:** `json::/com/provar/Screenflows/ScreenFlowAddress` +- **type:** `container` +- **tagName:** `flowruntime-address` +- **interactions:** `Clear`, `Set`, `Set Street`, `Set City`, `Set Province`, `Set Country`, `Set Postal Code` +- **attributes:** `Address label`, `Visible`, `City`, `Country`, `Postal code`, `Province`, `Street` + +#### Screen Flow Announcer + +- **name:** `json::/com/provar/Screenflows/ScreenFlowAnnouncer` +- **type:** `container` +- **tagName:** `flowruntime-a11y-announcer` + +#### Screen Flow Checkbox Input + +- **name:** `json::/com/provar/Screenflows/ScreenFlowCheckboxInput` +- **type:** `container` +- **tagName:** `flowruntime-flow-screen-input` +- **interactions:** `Check`, `Uncheck`, `Toggle`, `Set` +- **attributes:** `Name`, `DataType`, `Label`, `Visible`, `Class`, `Value` + +#### Screen Flow Choice Lookup + +- **name:** `json::/com/provar/Screenflows/ScreenFlowChoiceLookup` +- **type:** `container` +- **tagName:** `flowruntime-choice-lookup` +- **interactions:** `Clear`, `Set` +- **attributes:** `Name`, `Visible`, `Label`, `Class`, `Required`, `Value`, `Message` + +#### Screen Flow Currency Input + +- **name:** `json::/com/provar/Screenflows/ScreenFlowCurrencyInput` +- **type:** `container` +- **tagName:** `flowruntime-flow-screen-input` +- **interactions:** `Clear`, `Set` +- **attributes:** `Name`, `FieldDataType`, `Label`, `Class`, `Required`, `Visible`, `Value`, `Message` +- **child elements:** 2 + +#### Screen Flow Date Input + +- **name:** `json::/com/provar/Screenflows/ScreenFlowDateInput` +- **type:** `container` +- **tagName:** `flowruntime-flow-screen-input` +- **interactions:** `Clear`, `Set`, `Set Date`, `Set Today` +- **attributes:** `Name`, `Visible`, `FieldDataType`, `Label`, `Class`, `Required`, `Value`, `Placeholder`, `Message` + +#### Screen Flow Date Time Input + +- **name:** `json::/com/provar/Screenflows/ScreenFlowDateTimeInput` +- **type:** `container` +- **tagName:** `flowruntime-flow-screen-input` +- **interactions:** `Clear`, `Set`, `Set Now`, `Set Date and Time`, `Set Date`, `Set Time` +- **attributes:** `Name`, `Visible`, `FieldDataType`, `Label`, `Class`, `Required`, `Value`, `Message` + +#### Screen Flow Display Text + +- **name:** `json::/com/provar/Screenflows/ScreenFlowDisplayText` +- **type:** `container` +- **tagName:** `flowruntime-display-text-lwc` +- **attributes:** `Name`, `Visible`, `Class`, `Value`, `FieldType`, `Label` + +#### Screen Flow Email + +- **name:** `json::/com/provar/Screenflows/ScreenFlowEmail` +- **type:** `container` +- **tagName:** `flowruntime-email` +- **interactions:** `Clear`, `Set` +- **attributes:** `Label`, `Visible`, `Class`, `Required`, `Disabled`, `Read only`, `Value` + +#### Screen Flow Error Content + +- **name:** `json::/com/provar/Screenflows/ScreenFlowErrorContent` +- **type:** `container` +- **tagName:** `flowruntime-error-content` +- **attributes:** `Visible`, `Error` + +#### Screen Flow Long Text Area Input + +- **name:** `json::/com/provar/Screenflows/ScreenFlowLongTextAreaInput` +- **type:** `container` +- **tagName:** `flowruntime-flow-screen-input` +- **interactions:** `Clear`, `Set` +- **attributes:** `Name`, `Visible`, `Label`, `Class`, `Required`, `Value`, `FieldType`, `Message` +- **child elements:** 2 + +#### Screen Flow Lwc Body + +- **name:** `json::/com/provar/Screenflows/ScreenFlowLwcBody` +- **type:** `container` +- **tagName:** `flowruntime-lwc-body` +- **attributes:** `FlowLabel`, `Screen Developer Name`, `Current Flow Version Id` + +#### Screen Flow Lwc Header + +- **name:** `json::/com/provar/Screenflows/ScreenFlowLwcHeader` +- **type:** `container` +- **tagName:** `flowruntime-lwc-header` + +#### Screen Flow Multi Checkbox Lwc + +- **name:** `json::/com/provar/Screenflows/ScreenFlowMultiCheckboxLwc` +- **type:** `container` +- **tagName:** `flowruntime-multi-checkbox-lwc` +- **interactions:** `Set`, `Clear`, `Select All` +- **attributes:** `Name`, `Visible`, `Label`, `Required`, `Class`, `Value`, `Message` + +#### Screen Flow Name + +- **name:** `json::/com/provar/Screenflows/ScreenFlowName` +- **type:** `container` +- **tagName:** `flowruntime-name` +- **interactions:** `Clear`, `Set`, `Set`, `Set`, `Set`, `Set`, `Set`, `Set` +- **attributes:** `Label`, `Visible`, `Disabled`, `Read only`, `Class`, `First Name`, `Last Name`, `Message` + +#### Screen Flow Navigation Bar + +- **name:** `json::/com/provar/Screenflows/ScreenFlowNavigationBar` +- **type:** `container` +- **tagName:** `flowruntime-navigation-bar` + +#### Screen Flow Number Input + +- **name:** `json::/com/provar/Screenflows/ScreenFlowNumberInput` +- **type:** `container` +- **tagName:** `flowruntime-flow-screen-input` +- **interactions:** `Clear`, `Set` +- **attributes:** `Name`, `Visible`, `Label`, `Class`, `Required`, `Value`, `FieldDataType`, `Message` + +#### Screen Flow Password Input + +- **name:** `json::/com/provar/Screenflows/ScreenFlowPasswordInput` +- **type:** `container` +- **tagName:** `flowruntime-flow-screen-input` +- **interactions:** `Clear`, `Set` +- **attributes:** `Name`, `Visible`, `Label`, `Class`, `Required`, `Value`, `FieldType` + +#### Screen Flow Phone + +- **name:** `json::/com/provar/Screenflows/ScreenFlowPhone` +- **type:** `container` +- **tagName:** `flowruntime-phone` +- **interactions:** `Clear`, `Set` +- **attributes:** `Label`, `Visible`, `Class`, `Required`, `Read only`, `Value`, `Placeholder` + +#### Screen Flow Picklist Input + +- **name:** `json::/com/provar/Screenflows/ScreenFlowPicklistInput` +- **type:** `container` +- **tagName:** `flowruntime-picklist-input-lwc` +- **interactions:** `Clear`, `Set` +- **attributes:** `Name`, `Visible`, `Label`, `Class`, `Options`, `Options List`, `Required`, `Value`, `Message` + +#### Screen Flow Radio Button Input + +- **name:** `json::/com/provar/Screenflows/ScreenFlowRadioButtonInput` +- **type:** `container` +- **tagName:** `flowruntime-radio-button-input-lwc` +- **interactions:** `Set`, `Clear` +- **attributes:** `Name`, `Label`, `Required`, `Class`, `Value`, `Visible`, `Message` + +#### Screen Flow Screen Field + +- **name:** `json::/com/provar/Screenflows/ScreenFlowScreenField` +- **type:** `container` +- **tagName:** `flowruntime-screen-field` +- **attributes:** `Field Name` + +#### Screen Flow Section With Header + +- **name:** `json::/com/provar/Screenflows/ScreenFlowSectionWithHeader` +- **type:** `container` +- **tagName:** `flowruntime-section-with-header` +- **attributes:** `Section Name`, `Visible`, `Section Heading` + +#### Screen Flow Slider + +- **name:** `json::/com/provar/Screenflows/ScreenFlowSlider` +- **type:** `container` +- **tagName:** `flowruntime-slider` +- **interactions:** `Set Slider Value` +- **attributes:** `Label`, `Visible`, `Disabled`, `Min`, `Max`, `Step`, `Value` + +#### Screen Flow Text Input + +- **name:** `json::/com/provar/Screenflows/ScreenFlowTextInput` +- **type:** `container` +- **tagName:** `flowruntime-flow-screen-input` +- **interactions:** `Clear`, `Set` +- **attributes:** `Name`, `Visible`, `Label`, `Class`, `Required`, `Value`, `FieldDataType`, `Message` + +#### Screen Flow Toggle + +- **name:** `json::/com/provar/Screenflows/ScreenFlowToggle` +- **type:** `container` +- **tagName:** `flowruntime-toggle` +- **interactions:** `Check`, `Uncheck`, `Toggle`, `Set` +- **attributes:** `Label`, `Visible`, `Message Toggle Active`, `Message Toggle Inactive`, `Class`, `Disabled`, `Value` + +#### Screen Flow Url + +- **name:** `json::/com/provar/Screenflows/ScreenFlowUrl` +- **type:** `container` +- **tagName:** `flowruntime-url` +- **interactions:** `Clear`, `Set` +- **attributes:** `Label`, `Visible`, `Class`, `Required`, `Disabled`, `Read only`, `Value`, `Message` + +--- + +## vlocityIns (v1.0.13) + +Package includes definitions for Vlocity Ins Components FACT elements +**Requires Provar:** >=2.10.2 + +### Components + +#### Vlocity Ins Block + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsBlock` +- **type:** `container` +- **tagName:** `vlocity_ins-block` +- **attributes:** `Data Style Id`, `Data Action Index`, `Data Omni Key` + +#### Vlocity Ins Button + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsButton` +- **type:** `container` +- **tagName:** `vlocity_ins-button` +- **interactions:** `Click` +- **attributes:** `Data Omni Key`, `Visible`, `Disabled`, `Label`, `Name`, `Title`, `Variant` + +#### Vlocity Button Group + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsButtonGroup` +- **type:** `container` +- **tagName:** `c-atlas-d-x-radio-extended-s` +- **interactions:** `Click` +- **attributes:** `Data Omni Key`, `Visible` + +#### Vlocity Flex Card State + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsCardState` +- **type:** `container` +- **tagName:** `vlocity_ins-flex-card-state` +- **interactions:** `Click` +- **attributes:** `Data Omni Key`, `Visible` + +#### Vlocity Ins Checkbox + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsCheckbox` +- **type:** `container` +- **tagName:** `vlocity_ins-omniscript-checkbox` +- **interactions:** `Check`, `Uncheck`, `Toggle`, `Set` +- **attributes:** `Data Omni Key`, `Visible`, `Name`, `Label`, `Disabled`, `Class`, `Required`, `Checked`, `Type` +- **child elements:** 2 + +#### Vlocity Ins Combobox + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsCombobox` +- **type:** `container` +- **tagName:** `vlocity_ins-combobox` +- **interactions:** `Set` +- **attributes:** `Data Omni Key`, `Visible`, `Disabled`, `Options`, `Label`, `Message`, `Name`, `Placeholder`, `Value Label`, `Read only`, `Required`, `Options`, `Options List`, `Value` +- **child elements:** 2 + +#### Vlocity Ins Currency + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsCurrency` +- **type:** `container` +- **tagName:** `vlocity_ins-omniscript-currency` +- **interactions:** `Clear`, `Set` +- **attributes:** `Data Omni Key`, `Visible`, `Name`, `Label`, `Required`, `Value`, `Message` +- **child elements:** 2 + +#### Vlocity Ins Custom Lwc + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsCustomLwc` +- **type:** `container` +- **tagName:** `vlocity_ins-omniscript-custom-lwc` +- **attributes:** `Data Omni Key`, `Visible` + +#### Vlocity Ins Date + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsDate` +- **type:** `container` +- **tagName:** `vlocity_ins-omniscript-date` +- **interactions:** `Set`, `Set Date`, `Set Today`, `Clear` +- **attributes:** `Data Omni Key`, `Visible`, `Disabled`, `Label`, `Name`, `Placeholder`, `Read only`, `Required`, `Display Value`, `Min`, `Max`, `Value` +- **child elements:** 2 + +#### Vlocity Ins Dr Extract Action + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsDrExtractAction` +- **type:** `container` +- **tagName:** `vlocity_ins-omniscript-dr-extract-action` +- **interactions:** `Click` +- **attributes:** `Data Omni Key`, `Class`, `Visible`, `Disabled`, `Label`, `Name`, `Title`, `Variant` + +#### Vlocity Ins Edit Block + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsEditBlock` +- **type:** `container` +- **tagName:** `vlocity_ins-omniscript-edit-block` +- **attributes:** `Data Omni Key` +- **child elements:** 1 + +#### Vlocity Ins Edit Block Wrapper + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsEditBlockWrapper` +- **type:** `container` +- **tagName:** `vlocity_ins-omniscript-edit-block-wrapper` +- **attributes:** `Visible`, `Data Omni Key` +- **child elements:** 1 + +#### Vlocity Ins Email + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsEmail` +- **type:** `container` +- **tagName:** `vlocity_ins-omniscript-email` +- **interactions:** `Clear`, `Set` +- **attributes:** `Data Omni Key`, `Visible`, `Name`, `Label`, `Disabled`, `Read only`, `Class`, `Required`, `Multiple`, `Value`, `Placeholder`, `Type`, `Message` +- **child elements:** 2 + +#### Vlocity Ins Flex Action + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsFlexAction` +- **type:** `container` +- **tagName:** `vlocity_ins-flex-action` +- **interactions:** `Click` +- **attributes:** `Data Style Id`, `Data Action Index`, `Data Omni Key`, `Data Omni Key`, `Visible` + +#### Vlocity Form Add Botton + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsFormAddBotton` +- **type:** `container` +- **tagName:** `div` +- **interactions:** `Click` +- **attributes:** `Data Omni Key`, `Visible` + +#### Vlocity Ins Formula + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsFormula` +- **type:** `container` +- **tagName:** `vlocity_ins-omniscript-formula` +- **interactions:** `Clear`, `Set` +- **attributes:** `Data Omni Key`, `Visible`, `Name`, `Label`, `Required`, `Value`, `Message` +- **child elements:** 2 + +#### Vlocity Ins Input + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsInput` +- **type:** `container` +- **tagName:** `vlocity_ins-input` +- **interactions:** `Clear`, `Set`, `Append` +- **attributes:** `Data Omni Key`, `Visible`, `Disabled`, `Label`, `Name`, `Placeholder`, `Read only`, `Required`, `Message`, `Value` +- **child elements:** 2 + +#### Vlocity Ip Action + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsIpAction` +- **type:** `container` +- **tagName:** `vlocity_ins-omniscript-ip-action` +- **interactions:** `Click` +- **attributes:** `Data Omni Key`, `Visible`, `Disabled`, `Label`, `Name`, `Title`, `Variant` + +#### Vlocity Ins Lookup + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsLookup` +- **type:** `container` +- **tagName:** `vlocity_ins-omniscript-lookup` +- **interactions:** `Set` +- **attributes:** `Data Omni Key`, `Visible`, `Disabled`, `Options`, `Label`, `Message`, `Name`, `Placeholder`, `Value Label`, `Read only`, `Required`, `Options`, `Options List`, `Value` +- **child elements:** 2 + +#### Vlocity Ins Multi Select + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsMultiSelect` +- **type:** `container` +- **tagName:** `vlocity_ins-omniscript-multiselect` +- **interactions:** `Check`, `Uncheck` +- **attributes:** `Data Omni Key`, `Visible`, `Name`, `Label`, `Disabled`, `Required` + +#### Vlocity Ins Navigate Action + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsNavigateAction` +- **type:** `container` +- **tagName:** `vlocity_ins-omniscript-navigate-action` +- **interactions:** `Click` +- **attributes:** `Data Omni Key`, `Visible`, `Disabled`, `Label`, `Name`, `Title`, `Variant` + +#### Vlocity Ins Number + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsNumber` +- **type:** `container` +- **tagName:** `vlocity_ins-omniscript-number` +- **interactions:** `Clear`, `Set` +- **attributes:** `Data Omni Key`, `Visible`, `Name`, `Label`, `Disabled`, `Read only`, `Required`, `Value`, `Formatted Value`, `Placeholder`, `Type`, `Message` +- **child elements:** 2 + +#### Vlocity Ins Omniscript Block + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsOmniscriptBlock` +- **type:** `container` +- **tagName:** `vlocity_ins-omniscript-block` +- **attributes:** `Data Style Id`, `Data Action Index`, `Data Omni Key` + +#### Vlocity Ins Output Field + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsOutputField` +- **type:** `container` +- **tagName:** `vlocity_ins-output-field` +- **attributes:** `Data Style Id`, `Data Omni Key` + +#### Vlocity Ins Places Type Ahead + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsPlacesTypeAhead` +- **type:** `container` +- **tagName:** `vlocity_ins-omniscript-places-typeahead` +- **interactions:** `Clear`, `Set`, `Append` +- **attributes:** `Data Omni Key`, `Visible`, `Disabled`, `Label`, `Name`, `Placeholder`, `Required`, `Message`, `Value` +- **child elements:** 2 + +#### Vlocity Ins Radio + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsRadio` +- **type:** `container` +- **tagName:** `vlocity_ins-omniscript-radio` +- **interactions:** `Set` +- **attributes:** `Data Omni Key`, `Visible`, `Disabled`, `Label`, `Name`, `Required`, `Value`, `Type` + +#### Vlocity Ins Select + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsSelect` +- **type:** `container` +- **tagName:** `vlocity_ins-omniscript-select` +- **interactions:** `Set` +- **attributes:** `Data Omni Key`, `Visible`, `Disabled`, `Options`, `Label`, `Message`, `Name`, `Placeholder`, `Value Label`, `Read only`, `Required`, `Options`, `Options List`, `Value` +- **child elements:** 2 + +#### Vlocity Ins Set Values + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsSetValues` +- **type:** `container` +- **tagName:** `vlocity_ins-omniscript-set-values` +- **interactions:** `Click` +- **attributes:** `Data Omni Key`, `Visible`, `Disabled`, `Label`, `Name`, `Title`, `Variant` + +#### Vlocity Ins Step + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsStep` +- **type:** `container` +- **tagName:** `vlocity_ins-omniscript-step` +- **attributes:** `Data Omni Key` + +#### Vlocity Ins Telephone + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsTelephone` +- **type:** `container` +- **tagName:** `vlocity_ins-omniscript-telephone` +- **interactions:** `Clear`, `Set` +- **attributes:** `Data Omni Key`, `Visible`, `Name`, `Label`, `Disabled`, `Read only`, `Required`, `Value`, `Formatted Value`, `Placeholder`, `Type`, `Message` +- **child elements:** 2 + +#### Vlocity Ins Text + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsText` +- **type:** `container` +- **tagName:** `vlocity_ins-omniscript-text` +- **interactions:** `Clear`, `Set`, `Append` +- **attributes:** `Data Omni Key`, `Visible`, `Disabled`, `Label`, `Name`, `Placeholder`, `Read only`, `Required`, `Message`, `Value` +- **child elements:** 2 + +#### Vlocity Ins Text Area + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsTextArea` +- **type:** `container` +- **tagName:** `vlocity_ins-omniscript-textarea` +- **interactions:** `Clear`, `Set`, `Append` +- **attributes:** `Data Omni Key`, `Visible`, `Name`, `Label`, `Disabled`, `Read only`, `Class`, `Required`, `Value`, `Placeholder`, `Message`, `Max length` +- **child elements:** 2 + +#### Vlocity Ins Transform Action + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsTransformAction` +- **type:** `container` +- **tagName:** `vlocity_ins-omniscript-dr-transform-action` +- **interactions:** `Click` +- **attributes:** `Data Omni Key`, `Visible`, `Disabled`, `Label`, `Name`, `Title`, `Variant` + +#### Vlocity Ins Type Ahead + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsTypeAhead` +- **type:** `container` +- **tagName:** `vlocity_ins-omniscript-typeahead` +- **interactions:** `Clear`, `Set`, `Append` +- **attributes:** `Data Omni Key`, `Visible`, `Disabled`, `Label`, `Name`, `Placeholder`, `Required`, `Message`, `Value` +- **child elements:** 2 + +#### Vlocity Ins Type Ahead Block + +- **name:** `json::/com/provar/vlocityInsCloud/VlocityInsTypeAheadBlock` +- **type:** `container` +- **tagName:** `vlocity_ins-omniscript-typeahead-block` +- **attributes:** `Data Omni Key` +- **child elements:** 1 + +--- diff --git a/docs/development.md b/docs/development.md index 808726e4..a84cffa9 100644 --- a/docs/development.md +++ b/docs/development.md @@ -19,18 +19,19 @@ Everything you need to clone, build, run locally, and test changes to `@provarte - [Full test + lint gate](#full-test--lint-gate) - [Linting and formatting](#linting-and-formatting) - [Developing the MCP server locally](#developing-the-mcp-server-locally) + - [MCP Inspector (interactive tool testing)](#mcp-inspector-interactive-tool-testing) - [Troubleshooting](#troubleshooting) --- ## Prerequisites -| Tool | Minimum version | How to check | -|------|----------------|--------------| -| Node.js | 18.0.0 | `node --version` | -| npm | 8+ (ships with Node 18) | `npm --version` | -| Salesforce CLI (`sf`) | any recent | `sf --version` | -| Git | any | `git --version` | +| Tool | Minimum version | How to check | +| --------------------- | ----------------------- | ---------------- | +| Node.js | 18.0.0 | `node --version` | +| npm | 8+ (ships with Node 18) | `npm --version` | +| Salesforce CLI (`sf`) | any recent | `sf --version` | +| Git | any | `git --version` | The Salesforce CLI is required to run the compiled plugin commands in development mode (`bin/dev.js`). Install it from [developer.salesforce.com/tools/salesforcecli](https://developer.salesforce.com/tools/salesforcecli) if needed. @@ -99,11 +100,13 @@ provardx-cli/ ## Build system overview This project uses **[Wireit](https://github.com/google/wireit)** as a build orchestrator on top of npm scripts. Wireit provides: + - **Incremental builds** — only recompiles files that changed - **Dependency ordering** — `build` depends on `compile` + `lint`; `test` depends on `test:compile` + `test:only` + `lint` - **Caching** — subsequent `npm run compile` runs skip unchanged files The compile step does two things: + 1. `tsc` — transpiles `src/**/*.ts` → `lib/` 2. `shx cp src/mcp/rules/*.json lib/mcp/rules/` — copies the JSON rules file (TypeScript does not copy non-`.ts` assets automatically) @@ -144,6 +147,7 @@ node bin/dev.js provar mcp start --allowed-paths /path/to/project ``` > **Tip:** On Unix you can `chmod +x bin/dev.js` and run it as `./bin/dev.js` or add a shell alias: +> > ```sh > alias sfdev="node $(pwd)/bin/dev.js" > sfdev provar mcp start @@ -159,11 +163,11 @@ node bin/dev.js provar mcp start --allowed-paths /path/to/project Unit tests live in `test/unit/mcp/` and cover all pure validator functions (no filesystem, no network, no Salesforce CLI required). There are three ways to run them, depending on your workflow: -| Command | When to use | -|---------|-------------| -| `npm run test:dev` | **Daily development** — always executes, never cached, fastest for iteration | +| Command | When to use | +| -------------------- | ------------------------------------------------------------------------------ | +| `npm run test:dev` | **Daily development** — always executes, never cached, fastest for iteration | | `npm run test:watch` | **TDD mode** — re-runs automatically whenever a `src/` or `test/` file changes | -| `npm run test:only` | **CI / pre-commit** — wireit-managed; skips if no files changed since last run | +| `npm run test:only` | **CI / pre-commit** — wireit-managed; skips if no files changed since last run | ```sh # Always runs — no caching, best for iterating on changes @@ -188,13 +192,13 @@ The test runner is **Mocha** with `ts-node/esm` as the loader, so test files are **Current test files and what they cover:** -| File | Covers | -|------|--------| -| `testCaseValidate.test.ts` | XML schema rules TC_001–TC_035 | -| `pageObjectValidate.test.ts` | Java PO rules PO_001–PO_080 | -| `pathPolicy.test.ts` | Path security policy (allowed paths, traversal) | -| `hierarchyValidate.test.ts` | Suite/plan/project structural + naming rules, `buildHierarchySummary` | -| `bestPracticesEngine.test.ts` | Scoring formula (exact Lambda parity), `runBestPractices` | +| File | Covers | +| ----------------------------- | --------------------------------------------------------------------- | +| `testCaseValidate.test.ts` | XML schema rules TC_001–TC_035 | +| `pageObjectValidate.test.ts` | Java PO rules PO_001–PO_080 | +| `pathPolicy.test.ts` | Path security policy (allowed paths, traversal) | +| `hierarchyValidate.test.ts` | Suite/plan/project structural + naming rules, `buildHierarchySummary` | +| `bestPracticesEngine.test.ts` | Scoring formula (exact Lambda parity), `runBestPractices` | ### Coverage @@ -282,8 +286,11 @@ Update `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) "command": "node", "args": [ "/absolute/path/to/provardx-cli/bin/dev.js", - "provar", "mcp", "start", - "--allowed-paths", "/path/to/your/provar/project" + "provar", + "mcp", + "start", + "--allowed-paths", + "/path/to/your/provar/project" ] } } @@ -292,6 +299,49 @@ Update `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) Restart Claude Desktop after saving. +### MCP Inspector (interactive tool testing) + +The [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) is a browser-based UI for calling MCP tools directly — no AI client required. Use it to iterate on tool schemas and verify request/response shapes. + +```sh +# Compile first, then launch the Inspector against the dev server +npm run compile +npx @modelcontextprotocol/inspector node bin/dev.js provar mcp start --allowed-paths /absolute/path/to/your/provar/project +``` + +> **MCP resources in dev mode:** `bin/dev.js` runs the TypeScript source via ts-node. The server automatically falls back to reading bundled Markdown resources (e.g. `provar://nitrox/component-catalog`, `provar://docs/step-reference`) from the repo-root `docs/` directory when the compiled `lib/mcp/docs/` path is not present, so resources work correctly after `npm run compile` with either entry point. + +> **Run this as a single line.** Line-wrapping the command (e.g. with a newline before `--allowed-paths`) causes the shell to treat `--allowed-paths` as a separate command and fail with `command not found`. Use `\` for explicit line continuation in bash if needed. + +If the Inspector fails with `Proxy Server PORT IS IN USE at port 6277`, a previous Inspector process is still running. Free the ports and try again: + +```sh +# macOS / Linux +kill $(lsof -ti :6277) $(lsof -ti :6274) 2>/dev/null + +# Windows PowerShell +foreach ($p in 6277,6274) { $c = Get-NetTCPConnection -LocalPort $p -EA 0; if ($c) { Stop-Process -Id $c.OwningProcess -Force } } +``` + +The command starts two processes: the MCP Inspector proxy (default port **6277**) and opens a browser tab at **http://localhost:6274**. From there you can: + +- Browse all registered tools and their input schemas +- Invoke any tool with custom JSON input +- Inspect the raw JSON-RPC request and response +- View server stderr logs in the **Notifications** panel + +> **Windows path note:** Use forward slashes or escaped backslashes in `--allowed-paths` (e.g. `C:/Users/you/provar-project` or `C:\\Users\\you\\provar-project`). The path must match one of the roots configured in `provardx-properties.json`. + +To pin a specific Inspector version or avoid repeated downloads, install it once globally: + +```sh +npm install -g @modelcontextprotocol/inspector +# Then run as: +mcp-inspector node bin/dev.js provar mcp start --allowed-paths /path/to/project +``` + +--- + ### Debugging MCP tool calls The MCP server writes structured logs to **stderr**. To see them when running manually: diff --git a/docs/mcp.md b/docs/mcp.md index 5024925e..6cc4f8d7 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -72,6 +72,7 @@ The Provar DX CLI ships with a built-in **Model Context Protocol (MCP) server** - [provar.loop.db](#provarloopdb) - [MCP Resources](#mcp-resources) - [provar://docs/step-reference](#provardocsstep-reference) + - [provar://nitrox/component-catalog](#provarnitroxcomponent-catalog) - [AI loop pattern](#ai-loop-pattern) - [Quality scores explained](#quality-scores-explained) - [API compatibility — `xml` vs `xml_content`](#api-compatibility--xml-vs-xml_content) @@ -1577,6 +1578,8 @@ NitroX is Provar's **Hybrid Model** for locators. Instead of hand-written Java P The five `provar_nitrox_*` tools let an AI agent discover existing NitroX page objects, read them as training context, validate new ones against the schema, generate fresh components from a description, and apply surgical edits via JSON merge-patch. +> **Tip:** Before calling `provar_nitrox_generate`, read the `provar://nitrox/component-catalog` resource to understand the component types, tagName conventions, interaction titles, and attribute patterns from the shipped base packages. + > **Note:** NitroX page objects are read and written directly from disk using the standard file-system path policy (`--allowed-paths`). No `sf` subprocess is involved. --- @@ -1935,7 +1938,7 @@ Generate a Provar XML test case that connects to an **external database** (SQL S ## MCP Resources -The Provar MCP server also exposes one **MCP resource** — structured reference content that AI clients can read directly from the server. +The Provar MCP server exposes **MCP resources** — structured reference content that AI clients can read directly from the server. --- @@ -1950,6 +1953,17 @@ The resource content is the same as `docs/PROVAR_TEST_STEP_REFERENCE.md` in this --- +### `provar://nitrox/component-catalog` + +Catalog of all shipped NitroX (Hybrid Model) base component packages. Lists every package with its components, types, tagNames, interactions, and attributes. Read this before calling `provar_nitrox_generate` to understand available component patterns and naming conventions. + +**URI:** `provar://nitrox/component-catalog` +**MIME type:** `text/markdown` + +The resource content is the same as `docs/NITROX_COMPONENT_CATALOG.md` in this repository, compiled into the package at build time. To regenerate the catalog after Provar ships updated NitroX packages, run `node scripts/generate-nitrox-catalog.cjs` on a machine with Provar NitroX installed, then commit the result. + +--- + ## AI loop pattern The automation tools are designed to support an **AI-driven fix loop**: an agent can iteratively improve test quality without leaving the chat session. diff --git a/package.json b/package.json index 60481051..99b723a7 100644 --- a/package.json +++ b/package.json @@ -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.16", + "version": "1.5.0-beta.17", "mcpName": "io.github.ProvarTesting/provar", "license": "BSD-3-Clause", "plugins": [ @@ -146,10 +146,11 @@ ] }, "compile": { - "command": "tsc -p . --pretty --incremental && shx mkdir -p lib/mcp/rules && shx cp src/mcp/rules/*.json lib/mcp/rules/ && shx mkdir -p lib/mcp/docs && shx cp docs/PROVAR_TEST_STEP_REFERENCE.md lib/mcp/docs/", + "command": "tsc -p . --pretty --incremental && shx mkdir -p lib/mcp/rules && shx cp src/mcp/rules/*.json lib/mcp/rules/ && shx mkdir -p lib/mcp/docs && shx cp docs/PROVAR_TEST_STEP_REFERENCE.md lib/mcp/docs/ && shx cp docs/NITROX_COMPONENT_CATALOG.md lib/mcp/docs/", "files": [ "src/**/*.ts", "src/mcp/rules/*.json", + "docs/NITROX_COMPONENT_CATALOG.md", "**/tsconfig.json", "messages/**" ], diff --git a/scripts/generate-nitrox-catalog.cjs b/scripts/generate-nitrox-catalog.cjs new file mode 100644 index 00000000..28e7a57a --- /dev/null +++ b/scripts/generate-nitrox-catalog.cjs @@ -0,0 +1,129 @@ +#!/usr/bin/env node +/** + * Developer utility: regenerate docs/NITROX_COMPONENT_CATALOG.md from the + * local Provar NitroX base package installation. + * + * Run this whenever Provar ships updated NitroX packages, then commit the result. + * + * Usage: + * node scripts/generate-nitrox-catalog.cjs + * + * Requires: ~/Provar/.nitroX/com/provar/fact/_extracted_all to exist. + * (Run Provar NitroX at least once on this machine to extract the base packages.) + */ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +const EXTRACTED_ALL_DIR = path.join(os.homedir(), 'Provar', '.nitroX', 'com', 'provar', 'fact', '_extracted_all'); +const OUTPUT_FILE = path.join(__dirname, '..', 'docs', 'NITROX_COMPONENT_CATALOG.md'); + +function safeReadJson(filePath) { + try { + return JSON.parse(fs.readFileSync(filePath, 'utf-8')); + } catch { + return null; + } +} + +function collectComponentFiles(pkgDir) { + const componentsDir = path.join(pkgDir, 'components'); + if (!fs.existsSync(componentsDir)) return []; + try { + return fs + .readdirSync(componentsDir) + .filter((f) => f.endsWith('.cp.json') || f.endsWith('.po.json')) + .sort() + .map((f) => path.join(componentsDir, f)); + } catch { + return []; + } +} + +function renderComponent(comp) { + const lines = []; + const heading = comp.label ?? comp.name ?? '(unnamed)'; + lines.push(`#### ${heading}`, ''); + if (comp.name) lines.push(`- **name:** \`${comp.name}\``); + if (comp.type) lines.push(`- **type:** \`${comp.type}\``); + if (comp.tagName) lines.push(`- **tagName:** \`${comp.tagName}\``); + + const interactions = (comp.interactions ?? []).map((i) => i.title ?? i.name ?? '').filter(Boolean); + if (interactions.length > 0) { + lines.push(`- **interactions:** ${interactions.map((n) => `\`${n}\``).join(', ')}`); + } + + const attributes = (comp.attributes ?? []).map((a) => a.title ?? a.attributeName ?? '').filter(Boolean); + if (attributes.length > 0) { + lines.push(`- **attributes:** ${attributes.map((n) => `\`${n}\``).join(', ')}`); + } + + const elementCount = (comp.elements ?? []).length; + if (elementCount > 0) lines.push(`- **child elements:** ${elementCount}`); + + lines.push(''); + return lines.join('\n'); +} + +function buildCatalog() { + if (!fs.existsSync(EXTRACTED_ALL_DIR)) { + console.error(`ERROR: packages not found at ${EXTRACTED_ALL_DIR}`); + console.error('Run Provar NitroX on this machine to extract base packages, then retry.'); + process.exit(1); + } + + const pkgDirEntries = fs + .readdirSync(EXTRACTED_ALL_DIR, { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .sort((a, b) => a.name.localeCompare(b.name)); + + const lines = [ + '# NitroX Component Package Catalog', + '', + 'Shipped base NitroX (Hybrid Model) component packages.', + 'Use as a reference when generating new NitroX components — match naming conventions,', + 'type strings, tagNames, interaction titles, and attribute names from these shipped packages.', + '', + '---', + '', + ]; + + for (const entry of pkgDirEntries) { + const pkgDir = path.join(EXTRACTED_ALL_DIR, entry.name); + const meta = safeReadJson(path.join(pkgDir, 'package.json')) ?? {}; + + const displayName = meta.name ?? entry.name; + const displayVersion = meta.version ? ` (v${meta.version})` : ''; + lines.push(`## ${displayName}${displayVersion}`); + + if (meta.description) lines.push('', meta.description); + if (meta.provarVersion) lines.push(`**Requires Provar:** ${meta.provarVersion}`); + lines.push(''); + + const componentFiles = collectComponentFiles(pkgDir); + if (componentFiles.length === 0) { + lines.push('_No component definitions found._', '', '---', ''); + continue; + } + + lines.push('### Components', ''); + for (const compFile of componentFiles) { + const parsed = safeReadJson(compFile); + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + lines.push(renderComponent(parsed)); + } + } + + lines.push('---', ''); + } + + return lines.join('\n'); +} + +const catalog = buildCatalog(); +fs.writeFileSync(OUTPUT_FILE, catalog, 'utf-8'); +const lineCount = catalog.split('\n').length; +console.log(`Written: docs/NITROX_COMPONENT_CATALOG.md (${lineCount} lines)`); diff --git a/server.json b/server.json index 54eca322..4bf8741a 100644 --- a/server.json +++ b/server.json @@ -14,12 +14,12 @@ "url": "https://github.com/ProvarTesting/provardx-cli", "source": "github" }, - "version": "1.5.0-beta.16", + "version": "1.5.0-beta.17", "packages": [ { "registryType": "npm", "identifier": "@provartesting/provardx-cli", - "version": "1.5.0-beta.16", + "version": "1.5.0-beta.17", "transport": { "type": "stdio" }, diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 6d63b3d6..56bde3e7 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -6,7 +6,7 @@ */ import { createRequire } from 'node:module'; -import { readFileSync } from 'node:fs'; +import { existsSync, readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; @@ -105,7 +105,36 @@ export function createProvarMcpServer(config: ServerConfig): McpServer { registerAllPrompts(server); // ── Documentation resources ────────────────────────────────────────────────── - const docsDir = join(dirname(fileURLToPath(import.meta.url)), 'docs'); + const docsDir = resolveDocsDir(dirname(fileURLToPath(import.meta.url))); + + server.resource( + 'provar-nitrox-component-catalog', + 'provar://nitrox/component-catalog', + { + description: + 'Catalog of all shipped NitroX (Hybrid Model) base component packages. Lists every package with its components, types, tagNames, interactions, and attributes. Read this before calling provar_nitrox_generate to understand available component patterns and naming conventions.', + mimeType: 'text/markdown', + }, + () => { + try { + const text = readFileSync(join(docsDir, 'NITROX_COMPONENT_CATALOG.md'), 'utf-8'); + return { + contents: [{ uri: 'provar://nitrox/component-catalog', mimeType: 'text/markdown', text }], + }; + } catch { + return { + contents: [ + { + uri: 'provar://nitrox/component-catalog', + mimeType: 'text/markdown', + text: '# NitroX Component Catalog\n\nCatalog not found. If you are developing from source, rebuild the package. Otherwise, reinstall or upgrade the plugin/package and try again.', + }, + ], + }; + } + } + ); + server.resource( 'provar-step-reference', 'provar://docs/step-reference', @@ -142,3 +171,13 @@ export function createProvarMcpServer(config: ServerConfig): McpServer { return server; } + +/** + * Resolve the docs directory for bundled MCP Markdown resources. + * In compiled output (lib/mcp/) the sibling docs/ dir exists; in dev/ts-node + * mode (src/mcp/) it doesn't, so fall back two levels to the repo-root docs/. + */ +export function resolveDocsDir(currentDir: string): string { + const sibling = join(currentDir, 'docs'); + return existsSync(sibling) ? sibling : join(currentDir, '..', '..', 'docs'); +} diff --git a/src/mcp/tools/automationTools.ts b/src/mcp/tools/automationTools.ts index 3433334d..b631c760 100644 --- a/src/mcp/tools/automationTools.ts +++ b/src/mcp/tools/automationTools.ts @@ -16,187 +16,10 @@ import { log } from '../logging/logger.js'; import type { ServerConfig } from '../server.js'; import { assertPathAllowed, PathPolicyError } from '../security/pathPolicy.js'; import { parseJUnitResults } from './antTools.js'; -import { sfSpawnHelper, SfNotFoundError } from './sfSpawn.js'; +import { runSfCommand } from './sfSpawn.js'; -// ── SF CLI discovery ────────────────────────────────────────────────────────── - -/** - * Returns candidate sf CLI paths in common npm/nvm/volta install locations. - * Used as a fallback when `sf` is not in PATH. - */ -export function getSfCommonPaths(): string[] { - const home = os.homedir(); - if (process.platform === 'win32') { - const appData = process.env['APPDATA'] ?? path.join(home, 'AppData', 'Roaming'); - return [ - path.join(appData, 'npm', 'sf.cmd'), - path.join('C:', 'Program Files', 'nodejs', 'sf.cmd'), - path.join('C:', 'Program Files (x86)', 'nodejs', 'sf.cmd'), - ]; - } - const candidates = [ - '/usr/local/bin/sf', - path.join(home, '.npm-global', 'bin', 'sf'), - path.join(home, '.local', 'bin', 'sf'), - path.join(home, '.volta', 'bin', 'sf'), - ]; - // nvm — scan the three most-recently installed Node versions - const nvmBinDir = path.join(process.env['NVM_DIR'] ?? path.join(home, '.nvm'), 'versions', 'node'); - if (fs.existsSync(nvmBinDir)) { - try { - for (const v of fs.readdirSync(nvmBinDir).sort().reverse().slice(0, 3)) { - candidates.push(path.join(nvmBinDir, v, 'bin', 'sf')); - } - } catch { - /* skip */ - } - } - return candidates; -} - -// ── Shared spawn helper ─────────────────────────────────────────────────────── - -const MAX_BUFFER = 50 * 1024 * 1024; // 50 MB — prevents ENOBUFS on verbose Provar runs - -interface SpawnResult { - stdout: string; - stderr: string; - exitCode: number; -} - -// Proactively resolve the sf executable path once on first use and cache it. -// This ensures sf is always found even when ENOENT is masked by other errors (e.g. ENOBUFS). -let cachedSfPath: string | null | undefined; // undefined = not yet probed - -/** - * Exposed for testing only — pre-seeds the cached sf executable path, bypassing the probe spawn. - * Pass `undefined` to reset the cache so the next call triggers a fresh probe. - */ -export function setSfPathCacheForTesting(value: string | null | undefined): void { - cachedSfPath = value; -} - -// Platform override used in tests so Windows-specific shell logic can be exercised on any OS. -let sfPlatformOverride: NodeJS.Platform | undefined; -/** Exposed for testing only — overrides process.platform for needsWindowsShell decisions. */ -export function setSfPlatformForTesting(platform: NodeJS.Platform | undefined): void { - sfPlatformOverride = platform; -} - -/** - * Returns true when spawning `executable` requires the Windows shell. - * On Windows, `.cmd` and `.bat` batch scripts cannot be executed directly by - * Node's spawnSync — they must be invoked through cmd.exe (i.e. shell: true). - * The bare name "sf" also needs this treatment on Windows because the file on - * disk is actually "sf.cmd" and Node won't auto-append the extension. - * - * The `platform` parameter defaults to `process.platform` and is exposed for - * unit testing so tests can verify both Windows and non-Windows behaviour - * without having to run on the corresponding OS. - */ -export function needsWindowsShell(executable: string, platform = process.platform): boolean { - if (platform !== 'win32') return false; - const lower = executable.toLowerCase(); - return lower.endsWith('.cmd') || lower.endsWith('.bat') || !path.extname(lower); -} - -function resolveSfExecutable(): string | null { - if (cachedSfPath !== undefined) return cachedSfPath; - const platform = sfPlatformOverride ?? process.platform; - - // Two-phase probe avoids false-positives on Windows with shell:true. - // When shell:true is used, cmd.exe spawns successfully even when `sf` is - // missing — it exits non-zero with "not recognised" in stderr but sets no - // probe.error. Trying shell:false first catches both cases correctly. - // - // First attempt: shell:false (works on Linux/macOS; gives ENOENT on Windows if - // sf.cmd is on PATH but requires the shell). - const probe = sfSpawnHelper.spawnSync('sf', ['--version'], { - encoding: 'utf-8', - shell: false, - maxBuffer: 1024 * 1024, - }); - if (!probe.error && probe.status === 0) { - cachedSfPath = 'sf'; - return cachedSfPath; - } - - // Windows fallback: retry with shell:true when the plain probe failed - // with ENOENT — meaning sf.cmd exists on PATH but can't run without the shell. - if (platform === 'win32' && (probe.error as NodeJS.ErrnoException | undefined)?.code === 'ENOENT') { - const probeShell = sfSpawnHelper.spawnSync('sf', ['--version'], { - encoding: 'utf-8', - shell: true, - maxBuffer: 1024 * 1024, - }); - if (!probeShell.error && probeShell.status === 0) { - cachedSfPath = 'sf'; - return cachedSfPath; - } - } - - // Fall back to common install locations - for (const candidate of getSfCommonPaths()) { - if (fs.existsSync(candidate)) { - cachedSfPath = candidate; - return cachedSfPath; - } - } - cachedSfPath = null; - return null; -} - -/** - * Reject shell metacharacters in an sf_path that will be executed via shell:true. - * On Windows, cmd.exe interprets & | ; < > ` ' " and newlines as shell syntax. - * A valid filesystem path should never contain these characters. - */ -function assertShellSafePath(sfPath: string): void { - if (/[&|;<>`'"\n\r]/.test(sfPath)) { - throw Object.assign( - new Error( - 'sf_path contains characters that are unsafe for shell execution on Windows ' + - '(& | ; < > ` \' " or line-breaks). Provide an absolute filesystem path to the sf executable.' - ), - { code: 'INVALID_SF_PATH' } - ); - } -} - -function runSfCommand(args: string[], sfPath?: string): SpawnResult { - // Use explicit path if provided; otherwise use cached probe result - const executable = sfPath ?? resolveSfExecutable(); - if (!executable) throw new SfNotFoundError(); - - const platform = sfPlatformOverride ?? process.platform; - const useShell = needsWindowsShell(executable, platform); - - // Guard against injection when shell:true is used with a user-supplied path. - // Common install locations returned by resolveSfExecutable() are safe by construction. - if (useShell && sfPath) { - assertShellSafePath(sfPath); - } - - const result = sfSpawnHelper.spawnSync(executable, args, { - encoding: 'utf-8', - shell: useShell, - maxBuffer: MAX_BUFFER, - }); - - if (result.error) { - const err = result.error as NodeJS.ErrnoException; - if (err.code === 'ENOENT') { - throw new SfNotFoundError(sfPath); - } - throw result.error; - } - - return { - stdout: result.stdout ?? '', - stderr: result.stderr ?? '', - exitCode: result.status ?? 1, - }; -} +// Re-export sf resolution helpers so existing test imports from automationTools continue to work +export { getSfCommonPaths, needsWindowsShell, setSfPathCacheForTesting, setSfPlatformForTesting } from './sfSpawn.js'; function handleSpawnError( err: unknown, diff --git a/src/mcp/tools/defectTools.ts b/src/mcp/tools/defectTools.ts index c408f01f..81ca6a70 100644 --- a/src/mcp/tools/defectTools.ts +++ b/src/mcp/tools/defectTools.ts @@ -45,8 +45,8 @@ interface FailureContext { // ── SF CLI helpers ───────────────────────────────────────────────────────────── -function runSfArgs(args: string[]): { stdout: string; stderr: string; exitCode: number } { - const { stdout, stderr, exitCode } = runSfCommand(args); +function runSfArgs(args: string[], sfPath?: string): { stdout: string; stderr: string; exitCode: number } { + const { stdout, stderr, exitCode } = runSfCommand(args, sfPath); return { stdout, stderr, exitCode }; } @@ -57,35 +57,22 @@ function formatSfCommandError(action: string, exitCode: number, stderr: string, : `${action} failed with exit code ${exitCode}`; } -function runQuery(soql: string, targetOrg: string): SfQueryResponse { - const { stdout, stderr, exitCode } = runSfArgs([ - 'data', - 'query', - '--query', - soql, - '--target-org', - targetOrg, - '--json', - ]); +function runQuery(soql: string, targetOrg: string, sfPath?: string): SfQueryResponse { + const { stdout, stderr, exitCode } = runSfArgs( + ['data', 'query', '--query', soql, '--target-org', targetOrg, '--json'], + sfPath + ); if (exitCode !== 0) { throw new Error(formatSfCommandError('Salesforce query', exitCode, stderr, stdout)); } return JSON.parse(stdout) as SfQueryResponse; } -function createRecord(sobject: string, values: string, targetOrg: string): string { - const { stdout, stderr, exitCode } = runSfArgs([ - 'data', - 'create', - 'record', - '--sobject', - sobject, - '--values', - values, - '--target-org', - targetOrg, - '--json', - ]); +function createRecord(sobject: string, values: string, targetOrg: string, sfPath?: string): string { + const { stdout, stderr, exitCode } = runSfArgs( + ['data', 'create', 'record', '--sobject', sobject, '--values', values, '--target-org', targetOrg, '--json'], + sfPath + ); if (exitCode !== 0) { throw new Error(formatSfCommandError(`Failed to create ${sobject}`, exitCode, stderr, stdout)); } @@ -115,12 +102,14 @@ export interface DefectCreateResult { export function createDefectsForRun( runId: string, targetOrg: string, - failedTestFilter?: string[] + failedTestFilter?: string[], + sfPath?: string ): { created: DefectCreateResult[]; skipped: number; message: string } { // Step 1: resolve job record ID from tracking ID const jobQuery = runQuery( `SELECT Id FROM provar__Test_Plan_Schedule_Job__c WHERE provar__Tracking_Id__c = '${soqlEscape(runId)}'`, - targetOrg + targetOrg, + sfPath ); if (jobQuery.result.totalSize === 0) { throw new Error(`No Test_Plan_Schedule_Job__c found with Tracking_Id__c = '${runId}'`); @@ -133,7 +122,8 @@ export function createDefectsForRun( FROM provar__Test_Cycle__c WHERE provar__Test_Plan_Schedule_Job__c = '${soqlEscape(jobId)}' LIMIT 1`, - targetOrg + targetOrg, + sfPath ); const cycle = cycleQuery.result.records[0] ?? {}; const browser = safeText(cycle['provar__Web_Browser__c'], 100); @@ -151,7 +141,8 @@ export function createDefectsForRun( FROM provar__Test_Execution__c WHERE provar__Test_Cycle__c = '${soqlEscape(cycleId)}' AND provar__Status__c = 'Failed'`, - targetOrg + targetOrg, + sfPath ); if (execQuery.result.totalSize === 0) { @@ -185,7 +176,8 @@ export function createDefectsForRun( AND provar__Result__c = 'Fail' ORDER BY provar__Sequence_No__c ASC LIMIT 1`, - targetOrg + targetOrg, + sfPath ); const step = stepQuery.result.records[0] ?? {}; @@ -227,7 +219,7 @@ export function createDefectsForRun( `provar__Description__c="${safeText(descLines, 2000)}" ` + 'provar__Status__c="Open"'; - const defectId = createRecord('provar__Defect__c', defectValues, targetOrg); + const defectId = createRecord('provar__Defect__c', defectValues, targetOrg, sfPath); // Step 5b: link TC → Defect const tcDefectValues = @@ -235,7 +227,7 @@ export function createDefectsForRun( `provar__Test_Case__c="${testCaseId}"` + (stepId ? ` provar__Test_Step__c="${stepId}"` : ''); - const tcDefectId = createRecord('provar__Test_Case_Defect__c', tcDefectValues, targetOrg); + const tcDefectId = createRecord('provar__Test_Case_Defect__c', tcDefectValues, targetOrg, sfPath); // Step 5c: link Execution → Defect (with step execution if available) const execDefectValues = @@ -243,7 +235,7 @@ export function createDefectsForRun( `provar__Test_Execution__c="${executionId}"` + (stepExecutionId ? ` provar__Test_Step_Execution__c="${stepExecutionId}"` : ''); - const execDefectId = createRecord('provar__Test_Execution_Defect__c', execDefectValues, targetOrg); + const execDefectId = createRecord('provar__Test_Execution_Defect__c', execDefectValues, targetOrg, sfPath); log('info', 'defect created', { defectId, tcDefectId, execDefectId, executionId }); @@ -281,14 +273,22 @@ export function registerQualityHubDefectCreate(server: McpServer): void { .describe( 'Optional filter — list of Test_Case__c record ID substrings to restrict defect creation to specific failures' ), + sf_path: z + .string() + .optional() + .describe( + 'Path to the sf CLI executable when not in PATH ' + + '(e.g. "C:\\\\Program Files\\\\sf\\\\bin\\\\sf.cmd" for the Windows standalone installer). ' + + 'Leave unset to use auto-discovery.' + ), }, }, - ({ run_id, target_org, failed_tests }) => { + ({ run_id, target_org, failed_tests, sf_path }) => { const requestId = makeRequestId(); log('info', 'provar_qualityhub_defect_create', { requestId, run_id, target_org }); try { - const result = createDefectsForRun(run_id, target_org, failed_tests); + const result = createDefectsForRun(run_id, target_org, failed_tests, sf_path); const response = { requestId, ...result }; return { content: [{ type: 'text' as const, text: JSON.stringify(response) }], diff --git a/src/mcp/tools/nitroXTools.ts b/src/mcp/tools/nitroXTools.ts index 7b94b1a8..1a8821b1 100644 --- a/src/mcp/tools/nitroXTools.ts +++ b/src/mcp/tools/nitroXTools.ts @@ -724,6 +724,8 @@ export function registerNitroXGenerate(server: McpServer, config: ServerConfig): 'Generate a new NitroX .po.json (Hybrid Model page object) from a component description.', "Applicable to any component type supported by Provar's Hybrid Model:", 'LWC, Screen Flow, Industry Components, Experience Cloud, HTML5.', + 'Read the provar-nitrox-component-catalog resource first to understand available component types,', + 'tagName conventions, interaction titles, and attribute patterns from shipped base packages.', 'All componentId fields are assigned fresh UUIDs. Returns JSON content;', 'writes to disk only when dry_run=false.', ].join(' '), diff --git a/src/mcp/tools/qualityHubTools.ts b/src/mcp/tools/qualityHubTools.ts index 74d037da..aa464af6 100644 --- a/src/mcp/tools/qualityHubTools.ts +++ b/src/mcp/tools/qualityHubTools.ts @@ -46,14 +46,25 @@ export function registerQualityHubConnect(server: McpServer): void { .optional() .default([]) .describe('Additional raw CLI flags to forward (e.g. ["--json"])'), + sf_path: z + .string() + .optional() + .describe( + 'Path to the sf CLI executable when not in PATH ' + + '(e.g. "C:\\\\Program Files\\\\sf\\\\bin\\\\sf.cmd" for the Windows standalone installer). ' + + 'Leave unset to use auto-discovery.' + ), }, }, - ({ target_org, flags }) => { + ({ target_org, flags, sf_path }) => { const requestId = makeRequestId(); log('info', 'provar_qualityhub_connect', { requestId, target_org }); try { - const result = runSfCommand(['provar', 'quality-hub', 'connect', '--target-org', target_org, ...flags]); + const result = runSfCommand( + ['provar', 'quality-hub', 'connect', '--target-org', target_org, ...flags], + sf_path + ); const response = { requestId, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr }; if (result.exitCode !== 0) { @@ -87,9 +98,17 @@ export function registerQualityHubDisplay(server: McpServer): void { inputSchema: { target_org: z.string().optional().describe('SF org alias or username (uses default if omitted)'), flags: z.array(z.string()).optional().default([]).describe('Additional raw CLI flags to forward'), + sf_path: z + .string() + .optional() + .describe( + 'Path to the sf CLI executable when not in PATH ' + + '(e.g. "C:\\\\Program Files\\\\sf\\\\bin\\\\sf.cmd" for the Windows standalone installer). ' + + 'Leave unset to use auto-discovery.' + ), }, }, - ({ target_org, flags }) => { + ({ target_org, flags, sf_path }) => { const requestId = makeRequestId(); log('info', 'provar_qualityhub_display', { requestId, target_org }); @@ -97,7 +116,7 @@ export function registerQualityHubDisplay(server: McpServer): void { const args = ['provar', 'quality-hub', 'display', ...flags]; if (target_org) args.splice(3, 0, '--target-org', target_org); - const result = runSfCommand(args); + const result = runSfCommand(args, sf_path); const response = { requestId, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr }; if (result.exitCode !== 0) { @@ -155,15 +174,26 @@ export function registerQualityHubTestRun(server: McpServer): void { .describe( 'Additional raw CLI flags (e.g. ["--plan-name", "SmokeTests"]). Avoid wildcards in --plan-name values — they skip QH plan-level reporting.' ), + sf_path: z + .string() + .optional() + .describe( + 'Path to the sf CLI executable when not in PATH ' + + '(e.g. "C:\\\\Program Files\\\\sf\\\\bin\\\\sf.cmd" for the Windows standalone installer). ' + + 'Leave unset to use auto-discovery.' + ), }, }, - ({ target_org, flags }) => { + ({ target_org, flags, sf_path }) => { const requestId = makeRequestId(); log('info', 'provar_qualityhub_testrun', { requestId, target_org }); try { const wildcardWarning = detectWildcardFlags(flags); - const result = runSfCommand(['provar', 'quality-hub', 'test', 'run', '--target-org', target_org, ...flags]); + const result = runSfCommand( + ['provar', 'quality-hub', 'test', 'run', '--target-org', target_org, ...flags], + sf_path + ); const response: Record = { requestId, exitCode: result.exitCode, @@ -207,25 +237,25 @@ export function registerQualityHubTestRunReport(server: McpServer): void { target_org: z.string().describe('SF org alias or username'), run_id: z.string().describe('Test run ID returned by provar_qualityhub_testrun'), flags: z.array(z.string()).optional().default([]).describe('Additional raw CLI flags'), + sf_path: z + .string() + .optional() + .describe( + 'Path to the sf CLI executable when not in PATH ' + + '(e.g. "C:\\\\Program Files\\\\sf\\\\bin\\\\sf.cmd" for the Windows standalone installer). ' + + 'Leave unset to use auto-discovery.' + ), }, }, - ({ target_org, run_id, flags }) => { + ({ target_org, run_id, flags, sf_path }) => { const requestId = makeRequestId(); log('info', 'provar_qualityhub_testrun_report', { requestId, target_org, run_id }); try { - const result = runSfCommand([ - 'provar', - 'quality-hub', - 'test', - 'run', - 'report', - '--target-org', - target_org, - '--run-id', - run_id, - ...flags, - ]); + const result = runSfCommand( + ['provar', 'quality-hub', 'test', 'run', 'report', '--target-org', target_org, '--run-id', run_id, ...flags], + sf_path + ); const response = { requestId, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr }; if (result.exitCode !== 0) { @@ -279,25 +309,25 @@ export function registerQualityHubTestRunAbort(server: McpServer): void { target_org: z.string().describe('SF org alias or username'), run_id: z.string().describe('Test run ID to abort'), flags: z.array(z.string()).optional().default([]).describe('Additional raw CLI flags'), + sf_path: z + .string() + .optional() + .describe( + 'Path to the sf CLI executable when not in PATH ' + + '(e.g. "C:\\\\Program Files\\\\sf\\\\bin\\\\sf.cmd" for the Windows standalone installer). ' + + 'Leave unset to use auto-discovery.' + ), }, }, - ({ target_org, run_id, flags }) => { + ({ target_org, run_id, flags, sf_path }) => { const requestId = makeRequestId(); log('info', 'provar_qualityhub_testrun_abort', { requestId, target_org, run_id }); try { - const result = runSfCommand([ - 'provar', - 'quality-hub', - 'test', - 'run', - 'abort', - '--target-org', - target_org, - '--run-id', - run_id, - ...flags, - ]); + const result = runSfCommand( + ['provar', 'quality-hub', 'test', 'run', 'abort', '--target-org', target_org, '--run-id', run_id, ...flags], + sf_path + ); const response = { requestId, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr }; if (result.exitCode !== 0) { @@ -336,22 +366,25 @@ export function registerQualityHubTestcaseRetrieve(server: McpServer): void { .optional() .default([]) .describe('Additional raw CLI flags (e.g. ["--user-story", "US-123"])'), + sf_path: z + .string() + .optional() + .describe( + 'Path to the sf CLI executable when not in PATH ' + + '(e.g. "C:\\\\Program Files\\\\sf\\\\bin\\\\sf.cmd" for the Windows standalone installer). ' + + 'Leave unset to use auto-discovery.' + ), }, }, - ({ target_org, flags }) => { + ({ target_org, flags, sf_path }) => { const requestId = makeRequestId(); log('info', 'provar_qualityhub_testcase_retrieve', { requestId, target_org }); try { - const result = runSfCommand([ - 'provar', - 'quality-hub', - 'testcase', - 'retrieve', - '--target-org', - target_org, - ...flags, - ]); + const result = runSfCommand( + ['provar', 'quality-hub', 'testcase', 'retrieve', '--target-org', target_org, ...flags], + sf_path + ); const response = { requestId, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr }; if (result.exitCode !== 0) { diff --git a/src/mcp/tools/sfSpawn.ts b/src/mcp/tools/sfSpawn.ts index 5b7fd3c3..f103385b 100644 --- a/src/mcp/tools/sfSpawn.ts +++ b/src/mcp/tools/sfSpawn.ts @@ -5,6 +5,9 @@ * For full license text, see LICENSE.md file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; import { spawnSync as _spawnSync } from 'node:child_process'; /** @@ -20,20 +23,18 @@ export const sfSpawnHelper = { export 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/volta install locations'; + const where = sfPath ? `at explicit path "${sfPath}"` : 'in PATH or common npm/nvm/volta 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").' + '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'; } } -// ── Shared spawn helper ─────────────────────────────────────────────────────── +// ── Shared result type ──────────────────────────────────────────────────────── export interface SpawnResult { stdout: string; @@ -41,17 +42,175 @@ export interface SpawnResult { exitCode: number; } +const MAX_BUFFER = 50 * 1024 * 1024; // 50 MB — prevents ENOBUFS on verbose Provar runs + +// ── SF CLI discovery ────────────────────────────────────────────────────────── + +/** + * Returns candidate sf CLI paths in common install locations. + * Used as a fallback when `sf` is not in PATH. + */ +export function getSfCommonPaths(): string[] { + const home = os.homedir(); + if (process.platform === 'win32') { + const appData = process.env['APPDATA'] ?? path.join(home, 'AppData', 'Roaming'); + return [ + path.join(appData, 'npm', 'sf.cmd'), + path.join('C:', 'Program Files', 'nodejs', 'sf.cmd'), + path.join('C:', 'Program Files (x86)', 'nodejs', 'sf.cmd'), + // Windows standalone installer (https://developer.salesforce.com/tools/salesforcecli) + path.join('C:', 'Program Files', 'sf', 'bin', 'sf.cmd'), + path.join('C:', 'Program Files', 'sf', 'client', 'bin', 'sf.cmd'), + ]; + } + const candidates = [ + '/usr/local/bin/sf', + path.join(home, '.npm-global', 'bin', 'sf'), + path.join(home, '.local', 'bin', 'sf'), + path.join(home, '.volta', 'bin', 'sf'), + ]; + // nvm — scan the three most-recently installed Node versions + const nvmBinDir = path.join(process.env['NVM_DIR'] ?? path.join(home, '.nvm'), 'versions', 'node'); + if (fs.existsSync(nvmBinDir)) { + try { + for (const v of fs.readdirSync(nvmBinDir).sort().reverse().slice(0, 3)) { + candidates.push(path.join(nvmBinDir, v, 'bin', 'sf')); + } + } catch { + /* skip */ + } + } + return candidates; +} + +// Proactively resolve the sf executable path once on first use and cache it. +// This ensures sf is always found even when ENOENT is masked by other errors (e.g. ENOBUFS). +let cachedSfPath: string | null | undefined; // undefined = not yet probed + +/** + * Exposed for testing only — pre-seeds the cached sf executable path, bypassing the probe spawn. + * Pass `undefined` to reset the cache so the next call triggers a fresh probe. + */ +export function setSfPathCacheForTesting(value: string | null | undefined): void { + cachedSfPath = value; +} + +// Platform override used in tests so Windows-specific shell logic can be exercised on any OS. +let sfPlatformOverride: NodeJS.Platform | undefined; + +/** Exposed for testing only — overrides process.platform for needsWindowsShell decisions. */ +export function setSfPlatformForTesting(platform: NodeJS.Platform | undefined): void { + sfPlatformOverride = platform; +} + +/** + * Returns true when spawning `executable` requires the Windows shell. + * On Windows, `.cmd` and `.bat` batch scripts cannot be executed directly by + * Node's spawnSync — they must be invoked through cmd.exe (i.e. shell: true). + * The bare name "sf" also needs this treatment on Windows because the file on + * disk is actually "sf.cmd" and Node won't auto-append the extension. + */ +export function needsWindowsShell(executable: string, platform = process.platform): boolean { + if (platform !== 'win32') return false; + const lower = executable.toLowerCase(); + return lower.endsWith('.cmd') || lower.endsWith('.bat') || !path.extname(lower); +} + +function resolveSfExecutable(): string | null { + if (cachedSfPath !== undefined) return cachedSfPath; + const platform = sfPlatformOverride ?? process.platform; + + // Two-phase probe avoids false-positives on Windows with shell:true. + // When shell:true is used, cmd.exe spawns successfully even when `sf` is + // missing — it exits non-zero with "not recognised" in stderr but sets no + // probe.error. Trying shell:false first catches both cases correctly. + // + // First attempt: shell:false (works on Linux/macOS; gives ENOENT on Windows if + // sf.cmd is on PATH but requires the shell). + const probe = sfSpawnHelper.spawnSync('sf', ['--version'], { + encoding: 'utf-8', + shell: false, + maxBuffer: 1024 * 1024, + }); + if (!probe.error && probe.status === 0) { + cachedSfPath = 'sf'; + return cachedSfPath; + } + + // Windows fallback: retry with shell:true when the plain probe failed + // with ENOENT — meaning sf.cmd exists on PATH but can't run without the shell. + if (platform === 'win32' && (probe.error as NodeJS.ErrnoException | undefined)?.code === 'ENOENT') { + const probeShell = sfSpawnHelper.spawnSync('sf', ['--version'], { + encoding: 'utf-8', + shell: true, + maxBuffer: 1024 * 1024, + }); + if (!probeShell.error && probeShell.status === 0) { + cachedSfPath = 'sf'; + return cachedSfPath; + } + } + + // Fall back to common install locations + for (const candidate of getSfCommonPaths()) { + if (fs.existsSync(candidate)) { + cachedSfPath = candidate; + return cachedSfPath; + } + } + cachedSfPath = null; + return null; +} + +/** + * Reject shell metacharacters in an sf_path that will be executed via shell:true. + * On Windows, cmd.exe interprets & | ; < > ` ' " and newlines as shell syntax. + * A valid filesystem path should never contain these characters. + */ +function assertShellSafePath(sfPath: string): void { + if (/[&|;<>`'"\n\r]/.test(sfPath)) { + throw Object.assign( + new Error( + 'sf_path contains characters that are unsafe for shell execution on Windows ' + + '(& | ; < > ` \' " or line-breaks). Provide an absolute filesystem path to the sf executable.' + ), + { code: 'INVALID_SF_PATH' } + ); + } +} + /** * Run `sf ` synchronously and return stdout, stderr, and exit code. - * Throws SfNotFoundError if the `sf` binary is not in PATH. + * Throws SfNotFoundError if the `sf` binary cannot be found. + * Pass `sfPath` to override auto-discovery with an explicit executable path. */ -export function runSfCommand(args: string[]): SpawnResult { - const result = sfSpawnHelper.spawnSync('sf', args, { encoding: 'utf-8', shell: false }); +export function runSfCommand(args: string[], sfPath?: string): SpawnResult { + // Treat empty/whitespace sfPath as absent so auto-discovery runs instead of + // throwing SfNotFoundError with no useful path hint. + const trimmedSfPath = sfPath?.trim(); + const resolvedSfPath = trimmedSfPath !== '' ? trimmedSfPath : undefined; + const executable = resolvedSfPath ?? resolveSfExecutable(); + if (!executable) throw new SfNotFoundError(); + + const platform = sfPlatformOverride ?? process.platform; + const useShell = needsWindowsShell(executable, platform); + + // Guard against injection when shell:true is used with a user-supplied path. + // Common install locations returned by resolveSfExecutable() are safe by construction. + if (useShell && resolvedSfPath) { + assertShellSafePath(resolvedSfPath); + } + + const result = sfSpawnHelper.spawnSync(executable, args, { + encoding: 'utf-8', + shell: useShell, + maxBuffer: MAX_BUFFER, + }); if (result.error) { const err = result.error as NodeJS.ErrnoException; if (err.code === 'ENOENT') { - throw new SfNotFoundError(); + throw new SfNotFoundError(resolvedSfPath); } throw result.error; } diff --git a/test/unit/mcp/automationTools.test.ts b/test/unit/mcp/automationTools.test.ts index 381ec475..2a9c3c30 100644 --- a/test/unit/mcp/automationTools.test.ts +++ b/test/unit/mcp/automationTools.test.ts @@ -16,6 +16,7 @@ import { sfSpawnHelper } from '../../../src/mcp/tools/sfSpawn.js'; import { needsWindowsShell, setSfPlatformForTesting, + getSfCommonPaths, filterTestRunOutput, setSfResultsPathForTesting, } from '../../../src/mcp/tools/automationTools.js'; @@ -928,3 +929,31 @@ describe('filterTestRunOutput', () => { assert.ok(filtered.includes('INFO Done'), 'Real output should remain'); }); }); + +// ── getSfCommonPaths — B2a Windows standalone installer paths ───────────────── + +describe('getSfCommonPaths', () => { + it('includes Windows standalone installer paths on win32 (B2a fix)', () => { + const origPlatform = Object.getOwnPropertyDescriptor(process, 'platform'); + Object.defineProperty(process, 'platform', { value: 'win32', configurable: true }); + try { + const paths = getSfCommonPaths(); + const sfBinPath = paths.find( + (p) => p.includes('Program Files') && p.includes('sf') && p.includes('bin') && !p.includes('client') + ); + const sfClientPath = paths.find((p) => p.includes('Program Files') && p.includes('sf') && p.includes('client')); + assert.ok(sfBinPath, 'Expected C:\\Program Files\\sf\\bin\\sf.cmd in win32 paths'); + assert.ok(sfClientPath, 'Expected C:\\Program Files\\sf\\client\\bin\\sf.cmd in win32 paths'); + } finally { + if (origPlatform) Object.defineProperty(process, 'platform', origPlatform); + } + }); + + it('does not include Windows standalone paths on non-Windows platforms', () => { + if (process.platform !== 'win32') { + const paths = getSfCommonPaths(); + assert.ok(!paths.some((p) => p.includes('Program Files')), 'Windows paths should not appear on non-Windows'); + assert.ok(paths.includes('/usr/local/bin/sf'), 'Linux/macOS fallback path should be present'); + } + }); +}); diff --git a/test/unit/mcp/nitroXTools.test.ts b/test/unit/mcp/nitroXTools.test.ts index b0d0c2a8..5ae0b221 100644 --- a/test/unit/mcp/nitroXTools.test.ts +++ b/test/unit/mcp/nitroXTools.test.ts @@ -17,14 +17,17 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; type ToolHandler = (args: Record) => unknown; class MockMcpServer { + public registrations: Array<{ name: string; description: string }> = []; private handlers = new Map(); public tool(name: string, _desc: string, _schema: unknown, handler: ToolHandler): void { this.handlers.set(name, handler); } - public registerTool(name: string, _config: unknown, handler: ToolHandler): void { + public registerTool(name: string, config: unknown, handler: ToolHandler): void { this.handlers.set(name, handler); + const desc = (config as Record)['description']; + if (typeof desc === 'string') this.registrations.push({ name, description: desc }); } public call(name: string, args: Record): ReturnType { @@ -533,6 +536,19 @@ describe('nitroXTools', () => { }); }); + // ── provar_nitrox_generate description ──────────────────────────────────── + + describe('provar_nitrox_generate description', () => { + it('references the nitrox-component-catalog resource', () => { + const reg = server.registrations.find((r) => r.name === 'provar_nitrox_generate'); + assert.ok(reg, 'provar_nitrox_generate not registered'); + assert.ok( + reg.description.includes('provar-nitrox-component-catalog'), + 'description should reference the component catalog resource' + ); + }); + }); + // ── provar_nitrox_patch ──────────────────────────────────────────────────── describe('provar_nitrox_patch', () => { diff --git a/test/unit/mcp/qualityHubTools.test.ts b/test/unit/mcp/qualityHubTools.test.ts index c7e8dc2a..11274d2f 100644 --- a/test/unit/mcp/qualityHubTools.test.ts +++ b/test/unit/mcp/qualityHubTools.test.ts @@ -7,9 +7,10 @@ /* eslint-disable camelcase */ import { strict as assert } from 'node:assert'; +import path from 'node:path'; import sinon from 'sinon'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { sfSpawnHelper } from '../../../src/mcp/tools/sfSpawn.js'; +import { sfSpawnHelper, getSfCommonPaths, setSfPathCacheForTesting } from '../../../src/mcp/tools/sfSpawn.js'; // ── Minimal mock server ─────────────────────────────────────────────────────── @@ -74,6 +75,8 @@ describe('qualityHubTools', () => { beforeEach(async () => { server = new MockMcpServer(); spawnStub = sinon.stub(sfSpawnHelper, 'spawnSync'); + // Pre-seed the sf path cache to bypass the probe spawn — tests control spawnStub directly + setSfPathCacheForTesting('sf'); const { registerAllQualityHubTools } = await import('../../../src/mcp/tools/qualityHubTools.js'); registerAllQualityHubTools(server as unknown as McpServer); @@ -81,6 +84,7 @@ describe('qualityHubTools', () => { afterEach(() => { sinon.restore(); + setSfPathCacheForTesting(undefined); // reset cache so next test probes cleanly }); // ── provar_qualityhub_connect ─────────────────────────────────────────────── @@ -391,4 +395,73 @@ describe('qualityHubTools', () => { assert.equal(parseBody(result).error_code, 'SF_NOT_FOUND'); }); }); + + // ── sf_path threading ───────────────────────────────────────────────────────── + + describe('sf_path threading', () => { + it('provar_qualityhub_connect uses explicit sf_path as the executable', () => { + spawnStub.returns(makeSpawnResult('ok', '', 0)); + server.call('provar_qualityhub_connect', { + target_org: 'myorg', + flags: [], + sf_path: '/custom/bin/sf', + }); + const [cmd] = spawnStub.firstCall.args as [string, string[]]; + assert.equal(cmd, '/custom/bin/sf'); + }); + + it('provar_qualityhub_display uses explicit sf_path as the executable', () => { + spawnStub.returns(makeSpawnResult('ok', '', 0)); + server.call('provar_qualityhub_display', { flags: [], sf_path: '/custom/bin/sf' }); + const [cmd] = spawnStub.firstCall.args as [string, string[]]; + assert.equal(cmd, '/custom/bin/sf'); + }); + + it('returns SF_NOT_FOUND with path hint when explicit sf_path gives ENOENT', () => { + spawnStub.returns(makeEnoentResult()); + const result = server.call('provar_qualityhub_connect', { + target_org: 'myorg', + flags: [], + sf_path: '/nonexistent/sf', + }); + assert.ok(isError(result)); + const body = parseBody(result); + assert.equal(body.error_code, 'SF_NOT_FOUND'); + assert.ok((body.message as string).includes('/nonexistent/sf'), 'message should include the bad path'); + }); + + it('returns SF_NOT_FOUND when no sf found and cache is null', () => { + setSfPathCacheForTesting(null); // simulate: probe already ran, nothing found + const result = server.call('provar_qualityhub_connect', { target_org: 'myorg', flags: [] }); + assert.ok(isError(result)); + assert.equal(parseBody(result).error_code, 'SF_NOT_FOUND'); + }); + }); + + // ── getSfCommonPaths — B2a Windows standalone installer paths ───────────────── + + describe('getSfCommonPaths', () => { + it('includes Windows standalone installer paths on win32', () => { + const origPlatform = Object.getOwnPropertyDescriptor(process, 'platform'); + Object.defineProperty(process, 'platform', { value: 'win32', configurable: true }); + try { + const paths = getSfCommonPaths(); + const expectedBin = path.join('C:', 'Program Files', 'sf', 'bin', 'sf.cmd'); + const expectedClientBin = path.join('C:', 'Program Files', 'sf', 'client', 'bin', 'sf.cmd'); + assert.ok(paths.includes(expectedBin), `Expected ${expectedBin} in common paths`); + assert.ok(paths.includes(expectedClientBin), `Expected ${expectedClientBin} in common paths`); + } finally { + if (origPlatform) Object.defineProperty(process, 'platform', origPlatform); + } + }); + + it('returns non-empty list on non-Windows platforms', () => { + // On the current test platform (Linux/macOS), paths should include /usr/local/bin/sf + if (process.platform !== 'win32') { + const paths = getSfCommonPaths(); + assert.ok(paths.length > 0); + assert.ok(paths.includes('/usr/local/bin/sf')); + } + }); + }); }); diff --git a/test/unit/mcp/server.test.ts b/test/unit/mcp/server.test.ts new file mode 100644 index 00000000..4692c151 --- /dev/null +++ b/test/unit/mcp/server.test.ts @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024 Provar Limited. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.md file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import { strict as assert } from 'node:assert'; +import path from 'node:path'; +import fs from 'node:fs'; +import os from 'node:os'; +import { describe, it, afterEach } from 'mocha'; +import { resolveDocsDir } from '../../../src/mcp/server.js'; + +describe('resolveDocsDir', () => { + const tmpDirs: string[] = []; + + afterEach(() => { + for (const d of tmpDirs) { + try { + fs.rmSync(d, { recursive: true, force: true }); + } catch { + // ignore cleanup errors + } + } + tmpDirs.length = 0; + }); + + function makeTmpDir(): string { + const d = fs.mkdtempSync(path.join(os.tmpdir(), 'provar-server-test-')); + tmpDirs.push(d); + return d; + } + + it('returns sibling docs/ when it exists (compiled lib/mcp/ mode)', () => { + const base = makeTmpDir(); + const sibling = path.join(base, 'docs'); + fs.mkdirSync(sibling); + assert.equal(resolveDocsDir(base), sibling); + }); + + it('falls back two levels to repo-root docs/ when sibling is absent (dev/ts-node mode)', () => { + const base = makeTmpDir(); + const expected = path.join(base, '..', '..', 'docs'); + assert.equal(resolveDocsDir(base), expected); + }); +}); diff --git a/test/unit/mcp/sfSpawn.test.ts b/test/unit/mcp/sfSpawn.test.ts new file mode 100644 index 00000000..60317464 --- /dev/null +++ b/test/unit/mcp/sfSpawn.test.ts @@ -0,0 +1,357 @@ +/* + * Copyright (c) 2024 Provar Limited. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.md file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import { strict as assert } from 'node:assert'; +import os from 'node:os'; +import path from 'node:path'; +import sinon from 'sinon'; +import { + sfSpawnHelper, + SfNotFoundError, + getSfCommonPaths, + needsWindowsShell, + runSfCommand, + setSfPathCacheForTesting, + setSfPlatformForTesting, + soqlEscape, +} from '../../../src/mcp/tools/sfSpawn.js'; + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +function makeSpawnOk( + stdout = '', + stderr = '' +): { stdout: string; stderr: string; status: number; error: undefined; pid: number; output: never[]; signal: null } { + return { stdout, stderr, status: 0, error: undefined, pid: 1, output: [], signal: null }; +} + +function makeSpawnFail( + exitCode = 1, + stdout = '', + stderr = '' +): { stdout: string; stderr: string; status: number; error: undefined; pid: number; output: never[]; signal: null } { + return { stdout, stderr, status: exitCode, error: undefined, pid: 1, output: [], signal: null }; +} + +function makeEnoent(): { + stdout: string; + stderr: string; + status: null; + error: Error & { code: string }; + pid: undefined; + output: never[]; + signal: null; +} { + return { + stdout: '', + stderr: '', + status: null, + error: Object.assign(new Error('spawn sf ENOENT'), { code: 'ENOENT' }), + pid: undefined, + output: [], + signal: null, + }; +} + +// ── SfNotFoundError ─────────────────────────────────────────────────────────── + +describe('SfNotFoundError', () => { + it('has code SF_NOT_FOUND', () => { + const err = new SfNotFoundError(); + assert.equal(err.code, 'SF_NOT_FOUND'); + assert.equal(err.name, 'SfNotFoundError'); + }); + + it('generic message mentions PATH and npm install hint', () => { + const err = new SfNotFoundError(); + assert.ok(err.message.includes('npm install -g @salesforce/cli')); + assert.ok(err.message.includes('PATH')); + }); + + it('path-specific message names the explicit path', () => { + const err = new SfNotFoundError('/custom/sf'); + assert.ok(err.message.includes('/custom/sf')); + assert.ok(err.message.includes('at explicit path')); + }); +}); + +// ── soqlEscape ──────────────────────────────────────────────────────────────── + +describe('soqlEscape', () => { + it('leaves strings without quotes unchanged', () => { + assert.equal(soqlEscape('hello world'), 'hello world'); + }); + + it('escapes single quotes', () => { + assert.equal(soqlEscape("O'Brien"), "O\\'Brien"); + }); + + it('escapes multiple quotes', () => { + assert.equal(soqlEscape("it's a 'test'"), "it\\'s a \\'test\\'"); + }); + + it('handles empty string', () => { + assert.equal(soqlEscape(''), ''); + }); +}); + +// ── getSfCommonPaths ────────────────────────────────────────────────────────── + +describe('getSfCommonPaths', () => { + it('returns an array of strings', () => { + const paths = getSfCommonPaths(); + assert.ok(Array.isArray(paths)); + assert.ok(paths.length > 0); + for (const p of paths) assert.equal(typeof p, 'string'); + }); + + it('includes Windows standalone installer paths on win32', () => { + const originalDescriptor = Object.getOwnPropertyDescriptor(process, 'platform'); + try { + Object.defineProperty(process, 'platform', { value: 'win32', configurable: true }); + const paths = getSfCommonPaths(); + assert.ok( + paths.some((p) => p.includes(path.join('Program Files', 'sf', 'bin'))), + 'should include C:\\Program Files\\sf\\bin\\sf.cmd' + ); + assert.ok( + paths.some((p) => p.includes(path.join('Program Files', 'sf', 'client', 'bin'))), + 'should include C:\\Program Files\\sf\\client\\bin\\sf.cmd' + ); + } finally { + if (originalDescriptor) Object.defineProperty(process, 'platform', originalDescriptor); + } + }); + + it('all Windows paths end with .cmd', () => { + const originalDescriptor = Object.getOwnPropertyDescriptor(process, 'platform'); + try { + Object.defineProperty(process, 'platform', { value: 'win32', configurable: true }); + const paths = getSfCommonPaths(); + for (const p of paths) assert.ok(p.endsWith('.cmd'), `expected .cmd: ${p}`); + } finally { + if (originalDescriptor) Object.defineProperty(process, 'platform', originalDescriptor); + } + }); + + it('includes home-relative paths on non-Windows', () => { + const originalDescriptor = Object.getOwnPropertyDescriptor(process, 'platform'); + try { + Object.defineProperty(process, 'platform', { value: 'linux', configurable: true }); + const paths = getSfCommonPaths(); + assert.ok(paths.includes('/usr/local/bin/sf')); + assert.ok(paths.some((p) => p.includes(os.homedir()))); + } finally { + if (originalDescriptor) Object.defineProperty(process, 'platform', originalDescriptor); + } + }); +}); + +// ── needsWindowsShell ───────────────────────────────────────────────────────── + +describe('needsWindowsShell', () => { + it('returns false on non-Windows regardless of executable', () => { + assert.equal(needsWindowsShell('sf', 'linux'), false); + assert.equal(needsWindowsShell('sf.cmd', 'darwin'), false); + assert.equal(needsWindowsShell('/usr/bin/sf', 'linux'), false); + }); + + it('returns true for .cmd on win32', () => { + assert.equal(needsWindowsShell('sf.cmd', 'win32'), true); + assert.equal(needsWindowsShell('C:\\npm\\sf.cmd', 'win32'), true); + }); + + it('returns true for .bat on win32', () => { + assert.equal(needsWindowsShell('sf.bat', 'win32'), true); + }); + + it('returns true for bare name (no extension) on win32', () => { + assert.equal(needsWindowsShell('sf', 'win32'), true); + }); + + it('returns false for .exe on win32', () => { + assert.equal(needsWindowsShell('sf.exe', 'win32'), false); + }); + + it('is case-insensitive for extension check', () => { + assert.equal(needsWindowsShell('SF.CMD', 'win32'), true); + assert.equal(needsWindowsShell('SF.BAT', 'win32'), true); + }); +}); + +// ── runSfCommand ────────────────────────────────────────────────────────────── + +describe('runSfCommand', () => { + let spawnStub: sinon.SinonStub; + + beforeEach(() => { + spawnStub = sinon.stub(sfSpawnHelper, 'spawnSync'); + setSfPathCacheForTesting('sf'); + setSfPlatformForTesting('linux'); + }); + + afterEach(() => { + sinon.restore(); + setSfPathCacheForTesting(undefined); + setSfPlatformForTesting(undefined); + }); + + it('returns stdout, stderr, and exitCode on success', () => { + spawnStub.returns(makeSpawnOk('output', 'warn')); + const result = runSfCommand(['data', 'query']); + assert.equal(result.stdout, 'output'); + assert.equal(result.stderr, 'warn'); + assert.equal(result.exitCode, 0); + }); + + it('passes args array to spawnSync', () => { + spawnStub.returns(makeSpawnOk()); + runSfCommand(['provar', 'quality-hub', 'connect', '--target-org', 'myorg']); + const [cmd, args] = spawnStub.firstCall.args as [string, string[]]; + assert.equal(cmd, 'sf'); + assert.deepEqual(args, ['provar', 'quality-hub', 'connect', '--target-org', 'myorg']); + }); + + it('uses explicit sfPath instead of cached path', () => { + spawnStub.returns(makeSpawnOk()); + runSfCommand(['--version'], '/custom/path/sf'); + const [cmd] = spawnStub.firstCall.args as [string, string[]]; + assert.equal(cmd, '/custom/path/sf'); + }); + + it('falls through to auto-discovery when sfPath is empty string', () => { + // Empty string is not a valid path — should behave as if sfPath was absent + spawnStub.returns(makeSpawnOk('ok')); + runSfCommand(['--version'], ''); + const [cmd] = spawnStub.firstCall.args as [string, string[]]; + assert.equal(cmd, 'sf'); // uses the cached path, not the empty string + }); + + it('falls through to auto-discovery when sfPath is whitespace only', () => { + spawnStub.returns(makeSpawnOk('ok')); + runSfCommand(['--version'], ' '); + const [cmd] = spawnStub.firstCall.args as [string, string[]]; + assert.equal(cmd, 'sf'); + }); + + it('throws SfNotFoundError when cache is null and no sfPath given', () => { + setSfPathCacheForTesting(null); + assert.throws( + () => runSfCommand(['--version']), + (err) => { + assert.ok(err instanceof SfNotFoundError); + assert.equal(err.code, 'SF_NOT_FOUND'); + return true; + } + ); + }); + + it('throws SfNotFoundError with path hint when explicit sfPath gives ENOENT', () => { + spawnStub.returns(makeEnoent()); + assert.throws( + () => runSfCommand(['--version'], '/bad/path/sf'), + (err) => { + assert.ok(err instanceof SfNotFoundError); + assert.ok(err.message.includes('/bad/path/sf')); + return true; + } + ); + }); + + it('rethrows non-ENOENT errors', () => { + const genericErr = new Error('unexpected failure'); + spawnStub.returns({ + stdout: '', + stderr: '', + status: null, + error: genericErr, + pid: undefined, + output: [], + signal: null, + }); + assert.throws(() => runSfCommand(['--version']), genericErr); + }); + + it('returns exitCode 1 when status is null and no error', () => { + spawnStub.returns({ stdout: '', stderr: '', status: null, error: undefined, pid: 1, output: [], signal: null }); + const result = runSfCommand(['--version']); + assert.equal(result.exitCode, 1); + }); + + it('returns non-zero exitCode from failed command', () => { + spawnStub.returns(makeSpawnFail(2, '', 'error text')); + const result = runSfCommand(['bad', 'command']); + assert.equal(result.exitCode, 2); + assert.equal(result.stderr, 'error text'); + }); + + describe('Windows shell path injection guard', () => { + beforeEach(() => { + setSfPlatformForTesting('win32'); + }); + + it('rejects sfPath containing & on win32', () => { + // Path ends with .cmd (triggers shell) but has & in directory name + assert.throws(() => runSfCommand(['--version'], 'C:\\sf & malicious\\sf.cmd'), /unsafe for shell execution/); + }); + + it('rejects sfPath containing | on win32', () => { + assert.throws(() => runSfCommand(['--version'], 'C:\\sf|evil\\sf.cmd'), /unsafe for shell execution/); + }); + + it('accepts clean .cmd path on win32', () => { + spawnStub.returns(makeSpawnOk('ok')); + // Should not throw — clean path + const result = runSfCommand(['--version'], 'C:\\Program Files\\sf\\bin\\sf.cmd'); + assert.equal(result.exitCode, 0); + }); + }); + + describe('probe-based resolution', () => { + beforeEach(() => { + setSfPathCacheForTesting(undefined); // force a fresh probe + setSfPlatformForTesting('linux'); + }); + + it('caches "sf" when shell:false probe succeeds', () => { + spawnStub.returns(makeSpawnOk('sf/2.0.0')); + runSfCommand(['--version']); + const [, args, opts] = spawnStub.firstCall.args as [string, string[], { shell: boolean }]; + assert.deepEqual(args, ['--version']); // probe call + assert.equal(opts.shell, false); + }); + + it('returns SF_NOT_FOUND when probe fails and no common path exists', () => { + // Both probe attempts fail with ENOENT, no common paths match + spawnStub.returns(makeEnoent()); + assert.throws( + () => runSfCommand(['--version']), + (err) => { + assert.ok(err instanceof SfNotFoundError); + return true; + } + ); + }); + + it('Windows two-phase probe: retries with shell:true on ENOENT', () => { + setSfPlatformForTesting('win32'); + // First call (shell:false) → ENOENT + spawnStub.onFirstCall().returns(makeEnoent()); + // Second call (shell:true) → success + spawnStub.onSecondCall().returns(makeSpawnOk('sf/2.0.0')); + // Third call → the actual command + spawnStub.onThirdCall().returns(makeSpawnOk('result')); + + const result = runSfCommand(['--version']); + assert.equal(result.stdout, 'result'); + + // Verify second probe used shell:true + const [, , opts] = spawnStub.secondCall.args as [string, string[], { shell: boolean }]; + assert.equal(opts.shell, true); + }); + }); +});