Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quickstart-guides.md
Original file line number Diff line number Diff line change
@@ -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=<preset>`.
9 changes: 9 additions & 0 deletions .claude/skills/cli-command-development/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -345,3 +345,12 @@ See [API Client Development](../api-client-development/SKILL.md#error-handling)
8. Update skill in `skills/b2c-cli/skills/b2c-<topic>/SKILL.md` if exists
9. Update CLI reference docs in `docs/cli/<topic>.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/<id>.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.
146 changes: 142 additions & 4 deletions .claude/skills/documentation/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand All @@ -26,10 +26,21 @@ docs/
│ ├── webdav.md
│ ├── jobs.md
│ └── ...
├── quickstart/ # Interactive Quickstart guides (page shims)
│ ├── index.md # topic listing rendered by <QuickstartIndex/>
│ ├── deploy-code.md
│ ├── jobs.md
│ └── ... # each renders <QuickstartGuide id="<id>" />
├── 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
└── <id>.ts # one file per guide
```

## Documentation Types
Expand Down Expand Up @@ -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/<id>.md` is just a 3-line shim:

```md
---
title: My guide · Quickstart
description: Short tagline.
layout: doc
sidebar: false
aside: false
---

<QuickstartGuide id="my-guide" />
```

The real content lives in `docs/.vitepress/data/adventures/<id>.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.

Expand Down Expand Up @@ -367,6 +482,10 @@ b2c <topic> <command> --flag value
1. Update `docs/cli/<topic>.md` with command documentation
2. Update `docs/.vitepress/config.mts` sidebar if new topic
3. Update `skills/b2c-cli/skills/b2c-<topic>/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

Expand All @@ -380,17 +499,36 @@ b2c <topic> <command> --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/<id>.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/`)

Expand Down
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
152 changes: 151 additions & 1 deletion docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,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<string, Set<string>>();
const loadAnchors = (filePath: string) => {
const cached = anchorsByFile.get(filePath);
if (cached) return cached;
const out = new Set<string>();
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<string, boolean | number | string | string[] | undefined>;
type Contrib = Record<string, boolean | number | string | string[] | undefined>;
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).`);
}

// Build configuration from environment
const isDevBuild = process.env.IS_DEV_BUILD === 'true';

Expand Down Expand Up @@ -216,8 +364,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);
},

// Show deeper heading levels in the outline; register group-icons md plugin
Expand Down Expand Up @@ -270,6 +419,7 @@ export default defineConfig({
},
nav: [
{text: 'Guides', link: '/guide/'},
{text: 'Quickstart', link: '/quickstart/'},
{text: 'Skills', link: '/guide/agent-skills'},
{text: 'VS Code', link: '/vscode-extension/'},
{text: 'MCP', link: '/mcp/'},
Expand Down
Loading
Loading