Install AI Tools

B2C Commerce tools, documentation, and skills for your assistant.

Claude

Install the plugin Recommended

bash
claude plugin marketplace add SalesforceCommerceCloud/b2c-developer-tooling
claude plugin install b2c-dx-mcp@b2c-developer-tooling

Start a new Claude Code session. To install for the current project only, run it from your project directory with --scope project.

Manual MCP setup
bash
claude mcp add --transport stdio --scope user b2c-dx-mcp -- npx -y @salesforce/b2c-dx-mcp@latest

Start a new session. To configure the current project only, run it from your project directory with --scope project. See Claude Code MCP setup.

Claude Desktop setup

Codex

Install the plugin Recommended

bash
codex plugin marketplace add SalesforceCommerceCloud/b2c-developer-tooling
codex plugin add b2c-dx-mcp@b2c-developer-tooling

Start a new Codex session in your project. This setup also works with the Codex IDE extension and the ChatGPT Work desktop app.

Manual MCP setup
bash
codex mcp add b2c-dx-mcp -- npx -y @salesforce/b2c-dx-mcp@latest

Or add this to ~/.codex/config.toml (or $CODEX_HOME/config.toml if customized):

toml
[mcp_servers.b2c-dx-mcp]
command = "npx"
args = ["-y", "@salesforce/b2c-dx-mcp@latest"]

Start a new session. See Codex MCP configuration.

ChatGPT online setup

VS Code

Install the plugin Recommended

  1. Open the Command Palette (Cmd/Ctrl+Shift+P) and run Chat: Install Plugin from Source.
  2. Enter SalesforceCommerceCloud/b2c-developer-tooling.
  3. Select b2c-dx-mcp and follow the installation prompts.
  4. Start a new chat in GitHub Copilot.
Manual MCP setup

Add this to .vscode/mcp.json in your workspace:

json
{
  "servers": {
    "b2c-dx-mcp": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@salesforce/b2c-dx-mcp@latest"]
    }
  }
}

See VS Code MCP setup.

Copilot CLI setup

Cursor

Reload the MCP server in Cursor after installation.

Manual MCP setup

Add this to .cursor/mcp.json in your project:

json
{
  "mcpServers": {
    "b2c-dx-mcp": {
      "command": "npx",
      "args": ["-y", "@salesforce/b2c-dx-mcp@latest"]
    }
  }
}

For all projects, use ~/.cursor/mcp.json instead.

See Cursor's MCP documentation.

OpenCode

Add this to opencode.json in your project:

json
{
  "mcp": {
    "b2c-dx-mcp": {
      "type": "local",
      "command": ["npx", "-y", "@salesforce/b2c-dx-mcp@latest"],
      "enabled": true
    }
  }
}

Restart OpenCode. For all projects, use ~/.config/opencode/opencode.json. See OpenCode MCP setup.

Gemini

Run:

bash
gemini mcp add --scope user b2c-dx-mcp -- npx -y @salesforce/b2c-dx-mcp@latest

Start a new Gemini CLI session. To configure the current project only, run it from your project directory with --scope project. See Gemini CLI MCP setup.

No separate skills plugins needed.

Other clients and manual setup →
Skip to content
View as Markdown
View as Markdown

CI/CD with GitHub Actions ​

The B2C Developer Tooling project provides official GitHub Actions for automating B2C Commerce operations in your CI/CD pipelines.

Overview ​

The official actions handle CLI installation, credential configuration, and Node.js setup automatically — so your workflow files stay focused on what you want to deploy rather than how to configure the tooling. High-level actions provide typed inputs for common operations like code deployment and data import, while a raw command passthrough covers everything else.

Action v2 installs CLI 2.x by default. Action v1 remains on CLI 1.x, so a workflow only adopts breaking CLI changes when its uses: references move from @v1 to @v2.

Staying with Action v1

Keep @v1 when a workflow must retain CLI 1.x behavior. For the operations migrated in CLI 2—job, code, bm users, bm roles, sites, and catalog discovery—Action v1 continues to use the CLI 1.x OCAPI implementations and legacy result shapes. CLI 1.x is not globally OCAPI-only: commands designed specifically for SCAPI continue to use SCAPI.

