diff --git a/.claude/skills/cli-command-development/SKILL.md b/.claude/skills/cli-command-development/SKILL.md new file mode 100644 index 00000000..8a8f1779 --- /dev/null +++ b/.claude/skills/cli-command-development/SKILL.md @@ -0,0 +1,327 @@ +--- +name: cli-command-development +description: Creating new CLI commands and topics for the B2C CLI using oclif +--- + +# CLI Command Development + +This skill covers creating new CLI commands and topics for the B2C CLI. + +## Command Organization + +Commands live in `packages/b2c-cli/src/commands/`. The directory structure maps directly to command names: + +``` +commands/ +├── code/ +│ ├── deploy.ts → b2c code deploy +│ ├── activate.ts → b2c code activate +│ └── list.ts → b2c code list +├── ods/ +│ ├── create.ts → b2c ods create +│ └── list.ts → b2c ods list +└── mrt/ + └── env/ + └── var/ + └── set.ts → b2c mrt env var set +``` + +## Command Class Hierarchy + +Choose the appropriate base class based on what your command needs: + +``` +BaseCommand (logging, JSON output, error handling) + └─ OAuthCommand (OAuth authentication) + ├─ InstanceCommand (B2C instance: hostname, code version) + │ ├─ CartridgeCommand (cartridge path + filters) + │ ├─ JobCommand (job execution helpers) + │ └─ WebDavCommand (WebDAV root directory) + ├─ MrtCommand (Managed Runtime API) + └─ OdsCommand (On-Demand Sandbox API) +``` + +Import from `@salesforce/b2c-tooling-sdk/cli`: + +```typescript +import { InstanceCommand, CartridgeCommand, OdsCommand } from '@salesforce/b2c-tooling-sdk/cli'; +``` + +## Standard Command Template + +```typescript +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root + */ +import {Args, Flags} from '@oclif/core'; +import {InstanceCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import {t} from '../../i18n/index.js'; + +interface MyCommandResponse { + success: boolean; + data: SomeType[]; +} + +export default class MyCommand extends InstanceCommand { + static description = t('commands.topic.mycommand.description', 'Human-readable description'); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> arg1', + '<%= config.bin %> <%= command.id %> --flag value', + '<%= config.bin %> <%= command.id %> --json', + ]; + + static args = { + name: Args.string({ + description: 'Description of the argument', + required: true, + }), + }; + + static flags = { + myFlag: Flags.string({ + char: 'm', + description: 'Flag description', + default: 'defaultValue', + }), + myBool: Flags.boolean({ + description: 'Boolean flag', + default: false, + }), + }; + + async run(): Promise { + // Validation - call appropriate require* methods + this.requireServer(); + + // Access parsed args and flags + const {name} = this.args; + const {myFlag, myBool} = this.flags; + + this.log(t('commands.topic.mycommand.working', 'Working on {{name}}...', {name})); + + // Implementation + const result = await this.instance.ocapi.GET('/some/endpoint'); + + if (!result.data) { + this.error(t('commands.topic.mycommand.error', 'Failed: {{message}}', { + message: result.response?.statusText || 'Unknown error', + })); + } + + const response: MyCommandResponse = { + success: true, + data: result.data, + }; + + // JSON mode returns the object directly (oclif handles serialization) + if (this.jsonEnabled()) { + return response; + } + + // Human-readable output + this.log('Success!'); + return response; + } +} +``` + +## Adding a New Topic + +When creating a new command topic, add it to `packages/b2c-cli/package.json` in the oclif section: + +```json +{ + "oclif": { + "topics": { + "newtopic": { + "description": "Commands for new functionality" + }, + "newtopic:subtopic": { + "description": "Subtopic commands" + } + } + } +} +``` + +## Flag Patterns + +### Common Flag Types + +```typescript +static flags = { + // String with short alias and env var fallback + server: Flags.string({ + char: 's', + description: 'Server hostname', + env: 'SFCC_SERVER', + }), + + // Integer with default + timeout: Flags.integer({ + description: 'Timeout in seconds', + default: 60, + }), + + // Boolean with --no-* variant + wait: Flags.boolean({ + description: 'Wait for completion', + default: true, + allowNo: true, // enables --no-wait + }), + + // Comma-separated multiple values + channels: Flags.string({ + description: 'Site channels (comma-separated)', + multiple: true, + multipleNonGreedy: true, + delimiter: ',', + }), + + // Enum-like options + format: Flags.string({ + description: 'Output format', + options: ['json', 'csv', 'table'], + default: 'table', + }), + + // Conditional flag + secret: Flags.string({ + description: 'Client secret (required for private clients)', + dependsOn: ['client-id'], + }), +}; +``` + +## Table Output + +Use `createTable` for consistent tabular output: + +```typescript +import {createTable, TableRenderer, type ColumnDef} from '@salesforce/b2c-tooling-sdk/cli'; + +type MyData = {id: string; name: string; status: string}; + +const COLUMNS: Record> = { + id: { + header: 'ID', + get: (item) => item.id, + }, + name: { + header: 'Name', + get: (item) => item.name, + }, + status: { + header: 'Status', + get: (item) => item.status, + extended: true, // Only shown with --extended + }, +}; + +const DEFAULT_COLUMNS = ['id', 'name']; +const tableRenderer = new TableRenderer(COLUMNS); + +// In run(): +tableRenderer.render(data, DEFAULT_COLUMNS); + +// With --columns flag support: +const columns = this.flags.columns + ? tableRenderer.validateColumnKeys(this.flags.columns.split(',')) + : DEFAULT_COLUMNS; +tableRenderer.render(data, columns); +``` + +## Internationalization + +All user-facing strings use the `t()` function: + +```typescript +import {t} from '../../i18n/index.js'; + +// Basic usage +this.log(t('commands.topic.cmd.message', 'Default message')); + +// With interpolation +this.log(t('commands.topic.cmd.working', 'Processing {{count}} items...', {count: 5})); + +// For errors +this.error(t('commands.topic.cmd.error', 'Failed: {{message}}', {message: err.message})); +``` + +Keys follow the pattern: `commands...` + +## Validation Methods + +Base classes provide validation helpers: + +```typescript +// From OAuthCommand +this.requireOAuthCredentials(); // Ensures clientId + clientSecret +this.hasOAuthCredentials(); // Returns boolean + +// From InstanceCommand +this.requireServer(); // Ensures hostname is set +this.requireCodeVersion(); // Ensures code version is set +this.requireWebDavCredentials(); // Ensures WebDAV auth (Basic or OAuth) + +// From MrtCommand +this.requireMrtCredentials(); // Ensures MRT API credentials +``` + +## Accessing Clients + +InstanceCommand provides lazy-loaded clients: + +```typescript +// OCAPI client (OAuth authenticated) +const result = await this.instance.ocapi.GET('/code_versions'); + +// WebDAV client (Basic or OAuth authenticated) +await this.instance.webdav.put('path/to/file', buffer); + +// ODS client (from OdsCommand) +const sandboxes = await this.odsClient.GET('/sandboxes'); + +// MRT client (from MrtCommand) +const projects = await this.mrtClient.GET('/api/projects/'); +``` + +## Error Handling + +```typescript +// Simple error (exits with code 1) +this.error('Something went wrong'); + +// Error with suggestions +this.error('Config file not found', { + suggestions: ['Run b2c auth login first', 'Check your dw.json file'], +}); + +// Warning (continues execution) +this.warn('Deprecated flag used'); + +// Structured API errors +if (result.error) { + this.error(t('commands.topic.cmd.apiError', 'API error: {{message}}', { + message: formatApiError(result.error), + })); +} +``` + +## Creating a Command Checklist + +1. Create file at `packages/b2c-cli/src/commands//.ts` +2. Choose appropriate base class +3. Define `static description`, `examples`, `args`, `flags` +4. Set `static enableJsonFlag = true` for JSON output support +5. Implement `run()` method with proper return type +6. Add topic to `package.json` if new +7. Add i18n keys for all user-facing strings +8. Update skill in `plugins/b2c-cli/skills/b2c-/SKILL.md` if exists +9. Update CLI reference docs in `docs/cli/.md` +10. Build and test: `pnpm run build && pnpm --filter @salesforce/b2c-cli run test` diff --git a/.claude/skills/documentation/SKILL.md b/.claude/skills/documentation/SKILL.md new file mode 100644 index 00000000..00f671e6 --- /dev/null +++ b/.claude/skills/documentation/SKILL.md @@ -0,0 +1,405 @@ +--- +name: documentation +description: Updating user guides, CLI reference, and API documentation for the B2C CLI project +--- + +# Documentation + +This skill covers updating documentation for the B2C CLI project. + +## Documentation Structure + +The project has three types of documentation: + +``` +docs/ +├── guide/ # User guides (manually written) +│ ├── index.md +│ ├── installation.md +│ ├── authentication.md +│ └── configuration.md +├── cli/ # CLI reference (manually written) +│ ├── index.md +│ ├── code.md +│ ├── webdav.md +│ ├── jobs.md +│ └── ... +├── api/ # API reference (auto-generated) +│ └── *.md +└── .vitepress/ # Vitepress configuration + └── config.mts +``` + +## Documentation Types + +### 1. User Guides (`docs/guide/`) + +Purpose: Help users get started and understand concepts. + +When to update: +- New features that need explanation +- Changes to installation or setup process +- New authentication methods +- Configuration changes + +Example structure: + +```markdown +# Getting Started + +Introduction to the topic. + +## Prerequisites + +- Node.js 22+ +- pnpm + +## Installation + +\`\`\`bash +pnpm install -g @salesforce/b2c-cli +\`\`\` + +## Next Steps + +- [Authentication](./authentication.md) +- [Configuration](./configuration.md) +``` + +### 2. CLI Reference (`docs/cli/`) + +Purpose: Document command syntax, flags, and usage examples. + +When to update: +- New commands added +- Flags added, removed, or changed +- Command behavior changes +- New examples needed + +Structure per command topic: + +```markdown +# Code Commands + +Commands for managing code versions and cartridge deployment. + +## b2c code deploy + +Deploy cartridges to a B2C Commerce instance. + +### Usage + +\`\`\`bash +b2c code deploy [PATH] [FLAGS] +\`\`\` + +### Arguments + +| Argument | Description | Required | Default | +|----------|-------------|----------|---------| +| PATH | Path to cartridges directory | No | . | + +### Flags + +| Flag | Short | Description | Default | +|------|-------|-------------|---------| +| --server | -s | Instance hostname | - | +| --code-version | -v | Code version name | - | +| --cartridge | -c | Include specific cartridges | - | +| --exclude-cartridge | -x | Exclude cartridges | - | + +### Examples + +\`\`\`bash +# Deploy all cartridges in current directory +b2c code deploy --server dev01.example.com --code-version v1 + +# Deploy specific cartridges +b2c code deploy ./cartridges -c app_storefront -c app_custom + +# Deploy excluding certain cartridges +b2c code deploy -x test_cartridge -x bm_extensions +\`\`\` + +### Authentication + +Requires WebDAV credentials (username/password) or OAuth. +``` + +### 3. API Reference (`docs/api/`) + +Purpose: Document the SDK programmatic API. + +This is **auto-generated** from TypeScript JSDoc comments using TypeDoc. + +Never edit files in `docs/api/` directly. Instead: + +1. Update JSDoc comments in SDK source files +2. Run `pnpm run docs:api` to regenerate + +## Writing JSDoc for API Docs + +### Module-Level Documentation + +Add to barrel files (`index.ts`): + +```typescript +/** + * Authentication strategies for B2C Commerce APIs. + * + * This module provides various authentication mechanisms: + * - OAuth 2.0 client credentials + * - Basic authentication + * - API key authentication + * + * @example + * ```typescript + * import { OAuthStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * + * const auth = new OAuthStrategy({ + * clientId: 'my-client', + * clientSecret: 'my-secret', + * }); + * ``` + * + * @module auth + */ +``` + +### Class Documentation + +```typescript +/** + * Client for WebDAV file operations on B2C Commerce instances. + * + * Supports uploading, downloading, and managing files in various + * WebDAV roots (Cartridges, IMPEX, Logs, etc.). + * + * @example + * ```typescript + * const client = new WebDavClient(hostname, auth); + * await client.put('Cartridges/v1/app_custom/file.js', content); + * ``` + */ +export class WebDavClient { + /** + * Creates a new WebDAV client. + * + * @param hostname - The B2C Commerce instance hostname + * @param auth - Authentication strategy to use + */ + constructor(hostname: string, auth: AuthStrategy) {} +} +``` + +### Function Documentation + +```typescript +/** + * Deploys cartridges to a B2C Commerce instance. + * + * Discovers cartridges in the specified path, creates a ZIP archive, + * and uploads via WebDAV. + * + * @param instance - The B2C instance to deploy to + * @param cartridgePath - Path containing cartridge directories + * @param options - Deployment options + * @returns Deployment result with uploaded cartridges + * + * @example + * ```typescript + * const result = await deployCartridges(instance, './cartridges', { + * include: ['app_storefront'], + * }); + * console.log(result.cartridges); + * ``` + * + * @throws {DeploymentError} If deployment fails + */ +export async function deployCartridges( + instance: B2CInstance, + cartridgePath: string, + options?: DeployOptions +): Promise {} +``` + +### Type Documentation + +```typescript +/** + * Configuration for OAuth authentication. + */ +export interface OAuthConfig { + /** OAuth client ID */ + clientId: string; + + /** OAuth client secret */ + clientSecret: string; + + /** OAuth scopes to request */ + scopes?: string[]; +} +``` + +## Building Documentation + +```bash +# Generate API docs from JSDoc +pnpm run docs:api + +# Start dev server (includes API generation) +pnpm run docs:dev + +# Build static site +pnpm run docs:build + +# Preview built site +pnpm run docs:preview +``` + +## TypeDoc Configuration + +Located in `typedoc.json`: + +```json +{ + "entryPoints": [ + "packages/b2c-tooling-sdk/src/auth/index.ts", + "packages/b2c-tooling-sdk/src/clients/index.ts", + "packages/b2c-tooling-sdk/src/operations/code/index.ts" + ], + "out": "docs/api", + "plugin": [ + "typedoc-plugin-markdown", + "typedoc-vitepress-theme" + ], + "exclude": ["**/*.generated.ts"] +} +``` + +When adding new SDK modules, add their barrel file to `entryPoints`. + +## Vitepress Configuration + +Located in `docs/.vitepress/config.mts`: + +```typescript +export default defineConfig({ + title: 'B2C CLI', + base: '/b2c-developer-tooling/', + + themeConfig: { + nav: [ + { text: 'Guide', link: '/guide/' }, + { text: 'CLI Reference', link: '/cli/' }, + { text: 'API Reference', link: '/api/' }, + ], + + sidebar: { + '/guide/': [ + { + text: 'Getting Started', + items: [ + { text: 'Installation', link: '/guide/installation' }, + { text: 'Authentication', link: '/guide/authentication' }, + ], + }, + ], + '/cli/': [ + { + text: 'Commands', + items: [ + { text: 'code', link: '/cli/code' }, + { text: 'webdav', link: '/cli/webdav' }, + ], + }, + ], + }, + }, +}); +``` + +When adding new CLI commands or guide pages, update the sidebar config. + +## Claude Code Skills (Plugin) + +The `plugins/b2c-cli/skills/` directory contains skills that teach Claude about using the CLI commands. These are distributed via the plugin. + +When to update: +- New CLI commands added +- Existing commands changed +- New usage patterns + +Skill format: + +```markdown +--- +name: b2c- +description: Brief description +--- + +# B2C Skill + +Overview of the command topic. + +## Examples + +### + +\`\`\`bash +# Comment explaining the command +b2c [args] [flags] +\`\`\` + +### + +\`\`\`bash +b2c --flag value +\`\`\` +``` + +## Documentation Update Checklist + +### When Adding a CLI Command + +1. Update `docs/cli/.md` with command documentation +2. Update `docs/.vitepress/config.mts` sidebar if new topic +3. Update `plugins/b2c-cli/skills/b2c-/SKILL.md` with examples + +### When Adding an SDK Module + +1. Write module-level JSDoc in barrel file +2. Write JSDoc for all public classes, functions, types +3. Add entry point to `typedoc.json` if new module +4. Run `pnpm run docs:api` to regenerate + +### When Changing CLI Behavior + +1. Update affected examples in `docs/cli/*.md` +2. Update affected examples in `plugins/b2c-cli/skills/*/SKILL.md` +3. Update guide pages if conceptual changes + +### When Adding Configuration Options + +1. Update `docs/guide/configuration.md` +2. Update relevant CLI command docs with new flags +3. Update skills with new flag examples + +## Navigation Structure + +**Top Navigation:** +- Guide (`/guide/`) +- CLI Reference (`/cli/`) +- API Reference (`/api/`) + +**Sidebar:** +- Contextual based on section +- API reference sidebar auto-generated from TypeDoc + +## Style Guidelines + +- Use code blocks with language hints (```bash, ```typescript) +- Include practical examples for every command/function +- Keep flag tables consistent across command docs +- Use relative links for internal references +- Avoid emojis unless specifically requested diff --git a/.claude/skills/sdk-module-development/SKILL.md b/.claude/skills/sdk-module-development/SKILL.md new file mode 100644 index 00000000..6d8427d8 --- /dev/null +++ b/.claude/skills/sdk-module-development/SKILL.md @@ -0,0 +1,395 @@ +--- +name: sdk-module-development +description: Adding new modules and exports to the @salesforce/b2c-tooling-sdk package +--- + +# SDK Module Development + +This skill covers adding new modules and exports to the `@salesforce/b2c-tooling-sdk` package. + +## Package Structure + +The SDK is organized into functional layers: + +``` +packages/b2c-tooling-sdk/src/ +├── auth/ # Authentication strategies +├── instance/ # B2CInstance entry point +├── clients/ # HTTP clients (WebDAV, OCAPI, SLAS, ODS, MRT) +├── platform/ # Platform APIs +├── operations/ # High-level business operations +│ ├── code/ # Code deployment +│ ├── jobs/ # Job execution +│ ├── sites/ # Site management +│ └── mrt/ # MRT deployments +├── cli/ # Base command classes for oclif +├── logging/ # Pino-based logging +├── errors/ # Error types +├── config/ # Configuration loading +└── i18n/ # Internationalization +``` + +## Barrel File Pattern + +Each module uses an `index.ts` barrel file that exports the public API: + +```typescript +/** + * Authentication strategies for B2C Commerce APIs. + * + * This module provides various authentication mechanisms for B2C Commerce: + * - OAuth 2.0 client credentials flow + * - Basic authentication for WebDAV + * - API key authentication + * + * @example + * ```typescript + * import { OAuthStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + * + * const auth = new OAuthStrategy({ + * clientId: 'my-client', + * clientSecret: 'my-secret', + * }); + * ``` + * + * @module auth + */ + +// Types +export type { AuthStrategy, AuthConfig } from './types.js'; + +// Strategies +export { BasicAuthStrategy } from './basic.js'; +export { OAuthStrategy, decodeJWT } from './oauth.js'; + +// Resolution helpers +export { resolveAuthStrategy, checkAvailableAuthMethods } from './resolve.js'; +``` + +Key points: +- Module-level JSDoc with `@module` tag for TypeDoc +- Include usage examples in the JSDoc +- Group exports logically (types, classes, functions) +- Only export public API items + +## Package.json Exports + +The `exports` field in `packages/b2c-tooling-sdk/package.json` uses a development condition pattern: + +```json +{ + "exports": { + "./newmodule": { + "development": "./src/newmodule/index.ts", + "import": { + "types": "./dist/esm/newmodule/index.d.ts", + "default": "./dist/esm/newmodule/index.js" + }, + "require": { + "types": "./dist/cjs/newmodule/index.d.ts", + "default": "./dist/cjs/newmodule/index.js" + } + } + } +} +``` + +The `development` condition: +- Points directly to TypeScript source files +- Used when running with `--conditions=development` (via `bin/dev.js`) +- Enables hot-reloading during development without rebuilding + +## Client Factory Pattern + +HTTP clients follow a consistent factory function pattern: + +```typescript +// src/clients/newapi.ts +import createClient, { type Client } from 'openapi-fetch'; +import type { AuthStrategy } from '../auth/types.js'; +import type { paths, components } from './newapi.generated.js'; +import { createAuthMiddleware, createLoggingMiddleware } from './middleware.js'; + +// Re-export generated types for consumers +export type { paths, components }; + +// Define client type alias +export type NewApiClient = Client; + +// Factory function (not class) +export function createNewApiClient( + hostname: string, + auth: AuthStrategy, + options?: { apiVersion?: string } +): NewApiClient { + const { apiVersion = 'v1' } = options ?? {}; + + const client = createClient({ + baseUrl: `https://${hostname}/api/newapi/${apiVersion}`, + }); + + // Middleware order: auth first (runs last), logging last (sees complete request) + client.use(createAuthMiddleware(auth)); + client.use(createLoggingMiddleware('NEWAPI')); + + return client; +} +``` + +Then export from the clients barrel: + +```typescript +// src/clients/index.ts +export { createNewApiClient } from './newapi.js'; +export type { NewApiClient, paths as NewApiPaths, components as NewApiComponents } from './newapi.js'; +``` + +## OpenAPI Type Generation + +For APIs with OpenAPI specs, generate TypeScript types: + +1. Add the spec file to `packages/b2c-tooling-sdk/specs/` + +2. Update the generate script in `package.json`: + +```json +{ + "scripts": { + "generate:types": "openapi-typescript specs/data-api.json -o src/clients/ocapi.generated.ts && openapi-typescript specs/newapi-v1.yaml -o src/clients/newapi.generated.ts" + } +} +``` + +3. Run generation: + +```bash +pnpm --filter @salesforce/b2c-tooling-sdk run generate:types +``` + +4. Import the generated types in your client: + +```typescript +import type { paths, components } from './newapi.generated.js'; +``` + +## Operations Module Pattern + +Operations group related business logic: + +``` +src/operations/newfeature/ +├── index.ts # Barrel file with module JSDoc +├── list.ts # List operation +├── create.ts # Create operation +└── types.ts # Shared types (if needed) +``` + +Example operation: + +```typescript +// src/operations/newfeature/list.ts +import type { NewApiClient } from '../../clients/newapi.js'; + +export interface ListOptions { + filter?: string; + limit?: number; +} + +export interface ListResult { + items: Item[]; + total: number; +} + +/** + * Lists items from the new feature API. + * + * @param client - The NewApi client instance + * @param options - List options + * @returns List result with items and total count + * + * @example + * ```typescript + * const result = await listItems(client, { limit: 10 }); + * console.log(result.items); + * ``` + */ +export async function listItems( + client: NewApiClient, + options?: ListOptions +): Promise { + const { data, error } = await client.GET('/items', { + params: { + query: { + filter: options?.filter, + limit: options?.limit, + }, + }, + }); + + if (error) { + throw new Error(`Failed to list items: ${error.message}`); + } + + return { + items: data.items, + total: data.total, + }; +} +``` + +Barrel file: + +```typescript +// src/operations/newfeature/index.ts +/** + * Operations for the new feature. + * + * @example + * ```typescript + * import { listItems, createItem } from '@salesforce/b2c-tooling-sdk/operations/newfeature'; + * ``` + * + * @module operations/newfeature + */ + +export { listItems, type ListOptions, type ListResult } from './list.js'; +export { createItem, type CreateOptions } from './create.js'; +``` + +## Adding a New Module - Step by Step + +### 1. Create the module directory + +```bash +mkdir -p packages/b2c-tooling-sdk/src/newmodule +``` + +### 2. Create the implementation file(s) + +```typescript +// src/newmodule/feature.ts +export interface FeatureConfig { + setting: string; +} + +export class Feature { + constructor(private config: FeatureConfig) {} + + doSomething(): string { + return this.config.setting; + } +} +``` + +### 3. Create the barrel file with module JSDoc + +```typescript +// src/newmodule/index.ts +/** + * New module for feature X. + * + * @example + * ```typescript + * import { Feature } from '@salesforce/b2c-tooling-sdk/newmodule'; + * const f = new Feature({ setting: 'value' }); + * ``` + * + * @module newmodule + */ + +export { Feature, type FeatureConfig } from './feature.js'; +``` + +### 4. Add to package.json exports + +```json +{ + "exports": { + "./newmodule": { + "development": "./src/newmodule/index.ts", + "import": { + "types": "./dist/esm/newmodule/index.d.ts", + "default": "./dist/esm/newmodule/index.js" + }, + "require": { + "types": "./dist/cjs/newmodule/index.d.ts", + "default": "./dist/cjs/newmodule/index.js" + } + } + } +} +``` + +### 5. Optionally export from main index + +If the module should be accessible from the main package export: + +```typescript +// src/index.ts +export { Feature } from './newmodule/index.js'; +export type { FeatureConfig } from './newmodule/index.js'; +``` + +### 6. Build and test + +```bash +pnpm --filter @salesforce/b2c-tooling-sdk run build +pnpm --filter @salesforce/b2c-tooling-sdk run test +``` + +## Build System + +The SDK builds to both ESM and CommonJS: + +```json +{ + "scripts": { + "build": "pnpm run generate:types && pnpm run build:esm && pnpm run build:cjs", + "build:esm": "tsc -p tsconfig.esm.json", + "build:cjs": "tsc -p tsconfig.cjs.json && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json" + } +} +``` + +TypeScript configs: +- `tsconfig.json` - Base config with strict settings +- `tsconfig.esm.json` - ESM build to `dist/esm/` +- `tsconfig.cjs.json` - CJS build to `dist/cjs/` + +## Import Patterns + +For consumers of the SDK: + +```typescript +// Main export +import { B2CInstance } from '@salesforce/b2c-tooling-sdk'; + +// Sub-module exports +import { OAuthStrategy } from '@salesforce/b2c-tooling-sdk/auth'; +import { WebDavClient } from '@salesforce/b2c-tooling-sdk/clients'; +import { findAndDeployCartridges } from '@salesforce/b2c-tooling-sdk/operations/code'; +import { createLogger } from '@salesforce/b2c-tooling-sdk/logging'; +import { CartridgeCommand } from '@salesforce/b2c-tooling-sdk/cli'; +``` + +In tests, always use package imports (not relative paths): + +```typescript +// Good - uses package exports +import { WebDavClient } from '@salesforce/b2c-tooling-sdk/clients'; + +// Avoid - relative paths +import { WebDavClient } from '../../src/clients/webdav.js'; +``` + +## New Module Checklist + +1. Create module directory under `src/` +2. Implement feature in separate files +3. Create `index.ts` barrel with module-level JSDoc +4. Add export to `package.json` with development condition +5. Optionally add to main `src/index.ts` exports +6. Write tests in `test/` mirroring the src structure +7. Run `pnpm run build` to verify compilation +8. Run `pnpm run test` to verify tests pass +9. Update TypeDoc entry points in `typedoc.json` if needed diff --git a/.claude/skills/testing/SKILL.md b/.claude/skills/testing/SKILL.md new file mode 100644 index 00000000..98c602e9 --- /dev/null +++ b/.claude/skills/testing/SKILL.md @@ -0,0 +1,530 @@ +--- +name: testing +description: Writing tests for the B2C CLI project using Mocha, Chai, and MSW +--- + +# Testing + +This skill covers writing tests for the B2C CLI project using Mocha, Chai, and MSW. + +## Test Framework Stack + +- **Test Runner**: Mocha +- **Assertions**: Chai (property-based) +- **HTTP Mocking**: MSW (Mock Service Worker) +- **Code Coverage**: c8 +- **TypeScript**: tsx (native execution without compilation) + +## Running Tests + +```bash +# Run all tests with coverage +pnpm run test + +# Run tests for specific package +pnpm --filter @salesforce/b2c-tooling-sdk run test +pnpm --filter @salesforce/b2c-cli run test + +# Run single test file (no coverage, faster) +cd packages/b2c-tooling-sdk +pnpm mocha "test/clients/webdav.test.ts" + +# Run tests matching pattern +pnpm mocha --grep "mkcol" "test/**/*.test.ts" + +# Watch mode for TDD +pnpm --filter @salesforce/b2c-tooling-sdk run test:watch +``` + +## Test Organization + +Tests mirror the source directory structure: + +``` +packages/b2c-tooling-sdk/ +├── src/ +│ └── clients/ +│ └── webdav.ts +└── test/ + └── clients/ + └── webdav.test.ts +``` + +Use `.test.ts` suffix for test files. + +## Import Patterns + +Always use package exports, not relative paths: + +```typescript +// Good - uses package exports +import { WebDavClient } from '@salesforce/b2c-tooling-sdk/clients'; +import { OAuthStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + +// Avoid - relative paths +import { WebDavClient } from '../../src/clients/webdav.js'; +``` + +This ensures tests use the same export paths as consumers. + +## HTTP Mocking with MSW + +### Basic Setup + +```typescript +import { expect } from 'chai'; +import { http, HttpResponse } from 'msw'; +import { setupServer } from 'msw/node'; +import { WebDavClient } from '@salesforce/b2c-tooling-sdk/clients'; +import { MockAuthStrategy } from '../helpers/mock-auth.js'; + +const TEST_HOST = 'test.salesforce.com'; +const BASE_URL = `https://${TEST_HOST}`; + +const server = setupServer(); + +describe('WebDavClient', () => { + let client: WebDavClient; + let mockAuth: MockAuthStrategy; + + before(() => { + server.listen({ onUnhandledRequest: 'error' }); + }); + + afterEach(() => { + server.resetHandlers(); + }); + + after(() => { + server.close(); + }); + + beforeEach(() => { + mockAuth = new MockAuthStrategy(); + client = new WebDavClient(TEST_HOST, mockAuth); + }); + + it('creates a directory successfully', async () => { + server.use( + http.all(`${BASE_URL}/*`, ({ request }) => { + if (request.method === 'MKCOL') { + return new HttpResponse(null, { status: 201 }); + } + return new HttpResponse(null, { status: 405 }); + }), + ); + + await client.mkcol('Cartridges/v1'); + // If no error thrown, test passes + }); +}); +``` + +### Request Capture Pattern + +To verify request details, capture requests in an array: + +```typescript +interface CapturedRequest { + method: string; + url: string; + headers: Headers; + body?: unknown; +} + +const requests: CapturedRequest[] = []; + +beforeEach(() => { + requests.length = 0; +}); + +it('sends correct headers', async () => { + server.use( + http.put(`${BASE_URL}/*`, async ({ request }) => { + requests.push({ + method: request.method, + url: request.url, + headers: request.headers, + body: await request.text(), + }); + return new HttpResponse(null, { status: 201 }); + }), + ); + + await client.put('path/to/file', Buffer.from('content')); + + expect(requests).to.have.length(1); + expect(requests[0].method).to.equal('PUT'); + expect(requests[0].headers.get('Authorization')).to.equal('Bearer test-token'); + expect(requests[0].body).to.equal('content'); +}); +``` + +### Mocking Different HTTP Methods + +```typescript +// GET request +http.get(`${BASE_URL}/api/items`, () => { + return HttpResponse.json({ items: [{ id: '1' }] }); +}); + +// POST request with body inspection +http.post(`${BASE_URL}/api/items`, async ({ request }) => { + const body = await request.json(); + return HttpResponse.json({ id: '123', ...body }, { status: 201 }); +}); + +// PUT request +http.put(`${BASE_URL}/api/items/:id`, ({ params }) => { + return HttpResponse.json({ id: params.id, updated: true }); +}); + +// DELETE request +http.delete(`${BASE_URL}/api/items/:id`, () => { + return new HttpResponse(null, { status: 204 }); +}); + +// Match any method +http.all(`${BASE_URL}/*`, ({ request }) => { + // Handle based on request.method +}); +``` + +### Error Responses + +```typescript +it('handles 404 errors', async () => { + server.use( + http.get(`${BASE_URL}/api/items/:id`, () => { + return HttpResponse.json( + { error: 'Not found' }, + { status: 404 } + ); + }), + ); + + try { + await client.getItem('nonexistent'); + expect.fail('Should have thrown'); + } catch (error) { + expect(error.message).to.include('404'); + } +}); + +it('handles network errors', async () => { + server.use( + http.get(`${BASE_URL}/api/items`, () => { + return HttpResponse.error(); + }), + ); + + try { + await client.listItems(); + expect.fail('Should have thrown'); + } catch (error) { + expect(error.message).to.include('network'); + } +}); +``` + +## MockAuthStrategy + +Use the test helper for authentication: + +```typescript +// test/helpers/mock-auth.ts +import type { AuthStrategy } from '@salesforce/b2c-tooling-sdk/auth'; + +export class MockAuthStrategy implements AuthStrategy { + constructor(private token: string = 'test-token') {} + + async fetch(url: string, init?: RequestInit): Promise { + const headers = new Headers(init?.headers); + headers.set('Authorization', `Bearer ${this.token}`); + return fetch(url, { ...init, headers }); + } + + async getAuthorizationHeader(): Promise { + return `Bearer ${this.token}`; + } +} +``` + +Usage: + +```typescript +import { MockAuthStrategy } from '../helpers/mock-auth.js'; + +const mockAuth = new MockAuthStrategy(); +const client = new WebDavClient(TEST_HOST, mockAuth); + +// Custom token for specific tests +const customAuth = new MockAuthStrategy('custom-token'); +``` + +## Testing Operations + +Operations tests verify higher-level business logic: + +```typescript +import { expect } from 'chai'; +import { http, HttpResponse } from 'msw'; +import { setupServer } from 'msw/node'; +import { uploadBundle } from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import { createMrtClient } from '@salesforce/b2c-tooling-sdk/clients'; +import { MockAuthStrategy } from '../helpers/mock-auth.js'; + +const server = setupServer(); + +describe('uploadBundle', () => { + const testBundle = { + message: 'Test bundle', + encoding: 'base64', + data: 'dGVzdC1kYXRh', + }; + + before(() => server.listen({ onUnhandledRequest: 'error' })); + afterEach(() => server.resetHandlers()); + after(() => server.close()); + + it('uploads bundle and returns result', async () => { + let receivedBody: unknown; + + server.use( + http.post('https://cloud.commercecloud.com/api/projects/:slug/builds/', async ({ request, params }) => { + receivedBody = await request.json(); + return HttpResponse.json({ + bundle_id: 123, + message: 'Bundle created', + }); + }), + ); + + const auth = new MockAuthStrategy(); + const client = createMrtClient({}, auth); + + const result = await uploadBundle(client, 'my-project', testBundle); + + expect(result.bundleId).to.equal(123); + expect(receivedBody).to.deep.include({ message: 'Test bundle' }); + }); +}); +``` + +## Testing Pure Logic + +For functions without HTTP calls: + +```typescript +import { expect } from 'chai'; +import { checkAvailableAuthMethods } from '@salesforce/b2c-tooling-sdk/auth'; + +describe('checkAvailableAuthMethods', () => { + it('returns client-credentials when credentials provided', () => { + const result = checkAvailableAuthMethods({ + clientId: 'test-client', + clientSecret: 'test-secret', + }); + + expect(result.available).to.include('client-credentials'); + }); + + it('returns unavailable with reason when secret missing', () => { + const result = checkAvailableAuthMethods( + { clientId: 'test-client' }, + ['client-credentials'] + ); + + expect(result.available).to.have.length(0); + expect(result.unavailable[0]).to.deep.equal({ + method: 'client-credentials', + reason: 'clientSecret is required', + }); + }); +}); +``` + +## Testing CLI Commands + +Use `@oclif/test` for CLI command tests: + +```typescript +import { runCommand } from '@oclif/test'; +import { expect } from 'chai'; + +describe('ods list', () => { + it('runs without errors', async () => { + const { error } = await runCommand('ods list --help'); + expect(error).to.be.undefined; + }); +}); +``` + +## End-to-End Tests + +E2E tests run against real infrastructure: + +```typescript +import { expect } from 'chai'; +import { execa } from 'execa'; +import path from 'node:path'; + +describe('ODS Lifecycle E2E', function () { + this.timeout(360_000); // 6 minutes + + const CLI_BIN = path.resolve(__dirname, '../../../bin/run.js'); + + before(function () { + if (!process.env.SFCC_CLIENT_ID || !process.env.SFCC_CLIENT_SECRET) { + this.skip(); + } + }); + + async function runCLI(args: string[]) { + return execa('node', [CLI_BIN, ...args], { + env: { ...process.env, SFCC_LOG_LEVEL: 'silent' }, + reject: false, + }); + } + + it('creates a sandbox', async function () { + this.timeout(300_000); + + const result = await runCLI([ + 'ods', 'create', + '--realm', process.env.TEST_REALM!, + '--ttl', '24', + '--json', + ]); + + expect(result.exitCode).to.equal(0); + const response = JSON.parse(result.stdout); + expect(response.id).to.be.a('string'); + }); +}); +``` + +## Test Structure Patterns + +### Describe/It Nesting + +```typescript +describe('WebDavClient', () => { + describe('mkcol', () => { + it('creates directory on success', async () => { }); + it('throws on 403 forbidden', async () => { }); + it('handles nested paths', async () => { }); + }); + + describe('put', () => { + it('uploads file content', async () => { }); + it('sets correct content-type', async () => { }); + }); +}); +``` + +### Setup/Teardown + +```typescript +describe('Feature', () => { + let sharedResource: Resource; + + before(() => { + // Once before all tests in this describe + }); + + after(() => { + // Once after all tests in this describe + }); + + beforeEach(() => { + // Before each test + sharedResource = new Resource(); + }); + + afterEach(() => { + // After each test + sharedResource.cleanup(); + }); +}); +``` + +## Chai Assertions + +Common assertion patterns: + +```typescript +// Equality +expect(value).to.equal('expected'); +expect(obj).to.deep.equal({ key: 'value' }); + +// Truthiness +expect(value).to.be.true; +expect(value).to.be.false; +expect(value).to.be.undefined; +expect(value).to.be.null; + +// Arrays +expect(arr).to.have.length(3); +expect(arr).to.include('item'); +expect(arr).to.deep.include({ id: '1' }); + +// Objects +expect(obj).to.have.property('key'); +expect(obj).to.have.property('key', 'value'); +expect(obj).to.deep.include({ subset: 'props' }); + +// Strings +expect(str).to.include('substring'); +expect(str).to.match(/pattern/); + +// Errors +expect(() => fn()).to.throw(); +expect(() => fn()).to.throw('message'); +expect(() => fn()).to.throw(ErrorType); + +// Async errors +try { + await asyncFn(); + expect.fail('Should have thrown'); +} catch (error) { + expect(error.message).to.include('expected'); +} +``` + +## Coverage + +Coverage is configured in `.c8rc.json`: + +```json +{ + "all": true, + "src": ["src"], + "exclude": ["src/clients/*.generated.ts", "test/**"], + "reporter": ["text", "text-summary", "html", "lcov"], + "check-coverage": true, + "lines": 5, + "functions": 5, + "branches": 5, + "statements": 5 +} +``` + +View coverage report: + +```bash +pnpm run test +# Then open coverage/index.html +``` + +## Writing Tests Checklist + +1. Create test file in `test/` mirroring source structure +2. Use `.test.ts` suffix +3. Import from package names, not relative paths +4. Set up MSW server for HTTP tests +5. Use MockAuthStrategy for authenticated clients +6. Test both success and error paths +7. Use request capture to verify HTTP call details +8. Clean up handlers with `afterEach(() => server.resetHandlers())` +9. Run tests: `pnpm --filter run test` diff --git a/AGENTS.md b/AGENTS.md index 09df5a57..e3de37b9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,6 +3,7 @@ This is a monorepo project with the following packages: - `./packages/b2c-cli` - the command line interface built with oclif - `./packages/b2c-tooling-sdk` - the SDK/library for B2C Commerce operations; supports the CLI and can be used standalone +- `./packages/b2c-dx-mcp` - Model Context Protocol server; also built with oclif ## Common Commands @@ -45,11 +46,7 @@ pnpm run -r lint ## Documentation -- prefer verbose jsdoc comments for all public methods and classes -- TypeDoc and vitepress will generate documentation from these comments in the `./docs/api` folder -- module level jsdocs will be used for organization; for example packages/b2c-tooling-sdk/src/auth/index.ts barrel file has the module level docs for the `auth` module -- see the typedoc.json file for configuration options including the entry points for documentation generation -- update the docs/ markdown files (non-generated) for user guides and CLI reference when updating major CLI functionalty or adding new commands +See [documentation skill](./.claude/skills/documentation/SKILL.md) for details on updating user guides, CLI reference, and API docs. ## Logging @@ -59,124 +56,21 @@ pnpm run -r lint ## Table Output -When rendering tabular data in CLI commands, use the shared `TableRenderer` utility from `@salesforce/b2c-tooling-sdk/cli`: +Use `createTable` from `@salesforce/b2c-tooling-sdk/cli` for tabular output. See [CLI command development skill](./.claude/skills/cli-command-development/SKILL.md) for patterns. -```typescript -import { createTable, type ColumnDef } from '@salesforce/b2c-tooling-sdk/cli'; +## Claude Code Skills -// Define columns with header and getter function -const COLUMNS: Record> = { - id: { header: 'ID', get: (item) => item.id }, - name: { header: 'Name', get: (item) => item.name }, - status: { header: 'Status', get: (item) => item.status }, -}; +**User-facing skills** (for CLI users): `./plugins/b2c-cli/skills/` - update when modifying CLI commands. -// Render the table -createTable(COLUMNS).render(data, ['id', 'name', 'status']); -``` - -Features: -- Dynamic column widths based on content -- Supports `extended` flag on columns for optional fields -- Use `TableRenderer` class directly for column validation helpers (e.g., `--columns` flag support) - -## Claude Code Skills Plugin - -The `./plugins/b2c-cli/skills/` directory contains Claude Code skills that teach Claude about the CLI commands. Each skill has a `SKILL.md` file with examples and documentation. - -**When modifying CLI commands:** -- Update the corresponding skill in `plugins/b2c-cli/skills/b2c-/SKILL.md` if it exists -- For breaking changes (renamed flags, removed arguments, changed behavior), update all affected examples - -**Skill format:** -```markdown ---- -name: b2c- -description: Brief description of what the skill teaches ---- - -# B2C Skill - -Overview of the command topic. - -## Examples - -### - -\`\`\`bash -# comment explaining the command -b2c [args] [flags] -\`\`\` -``` +**Developer skills** (for contributors): `./.claude/skills/` - covers CLI development, SDK modules, testing, and documentation. ## Testing -Tests use Mocha + Chai with c8 for coverage. HTTP mocking uses MSW (Mock Service Worker). - -### Running Tests +See [testing skill](./.claude/skills/testing/SKILL.md) for patterns on writing tests with Mocha, Chai, and MSW. +Quick commands: ```bash -# Run all tests with coverage -pnpm run test - -# Run tests for specific package -pnpm --filter @salesforce/b2c-tooling-sdk run test - -# Run single test file (no coverage, faster) -cd packages/b2c-tooling-sdk -pnpm mocha "test/clients/webdav.test.ts" - -# Run tests matching pattern -pnpm mocha --grep "mkcol" "test/**/*.test.ts" - -# Watch mode for TDD -pnpm --filter @salesforce/b2c-tooling-sdk run test:watch +pnpm run test # Run all tests +pnpm --filter @salesforce/b2c-tooling-sdk run test # Test specific package +pnpm mocha "test/clients/webdav.test.ts" # Single file (no coverage) ``` - -### Writing Tests - -- Place tests in `packages//test/` mirroring the src structure -- Use `.test.ts` suffix for test files -- Import from package names, not relative paths: - ```typescript - // Good - uses package exports - import { WebDavClient } from '@salesforce/b2c-tooling-sdk/clients'; - - // Avoid - relative paths - import { WebDavClient } from '../../src/clients/webdav.js'; - ``` - -### HTTP Mocking with MSW - -For testing HTTP clients, use MSW to mock at the network level: - -```typescript -import { http, HttpResponse } from 'msw'; -import { setupServer } from 'msw/node'; - -const server = setupServer(); - -before(() => server.listen({ onUnhandledRequest: 'error' })); -afterEach(() => server.resetHandlers()); -after(() => server.close()); - -it('makes HTTP request', async () => { - server.use( - http.get('https://example.com/api/*', () => { - return HttpResponse.json({ data: 'test' }); - }), - ); - - // Test code that makes HTTP requests... -}); -``` - -### Test Helpers - -- `test/helpers/mock-auth.ts` - Mock AuthStrategy for testing HTTP clients - -### Coverage - -- Coverage reports generated in `coverage/` directory -- SDK package has 5% minimum threshold (will increase as tests are added) -- CI publishes coverage summary to GitHub Actions job summary