diff --git a/.changeset/quickstart-guides.md b/.changeset/quickstart-guides.md new file mode 100644 index 000000000..dc6450375 --- /dev/null +++ b/.changeset/quickstart-guides.md @@ -0,0 +1,5 @@ +--- +'@salesforce/b2c-dx-docs': minor +--- + +Add interactive Quickstart guides at `/quickstart/` covering 17 common setup tasks (deploy code, set up an AI coding agent, manage sandboxes, install the VS Code extension, run jobs, manage Page Designer content, configure CI/CD, configure SCAPI access, tail/search logs, debug server-side scripts, manage Account Manager + BM admin, manage cartridge paths, configure multiple instances, deploy to Managed Runtime, manage SLAS clients, migrate from sfcc-ci, and download API docs). Each guide walks the user through the minimum config for their task and synthesises a `dw.json` snippet, anchored checklist, and verify command. Filterable by tag, searchable by title, and deep-linkable via `?qs=`. diff --git a/.claude/skills/cli-command-development/SKILL.md b/.claude/skills/cli-command-development/SKILL.md index b1332b7e3..ba3ada7bc 100644 --- a/.claude/skills/cli-command-development/SKILL.md +++ b/.claude/skills/cli-command-development/SKILL.md @@ -345,3 +345,12 @@ See [API Client Development](../api-client-development/SKILL.md#error-handling) 8. Update skill in `skills/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` +11. **Evaluate Quickstart guide impact** — if this command is part of an + existing guided workflow (e.g., new `b2c sites cartridges …` flag goes + in the `cartridge-path` guide), update + `docs/.vitepress/data/adventures/.ts`. If this is a brand-new + command surface that benefits from a step-by-step setup wizard, + consider adding a new guide (see `PLAN_guides.md` and the + [documentation skill](../documentation/SKILL.md)). Run + `pnpm --filter @salesforce/b2c-dx-docs run docs:build` afterward — the + anchor checker validates every doc link the guides reference. diff --git a/.claude/skills/documentation/SKILL.md b/.claude/skills/documentation/SKILL.md index 7c7f768d6..72035c4b7 100644 --- a/.claude/skills/documentation/SKILL.md +++ b/.claude/skills/documentation/SKILL.md @@ -11,7 +11,7 @@ This skill covers updating documentation for the B2C CLI project. ## Documentation Structure -The project has three types of documentation: +The project has four types of documentation: ``` docs/ @@ -26,10 +26,21 @@ docs/ │ ├── webdav.md │ ├── jobs.md │ └── ... +├── quickstart/ # Interactive Quickstart guides (page shims) +│ ├── index.md # topic listing rendered by +│ ├── deploy-code.md +│ ├── jobs.md +│ └── ... # each renders ├── api/ # API reference (auto-generated) │ └── *.md -└── .vitepress/ # Vitepress configuration - └── config.mts +└── .vitepress/ + ├── config.mts + └── data/adventures/ # Quickstart guide DATA (TS source of truth) + ├── _types.ts + ├── _authoring.ts # defineAdventure / step / choice / md / doc + ├── _helpers.ts # dwJson / ocapiConfig / scopes / link / check + ├── index.ts # registry + presets + feature flags + └── .ts # one file per guide ``` ## Documentation Types @@ -128,7 +139,111 @@ b2c code deploy -x test_cartridge -x bm_extensions Requires WebDAV credentials (username/password) or OAuth. ``` -### 3. API Reference (`docs/api/`) +### 3. Quickstart Guides (`docs/quickstart/` + `docs/.vitepress/data/adventures/`) + +Purpose: Interactive, branching wizards that synthesise a minimal `dw.json` ++ checklist + verify command for a specific user task. The user-facing name +is **Quickstart guide**; the internal data type is `Adventure` and the +authoring helpers are named accordingly (legacy term — keep it inside +`.ts` files only, never in user-visible UI strings). + +Each guide is a typed `Adventure` object built with `defineAdventure({...})`. +The page at `docs/quickstart/.md` is just a 3-line shim: + +```md +--- +title: My guide · Quickstart +description: Short tagline. +layout: doc +sidebar: false +aside: false +--- + + +``` + +The real content lives in `docs/.vitepress/data/adventures/.ts`: + +```ts +import {choice, defineAdventure, doc, md, step} from './_authoring.js'; +import {check, dwJson, link, ocapiConfig, scopes} from './_helpers.js'; + +export const myGuide = defineAdventure({ + id: 'my-guide', + title: 'Do the thing', + tagline: 'One-line summary.', + icon: 'mdi:something', + tags: ['oauth', 'webdav'], // search / filter on the index + priority: 'common', // 'core' | 'common' | 'specialized' | 'niche' + intro: 'Optional preamble shown above step 1.', + steps: [ + step('auth', { + title: 'How will you authenticate?', + doc: doc('/guide/authentication', 'account-manager-api-client'), + choices: [ + choice('client-credentials', { + title: 'Client Credentials', + icon: 'mdi:key-variant', + body: md`Recommended for CI. See [JWT setup](/guide/authentication#jwt-authentication-certificate-based).`, + contributes: {authMethod: 'client-credentials'}, + }), + ], + }), + ], + synthesize(state) { + return { + dwJson: dwJson({hostname: true, clientId: true, clientSecret: state.authMethod === 'client-credentials'}), + checklist: [check('Create an Account Manager API client', link('/guide/authentication', 'creating-an-api-client', 'Creating an API Client'))], + verifyCommand: 'b2c whatever', + }; + }, +}); +``` + +After authoring, register the guide in `docs/.vitepress/data/adventures/index.ts` +(import + push into the `adventures` array under the matching priority comment). + +**When to update an existing guide:** +- A CLI command, flag, or env-var name referenced in the synthesizer or a + choice body changed. +- A doc heading anchor referenced via `doc()` / `link()` / a markdown + `[text](/path#anchor)` link was renamed (the build-time anchor checker + catches this — `pnpm --filter @salesforce/b2c-dx-docs run docs:build`). +- An auth/role/scope/permission requirement changed. + +**When to add a new guide:** +- A new CLI command surface or workflow that needs more than a doc page — + i.e., the user has to make decisions and we can synthesise a useful + `dw.json` / checklist for each path. +- See `PLAN_guides.md` at the repo root for the prioritised backlog. + +**When to remove a guide:** +- The underlying CLI surface is deprecated or has been merged into another + guide. Remove the `.ts`, the `.md` shim, and the registry entry. + +**Authoring helpers (do not reinvent these):** +| Helper | Purpose | +|--------|---------| +| `defineAdventure` | Wraps the literal in a typed `Adventure`; normalises step array → record. | +| `step(id, {...})` | One step in the wizard. Has a `doc:` anchor and a list of choices. | +| `choice(id, {...})` | One option inside a step. `contributes:` feeds the synthesizer. | +| `md\`…\`` | Tagged template for multi-line markdown bodies. | +| `doc(path, hash, label)` | Internal doc anchor for `step.doc` and choice `body` links. | +| `link(...)` | Same shape as `doc` but used inside synthesizer `check(...)` items. | +| `check(text, href)` | One numbered checklist item in synthesised output. | +| `dwJson({...})` | Builds a placeholder dw.json snippet with the right keys. | +| `ocapiConfig(client, [...])` | OCAPI Data API JSON for a list of features (`'codeVersions'`, `'jobs'`, `'sites'`, `'siteCartridges'`). | +| `scopes(...)` | Computes the OAuth scope list from named bundles. | + +**Validation:** +- `pnpm --filter @salesforce/b2c-dx-docs run docs:typecheck` — strict tsc. +- `pnpm --filter @salesforce/b2c-dx-docs run docs:build` — the build hook + walks every reachable choice combination, calls `synthesize`, and + validates every `step.doc` anchor, every checklist `link()`, every + markdown `[text](/path#anchor)` inside choice `body` strings, and every + link inside synthesizer `warnings`. + +### 4. API Reference (`docs/api/`) Purpose: Document the SDK programmatic API. @@ -367,6 +482,10 @@ b2c --flag value 1. Update `docs/cli/.md` with command documentation 2. Update `docs/.vitepress/config.mts` sidebar if new topic 3. Update `skills/b2c-cli/skills/b2c-/SKILL.md` with examples +4. **Evaluate Quickstart impact**: does this command warrant its own guide + under `docs/.vitepress/data/adventures/`, or extend an existing one + (e.g., a new `b2c sites …` subcommand may belong in the `cartridge-path` + guide)? See `PLAN_guides.md` for the prioritised backlog. ### When Adding an SDK Module @@ -380,17 +499,36 @@ b2c --flag value 1. Update affected examples in `docs/cli/*.md` 2. Update affected examples in `skills/b2c-cli/skills/*/SKILL.md` 3. Update guide pages if conceptual changes +4. **Sweep matching Quickstart guides**: every command name, flag, env var, + or anchor used in a guide's `synthesize` / choice `body` / warning lives + in `docs/.vitepress/data/adventures/.ts`. Run + `pnpm --filter @salesforce/b2c-dx-docs run docs:build` — the anchor + checker will flag broken doc links automatically; renamed commands / + flags need manual review. ### 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 +4. **Update Quickstart helpers if needed**: if the new option is a new + `dw.json` field, add a placeholder mapping to `dwJson()` in + `docs/.vitepress/data/adventures/_helpers.ts`. If it's a new OAuth + scope, add a `SCOPE_BUNDLES` entry. Then surface it in the relevant + guide(s). + +### When Renaming a Doc Heading + +1. The build-time anchor checker (`pnpm run docs:build`) will fail loudly + with the old slug — fix every `doc()` / `link()` / markdown + `[text](/path#anchor)` reference in + `docs/.vitepress/data/adventures/*.ts`. ## Navigation Structure **Top Navigation:** - Guide (`/guide/`) +- Quickstart (`/quickstart/`) - CLI Reference (`/cli/`) - API Reference (`/api/`) diff --git a/AGENTS.md b/AGENTS.md index 9ec50893f..eb49385f5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -93,8 +93,9 @@ The header is enforced by eslint via `eslint-plugin-header`. The canonical defin - Update docs in `./docs/` folder and relevant skills in `./skills/b2c-cli/skills/` when updating or adding CLI commands. - When adding new SDK modules, update `docs/typedoc.json` entry points to include the new module's barrel file so API docs are generated. +- **Quickstart guides** (`docs/quickstart/` + `docs/.vitepress/data/adventures/`) are interactive setup wizards. When you change auth, configuration, CLI commands, or doc anchors, sweep the relevant guide(s) — the build-time anchor checker (`pnpm --filter @salesforce/b2c-dx-docs run docs:build`) fails loudly on broken doc links, but renamed commands / flags need manual review. When adding a new CLI surface, evaluate whether a new guide is warranted (see `PLAN_guides.md` for the prioritised backlog). Internal terminology only: `Adventure` / `defineAdventure` lives in `.ts` files; user-visible UI says "Quickstart guide". -See [documentation skill](./.claude/skills/documentation/SKILL.md) for details on updating user guides, CLI reference, and API docs. +See [documentation skill](./.claude/skills/documentation/SKILL.md) for details on updating user guides, CLI reference, Quickstart guides, and API docs. ```bash # Run docs dev server (from project root) diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index d135332c9..6f1c145dd 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -22,6 +22,154 @@ function copyMarkdownSources(srcDir: string, outDir: string) { } } +// Validate that every Setup Adventure doc anchor resolves to a real heading +// in the corresponding source `.md` file. Called from `buildEnd`. +async function checkAdventureAnchors(srcDir: string) { + const {adventures, flags} = await import('./data/adventures/index.js'); + const slugify = (heading: string): string => { + const explicit = heading.match(/\{#([^}]+)\}\s*$/); + if (explicit) return explicit[1].trim(); + return heading + .toLowerCase() + .trim() + .replace(/[`*_~]/g, '') + .replace(/<[^>]+>/g, '') + .replace(/[^\p{L}\p{N}\s-]/gu, '') + .replace(/\s+/g, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, ''); + }; + const anchorsByFile = new Map>(); + const loadAnchors = (filePath: string) => { + const cached = anchorsByFile.get(filePath); + if (cached) return cached; + const out = new Set(); + if (!fs.existsSync(filePath)) { + anchorsByFile.set(filePath, out); + return out; + } + const lines = fs.readFileSync(filePath, 'utf8').split(/\r?\n/); + let inFence = false; + for (const line of lines) { + if (/^```/.test(line)) { + inFence = !inFence; + continue; + } + if (inFence) continue; + const m = line.match(/^#{1,6}\s+(.+?)\s*$/); + if (!m) continue; + out.add(slugify(m[1])); + } + anchorsByFile.set(filePath, out); + return out; + }; + const resolveDoc = (docPath: string) => { + const trimmed = docPath.replace(/\/$/, ''); + const candidates = [path.join(srcDir, `${trimmed}.md`), path.join(srcDir, trimmed, 'index.md')]; + for (const c of candidates) if (fs.existsSync(c)) return c; + return candidates[0]; + }; + const issues: string[] = []; + const checkAnchor = (advId: string, source: string, a: {hash?: string; label: string; path: string}) => { + if (/^https?:\/\//.test(a.path)) return; // external URLs aren't ours to validate + const file = resolveDoc(a.path); + if (!fs.existsSync(file)) { + issues.push(`[${advId}] ${source} → ${a.path} — source file not found`); + return; + } + if (!a.hash) return; + const anchors = loadAnchors(file); + if (!anchors.has(a.hash)) { + issues.push(`[${advId}] ${source} → ${a.path}#${a.hash} — anchor not found in ${path.relative(srcDir, file)}`); + } + }; + + // Walk every `[text](url)` link inside a free-form markdown string (used + // by Choice.body and synthesized warning entries). Internal absolute + // paths land in the same `checkAnchor` validator as docAnchors above. + const checkMarkdownLinks = (advId: string, source: string, md: string | undefined) => { + if (!md) return; + const linkRe = /\[[^\]]+\]\(([^)\s]+)\)/g; + let m: RegExpExecArray | null; + while ((m = linkRe.exec(md)) !== null) { + const url = m[1]; + if (!url.startsWith('/')) continue; // external / anchor-only — out of scope + const [pathPart, hashPart] = url.split('#'); + checkAnchor(advId, source, {path: pathPart, hash: hashPart, label: url}); + } + }; + type State = Record; + type Contrib = Record; + const mergeContrib = (accum: State, contrib: Contrib | undefined) => { + if (!contrib) return; + for (const [k, v] of Object.entries(contrib)) { + if (Array.isArray(v)) { + const prev = accum[k]; + const merged = Array.isArray(prev) ? [...prev, ...v] : [...v]; + accum[k] = Array.from(new Set(merged)); + } else { + accum[k] = v; + } + } + }; + const checkSynth = (advId: string, accum: State) => { + const r = adventures.find((a) => a.id === advId)!.synthesize(accum, flags); + for (const item of r.checklist) checkAnchor(advId, `checklist:${item.text}`, item.href); + if (r.warnings) { + for (const [i, w] of r.warnings.entries()) checkMarkdownLinks(advId, `warning[${i}]`, w); + } + }; + for (const adventure of adventures) { + for (const stepId of adventure.stepOrder) { + const step = adventure.steps[stepId]; + checkAnchor(adventure.id, `step:${stepId}`, step.docAnchor); + // Body markdown can carry links too — validate against an empty state + // (choice bodies don't depend on selection). + for (const c of step.choices({}, flags)) { + checkMarkdownLinks(adventure.id, `choice:${stepId}.${c.id}.body`, c.body); + } + } + const enumerate = (idx: number, accum: State) => { + const visible = adventure.stepOrder + .map((id) => adventure.steps[id]) + .filter((s) => !s.showIf || s.showIf(accum, flags)); + if (idx >= visible.length) { + checkSynth(adventure.id, accum); + return; + } + const step = visible[idx]; + const choices = step.choices(accum, flags).filter((c) => !c.featureFlag || flags[c.featureFlag]); + if (choices.length === 0) { + checkSynth(adventure.id, accum); + return; + } + if (step.multiSelect) { + // Cover representative subsets: each pick alone, plus all picks + // together. Sufficient for synthesizer branch coverage without + // exploding to 2^n combinations. + const subsets = [...choices.map((c) => [c]), choices]; + for (const subset of subsets) { + const next = {...accum}; + for (const c of subset) mergeContrib(next, c.contributes); + enumerate(idx + 1, next); + } + } else { + for (const c of choices) { + const next = {...accum}; + mergeContrib(next, c.contributes); + enumerate(idx + 1, next); + } + } + }; + enumerate(0, {}); + } + if (issues.length > 0) { + const msg = `Quickstart anchor check failed (${issues.length} issue${issues.length === 1 ? '' : 's'}):\n ${issues.join('\n ')}`; + throw new Error(msg); + } + console.log(`✓ All Quickstart anchors resolve (${adventures.length} guides checked).`); +} + // Extract the committed Salesforce Help corpus tarball (docs/help-content.tar.gz) // into /help so the converted .md pages are served verbatim at // /help//.md. The tarball is the committed artifact (one @@ -265,8 +413,9 @@ export default defineConfig({ // Ignore dead links in api-readme.md (links are valid after TypeDoc generates the API docs) ignoreDeadLinks: [/^\.\/clients\//], - buildEnd(siteConfig) { + async buildEnd(siteConfig) { copyMarkdownSources(siteConfig.srcDir, siteConfig.outDir); + await checkAdventureAnchors(siteConfig.srcDir); // Extract the Salesforce Help corpus straight into the build output (raw // .md served verbatim; fetched by `b2c docs read` via each entry's // sourceUrl). Done here — in buildEnd — because it only matters for the @@ -285,6 +434,11 @@ export default defineConfig({ }, vite: { + // Avoid the default 5173 — it collides with other VitePress / Vite + // dev servers commonly running on this machine. Override per-run with + // `VITEPRESS_PORT= pnpm run docs:dev`. (`--port` doesn't reach + // vitepress through the chained `docs:api && vitepress dev` script.) + server: {port: Number(process.env.VITEPRESS_PORT) || 5180, strictPort: false}, plugins: [ groupIconVitePlugin({ customIcon: { @@ -331,6 +485,7 @@ export default defineConfig({ }, nav: [ {text: 'Guides', link: '/guide/'}, + {text: 'Quickstart', link: '/quickstart/'}, {text: 'Agent Plugins', link: '/guide/agent-skills'}, {text: 'VS Code', link: '/vscode-extension/'}, {text: 'MCP', link: '/mcp/'}, diff --git a/docs/.vitepress/data/adventures/_authoring.ts b/docs/.vitepress/data/adventures/_authoring.ts new file mode 100644 index 000000000..36d40c13d --- /dev/null +++ b/docs/.vitepress/data/adventures/_authoring.ts @@ -0,0 +1,137 @@ +// Authoring helpers for adventure files. Use `defineAdventure` to declare +// the structure as plain data; use `step`, `choice`, `doc`, `md`, `check`, +// `link` to keep individual definitions concise. The renderer (Vue +// components) consumes the resulting Adventure objects directly. +// +// Why this exists: see PLAN_guides.md "Architecture decisions" — keeping the +// adventure as one structured TS object makes the data the single source of +// truth for the index card, presets, anchor validation, and the wizard +// renderer. No `.vue` parsing required. + +import type {Adventure, AdventureState, Choice, DocAnchor, Flags, Step, SynthesizedConfig} from './_types.js'; + +// --------------------------------------------------------------------------- +// defineAdventure — accepts an array-style steps definition (more readable +// than the legacy Record shape) and normalises into Adventure shape used by +// the renderer + checker. +// --------------------------------------------------------------------------- + +export interface AdventureInput { + id: string; + title: string; + tagline: string; + intro?: string; + icon?: string; + tags?: string[]; + priority?: Adventure['priority']; + // Structure as an ordered array; ids preserved. + steps: Step[]; + synthesize: (state: AdventureState, flags: Flags) => SynthesizedConfig; +} + +export function defineAdventure(input: AdventureInput): Adventure { + const stepOrder = input.steps.map((s) => s.id); + const stepRecord: Record = Object.fromEntries(input.steps.map((s) => [s.id, s])); + return { + id: input.id, + title: input.title, + tagline: input.tagline, + intro: input.intro, + icon: input.icon, + tags: input.tags, + priority: input.priority, + stepOrder, + steps: stepRecord, + synthesize: input.synthesize, + }; +} + +// --------------------------------------------------------------------------- +// step / choice — concise, typed factories. Each accepts the rest of the +// shape so authors don't repeat `id` twice. +// --------------------------------------------------------------------------- + +export interface StepInput { + title: string; + subtitle?: string; + doc: DocAnchor; + multiSelect?: boolean; + minPicks?: number; + maxPicks?: number; + showIf?: (state: AdventureState, flags: Flags) => boolean; + // Either a static array (most common) or a function-of-state (for steps + // whose choice list depends on prior picks). The renderer accepts a + // function via `Step.choices`, so we wrap the array form. + choices: Choice[] | ((state: AdventureState, flags: Flags) => Choice[]); +} + +export function step(id: string, input: StepInput): Step { + const choices = typeof input.choices === 'function' ? input.choices : () => input.choices as Choice[]; + return { + id, + title: input.title, + subtitle: input.subtitle, + multiSelect: input.multiSelect, + minPicks: input.minPicks, + maxPicks: input.maxPicks, + showIf: input.showIf, + docAnchor: input.doc, + choices, + }; +} + +export type ChoiceInput = Omit; + +export function choice(id: string, input: ChoiceInput): Choice { + return {id, ...input}; +} + +// --------------------------------------------------------------------------- +// doc — produces a DocAnchor with sensible defaults. `link` (in _helpers.ts) +// is the older spelling; this is its rename for readability in adventure +// files. Both produce the same shape. +// --------------------------------------------------------------------------- + +export function doc(path: string, hash?: string, label?: string): DocAnchor { + return {path, hash, label: label ?? deriveLabel(path, hash)}; +} + +function deriveLabel(path: string, hash?: string): string { + const last = path.split('/').filter(Boolean).pop() ?? path; + const titled = last + .split('-') + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(' '); + return hash ? `${titled} · ${hash}` : titled; +} + +// --------------------------------------------------------------------------- +// md — tagged template literal for multi-line markdown content. Pure string +// concatenation today; the tag exists for IDE highlighting and so we can +// later enforce or transform markdown bodies at build time without churning +// every adventure file. +// --------------------------------------------------------------------------- + +export function md(strings: TemplateStringsArray, ...values: unknown[]): string { + let out = strings[0]; + for (let i = 0; i < values.length; i++) { + out += String(values[i]) + strings[i + 1]; + } + return dedent(out).trim(); +} + +// Strip the leading whitespace that template literals carry over from +// indented source code. Finds the smallest non-zero indent across non-empty +// lines and removes it from every line. +function dedent(s: string): string { + const lines = s.split('\n'); + let min = Infinity; + for (const line of lines) { + if (!line.trim()) continue; + const m = line.match(/^[ \t]*/); + const indent = m ? m[0].length : 0; + if (indent < min) min = indent; + } + if (min === Infinity || min === 0) return s; + return lines.map((l) => l.slice(min)).join('\n'); +} diff --git a/docs/.vitepress/data/adventures/_helpers.ts b/docs/.vitepress/data/adventures/_helpers.ts new file mode 100644 index 000000000..d947074db --- /dev/null +++ b/docs/.vitepress/data/adventures/_helpers.ts @@ -0,0 +1,156 @@ +// Builders shared by adventure synthesizers. Compose these instead of +// hand-writing JSON or anchor strings so the output stays consistent and the +// anchor checker can validate every link. + +import type {ChecklistItem, DocAnchor} from './_types.js'; + +export interface DwJsonInput { + hostname?: boolean | string; // true => placeholder + username?: boolean | string; + password?: boolean | string; + clientId?: boolean | string; + clientSecret?: boolean | string; + codeVersion?: boolean | string; + shortCode?: boolean | string; + tenantId?: boolean | string; + realm?: boolean | string; + contentLibrary?: boolean | string; + libraries?: string[]; + comment?: string; +} + +const PLACEHOLDERS: Record, string> = { + hostname: '.dx.commercecloud.salesforce.com', + username: '', + password: '', + clientId: '', + clientSecret: '', + codeVersion: 'version1', + shortCode: '', + tenantId: '', + realm: '', + contentLibrary: '', +}; + +// Order matters: render fields in a stable, scannable order. +const FIELD_ORDER: (keyof typeof PLACEHOLDERS)[] = [ + 'hostname', + 'codeVersion', + 'username', + 'password', + 'clientId', + 'clientSecret', + 'shortCode', + 'tenantId', + 'realm', + 'contentLibrary', +]; + +const KEY_FOR: Record = { + hostname: 'hostname', + username: 'username', + password: 'password', + clientId: 'client-id', + clientSecret: 'client-secret', + codeVersion: 'code-version', + shortCode: 'short-code', + tenantId: 'tenant-id', + realm: 'realm', + contentLibrary: 'content-library', +}; + +export function dwJson(input: DwJsonInput): string { + const lines: string[] = ['{']; + const entries: string[] = []; + for (const field of FIELD_ORDER) { + const value = input[field]; + if (value === undefined || value === false) continue; + const rendered = value === true ? PLACEHOLDERS[field] : (value as string); + entries.push(` "${KEY_FOR[field]}": "${rendered}"`); + } + if (input.libraries && input.libraries.length > 0) { + const arr = input.libraries.map((id) => `"${id}"`).join(', '); + entries.push(` "libraries": [${arr}]`); + } + lines.push(entries.join(',\n')); + lines.push('}'); + return lines.join('\n'); +} + +export function envFile(vars: Record): string { + return Object.entries(vars) + .map(([k, v]) => `${k}=${v}`) + .join('\n'); +} + +export function link(path: string, hash: string | undefined, label: string): DocAnchor { + return {path, hash, label}; +} + +export function check(text: string, href: DocAnchor): ChecklistItem { + return {text, href}; +} + +// OCAPI Data API resource snippets, keyed by feature. Authors compose these +// into a single Business-Manager-ready JSON in synthesizers. +interface OcapiResource { + resource_id: string; + methods: string[]; +} + +export const OCAPI_RESOURCES: Record = { + codeVersions: [ + {resource_id: '/code_versions', methods: ['get']}, + {resource_id: '/code_versions/*', methods: ['get', 'put', 'patch', 'delete']}, + ], + jobs: [ + {resource_id: '/jobs/*/executions', methods: ['post']}, + {resource_id: '/jobs/*/executions/*', methods: ['get']}, + {resource_id: '/job_execution_search', methods: ['post']}, + ], + sites: [ + {resource_id: '/sites', methods: ['get']}, + {resource_id: '/sites/*', methods: ['get']}, + ], + // Cartridge-path mutations on a site (used by `b2c sites cartridges + // add/remove/set`). Source: docs/cli/sites.md "Required OCAPI Permissions". + siteCartridges: [{resource_id: '/sites/*/cartridges', methods: ['post', 'put', 'delete']}], +}; + +export type OcapiFeature = 'codeVersions' | 'jobs' | 'siteCartridges' | 'sites'; + +export function ocapiConfig(clientId: string, features: OcapiFeature[]): string { + const resources: OcapiResource[] = features.flatMap((f) => OCAPI_RESOURCES[f] ?? []); + const body = { + _v: '24.5', + clients: [ + { + client_id: clientId, + resources: resources.map((r) => ({ + ...r, + read_attributes: '(**)', + write_attributes: '(**)', + })), + }, + ], + }; + return JSON.stringify(body, null, 2); +} + +// Centralised list of OAuth scopes by purpose. Synthesizers pick from these. +export const SCOPE_BUNDLES = { + baseline: ['mail', 'roles', 'tenantFilter', 'openid'], + scapiSchemas: ['sfcc.scapi-schemas'], + scapiCustomApis: ['sfcc.custom-apis'], + ecdnRead: ['sfcc.cdn-zones'], + ecdnWrite: ['sfcc.cdn-zones.rw'], + // Required by `b2c scapi replications` — the SDK client requests this + // explicitly. Source: packages/b2c-tooling-sdk/src/clients/granular-replications.ts + replicationsRw: ['sfcc.granular-replications.rw'], +} as const; + +export function scopes(...bundles: (keyof typeof SCOPE_BUNDLES)[]): string { + const out = new Set(); + for (const b of bundles) for (const s of SCOPE_BUNDLES[b]) out.add(s); + return Array.from(out).join(' '); +} diff --git a/docs/.vitepress/data/adventures/_types.ts b/docs/.vitepress/data/adventures/_types.ts new file mode 100644 index 000000000..6b7586066 --- /dev/null +++ b/docs/.vitepress/data/adventures/_types.ts @@ -0,0 +1,133 @@ +// Types for the Setup Adventure wizard. Author one adventure per file under +// this directory and register it in `index.ts`. + +export type ApiSurface = 'ocapi' | 'scapi' | 'both'; + +export type BadgeTone = 'beta' | 'quick' | 'complex' | 'info'; + +export interface Badge { + text: string; + tone?: BadgeTone; +} + +// Flat record of accumulated picks. Keys are arbitrary strings owned by each +// adventure (e.g., 'authMethod', 'instanceType', 'ide'). Multi-select steps +// produce string[] values for the keys their choices contribute to. +export type AdventureState = Record; + +export type Flags = Record; + +export interface DocAnchor { + // Either an internal docs path without `.md` (e.g., '/guide/authentication') + // or a fully-qualified external URL (e.g., 'https://docs.claude.com/...'). + // External URLs are passed through verbatim and skipped by the build-time + // anchor checker. + path: string; + // Heading slug (without `#`), e.g., 'webdav-access'. Optional means link to top. + // Ignored for external URLs (put any fragment directly in `path`). + hash?: string; + // Human-readable label shown in the "Learn more" link + label: string; +} + +export interface ChecklistItem { + text: string; + // Same shape as DocAnchor; resolved to an internal link with VitePress base path + href: DocAnchor; +} + +export interface SynthesizedConfig { + // dw.json snippet with tokens. Will be rendered as a code block. + dwJson: string; + // Optional `.env` tab content (also placeholder-friendly). + env?: string; + // Numbered checklist of steps that link back into existing prose docs. + checklist: ChecklistItem[]; + // Free-form contextual warnings. Rendered as a styled callout. + warnings?: string[]; + // The single command that proves setup works (e.g., 'b2c code list'). + verifyCommand: string; +} + +export interface Choice { + id: string; + title: string; + // Vendor or category subtitle (e.g., 'Anthropic', 'Quick') + subtitle?: string; + // Plain-text description (legacy; escaped on render). Prefer `body` for + // new authoring so links and inline code render. + description?: string; + // Markdown body — rendered to HTML at display time. Supports inline + // links (with VitePress base-path resolution), bold, italic, lists, and + // inline/fenced code. Use the `md` template tag for multi-line authoring. + body?: string; + // Iconify name (reuse the project's group-icons set when possible) + icon?: string; + badges?: Badge[]; + // Picks contribute these key/values into AdventureState + contributes?: AdventureState; + // Step id to advance to. `null` = terminal (show output). Omit = next step in `Adventure.stepOrder`. + next?: null | string; + // If set, this choice is hidden unless `flags[featureFlag]` is true. + featureFlag?: string; + // Tag the choice with the API surface it implies. `synthesize` reads this + // alongside the `scapi-migration` flag to pick scopes/permissions. + apiSurface?: ApiSurface; +} + +export interface Step { + id: string; + title: string; + subtitle?: string; + // Conditionally show this step based on accumulated state. + showIf?: (state: AdventureState, flags: Flags) => boolean; + // Anchor in existing prose docs that explains this step in detail. + docAnchor: DocAnchor; + // Function-of-state so a step's choices can branch on prior picks. + choices: (state: AdventureState, flags: Flags) => Choice[]; + // When true, the user can pick multiple choices and confirm. Choice + // contributions for *array-valued* keys are merged across picks (deduped), + // so downstream steps and synthesizers can branch on the full set. + // Optional `minPicks` (default 1) and `maxPicks` (default unlimited) bound + // the selection. + multiSelect?: boolean; + minPicks?: number; + maxPicks?: number; +} + +export type AdventurePriority = 'core' | 'common' | 'specialized' | 'niche'; + +export interface Adventure { + id: string; + title: string; + tagline: string; + // Optional short blurb shown above step 1. + intro?: string; + badges?: Badge[]; + // Iconify name shown next to title and on the index card. + icon?: string; + // Tags surface on the index card and feed the search/filter UI. + tags?: string[]; + // Bucket on the index page (Core / Common / Specialized / Niche). + priority?: AdventurePriority; + // Linear default order; branching is handled per-choice via `next`. + stepOrder: string[]; + steps: Record; + synthesize: (state: AdventureState, flags: Flags) => SynthesizedConfig; +} + +export interface QuickStart { + id: string; + label: string; + description?: string; + badges?: Badge[]; + adventureId: string; + // Pre-applied state. Wizard advances all steps that fully match these picks. + preselect: AdventureState; +} + +export interface AdventureRegistry { + flags: Flags; + adventures: Adventure[]; + quickStarts: QuickStart[]; +} diff --git a/docs/.vitepress/data/adventures/account-manager.ts b/docs/.vitepress/data/adventures/account-manager.ts new file mode 100644 index 000000000..9f0475f64 --- /dev/null +++ b/docs/.vitepress/data/adventures/account-manager.ts @@ -0,0 +1,338 @@ +// Adventure: Manage Account Manager, BM roles & users (b2c am + b2c bm). +// +// Covers both `b2c am` (Account Manager: org users, API clients, orgs, roles) +// and `b2c bm` (Business Manager admin: roles, users, access keys, whoami) +// since both are admin-flavored and overlap on workflows like "manage who +// can access this instance". + +import {choice, defineAdventure, doc, md, step} from './_authoring.js'; +import {check, dwJson, link, scopes} from './_helpers.js'; +import type {AdventureState} from './_types.js'; + +// Hand-rolled OCAPI Data API snippet for BM administration resources. +// The shared `ocapiConfig()` helper only covers code-versions/jobs/sites, +// so we inline the BM admin resources here. +function bmAdminOcapi(clientId: string): string { + const resources = [ + {resource_id: '/roles', methods: ['get']}, + {resource_id: '/roles/*', methods: ['get', 'put', 'delete']}, + {resource_id: '/roles/*/users', methods: ['get']}, + {resource_id: '/roles/*/users/*', methods: ['put', 'delete']}, + {resource_id: '/roles/*/permissions', methods: ['get', 'put']}, + {resource_id: '/users', methods: ['get']}, + {resource_id: '/users/*', methods: ['get', 'patch', 'delete']}, + {resource_id: '/users/this', methods: ['get']}, + {resource_id: '/users/*/access_key/*', methods: ['get', 'put', 'patch', 'delete']}, + {resource_id: '/user_search', methods: ['post']}, + ]; + return JSON.stringify( + { + _v: '24.5', + clients: [ + { + client_id: clientId, + resources: resources.map((r) => ({...r, read_attributes: '(**)', write_attributes: '(**)'})), + }, + ], + }, + null, + 2, + ); +} + +export const accountManagerAdventure = defineAdventure({ + id: 'account-manager', + title: 'Manage Account Manager, BM roles & users', + tagline: + 'Configure access to Account Manager (org users, API clients) and Business Manager admin (roles, permissions, access keys).', + icon: 'mdi:account-cog-outline', + tags: ['account-manager', 'api-clients', 'roles', 'users', 'admin', 'bm'], + priority: 'common', + intro: + 'Account Manager covers cross-instance identity (org users, API clients, orgs). Business Manager admin covers per-instance roles, users, and access keys via the OCAPI Data API. Pick the target you need — some commands are user-auth only, and `bm access-key` writes need an extra BM functional permission.', + + steps: [ + step('target', { + title: 'What do you want to manage?', + doc: doc('/guide/authentication', 'overview', 'Authentication overview'), + choices: [ + choice('am', { + title: 'Account Manager only', + subtitle: 'Org users, API clients, orgs', + icon: 'mdi:office-building-cog-outline', + body: md`Cross-instance identity: \`b2c am users\`, \`b2c am roles\`, \`b2c am clients\`, \`b2c am orgs\`.`, + contributes: {target: 'am'}, + }), + choice('bm', { + title: 'Business Manager admin only', + subtitle: 'Per-instance roles, users, access keys', + icon: 'mdi:shield-account-outline', + body: md`Instance-scoped admin via OCAPI Data API: \`b2c bm roles\`, \`b2c bm users\`, \`b2c bm access-key\`, \`b2c bm whoami\`.`, + contributes: {target: 'bm'}, + }), + choice('both', { + title: 'Both', + subtitle: 'AM + BM admin', + icon: 'mdi:account-cog-outline', + badges: [{text: 'Recommended', tone: 'info'}], + body: md`Set up credentials that work across Account Manager and Business Manager admin commands.`, + contributes: {target: 'both'}, + }), + ], + }), + + step('auth', { + title: 'How will you authenticate?', + doc: doc('/guide/authentication', 'authentication-methods', 'Authentication Methods'), + choices: [ + choice('user-auth', { + title: 'User Auth (Browser)', + subtitle: 'Implicit / b2c auth login', + icon: 'mdi:account-arrow-right-outline', + badges: [{text: 'Recommended for AM', tone: 'info'}], + body: md`Required for \`am clients create/update/delete\`, \`am orgs\`, \`bm whoami\`, and \`bm access-key\`. Uses your real user roles and BM functional permissions.`, + contributes: {authMethod: 'implicit'}, + }), + choice('client-credentials', { + title: 'Client Credentials', + subtitle: 'For automation / CI/CD', + icon: 'mdi:key-variant', + badges: [{text: 'CI', tone: 'quick'}], + body: md`Service-account token. Works for read-only AM (\`users list\`, \`roles list\`) and most BM \`roles\`/\`users\` commands once OCAPI is configured.`, + contributes: {authMethod: 'client-credentials'}, + }), + choice('jwt', { + title: 'JWT Bearer', + subtitle: 'Certificate-based', + icon: 'mdi:certificate-outline', + body: md`Cert pair instead of a client secret. See [JWT setup](/guide/authentication#jwt-authentication-certificate-based).`, + contributes: {authMethod: 'jwt'}, + }), + ], + }), + + step('ocapi', { + title: 'Configure OCAPI for BM admin?', + subtitle: 'Required for any b2c bm command other than user-auth-only ones.', + showIf: (state: AdventureState) => state.target === 'bm' || state.target === 'both', + doc: doc('/guide/authentication', 'minimal-configuration-by-feature', 'Minimal Configuration by Feature'), + choices: [ + choice('yes', { + title: 'Yes — auto-config snippet', + subtitle: 'roles, users, access_key, user_search', + icon: 'mdi:check-circle-outline', + badges: [{text: 'Recommended', tone: 'info'}], + body: md`Emits a JSON block to paste into Business Manager → Site Development → Open Commerce API Settings → Data API.`, + contributes: {needsOcapi: true}, + }), + choice('no', { + title: 'Skip — already configured', + subtitle: 'Or only using bm whoami / access-key', + icon: 'mdi:skip-next-outline', + body: md`\`bm whoami\` and \`bm access-key\` use user-auth and don't need OCAPI Data API entries beyond \`/users/this\` and \`/users/*/access_key/*\`.`, + contributes: {needsOcapi: false}, + }), + ], + }), + + step('persistence', { + title: 'How should the CLI find your config?', + doc: doc('/guide/configuration', 'configuration-file', 'Configuration file (dw.json)'), + choices: [ + choice('dw-json', { + title: 'dw.json (project root)', + subtitle: 'Recommended', + icon: 'mdi:file-cog-outline', + body: md`Per-project config file. Walk-up discovery from the current directory.`, + contributes: {persistence: 'dw-json'}, + }), + choice('env', { + title: '.env / environment variables', + subtitle: 'CI-friendly', + icon: 'mdi:console-line', + body: md`Use \`SFCC_*\` environment variables — the CLI auto-loads \`.env\` files.`, + contributes: {persistence: 'env'}, + }), + choice('stateful', { + title: 'Stateful (b2c auth login)', + subtitle: 'One browser login, reused', + icon: 'mdi:lock-open-check-outline', + body: md`Best for interactive use — sign in once and reuse the session across \`am\` and \`bm\` commands until it expires.`, + contributes: {persistence: 'stateful'}, + }), + ], + }), + ], + + synthesize(state) { + const target = (state.target as string) ?? 'both'; + const includesAm = target === 'am' || target === 'both'; + const includesBm = target === 'bm' || target === 'both'; + + const isJwt = state.authMethod === 'jwt'; + const isImplicit = state.authMethod === 'implicit'; + const isClientCreds = state.authMethod === 'client-credentials'; + + const persistence = (state.persistence as string) ?? 'dw-json'; + const useEnv = persistence === 'env'; + const useStateful = persistence === 'stateful'; + + const needsBmHost = includesBm; // BM commands target a specific Commerce instance + const needsClientSecret = isClientCreds; + const needsClientId = isClientCreds || isJwt; + + // Configure OCAPI? Only meaningful when BM admin is in scope. + const needsOcapi = includesBm && state.needsOcapi === true; + + // dw.json / env synthesis ------------------------------------------------ + // Edge case: AM-only + implicit + dw-json has nothing to write — emit a + // comment-only note so users don't paste an empty `{}`. + const noDwJsonNeeded = !needsBmHost && !needsClientId && !needsClientSecret; + const dw = useStateful + ? '// Stateful auth (b2c auth login) — no dw.json required for one-shot AM commands.\n// For BM commands, set the instance hostname (and optional client) via dw.json or flags.' + : useEnv + ? '# Using environment variables — see .env tab below.' + : noDwJsonNeeded + ? '// No dw.json required — AM commands authenticate via the built-in public client (browser).' + : dwJson({ + hostname: needsBmHost, + clientId: needsClientId, + clientSecret: needsClientSecret, + }); + + const envLines = useEnv + ? [ + needsBmHost ? 'SFCC_SERVER=.dx.commercecloud.salesforce.com' : '', + needsClientId ? 'SFCC_CLIENT_ID=' : '', + needsClientSecret ? 'SFCC_CLIENT_SECRET=' : '', + isJwt ? 'SFCC_JWT_CERT=./cert.pem' : '', + isJwt ? 'SFCC_JWT_KEY=./key.pem' : '', + ] + .filter(Boolean) + .join('\n') + : undefined; + + // Checklist -------------------------------------------------------------- + const checklist = [ + // Auth setup + ...(isImplicit + ? [ + check( + 'Sign in with `b2c auth login`', + link('/guide/authentication', 'authentication-methods', 'Authentication Methods'), + ), + ] + : [ + check( + 'Create an Account Manager API client', + link('/guide/authentication', 'creating-an-api-client', 'Creating an API Client'), + ), + ]), + + // Roles - depends on target + ...(includesAm + ? [ + check( + isImplicit + ? 'Assign Account Administrator (or User Administrator) to your AM user' + : 'Assign User Administrator to the API client (read-only AM users/roles)', + link( + '/guide/authentication', + 'understanding-roles-and-tenant-filters', + 'Understanding Roles and Tenant Filters', + ), + ), + ] + : []), + ...(includesBm && !isImplicit + ? [ + check( + `Add Default Scopes: ${scopes('baseline')}`, + link('/guide/authentication', 'configuring-scopes', 'Configuring Scopes'), + ), + check( + 'Add a tenant filter on the API client roles', + link('/guide/authentication', 'configuring-tenant-filter', 'Configuring Tenant Filter'), + ), + ] + : []), + + // OCAPI - only for BM admin + ...(needsOcapi + ? [ + check( + 'Enable BM administration resources in OCAPI Data API', + link('/guide/authentication', 'minimal-configuration-by-feature', 'Minimal Configuration by Feature'), + ), + ] + : []), + + // Persistence + ...(useStateful + ? [ + check( + 'Run `b2c auth login` to start a stateful session', + link('/guide/configuration', 'configuration-file', 'Configuration File'), + ), + ] + : [ + check( + useEnv ? 'Set SFCC_* environment variables' : 'Save the dw.json snippet to your project root', + link( + '/guide/configuration', + useEnv ? 'environment-variables' : 'configuration-file', + useEnv ? 'Environment Variables' : 'Configuration File', + ), + ), + ]), + ]; + + // Warnings --------------------------------------------------------------- + const warnings: string[] = []; + + if (needsOcapi) { + warnings.push( + `Paste this BM administration OCAPI Data API config into Business Manager (Site Development → Open Commerce API Settings → Data API):\n\n\`\`\`json\n${bmAdminOcapi('')}\n\`\`\``, + ); + } + + if (includesAm && (isClientCreds || isJwt)) { + warnings.push( + 'AM API client management (`am clients create/update/delete`) and `am orgs` are user-auth only — run `b2c auth login` first or pass `--user-auth`. Client-credentials work for read-only `am users list/get` and `am roles list/get` only.', + ); + } + + if (includesBm && !isImplicit) { + warnings.push( + '`b2c bm whoami` and `b2c bm access-key …` need a token that resolves to a real BM user, so they default to browser-based user-auth. You can override with `--auth-methods client-credentials` only if your service client is configured to issue user-bearing tokens.', + ); + } + if (includesBm) { + warnings.push( + 'Access-key writes (`bm access-key create/set/delete`) additionally require the `Manage_Users_Access_Keys` BM functional permission on your user account.', + ); + } + + if (isImplicit && !useStateful) { + warnings.push( + 'User auth opens a browser per session — fine for development. Use `b2c auth login` (stateful) to reuse a single session across commands.', + ); + } + + // Verify command -------------------------------------------------------- + // Pick the smallest command that proves the chosen target works. + const verifyCommand = includesBm + ? isImplicit + ? 'b2c bm whoami' + : 'b2c bm roles list' + : 'b2c am users list'; + + return { + dwJson: dw, + env: envLines, + checklist, + warnings, + verifyCommand, + }; + }, +}); diff --git a/docs/.vitepress/data/adventures/agent-mcp.ts b/docs/.vitepress/data/adventures/agent-mcp.ts new file mode 100644 index 000000000..918bf4c44 --- /dev/null +++ b/docs/.vitepress/data/adventures/agent-mcp.ts @@ -0,0 +1,320 @@ +// Adventure: Set up an AI coding agent (MCP server + agent skills). + +import {choice, defineAdventure, doc, md, step} from './_authoring.js'; +import {check, link} from './_helpers.js'; +import type {AdventureState} from './_types.js'; + +function ideAnchor(ide: string): {hash: string; label: string} { + switch (ide) { + case 'claude-code': + return {hash: 'claude-code', label: 'Claude Code'}; + case 'cursor': + return {hash: 'other-ides', label: 'Other IDEs'}; + case 'copilot-vscode': + case 'copilot-cli': + return {hash: 'copilot', label: 'Copilot'}; + case 'codex': + return {hash: 'codex', label: 'Codex'}; + case 'agentforce-vibes': + return {hash: 'agentforce-vibes', label: 'Agentforce Vibes'}; + default: + return {hash: 'quick-start', label: 'Quick Start'}; + } +} + +function vendorSkillsDoc(ide: string): {label: string; url: string} | null { + switch (ide) { + case 'claude-code': + return {label: 'Claude Code plugins (vendor docs)', url: 'https://docs.claude.com/en/docs/claude-code/plugins'}; + case 'cursor': + return {label: 'Cursor skills (vendor docs)', url: 'https://cursor.com/docs/context/skills'}; + case 'copilot-vscode': + return { + label: 'VS Code agent skills (vendor docs)', + url: 'https://code.visualstudio.com/docs/copilot/customization/agent-skills', + }; + case 'copilot-cli': + return {label: 'GitHub Copilot CLI (vendor docs)', url: 'https://github.com/github/copilot-cli'}; + case 'codex': + return {label: 'Codex CLI (vendor docs)', url: 'https://github.com/openai/codex'}; + case 'agentforce-vibes': + return { + label: 'Agentforce Vibes skills (vendor docs)', + url: 'https://developer.salesforce.com/docs/platform/einstein-for-devs/guide/skills.html', + }; + default: + return null; + } +} + +function vendorMcpDoc(ide: string): {label: string; url: string} | null { + switch (ide) { + case 'claude-code': + return {label: 'Claude Code MCP (vendor docs)', url: 'https://docs.claude.com/en/docs/claude-code/mcp'}; + case 'cursor': + return { + label: 'Cursor MCP configuration (vendor docs)', + url: 'https://cursor.com/docs/context/mcp#configuration-locations', + }; + case 'copilot-vscode': + return { + label: 'VS Code MCP servers (vendor docs)', + url: 'https://code.visualstudio.com/docs/copilot/customization/mcp-servers', + }; + default: + return null; + } +} + +function mcpAnchor(ide: string): {hash: string; label: string} { + switch (ide) { + case 'claude-code': + return {hash: 'claude-code', label: 'MCP for Claude Code'}; + case 'cursor': + return {hash: 'cursor', label: 'MCP for Cursor'}; + case 'copilot-vscode': + return {hash: 'github-copilot', label: 'MCP for GitHub Copilot'}; + default: + return {hash: 'after-installation', label: 'MCP After Installation'}; + } +} + +export const agentMcpAdventure = defineAdventure({ + id: 'agent-mcp', + title: 'Set up an AI coding agent', + tagline: 'Install B2C agent skills and the MCP server in your IDE.', + icon: 'mdi:robot-outline', + tags: ['ai', 'mcp', 'skills', 'ci-cd', 'vscode'], + priority: 'core', + intro: + 'Pair the B2C agent skills with the MCP server so your coding agent can both understand the platform and call CLI/MCP tools. Pick your IDE first; everything else flows from that.', + + steps: [ + step('ide', { + title: 'Which IDE / agent?', + doc: doc('/guide/agent-skills', 'quick-start', 'Agent Skills Quick Start'), + choices: [ + choice('claude-code', { + title: 'Claude Code', + subtitle: 'Anthropic', + icon: 'logos:claude-icon', + badges: [{text: 'Recommended', tone: 'info'}], + body: md`Marketplace install for skills + MCP via the official plugin.`, + contributes: {ide: 'claude-code'}, + }), + choice('cursor', { + title: 'Cursor', + subtitle: 'Anysphere', + icon: 'mdi:cursor-default-outline', + body: md`Skills via \`b2c setup skills\`; MCP via project-level \`.cursor/mcp.json\`.`, + contributes: {ide: 'cursor'}, + }), + choice('copilot-vscode', { + title: 'GitHub Copilot (VS Code)', + subtitle: 'GitHub', + icon: 'logos:visual-studio-code', + body: md`Skills via Command Palette (Chat: Install Plugin from Source); MCP via \`.vscode/mcp.json\`.`, + contributes: {ide: 'copilot-vscode'}, + }), + choice('copilot-cli', { + title: 'Copilot CLI', + subtitle: 'GitHub', + icon: 'logos:github-copilot', + body: md`Marketplace install via the \`copilot\` CLI.`, + contributes: {ide: 'copilot-cli'}, + }), + choice('codex', { + title: 'Codex', + subtitle: 'OpenAI', + icon: 'simple-icons:openai', + body: md`Marketplace install via the \`codex\` CLI; MCP not yet available.`, + contributes: {ide: 'codex'}, + }), + choice('agentforce-vibes', { + title: 'Agentforce Vibes', + subtitle: 'Salesforce', + icon: 'mdi:flash-outline', + body: md`Skills via \`b2c setup skills\`; MCP via direct add.`, + contributes: {ide: 'agentforce-vibes'}, + }), + ], + }), + + step('skills', { + title: 'Which skill packs?', + subtitle: 'Pick one or more — each pack covers a different layer of the stack.', + multiSelect: true, + minPicks: 1, + doc: doc('/guide/agent-skills', 'available-plugins', 'Available Plugins'), + choices: [ + choice('b2c-cli', { + title: 'b2c-cli', + subtitle: 'CLI commands', + icon: 'mdi:console', + body: md`Code deploy, jobs, ODS, WebDAV, site archives.`, + contributes: {skills: ['b2c-cli']}, + }), + choice('b2c', { + title: 'b2c', + subtitle: 'Platform patterns', + icon: 'mdi:salesforce', + body: md`Controllers, ISML, hooks, Page Designer, Custom APIs, services.`, + contributes: {skills: ['b2c']}, + }), + choice('storefront-next', { + title: 'storefront-next', + subtitle: 'Storefront Next', + icon: 'mdi:rocket-outline', + body: md`PWA scaffolding, routing, auth, deployment to MRT.`, + contributes: {skills: ['storefront-next']}, + }), + ], + }), + + step('mcp', { + title: 'Install the MCP server?', + subtitle: 'MCP gives your agent direct tool calls in addition to skills.', + doc: doc('/mcp/installation', undefined, 'MCP Installation'), + showIf: (state: AdventureState) => state.ide !== 'codex', + choices: [ + choice('yes', { + title: 'Yes — install MCP', + subtitle: 'Recommended', + icon: 'mdi:check-circle-outline', + badges: [{text: 'Recommended', tone: 'info'}], + body: md`Adds direct tool-calls (deploy, watch, schemas, MRT) on top of skills.`, + contributes: {includeMcp: true}, + }), + choice('no', { + title: 'No — skills only', + icon: 'mdi:close-circle-outline', + body: md`Skip MCP for now; install later from the same marketplace.`, + contributes: {includeMcp: false}, + }), + ], + }), + + step('toolsets', { + title: 'Which MCP toolsets?', + subtitle: 'Pre-select the tools your project actually needs.', + doc: doc('/mcp/configuration', 'toolset-selection', 'Toolset Selection'), + showIf: (state: AdventureState) => state.includeMcp === true, + choices: [ + choice('all', { + title: 'All toolsets', + subtitle: 'Everything', + icon: 'mdi:toolbox', + badges: [{text: 'Quick', tone: 'quick'}], + body: md`CARTRIDGES, SCAPI, MRT, PWAV3, STOREFRONTNEXT.`, + contributes: {toolsets: 'all'}, + }), + choice('cartridges-scapi', { + title: 'Cartridges + SCAPI', + subtitle: 'Most common', + icon: 'mdi:package-variant', + body: md`For SFRA-based projects with Custom APIs.`, + contributes: {toolsets: 'CARTRIDGES,SCAPI'}, + }), + choice('storefront-next', { + title: 'Storefront Next + MRT', + subtitle: 'Headless', + icon: 'mdi:rocket-launch-outline', + body: md`For Storefront Next projects deploying to Managed Runtime.`, + contributes: {toolsets: 'STOREFRONTNEXT,MRT'}, + }), + ], + }), + ], + + synthesize(state) { + const ide = String(state.ide ?? 'claude-code'); + const skillsList = Array.isArray(state.skills) ? state.skills : ['b2c-cli', 'b2c', 'storefront-next']; + const includeMcp = state.includeMcp === true; + const toolsets = String(state.toolsets ?? 'all'); + + let installBlock = ''; + if (ide === 'claude-code') { + const lines = ['claude plugin marketplace add SalesforceCommerceCloud/b2c-developer-tooling']; + for (const s of skillsList) lines.push(`claude plugin install ${s}`); + if (includeMcp) lines.push('claude plugin install b2c-dx-mcp --scope project'); + installBlock = lines.join('\n'); + } else if (ide === 'copilot-cli') { + const lines = ['copilot plugin marketplace add SalesforceCommerceCloud/b2c-developer-tooling']; + for (const s of skillsList) lines.push(`copilot plugin install ${s}@b2c-developer-tooling`); + installBlock = lines.join('\n'); + } else if (ide === 'codex') { + installBlock = + '# In a terminal:\ncodex plugin marketplace add SalesforceCommerceCloud/b2c-developer-tooling\n# Then in Codex:\n# /plugins → select "B2C Developer Tooling" → install desired plugins'; + } else if (ide === 'cursor') { + installBlock = skillsList.map((s) => `npx @salesforce/b2c-cli setup skills ${s} --ide cursor`).join('\n'); + } else if (ide === 'copilot-vscode') { + installBlock = [ + '# In VS Code, open the Command Palette (Cmd/Ctrl+Shift+P) and run:', + '# Chat: Install Plugin from Source', + '# Then enter:', + '# SalesforceCommerceCloud/b2c-developer-tooling', + '# To update later: Extensions view → ··· menu → Check for Extension Updates', + ].join('\n'); + } else if (ide === 'agentforce-vibes') { + installBlock = skillsList + .map((s) => `npx @salesforce/b2c-cli setup skills ${s} --ide agentforce-vibes`) + .join('\n'); + } + + let mcpConfig = ''; + if (includeMcp && (ide === 'cursor' || ide === 'copilot-vscode')) { + const filePath = ide === 'cursor' ? '.cursor/mcp.json' : '.vscode/mcp.json'; + const topKey = ide === 'cursor' ? 'mcpServers' : 'servers'; + const extra = ide === 'cursor' ? '' : '"type": "stdio",\n '; + mcpConfig = `Add the following to \`${filePath}\` in your project root:\n\n\`\`\`json\n{\n "${topKey}": {\n "b2c-dx-mcp": {\n ${extra}"command": "npx",\n "args": ["-y", "@salesforce/b2c-dx-mcp@latest", "--toolsets", "${toolsets}", "--allow-non-ga-tools"]\n }\n }\n}\n\`\`\``; + } else if (includeMcp && ide === 'agentforce-vibes') { + mcpConfig = 'For Agentforce Vibes, follow the IDE-specific MCP setup linked in the checklist.'; + } + + const ideDoc = ideAnchor(ide); + const mcpDoc = mcpAnchor(ide); + const vendorSkills = vendorSkillsDoc(ide); + const vendorMcp = vendorMcpDoc(ide); + + const checklist = [ + check(`Install B2C agent skills for ${ideDoc.label}`, link('/guide/agent-skills', ideDoc.hash, ideDoc.label)), + ...(vendorSkills + ? [check(`Reference: ${vendorSkills.label}`, link(vendorSkills.url, undefined, vendorSkills.label))] + : []), + ...(includeMcp + ? [ + check(`Install the MCP server for ${ideDoc.label}`, link('/mcp/installation', mcpDoc.hash, mcpDoc.label)), + ...(vendorMcp ? [check(`Reference: ${vendorMcp.label}`, link(vendorMcp.url, undefined, vendorMcp.label))] : []), + check( + 'Pick your MCP toolsets (or accept defaults)', + link('/mcp/configuration', 'toolset-selection', 'Toolset Selection'), + ), + ] + : []), + check( + 'Provide credentials via dw.json or .env in the project root', + link('/mcp/configuration', 'dw-json', 'MCP Credentials (dw.json)'), + ), + ]; + + const warnings: string[] = []; + if (mcpConfig) warnings.push(mcpConfig); + if (ide === 'codex') { + warnings.push( + 'Codex does not yet support an MCP plugin via the marketplace. Skills are installed; install the MCP server separately when Codex adds support.', + ); + } + + return { + dwJson: installBlock, + checklist, + warnings, + verifyCommand: + ide === 'claude-code' + ? 'claude plugin list' + : ide === 'copilot-cli' + ? 'copilot plugin list' + : 'In your IDE, ask the agent: "What B2C skills do you have available?"', + }; + }, +}); diff --git a/docs/.vitepress/data/adventures/cartridge-path.ts b/docs/.vitepress/data/adventures/cartridge-path.ts new file mode 100644 index 000000000..7fc93d6c8 --- /dev/null +++ b/docs/.vitepress/data/adventures/cartridge-path.ts @@ -0,0 +1,192 @@ +// Adventure: Manage site cartridge paths (b2c sites cartridges). + +import {choice, defineAdventure, doc, md, step} from './_authoring.js'; +import {check, dwJson, link, ocapiConfig, scopes} from './_helpers.js'; + +export const cartridgePathAdventure = defineAdventure({ + id: 'cartridge-path', + title: 'Manage site cartridge paths', + tagline: "List, add, remove, or reorder cartridges in a site's cartridge path.", + icon: 'mdi:layers-outline', + tags: ['sites', 'cartridges', 'deploy'], + priority: 'common', + intro: + 'b2c sites cartridges manages the ordered list of cartridges active on a storefront. It uses OAuth + OCAPI by default and falls back to site archive import/export when /sites/*/cartridges OCAPI permissions are not available.', + + steps: [ + step('operation', { + title: 'What do you want to do?', + subtitle: 'Read-only operations are always safe; destructive operations honor safety mode.', + doc: doc('/cli/sites', undefined, 'Sites command reference'), + choices: [ + choice('list', { + title: 'List cartridge path', + subtitle: 'Read-only inspection', + icon: 'mdi:format-list-bulleted', + badges: [{text: 'Quick', tone: 'quick'}], + body: md`\`b2c sites cartridges list\` — show the ordered cartridges for a site.`, + contributes: {operation: 'list'}, + }), + choice('add', { + title: 'Add a cartridge', + subtitle: 'Insert at first / last / before / after', + icon: 'mdi:playlist-plus', + body: md`\`b2c sites cartridges add --position --target \`.`, + contributes: {operation: 'add'}, + }), + choice('remove', { + title: 'Remove a cartridge', + subtitle: 'Destructive — gated by safety mode', + icon: 'mdi:playlist-minus', + badges: [{text: 'Complex', tone: 'complex'}], + body: md`\`b2c sites cartridges remove \`. Blocked when \`SFCC_SAFETY_LEVEL\` is \`NO_DELETE\` or stricter (default \`NONE\` allows).`, + contributes: {operation: 'remove'}, + }), + choice('set', { + title: 'Replace cartridge path', + subtitle: 'Destructive — overwrites the entire path', + icon: 'mdi:swap-horizontal', + badges: [{text: 'Complex', tone: 'complex'}], + body: md`\`b2c sites cartridges set "cart1:cart2:cart3"\`. Blocked when \`SFCC_SAFETY_LEVEL\` is \`NO_DELETE\` or stricter (default \`NONE\` allows).`, + contributes: {operation: 'set'}, + }), + ], + }), + + step('auth', { + title: 'How will the CLI talk to /sites?', + subtitle: + 'The default path is OAuth + OCAPI; if /sites/*/cartridges is not granted, the CLI falls back to site archive import.', + doc: doc('/guide/authentication', 'account-manager-api-client', 'Account Manager API Client'), + choices: [ + choice('ocapi-direct', { + title: 'OAuth + OCAPI (direct)', + subtitle: 'Recommended', + icon: 'mdi:key-variant', + badges: [{text: 'Recommended', tone: 'info'}], + body: md`Account Manager API client with OCAPI permissions for \`/sites\`, \`/sites/*\`, and \`/sites/*/cartridges\`. Fastest and most direct.`, + contributes: {authStrategy: 'ocapi-direct'}, + }), + choice('bm-fallback', { + title: 'Site archive fallback', + subtitle: 'When /sites/*/cartridges OCAPI access is not available', + icon: 'mdi:archive-arrow-down-outline', + body: md`Uses \`sfcc-site-archive-import\` + WebDAV \`Impex/\`. Use the \`--bm\` flag (shorthand for \`--site-id Sites-Site\`) to target the Business Manager cartridge path; BM updates always go through this fallback.`, + contributes: {authStrategy: 'bm-fallback'}, + }), + ], + }), + + step('persistence', { + title: 'How should the CLI find your config?', + subtitle: 'The site ID is always passed via --site-id (or --bm).', + doc: doc('/guide/configuration', 'configuration-file', 'Configuration file (dw.json)'), + choices: [ + choice('dw-json', { + title: 'dw.json (project root)', + subtitle: 'Recommended', + icon: 'mdi:file-cog-outline', + body: md`Per-project config file with walk-up discovery. Pass \`--site-id \` on each invocation.`, + contributes: {configSource: 'dw-json'}, + }), + choice('env', { + title: '.env / environment variables', + subtitle: 'CI-friendly', + icon: 'mdi:console-line', + body: md`Use \`SFCC_*\` env vars for hostname + credentials; the CLI auto-loads a \`.env\` file. Note: there is no \`SFCC_SITE_ID\` for these commands — \`--site-id\` (or \`--bm\`) is required on every invocation.`, + contributes: {configSource: 'env'}, + }), + ], + }), + ], + + synthesize(state) { + const op = (state.operation as string) ?? 'list'; + const isDestructive = op === 'remove' || op === 'set'; + const useBmFallback = state.authStrategy === 'bm-fallback'; + const useEnv = state.configSource === 'env'; + + const dw = useEnv + ? '# Using environment variables — see .env tab below.' + : dwJson({hostname: true, clientId: true, clientSecret: true}); + + const env = useEnv + ? [ + 'SFCC_SERVER=.dx.commercecloud.salesforce.com', + 'SFCC_CLIENT_ID=', + 'SFCC_CLIENT_SECRET=', + ].join('\n') + : undefined; + + const ocapi = useBmFallback + ? ocapiConfig('', ['sites']) + : ocapiConfig('', ['sites', 'siteCartridges']); + + const checklist = [ + check( + 'Create an Account Manager API client', + link('/guide/authentication', 'creating-an-api-client', 'Creating an API Client'), + ), + check( + `Add Default Scopes: ${scopes('baseline')}`, + link('/guide/authentication', 'configuring-scopes', 'Configuring Scopes'), + ), + check( + 'Add a tenant filter on the Sandbox API User role', + link('/guide/authentication', 'configuring-tenant-filter', 'Configuring Tenant Filter'), + ), + check( + useBmFallback + ? 'Enable /sites and /sites/* in OCAPI Data API (Business Manager)' + : 'Enable /sites, /sites/*, and /sites/*/cartridges in OCAPI Data API (Business Manager)', + link('/guide/authentication', 'ocapi-configuration', 'OCAPI Configuration'), + ), + ...(useBmFallback + ? [ + check( + 'Grant Job Execution permission for sfcc-site-archive-import (BM fallback only)', + link('/guide/authentication', 'ocapi-configuration', 'OCAPI Configuration'), + ), + check( + 'Grant WebDAV write access to /Impex (BM fallback only)', + link('/guide/authentication', 'webdav-access', 'WebDAV Access'), + ), + ] + : []), + check( + useEnv ? 'Set SFCC_* environment variables' : 'Save the dw.json snippet to your project root', + link( + '/guide/configuration', + useEnv ? 'environment-variables' : 'configuration-file', + useEnv ? 'Environment Variables' : 'Configuration File', + ), + ), + ]; + + const warnings: string[] = [ + `Paste the following block into Business Manager → Administration → Site Development → Open Commerce API Settings → Data API:\n\n\`\`\`json\n${ocapi}\n\`\`\``, + ]; + + if (isDestructive) { + warnings.push( + 'Destructive cartridge-path commands (`remove`, `set`) are blocked when `SFCC_SAFETY_LEVEL` is `NO_DELETE` or stricter. For production sites, prefer `b2c sites cartridges add --position --target ` over wholesale `set`.', + ); + } + + if (useBmFallback) { + warnings.push( + 'Without `/sites/*/cartridges` OCAPI permissions, the CLI falls back to site archive import/export — which requires `sfcc-site-archive-import` job execution permission and WebDAV write access to `Impex/`.', + ); + } + + const verifyCommand = 'b2c sites cartridges list --site-id '; + + return { + dwJson: dw, + env, + checklist, + warnings, + verifyCommand, + }; + }, +}); diff --git a/docs/.vitepress/data/adventures/ci-cd.ts b/docs/.vitepress/data/adventures/ci-cd.ts new file mode 100644 index 000000000..f41124eee --- /dev/null +++ b/docs/.vitepress/data/adventures/ci-cd.ts @@ -0,0 +1,289 @@ +// Adventure: Set up CI/CD pipeline. + +import {choice, defineAdventure, doc, md, step} from './_authoring.js'; +import {check, link, ocapiConfig, scopes} from './_helpers.js'; + +export const ciCdAdventure = defineAdventure({ + id: 'ci-cd', + title: 'Set up CI/CD pipeline', + tagline: 'Automate cartridge deployment from GitHub Actions or another CI runner.', + icon: 'mdi:source-branch', + tags: ['ci-cd', 'automation', 'client-credentials', 'safety-mode', 'deploy', 'github'], + priority: 'common', + intro: + "CI/CD runs the CLI non-interactively. You'll wire credentials through your runner's secret store, pick an authentication method that works without a human at the keyboard, and choose a safety level so production deploys can't accidentally delete or overwrite data.", + + steps: [ + step('runner', { + title: 'Which CI runner are you using?', + doc: doc('/guide/ci-cd', 'overview', 'CI/CD overview'), + choices: [ + choice('github', { + title: 'GitHub Actions', + subtitle: 'Most common runner', + icon: 'mdi:github', + badges: [{text: 'Recommended', tone: 'info'}], + body: md`Run the B2C CLI in a GitHub Actions workflow with secrets from the repository's secret store. See the [CI/CD quick start](/guide/ci-cd#quick-start-deploy-cartridges).`, + contributes: {runner: 'github'}, + }), + choice('generic', { + title: 'GitLab / Jenkins / Other', + subtitle: 'Generic runner', + icon: 'mdi:server-outline', + body: md`Install the CLI in your job and load \`SFCC_*\` env vars from the runner's secret store.`, + contributes: {runner: 'generic'}, + }), + ], + }), + + step('auth', { + title: 'How will the runner authenticate?', + subtitle: 'Both options are non-interactive — pick what your security policy allows.', + doc: doc('/guide/authentication', 'account-manager-api-client', 'Account Manager API Client'), + choices: [ + choice('client-credentials', { + title: 'Client Credentials', + subtitle: 'Client ID + secret', + icon: 'mdi:key-variant', + badges: [{text: 'Common', tone: 'quick'}], + body: md`Account Manager API client with a client secret stored in your runner's secret store.`, + contributes: {authMethod: 'client-credentials'}, + }), + choice('jwt', { + title: 'JWT Bearer', + subtitle: 'Certificate-based', + icon: 'mdi:certificate-outline', + badges: [{text: 'Preferred where allowed', tone: 'info'}], + body: md`Use a public/private cert pair instead of a long-lived secret. See [JWT setup](/guide/authentication#jwt-authentication-certificate-based).`, + contributes: {authMethod: 'jwt'}, + }), + ], + }), + + step('scope', { + title: 'What will the pipeline deploy?', + doc: doc('/guide/ci-cd', 'quick-start-deploy-cartridges', 'Quick Start: Deploy Cartridges'), + choices: [ + choice('cartridges', { + title: 'Cartridges only', + subtitle: 'Deploy + activate', + icon: 'mdi:cloud-upload-outline', + body: md`Build, upload, and activate a code version with \`b2c code deploy\`.`, + contributes: {deployScope: 'cartridges'}, + }), + choice('cartridges-jobs', { + title: 'Cartridges + job-driven imports', + subtitle: 'Adds Jobs OCAPI scope', + icon: 'mdi:cog-play-outline', + body: md`Also runs site / catalog imports via \`b2c webdav put\` + \`b2c job run sfcc-site-archive-import\` (followed by \`b2c job wait\`).`, + contributes: {deployScope: 'cartridges-jobs'}, + }), + ], + }), + + step('safety', { + title: 'What safety level should the pipeline enforce?', + subtitle: 'Set SFCC_SAFETY_LEVEL so destructive operations are blocked at the SDK middleware layer.', + doc: doc('/guide/safety', 'safety-levels', 'Safety Levels'), + choices: [ + choice('none', { + title: 'NONE', + subtitle: 'No restrictions', + icon: 'mdi:lock-open-variant-outline', + body: md`Default. Acceptable for ephemeral sandbox pipelines only.`, + contributes: {safetyLevel: 'NONE'}, + }), + choice('no-delete', { + title: 'NO_DELETE', + subtitle: 'Block DELETE operations', + icon: 'mdi:lock-outline', + badges: [{text: 'Common', tone: 'quick'}], + body: md`A reasonable default for shared sandboxes — uploads and activations still work.`, + contributes: {safetyLevel: 'NO_DELETE'}, + }), + choice('no-update', { + title: 'NO_UPDATE', + subtitle: 'Block deletes + reset/stop/restart', + icon: 'mdi:shield-lock-outline', + badges: [{text: 'Recommended', tone: 'info'}], + body: md`Recommended for production pipelines — preserves the ability to deploy code while blocking destructive admin ops.`, + contributes: {safetyLevel: 'NO_UPDATE'}, + }), + choice('read-only', { + title: 'READ_ONLY', + subtitle: 'Block all writes', + icon: 'mdi:eye-lock-outline', + body: md`Audit / verification jobs that should never modify the instance.`, + contributes: {safetyLevel: 'READ_ONLY'}, + }), + ], + }), + ], + + synthesize(state) { + const runner = (state.runner as string) ?? 'github'; + const authMethod = (state.authMethod as string) ?? 'client-credentials'; + const deployScope = (state.deployScope as string) ?? 'cartridges'; + const safetyLevel = (state.safetyLevel as string) ?? 'NO_DELETE'; + + const isJwt = authMethod === 'jwt'; + const includeJobs = deployScope === 'cartridges-jobs'; + const isGithub = runner === 'github'; + + const dw = "# CI/CD prefers environment variables from your runner's secret store — see the .env tab."; + + const envLines = [ + 'SFCC_SERVER=.dx.commercecloud.salesforce.com', + 'SFCC_CODE_VERSION=version1', + 'SFCC_CLIENT_ID=', + isJwt ? 'SFCC_JWT_CERT=./cert.pem' : 'SFCC_CLIENT_SECRET=', + isJwt ? 'SFCC_JWT_KEY=./key.pem' : '', + 'SFCC_USERNAME=', + 'SFCC_PASSWORD=', + `SFCC_SAFETY_LEVEL=${safetyLevel}`, + ] + .filter(Boolean) + .join('\n'); + + const checklist = [ + check( + isJwt + ? 'Create an Account Manager API client with JWT (certificate) auth' + : 'Create an Account Manager API client with a client secret', + link('/guide/authentication', 'creating-an-api-client', 'Creating an API Client'), + ), + check( + `Add Default Scopes: ${scopes('baseline')}`, + link('/guide/authentication', 'configuring-scopes', 'Configuring Scopes'), + ), + ...(isJwt + ? [ + check( + 'Generate a certificate pair and register the public cert in Account Manager', + link( + '/guide/authentication', + 'jwt-authentication-certificate-based', + 'JWT Authentication (Certificate-Based)', + ), + ), + ] + : []), + check( + 'Generate a WebDAV access key for cartridge uploads', + link('/guide/authentication', 'option-a-basic-authentication-user-access', 'Basic Authentication'), + ), + check( + 'Enable code_versions in OCAPI Data API (Business Manager)', + link('/guide/authentication', 'ocapi-configuration', 'OCAPI Configuration'), + ), + ...(includeJobs + ? [ + check( + 'Enable Jobs in OCAPI Data API (Business Manager)', + link('/guide/authentication', 'ocapi-configuration', 'OCAPI Configuration'), + ), + ] + : []), + check( + isGithub + ? 'Add SFCC_* values as GitHub repository secrets / variables' + : "Store SFCC_* values in your CI runner's secret store", + link('/guide/ci-cd', 'authentication', isGithub ? 'GitHub Actions: Authentication' : 'CI/CD Authentication'), + ), + check( + `Set SFCC_SAFETY_LEVEL=${safetyLevel} for production deployments`, + link('/guide/safety', 'safety-levels', 'Safety Levels'), + ), + ]; + + const ocapi = ocapiConfig('', includeJobs ? ['codeVersions', 'jobs'] : ['codeVersions']); + + const warnings: string[] = [ + "Never commit `dw.json` or `.env` files containing real secrets — always load them from your runner's secret store.", + `Paste the following block into Business Manager → Administration → Site Development → Open Commerce API Settings → Data API:\n\n\`\`\`json\n${ocapi}\n\`\`\``, + ]; + + if (isGithub) { + const ghLines: string[] = [ + 'name: Deploy', + '', + 'on:', + ' push:', + ' branches: [main]', + '', + 'jobs:', + ' deploy:', + ' runs-on: ubuntu-latest', + ' env:', + ' SFCC_SERVER: ${{ vars.SFCC_SERVER }}', + ' SFCC_CODE_VERSION: ${{ vars.SFCC_CODE_VERSION }}', + ' SFCC_CLIENT_ID: ${{ secrets.SFCC_CLIENT_ID }}', + ]; + if (isJwt) { + ghLines.push(' SFCC_JWT_CERT: ./cert.pem'); + ghLines.push(' SFCC_JWT_KEY: ./key.pem'); + } else { + ghLines.push(' SFCC_CLIENT_SECRET: ${{ secrets.SFCC_CLIENT_SECRET }}'); + } + ghLines.push( + ' SFCC_USERNAME: ${{ secrets.SFCC_USERNAME }}', + ' SFCC_PASSWORD: ${{ secrets.SFCC_PASSWORD }}', + ` SFCC_SAFETY_LEVEL: ${safetyLevel}`, + ' steps:', + ' - uses: actions/checkout@v4', + ' - uses: actions/setup-node@v4', + ' with:', + ' node-version: 22', + ' - run: npm install -g @salesforce/b2c-cli', + ); + if (isJwt) { + ghLines.push( + ' - name: Write JWT credentials', + ' run: |', + ' printf "%s" "${{ secrets.SFCC_JWT_CERT_PEM }}" > cert.pem', + ' printf "%s" "${{ secrets.SFCC_JWT_KEY_PEM }}" > key.pem', + ); + } + ghLines.push(' - run: b2c code deploy --activate'); + if (includeJobs) { + ghLines.push( + ' - name: Upload site archive', + ' run: b2c webdav put ./site-archive.zip Impex/src/instance/', + ' - name: Run site archive import', + ' id: import', + ' run: b2c job run sfcc-site-archive-import --json | tee job.json', + ' - name: Wait for import', + ' run: |', + ' JOB_ID=$(jq -r .id job.json)', + ' EXEC_ID=$(jq -r .executionId job.json)', + ' b2c job wait "$JOB_ID" "$EXEC_ID"', + ); + } + warnings.push(`Example GitHub Actions workflow:\n\n\`\`\`yaml\n${ghLines.join('\n')}\n\`\`\``); + } else { + warnings.push( + 'Generic runners: install the CLI with `npm i -g @salesforce/b2c-cli`, then run `b2c code deploy --code-version=$SFCC_CODE_VERSION --activate` after exporting the SFCC_* env vars from your secret store.', + ); + } + + if (isJwt) { + warnings.push( + 'For JWT auth, write the cert/key files to disk in a single setup step (e.g., `echo "$SFCC_JWT_CERT_PEM" > cert.pem`) and point `SFCC_JWT_CERT` / `SFCC_JWT_KEY` at them. Avoid committing certs.', + ); + } + + if (safetyLevel === 'NONE') { + warnings.push( + 'You picked SFCC_SAFETY_LEVEL=NONE — destructive operations are not blocked. Reserve this for sandbox-only workflows.', + ); + } + + return { + dwJson: dw, + env: envLines, + checklist, + warnings, + verifyCommand: 'b2c code list', + }; + }, +}); diff --git a/docs/.vitepress/data/adventures/debug.ts b/docs/.vitepress/data/adventures/debug.ts new file mode 100644 index 000000000..8dbdefc43 --- /dev/null +++ b/docs/.vitepress/data/adventures/debug.ts @@ -0,0 +1,189 @@ +// Adventure: Debug server-side scripts (b2c debug, b2c debug cli). +// +// The B2C script debugger (SDAPI) requires Basic auth (BM username + WebDAV +// access key). OAuth tokens are not sufficient. This wizard helps the user +// pick a debugging surface (VS Code DAP, CLI REPL, or JSONL RPC) and +// persistence strategy, then synthesizes the right dw.json / env vars + +// checklist. + +import {choice, defineAdventure, doc, md, step} from './_authoring.js'; +import {check, dwJson, link} from './_helpers.js'; + +export const debugAdventure = defineAdventure({ + id: 'debug', + title: 'Debug server-side scripts', + tagline: 'Set breakpoints and step through cartridges, jobs, and APIs from your IDE or a REPL.', + icon: 'mdi:bug-outline', + tags: ['debug', 'vscode', 'dap', 'repl'], + priority: 'common', + intro: + "The B2C script debugger lets you pause server-side scripts, inspect variables, and step through controllers, jobs, hooks, and Custom APIs. It speaks SDAPI under the hood and authenticates with Basic auth (BM username + WebDAV access key) — OAuth tokens won't work.", + + steps: [ + step('surface', { + title: 'How will you drive the debugger?', + doc: doc('/cli/debug', undefined, 'Debug Commands'), + choices: [ + choice('vscode', { + title: 'VS Code (DAP)', + subtitle: 'Recommended for IDEs', + icon: 'mdi:microsoft-visual-studio-code', + badges: [{text: 'Recommended', tone: 'info'}], + body: md`Use the B2C DX VS Code extension or wire \`b2c debug\` in as a DAP adapter for any IDE that speaks the Debug Adapter Protocol.`, + contributes: {surface: 'vscode'}, + }), + choice('repl', { + title: 'Terminal REPL', + subtitle: 'b2c debug cli', + icon: 'mdi:console', + badges: [{text: 'Quick', tone: 'quick'}], + body: md`Interactive terminal session with \`break\`, \`continue\`, \`step\`, \`vars\`, and \`eval\` commands. No IDE setup required.`, + contributes: {surface: 'repl'}, + }), + choice('rpc', { + title: 'JSONL RPC', + subtitle: 'b2c debug cli --rpc', + icon: 'mdi:robot-outline', + body: md`Headless JSONL-over-stdio mode for scripts and agents. Drive breakpoints and stepping programmatically.`, + contributes: {surface: 'rpc'}, + }), + ], + }), + + step('persistence', { + title: 'How should the CLI find your config?', + doc: doc('/guide/configuration', 'configuration-file', 'Configuration file (dw.json)'), + choices: [ + choice('dw-json', { + title: 'dw.json (project root)', + subtitle: 'Recommended', + icon: 'mdi:file-cog-outline', + body: md`Per-project config file with walk-up discovery — usually colocated with your cartridges.`, + contributes: {configSource: 'dw-json'}, + }), + choice('env', { + title: '.env / environment variables', + subtitle: 'CI-friendly', + icon: 'mdi:console-line', + body: md`Use \`SFCC_*\` env vars; the CLI auto-loads a \`.env\` file.`, + contributes: {configSource: 'env'}, + }), + ], + }), + ], + + synthesize(state) { + const surface = state.surface as string | undefined; + const useEnv = state.configSource === 'env'; + + const isVscode = surface === 'vscode'; + const isRepl = surface === 'repl'; + const isRpc = surface === 'rpc'; + + const dw = useEnv + ? '# Using environment variables — see .env tab below.' + : dwJson({ + hostname: true, + codeVersion: 'version1', + username: true, + password: true, + }); + + const env = useEnv + ? [ + 'SFCC_SERVER=.dx.commercecloud.salesforce.com', + 'SFCC_CODE_VERSION=version1', + 'SFCC_USERNAME=', + 'SFCC_PASSWORD=', + ].join('\n') + : undefined; + + const checklist = [ + check( + 'Enable Script Debugger in Business Manager (Administration → Development Configuration → Script Debugger)', + link('/cli/debug', undefined, 'Debug Commands · Authentication'), + ), + check( + 'Generate a WebDAV access key for your BM user', + link('/guide/authentication', 'option-a-basic-authentication-user-access', 'Basic Authentication'), + ), + check( + useEnv + ? 'Set SFCC_SERVER, SFCC_USERNAME, and SFCC_PASSWORD environment variables' + : 'Save the dw.json snippet to your project root (alongside your cartridges)', + link( + '/guide/configuration', + useEnv ? 'environment-variables' : 'configuration-file', + useEnv ? 'Environment Variables' : 'Configuration File', + ), + ), + ...(isVscode + ? [ + check( + 'Install the B2C DX VS Code extension (recommended) — it ships its own DAP integration', + link('/vscode-extension/', undefined, 'B2C DX VS Code Extension'), + ), + check( + 'Or wire `b2c debug` into your IDE as a custom DAP adapter', + link('/cli/debug', undefined, 'b2c debug · IDE Integration'), + ), + ] + : []), + ...(isRpc + ? [ + check( + 'Drive the debugger over JSONL stdio with `b2c debug cli --rpc`', + link('/cli/debug', undefined, 'RPC Mode'), + ), + ] + : []), + ]; + + const warnings: string[] = [ + 'OAuth tokens cannot be used for the script debugger — Basic auth (BM username + WebDAV access key) is required.', + 'Make sure your local cartridge tree matches the code version deployed to the instance, otherwise breakpoints may not bind.', + ]; + + if (isVscode) { + // Snippet matches the contributed debugger type in the B2C DX VS Code + // extension (packages/b2c-vs-extension/package.json contributes.debuggers). + warnings.push( + 'Sample VS Code launch.json snippet:\n\n' + + '```json\n' + + JSON.stringify( + { + version: '0.2.0', + configurations: [ + { + type: 'b2c-script', + request: 'launch', + name: 'B2C Script Debugger', + cartridgePath: '${workspaceFolder}/cartridges', + }, + ], + }, + null, + 2, + ) + + '\n```', + ); + } + + let verifyCommand: string; + if (isRepl) { + verifyCommand = 'b2c debug cli'; + } else if (isRpc) { + verifyCommand = 'b2c debug cli --rpc'; + } else { + verifyCommand = 'b2c debug --help'; + } + + return { + dwJson: dw, + env, + checklist, + warnings, + verifyCommand, + }; + }, +}); diff --git a/docs/.vitepress/data/adventures/deploy-code.ts b/docs/.vitepress/data/adventures/deploy-code.ts new file mode 100644 index 000000000..e88a804fa --- /dev/null +++ b/docs/.vitepress/data/adventures/deploy-code.ts @@ -0,0 +1,195 @@ +// Adventure: Deploy cartridge code (b2c code deploy / watch / activate). + +import {choice, defineAdventure, doc, md, step} from './_authoring.js'; +import {check, dwJson, link, ocapiConfig, scopes} from './_helpers.js'; + +export const deployCodeAdventure = defineAdventure({ + id: 'deploy-code', + title: 'Deploy cartridge code', + tagline: 'Upload, watch, and activate cartridges on a B2C Commerce instance.', + icon: 'mdi:cloud-upload-outline', + tags: ['code', 'webdav', 'ocapi', 'deploy', 'sfra', 'cartridges'], + priority: 'core', + intro: + 'Code deployment uses WebDAV to upload files. Listing and activating code versions additionally requires OAuth + OCAPI.', + + steps: [ + step('instance', { + title: 'Where will you deploy?', + doc: doc('/guide/authentication', 'overview', 'Authentication overview'), + choices: [ + choice('ods', { + title: 'On-Demand Sandbox', + subtitle: 'Realm-managed', + icon: 'mdi:flask-outline', + body: md`Short-lived sandbox provisioned with \`b2c sandbox\`.`, + contributes: {instanceType: 'ods'}, + }), + choice('primary', { + title: 'Primary Instance', + subtitle: 'Development / Staging / Production', + icon: 'mdi:server', + body: md`A long-running instance with its own hostname and credentials.`, + contributes: {instanceType: 'primary'}, + }), + ], + }), + + step('webdav', { + title: 'How will WebDAV authenticate?', + subtitle: 'WebDAV is required for the actual file upload.', + doc: doc('/guide/authentication', 'webdav-access', 'WebDAV Access'), + choices: [ + choice('basic', { + title: 'BM username + access key', + subtitle: 'Recommended', + icon: 'mdi:key-outline', + badges: [{text: 'Quick', tone: 'quick'}], + body: md`Best performance for large uploads. Generate the access key in Business Manager.`, + contributes: {webdavAuth: 'basic'}, + }), + choice('oauth', { + title: 'OAuth (client credentials)', + subtitle: 'Use the same client as OCAPI', + icon: 'mdi:key-variant', + body: md`Requires WebDAV Client Permissions configured in BM for your \`client_id\`.`, + contributes: {webdavAuth: 'oauth'}, + }), + ], + }), + + step('activation', { + title: 'Will you list / activate code versions from the CLI?', + subtitle: 'b2c code list, activate, delete need OCAPI.', + doc: doc('/guide/authentication', 'ocapi-configuration', 'OCAPI Configuration'), + choices: [ + choice('yes', { + title: 'Yes — full lifecycle', + subtitle: 'list / activate / delete', + icon: 'mdi:check-circle-outline', + badges: [{text: 'Recommended', tone: 'info'}], + body: md`Adds an Account Manager API client and an OCAPI Data API entry for \`/code_versions\`.`, + contributes: {needsOcapi: true}, + }), + choice('no', { + title: 'No — upload only', + subtitle: 'b2c code deploy / watch', + icon: 'mdi:upload', + body: md`Skip OCAPI. You can still upload but will need to activate code versions in BM manually.`, + contributes: {needsOcapi: false}, + }), + ], + }), + + step('persistence', { + title: 'How should the CLI find your config?', + doc: doc('/guide/configuration', 'configuration-file', 'Configuration file (dw.json)'), + choices: [ + choice('dw-json', { + title: 'dw.json (project root)', + subtitle: 'Recommended', + icon: 'mdi:file-cog-outline', + body: md`Per-project config file with walk-up discovery.`, + contributes: {configSource: 'dw-json'}, + }), + choice('env', { + title: '.env / environment variables', + subtitle: 'CI-friendly', + icon: 'mdi:console-line', + body: md`Use \`SFCC_*\` env vars; the CLI auto-loads a \`.env\` file.`, + contributes: {configSource: 'env'}, + }), + ], + }), + ], + + synthesize(state) { + const useBasic = state.webdavAuth === 'basic'; + const needsOcapi = state.needsOcapi === true; + const useEnv = state.configSource === 'env'; + + const dw = useEnv + ? '# Using environment variables — see .env tab below.' + : dwJson({ + hostname: true, + codeVersion: 'version1', + username: useBasic, + password: useBasic, + clientId: needsOcapi || !useBasic, + clientSecret: needsOcapi || !useBasic, + }); + + const envLines = [ + 'SFCC_SERVER=.dx.commercecloud.salesforce.com', + 'SFCC_CODE_VERSION=version1', + useBasic ? 'SFCC_USERNAME=' : '', + useBasic ? 'SFCC_PASSWORD=' : '', + needsOcapi || !useBasic ? 'SFCC_CLIENT_ID=' : '', + needsOcapi || !useBasic ? 'SFCC_CLIENT_SECRET=' : '', + ] + .filter(Boolean) + .join('\n'); + + const checklist = [ + ...(useBasic + ? [ + check( + 'Generate a WebDAV access key for your BM user', + link('/guide/authentication', 'option-a-basic-authentication-user-access', 'Basic Authentication'), + ), + ] + : [ + check( + 'Create an Account Manager API client', + link('/guide/authentication', 'creating-an-api-client', 'Creating an API Client'), + ), + check( + 'Configure WebDAV Client Permissions in Business Manager', + link('/guide/authentication', 'option-b-oauth-based-webdav-api-client-access', 'OAuth-based WebDAV'), + ), + ]), + ...(needsOcapi + ? [ + check( + 'Create an Account Manager API client (if not already)', + link('/guide/authentication', 'creating-an-api-client', 'Creating an API Client'), + ), + check( + `Add Default Scopes: ${scopes('baseline')}`, + link('/guide/authentication', 'configuring-scopes', 'Configuring Scopes'), + ), + check( + 'Enable code_versions in OCAPI Data API (Business Manager)', + link('/guide/authentication', 'ocapi-configuration', 'OCAPI Configuration'), + ), + ] + : []), + check( + useEnv ? 'Set SFCC_* environment variables' : 'Save the dw.json snippet to your project root', + link( + '/guide/configuration', + useEnv ? 'environment-variables' : 'configuration-file', + useEnv ? 'Environment Variables' : 'Configuration File', + ), + ), + ]; + + const warnings: string[] = []; + if (needsOcapi) { + warnings.push( + `Paste this OCAPI Data API config into Business Manager (Site Development → Open Commerce API Settings → Data API):\n\n\`\`\`json\n${ocapiConfig('', ['codeVersions'])}\n\`\`\``, + ); + } + if (state.instanceType === 'ods') { + warnings.push('On ODS, hostname and credentials change per sandbox — refresh dw.json with `b2c sandbox info`.'); + } + + return { + dwJson: dw, + env: useEnv ? envLines : undefined, + checklist, + warnings, + verifyCommand: needsOcapi ? 'b2c code list' : 'b2c webdav ls --root=cartridges', + }; + }, +}); diff --git a/docs/.vitepress/data/adventures/index.ts b/docs/.vitepress/data/adventures/index.ts new file mode 100644 index 000000000..6e27ffbbb --- /dev/null +++ b/docs/.vitepress/data/adventures/index.ts @@ -0,0 +1,79 @@ +// Setup Adventure registry. Adding an adventure = one new file in this +// directory + one entry in `adventures` below + one Markdown page under +// `docs/setup/.md`. + +import {accountManagerAdventure} from './account-manager.js'; +import {agentMcpAdventure} from './agent-mcp.js'; +import {cartridgePathAdventure} from './cartridge-path.js'; +import {ciCdAdventure} from './ci-cd.js'; +import {debugAdventure} from './debug.js'; +import {deployCodeAdventure} from './deploy-code.js'; +import {jobsAdventure} from './jobs.js'; +import {logsAdventure} from './logs.js'; +import {migrateSfccCiAdventure} from './migrate-sfcc-ci.js'; +import {mrtDeployAdventure} from './mrt-deploy.js'; +import {multiInstanceAdventure} from './multi-instance.js'; +import {pageDesignerAdventure} from './page-designer.js'; +import {sandboxAdventure} from './sandbox.js'; +import {scapiAccessAdventure} from './scapi-access.js'; +import {scriptApiDocsAdventure} from './script-api-docs.js'; +import {slasClientsAdventure} from './slas-clients.js'; +import {vscodeExtensionAdventure} from './vscode-extension.js'; +import type {Adventure, AdventureRegistry, Flags, QuickStart} from './_types.js'; + +// Feature flags consulted by step `showIf` and choice `featureFlag`. Flip a +// flag to swap a path site-wide without changing component code. +export const flags: Flags = { + // When true, adventures prefer SCAPI over OCAPI for permissions/scopes. + // Today: false — the SCAPI variants are authored side-by-side but hidden. + 'scapi-migration': false, +}; + +export const adventures: Adventure[] = [ + // Core + deployCodeAdventure, + agentMcpAdventure, + sandboxAdventure, + vscodeExtensionAdventure, + // Common + jobsAdventure, + pageDesignerAdventure, + ciCdAdventure, + scapiAccessAdventure, + logsAdventure, + debugAdventure, + accountManagerAdventure, + cartridgePathAdventure, + multiInstanceAdventure, + // Specialized + mrtDeployAdventure, + slasClientsAdventure, + migrateSfccCiAdventure, + // Niche + scriptApiDocsAdventure, +]; + +export const quickStarts: QuickStart[] = [ + { + id: 'claude-code-skills-mcp', + label: 'Claude Code · skills + MCP', + description: 'Install all three skill packs and the MCP server in Claude Code.', + badges: [{text: 'Quick', tone: 'quick'}], + adventureId: 'agent-mcp', + preselect: {ide: 'claude-code', skills: ['b2c-cli', 'b2c', 'storefront-next'], includeMcp: true, toolsets: 'all'}, + }, + { + id: 'deploy-ods-basic', + label: 'Deploy code to ODS · WebDAV access key', + description: 'Upload + activate cartridges with a BM access key + OAuth.', + badges: [{text: 'Quick', tone: 'quick'}], + adventureId: 'deploy-code', + preselect: {instanceType: 'ods', webdavAuth: 'basic', needsOcapi: true, configSource: 'dw-json'}, + }, +]; + +export const registry: AdventureRegistry = {flags, adventures, quickStarts}; + +export function getAdventure(id: string): Adventure | undefined { + return adventures.find((a) => a.id === id); +} diff --git a/docs/.vitepress/data/adventures/jobs.ts b/docs/.vitepress/data/adventures/jobs.ts new file mode 100644 index 000000000..d39ce7fe3 --- /dev/null +++ b/docs/.vitepress/data/adventures/jobs.ts @@ -0,0 +1,148 @@ +// Adventure: Run jobs (b2c job). + +import {choice, defineAdventure, doc, md, step} from './_authoring.js'; +import {check, dwJson, link, ocapiConfig, scopes} from './_helpers.js'; + +export const jobsAdventure = defineAdventure({ + id: 'jobs', + title: 'Run jobs', + tagline: 'Trigger and watch B2C Commerce jobs from the CLI.', + icon: 'mdi:cog-play-outline', + tags: ['jobs', 'ocapi', 'oauth', 'automation'], + priority: 'common', + intro: + 'Jobs are run via OCAPI. You need an Account Manager API client with OAuth and an OCAPI configuration in Business Manager that grants the Jobs resource.', + + steps: [ + step('instance', { + title: 'Where will you run jobs?', + doc: doc('/guide/authentication', 'overview', 'Authentication overview'), + choices: [ + choice('ods', { + title: 'On-Demand Sandbox', + subtitle: 'Realm-managed', + icon: 'mdi:flask-outline', + body: md`A short-lived sandbox provisioned via \`b2c sandbox\`.`, + contributes: {instanceType: 'ods'}, + }), + choice('primary', { + title: 'Primary Instance', + subtitle: 'Development / Staging / Production', + icon: 'mdi:server', + body: md`A long-running instance you connect to with hostname + credentials.`, + contributes: {instanceType: 'primary'}, + }), + ], + }), + + step('auth', { + title: 'How will you authenticate?', + doc: doc('/guide/authentication', 'account-manager-api-client', 'Account Manager API Client'), + choices: [ + choice('client-credentials', { + title: 'Client Credentials', + subtitle: 'Recommended for CI/CD', + icon: 'mdi:key-variant', + badges: [{text: 'CI', tone: 'quick'}], + body: md`Account Manager API client with a client secret. Non-interactive, ideal for automation.`, + contributes: {authMethod: 'client-credentials'}, + }), + choice('jwt', { + title: 'JWT Bearer', + subtitle: 'Certificate-based', + icon: 'mdi:certificate-outline', + body: md`Use a public/private cert pair instead of a client secret. See [JWT setup](/guide/authentication#jwt-authentication-certificate-based).`, + contributes: {authMethod: 'jwt'}, + }), + choice('user-auth', { + title: 'User Auth (Browser)', + subtitle: 'Implicit', + icon: 'mdi:account-arrow-right-outline', + body: md`Browser-based login. Useful for local development without a stored secret.`, + contributes: {authMethod: 'implicit'}, + }), + ], + }), + + step('persistence', { + title: 'How should the CLI find your config?', + doc: doc('/guide/configuration', 'configuration-file', 'Configuration file (dw.json)'), + choices: [ + choice('dw-json', { + title: 'dw.json (project root)', + subtitle: 'Recommended', + icon: 'mdi:file-cog-outline', + body: md`Per-project config file. Walk-up discovery from the current directory.`, + contributes: {configSource: 'dw-json'}, + }), + choice('env', { + title: '.env / environment variables', + subtitle: 'CI-friendly', + icon: 'mdi:console-line', + body: md`Use \`SFCC_*\` environment variables — the CLI also auto-loads \`.env\` files.`, + contributes: {configSource: 'env'}, + }), + ], + }), + ], + + synthesize(state) { + const isJwt = state.authMethod === 'jwt'; + const isImplicit = state.authMethod === 'implicit'; + const useEnv = state.configSource === 'env'; + + const dw = useEnv + ? '# Using environment variables — see .env tab below.' + : dwJson({hostname: true, clientId: true, clientSecret: !isJwt && !isImplicit}); + + const env = useEnv + ? [ + 'SFCC_SERVER=.dx.commercecloud.salesforce.com', + 'SFCC_CLIENT_ID=', + isJwt ? 'SFCC_JWT_CERT=./cert.pem' : '', + isJwt ? 'SFCC_JWT_KEY=./key.pem' : '', + !isJwt && !isImplicit ? 'SFCC_CLIENT_SECRET=' : '', + ] + .filter(Boolean) + .join('\n') + : undefined; + + const ocapi = ocapiConfig('', ['jobs']); + + return { + dwJson: dw, + env, + checklist: [ + check( + 'Create an Account Manager API client', + link('/guide/authentication', 'creating-an-api-client', 'Creating an API Client'), + ), + check( + `Add Default Scopes: ${scopes('baseline')}`, + link('/guide/authentication', 'configuring-scopes', 'Configuring Scopes'), + ), + check( + 'Add a tenant filter on the Sandbox API User role', + link('/guide/authentication', 'configuring-tenant-filter', 'Configuring Tenant Filter'), + ), + check( + 'Enable Jobs in OCAPI Data API (Business Manager)', + link('/guide/authentication', 'ocapi-configuration', 'OCAPI Configuration'), + ), + check( + useEnv ? 'Set SFCC_* environment variables' : 'Save the dw.json snippet to your project root', + link( + '/guide/configuration', + useEnv ? 'environment-variables' : 'configuration-file', + useEnv ? 'Environment Variables' : 'Configuration File', + ), + ), + ], + warnings: [ + `Paste the following block into Business Manager → Administration → Site Development → Open Commerce API Settings → Data API:\n\n\`\`\`json\n${ocapi}\n\`\`\``, + ...(isImplicit ? ['User auth opens a browser per session — fine for development, not for CI.'] : []), + ], + verifyCommand: 'b2c job search', + }; + }, +}); diff --git a/docs/.vitepress/data/adventures/logs.ts b/docs/.vitepress/data/adventures/logs.ts new file mode 100644 index 000000000..6029e82fd --- /dev/null +++ b/docs/.vitepress/data/adventures/logs.ts @@ -0,0 +1,127 @@ +// Adventure: Tail and search logs (b2c logs). + +import {choice, defineAdventure, doc, md, step} from './_authoring.js'; +import {check, dwJson, link} from './_helpers.js'; + +export const logsAdventure = defineAdventure({ + id: 'logs', + title: 'Tail and search logs', + tagline: 'Stream and filter B2C Commerce instance logs from the terminal.', + icon: 'mdi:text-search', + tags: ['logs', 'webdav', 'debugging'], + priority: 'common', + intro: + 'Logs commands (b2c logs tail, get, list) read log files over WebDAV. You only need WebDAV credentials — no OCAPI configuration required.', + + steps: [ + step('webdav', { + title: 'How will WebDAV authenticate?', + subtitle: "Logs are read directly from the instance's WebDAV log share.", + doc: doc('/guide/authentication', 'webdav-access', 'WebDAV Access'), + choices: [ + choice('basic', { + title: 'BM username + access key', + subtitle: 'Recommended', + icon: 'mdi:key-outline', + badges: [{text: 'Quick', tone: 'quick'}], + body: md`Generate a WebDAV access key for your Business Manager user. Fastest path to a working \`b2c logs tail\`.`, + contributes: {webdavAuth: 'basic'}, + }), + choice('oauth', { + title: 'OAuth (client credentials)', + subtitle: 'Reuse an Account Manager client', + icon: 'mdi:key-variant', + body: md`Use an Account Manager API client. Requires WebDAV Client Permissions configured in BM for your \`client_id\`.`, + contributes: {webdavAuth: 'oauth'}, + }), + ], + }), + + step('persistence', { + title: 'How should the CLI find your config?', + doc: doc('/guide/configuration', 'configuration-file', 'Configuration file (dw.json)'), + choices: [ + choice('dw-json', { + title: 'dw.json (project root)', + subtitle: 'Recommended', + icon: 'mdi:file-cog-outline', + body: md`Per-project config file with walk-up discovery from the current directory.`, + contributes: {configSource: 'dw-json'}, + }), + choice('env', { + title: '.env / environment variables', + subtitle: 'CI-friendly', + icon: 'mdi:console-line', + body: md`Use \`SFCC_*\` env vars; the CLI auto-loads a \`.env\` file.`, + contributes: {configSource: 'env'}, + }), + ], + }), + ], + + synthesize(state) { + const useBasic = state.webdavAuth === 'basic'; + const useEnv = state.configSource === 'env'; + + const dw = useEnv + ? '# Using environment variables — see .env tab below.' + : dwJson({ + hostname: true, + username: useBasic, + password: useBasic, + clientId: !useBasic, + clientSecret: !useBasic, + }); + + const envLines = [ + 'SFCC_SERVER=.dx.commercecloud.salesforce.com', + useBasic ? 'SFCC_USERNAME=' : '', + useBasic ? 'SFCC_PASSWORD=' : '', + !useBasic ? 'SFCC_CLIENT_ID=' : '', + !useBasic ? 'SFCC_CLIENT_SECRET=' : '', + ] + .filter(Boolean) + .join('\n'); + + const checklist = [ + ...(useBasic + ? [ + check( + 'Generate a WebDAV access key for your BM user', + link('/guide/authentication', 'option-a-basic-authentication-user-access', 'Basic Authentication'), + ), + ] + : [ + check( + 'Create an Account Manager API client', + link('/guide/authentication', 'creating-an-api-client', 'Creating an API Client'), + ), + check( + 'Configure WebDAV Client Permissions in Business Manager', + link('/guide/authentication', 'option-b-oauth-based-webdav-api-client-access', 'OAuth-based WebDAV'), + ), + ]), + check( + useEnv ? 'Set SFCC_* environment variables' : 'Save the dw.json snippet to your project root', + link( + '/guide/configuration', + useEnv ? 'environment-variables' : 'configuration-file', + useEnv ? 'Environment Variables' : 'Configuration File', + ), + ), + ]; + + const warnings: string[] = [ + 'Filter aggressively to avoid noise. Example — recent ERROR entries containing "OutOfMemory":\n\n```bash\nb2c logs get --level error --since 1h --search "OutOfMemory"\n```', + 'Tail in real-time with rich filtering — runs until Ctrl+C:\n\n```bash\nb2c logs tail --filter customerror --level ERROR --search "OrderMgr"\n```', + ]; + + return { + dwJson: dw, + env: useEnv ? envLines : undefined, + checklist, + warnings, + verifyCommand: 'b2c logs get --count 10', + }; + }, +}); diff --git a/docs/.vitepress/data/adventures/migrate-sfcc-ci.ts b/docs/.vitepress/data/adventures/migrate-sfcc-ci.ts new file mode 100644 index 000000000..5f0cc5366 --- /dev/null +++ b/docs/.vitepress/data/adventures/migrate-sfcc-ci.ts @@ -0,0 +1,359 @@ +// Adventure: Migrate from sfcc-ci. +// +// Mirrors docs/guide/sfcc-ci-migration.md. The synthesized output's most +// valuable artifact is the side-by-side command translation table, plus a +// CI snippet showing the new env-var-based auth pattern. + +import {choice, defineAdventure, doc, md, step} from './_authoring.js'; +import {check, dwJson, link} from './_helpers.js'; + +// Side-by-side translation table rows, grouped by command surface. +const ROWS_BY_COMMAND: Record = { + auth: [ + '| `sfcc-ci client:auth ` | `b2c auth client --client-id --client-secret ` |', + '| `sfcc-ci client:auth:renew` | `b2c auth client renew` |', + '| `sfcc-ci client:auth:token` | `b2c auth client token` |', + ], + code: [ + '| `sfcc-ci code:list` | `b2c code list` |', + '| `sfcc-ci code:deploy ` | `b2c code deploy --activate` |', + '| `sfcc-ci code:activate ` | `b2c code activate ` |', + '| `sfcc-ci code:delete` | `b2c code delete` |', + ], + instance: [ + '| `sfcc-ci instance:upload ` | `b2c webdav put ` |', + '| `sfcc-ci instance:import ` | `b2c webdav put /Impex/src/instance/` then `b2c job run sfcc-site-archive-import --wait` |', + '| `sfcc-ci instance:export` | `b2c content export` |', + ], + job: [ + '| `sfcc-ci job:run ` | `b2c job run --wait` |', + '| `sfcc-ci job:status ` | `b2c job wait ` |', + ], + sandbox: [ + '| `sfcc-ci sandbox:list` | `b2c sandbox list` |', + '| `sfcc-ci sandbox:create` | `b2c sandbox create` |', + '| `sfcc-ci sandbox:delete` | `b2c sandbox delete` |', + '| `sfcc-ci sandbox:reset` | `b2c sandbox reset` |', + ], +}; + +const SURFACE_ROWS: Record = { + 'code-deploy': [...ROWS_BY_COMMAND.auth, ...ROWS_BY_COMMAND.code], + 'data-import': [...ROWS_BY_COMMAND.auth, ...ROWS_BY_COMMAND.instance, ...ROWS_BY_COMMAND.job], + jobs: [...ROWS_BY_COMMAND.auth, ...ROWS_BY_COMMAND.job], + sandbox: [...ROWS_BY_COMMAND.auth, ...ROWS_BY_COMMAND.sandbox], + all: [ + ...ROWS_BY_COMMAND.auth, + ...ROWS_BY_COMMAND.code, + ...ROWS_BY_COMMAND.instance, + ...ROWS_BY_COMMAND.job, + ...ROWS_BY_COMMAND.sandbox, + ], +}; + +export const migrateSfccCiAdventure = defineAdventure({ + id: 'migrate-sfcc-ci', + title: 'Migrate from sfcc-ci', + tagline: 'Move from the legacy sfcc-ci tool to the B2C CLI without breaking your CI/CD pipelines.', + icon: 'mdi:swap-horizontal-bold', + tags: ['migration', 'sfcc-ci', 'automation'], + priority: 'specialized', + intro: + 'sfcc-ci is deprecated. The B2C CLI is its drop-in replacement — colon-syntax aliases (b2c code:deploy), legacy env var names (SFCC_OAUTH_CLIENT_ID), and the dw.json file all keep working. This adventure picks the right migration path for your workflow and produces a side-by-side command translation table plus a before/after CI snippet.', + + steps: [ + step('surface', { + title: 'Which sfcc-ci commands are you replacing?', + subtitle: + 'Pick the workflow that matches most of your existing pipeline. The translation table will be focused on this surface.', + doc: doc('/guide/sfcc-ci-migration', 'command-mapping', 'Command Mapping'), + choices: [ + choice('code-deploy', { + title: 'Code deploy + activate', + subtitle: 'code:deploy / code:activate', + icon: 'mdi:cloud-upload-outline', + badges: [{text: 'Most common', tone: 'quick'}], + body: md` + Build, upload, and activate cartridges with \`b2c code deploy --activate\`. + See the [Code Management](/guide/sfcc-ci-migration#code-management) + section of the migration guide. + `, + contributes: {surface: 'code-deploy'}, + }), + choice('data-import', { + title: 'Data / site import + export', + subtitle: 'instance:upload / instance:import', + icon: 'mdi:database-arrow-up-outline', + body: md` + Replace \`sfcc-ci instance:*\` with \`b2c webdav put\` + + \`b2c job run sfcc-site-archive-import\`. + `, + contributes: {surface: 'data-import'}, + }), + choice('jobs', { + title: 'Job orchestration', + subtitle: 'job:run / job:status', + icon: 'mdi:cog-play-outline', + body: md`Trigger and wait on B2C jobs from CI with \`b2c job run --wait\`.`, + contributes: {surface: 'jobs'}, + }), + choice('sandbox', { + title: 'Sandbox lifecycle', + subtitle: 'sandbox:create / sandbox:reset', + icon: 'mdi:flask-outline', + body: md`Provision and tear down On-Demand Sandboxes with \`b2c sandbox\`.`, + contributes: {surface: 'sandbox'}, + }), + choice('all', { + title: 'All of the above', + subtitle: 'Full pipeline migration', + icon: 'mdi:format-list-checks', + body: md`Get the complete translation table covering every surface.`, + contributes: {surface: 'all'}, + }), + ], + }), + + step('auth', { + title: 'How should the CLI authenticate?', + subtitle: + 'sfcc-ci defaulted to a stateful flow (client:auth stores a token). The B2C CLI supports that pattern but recommends stateless env-var auth for CI.', + doc: doc('/guide/sfcc-ci-migration', 'authentication', 'Authentication'), + choices: [ + choice('stateless', { + title: 'Stateless · env vars', + subtitle: 'Recommended for CI/CD', + icon: 'mdi:console-line', + badges: [{text: 'Recommended', tone: 'info'}], + body: md` + Set \`SFCC_CLIENT_ID\` / \`SFCC_CLIENT_SECRET\` in your runner's + secret store — no separate \`client:auth\` step. See + [Stateless Auth](/guide/sfcc-ci-migration#stateless-auth-recommended-for-cicd). + `, + contributes: {authMode: 'stateless'}, + }), + choice('stateful', { + title: 'Stateful · b2c auth client', + subtitle: 'Closest sfcc-ci analog', + icon: 'mdi:key-variant', + badges: [{text: 'Drop-in', tone: 'quick'}], + body: md` + Mirror sfcc-ci's \`client:auth\` with \`b2c auth client --renew\` — + stores a token, reused across commands. See + [b2c auth client](/cli/auth#b2c-auth-client). + `, + contributes: {authMode: 'stateful'}, + }), + choice('dw-json', { + title: 'dw.json at project root', + subtitle: 'Local dev / cartridge repos', + icon: 'mdi:file-cog-outline', + body: md` + Keep the existing \`dw.json\` file — both kebab-case (\`client-id\`) + and camelCase (\`clientId\`) keys are accepted by the CLI's config + reader. + `, + contributes: {authMode: 'dw-json'}, + }), + ], + }), + + step('safety', { + title: 'What safety level should production enforce?', + subtitle: + 'sfcc-ci had no destructive-op guard. The B2C CLI lets you gate destructive calls per environment — strongly recommended for production CI.', + doc: doc('/guide/safety', 'safety-levels', 'Safety Levels'), + choices: [ + choice('none', { + title: 'NONE', + subtitle: 'No restrictions (sfcc-ci parity)', + icon: 'mdi:lock-open-variant-outline', + body: md`Matches sfcc-ci's behavior. Acceptable for ephemeral sandbox pipelines.`, + contributes: {safetyLevel: 'NONE'}, + }), + choice('no-delete', { + title: 'NO_DELETE', + subtitle: 'Block DELETE operations', + icon: 'mdi:lock-outline', + badges: [{text: 'Recommended', tone: 'info'}], + body: md`A reasonable default — uploads and activations still work, but accidental deletions are blocked.`, + contributes: {safetyLevel: 'NO_DELETE'}, + }), + choice('no-update', { + title: 'NO_UPDATE', + subtitle: 'Block deletes + reset/stop/restart', + icon: 'mdi:shield-lock-outline', + body: md`Stricter — preserves the ability to deploy code while blocking destructive admin ops.`, + contributes: {safetyLevel: 'NO_UPDATE'}, + }), + choice('read-only', { + title: 'READ_ONLY', + subtitle: 'Block all writes', + icon: 'mdi:eye-lock-outline', + body: md`For audit / verification jobs that should never modify the instance.`, + contributes: {safetyLevel: 'READ_ONLY'}, + }), + ], + }), + ], + + synthesize(state) { + const surface = (state.surface as string) ?? 'code-deploy'; + const authMode = (state.authMode as string) ?? 'stateless'; + const safetyLevel = (state.safetyLevel as string) ?? 'NO_DELETE'; + + const isStateful = authMode === 'stateful'; + const isDwJson = authMode === 'dw-json'; + const isStateless = authMode === 'stateless'; + + // Configuration block: dw.json for the dw-json branch, otherwise a comment + // pointing at the .env tab. + const dw = isDwJson + ? dwJson({ + hostname: true, + username: true, + password: true, + clientId: true, + clientSecret: true, + codeVersion: true, + }) + : '# sfcc-ci users moving to CI prefer environment variables — see the .env tab.'; + + const envLines = [ + 'SFCC_SERVER=.dx.commercecloud.salesforce.com', + 'SFCC_CLIENT_ID=', + 'SFCC_CLIENT_SECRET=', + 'SFCC_CODE_VERSION=version1', + 'SFCC_USERNAME=', + 'SFCC_PASSWORD=', + `SFCC_SAFETY_LEVEL=${safetyLevel}`, + '', + '# sfcc-ci compatibility — these legacy names are still accepted:', + '# SFCC_OAUTH_CLIENT_ID, SFCC_OAUTH_CLIENT_SECRET, SFCC_LOGIN_URL', + ].join('\n'); + + const checklist = [ + check( + 'Install the B2C CLI: `npm install -g @salesforce/b2c-cli`', + link('/guide/installation', undefined, 'Installation'), + ), + ...(isStateful + ? [ + check( + 'Replace `sfcc-ci client:auth` with `b2c auth client --renew` to keep the stored-token workflow', + link('/cli/auth', 'b2c-auth-client', 'b2c auth client'), + ), + ] + : []), + ...(isStateless + ? [ + check( + 'Drop the `sfcc-ci client:auth` step entirely — exporting `SFCC_CLIENT_ID` / `SFCC_CLIENT_SECRET` is enough', + link('/guide/sfcc-ci-migration', 'stateless-auth-recommended-for-cicd', 'Stateless Auth'), + ), + ] + : []), + ...(isDwJson + ? [ + check( + 'Save the dw.json snippet at your project root — both kebab-case (`client-id`) and camelCase (`clientId`) keys are accepted', + link('/guide/configuration', 'configuration-file', 'Configuration File'), + ), + ] + : []), + check( + 'Rewrite CI scripts using the command translation table below', + link('/guide/sfcc-ci-migration', 'command-mapping', 'Command Mapping'), + ), + check( + `Set SFCC_SAFETY_LEVEL=${safetyLevel} in production env to gate destructive operations`, + link('/guide/safety', 'safety-levels', 'Safety Levels'), + ), + check( + 'Update existing env vars: `SFCC_OAUTH_CLIENT_ID`, `SFCC_OAUTH_CLIENT_SECRET`, and `SFCC_LOGIN_URL` are still accepted as aliases', + link('/guide/sfcc-ci-migration', 'environment-variables', 'Environment Variables'), + ), + ]; + + // Side-by-side translation table. Curated for the picked surface so the + // table stays focused on commands the user actually runs today. + const tableHeader = '| sfcc-ci | B2C CLI |\n|---------|---------|'; + const tableRows = (SURFACE_ROWS[surface] ?? SURFACE_ROWS['code-deploy']).join('\n'); + const translationTable = `${tableHeader}\n${tableRows}`; + + // CI snippet: the canonical "before / after" for the chosen auth mode. + const ciLines: string[] = [ + '# Before (sfcc-ci):', + 'sfcc-ci client:auth $SFCC_OAUTH_CLIENT_ID $SFCC_OAUTH_CLIENT_SECRET', + ]; + if (surface === 'code-deploy' || surface === 'all') { + ciLines.push('sfcc-ci code:deploy build/code.zip -i $INSTANCE', 'sfcc-ci code:activate v1 -i $INSTANCE'); + } else if (surface === 'data-import') { + ciLines.push('sfcc-ci instance:import site.zip -i $INSTANCE'); + } else if (surface === 'jobs') { + ciLines.push('sfcc-ci job:run ImportCatalogs -i $INSTANCE'); + } else if (surface === 'sandbox') { + ciLines.push('sfcc-ci sandbox:create -r '); + } + + ciLines.push('', '# After (B2C CLI):'); + if (isStateful) { + ciLines.push( + 'export SFCC_SERVER=$INSTANCE', + 'b2c auth client --client-id $SFCC_OAUTH_CLIENT_ID --client-secret $SFCC_OAUTH_CLIENT_SECRET --renew', + ); + } else if (isStateless) { + ciLines.push( + '# Just export env vars — no separate auth step', + 'export SFCC_SERVER=$INSTANCE', + 'export SFCC_CLIENT_ID=$SFCC_OAUTH_CLIENT_ID', + 'export SFCC_CLIENT_SECRET=$SFCC_OAUTH_CLIENT_SECRET', + ); + } else { + ciLines.push('# dw.json at project root supplies hostname + credentials'); + } + ciLines.push(`export SFCC_SAFETY_LEVEL=${safetyLevel}`); + + if (surface === 'code-deploy' || surface === 'all') { + ciLines.push('b2c code deploy --activate'); + } else if (surface === 'data-import') { + // No `b2c content import` command exists — upload via WebDAV then run the + // built-in site-archive-import job. + ciLines.push( + 'b2c webdav put site.zip /Impex/src/instance/site.zip', + 'b2c job run sfcc-site-archive-import --wait', + ); + } else if (surface === 'jobs') { + ciLines.push('b2c job run ImportCatalogs --wait'); + } else if (surface === 'sandbox') { + ciLines.push('b2c sandbox create --realm '); + } + + const warnings: string[] = [ + `**Command translation** — the most common sfcc-ci → B2C CLI replacements:\n\n${translationTable}\n\nThe full table lives in the [sfcc-ci migration guide](/guide/sfcc-ci-migration#command-mapping).`, + `**Before / after CI snippet:**\n\n\`\`\`bash\n${ciLines.join('\n')}\n\`\`\``, + 'Safety mode: sfcc-ci had no built-in destructive-op guard. Set `SFCC_SAFETY_LEVEL=NO_DELETE` (or stricter) in your production CI to opt into the new safety net — see [Safety Levels](/guide/safety#safety-levels).', + 'Legacy env var names (`SFCC_OAUTH_CLIENT_ID`, `SFCC_OAUTH_CLIENT_SECRET`, `SFCC_LOGIN_URL`) are still accepted, so existing pipelines keep working while you migrate.', + ]; + + if (isStateful) { + warnings.push( + 'Stateful mode stores the OAuth token under your home directory. Fine for local dev — for CI runners that spin up fresh per job, prefer the stateless (env-var) mode instead.', + ); + } + + if (safetyLevel === 'NONE') { + warnings.push( + "You picked SFCC_SAFETY_LEVEL=NONE — destructive operations are not blocked. That matches sfcc-ci's behavior, but consider raising to `NO_DELETE` for production.", + ); + } + + return { + dwJson: dw, + env: envLines, + checklist, + warnings, + verifyCommand: 'b2c code list', + }; + }, +}); diff --git a/docs/.vitepress/data/adventures/mrt-deploy.ts b/docs/.vitepress/data/adventures/mrt-deploy.ts new file mode 100644 index 000000000..37fbe7b39 --- /dev/null +++ b/docs/.vitepress/data/adventures/mrt-deploy.ts @@ -0,0 +1,217 @@ +// Adventure: Managed Runtime deployments (b2c mrt). + +import {choice, defineAdventure, doc, md, step} from './_authoring.js'; +import {check, link} from './_helpers.js'; + +export const mrtDeployAdventure = defineAdventure({ + id: 'mrt-deploy', + title: 'Managed Runtime deployments', + tagline: 'Configure projects, environments, and bundle deploys for PWA Kit and Storefront Next on Managed Runtime.', + icon: 'mdi:rocket-launch-outline', + tags: ['mrt', 'managed-runtime', 'pwa', 'deploy', 'storefront-next'], + priority: 'specialized', + intro: + "Managed Runtime commands talk to the MRT control plane with an API key (separate from Account Manager OAuth). You'll pick what you're deploying, where the API key lives, and whether deploys are run by hand or by CI.", + + steps: [ + step('project-type', { + title: 'What are you deploying?', + subtitle: 'Both project types use the same b2c mrt commands — only the build artifacts differ.', + doc: doc('/cli/mrt', 'command-overview', 'MRT command overview'), + choices: [ + choice('pwa-kit', { + title: 'PWA Kit v3', + subtitle: 'React storefront', + icon: 'mdi:react', + badges: [{text: 'Common', tone: 'quick'}], + body: md`Build with \`npm run build\`; deploy the \`build/\` directory with \`b2c mrt bundle deploy\`.`, + contributes: {projectType: 'pwa-kit'}, + }), + choice('storefront-next', { + title: 'Storefront Next', + subtitle: 'Next.js storefront (closed pilot)', + icon: 'simple-icons:nextdotjs', + badges: [{text: 'Pilot', tone: 'beta'}], + body: md`Build the Storefront Next app, then deploy its build output with \`b2c mrt bundle deploy\`. Access is currently limited to pilot customers — see [Storefront Next on Managed Runtime](/guide/storefront-next) for the full setup.`, + contributes: {projectType: 'storefront-next'}, + }), + choice('both', { + title: 'Both', + subtitle: 'Multiple frontends', + icon: 'mdi:layers-triple-outline', + body: md`Manage PWA Kit and Storefront Next under the same MRT account — one project per app, deployed independently.`, + contributes: {projectType: 'both'}, + }), + ], + }), + + step('credentials', { + title: 'Where should the CLI find your MRT API key?', + subtitle: 'MRT auth is separate from Account Manager OAuth — pick one place to keep the key.', + doc: doc('/guide/authentication', 'managed-runtime-api-key', 'Managed Runtime API Key'), + choices: [ + choice('mobify', { + title: '~/.mobify file', + subtitle: 'Saved with save-credentials', + icon: 'mdi:file-key-outline', + badges: [{text: 'Recommended', tone: 'info'}], + body: md`Run \`b2c mrt save-credentials --user --api-key \` once. The CLI reads \`~/.mobify\` for every subsequent \`b2c mrt\` call.`, + contributes: {credSource: 'mobify'}, + }), + choice('env', { + title: 'Environment variables', + subtitle: 'MRT_API_KEY / MRT_PROJECT / MRT_ENVIRONMENT', + icon: 'mdi:console-line', + badges: [{text: 'CI', tone: 'quick'}], + body: md`Export \`MRT_API_KEY\` (and optionally \`MRT_PROJECT\` / \`MRT_ENVIRONMENT\`) from your shell or your runner's secret store. Best for CI/CD.`, + contributes: {credSource: 'env'}, + }), + choice('flags', { + title: 'Flags on every command', + subtitle: '--api-key / --project / --environment', + icon: 'mdi:flag-outline', + body: md`Pass credentials and the target on every invocation. Useful when juggling multiple MRT accounts in one shell.`, + contributes: {credSource: 'flags'}, + }), + ], + }), + + step('trigger', { + title: 'Who runs `b2c mrt bundle deploy`?', + doc: doc('/cli/mrt', 'b2c-mrt-bundle-deploy', 'b2c mrt bundle deploy'), + choices: [ + choice('manual', { + title: 'Manual', + subtitle: 'Developer runs it locally', + icon: 'mdi:account-hard-hat-outline', + body: md`You build the bundle and run \`b2c mrt bundle deploy\` from your laptop. Add \`--wait\` to block until the deployment finishes.`, + contributes: {trigger: 'manual'}, + }), + choice('ci', { + title: 'CI / CD', + subtitle: 'GitHub Actions or other runner', + icon: 'mdi:source-branch', + body: md`A pipeline builds the storefront and runs \`b2c mrt bundle deploy\` with credentials from the runner's secret store. See the example workflow in the warnings panel.`, + contributes: {trigger: 'ci'}, + }), + ], + }), + ], + + synthesize(state) { + const projectType = (state.projectType as string) ?? 'pwa-kit'; + const credSource = (state.credSource as string) ?? 'mobify'; + const trigger = (state.trigger as string) ?? 'manual'; + + const isPwa = projectType === 'pwa-kit'; + const isNext = projectType === 'storefront-next'; + const isBoth = projectType === 'both'; + const useMobify = credSource === 'mobify'; + const useEnv = credSource === 'env'; + const useFlags = credSource === 'flags'; + const isCi = trigger === 'ci'; + + const dw = useMobify + ? '# MRT credentials live in ~/.mobify (written by `b2c mrt save-credentials`).\n# No dw.json fields are required for MRT commands.\n#\n# Optional: set `mrtProject` / `mrtEnvironment` in dw.json to default the\n# `--project` / `--environment` flags for a given checkout.' + : useFlags + ? '# Credentials passed per-command via `--api-key`, `--project`, `--environment` flags.\n# Nothing to put in dw.json — see the verify command at the bottom.' + : '# Using environment variables — see .env tab below.'; + + const envLines = [ + 'MRT_API_KEY=', + 'MRT_PROJECT=', + 'MRT_ENVIRONMENT=', + ].join('\n'); + + const buildStep = isPwa + ? 'Build your PWA Kit storefront (`npm run build`) so the `build/` directory contains the bundle to deploy' + : isNext + ? 'Build your Storefront Next app (`npm run build`) so the configured `--build-dir` contains the bundle to deploy' + : 'Build each storefront (`npm run build`) so its `build/` directory is ready to deploy'; + + const checklist = [ + check( + 'Get an MRT API key from the Managed Runtime dashboard', + link('/guide/authentication', 'getting-an-mrt-api-key', 'Getting an MRT API Key'), + ), + check( + useMobify + ? 'Save credentials with `b2c mrt save-credentials --user --api-key `' + : useEnv + ? 'Set `MRT_API_KEY` (and optionally `MRT_PROJECT` / `MRT_ENVIRONMENT`) in your shell or CI secret store' + : 'Pass `--api-key`, `--project`, and `--environment` on every `b2c mrt` invocation', + link('/guide/authentication', 'configuring-the-api-key', 'Configuring the API Key'), + ), + check( + 'Confirm the project exists with `b2c mrt project list` and pick (or create) an environment with `b2c mrt env list` / `b2c mrt env create`', + link('/cli/mrt', 'project-commands', 'Project Commands'), + ), + check( + buildStep, + isNext + ? link('/guide/storefront-next', 'step-5-deploy', 'Storefront Next: Deploy') + : link('/cli/mrt', 'b2c-mrt-bundle-deploy', 'b2c mrt bundle deploy'), + ), + check( + 'Run `b2c mrt bundle deploy --project --environment ` (add `--wait` to block until the deployment is live)', + link('/cli/mrt', 'b2c-mrt-bundle-deploy', 'b2c mrt bundle deploy'), + ), + check( + 'Tail real-time application logs with `b2c mrt tail-logs -p -e ` while validating the deploy', + link('/cli/mrt', 'b2c-mrt-tail-logs', 'b2c mrt tail-logs'), + ), + ]; + + const warnings: string[] = [ + "MRT API keys grant access to every project on your Managed Runtime account — treat them like a password and store them in your CI runner's secret store, never in `dw.json` or git.", + ]; + + if (useMobify) { + warnings.push( + 'The `~/.mobify` file is plaintext JSON. On shared workstations consider using `MRT_API_KEY` from a secret manager instead.', + ); + } + + if (isCi) { + const ciLines: string[] = [ + 'name: Deploy to MRT', + '', + 'on:', + ' push:', + ' branches: [main]', + '', + 'jobs:', + ' deploy:', + ' runs-on: ubuntu-latest', + ' env:', + ' MRT_API_KEY: ${{ secrets.MRT_API_KEY }}', + ' MRT_PROJECT: ', + ' MRT_ENVIRONMENT: ', + ' steps:', + ' - uses: actions/checkout@v4', + ' - uses: actions/setup-node@v4', + ' with:', + ' node-version: 22', + ' - run: npm ci', + ' - run: npm run build', + ' - run: npm install -g @salesforce/b2c-cli', + ' - run: b2c mrt bundle deploy --wait', + ]; + warnings.push(`Example GitHub Actions workflow:\n\n\`\`\`yaml\n${ciLines.join('\n')}\n\`\`\``); + } + + if (isNext || isBoth) { + warnings.push( + 'Storefront Next has its own end-to-end onboarding (SLAS client, environment vars, multi-site config). See [Storefront Next on Managed Runtime](/guide/storefront-next) for the full walkthrough.', + ); + } + + return { + dwJson: dw, + env: useEnv ? envLines : undefined, + checklist, + warnings, + verifyCommand: 'b2c mrt project list', + }; + }, +}); diff --git a/docs/.vitepress/data/adventures/multi-instance.ts b/docs/.vitepress/data/adventures/multi-instance.ts new file mode 100644 index 000000000..7c1f1146e --- /dev/null +++ b/docs/.vitepress/data/adventures/multi-instance.ts @@ -0,0 +1,256 @@ +// Adventure: Configure multiple instances. +// +// Walks the user through structuring `dw.json` with a `configs` array +// (named profiles) and choosing how to switch between them — the +// `--instance` flag, `SFCC_INSTANCE` env var, or the +// `b2c setup instance set-active` wizard. + +import {choice, defineAdventure, doc, md, step} from './_authoring.js'; +import {check, link} from './_helpers.js'; + +interface InstanceProfile { + authMode: 'mixed' | 'per-instance-oauth' | 'shared-oauth'; + codeVersion: string; + hostname: string; + name: string; +} + +const PROFILES_2: InstanceProfile[] = [ + {name: 'dev', hostname: '.dx.commercecloud.salesforce.com', codeVersion: 'version1', authMode: 'shared-oauth'}, + { + name: 'production', + hostname: '.dx.commercecloud.salesforce.com', + codeVersion: 'version1', + authMode: 'shared-oauth', + }, +]; + +const PROFILES_3: InstanceProfile[] = [ + {name: 'dev', hostname: '.dx.commercecloud.salesforce.com', codeVersion: 'version1', authMode: 'shared-oauth'}, + { + name: 'staging', + hostname: '.dx.commercecloud.salesforce.com', + codeVersion: 'version1', + authMode: 'shared-oauth', + }, + { + name: 'production', + hostname: '.dx.commercecloud.salesforce.com', + codeVersion: 'version1', + authMode: 'shared-oauth', + }, +]; + +// The shared `dwJson()` helper only emits a single-instance object. For this +// adventure we hand-build a top-level `configs` array with named profiles and +// an `"active": true` selector on the first entry. +function buildDwJson(profiles: InstanceProfile[], authStrategy: string): string { + const lines: string[] = ['{', ' "configs": [']; + profiles.forEach((p, idx) => { + const isActive = idx === 0; + const fields: string[] = []; + fields.push(` "name": "${p.name}"`); + if (isActive) fields.push(` "active": true`); + fields.push(` "hostname": "${p.hostname}"`); + fields.push(` "code-version": "${p.codeVersion}"`); + + if (authStrategy === 'shared-oauth') { + // Same OAuth credentials reused across all instances. + fields.push(` "client-id": ""`); + fields.push(` "client-secret": ""`); + } else if (authStrategy === 'per-instance-oauth') { + // Distinct OAuth credentials per instance. + const upper = p.name.toUpperCase(); + fields.push(` "client-id": "<${upper}_CLIENT_ID>"`); + fields.push(` "client-secret": "<${upper}_CLIENT_SECRET>"`); + } else { + // Mixed: basic auth on dev/staging, OAuth on prod. + if (p.name === 'production') { + fields.push(` "client-id": ""`); + fields.push(` "client-secret": ""`); + } else { + fields.push(` "username": "<${p.name.toUpperCase()}_BM_USERNAME>"`); + fields.push(` "password": "<${p.name.toUpperCase()}_WEBDAV_ACCESS_KEY>"`); + } + } + + lines.push(' {'); + lines.push(fields.join(',\n')); + lines.push(idx === profiles.length - 1 ? ' }' : ' },'); + }); + lines.push(' ]'); + lines.push('}'); + return lines.join('\n'); +} + +export const multiInstanceAdventure = defineAdventure({ + id: 'multi-instance', + title: 'Configure multiple instances', + tagline: 'Switch between dev, staging, and production with named profiles in dw.json.', + icon: 'mdi:swap-horizontal-circle-outline', + tags: ['configuration', 'dw-json', 'multi-instance'], + priority: 'common', + intro: + 'The CLI supports a `configs` array in `dw.json` so one project can target multiple B2C instances. Pick how many environments you need, how authentication is shared across them, and how you want to switch.', + + steps: [ + step('envCount', { + title: 'How many environments do you need?', + doc: doc('/guide/configuration', 'multiple-instances', 'Multiple Instances'), + choices: [ + choice('two', { + title: 'Two: dev + production', + subtitle: 'Minimal setup', + icon: 'mdi:numeric-2-circle-outline', + badges: [{text: 'Quick', tone: 'quick'}], + body: md`A development sandbox and a production instance. Good starting point for small teams.`, + contributes: {envCount: '2'}, + }), + choice('three', { + title: 'Three: dev + staging + production', + subtitle: 'Recommended', + icon: 'mdi:numeric-3-circle-outline', + badges: [{text: 'Recommended', tone: 'info'}], + body: md`Adds a staging tier so you can rehearse releases against production-like data before promoting code.`, + contributes: {envCount: '3'}, + }), + ], + }), + + step('authStrategy', { + title: 'How are credentials structured?', + subtitle: 'Each entry in the `configs` array can carry its own auth fields.', + doc: doc('/guide/configuration', 'multiple-instances', 'Multiple Instances'), + choices: [ + choice('shared-oauth', { + title: 'One OAuth client, all instances', + subtitle: 'Simplest', + icon: 'mdi:key-link', + body: md`Reuse the same Account Manager \`client-id\` / \`client-secret\` across every entry. Quick to set up; relies on AM tenant filters for scoping.`, + contributes: {authStrategy: 'shared-oauth'}, + }), + choice('per-instance-oauth', { + title: 'Separate OAuth client per instance', + subtitle: 'Stronger isolation', + icon: 'mdi:key-chain-variant', + badges: [{text: 'Recommended', tone: 'info'}], + body: md`Each instance gets its own \`client-id\` / \`client-secret\`. Lets you rotate or revoke production credentials without disturbing dev.`, + contributes: {authStrategy: 'per-instance-oauth'}, + }), + choice('mixed', { + title: 'Mixed: basic auth on lower envs, OAuth on prod', + subtitle: 'Pragmatic', + icon: 'mdi:shield-half-full', + body: md`WebDAV username + access key on dev/staging for fast uploads, OAuth client credentials on production for full OCAPI access.`, + contributes: {authStrategy: 'mixed'}, + }), + ], + }), + + step('switchMethod', { + title: 'How will you switch the active instance?', + doc: doc('/guide/configuration', 'switching-instances', 'Switching Instances'), + choices: [ + choice('set-active', { + title: '`b2c setup instance set-active `', + subtitle: 'Interactive default', + icon: 'mdi:cursor-default-click-outline', + badges: [{text: 'Recommended', tone: 'info'}], + body: md`Sets the \`"active": true\` flag in \`dw.json\`. Run with no argument for a searchable picker.`, + contributes: {switchMethod: 'set-active'}, + }), + choice('flag', { + title: '`--instance ` per command', + subtitle: 'One-off override', + icon: 'mdi:flag-outline', + body: md`Pass \`-i staging\` on a single command without changing the active default. Useful for ad-hoc inspection.`, + contributes: {switchMethod: 'flag'}, + }), + choice('env', { + title: '`SFCC_INSTANCE` env var', + subtitle: 'CI-friendly', + icon: 'mdi:console-line', + badges: [{text: 'CI', tone: 'quick'}], + body: md`Set \`SFCC_INSTANCE=production\` for the whole shell or pipeline job. Keeps the active selection out of source control.`, + contributes: {switchMethod: 'env'}, + }), + ], + }), + ], + + synthesize(state) { + const envCount = (state.envCount as string) ?? '3'; + const authStrategy = (state.authStrategy as string) ?? 'shared-oauth'; + const switchMethod = (state.switchMethod as string) ?? 'set-active'; + + const profiles = envCount === '2' ? PROFILES_2 : PROFILES_3; + const dw = buildDwJson(profiles, authStrategy); + + // For CI-style switching, suggest SFCC_INSTANCE in env tab. + const env = + switchMethod === 'env' + ? '# Pick the active instance for this shell / CI job.\nSFCC_INSTANCE=production' + : undefined; + + const checklist = [ + check( + 'Structure dw.json with a `configs` array of named instances', + link('/guide/configuration', 'multiple-instances', 'Multiple Instances'), + ), + check( + 'Add instances interactively with `b2c setup instance create `', + link('/guide/configuration', 'quick-setup', 'Quick Setup'), + ), + check( + 'List configured instances with `b2c setup instance list`', + link('/guide/configuration', 'listing-and-removing', 'Listing and Removing'), + ), + ...(switchMethod === 'set-active' + ? [ + check( + 'Switch the default with `b2c setup instance set-active `', + link('/guide/configuration', 'switching-instances', 'Switching Instances'), + ), + ] + : []), + ...(switchMethod === 'flag' + ? [ + check( + 'Use the `-i` / `--instance ` flag to override per command', + link('/guide/configuration', 'multiple-instances', 'Multiple Instances'), + ), + ] + : []), + ...(switchMethod === 'env' + ? [ + check( + 'Set `SFCC_INSTANCE=` in your shell or CI environment', + link('/guide/configuration', 'environment-variables', 'Environment Variables'), + ), + ] + : []), + ]; + + const warnings: string[] = [ + 'Tip: keep a separate `code-version` per instance if your environments deploy to different active versions (e.g., `version_dev` on dev, `version_prod` on production).', + ]; + if (switchMethod !== 'env') { + warnings.push( + 'For CI pipelines, prefer `SFCC_INSTANCE=production` (or `--instance production`) over committing `"active": true` on the production profile — it keeps the active instance out of source control.', + ); + } + if (authStrategy === 'shared-oauth') { + warnings.push( + 'Sharing one client across environments is convenient, but for production consider a dedicated Account Manager API client with tighter scopes and a tenant filter.', + ); + } + + return { + dwJson: dw, + env, + checklist, + warnings, + verifyCommand: 'b2c setup instance list', + }; + }, +}); diff --git a/docs/.vitepress/data/adventures/page-designer.ts b/docs/.vitepress/data/adventures/page-designer.ts new file mode 100644 index 000000000..1ade74274 --- /dev/null +++ b/docs/.vitepress/data/adventures/page-designer.ts @@ -0,0 +1,185 @@ +// Adventure: Page Designer content (b2c content + VS Code Content Libraries tree). + +import {choice, defineAdventure, doc, md, step} from './_authoring.js'; +import {check, dwJson, link, ocapiConfig, scopes} from './_helpers.js'; + +export const pageDesignerAdventure = defineAdventure({ + id: 'page-designer', + title: 'Page Designer content', + tagline: + 'Use b2c content and the VS Code Content Libraries tree to inspect, export, and edit Page Designer pages.', + icon: 'mdi:view-grid-outline', + tags: ['content', 'page-designer', 'webdav', 'ocapi', 'vscode'], + priority: 'common', + intro: + 'Page Designer tooling reads library XML over WebDAV and uses an OCAPI Jobs export to fetch fresh content. You configure a library list once, and both the CLI and the VS Code extension pick it up.', + + steps: [ + step('surface', { + title: 'Where will you work with content?', + doc: doc('/vscode-extension/', undefined, 'VS Code Extension'), + choices: [ + choice('cli', { + title: 'CLI (b2c content)', + subtitle: 'Terminal', + icon: 'mdi:console-line', + body: md`Run \`b2c content list / export / validate\` from a shell.`, + contributes: {surface: 'cli'}, + }), + choice('vscode', { + title: 'VS Code extension', + subtitle: 'Library Explorer tree', + icon: 'mdi:microsoft-visual-studio-code', + badges: [{text: 'Best UX', tone: 'info'}], + body: md`Visual tree view with search and round-trip XML editing.`, + contributes: {surface: 'vscode'}, + }), + choice('both', { + title: 'Both', + icon: 'mdi:swap-horizontal', + body: md`Configure once; the same \`dw.json\` drives both surfaces.`, + contributes: {surface: 'both'}, + }), + ], + }), + + step('library', { + title: 'What kind of library?', + doc: doc('/guide/configuration', 'content-libraries-example', 'Content Libraries Example'), + choices: [ + choice('shared', { + title: 'Shared library', + subtitle: 'Org-level', + icon: 'mdi:folder-outline', + body: md`A library that multiple sites can reference.`, + contributes: {libraryKind: 'shared'}, + }), + choice('site', { + title: 'Site library', + subtitle: 'Site-private', + icon: 'mdi:folder-account-outline', + body: md`A site-private library — the library ID is the site ID.`, + contributes: {libraryKind: 'site'}, + }), + choice('mixed', { + title: 'Mixed (multiple libraries)', + icon: 'mdi:folder-multiple-outline', + body: md`List several libraries; mark site-private ones explicitly.`, + contributes: {libraryKind: 'mixed'}, + }), + ], + }), + + step('auth', { + title: 'How will you authenticate?', + doc: doc('/guide/authentication', 'account-manager-api-client', 'Account Manager API Client'), + choices: [ + choice('client-credentials', { + title: 'Client Credentials', + subtitle: 'Recommended', + icon: 'mdi:key-variant', + body: md`API client with secret. Same client covers OAuth + OCAPI Jobs.`, + contributes: {authMethod: 'client-credentials'}, + }), + choice('jwt', { + title: 'JWT Bearer', + subtitle: 'Certificate-based', + icon: 'mdi:certificate-outline', + body: md`Cert pair instead of client secret.`, + contributes: {authMethod: 'jwt'}, + }), + ], + }), + + step('persistence', { + title: 'Where should the libraries config live?', + doc: doc('/guide/configuration', 'content-libraries-example', 'Content Libraries Example'), + choices: [ + choice('dw-json', { + title: 'dw.json (instance-scoped)', + subtitle: 'Recommended', + icon: 'mdi:file-cog-outline', + body: md`Lives next to credentials. Good if libraries vary per environment.`, + contributes: {configSource: 'dw-json'}, + }), + choice('package-json', { + title: 'package.json (project-scoped)', + subtitle: 'Shareable via VCS', + icon: 'mdi:package-variant-closed', + body: md`Use the \`b2c\` block in \`package.json\` so the whole team picks it up.`, + contributes: {configSource: 'package-json'}, + }), + ], + }), + ], + + synthesize(state) { + const isJwt = state.authMethod === 'jwt'; + const usingPackage = state.configSource === 'package-json'; + + let libraries: string[]; + if (state.libraryKind === 'site') { + libraries = ['']; + } else if (state.libraryKind === 'mixed') { + libraries = ['', '']; + } else { + libraries = ['']; + } + + const dw = usingPackage + ? dwJson({hostname: true, clientId: true, clientSecret: !isJwt, username: true, password: true}) + : dwJson({hostname: true, clientId: true, clientSecret: !isJwt, username: true, password: true, libraries}); + + const ocapiSnippet = ocapiConfig('', ['jobs', 'sites']); + + const warnings: string[] = [ + `Add this OCAPI Data API config in Business Manager so the site-export job can run:\n\n\`\`\`json\n${ocapiSnippet}\n\`\`\``, + ]; + if (state.libraryKind === 'site' || state.libraryKind === 'mixed') { + warnings.push( + 'For site-private libraries use the object form so commands default --site-library correctly:\n\n```json\n{ "libraries": [{"id": "", "siteLibrary": true}] }\n```', + ); + } + if (state.surface === 'vscode' || state.surface === 'both') { + warnings.push( + 'Enable the Content Libraries tree in VS Code: set `b2c-dx.features.contentLibraries: true` in `.vscode/settings.json` and reload the window.', + ); + } + + return { + dwJson: dw, + checklist: [ + check( + 'Create an Account Manager API client', + link('/guide/authentication', 'creating-an-api-client', 'Creating an API Client'), + ), + check( + `Add Default Scopes: ${scopes('baseline')}`, + link('/guide/authentication', 'configuring-scopes', 'Configuring Scopes'), + ), + check( + 'Enable Jobs and Sites in OCAPI Data API', + link('/guide/authentication', 'ocapi-configuration', 'OCAPI Configuration'), + ), + check( + 'Generate a WebDAV access key (BM username + key)', + link('/guide/authentication', 'option-a-basic-authentication-user-access', 'Basic Authentication'), + ), + check( + usingPackage ? 'Add a `libraries` array under the `b2c` key in package.json' : 'List your libraries in dw.json', + link('/guide/configuration', 'content-libraries-example', 'Content Libraries Example'), + ), + ...(state.surface === 'vscode' || state.surface === 'both' + ? [ + check( + 'Install the B2C DX VS Code extension', + link('/vscode-extension/installation', undefined, 'VS Code Extension Installation'), + ), + ] + : []), + ], + warnings, + verifyCommand: 'b2c content list --library ', + }; + }, +}); diff --git a/docs/.vitepress/data/adventures/sandbox.ts b/docs/.vitepress/data/adventures/sandbox.ts new file mode 100644 index 000000000..488ac8358 --- /dev/null +++ b/docs/.vitepress/data/adventures/sandbox.ts @@ -0,0 +1,182 @@ +// Adventure: Manage sandboxes (b2c sandbox). +// +// Sandbox commands authenticate via the CLI's built-in public client by +// default, so the simplest path requires zero configuration. This wizard +// branches on auth method (browser / client credentials / JWT) and on how +// the user wants to supply the realm and (optional) credentials. + +import {choice, defineAdventure, doc, md, step} from './_authoring.js'; +import {check, dwJson, link} from './_helpers.js'; + +export const sandboxAdventure = defineAdventure({ + id: 'sandbox', + title: 'Manage sandboxes', + tagline: 'Create, start/stop, and delete on-demand sandboxes from the CLI.', + icon: 'mdi:flask-empty-outline', + tags: ['sandbox', 'ods', 'oauth', 'realm'], + priority: 'core', + intro: + "Sandbox commands work out of the box — the CLI ships with a built-in public client that authenticates you via your browser. For automation or CI you can swap in your own Account Manager API client.", + + steps: [ + step('auth', { + title: 'How will you authenticate?', + doc: doc('/guide/authentication', 'account-manager-api-client', 'Account Manager API Client'), + choices: [ + choice('implicit', { + title: 'Browser login (default)', + subtitle: 'Zero config', + icon: 'mdi:account-arrow-right-outline', + badges: [{text: 'Quick', tone: 'quick'}], + body: md`Use the CLI's built-in public client. \`b2c sandbox list\` opens a browser window for login on first use. Your user account just needs the \`Sandbox API User\` role with a tenant filter.`, + contributes: {authMethod: 'implicit'}, + }), + choice('client-credentials', { + title: 'Client Credentials', + subtitle: 'Recommended for CI/CD', + icon: 'mdi:key-variant', + badges: [{text: 'CI', tone: 'quick'}], + body: md`Account Manager API client with a client secret. Non-interactive — assign the \`Sandbox API User\` role on the client and configure a tenant filter for the realm.`, + contributes: {authMethod: 'client-credentials'}, + }), + choice('jwt', { + title: 'JWT Bearer', + subtitle: 'Certificate-based', + icon: 'mdi:certificate-outline', + body: md`Use a public/private cert pair instead of a secret. See [JWT setup](/guide/authentication#jwt-authentication-certificate-based).`, + contributes: {authMethod: 'jwt'}, + }), + ], + }), + + step('persistence', { + title: 'How should the CLI find your realm and credentials?', + subtitle: 'Sandbox commands need a realm — either stored in config or passed via --realm.', + doc: doc('/guide/configuration', 'configuration-file', 'Configuration file (dw.json)'), + choices: [ + choice('dw-json', { + title: 'dw.json (project root)', + subtitle: 'Recommended', + icon: 'mdi:file-cog-outline', + badges: [{text: 'Recommended', tone: 'info'}], + body: md`Stores \`realm\` (and optional credentials) in a per-project file. Walk-up discovery from the current directory.`, + contributes: {configSource: 'dw-json'}, + }), + choice('env', { + title: '.env / environment variables', + subtitle: 'Credentials only — pass --realm per command', + icon: 'mdi:console-line', + body: md`Use \`SFCC_CLIENT_ID\` / \`SFCC_CLIENT_SECRET\` for OAuth credentials. There is no env var for \`--realm\`, so pass it on each \`b2c sandbox\` invocation (or store it in \`dw.json\`).`, + contributes: {configSource: 'env'}, + }), + choice('flags', { + title: 'Flags on every command', + subtitle: 'No persistence', + icon: 'mdi:flag-outline', + body: md`Pass \`--realm \` on every \`b2c sandbox\` invocation. Useful for one-off scripts or when juggling multiple realms.`, + contributes: {configSource: 'flags'}, + }), + ], + }), + ], + + synthesize(state) { + const authMethod = String(state.authMethod ?? 'implicit'); + const isImplicit = authMethod === 'implicit'; + const isJwt = authMethod === 'jwt'; + const isClientCreds = authMethod === 'client-credentials'; + const useEnv = state.configSource === 'env'; + const useFlags = state.configSource === 'flags'; + + // Realm is only persisted when the user picks a stored config source. + const persistRealm = !useFlags; + + let dw: string; + if (useFlags) { + dw = '# No config file needed — pass --realm on every command.\n# Example:\n# b2c sandbox list --realm \n# b2c sandbox create --realm '; + } else if (useEnv) { + dw = '# Using environment variables — see .env tab below.'; + } else { + dw = dwJson({ + realm: persistRealm, + clientId: isClientCreds || isJwt, + clientSecret: isClientCreds, + }); + } + + // There is no env var that fills `--realm` for sandbox commands. Env mode + // covers the OAuth credentials only — the realm itself must be in dw.json + // or on the flag. + const env = useEnv + ? [ + '# --realm is not env-configurable; pass it on each command or use dw.json', + isClientCreds || isJwt ? 'SFCC_CLIENT_ID=' : '', + isClientCreds ? 'SFCC_CLIENT_SECRET=' : '', + isJwt ? 'SFCC_JWT_CERT=./cert.pem' : '', + isJwt ? 'SFCC_JWT_KEY=./key.pem' : '', + ] + .filter(Boolean) + .join('\n') + : undefined; + + const checklist = [ + ...(isImplicit + ? [ + check( + 'Confirm your user has the Sandbox API User role with a tenant filter', + link('/guide/authentication', 'configuring-tenant-filter', 'Configuring Tenant Filter'), + ), + ] + : [ + check( + 'Create an Account Manager API client', + link('/guide/authentication', 'creating-an-api-client', 'Creating an API Client'), + ), + check( + 'Assign the Sandbox API User role to the API client', + link('/guide/authentication', 'for-client-credentials-roles-on-api-client', 'Roles for Client Credentials'), + ), + check( + 'Add a tenant filter for your realm on the Sandbox API User role', + link('/guide/authentication', 'configuring-tenant-filter', 'Configuring Tenant Filter'), + ), + ]), + ...(useFlags + ? [ + check( + 'Pass --realm on every sandbox command (or use stored config)', + link('/cli/sandbox', undefined, 'Sandbox Commands'), + ), + ] + : [ + check( + useEnv ? 'Set SFCC_* environment variables' : 'Save the dw.json snippet to your project root', + link( + '/guide/configuration', + useEnv ? 'environment-variables' : 'configuration-file', + useEnv ? 'Environment Variables' : 'Configuration File', + ), + ), + ]), + ]; + + const warnings: string[] = []; + if (isImplicit) { + warnings.push( + 'Browser auth opens a login window the first time and on token expiry. Great for local development; use client credentials for CI/CD.', + ); + } + + // `--realm` is required on sandbox create and recommended on list; the + // dw.json `realm` field is not yet wired as a default for these flags. + // Always include `--realm` in the verify command so the user sees the + // canonical invocation. + return { + dwJson: dw, + env, + checklist, + warnings, + verifyCommand: 'b2c sandbox list --realm ', + }; + }, +}); diff --git a/docs/.vitepress/data/adventures/scapi-access.ts b/docs/.vitepress/data/adventures/scapi-access.ts new file mode 100644 index 000000000..acb11cbd8 --- /dev/null +++ b/docs/.vitepress/data/adventures/scapi-access.ts @@ -0,0 +1,262 @@ +// Adventure: Configure SCAPI access (b2c scapi / b2c ecdn). + +import {choice, defineAdventure, doc, md, step} from './_authoring.js'; +import {check, dwJson, link, scopes} from './_helpers.js'; +import type {AdventureState} from './_types.js'; + +type ScopeBundle = 'baseline' | 'ecdnRead' | 'ecdnWrite' | 'replicationsRw' | 'scapiCustomApis' | 'scapiSchemas'; + +function selectedSurfaces(state: AdventureState): string[] { + const raw = state.surfaces; + return Array.isArray(raw) ? raw : []; +} + +function bundlesFor(surfaces: string[]): ScopeBundle[] { + const out = new Set(['baseline']); + if (surfaces.includes('ecdn')) { + out.add('ecdnRead'); + out.add('ecdnWrite'); + } + if (surfaces.includes('schemas')) out.add('scapiSchemas'); + if (surfaces.includes('custom-apis')) out.add('scapiCustomApis'); + if (surfaces.includes('replications')) out.add('replicationsRw'); + return Array.from(out); +} + +function pickVerifyCommand(surfaces: string[]): string { + // Prefer the lightest read-only command for whichever surface was picked. + // Replications has to come before schemas/custom because a replications-only + // client won't have those scopes. + if (surfaces.includes('ecdn')) return 'b2c ecdn zones list'; + if (surfaces.includes('replications')) return 'b2c scapi replications list'; + if (surfaces.includes('schemas')) return 'b2c scapi schemas list'; + if (surfaces.includes('custom-apis')) return 'b2c scapi custom status'; + return 'b2c auth client token'; +} + +export const scapiAccessAdventure = defineAdventure({ + id: 'scapi-access', + title: 'Configure SCAPI access', + tagline: 'Set up OAuth client + tenant for eCDN, Custom APIs, schemas, and replications.', + icon: 'mdi:api', + tags: ['scapi', 'oauth', 'ecdn', 'custom-apis', 'tenant-id'], + priority: 'common', + intro: + "SCAPI commands authenticate via Account Manager OAuth with the Salesforce Commerce API role, a tenant filter, and one or more sfcc.* scopes. Pick the surfaces you'll use and the wizard will compute the right scope set and config snippet.", + + steps: [ + step('surfaces', { + title: 'Which SCAPI surfaces will you use?', + subtitle: 'Pick one or more — the wizard computes the union of required scopes.', + multiSelect: true, + minPicks: 1, + doc: doc('/guide/authentication', 'scapi-authentication', 'SCAPI Authentication'), + choices: [ + choice('ecdn', { + title: 'eCDN', + subtitle: 'b2c ecdn zones / cache / certificates', + icon: 'mdi:cloud-outline', + body: md`Manage Cloudflare-backed eCDN zones, cache purges, certificates, and WAF rules. Adds \`sfcc.cdn-zones\` and \`sfcc.cdn-zones.rw\`.`, + contributes: {surfaces: ['ecdn']}, + }), + choice('schemas', { + title: 'SCAPI Schemas', + subtitle: 'b2c scapi schemas list / get', + icon: 'mdi:file-document-outline', + body: md`Discover and inspect Shopper / Admin SCAPI OpenAPI schemas. Adds \`sfcc.scapi-schemas\`.`, + contributes: {surfaces: ['schemas']}, + }), + choice('custom-apis', { + title: 'Custom APIs', + subtitle: 'b2c scapi custom status', + icon: 'mdi:application-braces-outline', + body: md`Inspect Custom API endpoint registration status across sites. Adds \`sfcc.custom-apis\`.`, + contributes: {surfaces: ['custom-apis']}, + }), + choice('replications', { + title: 'Replications', + subtitle: 'b2c scapi replications', + icon: 'mdi:database-sync-outline', + body: md`Trigger and monitor granular replications (publish staging items to production) via SCAPI. Adds \`sfcc.granular-replications.rw\`.`, + contributes: {surfaces: ['replications']}, + }), + ], + }), + + step('auth', { + title: 'How will the API client authenticate?', + doc: doc('/guide/authentication', 'account-manager-api-client', 'Account Manager API Client'), + choices: [ + choice('client-credentials', { + title: 'Client Credentials', + subtitle: 'client_id + client_secret', + icon: 'mdi:key-variant', + badges: [{text: 'Recommended', tone: 'info'}], + body: md`The typical setup — the API client has a Token Endpoint Auth Method of \`client_secret_post\` or \`client_secret_basic\`.`, + contributes: {authMethod: 'client-credentials'}, + }), + choice('jwt', { + title: 'JWT Bearer', + subtitle: 'Certificate-based', + icon: 'mdi:certificate-outline', + badges: [{text: 'Complex', tone: 'complex'}], + body: md`Use a public/private cert pair instead of a client secret.`, + contributes: {authMethod: 'jwt'}, + }), + ], + }), + + step('tenant', { + title: 'Where will the CLI find your tenant-id?', + subtitle: 'SCAPI requires a tenant-id (realm) on every request.', + doc: doc('/guide/authentication', 'configuring-tenant-filter', 'Configuring Tenant Filter'), + choices: [ + choice('dw-json', { + title: 'dw.json (project root)', + subtitle: 'Recommended', + icon: 'mdi:file-cog-outline', + body: md`Add \`"tenant-id": ""\` to your project's \`dw.json\`.`, + contributes: {tenantSource: 'dw-json'}, + }), + choice('env', { + title: 'Environment variable', + subtitle: 'SFCC_TENANT_ID', + icon: 'mdi:console-line', + body: md`CI-friendly. The CLI auto-loads \`.env\` files.`, + contributes: {tenantSource: 'env'}, + }), + choice('flag', { + title: 'Per-command flag', + subtitle: '--tenant-id', + icon: 'mdi:flag-outline', + body: md`Pass \`--tenant-id zzxy_prd\` on each invocation. Useful for one-off scripts.`, + contributes: {tenantSource: 'flag'}, + }), + ], + }), + + step('short-code', { + title: 'Where will the CLI find your SCAPI short-code?', + subtitle: 'Required for scapi schemas, scapi custom, and scapi replications.', + showIf: (state: AdventureState) => + selectedSurfaces(state).some((s) => s === 'schemas' || s === 'custom-apis' || s === 'replications'), + doc: doc('/guide/authentication', 'scapi-authentication', 'SCAPI Authentication'), + choices: [ + choice('dw-json', { + title: 'dw.json', + subtitle: 'short-code key', + icon: 'mdi:file-cog-outline', + body: md`Add \`"short-code": ""\` to your project's \`dw.json\`.`, + contributes: {shortCodeSource: 'dw-json'}, + }), + choice('env', { + title: 'Environment variable', + subtitle: 'SFCC_SHORTCODE', + icon: 'mdi:console-line', + body: md`Set \`SFCC_SHORTCODE\` alongside your other SCAPI env vars.`, + contributes: {shortCodeSource: 'env'}, + }), + choice('flag', { + title: 'Per-command flag', + subtitle: '--short-code', + icon: 'mdi:flag-outline', + body: md`Pass \`--short-code kv7kzm78\` on each invocation.`, + contributes: {shortCodeSource: 'flag'}, + }), + ], + }), + ], + + synthesize(state) { + const surfaces = selectedSurfaces(state); + const isJwt = state.authMethod === 'jwt'; + const tenantSource = (state.tenantSource as string) || 'dw-json'; + const shortCodeSource = (state.shortCodeSource as string) || 'dw-json'; + const useEnv = tenantSource === 'env' || shortCodeSource === 'env'; + const needsShortCode = surfaces.some((s) => s === 'schemas' || s === 'custom-apis' || s === 'replications'); + + const bundles = bundlesFor(surfaces); + const computedScopes = scopes(...bundles); + + const dw = dwJson({ + clientId: true, + clientSecret: !isJwt, + tenantId: tenantSource === 'dw-json', + shortCode: needsShortCode && shortCodeSource === 'dw-json', + }); + + const envLines = [ + 'SFCC_CLIENT_ID=', + isJwt ? 'SFCC_JWT_CERT=./cert.pem' : '', + isJwt ? 'SFCC_JWT_KEY=./key.pem' : '', + !isJwt ? 'SFCC_CLIENT_SECRET=' : '', + tenantSource === 'env' ? 'SFCC_TENANT_ID=' : '', + needsShortCode && shortCodeSource === 'env' ? 'SFCC_SHORTCODE=' : '', + ] + .filter(Boolean) + .join('\n'); + + const surfaceLabels: Record = { + 'custom-apis': 'Custom APIs (`b2c scapi custom`)', + ecdn: 'eCDN (`b2c ecdn`)', + replications: 'Replications (`b2c scapi replications`)', + schemas: 'SCAPI Schemas (`b2c scapi schemas`)', + }; + const surfaceList = surfaces.length > 0 ? surfaces.map((s) => surfaceLabels[s] ?? s).join(', ') : 'None selected'; + + const checklist = [ + check( + 'Create an Account Manager API client and pick a Token Endpoint Auth Method', + link('/guide/authentication', 'creating-an-api-client', 'Creating an API Client'), + ), + check( + 'Assign the Salesforce Commerce API role with a tenant filter', + link('/guide/authentication', 'assigning-roles', 'Assigning Roles'), + ), + check( + `Add Default Scopes: ${computedScopes}`, + link('/guide/authentication', 'configuring-scopes', 'Configuring Scopes'), + ), + check( + 'Set the tenant filter to the realm/tenant IDs your client may access', + link('/guide/authentication', 'configuring-tenant-filter', 'Configuring Tenant Filter'), + ), + ...(isJwt + ? [ + check( + 'Register your public certificate and configure JWT credentials', + link('/guide/authentication', 'jwt-authentication-certificate-based', 'JWT Authentication'), + ), + ] + : []), + check( + useEnv + ? `Set SFCC_* environment variables (tenant-id${needsShortCode ? ' + short-code' : ''})` + : `Save dw.json with tenant-id${needsShortCode ? ' + short-code' : ''}`, + link( + '/guide/configuration', + useEnv ? 'environment-variables' : 'configuration-file', + useEnv ? 'Environment Variables' : 'Configuration File', + ), + ), + ]; + + const warnings: string[] = [ + `Selected surfaces: ${surfaceList}`, + "Do NOT add `SALESFORCE_COMMERCE_API` as a scope — that is a *role* you assign to the API client, not a scope. The CLI auto-requests the scopes above; they only need to appear in the client's Default Scopes list.", + ]; + if (needsShortCode) { + warnings.push( + 'The SCAPI short-code is the per-realm subdomain in your SCAPI URL (e.g., `kv7kzm78`). It is required for `scapi schemas`, `scapi custom`, and `scapi replications`.', + ); + } + + return { + dwJson: dw, + env: useEnv ? envLines : undefined, + checklist, + warnings, + verifyCommand: pickVerifyCommand(surfaces), + }; + }, +}); diff --git a/docs/.vitepress/data/adventures/script-api-docs.ts b/docs/.vitepress/data/adventures/script-api-docs.ts new file mode 100644 index 000000000..3de5cfafb --- /dev/null +++ b/docs/.vitepress/data/adventures/script-api-docs.ts @@ -0,0 +1,168 @@ +// Adventure: Download API Docs (b2c docs). + +import {choice, defineAdventure, doc, md, step} from './_authoring.js'; +import {check, dwJson, link} from './_helpers.js'; + +export const scriptApiDocsAdventure = defineAdventure({ + id: 'script-api-docs', + title: 'Download API Docs', + tagline: 'Search and read instance-specific Script API documentation offline.', + icon: 'mdi:book-open-page-variant-outline', + tags: ['docs', 'offline'], + priority: 'niche', + intro: + 'The CLI ships with bundled Script API docs and XSD schemas — search and read work offline with zero config. Only download fresh docs if you need APIs specific to your instance version.', + + steps: [ + step('mode', { + title: 'Bundled docs or download fresh?', + doc: doc('/cli/docs', undefined, 'Docs Commands'), + choices: [ + choice('bundled', { + title: 'Use bundled docs', + subtitle: 'Zero config · offline', + icon: 'mdi:book-open-outline', + badges: [{text: 'Quick', tone: 'quick'}], + body: md`\`b2c docs search\`, \`b2c docs read\`, and \`b2c docs schema\` work immediately against docs shipped with the CLI release. No credentials required.`, + contributes: {mode: 'bundled'}, + }), + choice('download', { + title: 'Download from an instance', + subtitle: 'Instance-specific APIs', + icon: 'mdi:cloud-download-outline', + body: md`Use \`b2c docs download\` to pull the Script API docs from a specific instance over WebDAV. Useful when you need APIs that match the instance version exactly.`, + contributes: {mode: 'download'}, + }), + ], + }), + + step('webdav', { + title: 'How will WebDAV authenticate?', + subtitle: 'b2c docs download fetches the docs archive over WebDAV.', + showIf: (state) => state.mode === 'download', + doc: doc('/guide/authentication', 'webdav-access', 'WebDAV Access'), + choices: [ + choice('basic', { + title: 'BM username + access key', + subtitle: 'Recommended', + icon: 'mdi:key-outline', + badges: [{text: 'Quick', tone: 'quick'}], + body: md`Generate a WebDAV access key for your Business Manager user — fastest path to a successful \`b2c docs download\`.`, + contributes: {webdavAuth: 'basic'}, + }), + choice('oauth', { + title: 'OAuth (client credentials)', + subtitle: 'Reuse an Account Manager client', + icon: 'mdi:key-variant', + body: md`Use an Account Manager API client with WebDAV Client Permissions configured in BM for your \`client_id\`.`, + contributes: {webdavAuth: 'oauth'}, + }), + ], + }), + + step('persistence', { + title: 'How should the CLI find your config?', + showIf: (state) => state.mode === 'download', + doc: doc('/guide/configuration', 'configuration-file', 'Configuration file (dw.json)'), + choices: [ + choice('dw-json', { + title: 'dw.json (project root)', + subtitle: 'Recommended', + icon: 'mdi:file-cog-outline', + body: md`Per-project config file with walk-up discovery from the current directory.`, + contributes: {configSource: 'dw-json'}, + }), + choice('env', { + title: '.env / environment variables', + subtitle: 'CI-friendly', + icon: 'mdi:console-line', + body: md`Use \`SFCC_*\` env vars; the CLI auto-loads a \`.env\` file.`, + contributes: {configSource: 'env'}, + }), + ], + }), + ], + + synthesize(state) { + const isDownload = state.mode === 'download'; + const useBasic = state.webdavAuth === 'basic'; + const useEnv = state.configSource === 'env'; + + // Bundled mode requires no configuration at all. + if (!isDownload) { + return { + dwJson: '# No config required — bundled docs work offline.', + checklist: [ + check( + 'Run `b2c docs search ` to query the bundled Script API docs', + link('/cli/docs', undefined, 'Docs Commands'), + ), + ], + warnings: [ + 'Bundled docs ship with the CLI release — versions track the CLI itself, not your instance. Run `b2c docs schema --list` to see the bundled XSD schemas (catalog, order, etc.) available for offline XML validation.', + ], + verifyCommand: 'b2c docs search isCreate', + }; + } + + const dw = useEnv + ? '# Using environment variables — see .env tab below.' + : dwJson({ + hostname: true, + username: useBasic, + password: useBasic, + clientId: !useBasic, + clientSecret: !useBasic, + }); + + const env = useEnv + ? [ + 'SFCC_SERVER=.dx.commercecloud.salesforce.com', + useBasic ? 'SFCC_USERNAME=' : '', + useBasic ? 'SFCC_PASSWORD=' : '', + !useBasic ? 'SFCC_CLIENT_ID=' : '', + !useBasic ? 'SFCC_CLIENT_SECRET=' : '', + ] + .filter(Boolean) + .join('\n') + : undefined; + + const checklist = [ + ...(useBasic + ? [ + check( + 'Generate a WebDAV access key for your BM user', + link('/guide/authentication', 'option-a-basic-authentication-user-access', 'Basic Authentication'), + ), + ] + : [ + check( + 'Configure WebDAV Client Permissions in Business Manager', + link('/guide/authentication', 'webdav-access', 'WebDAV Access'), + ), + ]), + check( + useEnv ? 'Set SFCC_* environment variables' : 'Save the dw.json snippet to your project root', + link( + '/guide/configuration', + useEnv ? 'environment-variables' : 'configuration-file', + useEnv ? 'Environment Variables' : 'Configuration File', + ), + ), + check( + 'Run `b2c docs download ./docs` to fetch fresh docs from your instance', + link('/cli/docs', undefined, 'Docs Commands'), + ), + ]; + + return { + dwJson: dw, + env, + checklist, + warnings: [ + 'After downloading, point `b2c docs search` and `b2c docs read` at the extracted directory — or replace the bundled docs to make instance-specific APIs available everywhere.', + ], + verifyCommand: 'b2c docs download ./docs', + }; + }, +}); diff --git a/docs/.vitepress/data/adventures/slas-clients.ts b/docs/.vitepress/data/adventures/slas-clients.ts new file mode 100644 index 000000000..2143bb4c6 --- /dev/null +++ b/docs/.vitepress/data/adventures/slas-clients.ts @@ -0,0 +1,325 @@ +// Adventure: Manage SLAS clients (b2c slas). +// +// Walks the user through configuring auth + tenant for the SLAS commands. +// Two main surfaces: +// - "management": `b2c slas client list/create/get/update/delete/open` — +// authenticated against the SLAS Admin API. Built-in public client works +// out of the box (browser login as a user with the SLAS Organization +// Administrator role + tenant filter). Server-side automation can use a +// custom Account Manager API client with the Sandbox API User role. +// - "token": `b2c slas token` — fetches a shopper access token. Needs the +// SLAS client id (public or private), site id, short-code, and tenant id. + +import {choice, defineAdventure, doc, md, step} from './_authoring.js'; +import {check, dwJson, link} from './_helpers.js'; + +export const slasClientsAdventure = defineAdventure({ + id: 'slas-clients', + title: 'Manage SLAS clients', + tagline: 'Create and configure Shopper Login & Access Service (SLAS) clients for headless storefronts.', + icon: 'mdi:account-key-outline', + tags: ['slas', 'headless', 'shopper', 'oauth'], + priority: 'specialized', + intro: + "SLAS commands work out of the box — the CLI's built-in public client authenticates you via browser login. You only need to set up a custom Account Manager API client for automation/CI. The SLAS Admin API is scoped per tenant, so a tenant id is always required.", + + steps: [ + step('surface', { + title: 'What do you want to do?', + doc: doc('/cli/slas', undefined, 'SLAS Commands'), + choices: [ + choice('management', { + title: 'Manage SLAS clients', + subtitle: 'b2c slas client create / list / get / update / delete', + icon: 'mdi:cog-outline', + badges: [{text: 'Recommended', tone: 'info'}], + body: md` + Create and inspect SLAS clients (public or private) on a tenant. Uses the + [SLAS Admin API](/cli/slas). + `, + contributes: {surface: 'management'}, + }), + choice('token', { + title: 'Fetch shopper tokens', + subtitle: 'b2c slas token', + icon: 'mdi:key-chain-variant', + body: md` + Mint a guest or registered-shopper access token against an existing SLAS client. Useful for testing + Shopper APIs from \`curl\` or your headless storefront. + `, + contributes: {surface: 'token'}, + }), + ], + }), + + step('auth', { + title: 'How will you authenticate to the SLAS Admin API?', + doc: doc('/guide/authentication', 'account-manager-api-client', 'Account Manager API Client'), + choices: [ + choice('implicit', { + title: 'Browser login (default)', + subtitle: 'Zero config', + icon: 'mdi:account-arrow-right-outline', + badges: [{text: 'Quick', tone: 'quick'}], + body: md` + Use the CLI's built-in public client. \`b2c slas client list\` opens a browser window for login on first + use. Your user account just needs the \`SLAS Organization Administrator\` role with a tenant filter. + `, + contributes: {authMethod: 'implicit'}, + }), + choice('client-credentials', { + title: 'Client Credentials', + subtitle: 'Recommended for CI/CD', + icon: 'mdi:key-variant', + badges: [{text: 'CI', tone: 'quick'}], + body: md` + Account Manager API client with a client secret. Non-interactive — assign the + \`Sandbox API User\` role on the client and configure a + [tenant filter](/guide/authentication#configuring-tenant-filter) for the tenant. + `, + contributes: {authMethod: 'client-credentials'}, + }), + ], + }), + + step('slas-client-type', { + title: 'Which kind of SLAS client will you mint a token for?', + subtitle: 'Public clients use PKCE (no secret). Private clients use client_credentials.', + showIf: (state) => state.surface === 'token', + doc: doc('/cli/slas', undefined, 'b2c slas token'), + choices: [ + choice('public', { + title: 'Public client (PKCE)', + subtitle: 'Browser / mobile / SPA', + icon: 'mdi:web', + badges: [{text: 'Recommended', tone: 'info'}], + body: md` + No secret — the CLI uses the authorization_code_pkce flow. This is the right choice for headless + storefronts (PWA Kit, Storefront Next) and any browser-side code. + `, + contributes: {slasClientType: 'public'}, + }), + choice('private', { + title: 'Private client (client_credentials)', + subtitle: 'Server-side only', + icon: 'mdi:server-security', + body: md` + Has a client secret. Use only for trusted server-to-server code paths — never embed in a browser + or mobile bundle. + `, + contributes: {slasClientType: 'private'}, + }), + ], + }), + + step('persistence', { + title: 'How should the CLI find your tenant id and credentials?', + subtitle: '--tenant-id is required on every SLAS command — store it once or pass it on each invocation.', + doc: doc('/guide/configuration', 'configuration-file', 'Configuration file (dw.json)'), + choices: [ + choice('dw-json', { + title: 'dw.json (project root)', + subtitle: 'Recommended', + icon: 'mdi:file-cog-outline', + badges: [{text: 'Recommended', tone: 'info'}], + body: md` + Stores \`tenant-id\` (plus credentials and any token-flow values) in a per-project file. + Walk-up discovery from the current directory. + `, + contributes: {configSource: 'dw-json'}, + }), + choice('env', { + title: '.env / environment variables', + subtitle: 'CI-friendly', + icon: 'mdi:console-line', + body: md` + Use \`SFCC_TENANT_ID\` (and \`SFCC_SLAS_CLIENT_ID\`, \`SFCC_SITE_ID\`, + \`SFCC_SHORTCODE\` for the token flow). The CLI auto-loads \`.env\` files. + `, + contributes: {configSource: 'env'}, + }), + choice('flags', { + title: 'Flags on every command', + subtitle: 'No persistence', + icon: 'mdi:flag-outline', + body: md` + Pass \`--tenant-id \` (and friends) on every \`b2c slas\` + invocation. Useful for one-off scripts or when juggling multiple tenants. + `, + contributes: {configSource: 'flags'}, + }), + ], + }), + ], + + synthesize(state) { + const surface = String(state.surface ?? 'management'); + const isToken = surface === 'token'; + const authMethod = String(state.authMethod ?? 'implicit'); + const isImplicit = authMethod === 'implicit'; + const isClientCreds = authMethod === 'client-credentials'; + + const configSource = String(state.configSource ?? 'dw-json'); + const useEnv = configSource === 'env'; + const useFlags = configSource === 'flags'; + + const isPrivate = state.slasClientType === 'private'; + + // ----------------------------------------------------------------- + // dw.json + env synthesis + // ----------------------------------------------------------------- + + let dw: string; + if (useFlags) { + dw = isToken + ? '# No config file needed — pass --tenant-id, --site-id, --short-code, and --slas-client-id on every command.\n# Example:\n# b2c slas token --tenant-id --site-id --short-code ' + : '# No config file needed — pass --tenant-id on every command.\n# Example:\n# b2c slas client list --tenant-id '; + } else if (useEnv) { + dw = '# Using environment variables — see .env tab below.'; + } else if (isToken) { + // dw.json doesn't have a first-class `site-id` placeholder builder, so + // assemble it by hand for the token surface. + const lines = [ + '{', + ' "tenant-id": "",', + ' "short-code": "",', + ' "site-id": "",', + ' "slas-client-id": ""', + ]; + if (isClientCreds) { + lines[lines.length - 1] += ','; + lines.push(' "client-id": "",', ' "client-secret": ""'); + } + lines.push('}'); + dw = lines.join('\n'); + } else { + // Management surface, dw-json + dw = dwJson({ + tenantId: true, + clientId: isClientCreds, + clientSecret: isClientCreds, + }); + } + + let env: string | undefined; + if (useEnv) { + const lines: string[] = ['SFCC_TENANT_ID=']; + if (isToken) { + lines.push('SFCC_SITE_ID='); + lines.push('SFCC_SHORTCODE='); + lines.push('SFCC_SLAS_CLIENT_ID='); + if (isPrivate) { + lines.push('SFCC_SLAS_CLIENT_SECRET='); + } + } + if (isClientCreds) { + lines.push('SFCC_CLIENT_ID='); + lines.push('SFCC_CLIENT_SECRET='); + } + env = lines.join('\n'); + } + + // ----------------------------------------------------------------- + // Checklist + // ----------------------------------------------------------------- + + const checklist = [ + ...(isImplicit + ? [ + check( + 'Sign in with the built-in client (browser login on first command)', + link('/cli/slas', undefined, 'SLAS Commands'), + ), + check( + 'Confirm your user has the SLAS Organization Administrator role with a tenant filter', + link('/guide/authentication', 'for-user-authentication-roles-on-user', 'Roles for User Authentication'), + ), + ] + : [ + check( + 'Create an Account Manager API client', + link('/guide/authentication', 'creating-an-api-client', 'Creating an API Client'), + ), + check( + 'Assign the Sandbox API User role to the API client', + link('/guide/authentication', 'for-client-credentials-roles-on-api-client', 'Roles for Client Credentials'), + ), + check( + 'Add a tenant filter for your tenant/realm on the Sandbox API User role', + link('/guide/authentication', 'configuring-tenant-filter', 'Configuring Tenant Filter'), + ), + ]), + ...(useFlags + ? [ + check( + isToken + ? 'Pass --tenant-id, --site-id, --short-code (and --slas-client-id) on every slas token command' + : 'Pass --tenant-id on every slas client command', + link('/cli/slas', undefined, 'SLAS Commands'), + ), + ] + : [ + check( + useEnv + ? `Set SFCC_TENANT_ID${isToken ? ' + SFCC_SITE_ID + SFCC_SHORTCODE + SFCC_SLAS_CLIENT_ID' : ''}${isClientCreds ? ' + SFCC_CLIENT_ID/SECRET' : ''}` + : `Save tenant-id${isToken ? ' + short-code + site-id + slas-client-id' : ''} to dw.json`, + link( + '/guide/configuration', + useEnv ? 'environment-variables' : 'configuration-file', + useEnv ? 'Environment Variables' : 'Configuration File', + ), + ), + ]), + ...(isToken + ? [ + check( + 'Note the SLAS client id (and secret, if private) from `b2c slas client create` output or Business Manager', + link('/cli/slas', undefined, 'b2c slas client create'), + ), + ] + : []), + ]; + + // ----------------------------------------------------------------- + // Warnings + // ----------------------------------------------------------------- + + const warnings: string[] = []; + if (isImplicit) { + warnings.push( + 'Browser auth opens a login window the first time and on token expiry. Great for local development; use client credentials for CI/CD.', + ); + } + if (isToken) { + warnings.push( + 'SLAS public clients (created with `--public`) have no secret — use the PKCE flow by omitting `--slas-client-secret`. Use private clients only for trusted server-side code.', + ); + if (!isPrivate) { + warnings.push( + 'If you omit `--slas-client-id`, `b2c slas token` auto-discovers the first public SLAS client for the tenant. Pin the id explicitly for reproducible CI runs.', + ); + } + } + + // ----------------------------------------------------------------- + // Verify command + // ----------------------------------------------------------------- + + let verifyCommand: string; + if (isToken) { + verifyCommand = isPrivate + ? 'b2c slas token --tenant-id --site-id --short-code --slas-client-id --slas-client-secret ' + : 'b2c slas token --tenant-id --site-id --short-code --slas-client-id '; + } else { + verifyCommand = 'b2c slas client list --tenant-id '; + } + + return { + dwJson: dw, + env, + checklist, + warnings, + verifyCommand, + }; + }, +}); diff --git a/docs/.vitepress/data/adventures/vscode-extension.ts b/docs/.vitepress/data/adventures/vscode-extension.ts new file mode 100644 index 000000000..d121772b7 --- /dev/null +++ b/docs/.vitepress/data/adventures/vscode-extension.ts @@ -0,0 +1,297 @@ +// Adventure: Install the VS Code extension. +// +// Walks through downloading the VSIX, installing it, picking which features +// to enable, and providing a dw.json (or .env) the extension can pick up. +// The synthesizer assembles a tailored dw.json based on the chosen feature +// mix — the extension reuses whatever the CLI uses, so the credentials +// surface is feature-driven, not connection-driven. + +import {choice, defineAdventure, doc, md, step} from './_authoring.js'; +import {check, dwJson, link} from './_helpers.js'; +import type {AdventureState} from './_types.js'; + +function hasFeature(state: AdventureState, id: string): boolean { + const features = Array.isArray(state.features) ? state.features : []; + return features.includes(id); +} + +export const vscodeExtensionAdventure = defineAdventure({ + id: 'vscode-extension', + title: 'Install the VS Code extension', + tagline: 'Set up the B2C DX VS Code extension and its features.', + icon: 'mdi:microsoft-visual-studio-code', + tags: ['vscode', 'sandbox', 'content', 'debug', 'webdav'], + priority: 'core', + intro: + 'The B2C DX VS Code Extension is in Developer Preview and ships as a pre-built VSIX from GitHub releases. It reuses your existing CLI configuration, so once your dw.json (or SFCC_* env vars) is in place, every feature you turn on picks up the same connection.', + + steps: [ + step('project', { + title: 'What kind of project?', + doc: doc('/vscode-extension/', undefined, 'VS Code Extension overview'), + choices: [ + choice('cartridges', { + title: 'Cartridges (SFRA / classic)', + subtitle: 'Most common', + icon: 'mdi:package-variant', + badges: [{text: 'Recommended', tone: 'info'}], + body: md`Server-side cartridges with WebDAV upload, code-version management, and the Script Debugger.`, + contributes: {projectType: 'cartridges'}, + }), + choice('headless', { + title: 'Headless (PWA Kit / Storefront Next)', + subtitle: 'Composable', + icon: 'mdi:rocket-outline', + body: md`Frontend-only project — the extension is mostly useful for the **SCAPI API Browser** and **Sandbox Realm Explorer**.`, + contributes: {projectType: 'headless'}, + }), + choice('both', { + title: 'Both', + subtitle: 'Cartridges + Headless', + icon: 'mdi:swap-horizontal', + body: md`Mixed workspace — pick all the features you actually use below.`, + contributes: {projectType: 'both'}, + }), + ], + }), + + step('features', { + title: 'Which features will you use?', + subtitle: "Pick the ones you'll actually use — anything you skip can be disabled in settings.", + multiSelect: true, + minPicks: 1, + doc: doc('/vscode-extension/', 'highlights', 'Extension Highlights'), + choices: [ + choice('sandbox', { + title: 'Sandbox Realm Explorer', + subtitle: 'ODS management', + icon: 'mdi:flask-outline', + badges: [{text: 'Recommended', tone: 'info'}], + body: md`Spin up, start, stop, and clone on-demand sandboxes from a tree view. Needs an OAuth client with the \`Sandbox API User\` role.`, + contributes: {features: ['sandbox']}, + }), + choice('codeSync', { + title: 'Cartridge Code Sync', + subtitle: 'Watch + upload', + icon: 'mdi:cloud-upload-outline', + body: md`Auto-upload cartridges as you save. Uses WebDAV + OCAPI for code-version operations.`, + contributes: {features: ['codeSync']}, + }), + choice('libraries', { + title: 'Library Explorer', + subtitle: 'Page Designer content', + icon: 'mdi:view-grid-outline', + body: md`Browse, edit, and round-trip Page Designer libraries. Reads library XML over WebDAV.`, + contributes: {features: ['libraries']}, + }), + choice('debugger', { + title: 'B2C Script Debugger', + subtitle: 'Server-side breakpoints', + icon: 'mdi:bug-outline', + body: md`Step through controllers, jobs, hooks, SCAPI hooks, and Custom APIs on the sandbox.`, + contributes: {features: ['debugger']}, + }), + choice('webdav', { + title: 'WebDAV Browser', + subtitle: 'Remote files', + icon: 'mdi:folder-network-outline', + body: md`Browse \`Catalogs/\`, \`Libraries/\`, and \`IMPEX/\` folders inside VS Code.`, + contributes: {features: ['webdav']}, + }), + choice('logs', { + title: 'Log Tailing', + subtitle: 'Live error/warn/info logs', + icon: 'mdi:console-line', + body: md`Stream \`error-*.log\`, \`warn-*.log\`, and \`info-*.log\` into an output channel.`, + contributes: {features: ['logs']}, + }), + choice('apiBrowser', { + title: 'SCAPI API Explorer', + subtitle: 'Try APIs in Swagger UI', + icon: 'mdi:api', + body: md`Explore SCAPI APIs and run authenticated requests. Needs \`short-code\` + \`tenant-id\`.`, + contributes: {features: ['apiBrowser']}, + }), + ], + }), + + step('config', { + title: 'How will you give the extension your credentials?', + doc: doc('/vscode-extension/configuration', 'connecting-to-a-b2c-instance', 'Connecting to a B2C Instance'), + choices: [ + choice('dw-json', { + title: 'dw.json (workspace root)', + subtitle: 'Recommended', + icon: 'mdi:file-cog-outline', + badges: [{text: 'Recommended', tone: 'info'}], + body: md`Per-project file the extension and CLI both read. Walk-up discovery from the workspace root.`, + contributes: {configSource: 'dw-json'}, + }), + choice('env', { + title: '.env / environment variables', + subtitle: 'CI-friendly', + icon: 'mdi:console-line', + body: md`Use \`SFCC_*\` env vars — VS Code picks them up from the launching shell.`, + contributes: {configSource: 'env'}, + }), + choice('existing', { + title: 'Use my existing CLI config', + subtitle: 'Already set up', + icon: 'mdi:check-circle-outline', + body: md`If \`b2c\` already works in this folder, the extension uses the same connection — no extra setup.`, + contributes: {configSource: 'dw-json'}, + }), + ], + }), + ], + + synthesize(state) { + const useEnv = state.configSource === 'env'; + const projectType = String(state.projectType ?? 'cartridges'); + const isHeadless = projectType === 'headless' || projectType === 'both'; + + const wantsSandbox = hasFeature(state, 'sandbox'); + const wantsLibraries = hasFeature(state, 'libraries'); + const wantsDebugger = hasFeature(state, 'debugger'); + const wantsWebdav = hasFeature(state, 'webdav'); + const wantsCodeSync = hasFeature(state, 'codeSync'); + const wantsLogs = hasFeature(state, 'logs'); + const wantsApi = hasFeature(state, 'apiBrowser'); + + // Different features need different fields. Compute the union. + const needsWebdav = wantsLibraries || wantsDebugger || wantsWebdav || wantsCodeSync || wantsLogs; + const needsOauth = wantsSandbox || wantsCodeSync || wantsApi; + const needsScapi = wantsApi; + + const dw = useEnv + ? '# Using environment variables — see .env tab below.' + : dwJson({ + hostname: true, + codeVersion: wantsCodeSync, + username: needsWebdav, + password: needsWebdav, + clientId: needsOauth, + clientSecret: needsOauth, + shortCode: needsScapi, + tenantId: needsScapi, + }); + + const env = useEnv + ? [ + 'SFCC_SERVER=.dx.commercecloud.salesforce.com', + wantsCodeSync ? 'SFCC_CODE_VERSION=version1' : '', + needsWebdav ? 'SFCC_USERNAME=' : '', + needsWebdav ? 'SFCC_PASSWORD=' : '', + needsOauth ? 'SFCC_CLIENT_ID=' : '', + needsOauth ? 'SFCC_CLIENT_SECRET=' : '', + needsScapi ? 'SFCC_SHORT_CODE=' : '', + needsScapi ? 'SFCC_TENANT_ID=' : '', + ] + .filter(Boolean) + .join('\n') + : undefined; + + // Build the disabled-feature toggles. The extension defaults every feature + // ON, so we only emit explicit `false` for features the user did NOT pick — + // and only when the user picked at least one feature, so we don't disable + // everything by accident on first load. + const allToggles: {id: string; key: string}[] = [ + {id: 'sandbox', key: 'b2c-dx.features.sandboxExplorer'}, + {id: 'webdav', key: 'b2c-dx.features.webdavBrowser'}, + {id: 'libraries', key: 'b2c-dx.features.contentLibraries'}, + {id: 'codeSync', key: 'b2c-dx.features.codeSync'}, + {id: 'logs', key: 'b2c-dx.features.logTailing'}, + {id: 'apiBrowser', key: 'b2c-dx.features.apiBrowser'}, + ]; + const featurePicks = Array.isArray(state.features) ? state.features : []; + const disabledToggles = featurePicks.length > 0 ? allToggles.filter((t) => !featurePicks.includes(t.id)) : []; + + const warnings: string[] = []; + + if (disabledToggles.length > 0) { + const settingsBody = disabledToggles.map((t) => ` "${t.key}": false`).join(',\n'); + warnings.push( + `Optional — disable the features you skipped in \`.vscode/settings.json\` to trim the UI:\n\n\`\`\`json\n{\n${settingsBody}\n}\n\`\`\``, + ); + } + + if (wantsLibraries) { + warnings.push( + 'For the **Library Explorer** tree to populate automatically, add a `contentLibrary` (or `libraries`) entry to your `dw.json`. See [Content Libraries Example](/guide/configuration#content-libraries-example).', + ); + } + + if (wantsDebugger) { + warnings.push( + 'The **B2C Script Debugger** activates only when a `b2c-script` launch configuration is used — it ignores the feature toggles above.', + ); + } + + if (isHeadless && (wantsCodeSync || wantsLibraries)) { + warnings.push( + 'Headless projects (PWA Kit / Storefront Next) usually do not need Cartridge Code Sync or the Library Explorer. Consider unchecking those features unless you also work in a cartridge repo.', + ); + } + + const checklist = [ + check( + 'Download the latest VSIX from the GitHub releases page', + link('/vscode-extension/installation', 'get-the-latest-build', 'Get the latest build'), + ), + check( + 'Install the VSIX into VS Code (or Cursor / VS Codium)', + link('/vscode-extension/installation', 'install-it', 'Install it'), + ), + check( + 'Have VS Code 1.105+ and the B2C CLI installed', + link('/vscode-extension/installation', 'before-you-start', 'Before you start'), + ), + check( + useEnv + ? 'Provide credentials via SFCC_* environment variables (or .env)' + : 'Add a dw.json at your project root with the fields each feature needs', + link( + '/vscode-extension/configuration', + useEnv ? 'connecting-to-a-b2c-instance' : 'example-dwjson', + useEnv ? 'Connecting to a B2C Instance' : 'Example dw.json', + ), + ), + check( + 'Confirm which credentials each feature you picked needs', + link('/vscode-extension/configuration', 'per-feature-requirements', 'Per-feature requirements'), + ), + ...(needsOauth + ? [ + check( + 'Create an Account Manager API client for OAuth-backed features', + link('/guide/authentication', 'account-manager-api-client', 'Account Manager API Client'), + ), + ] + : []), + ...(needsWebdav + ? [ + check( + 'Generate a WebDAV access key (BM username + key)', + link('/guide/authentication', 'webdav-access', 'WebDAV Access'), + ), + ] + : []), + ...(disabledToggles.length > 0 + ? [ + check( + 'Optionally disable unused features in .vscode/settings.json', + link('/vscode-extension/configuration', 'feature-toggles', 'Feature toggles'), + ), + ] + : []), + ]; + + return { + dwJson: dw, + env, + checklist, + warnings, + verifyCommand: 'b2c setup inspect', + }; + }, +}); diff --git a/docs/.vitepress/shims-vue.d.ts b/docs/.vitepress/shims-vue.d.ts new file mode 100644 index 000000000..c1c58f033 --- /dev/null +++ b/docs/.vitepress/shims-vue.d.ts @@ -0,0 +1,5 @@ +declare module '*.vue' { + import type {DefineComponent} from 'vue'; + const component: DefineComponent, Record, unknown>; + export default component; +} diff --git a/docs/.vitepress/theme/MarkdownActions.vue b/docs/.vitepress/theme/MarkdownActions.vue index 2dc7a5b4e..e295341a0 100644 --- a/docs/.vitepress/theme/MarkdownActions.vue +++ b/docs/.vitepress/theme/MarkdownActions.vue @@ -9,6 +9,11 @@ const {page} = useData(); const mdUrl = computed(() => { const rel = page.value.relativePath; if (!rel) return ''; + // Quickstart pages either render the wizard (sub-pages, just a 3-line + // shim) or the topic index (also just a component). Neither raw .md is + // useful as Copy-for-LLM, so suppress the buttons sitewide on + // /quickstart/. To re-enable per-page, just remove this guard. + if (rel.startsWith('quickstart/')) return ''; return withBase('/' + rel); }); diff --git a/docs/.vitepress/theme/adventure/AdventureOutput.vue b/docs/.vitepress/theme/adventure/AdventureOutput.vue new file mode 100644 index 000000000..3c8e0f8f0 --- /dev/null +++ b/docs/.vitepress/theme/adventure/AdventureOutput.vue @@ -0,0 +1,248 @@ + + + + + diff --git a/docs/.vitepress/theme/adventure/AdventureStep.vue b/docs/.vitepress/theme/adventure/AdventureStep.vue new file mode 100644 index 000000000..7df0ffde1 --- /dev/null +++ b/docs/.vitepress/theme/adventure/AdventureStep.vue @@ -0,0 +1,240 @@ + + + + + diff --git a/docs/.vitepress/theme/adventure/ChoiceCard.vue b/docs/.vitepress/theme/adventure/ChoiceCard.vue new file mode 100644 index 000000000..84858adfb --- /dev/null +++ b/docs/.vitepress/theme/adventure/ChoiceCard.vue @@ -0,0 +1,168 @@ + + + + + diff --git a/docs/.vitepress/theme/adventure/QuickstartGuide.vue b/docs/.vitepress/theme/adventure/QuickstartGuide.vue new file mode 100644 index 000000000..7005b6172 --- /dev/null +++ b/docs/.vitepress/theme/adventure/QuickstartGuide.vue @@ -0,0 +1,338 @@ + + + + + diff --git a/docs/.vitepress/theme/adventure/QuickstartIndex.vue b/docs/.vitepress/theme/adventure/QuickstartIndex.vue new file mode 100644 index 000000000..95114ebef --- /dev/null +++ b/docs/.vitepress/theme/adventure/QuickstartIndex.vue @@ -0,0 +1,494 @@ + + + + + diff --git a/docs/.vitepress/theme/adventure/_markdown.ts b/docs/.vitepress/theme/adventure/_markdown.ts new file mode 100644 index 000000000..def19066b --- /dev/null +++ b/docs/.vitepress/theme/adventure/_markdown.ts @@ -0,0 +1,125 @@ +// Tiny markdown-to-HTML renderer for adventure body / warning content. +// Supports the limited subset adventures actually need: +// +// `inline code` -> inline code +// ```fenced code``` ->
fenced code
+// **bold** -> bold +// *italic* / _italic_ -> italic +// [text](url) -> text +// - list item ->
  • item
  • ...
+// newline ->
(between paragraphs only) +// +// Internal absolute paths (those starting with `/`) are passed through the +// `resolveHref` callback so callers can apply VitePress's `withBase` for +// dev/stable build paths. External URLs (http/https) and anchor-only links +// (#foo) are passed through verbatim. + +export type ResolveHref = (path: string) => string; + +const ESCAPE_HTML: Record = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', +}; + +function escapeHtml(s: string): string { + return s.replace(/[&<>"]/g, (c) => ESCAPE_HTML[c] ?? c); +} + +function resolveUrl(url: string, resolveHref: ResolveHref): string { + if (/^[a-z]+:/i.test(url) || url.startsWith('//') || url.startsWith('#') || url.startsWith('mailto:')) return url; + if (url.startsWith('/')) return resolveHref(url); + return url; +} + +// Sentinel that's vanishingly unlikely to appear in adventure prose. Used +// to stash already-rendered HTML through the inline pipeline so its +// contents aren't re-escaped by subsequent regex passes. +const STASH_OPEN = '__B2CMD_PLACEHOLDER_'; +const STASH_CLOSE = '__'; + +// Render inline markdown (no block-level handling). Used by the warning +// renderer for non-code content and inline contexts. +export function renderInline(text: string, resolveHref: ResolveHref): string { + const placeholders: string[] = []; + const stash = (html: string) => { + const idx = placeholders.length; + placeholders.push(html); + return `${STASH_OPEN}${idx}${STASH_CLOSE}`; + }; + + // 1. Inline code spans (stash first so their contents aren't touched by + // later replacements). + let s = text.replace(/`([^`]+)`/g, (_m, body) => stash(`${escapeHtml(body)}`)); + + // 2. Escape what remains so `<` / `>` in prose can't smuggle markup. + s = escapeHtml(s); + + // 3. Links — `[text](url)`. + s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_m, label, url) => { + const resolvedUrl = resolveUrl(url, resolveHref); + return `${label}`; + }); + + // 4. Bold + italic (** and * — keep simple, non-overlapping). + s = s.replace(/\*\*([^*]+)\*\*/g, '$1'); + s = s.replace(/(^|[^*])\*([^*\s][^*]*[^*\s]|[^*\s])\*(?!\*)/g, '$1$2'); + + // 5. Restore stashed placeholders. + const stashRe = new RegExp(`${STASH_OPEN}(\\d+)${STASH_CLOSE}`, 'g'); + s = s.replace(stashRe, (_m, i) => placeholders[Number(i)]); + + return s; +} + +// Render a block of markdown to HTML — handles fenced code, inline markdown, +// list items, and paragraph breaks. +export function renderBlock(text: string, resolveHref: ResolveHref): string { + const blocks: string[] = []; + const fence = /```(\w+)?\n([\s\S]*?)```/g; + let last = 0; + let m: RegExpExecArray | null; + while ((m = fence.exec(text)) !== null) { + if (m.index > last) blocks.push(renderProse(text.slice(last, m.index), resolveHref)); + const lang = m[1] ?? ''; + const body = escapeHtml(m[2]); + blocks.push(`
${body}
`); + last = m.index + m[0].length; + } + if (last < text.length) blocks.push(renderProse(text.slice(last), resolveHref)); + return blocks.join(''); +} + +// Render a non-code-fenced chunk: handles list items + paragraphs + +// inline markdown. +function renderProse(chunk: string, resolveHref: ResolveHref): string { + const lines = chunk.split('\n'); + const out: string[] = []; + let i = 0; + while (i < lines.length) { + const line = lines[i]; + if (/^\s*$/.test(line)) { + i++; + continue; + } + // List + if (/^\s*[-*]\s+/.test(line)) { + const items: string[] = []; + while (i < lines.length && /^\s*[-*]\s+/.test(lines[i])) { + items.push(`
  • ${renderInline(lines[i].replace(/^\s*[-*]\s+/, ''), resolveHref)}
  • `); + i++; + } + out.push(`
      ${items.join('')}
    `); + continue; + } + // Paragraph (one or more consecutive non-blank, non-list lines) + const para: string[] = []; + while (i < lines.length && lines[i].trim() && !/^\s*[-*]\s+/.test(lines[i])) { + para.push(lines[i]); + i++; + } + out.push(renderInline(para.join('\n').replace(/\n/g, '
    '), resolveHref)); + } + return out.join(''); +} diff --git a/docs/.vitepress/theme/adventure/useCopyableCode.ts b/docs/.vitepress/theme/adventure/useCopyableCode.ts new file mode 100644 index 000000000..2e8bec318 --- /dev/null +++ b/docs/.vitepress/theme/adventure/useCopyableCode.ts @@ -0,0 +1,66 @@ +// Walks a container for
     elements and injects a Copy button into
    +// each one. Idempotent — re-running on the same container won't double-add.
    +//
    +// Used by AdventureOutput (warnings + verify) and QChoice (rich descriptions
    +// that may include fenced code blocks via the slot).
    +
    +import {nextTick, onMounted, onUpdated, type Ref} from 'vue';
    +
    +const MARKER_ATTR = 'data-b2c-copy-attached';
    +
    +function attachCopyButton(pre: HTMLPreElement) {
    +  if (pre.hasAttribute(MARKER_ATTR)) return;
    +  pre.setAttribute(MARKER_ATTR, 'true');
    +
    +  // Some VitePress themes wrap 
     in 
    with + // existing copy buttons — skip those so we don't double-decorate. + const parent = pre.parentElement; + if (parent?.querySelector('.copy')) return; + + // Position the wrapper as relative for absolute button placement. + pre.style.position = pre.style.position || 'relative'; + + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'b2c-copy-btn'; + btn.textContent = 'Copy'; + btn.setAttribute('aria-label', 'Copy code'); + + btn.addEventListener('click', async (e) => { + e.preventDefault(); + e.stopPropagation(); + const code = pre.querySelector('code'); + const text = code?.textContent ?? pre.textContent ?? ''; + try { + await navigator.clipboard.writeText(text); + btn.textContent = 'Copied!'; + btn.classList.add('b2c-copy-btn--copied'); + setTimeout(() => { + btn.textContent = 'Copy'; + btn.classList.remove('b2c-copy-btn--copied'); + }, 1600); + } catch (err) { + console.error('Copy failed:', err); + } + }); + + pre.appendChild(btn); +} + +export function useCopyableCode(containerRef: Ref) { + function refresh() { + const root = containerRef.value; + if (!root) return; + const pres = root.querySelectorAll('pre'); + pres.forEach(attachCopyButton); + } + + onMounted(() => { + nextTick(() => refresh()); + }); + onUpdated(() => { + nextTick(() => refresh()); + }); + + return {refresh}; +} diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css index 69bca1d3b..a09a5cf24 100644 --- a/docs/.vitepress/theme/custom.css +++ b/docs/.vitepress/theme/custom.css @@ -59,3 +59,37 @@ width: 32px; height: 32px; } + +/* Copy button injected by useCopyableCode for inline code blocks. Lives + outside Vue's scoped-style boundary, so rules go here. */ +.b2c-copy-btn { + position: absolute; + top: 6px; + right: 6px; + font-size: 11px; + font-family: inherit; + padding: 3px 9px; + background: var(--vp-c-bg-soft); + color: var(--vp-c-text-2); + border: 1px solid var(--vp-c-divider); + border-radius: 6px; + cursor: pointer; + opacity: 0; + transition: opacity 0.15s, color 0.15s, border-color 0.15s; +} + +pre:hover > .b2c-copy-btn, +.b2c-copy-btn:focus { + opacity: 1; +} + +.b2c-copy-btn:hover { + color: var(--vp-c-brand-1); + border-color: var(--vp-c-brand-1); +} + +.b2c-copy-btn--copied { + color: var(--vp-c-brand-1); + border-color: var(--vp-c-brand-1); + opacity: 1; +} diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index 69e4c5ed5..3c970c8da 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -1,13 +1,16 @@ import {h} from 'vue'; +import type {Theme} from 'vitepress'; import DefaultTheme from 'vitepress/theme'; import type {Router} from 'vitepress'; import './custom.css'; import 'virtual:group-icons.css'; import HomeLayout from './HomeLayout.vue'; import MarkdownActions from './MarkdownActions.vue'; +import QuickstartGuide from './adventure/QuickstartGuide.vue'; +import QuickstartIndex from './adventure/QuickstartIndex.vue'; import {lookupRedirect} from './redirects'; -export default { +const theme: Theme = { extends: DefaultTheme, Layout() { return h(DefaultTheme.Layout, null, { @@ -17,6 +20,8 @@ export default { }, enhanceApp({app, router, siteData}) { app.component('b2c-home', HomeLayout); + app.component('QuickstartGuide', QuickstartGuide); + app.component('QuickstartIndex', QuickstartIndex); // Client-side redirects for moved/merged pages (SSR-safe: browser only). if (typeof window !== 'undefined') { @@ -40,3 +45,5 @@ export default { } }, }; + +export default theme; diff --git a/docs/guide/authentication.md b/docs/guide/authentication.md index 5201de52d..16fd95d33 100644 --- a/docs/guide/authentication.md +++ b/docs/guide/authentication.md @@ -6,6 +6,10 @@ description: Set up authentication for the B2C CLI including Account Manager API This guide covers setting up authentication for the B2C CLI, including Account Manager API clients, OCAPI permissions, and WebDAV access. +::: tip Prefer a guided path? +The [Quickstart](/quickstart/) picks the minimum auth + permissions you need for a specific task (e.g. *deploy cartridge code*, *run jobs*, *work with Page Designer content*) and produces a copy-pasteable `dw.json` plus a verify command. +::: + ## Overview The CLI uses different authentication mechanisms depending on the operation: diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index 5695a83d2..bb2119afa 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -6,6 +6,10 @@ description: Configure the B2C CLI with environment variables, dw.json files, an The B2C CLI automatically detects and uses available credentials. You can provide credentials via CLI flags, environment variables, or configuration files. +::: tip Prefer a guided path? +The [Quickstart](/quickstart/) walks you through the minimum config for common tasks (deploy code, run jobs, work with Page Designer content, set up an AI coding agent) and synthesises a `dw.json` snippet plus a verify command. +::: + ::: tip For detailed setup instructions including Account Manager API client creation, role configuration, and OCAPI setup, see the [Authentication Setup](./authentication) guide. ::: diff --git a/docs/index.md b/docs/index.md index 08904eabe..dfb6a7767 100644 --- a/docs/index.md +++ b/docs/index.md @@ -16,6 +16,9 @@ hero: alt: Agentic B2C Developer Toolkit — CLI, Agentforce Vibes, and Claude Code actions: - theme: brand + text: Quickstart + link: /quickstart/ + - theme: alt text: Get Started link: /guide/ - theme: alt diff --git a/docs/package.json b/docs/package.json index 28dbfb98f..157ba62b1 100644 --- a/docs/package.json +++ b/docs/package.json @@ -7,7 +7,8 @@ "docs:api": "typedoc", "docs:dev": "pnpm run docs:api && vitepress dev", "docs:build": "pnpm run docs:api && vitepress build", - "docs:preview": "vitepress preview" + "docs:preview": "vitepress preview", + "docs:typecheck": "pnpm run docs:api && tsc --noEmit -p tsconfig.json" }, "devDependencies": { "@salesforce/b2c-tooling-sdk": "workspace:*", diff --git a/docs/quickstart/account-manager.md b/docs/quickstart/account-manager.md new file mode 100644 index 000000000..e1af35ea3 --- /dev/null +++ b/docs/quickstart/account-manager.md @@ -0,0 +1,9 @@ +--- +title: Account Manager + BM admin · Quickstart +description: Configure CLI access for Account Manager and Business Manager administrative commands. +layout: doc +sidebar: false +aside: false +--- + + diff --git a/docs/quickstart/agent-mcp.md b/docs/quickstart/agent-mcp.md new file mode 100644 index 000000000..9c058a91d --- /dev/null +++ b/docs/quickstart/agent-mcp.md @@ -0,0 +1,9 @@ +--- +title: Set up an AI coding agent · Quickstart +description: Install B2C agent skills and the MCP server for your IDE. +layout: doc +sidebar: false +aside: false +--- + + diff --git a/docs/quickstart/cartridge-path.md b/docs/quickstart/cartridge-path.md new file mode 100644 index 000000000..7e6320d1a --- /dev/null +++ b/docs/quickstart/cartridge-path.md @@ -0,0 +1,9 @@ +--- +title: Manage site cartridge paths · Quickstart +description: Configure the CLI to inspect and update site cartridge paths. +layout: doc +sidebar: false +aside: false +--- + + diff --git a/docs/quickstart/ci-cd.md b/docs/quickstart/ci-cd.md new file mode 100644 index 000000000..1edc85a75 --- /dev/null +++ b/docs/quickstart/ci-cd.md @@ -0,0 +1,9 @@ +--- +title: Set up CI/CD pipeline · Quickstart +description: Configure credentials, safety mode, and a runner for automated B2C deployments. +layout: doc +sidebar: false +aside: false +--- + + diff --git a/docs/quickstart/debug.md b/docs/quickstart/debug.md new file mode 100644 index 000000000..14b07f905 --- /dev/null +++ b/docs/quickstart/debug.md @@ -0,0 +1,9 @@ +--- +title: Debug server-side scripts · Quickstart +description: Configure the CLI to debug B2C Commerce scripts via VS Code or a REPL. +layout: doc +sidebar: false +aside: false +--- + + diff --git a/docs/quickstart/deploy-code.md b/docs/quickstart/deploy-code.md new file mode 100644 index 000000000..25d8f7182 --- /dev/null +++ b/docs/quickstart/deploy-code.md @@ -0,0 +1,9 @@ +--- +title: Deploy cartridge code · Quickstart +description: Walk through the configuration needed to deploy cartridge code with the B2C CLI. +layout: doc +sidebar: false +aside: false +--- + + diff --git a/docs/quickstart/index.md b/docs/quickstart/index.md new file mode 100644 index 000000000..ebeff28ff --- /dev/null +++ b/docs/quickstart/index.md @@ -0,0 +1,9 @@ +--- +title: Quickstart +description: Pick what you want to do, and get the minimum config — dw.json, doc links, and a verify command. +layout: doc +sidebar: false +aside: false +--- + + diff --git a/docs/quickstart/jobs.md b/docs/quickstart/jobs.md new file mode 100644 index 000000000..111cc6322 --- /dev/null +++ b/docs/quickstart/jobs.md @@ -0,0 +1,9 @@ +--- +title: Run jobs · Quickstart +description: Walk through the configuration needed to run B2C Commerce jobs with the B2C CLI. +layout: doc +sidebar: false +aside: false +--- + + diff --git a/docs/quickstart/logs.md b/docs/quickstart/logs.md new file mode 100644 index 000000000..4a331aa6c --- /dev/null +++ b/docs/quickstart/logs.md @@ -0,0 +1,9 @@ +--- +title: Tail and search logs · Quickstart +description: Configure WebDAV credentials so the CLI can stream and filter instance logs. +layout: doc +sidebar: false +aside: false +--- + + diff --git a/docs/quickstart/migrate-sfcc-ci.md b/docs/quickstart/migrate-sfcc-ci.md new file mode 100644 index 000000000..1ea17e943 --- /dev/null +++ b/docs/quickstart/migrate-sfcc-ci.md @@ -0,0 +1,9 @@ +--- +title: Migrate from sfcc-ci · Quickstart +description: Replace sfcc-ci with the B2C CLI in your CI/CD pipelines. +layout: doc +sidebar: false +aside: false +--- + + diff --git a/docs/quickstart/mrt-deploy.md b/docs/quickstart/mrt-deploy.md new file mode 100644 index 000000000..081f72e23 --- /dev/null +++ b/docs/quickstart/mrt-deploy.md @@ -0,0 +1,9 @@ +--- +title: Managed Runtime deployments · Quickstart +description: Configure the CLI to deploy bundles to Managed Runtime. +layout: doc +sidebar: false +aside: false +--- + + diff --git a/docs/quickstart/multi-instance.md b/docs/quickstart/multi-instance.md new file mode 100644 index 000000000..6acae6dd3 --- /dev/null +++ b/docs/quickstart/multi-instance.md @@ -0,0 +1,9 @@ +--- +title: Configure multiple instances · Quickstart +description: Set up named instances (dev, staging, prod) and switch between them. +layout: doc +sidebar: false +aside: false +--- + + diff --git a/docs/quickstart/page-designer.md b/docs/quickstart/page-designer.md new file mode 100644 index 000000000..9fb32c22f --- /dev/null +++ b/docs/quickstart/page-designer.md @@ -0,0 +1,9 @@ +--- +title: Page Designer content · Quickstart +description: Walk through the configuration needed to use Page Designer content via the CLI and VS Code extension. +layout: doc +sidebar: false +aside: false +--- + + diff --git a/docs/quickstart/sandbox.md b/docs/quickstart/sandbox.md new file mode 100644 index 000000000..71933a8c0 --- /dev/null +++ b/docs/quickstart/sandbox.md @@ -0,0 +1,9 @@ +--- +title: Manage sandboxes · Quickstart +description: Configure the CLI to create and manage on-demand sandboxes. +layout: doc +sidebar: false +aside: false +--- + + diff --git a/docs/quickstart/scapi-access.md b/docs/quickstart/scapi-access.md new file mode 100644 index 000000000..4008002fc --- /dev/null +++ b/docs/quickstart/scapi-access.md @@ -0,0 +1,9 @@ +--- +title: Configure SCAPI access · Quickstart +description: Set up the OAuth client, role, scopes, and tenant for SCAPI commands. +layout: doc +sidebar: false +aside: false +--- + + diff --git a/docs/quickstart/script-api-docs.md b/docs/quickstart/script-api-docs.md new file mode 100644 index 000000000..901e4856a --- /dev/null +++ b/docs/quickstart/script-api-docs.md @@ -0,0 +1,9 @@ +--- +title: Download API Docs · Quickstart +description: Search and read B2C Script API documentation offline, with optional instance-specific downloads. +layout: doc +sidebar: false +aside: false +--- + + diff --git a/docs/quickstart/slas-clients.md b/docs/quickstart/slas-clients.md new file mode 100644 index 000000000..ddaa43666 --- /dev/null +++ b/docs/quickstart/slas-clients.md @@ -0,0 +1,9 @@ +--- +title: Manage SLAS clients · Quickstart +description: Configure the CLI to create and inspect SLAS clients for headless storefronts. +layout: doc +sidebar: false +aside: false +--- + + diff --git a/docs/quickstart/vscode-extension.md b/docs/quickstart/vscode-extension.md new file mode 100644 index 000000000..85a02a675 --- /dev/null +++ b/docs/quickstart/vscode-extension.md @@ -0,0 +1,9 @@ +--- +title: Install the VS Code extension · Quickstart +description: Set up the B2C DX VS Code extension and pick the features you want. +layout: doc +sidebar: false +aside: false +--- + + diff --git a/docs/scripts/check-adventure-anchors.ts b/docs/scripts/check-adventure-anchors.ts new file mode 100644 index 000000000..4550fcbf8 --- /dev/null +++ b/docs/scripts/check-adventure-anchors.ts @@ -0,0 +1,201 @@ +#!/usr/bin/env tsx +// +// Validates that every doc anchor referenced by a Setup Adventure (step +// `docAnchor` and checklist `href`) resolves to an actual heading in the +// corresponding source `.md` file. Run from `docs/` via `tsx`. Fails with a +// non-zero exit code on any missing anchor. + +import fs from 'node:fs'; +import path from 'node:path'; +import url from 'node:url'; +import {adventures, flags} from '../.vitepress/data/adventures/index.js'; +import type {Adventure, AdventureState, DocAnchor, Step} from '../.vitepress/data/adventures/_types.js'; + +const __filename = url.fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const DOCS_DIR = path.resolve(__dirname, '..'); + +// Slugify a heading the way markdown-it-anchor does (which VitePress uses): +// trim, lowercase, drop characters that aren't word/space/hyphen, replace +// spaces with hyphens. Honours explicit `{#custom-id}` overrides. +function slugify(heading: string): string { + const explicit = heading.match(/\{#([^}]+)\}\s*$/); + if (explicit) return explicit[1].trim(); + return heading + .toLowerCase() + .trim() + .replace(/[`*_~]/g, '') + .replace(/<[^>]+>/g, '') + .replace(/[^\p{L}\p{N}\s-]/gu, '') + .replace(/\s+/g, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, ''); +} + +const anchorsByFile = new Map>(); + +function loadAnchors(filePath: string): Set { + const cached = anchorsByFile.get(filePath); + if (cached) return cached; + const out = new Set(); + if (!fs.existsSync(filePath)) { + anchorsByFile.set(filePath, out); + return out; + } + const lines = fs.readFileSync(filePath, 'utf8').split(/\r?\n/); + let inFence = false; + for (const line of lines) { + if (/^```/.test(line)) { + inFence = !inFence; + continue; + } + if (inFence) continue; + const m = line.match(/^#{1,6}\s+(.+?)\s*$/); + if (!m) continue; + out.add(slugify(m[1])); + } + anchorsByFile.set(filePath, out); + return out; +} + +function resolveDocFile(docPath: string): string { + // Adventures use paths without `.md`. Try `.md` then `/index.md`. + const trimmed = docPath.replace(/\/$/, ''); + const candidates = [ + path.join(DOCS_DIR, `${trimmed}.md`), + path.join(DOCS_DIR, trimmed, 'index.md'), + ]; + for (const c of candidates) if (fs.existsSync(c)) return c; + return candidates[0]; +} + +interface Issue { + adventure: string; + source: string; + anchor: DocAnchor; + reason: string; +} + +const issues: Issue[] = []; + +function check(adventureId: string, source: string, a: DocAnchor) { + if (/^https?:\/\//.test(a.path)) return; // external URLs aren't ours to validate + const file = resolveDocFile(a.path); + if (!fs.existsSync(file)) { + issues.push({adventure: adventureId, source, anchor: a, reason: `Source file not found: ${file}`}); + return; + } + if (!a.hash) return; // top-of-page is always fine + const anchors = loadAnchors(file); + if (!anchors.has(a.hash)) { + issues.push({ + adventure: adventureId, + source, + anchor: a, + reason: `Anchor "#${a.hash}" not found in ${path.relative(DOCS_DIR, file)}`, + }); + } +} + +function mergeContrib(accum: AdventureState, contrib: AdventureState | undefined) { + if (!contrib) return; + for (const [k, v] of Object.entries(contrib)) { + if (Array.isArray(v)) { + const prev = accum[k]; + const merged = Array.isArray(prev) ? [...prev, ...v] : [...v]; + accum[k] = Array.from(new Set(merged)); + } else { + accum[k] = v; + } + } +} + +// DFS over an adventure's choice tree to enumerate every reachable state. +// Multi-select steps fan out to {each pick alone, all picks together} — +// enough subsets to cover synthesizer branches without 2^n explosion. +function enumerateStates(adventure: Adventure): AdventureState[] { + const results: AdventureState[] = []; + function visit(stepIdx: number, accum: AdventureState) { + const visibleSteps = adventure.stepOrder + .map((id) => adventure.steps[id]) + .filter((s) => !s.showIf || s.showIf(accum, flags)); + if (stepIdx >= visibleSteps.length) { + results.push({...accum}); + return; + } + const step: Step = visibleSteps[stepIdx]; + const choices = step.choices(accum, flags).filter((c) => !c.featureFlag || flags[c.featureFlag]); + if (choices.length === 0) { + results.push({...accum}); + return; + } + if (step.multiSelect) { + const subsets = [...choices.map((c) => [c]), choices]; + for (const subset of subsets) { + const next = {...accum}; + for (const c of subset) mergeContrib(next, c.contributes); + visit(stepIdx + 1, next); + } + } else { + for (const c of choices) { + const next = {...accum}; + mergeContrib(next, c.contributes); + visit(stepIdx + 1, next); + } + } + } + visit(0, {}); + return results; +} + +// Extract internal `[text](/path#hash)` links from a markdown blob and run +// each through the anchor validator. External URLs and pure anchors get +// passed over. +function checkMarkdownLinks(advId: string, source: string, md: string | undefined) { + if (!md) return; + const linkRe = /\[[^\]]+\]\(([^)\s]+)\)/g; + let m: RegExpExecArray | null; + while ((m = linkRe.exec(md)) !== null) { + const url = m[1]; + if (!url.startsWith('/')) continue; + const [pathPart, hashPart] = url.split('#'); + check(advId, source, {path: pathPart, hash: hashPart, label: url}); + } +} + +for (const adventure of adventures) { + for (const stepId of adventure.stepOrder) { + const step = adventure.steps[stepId]; + check(adventure.id, `step:${stepId}`, step.docAnchor); + for (const c of step.choices({}, flags)) { + checkMarkdownLinks(adventure.id, `choice:${stepId}.${c.id}.body`, c.body); + } + } + for (const state of enumerateStates(adventure)) { + const result = adventure.synthesize(state, flags); + for (const item of result.checklist) { + check(adventure.id, `checklist:${item.text}`, item.href); + } + if (result.warnings) { + for (const [i, w] of result.warnings.entries()) { + checkMarkdownLinks(adventure.id, `warning[${i}]`, w); + } + } + } +} + +if (issues.length === 0) { + // eslint-disable-next-line no-console + console.log(`✓ All Quickstart anchors resolve (${adventures.length} guides checked).`); + process.exit(0); +} + +// eslint-disable-next-line no-console +console.error(`✗ ${issues.length} Quickstart anchor issue(s):`); +for (const i of issues) { + // eslint-disable-next-line no-console + console.error( + ` [${i.adventure}] ${i.source}\n → ${i.anchor.path}${i.anchor.hash ? `#${i.anchor.hash}` : ''} (${i.anchor.label})\n ${i.reason}`, + ); +} +process.exit(1); diff --git a/docs/tsconfig.json b/docs/tsconfig.json new file mode 100644 index 000000000..f1d9f8d9c --- /dev/null +++ b/docs/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "lib": ["ESNext", "DOM"], + "types": ["node"], + "esModuleInterop": true, + "isolatedModules": true, + "resolveJsonModule": true, + "verbatimModuleSyntax": false + }, + "include": [ + ".vitepress/config.mts", + ".vitepress/shims-vue.d.ts", + ".vitepress/data/**/*.ts", + ".vitepress/theme/**/*.ts", + "scripts/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist", + ".vitepress/dist", + ".vitepress/cache", + "api" + ] +}