The actions are available from the SalesforceCommerceCloud/b2c-developer-tooling repository and support:

  • Code deployment — deploy and activate cartridges
  • Data import — import site archives in a single step
  • MRT deployment — push and deploy MRT storefront bundles
  • Job execution — run B2C jobs with wait and timeout
  • WebDAV uploads — upload files for data import, content, etc.
  • Any CLI command — raw passthrough for operations not covered by high-level actions

All actions are composite YAML — no compiled JavaScript, fully transparent and auditable. The high-level actions (code-deploy, data-import, job-run, mrt-deploy, webdav-upload) reuse an already-installed CLI and only install one when none is present — so running a deploy after a setup step, or repeated operations on a self-hosted runner, do not trigger a redundant reinstall. The setup action called directly is explicit: it installs the version you request. Actions expose structured JSON outputs for downstream workflow steps.

Authentication ​

Store credentials as GitHub repository secrets and non-sensitive configuration as repository variables.

Recommended secrets:

SecretDescription
SFCC_CLIENT_IDOAuth Client ID
SFCC_CLIENT_SECRETOAuth Client Secret
SFCC_USERNAMEWebDAV username
SFCC_PASSWORDWebDAV password/access key
MRT_API_KEYMRT API key

Recommended variables:

VariableDescription
SFCC_SERVERB2C instance hostname
MRT_PROJECTMRT project slug
MRT_ENVIRONMENTMRT environment

Credentials can be passed per-action or set once with the setup action so they're available to all subsequent steps.

Quick Start: Deploy Cartridges ​

yaml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # Install the B2C CLI and configure credentials for subsequent steps
      - uses: SalesforceCommerceCloud/b2c-developer-tooling/actions/setup@v2
        with:
          client-id: ${{ secrets.SFCC_CLIENT_ID }}
          client-secret: ${{ secrets.SFCC_CLIENT_SECRET }}
          server: ${{ vars.SFCC_SERVER }}
          username: ${{ secrets.SFCC_USERNAME }}
          password: ${{ secrets.SFCC_PASSWORD }}

      # Run your build steps as usual
      - run: npm ci
      - run: npm run build

      # Generate a code version from the branch name and date
      - name: Set code version
        id: version
        run: |
          BRANCH=$(echo "$GITHUB_REF_NAME" | tr '/' '-')
          echo "code-version=${BRANCH}-$(date +%Y%m%d-%H%M%S)" >> "$GITHUB_OUTPUT"

      # Deploy cartridges — only operation-specific inputs needed
      - uses: SalesforceCommerceCloud/b2c-developer-tooling/actions/code-deploy@v2
        with:
          code-version: ${{ steps.version.outputs.code-version }}
          activate: true

The setup step installs the CLI and configures credentials for all subsequent steps. Everything after that — your build, version calculation, and deploy — can focus on your project's needs.

Actions Reference ​

Root Action ​

uses: SalesforceCommerceCloud/b2c-developer-tooling@v2

Combines setup and command execution. Pass a command to run a CLI command, or omit it for setup-only.

InputDefaultDescription
command—CLI command to run
version2CLI version to install
node-version22Node.js version
jsontrueAppend --json flag and parse output
working-directory.Working directory
Auth inputs—See Authentication

Setup ​

uses: SalesforceCommerceCloud/b2c-developer-tooling/actions/setup@v2

Installs the CLI and writes credentials to environment variables. Use this when you need multiple steps after setup. Called directly, setup always installs the requested version. Set skip-if-present: 'true' to reuse an already-installed CLI and install only when none is present (this is what the high-level actions do internally so they never reinstall on top of an existing CLI).

yaml
- uses: SalesforceCommerceCloud/b2c-developer-tooling/actions/setup@v2
  with:
    client-id: ${{ secrets.SFCC_CLIENT_ID }}
    client-secret: ${{ secrets.SFCC_CLIENT_SECRET }}
    server: ${{ vars.SFCC_SERVER }}
    plugins: |
      @myorg/b2c-plugin-custom
      sfcc-solutions-share/b2c-plugin-intellij-sfcc-config

Plugins are installed after the CLI; already-installed plugins are skipped by exact name match. Each line is an npm package name or GitHub owner/repo. For reliable skip-on-reinstall, prefer the published npm package name — when a GitHub owner/repo slug differs from the package it publishes, the plugin is reinstalled each run (harmless, just slower).

The setup action accepts the following inputs (each maps to the corresponding SFCC_* environment variable):

InputEnvironment Variable
client-idSFCC_CLIENT_ID
client-secretSFCC_CLIENT_SECRET
serverSFCC_SERVER
code-versionSFCC_CODE_VERSION
usernameSFCC_USERNAME
passwordSFCC_PASSWORD
short-codeSFCC_SHORTCODE
tenant-idSFCC_TENANT_ID
account-manager-hostSFCC_ACCOUNT_MANAGER_HOST
webdav-serverSFCC_WEBDAV_SERVER
certificateSFCC_CERTIFICATE
certificate-base64SFCC_CERTIFICATE (decoded to a temp file)
certificate-passphraseSFCC_CERTIFICATE_PASSPHRASE
selfsignedSFCC_SELFSIGNED
mrt-api-keyMRT_API_KEY
mrt-projectMRT_PROJECT
mrt-environmentMRT_ENVIRONMENT
log-levelSFCC_LOG_LEVEL

The webdav-server, certificate, certificate-base64, certificate-passphrase, and selfsigned inputs are only needed for staging environments that require a separate WebDAV hostname and a client certificate (mTLS). See Staging Environments (Two-Factor mTLS).

Run ​

uses: SalesforceCommerceCloud/b2c-developer-tooling/actions/run@v2

Executes any CLI command. Pairs with the setup action.

yaml
- uses: SalesforceCommerceCloud/b2c-developer-tooling/actions/run@v2
  with:
    command: 'sandbox list --realm abcd'
InputDefaultDescription
command(required)CLI command to run
jsontrueAppend --json and parse output
working-directory.Working directory

Code Deploy ​

uses: SalesforceCommerceCloud/b2c-developer-tooling/actions/code-deploy@v2

Deploy cartridges with typed inputs.

yaml
- uses: SalesforceCommerceCloud/b2c-developer-tooling/actions/code-deploy@v2
  with:
    client-id: ${{ secrets.SFCC_CLIENT_ID }}
    client-secret: ${{ secrets.SFCC_CLIENT_SECRET }}
    server: ${{ vars.SFCC_SERVER }}
    username: ${{ secrets.SFCC_USERNAME }}
    password: ${{ secrets.SFCC_PASSWORD }}
    code-version: ${{ vars.SFCC_CODE_VERSION }}
    activate: true
    cartridges: 'app_storefront_base,app_custom'
InputDefaultDescription
cartridge-path.Path to cartridge source directory
activatefalseActivate code version after deploy
code-version—Code version (overrides env)
cartridges—Comma-separated cartridges to include
exclude-cartridges—Comma-separated cartridges to exclude
deletefalseDelete existing cartridges first

Data Import ​

uses: SalesforceCommerceCloud/b2c-developer-tooling/actions/data-import@v2

Import a site archive. Handles upload, job execution, waiting, and cleanup in one step.

yaml
- uses: SalesforceCommerceCloud/b2c-developer-tooling/actions/data-import@v2
  with:
    client-id: ${{ secrets.SFCC_CLIENT_ID }}
    client-secret: ${{ secrets.SFCC_CLIENT_SECRET }}
    server: ${{ vars.SFCC_SERVER }}
    username: ${{ secrets.SFCC_USERNAME }}
    password: ${{ secrets.SFCC_PASSWORD }}
    target: './export/site-import.zip'
    timeout: 600
InputDefaultDescription
target(required)Local file, directory, or zip to import
timeout—Timeout in seconds
keep-archivefalseKeep archive on instance after import
show-logtrueShow job log on failure

MRT Deploy ​

uses: SalesforceCommerceCloud/b2c-developer-tooling/actions/mrt-deploy@v2

Push and deploy an MRT bundle.

yaml
- uses: SalesforceCommerceCloud/b2c-developer-tooling/actions/mrt-deploy@v2
  with:
    mrt-api-key: ${{ secrets.MRT_API_KEY }}
    project: ${{ vars.MRT_PROJECT }}
    environment: ${{ vars.MRT_ENVIRONMENT }}
    build-directory: build
    message: 'Deploy from CI'
InputDefaultDescription
project—MRT project slug
environment—Target environment
build-directorybuildLocal build directory
message—Bundle message
bundle-id—Deploy existing bundle by ID

Job Run ​

uses: SalesforceCommerceCloud/b2c-developer-tooling/actions/job-run@v2

Execute a B2C job and optionally wait for completion.

yaml
- uses: SalesforceCommerceCloud/b2c-developer-tooling/actions/job-run@v2
  with:
    job-id: 'sfcc-site-archive-import'
    wait: true
    timeout: 600
    parameters: |
      ImportFile=site-import.zip
      ImportMode=merge
InputDefaultDescription
job-id(required)Job ID to execute
waittrueWait for completion
timeout900Timeout in seconds (when wait=true)
parameters—KEY=VALUE pairs, one per line
show-logtrueShow job log on failure

WebDAV Upload ​

uses: SalesforceCommerceCloud/b2c-developer-tooling/actions/webdav-upload@v2

Upload files via WebDAV.

yaml
- uses: SalesforceCommerceCloud/b2c-developer-tooling/actions/webdav-upload@v2
  with:
    local-path: './export/site-import.zip'
    remote-path: 'src/instance/'
    root: IMPEX
InputDefaultDescription
local-path(required)Local file or directory
remote-path(required)Remote destination path
rootIMPEXWebDAV root (IMPEX, TEMP, CARTRIDGES, etc.)

Staging Environments (Two-Factor mTLS) ​

Hyperforce staging: create your certificates first

On Hyperforce, you manage the CA for code upload client certificates yourself through eCDN. See Set Up Two-Factor Code Upload to create and register a CA once, then issue a dedicated .p12 for each pipeline:

bash
b2c ecdn mtls issue --ca-cert-file ca.pem --ca-key-file ca.key --name github-actions --output ./ci.p12

After migration, code upload uses the staging hostname (staging-<realm>-<customer>.demandware.net) set as server; webdav-server and selfsigned aren't needed. The separate cert.staging.* WebDAV hostname in the examples below applies only until your realm is migrated.

Internal staging instances typically require:

  • A separate WebDAV hostname (often a cert.* variant of the main hostname)
  • A PKCS12 (.p12) client certificate with passphrase for mutual TLS
  • Permission to accept self-signed server certificates

See Two-Factor Authentication (mTLS) for the underlying configuration model.

CLI Flags ​

The same options that go in dw.json are available as CLI flags. For local use against a staging instance:

bash
b2c code deploy \
  --server staging-internal-ccdemo.demandware.net \
  --webdav-server cert.staging.internal.ccdemo.demandware.net \
  --certificate /path/to/STG-2FA-ccdemo/deploy.p12 \
  --passphrase 'your-cert-passphrase' \
  --selfsigned \
  --client-id "$SFCC_CLIENT_ID" \
  --client-secret "$SFCC_CLIENT_SECRET"
Flagdw.json FieldEnvironment Variable
--serverhostnameSFCC_SERVER
--webdav-serverwebdav-hostnameSFCC_WEBDAV_SERVER
--certificatecertificateSFCC_CERTIFICATE
--passphrasecertificate-passphraseSFCC_CERTIFICATE_PASSPHRASE
--selfsignedself-signedSFCC_SELFSIGNED

GitHub Actions ​

Staging mTLS works with the standard actions — the setup action accepts webdav-server, certificate-base64, certificate-passphrase, and selfsigned inputs alongside the usual auth inputs.

Because the .p12 is a binary file, store it as a base64-encoded GitHub secret and pass it to certificate-base64 (Actions v2.2.0 and later). setup decodes it to an owner-only file in the runner's temp directory and points SFCC_CERTIFICATE at it. To use a certificate file that's already on disk, pass its path to certificate instead.

These are in addition to the authentication secrets and variables — the same SFCC_* names used elsewhere map straight through to the setup inputs:

SecretMaps to input → env varDescription
SFCC_CLIENT_IDclient-id → SFCC_CLIENT_IDOAuth Client ID
SFCC_CLIENT_SECRETclient-secret → SFCC_CLIENT_SECRETOAuth Client Secret
SFCC_CERTIFICATE_PASSPHRASEcertificate-passphrase → SFCC_CERTIFICATE_PASSPHRASEPassphrase for the .p12
STAGING_CERTIFICATE_P12_BASE64certificate-base64 → SFCC_CERTIFICATE (decoded path)Base64-encoded .p12 client certificate
VariableMaps to input → env varDescription
SFCC_SERVERserver → SFCC_SERVERe.g. staging-internal-ccdemo.demandware.net
SFCC_WEBDAV_SERVERwebdav-server → SFCC_WEBDAV_SERVERe.g. cert.staging.internal.ccdemo.demandware.net

STAGING_CERTIFICATE_P12_BASE64 holds the base64 of the certificate file rather than a setting, so it isn't named like an SFCC_* environment variable.

To create the base64 secret locally:

bash
base64 -i deploy.p12 | pbcopy   # macOS
# or
base64 -w0 deploy.p12           # Linux

Then paste the value into a GitHub repository secret.

Workflow example:

yaml
name: Deploy to Staging

on:
  workflow_dispatch:

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: SalesforceCommerceCloud/b2c-developer-tooling/actions/setup@v2
        with:
          client-id: ${{ secrets.SFCC_CLIENT_ID }}
          client-secret: ${{ secrets.SFCC_CLIENT_SECRET }}
          server: ${{ vars.SFCC_SERVER }}
          webdav-server: ${{ vars.SFCC_WEBDAV_SERVER }}
          certificate-base64: ${{ secrets.STAGING_CERTIFICATE_P12_BASE64 }}
          certificate-passphrase: ${{ secrets.SFCC_CERTIFICATE_PASSPHRASE }}
          selfsigned: 'true'

      - run: npm ci && npm run build

      - uses: SalesforceCommerceCloud/b2c-developer-tooling/actions/code-deploy@v2
        with:
          code-version: staging-${{ github.run_number }}
          activate: true

Once the setup step writes SFCC_CERTIFICATE, SFCC_WEBDAV_SERVER, etc. to $GITHUB_ENV, every subsequent action picks them up automatically — no need to repeat them on code-deploy, data-import, job-run, or webdav-upload.

Multiple Environments in One Workflow

If a single workflow targets both a normal sandbox and a staging instance, run setup again before each phase with the appropriate inputs. The second setup only overrides env vars for inputs you actually pass — anything left blank keeps its value from the previous setup. To fully switch environments, re-pass every variable that should change (or use the env: block on individual steps to scope overrides).

Cleanup

setup writes the decoded .p12 to $RUNNER_TEMP, which the runner clears after every job. Never commit the certificate or its base64 value to the repository.

Patterns ​

Data Import Pipeline ​

Import a site archive:

yaml
name: Data Import

on:
  workflow_dispatch:
    inputs:
      import-file:
        description: 'Path to the site import archive'
        required: true
        default: 'export/site-import.zip'

jobs:
  import:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: SalesforceCommerceCloud/b2c-developer-tooling/actions/data-import@v2
        with:
          client-id: ${{ secrets.SFCC_CLIENT_ID }}
          client-secret: ${{ secrets.SFCC_CLIENT_SECRET }}
          server: ${{ vars.SFCC_SERVER }}
          username: ${{ secrets.SFCC_USERNAME }}
          password: ${{ secrets.SFCC_PASSWORD }}
          target: ${{ github.event.inputs.import-file }}
          timeout: 600

MRT Release Deploy ​

Build and deploy an MRT storefront on release:

yaml
name: MRT Deploy

on:
  release:
    types: [published]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build storefront
        run: npm run build

      - uses: SalesforceCommerceCloud/b2c-developer-tooling/actions/mrt-deploy@v2
        with:
          mrt-api-key: ${{ secrets.MRT_API_KEY }}
          project: ${{ vars.MRT_PROJECT }}
          environment: ${{ vars.MRT_ENVIRONMENT }}
          build-directory: build
          message: 'Release ${{ github.event.release.tag_name }}'

Using Outputs ​

When json is enabled (the default), the result output contains the command's structured JSON. Reference it directly in downstream steps:

yaml
- uses: SalesforceCommerceCloud/b2c-developer-tooling/actions/code-deploy@v2
  id: deploy
  with:
    code-version: v25_03_1
    activate: true

- name: Show deploy result
  run: echo '${{ steps.deploy.outputs.result }}'
json
{
  "cartridges": [
    {"name": "app_storefront_base", "dest": "app_storefront_base", "src": "..."},
    {"name": "app_custom", "dest": "app_custom", "src": "..."}
  ],
  "codeVersion": "v25_03_1",
  "reloaded": true
}

Actions exit with the CLI's exit code, so a failed job will fail the step. Use continue-on-error and fromJSON() when you need to inspect the result after a failure:

yaml
- uses: SalesforceCommerceCloud/b2c-developer-tooling/actions/job-run@v2
  id: job
  continue-on-error: true
  with:
    job-id: 'sfcc-site-archive-import'
    wait: true

- name: Handle job failure
  if: steps.job.outputs.exit-code != '0'
  run: echo "Job failed with status ${{ fromJSON(steps.job.outputs.result).exitStatus.code }}"

Version Pinning ​

Use the floating Action major to receive backward-compatible Action updates. Action v2 selects the latest CLI 2.x release by default; Action v1 selects the latest CLI 1.x release.

yaml
- name: Pin the CLI while following compatible Action v2 updates
  uses: SalesforceCommerceCloud/b2c-developer-tooling@v2
  with:
    version: '2.0.0' # Pin an exact CLI version

- name: Pin both the Action suite and CLI
  uses: SalesforceCommerceCloud/b2c-developer-tooling@v2.2.0
  with:
    version: '2.0.0'

Use an immutable Action tag such as @v2.2.0, or a full commit SHA, when the workflow must not receive automatic Action updates. Set version: latest explicitly only when it should cross future CLI major versions automatically.

Upgrade from Action v1 ​

Change every B2C Action reference in the workflow from @v1 to @v2; do not mix majors in one job because high-level actions reuse an already-installed CLI. CLI 2 normalizes structured job results to camelCase, so update parsed fields such as execution_status and exit_status.code to executionStatus and exitStatus.code. Review any other command JSON consumed by the workflow before upgrading.

Keep @v1 to remain on the maintained CLI 1.x line. For operations migrated to SCAPI-first in CLI 2, this also preserves their CLI 1.x OCAPI behavior and legacy result shapes. An explicit version: '1' follows the newest published CLI 1.x maintenance release; an exact value such as 1.23.2 freezes the CLI as well.

Reproducibility: Each released high-level or root action internally references its matching immutable actions/setup@v2.x.y and actions/run@v2.x.y release. Pin the outer action to an exact release tag for a fixed Action suite. Use direct actions/setup and actions/run references pinned to full commit SHAs when organizational policy requires SHA pins for every action.

Plugins ​

The CLI supports plugins for custom configuration sources, HTTP middleware, and more. Install plugins in CI with the plugins input on the setup action:

yaml
- uses: SalesforceCommerceCloud/b2c-developer-tooling/actions/setup@v2
  with:
    client-id: ${{ secrets.SFCC_CLIENT_ID }}
    client-secret: ${{ secrets.SFCC_CLIENT_SECRET }}
    server: ${{ vars.SFCC_SERVER }}
    plugins: |
      @myorg/b2c-plugin-custom
      sfcc-solutions-share/b2c-plugin-intellij-sfcc-config

Each line is an npm package name or GitHub owner/repo. Plugins are installed after the CLI; already-installed plugins are skipped on re-invocation.

Logging ​

The setup action accepts a log-level input that sets SFCC_LOG_LEVEL for all subsequent steps:

yaml
- uses: SalesforceCommerceCloud/b2c-developer-tooling/actions/setup@v2
  with:
    log-level: debug

Available levels (most to least verbose): trace, debug, info (default), warn, error, silent.

You can also set the environment variable directly in your workflow:

yaml
env:
  SFCC_LOG_LEVEL: debug

Logs are always human-readable on stderr. The --json flag only controls the structured result on stdout. If you need machine-readable log lines (e.g., for log aggregation), set SFCC_JSON_LOGS=true.

CI Defaults ​

All actions automatically configure:

  • NO_COLOR=1 — clean log output without ANSI colors