From ee02c1254c3fe6e50e42edadee2fd1512cfdca89 Mon Sep 17 00:00:00 2001 From: raj pandey Date: Wed, 10 Jun 2026 18:27:06 +0530 Subject: [PATCH 1/2] feat: add contentful-to-contentstack migration companion skill (beta) --- .../SKILL.md | 933 ++++++++++++++++++ ...NTFUL_TO_CONTENTSTACK_MIGRATION_CONTEXT.md | 907 +++++++++++++++++ .../scripts/01_contentful_residue.sh | 27 + .../scripts/02_field_access.sh | 17 + .../scripts/03_sdk_init.sh | 28 + .../scripts/04_query_builder.sh | 21 + .../scripts/05_references.sh | 27 + .../scripts/06_richtext.sh | 26 + .../scripts/07_assets.sh | 20 + .../scripts/08_locales.sh | 17 + .../scripts/09_graphql.sh | 18 + .../scripts/10_livepreview.sh | 34 + .../scripts/11_build_typecheck.sh | 54 + .../scripts/12_secrets.sh | 27 + .../scripts/13_todos_report.sh | 10 + .../scripts/_lib.sh | 109 ++ .../scripts/check_prereqs.py | 184 ++++ .../scripts/log.sh | 97 ++ .../scripts/parse_import_summary.py | 168 ++++ .../scripts/run-all.sh | 77 ++ 20 files changed, 2801 insertions(+) create mode 100644 skills/contentstack-migration-companion-beta/SKILL.md create mode 100644 skills/contentstack-migration-companion-beta/references/CONTENTFUL_TO_CONTENTSTACK_MIGRATION_CONTEXT.md create mode 100755 skills/contentstack-migration-companion-beta/scripts/01_contentful_residue.sh create mode 100755 skills/contentstack-migration-companion-beta/scripts/02_field_access.sh create mode 100755 skills/contentstack-migration-companion-beta/scripts/03_sdk_init.sh create mode 100755 skills/contentstack-migration-companion-beta/scripts/04_query_builder.sh create mode 100755 skills/contentstack-migration-companion-beta/scripts/05_references.sh create mode 100755 skills/contentstack-migration-companion-beta/scripts/06_richtext.sh create mode 100755 skills/contentstack-migration-companion-beta/scripts/07_assets.sh create mode 100755 skills/contentstack-migration-companion-beta/scripts/08_locales.sh create mode 100755 skills/contentstack-migration-companion-beta/scripts/09_graphql.sh create mode 100755 skills/contentstack-migration-companion-beta/scripts/10_livepreview.sh create mode 100755 skills/contentstack-migration-companion-beta/scripts/11_build_typecheck.sh create mode 100755 skills/contentstack-migration-companion-beta/scripts/12_secrets.sh create mode 100755 skills/contentstack-migration-companion-beta/scripts/13_todos_report.sh create mode 100755 skills/contentstack-migration-companion-beta/scripts/_lib.sh create mode 100644 skills/contentstack-migration-companion-beta/scripts/check_prereqs.py create mode 100755 skills/contentstack-migration-companion-beta/scripts/log.sh create mode 100644 skills/contentstack-migration-companion-beta/scripts/parse_import_summary.py create mode 100755 skills/contentstack-migration-companion-beta/scripts/run-all.sh diff --git a/skills/contentstack-migration-companion-beta/SKILL.md b/skills/contentstack-migration-companion-beta/SKILL.md new file mode 100644 index 0000000..e14cef9 --- /dev/null +++ b/skills/contentstack-migration-companion-beta/SKILL.md @@ -0,0 +1,933 @@ +--- +name: contentstack-migration-companion-beta +description: Migration companion for moving to Contentstack from another CMS. Guides content and application migration through a structured, step-by-step workflow. Currently supports migrations from Contentful, with additional source platforms planned. Use when users want to migrate, move, switch, port, or re-platform to Contentstack, including content models, content, assets, locales, application integrations, or website code. Trigger on requests such as "migrate to Contentstack", "move to Contentstack", "switch to Contentstack", "migrate from Contentful", "move my Contentful space", "port my CMS", or similar migration-related requests. The skill validates prerequisites, guides the migration process, provides progress checkpoints, and delivers a completion summary. Prefer this skill whenever a Contentstack migration is requested. +--- + +# contentstack-migration-companion-beta + +Guide a user through migrating a project from **Contentful** to **Contentstack** — +first the **content** (content types, entries, assets, locales) via the Contentstack +CLI migrate plugin, then the **website code** that reads from the CMS. + +This is a sequential workflow where **each step produces output that the next step +consumes** (the create command produces a populated stack and a bundle with credentials, +which feeds the code migration). Treat the artifact paths and counts that each command +prints as state you must capture and carry forward. + +## Operating principles + +Follow these throughout — they matter more than any single command: + +- **Work in a unique session workspace.** At the very start of Step 1, create a + session-scoped directory by running: + ```bash + SESSION_ID=$(date +%Y%m%d-%H%M%S) && SESSION_DIR="/tmp/migrate-to-cs/$SESSION_ID" && mkdir -p "$SESSION_DIR" && echo "SESSION_DIR=$SESSION_DIR" + ``` + Record the printed `SESSION_DIR` value (e.g. `/tmp/migrate-to-cs/20260608-143022`) and + **carry it as a concrete literal** through every shell command in this migration — do not + regenerate it. This keeps each migration run isolated so concurrent sessions and re-runs + never collide. If the user points you to a different workspace, use their path instead. +- **Bundled scripts & references live next to this skill — resolve them via `{SKILL_DIR}`.** + This skill ships helper scripts (a `scripts/` folder) and reference docs (a `references/` + folder) **alongside this `SKILL.md` file**. Wherever these instructions write `{SKILL_DIR}`, + substitute the absolute path of the directory this `SKILL.md` was loaded from — i.e. the + skill's own install directory. **Do not assume a fixed path.** The location differs by AI + assistant, by OS, and by whether the skill was installed per-project or per-user — for example + it may be `/.claude/skills/contentstack-migration-companion-beta`, + `~/.claude/skills/contentstack-migration-companion-beta`, or a Windows path like + `%USERPROFILE%\.claude\skills\contentstack-migration-companion-beta`. Determine the real path once + (it is the folder you read this `SKILL.md` from; if unsure, search the workspace and home + directory for `*/contentstack-migration-companion-beta/SKILL.md`), and for shell commands set it as + a variable up front (`SKILL_DIR=""`) so bundled scripts can be invoked as + `"$SKILL_DIR/scripts/"`. The bundled scripts self-locate their own siblings, so once you + invoke them by absolute path they work regardless of your current directory. +- **One step at a time, and show the result.** After each command, surface the meaningful + output to the user — the summary tables, the counts, the artifact path — not a wall of + raw logs. The user is watching this like a progress bar; give them a clean status, then + the path/handle the next step needs. +- **Track migration progress with the checklist.** At each `[PROGRESS]` trigger below, output + a progress block in your response using these emoji: `✅` = completed, `⏳` = currently running, + `⬜` = not yet started. Example for Step 3 in progress: + ``` + **Migration progress** + - ✅ Step 1 — Prerequisites & inputs + - ✅ Step 2 — Install migrate plugin + - ⏳ Step 3 — Content Migration + - ⬜ Step 4 — Code Migration + ``` + Output it at two moments: (1) at the start of each step, and (2) when each step's eval passes. + Only one step is `⏳` at a time. Step 4 stays `⏳` through all sub-steps 4.1–4.6. + Step 5 (Welcome) is **not** tracked — it triggers automatically once Step 4 is ✅. +- **Gate the destructive or expensive steps.** Confirm before logging in, before creating a + new stack, and before editing the user's code. These either touch live accounts or modify + their repo, so a quick "ready to proceed?" prevents nasty surprises. +- **Capture the outputs explicitly.** When a command prints a bundle path, a log directory, + or a stack API key, record the exact path/value and reuse it verbatim. Do not guess paths — + read them back from the command output. +- **Browser-based login is normal here.** `csdx auth:login --oauth` opens the user's browser; + the terminal then blocks and auto-detects when they finish. Run it, tell the user to complete + login in the browser, and simply wait for the command to return — do not try to script the + browser or kill the command. +- **Currently Contentful is the only supported source.** Do not ask the user which legacy + platform they're on; assume Contentful. (The CLI flag is `--legacy contentful`.) +- **If a step fails, stop and diagnose** rather than barrelling ahead. Most failures here are + recoverable (expired token → re-login, missing content model → inform the user), and the + relevant recovery is described in the step that can fail. +- **Never display code in your text output.** Do not show shell commands, code snippets, + scripts, or any fenced code blocks (``` blocks) in your chat messages at any point during + the migration. Just run commands silently and report the result in plain prose. The user + sees tool calls in the tool panel — repeating code in chat is noise. + +## The migration at a glance + +| # | Step | Command (core) | Produces | +| --- | -------------------------- | ------------------------------------------ | -------------------------------------- | +| 1 | Prerequisites & inputs | prereq check script | verified env + gathered inputs | +| 2 | Install migrate plugin | `csdx plugins:link .` (or `plugins:add`) | `csdx migrate:*` available | +| 3 | Content Migration | `csdx migrate:create` | populated stack + bundle + credentials | +| 4 | Code Migration | detect → plan → rewrite → eval (13 checks) | rewritten data layer | +| 5 | Welcome to Contentstack 🎉 | — | celebration + next steps | + +Work through them in order. The sections below give the exact commands, what to show the +user, and what to carry forward. + +> **Self-contained skill.** Everything Step 4 needs — the full code-migration procedure, the +> Contentful → Contentstack SDK reference (`references/`), and the eval scripts (`scripts/`) — +> ships inside this one skill. There is no separate code-migration skill to install; resolve the +> bundled files via `{SKILL_DIR}` as described above. + +--- + +## Step 1 — Prerequisites & inputs + +> **[PROGRESS]** Output the migration progress block: +> Step 1 → `"in_progress"`, Steps 2–4 → `"pending"`. + +### 1.0 — Create session workspace + +Before doing anything else, create the unique session directory for this migration run: + +```bash +SESSION_ID=$(date +%Y%m%d-%H%M%S) && SESSION_DIR="/tmp/migrate-to-cs/$SESSION_ID" && mkdir -p "$SESSION_DIR" && echo "SESSION_DIR=$SESSION_DIR" +``` + +**Record the printed `SESSION_DIR` path exactly** (e.g. `/tmp/migrate-to-cs/20260608-143022`). +Substitute this concrete value wherever these instructions reference `$SESSION_DIR`. +Do not regenerate it — every step in this migration must use the same directory so artifacts +chain correctly. + +Tell the user: "Session workspace created at ``." + +### 1.1 — Detect the Python 3 command + +Run this to find the correct Python 3 command for this environment: + +```bash +if python3 --version 2>&1 | grep -q "Python 3"; then + PYTHON_CMD=python3 +elif python --version 2>&1 | grep -q "Python 3"; then + PYTHON_CMD=python +else + PYTHON_CMD="" +fi +echo "PYTHON_CMD=$PYTHON_CMD" +``` + +**Record `PYTHON_CMD` exactly as printed.** Substitute `$PYTHON_CMD` wherever these instructions +show a Python invocation — do not hardcode `python3` or `python`. + +If `PYTHON_CMD` is empty, stop immediately and tell the user: + +> "Python 3 is required but not found. Install it from python.org or via your package manager +> (e.g. `brew install python3` on macOS, `sudo apt install python3` on Ubuntu, +> or download the installer from python.org on Windows), then try again." + +Do not proceed past this point until Python 3 is detected. + +### 1.2 — Run the prerequisite checker + +Run this single script. It silently evaluates Node.js, installs any missing CLIs (`csdx`, +`contentful`), checks the Contentstack region and login, and checks the Contentful login and +spaces — all in one pass. It outputs a JSON summary: + +```bash +$PYTHON_CMD "{SKILL_DIR}/scripts/check_prereqs.py" +``` + +Parse the JSON result and carry every field forward as session state. + +**Hard blocker:** If the script exits with code 1, Node.js is missing or too old. Stop immediately +and tell the user the exact problem: + +- `node.error == "not_installed"` → "Node.js is not installed. Install it via `nvm install 20` + or from nodejs.org, then try again." +- `node.ok == false` (e.g. `node.version == "v18.x"`) → "Node `` is too old — Node 20+ + is required. Run `nvm install 20 && nvm use 20`, then try again." + +Do not continue past this point until Node 20+ is confirmed. + +### 1.3 — Handle missing Contentstack login (if needed) + +Skip this sub-step if `cs_login.ok` is true in the JSON. + +If `cs_login.ok` is false (needs_login) or `cs_login.org_uid` is null (needs_oauth_reauth), +trigger a fresh OAuth login: + +```bash +csdx auth:login --oauth +``` + +Tell the user: _"A browser window is opening — complete the Contentstack login there, then come +back here."_ Wait for the command to return (it blocks until the browser flow finishes), then +**re-run the prereq checker** (same command as 1.2) to capture the updated email and org UID. + +If the org UID is still missing after the retry, tell the user: + +> "OAuth login did not store an org UID. Please re-run `csdx auth:login --oauth` manually, then +> let me know when done so I can retry." + +### 1.4 — Handle missing Contentful login (if needed) + +Skip this sub-step if `contentful_login.ok` is true in the JSON. + +`contentful login` is interactive (it needs the user to press Y and paste a token) — you cannot +run it as a Bash command. Instruct the user to run it themselves: + +> "Please run this command in your terminal: +> +> ``` +> contentful login +> ``` +> +> It will ask **'Continue login on the browser? (Y/n)'** — press **Y**. +> A browser window will open — sign in there. +> When the browser login completes, the terminal will show **'Paste your token here:'** — +> copy your Management Token from the browser page and paste it, then press Enter. +> Come back here once the login confirms success." + +**Wait for the user to confirm they have completed the login**, then re-run the prereq checker +to pick up the new session. + +> The Management Token is a secret: do not ask the user to share it with you, do not echo it +> back in your summaries, and do not write it into any file in this workspace. + +### 1.5 — Show the prerequisites summary and confirm + +Once `cs_login.ok` and `contentful_login.ok` are both true, display a summary table from the +JSON result. Use ✅ for items that look good and ⚠️ for anything that may need attention: + +| Check | Status | Detail | +| ----------------------- | ----------------------------------------------------- | ----------------------------- | +| Python 3 | ✅ `<$PYTHON_CMD --version output>` | | +| Node.js | ✅ `` | | +| Contentstack CLI (csdx) | ✅ `` | | +| Contentstack region | ⚙️ `` | | +| Contentstack login | ✅ `` | Org UID: `` | +| Contentful CLI | ✅ `` | | +| Contentful login | ✅ ` ` | | + +Then ask this single question: + +> "Everything looks good — ready to proceed? Or would you like to change anything before +> we start? +> +> - **proceed** — start the migration +> - **region** — switch the Contentstack region +> - **contentstack login** — switch the Contentstack account +> - **contentful login** — switch the Contentful account" + +**Wait for the user's answer before continuing.** + +| Answer | Action | +| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| "proceed" / "yes" / any confirmation | Skip to 1.6 | +| "region" | Present the region menu (see below), wait for selection, run `csdx config:set:region `, re-run the prereq checker, re-display the summary | +| "contentstack login" | Run `csdx auth:login --oauth`, wait for browser login, re-run the prereq checker, re-display the summary | +| "contentful login" | Instruct the user to run `contentful login` themselves (same as 1.4), wait for confirmation, re-run the prereq checker, re-display the summary | + +**Region picker** — when the user asks to change the region, show this menu and wait for their +choice before running the command: + +> "Which region is your destination stack in? +> +> 1. AWS-NA — AWS North America +> 2. AWS-EU — AWS Europe +> 3. AWS-AU — AWS Australia +> 4. AZURE-NA — Azure North America +> 5. AZURE-EU — Azure Europe +> 6. GCP-NA — Google Cloud North America +> 7. GCP-EU — Google Cloud Europe" + +Map the number to its region code and run: + +```bash +csdx config:set:region # e.g. csdx config:set:region AWS-EU +``` + +Repeat the summary + question until the user confirms they are ready to proceed. + +### 1.6 — Contentful Space ID (select from list) + +The `contentful_spaces` array in the prereq JSON already holds all accessible spaces (populated +from the `contentful space list` call run inside the checker). + +**If there is exactly one space in the list**, select it automatically — do not ask the user. +Announce the selection: + +> "Found one Contentful space: **My Marketing Site** (`abc123`). Using it automatically." + +Capture its ID as `SPACE_ID` and continue. + +**If there are two or more spaces**, present them as a numbered menu +— **do not ask the user to type or paste a Space ID**: + +``` +1. My Marketing Site (abc123) +2. Developer Sandbox (def456) +… +``` + +Ask: + +> "Which space do you want to migrate? Enter the number." + +**Wait for the answer.** Map the number back to the Space ID and capture it as `SPACE_ID`. + +If the list is empty (no spaces parsed), fall back to asking: + +> "I couldn't list your Contentful spaces. Please enter the Space ID directly." + +Then confirm the token can reach the selected space: + +```bash +contentful space use --space-id +``` + +If this fails, the token likely lacks access to that space — tell the user and ask them to +re-check the selection or switch to a Contentful account that has the right access. + +### Eval — Verify Step 1 before proceeding + +```bash +$PYTHON_CMD --version # must print Python 3.x +node --version # must print v20.x or higher +csdx --version # must print a version number +csdx auth:whoami # must print a logged-in email +contentful --version # must print a version number +contentful space list # must return a list of spaces — not "You have to be logged in" +``` + +**Pass criteria:** + +- `$PYTHON_CMD` detected and exits 0 with `Python 3.x` +- Node major version ≥ 20 +- `csdx auth:whoami` returns an email (not "No user logged in") +- `cs_login.org_uid` is non-null (captured from prereq JSON in step 1.2 / 1.3) +- `contentful space list` returns rows without an auth error +- `SPACE_ID` captured from the user's selection in 1.6 + +> Note: `contentful whoami` is not available in all CLI versions — use `contentful space list` +> to verify authentication instead. + +If every check passes: + +> **[PROGRESS]** Output the migration progress block: +> Step 1 → `"completed"`, Step 2 → `"in_progress"`, Steps 3–4 → `"pending"`. + +Then proceed to Step 2. If any check fails, fix the issue (re-run the relevant sub-step) and re-verify. + +--- + +## Step 2 — Install the migrate CLI plugin + +> **[PROGRESS]** Output the migration progress block: +> Step 1 → `"completed"`, Step 2 → `"in_progress"`, Steps 3–4 → `"pending"`. + +The `csdx migrate:*` commands come from `@contentstack/cli-external-migrate`. Run this block to +install or update it automatically — it is intentionally silent unless something changes or fails: + +```bash +PLUGIN_NAME="@contentstack/cli-external-migrate" + +# Version currently installed (empty string if not installed) +INSTALLED=$(csdx plugins 2>/dev/null \ + | grep -F "$PLUGIN_NAME" \ + | grep -oE '[0-9]+\.[0-9]+\.[0-9]+[^ ]*' \ + | head -1) + +# Latest version on npm +LATEST=$(npm view "$PLUGIN_NAME" version 2>/dev/null) + +if [ -z "$LATEST" ]; then + echo "PLUGIN_NPM_UNAVAILABLE" +elif [ -z "$INSTALLED" ]; then + csdx plugins:install "$PLUGIN_NAME" && echo "PLUGIN_INSTALLED:$LATEST" || echo "PLUGIN_INSTALL_FAILED" +elif [ "$INSTALLED" = "$LATEST" ]; then + echo "PLUGIN_UP_TO_DATE:$INSTALLED" +else + csdx plugins:uninstall "$PLUGIN_NAME" \ + && csdx plugins:install "$PLUGIN_NAME" \ + && echo "PLUGIN_UPDATED:$INSTALLED→$LATEST" \ + || echo "PLUGIN_UPDATE_FAILED:$INSTALLED→$LATEST" +fi +``` + +Interpret the last line: + +- **`PLUGIN_UP_TO_DATE:`** → already on the latest version; proceed to the Eval. (Stay + silent — don't report "already up to date" to the user.) +- **`PLUGIN_INSTALLED:`** → freshly installed. Briefly tell the user, then proceed. +- **`PLUGIN_UPDATED:`** → uninstalled old version and installed latest. Briefly tell + the user the plugin was updated, then proceed. +- **`PLUGIN_INSTALL_FAILED`** / **`PLUGIN_UPDATE_FAILED:`** → the install or + reinstall errored. Show the user the error output, then display this message and wait: + + > The automatic plugin install failed. Please run the following command manually in your + > terminal, then click **Continue** when done: + > + > ``` + > csdx plugins:install @contentstack/cli-external-migrate + > ``` + + After the user clicks Continue, skip straight to the **Eval** below to verify the plugin is + working before proceeding. + +- **`PLUGIN_NPM_UNAVAILABLE`** → `npm view` returned nothing (network issue or package not yet + published). Show the user this message and wait: + + > Could not reach npm to check the plugin version. Please run the following command manually + > in your terminal, then click **Continue** when done: + > + > ``` + > csdx plugins:install @contentstack/cli-external-migrate + > ``` + + After the user clicks Continue, proceed to the **Eval** below. + +### Eval — Verify Step 2 before proceeding + +```bash +csdx migrate --help +``` + +**Pass criteria:** + +- Output includes `migrate:create` (the one-shot command used in Step 3) + +If `migrate:create` is present, update the migration checklist to set cf-step2="completed" and cf-step3="in_progress", then proceed to Step 3. If the command errors or `migrate:create` is missing, re-run the install block above and re-verify. + +--- + +## Step 3 — Content Migration + +> **[PROGRESS]** Output the migration progress block: +> Steps 1–2 → `"completed"`, Step 3 → `"in_progress"`, Step 4 → `"pending"`. + +This single command exports the Contentful space, converts the content to a Contentstack +bundle, and imports it into a brand-new stack — all in one shot. The master locale is +auto-detected from the export's default locale. + +### 3.0 — Retrieve the stored Management Token + +The Contentful CLI stores the token from the login in `~/.contentfulrc.json`. Read it now +so `migrate:create` can use it without prompting: + +```bash +$PYTHON_CMD -c " +import json, sys, pathlib +for p in ['~/.contentfulrc.json', '~/.config/contentful/config.json']: + f = pathlib.Path(p).expanduser() + if f.exists(): + d = json.loads(f.read_text()) + tok = d.get('managementToken') or d.get('cmaToken') or d.get('management_token') + if tok: + print(tok) + sys.exit(0) +print('NOT_FOUND', file=sys.stderr) +sys.exit(1) +" +``` + +Capture the printed value as `CONTENTFUL_MANAGEMENT_TOKEN`. **Do not display it to the user +or write it to any file.** + +If the token is NOT found (exits 1), tell the user the token could not be resolved. Ask them +to run `contentful login` again, then retry. + +### 3.1 — Confirm before running + +Tell the user: + +> "Ready to start the content migration. This will export your Contentful space, convert +> the content, and import it into a new stack under your org. Shall I proceed?" + +**Wait for confirmation before running the command.** + +### 3.2 — Run migrate:create (output captured to log file) + +Run from `$SESSION_DIR` so that the import `logs/` directory and `_backup_*/` are written +there, keeping the user's project directory clean. Pipe through `tee` so output streams live +and is also saved for parsing: + +```bash +cd "$SESSION_DIR" && \ +csdx migrate:create --legacy contentful \ + --space-id "$SPACE_ID" \ + --management-token "$CONTENTFUL_MANAGEMENT_TOKEN" \ + --org "$ORG_UID" \ + --download-assets \ + -o "$SESSION_DIR" \ + --workspace "$SESSION_DIR" \ + -y \ + 2>&1 | tee "$SESSION_DIR/migrate-create.log" +``` + +Flag notes: + +- `--space-id` / `--management-token` — Contentful source (token resolved in 3.0) +- `--org` — the org UID captured in Step 1; a new stack is created here +- `--download-assets` — include asset binaries in the migration +- `-o "$SESSION_DIR"` — bundle written to `$SESSION_DIR/bundle/` +- `--workspace "$SESSION_DIR"` — export JSON saved to `$SESSION_DIR/export.json` +- `-y` — skip internal confirmation prompts (we already confirmed above) +- `cd "$SESSION_DIR"` — ensures `logs/` and `_backup_*/` land in the session dir, not CWD + +The command runs three phases and prints this progression: + +**Phase 1 — Export** (streams fetch progress, ends with the entity-count table): + +``` +┌────────────────────────┐ +│ Exported entities │ +├───────────────────┬────┤ +│ Content Types │ 16 │ +│ Entries │ 53 │ +│ Assets │ 21 │ +│ Locales │ 2 │ +│ … │ … │ +└───────────────────┴────┘ +Stored space data to json file at: /export.json +``` + +**Phase 2 — Convert** (transforms export into Contentstack bundle): + +``` + validate ✓ export.json + extract ✓ 2 locales · 16 types + transform ✓ 53 entries · 13 types → /bundle + Bundle: /bundle (16 types, 53 entries) +``` + +**Phase 3 — Import** (creates stack, imports content, ends with the summary box): + +``` +✓ Stack created · via cma +────────────────────────────────────── + Stack name : Contentful Migration 2026-06-08 + Stack key : blt3e69b8da307655a7 + Region : AWS-NA +────────────────────────────────────── +… (import progress) … +SUCCESS: Successfully imported the content to the stack named … with the API key blt… . +SUCCESS: The log has been stored at: /logs +✓ Bundle metadata written: /bundle/metadata.json +✓ Migration complete +────────────────────────────────────── + Stack name : Contentful Migration 2026-06-08 + Stack key : blt3e69b8da307655a7 + Region : AWS-NA +────────────────────────────────────── +``` + +### 3.3 — Parse the output and extract session variables + +Once the command returns, parse the captured log with exact patterns matching the real output: + +```bash +$PYTHON_CMD - <<'PYEOF' +import re, pathlib, os + +raw = pathlib.Path(os.environ["SESSION_DIR"] + "/migrate-create.log").read_text() +# Strip ANSI escape codes — the import phase wraps paths in color sequences +log = re.sub(r'\x1b\[[0-9;]*m', '', raw) + +# Patterns matched against real command output +export_json = re.search(r'Stored space data to json file at:\s+(\S+)', log) +bundle_dir = re.search(r'Bundle:\s+(\S+)', log) +stack_name = re.findall(r'Stack name\s*:\s*(.+)', log) +stack_key = re.findall(r'Stack key\s*:\s*(blt\w+)', log) +region = re.findall(r'Region\s*:\s*(\S+)', log) +log_dir = re.search(r'SUCCESS: The log has been stored at:\s+(\S+)',log) +metadata = re.search(r'Bundle metadata written:\s+(\S+)', log) + +session_dir = os.environ["SESSION_DIR"] +# Use last match for stack fields — the final summary box is authoritative +print("EXPORT_JSON=" + (export_json.group(1).strip() if export_json else session_dir + "/export.json")) +print("BUNDLE_DIR=" + (bundle_dir.group(1).strip() if bundle_dir else session_dir + "/bundle")) +print("STACK_NAME=" + (stack_name[-1].strip() if stack_name else "UNKNOWN")) +print("STACK_KEY=" + (stack_key[-1].strip() if stack_key else "UNKNOWN")) +print("REGION=" + (region[-1].strip() if region else "UNKNOWN")) +print("LOG_DIR=" + (log_dir.group(1).strip() if log_dir else "UNKNOWN")) +print("METADATA_PATH=" + (metadata.group(1).strip() if metadata else session_dir + "/bundle/metadata.json")) +PYEOF +``` + +Capture each printed `KEY=value` as a session variable: + +| Variable | Exact source line in the log | Used in | +| --------------- | --------------------------------------------------------- | ------------------ | +| `EXPORT_JSON` | `Stored space data to json file at: ` | Eval below | +| `BUNDLE_DIR` | `Bundle: (N types, N entries)` | Derived paths | +| `STACK_NAME` | `Stack name : …` (last occurrence — final summary box) | Step 5 recap | +| `STACK_KEY` | `Stack key : blt…` (last occurrence — final summary box) | Step 5 recap | +| `REGION` | `Region : …` (last occurrence — final summary box) | Step 4 env vars | +| `LOG_DIR` | `SUCCESS: The log has been stored at: ` | Eval below | +| `METADATA_PATH` | `✓ Bundle metadata written: ` | Step 4 credentials | + +Also set the mapper path (always `$BUNDLE_DIR/mapper.json`): + +```bash +MAPPER_PATH="$BUNDLE_DIR/mapper.json" +``` + +If any value is `UNKNOWN`, check `$SESSION_DIR/migrate-create.log` directly for the line and +set it manually before continuing. + +### 3.4 — Show the user a progress summary + +Show the entity-count table and the final stack summary, both taken from the captured log: + +```bash +# Entity count table from Phase 1 (export) +grep -A 18 'Exported entities' "$SESSION_DIR/migrate-create.log" | head -20 + +# Final stack summary box from Phase 3 (last occurrence) +grep -A 4 'Stack name' "$SESSION_DIR/migrate-create.log" | tail -6 +``` + +Report in plain prose: + +> "Content migration complete — stack **`$STACK_NAME`** (`$STACK_KEY`, `$REGION`) is ready." + +### Handle token expiry mid-run + +The Contentstack OAuth token can expire during a long import. The symptom in the log is: + +``` +stack creation failed. + CMA: 401 The provided access token is invalid or expired or revoked +``` + +Re-authenticate and re-run from step 3.1: + +```bash +csdx auth:login --oauth +``` + +The command is safe to re-run — it creates a fresh stack each time. + +### Eval — Verify Step 3 before proceeding + +Confirm the key artifacts exist: + +```bash +ls -lh "$METADATA_PATH" +ls -lh "$MAPPER_PATH" +``` + +Run the bundled import summary parser to verify entity counts match what was exported: + +```bash +$PYTHON_CMD "$SKILL_DIR/scripts/parse_import_summary.py" "$LOG_DIR" --export "$EXPORT_JSON" +``` + +**Pass criteria:** + +- `STACK_KEY` starts with `blt` (a real stack was created) +- `METADATA_PATH` and `MAPPER_PATH` both exist +- `SUCCESS: Successfully imported…` line is present in the captured log +- Imported counts match exported counts; any divergence must be explained before proceeding + +Present the counts table to the user: + +``` +Module Imported (Exported) +Locales 2 2 +Content Types 16 16 +Assets 21 21 +Entries 53 53 +✓ Imported into stack "" () +``` + +If all pass, update the migration checklist to set cf-step3="completed" and cf-step4="in_progress", then +proceed to Step 4. If counts diverge or artifacts are missing, surface the discrepancy and +point the user at `$LOG_DIR` — do not proceed until resolved. + +### Session variables carried into Step 4 + +| Variable | Value | Purpose | +| --------------- | -------------------------------------- | -------------------------------------------------------- | +| `BUNDLE_DIR` | from log (or `$SESSION_DIR/bundle`) | Root of the import bundle | +| `METADATA_PATH` | from `✓ Bundle metadata written:` line | Stack credentials — API key, delivery token, environment | +| `MAPPER_PATH` | `$BUNDLE_DIR/mapper.json` | Contentful → Contentstack field-UID mapping | +| `STACK_NAME` | from final summary box | Step 5 recap | +| `STACK_KEY` | from final summary box | Step 5 recap | +| `REGION` | from final summary box | `.env` setup in Step 4 | + +--- + +## Step 4 — Code Migration + +> **[PROGRESS]** Output the migration progress block: +> Steps 1–3 → `"completed"`, Step 4 → `"in_progress"`. + +With the content now in Contentstack, migrate the **application code** that reads from the CMS. +This step runs a full detect → plan → rewrite → eval cycle with 13 post-migration checks. + +### 4.1 — Collect inputs + +**`repoPath`** — local file system path to the application to migrate. +Ask the user: + +> "What is the local path to the codebase you want to migrate? (e.g. `/Users/you/projects/my-app`)" +> If it's a remote git repo, clone it first: +> +> ```bash +> cd "$SESSION_DIR" && git clone +> ``` +> +> Then use the cloned directory as `repoPath`. + +**`mapperPath`** — the field-mapping JSON produced by Step 3. Confirm it exists: + +```bash +ls -lh "$SESSION_DIR/bundle/mapper.json" +``` + +If the file is at a different path (e.g. the user used a custom workspace), ask them to confirm +the exact path. + +Read `mapperPath` with the Read tool and extract the `fieldMapping` arrays from each content type +to build the Contentful-field-UID → Contentstack-field-UID table. Present the extracted table to +the user for confirmation before proceeding. + +**`metadataPath`** — the credentials JSON written by Step 3's import. Confirm it exists and read it: + +```bash +ls -lh "$SESSION_DIR/bundle/metadata.json" +``` + +Read `metadataPath` with the Read tool. It supplies the new stack's Delivery SDK credentials so +you **do not have to ask the user for them**: + +| metadata.json key | Use as | Notes | +| ----------------- | ------------------- | --------------------------------------------------- | +| `stack_api_key` | `CS_API_KEY` | the `blt…` **Stack API Key** — **not** `stack_id` | +| `delivery_token` | `CS_DELIVERY_TOKEN` | for the published-content SDK | +| `preview_token` | `CS_PREVIEW_TOKEN` | only if the app uses Live Preview (§18) | +| `environment` | `CS_ENVIRONMENT` | e.g. `master` | +| `stack_id` | (stack UID) | identifier only — do **not** use as the SDK api key | + +The region (e.g. `AWS-NA`) comes from `csdx config:get:region`. Treat the tokens as secrets — +never echo them back or write them into files in this workspace; only set them in the migrated +app's local `.env` (which should be gitignored). If `metadata.json` is missing, fall back to +asking the user for `CS_API_KEY`, `CS_DELIVERY_TOKEN`, and `CS_ENVIRONMENT`. + +### 4.2 — Logging + +First resolve `{SKILL_DIR}` (see the "Bundled scripts & references" operating principle) — the +directory this skill is installed in — and set it as a shell variable so every command below can +reach the bundled scripts portably, on any OS or assistant: + +```bash +SKILL_DIR="" # the folder holding this SKILL.md +``` + +From the first action in this step, record everything to the session log using +`"$SKILL_DIR/scripts/log.sh"`: + +- `log.sh user-input ""` — every user input/communication. +- `log.sh decision ""`. +- `log.sh ai-action ""` and `communication ""`. +- `log.sh exception ""` — log all exceptions. +- Run shell commands through it so output + exit codes are captured and failures auto-log as + exceptions: `log.sh run "typecheck" -- npx tsc --noEmit`. + +End with `log.sh summary`. The log lands in `/.migration/` (`session.log` + +`session.jsonl` + per-command output). See `"$SKILL_DIR/scripts/README.md"`. + +### 4.3 — Reference (read first) + +The complete, source-verified mapping for every API, response shape, query operator, rich-text +renderer, asset transform, GraphQL query, and Live Preview API lives in: + +**`{SKILL_DIR}/references/CONTENTFUL_TO_CONTENTSTACK_MIGRATION_CONTEXT.md`** (read it with the +Read tool from this skill's install dir — see the `{SKILL_DIR}` operating principle) + +Read it in full before doing anything. It is the single source of truth — follow it; do not rely +on prior knowledge where the doc is specific. Key map: + +- §0.1 — detect the data-access approach (decision table) +- §1–§16 — REST Delivery SDK (`@contentstack/delivery-sdk`) mapping +- §17 — GraphQL Content API migration +- §18 — Live Preview / draft mode migration +- §19 — raw REST / `fetch` and framework source plugins +- §13 gotchas, §15 checklist + +### 4.4 — Procedure + +1. **DETECT (doc §0.1).** Determine the app's language, framework, and which data-access + approach(es) it uses, plus whether it implements Live Preview / draft mode. Migrate in the SAME + language and framework, preserving the SAME approach (REST→REST, GraphQL→GraphQL, preview→preview). + Report findings and PAUSE for confirmation. + +2. **PREREQUISITES.** Confirm the target Contentstack stack already has the matching content model + and published content, and confirm the Contentful-field-ID → Contentstack-field-UID map from + step 4.1. If any UIDs are unknown, infer from a sample Contentstack entry and FLAG every guess. + Confirm env vars (`CS_API_KEY`, `CS_DELIVERY_TOKEN`, `CS_ENVIRONMENT`, + region/branch, + + preview token if Live Preview) — these come from `metadataPath` (step 4.1), not the user + (use `stack_api_key`, NOT `stack_id`, for `CS_API_KEY`). If the content model or entries do + not yet exist in Contentstack, + STOP — code migration requires content to be imported first (Steps 1–3). + +3. **PLAN.** Produce a table: file:line → source call → Contentstack equivalent (cite the doc + section) → field-UID dependencies → risk notes. Show it and PAUSE before editing any file. + +4. **MIGRATE** per the doc, following the section matching each detected approach: + - REST Delivery SDK: §1–§16. GraphQL: §17. Raw REST / framework plugins: §19. + - Rich text / assets / locales / pagination: §9–§12. + Make minimal, mechanical edits that match surrounding code style and the framework's existing + data-fetching idioms. Preserve function/component contracts. + +5. **LIVE PREVIEW.** If step 1 found preview/draft-mode, reimplement it per §18, matching the + source's scope (routes, components, SSR vs client, click-to-edit vs read-only). Keep preview + tokens server-side and preserve existing preview gating/routing. + +6. **VERIFY — run the eval suite (this is mandatory, not optional).** + Install deps first so the build eval is meaningful, then run the bundled evals in parallel: + ```bash + bash "$SKILL_DIR/scripts/run-all.sh" + ``` + For maximum parallelism you may instead spawn one agent per `"$SKILL_DIR/scripts/"`NN\_\*.sh. + See `"$SKILL_DIR/scripts/README.md"` for what each check catches and exit-code semantics. + - **Hard-gate FAIL/ERROR (residue, field-access, sdk-init, build, secrets) ⇒ the migration is NOT + done.** Fix and re-run until they pass. + - **Triage every review-eval finding** at its `file:line` — static greps flag _suspects_, not + proven bugs. Fix the true positives; state explicitly why any remaining ones are safe. Never + dismiss findings silently. + - A green build is necessary but **not sufficient** — it cannot catch reference-array bugs, wrong + field UIDs, or RTE output. Still smoke-test live queries against the real Contentstack stack. + - For anything the doc marks "verify against current docs" (GraphQL hosts/headers, Live Preview + front-end API), confirm before finalizing rather than guessing. + +### 4.5 — Guardrails + +- Do NOT modify content modeling or move/import entries — out of scope for this step. +- Convert every reference dereference to safe array access (`?.[0]`) and audit null safety + (Contentstack resolves references to arrays, not single objects). +- When a UID or behavior is uncertain, leave a `// TODO(migration):` comment and list it — never + guess silently. + +### 4.6 — Wrap up + +> **[PROGRESS]** Output the migration progress block: +> Steps 1–4 all → `"completed"`. Then immediately proceed to Step 5. + +Give the user a final summary of the whole migration journey: + +- **Content:** counts imported into the stack (from Step 3). +- **Code:** files changed, the rich-text rendering strategy chosen, guessed UIDs to verify, and + any `TODO(migration)` call sites still needing attention. +- **Eval results:** whether all hard gates passed and which review findings remain to triage. +- **Detection summary:** language/framework/approach(es) detected; whether Live Preview was + present and how it was reimplemented; every guessed UID; every TODO; and any call site not + fully migrated (geo queries, `locale: '*'`, cross-space ResourceLinks, GraphQL features without + an equivalent). +- **Next steps:** set the `CS_*` env vars in the app's local `.env` from `metadata.json` + (`CS_API_KEY` = `stack_api_key`, `CS_DELIVERY_TOKEN` = `delivery_token`, `CS_ENVIRONMENT` = + `environment`, plus `CS_PREVIEW_TOKEN` = `preview_token` if Live Preview, and the region from + `csdx config:get:region`), run the app against Contentstack, and verify pages render. + +--- + +## Step 5 — Welcome to Contentstack 🎉 + +The migration is complete. Display the following message to the user — render it exactly as shown, preserving the section headings, icons, and line breaks: + +--- + +![Contentstack](https://images.contentstack.io/v3/assets/blt77d44a06c81b1730/blt2e24a315fedaeaf7/68bc10f25f14881bc908b6c2/CS_OnlyLogo.webp) + +``` +╔══════════════════════════════════════════════════════════════════╗ +║ ║ +║ 🚀 Welcome to Contentstack! ║ +║ ║ +║ The Agentic Experience Platform ║ +║ ║ +╚══════════════════════════════════════════════════════════════════╝ +``` + +> _"The world's best digital experiences run on Contentstack."_ + +**🎊 Your migration is complete.** + +Your content, content types, assets, and website code have all successfully moved from Contentful to Contentstack. Here's a quick recap of what just happened: + +| ✅ | What was migrated | +| --- | --------------------------------- | +| 📦 | Content types & field definitions | +| 🖼️ | Assets & media | +| 📝 | Entries & localized content | +| 💻 | Website data-layer code | + +### 📂 Where to find your migrated files + +Using the exact paths captured during this session, fill in and display this table: + +| Artifact | Location | +| --------------------------------- | ------------------------------------------------------ | +| 🗂️ **Contentful export** | The `export.json` path in `$SESSION_DIR` (from Step 3) | +| 📦 **Contentstack import bundle** | `$SESSION_DIR/bundle/` (from Step 3) | +| 🔑 **Stack credentials** | `$SESSION_DIR/bundle/metadata.json` (from Step 3) | +| 📋 **Import logs** | The log directory printed by `migrate:create` (Step 3) | +| 💻 **Migrated website code** | The `repoPath` the user provided in Step 4 | +| 🔍 **Code migration log** | `/.migration/session.log` | + +Do not use default or guessed paths — substitute only the real paths you captured from each step's command output. + +--- + +### 🌟 You're now on the platform trusted by the world's top brands + +Contentstack powers digital experiences for enterprises across retail, media, finance, and beyond — delivering content at scale, across every channel, without compromise. + +Here's what you've unlocked: + +| Capability | What it means for you | +| --------------------------------- | --------------------------------------------------------------- | +| ⚡ **Composable architecture** | Mix and match best-of-breed tools — your stack, your way | +| 🌍 **Multi-region delivery** | Content served fast, anywhere on the globe | +| 🔄 **Omnichannel publishing** | Web, mobile, IoT, voice — one content hub for all | +| 🛡️ **Enterprise-grade security** | SOC 2 Type II, GDPR, HIPAA-ready | +| 🤝 **Dedicated support** | Real humans, not just docs — onboarding, migrations, and beyond | +| 🧩 **Marketplace & integrations** | 100+ pre-built connectors — plug in what you already use | + +--- + +### 🚦 Recommended next steps + +1. **Verify your live site** — start your app with the new `CS_*` env vars and confirm pages render against Contentstack. +2. **Explore the Contentstack dashboard** — invite your team, set up roles, and configure publishing workflows. +3. **Set up webhooks & automation** — trigger deploys, Slack alerts, or custom workflows on publish events. +4. **Review the Contentstack Docs** — [contentstack.com/docs](https://www.contentstack.com/docs) is your go-to for Delivery SDK, GraphQL, and Live Preview guides. +5. **Talk to your account team** — let them know you're live; they can unlock additional features and walk you through advanced capabilities. + +--- + +> 💡 **Tip:** Bookmark the [Contentstack Developer Hub](https://www.contentstack.com/developers) — it has SDK references, starter apps, and video tutorials to help you get the most out of your new platform. + +--- + +_Congratulations on completing your migration. +You've made the right call — the world's best digital experiences are built on Contentstack, and now yours will be too._ 🚀 diff --git a/skills/contentstack-migration-companion-beta/references/CONTENTFUL_TO_CONTENTSTACK_MIGRATION_CONTEXT.md b/skills/contentstack-migration-companion-beta/references/CONTENTFUL_TO_CONTENTSTACK_MIGRATION_CONTEXT.md new file mode 100644 index 0000000..d19ab85 --- /dev/null +++ b/skills/contentstack-migration-companion-beta/references/CONTENTFUL_TO_CONTENTSTACK_MIGRATION_CONTEXT.md @@ -0,0 +1,907 @@ +# Contentful → Contentstack Migration Context (for AI Agents) + +> **Purpose of this file.** This is a reference context document for AI coding models tasked +> with migrating a **web application that consumes Contentful** so that it consumes +> **Contentstack** instead. It maps concepts, SDK APIs, query operators, field types, +> rich-text rendering, assets, locales and pagination from the Contentful JS SDKs to the +> Contentstack TypeScript Delivery SDK (`@contentstack/delivery-sdk`) and its helper +> library (`@contentstack/utils`). +> +> **It covers all the common ways a web app consumes Contentful, not just one SDK:** +> the REST Delivery SDK (`contentful`), the **GraphQL** Content API (§17), **Live Preview / +> draft mode** (§18), raw REST / `fetch` access and framework source plugins (§19). Always +> begin by detecting which approach(es) the target app uses (§0.1) and migrate **like-for-like** +> — REST→REST, GraphQL→GraphQL, preview→preview — preserving the app's language and framework. +> +> **Scope:** _application / SDK code migration_ — i.e. rewriting the data-fetching and +> rendering layer of a website. It is **not** a content/data ETL guide (moving entries +> between stacks is a separate task done via the Contentstack Management API / CLI / import +> tooling). Where content-modeling concepts are mentioned, they exist only to help the agent +> map the response shapes correctly. + +--- + +## 0. How an AI agent should use this document + +0. **Detect language, framework, and data-access approach first (§0.1).** Migrate in the + *same* language and framework, and preserve the *same* data-access style (REST SDK → + Delivery SDK; GraphQL → Contentstack GraphQL; raw fetch → raw fetch). Do not change paradigms. +1. **Identify the Contentful surface in the target app.** Search the codebase for + `from 'contentful'`, `from 'contentful-management'`, `createClient`, `getEntries`, + `getEntry`, `getAssets`, `.fields.`, `.sys.`, `@contentful/rich-text-*`, and image URLs + on `fields.file.url`. Also search for GraphQL (`graphql.contentful.com`, `gql`, + `@apollo/client`, `urql`, `graphql-request`, `*.graphql`) and Live Preview + (`preview.contentful.com`, CPA token, `@contentful/live-preview`, `draftMode`). +2. **Classify each call site** using the mapping tables below (client init, read, query, + reference resolution, rich text, asset/image, locale, pagination) — and the approach-specific + sections (§17 GraphQL, §18 Live Preview, §19 raw REST / frameworks). +3. **Rewrite each call site** to the Contentstack equivalent, paying special attention to + the **response-shape differences** (Section 6) — this is the single largest source of + migration bugs. Contentful nests content under `sys`/`fields`; Contentstack flattens it. +4. **Migrate rendering** (rich text, images) using `@contentstack/utils` (Section 9–10). +5. **Migrate Live Preview / draft mode** (§18) if the source app implements it. +6. **Verify** against the gotchas (Section 13) and the checklist (Section 15). + +### 0.1 Detect the data-access approach (decide the migration path) + +| Signal found in the source app | Contentful approach | Migrate to | See | +|---|---|---|---| +| `import { createClient } from 'contentful'`, `getEntry(s)`, `getAsset(s)` | REST Delivery SDK (CDA) | `@contentstack/delivery-sdk` builder | §1–§16 | +| `graphql.contentful.com`, `gql`/`*.graphql`, Apollo/urql/graphql-request | GraphQL Content API | Contentstack GraphQL Content Delivery API (same GraphQL client) | §17 | +| `host: 'preview.contentful.com'`, CPA token, `@contentful/live-preview`, `useContentfulLiveUpdates`, Next `draftMode` | Live Preview / draft mode | Contentstack Live Preview (`live_preview` config + `@contentstack/live-preview-utils`) | §18 | +| Raw `fetch`/`axios` to `cdn.contentful.com/...`, or `gatsby-source-contentful` | Direct REST / build-time source plugin | Contentstack REST endpoints / `@contentstack/gatsby-source-contentstack` | §19 | +| `import 'contentful-management'` | Management API (editorial tooling) | `@contentstack/management` (out of scope here; note it) | §1 | + +A single app may use **several** of these at once (e.g. GraphQL for reads + Live Preview for +the editor). Migrate each surface in kind. + +**Golden rule:** Contentful and Contentstack are both API-first headless CMSs with a similar +mental model (a *space/stack* contains *content types*, *entries*, and *assets*, published to +*environments*). The migration is mostly mechanical *if* the response-shape and +field-addressing differences are handled rigorously. + +--- + +## 1. Package & import mapping + +| Concern | Contentful | Contentstack | +|---|---|---| +| Content **delivery** (read) SDK | `contentful` (CDA/CPA) | `@contentstack/delivery-sdk` | +| Content **management** (write) SDK | `contentful-management` (CMA) | `@contentstack/management` (out of scope here) | +| Shared HTTP/core layer (internal) | `contentful-sdk-core` | `@contentstack/core` | +| Rich-text / utility rendering | `@contentful/rich-text-html-renderer`, `@contentful/rich-text-react-renderer`, `@contentful/rich-text-types` | `@contentstack/utils` | + +```bash +# remove +npm uninstall contentful contentful-management \ + @contentful/rich-text-html-renderer @contentful/rich-text-react-renderer @contentful/rich-text-types + +# add +npm install @contentstack/delivery-sdk @contentstack/utils +# optional, only if cache policies are used: +npm install @contentstack/persistence-plugin +``` + +```ts +// Contentful +import { createClient } from 'contentful' + +// Contentstack +import contentstack from '@contentstack/delivery-sdk' +import * as Utils from '@contentstack/utils' // rich text, embedded items +``` + +> A web app that *reads* content uses the Contentful **CDA** SDK (`contentful`). Some apps +> also use `contentful-management` (CMA) for previews or editorial tooling. This document +> focuses on the read path (CDA → Contentstack Delivery). The CMA query/entity model +> (`client.entry.getMany({ spaceId, environmentId, query })`) shares the same query-operator +> and data-model semantics described below. + +--- + +## 2. Terminology mapping + +| Contentful | Contentstack | Notes | +|---|---|---| +| Space | Stack | Top-level content container. | +| Space ID | API Key | Identifies the container in client init. | +| Content Delivery API token (CDA) | Delivery Token | Read token. | +| Content Preview API token (CPA) | Delivery Token of a *preview* / Live Preview token | Preview handled via `live_preview` config. | +| Environment (`master`, …) | Environment | Both publish to named environments. Required in Contentstack init. | +| Content Type | Content Type | Same concept. UID identifies it. | +| Entry | Entry | Same concept. | +| Asset | Asset | Media file. | +| Field | Field | Addressed differently (see §6). | +| `sys.id` | `uid` | Stable identifier of an entry/asset/content type. | +| Locale (`en-US`) | Locale (`en-us`) | Contentstack locale codes are lower-cased (`en-us`, `fr-fr`). | +| Tag | Tag | Contentstack also has Taxonomies (richer). | +| Rich Text (RichText document) | JSON RTE / Supercharged RTE | Different JSON shape & renderer (see §9). | +| Reference (Link) | Reference field | Resolved via `includeReference` instead of `include` depth. | +| Modular content (Array of links) | Modular Blocks / Reference | — | +| Region/host | Region/host | Contentstack uses an explicit `Region` enum (US/EU/AU/Azure/GCP). | + +--- + +## 3. Client initialization + +### Contentful (CDA) +```ts +import { createClient } from 'contentful' + +const client = createClient({ + space: 'SPACE_ID', + accessToken: 'CDA_TOKEN', + environment: 'master', // optional, defaults to 'master' + host: 'cdn.contentful.com', // 'preview.contentful.com' for CPA +}) +``` + +### Contentstack (Delivery) +```ts +import contentstack, { Region } from '@contentstack/delivery-sdk' + +const stack = contentstack.stack({ + apiKey: 'API_KEY', // was: space + deliveryToken: 'DELIVERY_TOKEN',// was: accessToken + environment: 'production', // REQUIRED (no default) + region: Region.US, // US (default) | EU | AU | AZURE_NA | AZURE_EU | GCP_NA | GCP_EU + locale: 'en-us', // optional default locale + // host: 'custom-cdn.example.com', // optional; overrides region host + // branch: 'main', // optional branch +}) +``` + +**Key init differences (verified against `src/stack/contentstack.ts`):** +- `apiKey`, `deliveryToken`, **and `environment` are all required**; the SDK throws on init if + any is missing. Contentful only requires `space` + `accessToken`. +- Region is a first-class enum. Don't hardcode hostnames unless you need a custom host. + Region → host: `US → cdn.contentstack.io`, `EU → eu-cdn.contentstack.com`, + `AU → au-cdn.contentstack.com`, `AZURE_NA → azure-na-cdn.contentstack.com`, + `AZURE_EU → azure-eu-cdn.contentstack.com`, `GCP_NA → gcp-na-cdn.contentstack.com`, + `GCP_EU → gcp-eu-cdn.contentstack.com`. +- Preview: Contentful switches `host` to `preview.contentful.com` with a CPA token. + Contentstack uses a `live_preview` config object + `stack.livePreviewQuery(...)` instead. +- The Contentstack client is a builder: you start from `stack.contentType(uid)` / + `stack.asset(uid)` and chain. There is no flat `client.getEntries(...)`. + +--- + +## 4. Reading content — method mapping + +Contentstack uses a **fluent builder** rooted at the `stack`. Calls terminate in +`.fetch()` (single object) or `.find()` (collection/query). + +| Operation | Contentful (CDA) | Contentstack | +|---|---|---| +| Single entry by id | `client.getEntry(entryId)` | `stack.contentType(ctUid).entry(entryUid).fetch()` | +| Entries of a content type | `client.getEntries({ content_type: 'blog' })` | `stack.contentType('blog').entry().query().find()` | +| All entries (filter on `content_type`) | `client.getEntries({ content_type })` | `stack.contentType(ctUid).entry().query()...find()` | +| Single asset | `client.getAsset(assetId)` | `stack.asset(assetUid).fetch()` | +| All assets | `client.getAssets(query)` | `stack.asset().query()...find()` (or `stack.asset().find()`) | +| Single content type (schema) | `client.getContentType(id)` | `stack.contentType(uid).fetch()` | +| All content types | `client.getContentTypes()` | `stack.contentType().find()` | +| Sync API | `client.sync({ initial: true })` | `stack.sync({ ... })` | +| Global field (Contentstack-only) | — | `stack.globalField(uid).fetch()` / `stack.globalField().find()` | +| Taxonomy query (Contentstack-only) | — | `stack.taxonomy()` | + +> **Important:** In Contentful, `content_type` is just a query parameter. In Contentstack, +> the content type UID is **part of the path** (`stack.contentType(uid)…`). A single +> Contentful `getEntries` call that mixed multiple content types must be split per content +> type in Contentstack, **or** use the asset/entry query without a content type only for assets. + +### Canonical examples + +```ts +// --- Single entry --- +// Contentful +const entry = await client.getEntry('blog123') +// Contentstack +const entry = await stack.contentType('blog_post').entry('blog123').fetch() + +// --- Collection --- +// Contentful +const res = await client.getEntries({ content_type: 'blog_post', limit: 10 }) +res.items.forEach(...) +// Contentstack +const res = await stack.contentType('blog_post').entry().query().limit(10).find() +res.entries?.forEach(...) +``` + +--- + +## 5. Query operator & modifier mapping + +Contentful expresses queries as a flat params object with bracketed operators +(`'fields.price[gte]': 10`). Contentstack uses a `Query` builder obtained via +`stack.contentType(uid).entry().query()`, with explicit methods **or** the generic +`.where(fieldUid, QueryOperation.X, value)`. + +Import the operator enums: +```ts +import { QueryOperation, QueryOperator } from '@contentstack/delivery-sdk' +``` + +### 5.1 Field comparison operators + +| Meaning | Contentful param | Contentstack builder method | Contentstack `.where(...)` | Raw operator | +|---|---|---|---|---| +| Equals | `'fields.x': v` | `.equalTo('x', v)` | `.where('x', QueryOperation.EQUALS, v)` | (bare value) | +| Not equals | `'fields.x[ne]': v` | `.notEqualTo('x', v)` | `.where('x', QueryOperation.NOT_EQUALS, v)` | `$ne` | +| In set | `'fields.x[in]': 'a,b'` | `.containedIn('x', ['a','b'])` | `.where('x', QueryOperation.INCLUDES, ['a','b'])` | `$in` | +| Not in set | `'fields.x[nin]': 'a,b'` | `.notContainedIn('x', ['a','b'])` | `.where('x', QueryOperation.EXCLUDES, ['a','b'])` | `$nin` | +| Greater than | `'fields.x[gt]': v` | `.greaterThan('x', v)` | `.where('x', QueryOperation.IS_GREATER_THAN, v)` | `$gt` | +| Greater or equal | `'fields.x[gte]': v` | `.greaterThanOrEqualTo('x', v)` | `.where('x', QueryOperation.IS_GREATER_THAN_OR_EQUAL, v)` | `$gte` | +| Less than | `'fields.x[lt]': v` | `.lessThan('x', v)` | `.where('x', QueryOperation.IS_LESS_THAN, v)` | `$lt` | +| Less or equal | `'fields.x[lte]': v` | `.lessThanOrEqualTo('x', v)` | `.where('x', QueryOperation.IS_LESS_THAN_OR_EQUAL, v)` | `$lte` | +| Field exists | `'fields.x[exists]': true` | `.exists('x')` / `.notExists('x')` | `.where('x', QueryOperation.EXISTS, true)` | `$exists` | +| Regex / match | `'fields.x[match]': 'foo'` | `.regex('x', '^foo', 'i')` | `.where('x', QueryOperation.MATCHES, 'foo')` | `$regex` (+ `$options`) | +| Tags | `'metadata.tags.sys.id[in]': '...'` | `.tags(['t1','t2'])` | — | `tags` | +| Full-text search | `query: 'term'` | `.search('term')` | — | `typeahead` param | + +> Notes: +> - Contentstack `.where(field, QueryOperation.EQUALS, v)` stores the bare value (no operator +> wrapper), matching Contentful's bare `'fields.x': v`. +> - Contentstack has no direct equivalent to Contentful geo operators `[near]` / `[within]` +> in the delivery builder; use `.where()`/`.addParams()` with the appropriate raw param if +> geo querying is required. +> - Contentful `[all]` (array contains all) has no first-class builder method; model with +> `$and` of `containedIn`/`equalTo` or `.addParams()`. + +### 5.2 Logical combination + +| Meaning | Contentful | Contentstack | +|---|---|---| +| AND of sub-queries | Multiple params are ANDed implicitly | `.and(q1, q2)` or `.queryOperator(QueryOperator.AND, q1, q2)` | +| OR of sub-queries | Not natively supported in a single CDA call (often multiple calls) | `.or(q1, q2)` or `.queryOperator(QueryOperator.OR, q1, q2)` | + +```ts +// Contentstack OR example +const q1 = stack.contentType('blog').entry().query().equalTo('category', 'news') +const q2 = stack.contentType('blog').entry().query().greaterThan('views', 1000) +const res = await stack.contentType('blog').entry().query().or(q1, q2).find() +``` + +### 5.3 Sorting, pagination, field selection + +| Meaning | Contentful | Contentstack | +|---|---|---| +| Sort ascending | `order: 'fields.title'` | `.orderByAscending('title')` | +| Sort descending | `order: '-fields.title'` | `.orderByDescending('title')` | +| Limit | `limit: 10` | `.limit(10)` | +| Skip / offset | `skip: 20` | `.skip(20)` | +| Total count in result | `res.total` (always present) | `.includeCount()` → `res.count` | +| Select only fields | `select: 'fields.title,fields.slug'` | `.only(['title','slug'])` | +| Exclude fields | (no direct CDA param) | `.except(['body'])` | +| Arbitrary raw param | add key to query object | `.param(key, value)` / `.addParams({...})` | + +> Contentstack `only`/`except` use a `BASE` scope under the hood +> (`only[BASE][]=title`); just pass field UIDs to the builder methods. +> +> **Placement (verified against source):** `.only()` / `.except()` live on `.entry()` / +> `.entries()` (and `.asset()`), **not** on the `.query()` object. Call them *before* `.query()`: +> `stack.contentType(ct).entry().only(['title','slug']).query().find()`. Chaining +> `.query().only(...)` will fail. + +--- + +## 6. Response shape mapping (MOST IMPORTANT) + +This is where most migration bugs originate. **Contentful wraps content in `sys` + `fields`; +Contentstack returns a flat entry object.** + +### 6.1 Single entry + +**Contentful:** +```jsonc +{ + "sys": { "id": "blog123", "contentType": { "sys": { "id": "blogPost" } }, + "createdAt": "...", "updatedAt": "...", "locale": "en-US" }, + "fields": { // keyed by field id; flattened to one locale by default + "title": "Hello", + "slug": "hello", + "author": { "sys": { "type": "Link", "linkType": "Entry", "id": "auth1" } } + }, + "metadata": { "tags": [] } +} +// access: entry.fields.title, entry.sys.id, entry.sys.contentType.sys.id +``` + +**Contentstack (verified against `BaseEntry` in `src/common/types.ts`):** +```jsonc +{ + "uid": "blt...", // was sys.id + "title": "Hello", // fields are TOP-LEVEL, not under .fields + "slug": "hello", + "locale": "en-us", // was sys.locale + "created_at": "...", // was sys.createdAt + "updated_at": "...", // was sys.updatedAt + "_version": 3, + "tags": [], + "publish_details": { "environment": "...", "locale": "en-us", "time": "...", "user": "..." }, + "author": [ { /* resolved referenced entry */ } ] // references are ARRAYS +} +// access: entry.title, entry.uid, entry.locale +``` + +### 6.2 Field address translation cheatsheet + +| Contentful access | Contentstack access | +|---|---| +| `entry.fields.title` | `entry.title` | +| `entry.fields.` | `entry.` (drop the `.fields.` prefix) | +| `entry.sys.id` | `entry.uid` | +| `entry.sys.contentType.sys.id` | known from the path (`contentType(uid)`); or `_content_type_uid` on resolved references | +| `entry.sys.createdAt` / `updatedAt` | `entry.created_at` / `entry.updated_at` | +| `entry.sys.locale` | `entry.locale` | +| `entry.sys.revision` / `version` | `entry._version` | +| `entry.metadata.tags` | `entry.tags` (+ richer Taxonomy support) | + +### 6.3 Collection result + +| Contentful | Contentstack | +|---|---| +| `res.items` (array) | `res.entries` (array; `res.assets` for asset queries) | +| `res.total` | `res.count` (only when `.includeCount()` was called) | +| `res.skip` / `res.limit` | echoed in request; not returned the same way | +| `res.includes.Entry` / `.Asset` | resolved inline into entry fields via `includeReference` | + +Contentstack `find()` returns `FindResponse`: +`{ entries?: T[]; assets?: T[]; content_types?: TContentType[]; count?: number }`. +`fetch()` returns the single object directly (the SDK unwraps `response.entry` / +`response.asset` / `response.content_type` for you). + +--- + +## 7. References / linked entries + +**Contentful** auto-resolves links up to a depth using `include` (0–10, default 1) and +returns unresolved links plus an `includes` sidecar that the SDK stitches into `fields`. + +**Contentstack** does **not** resolve references by default. You must explicitly request each +reference field by UID via `includeReference`. Resolved references appear **inline as arrays** +on the entry. + +| Contentful | Contentstack | +|---|---| +| `getEntries({ content_type, include: 2 })` | `.includeReference('author', 'author.company')` (dot-path for nested) | +| automatic link resolution | explicit per-field `includeReference(...)` | +| `res.includes.Entry/Asset` | inline arrays on the resolved field | +| `links_to_entry` (reverse lookup) | `.whereIn(refUid, subQuery)` / `.referenceIn(field, subQuery)` | +| reference NOT matching | `.whereNotIn(...)` / `.referenceNotIn(...)` | +| include content type uid of refs | `.includeReferenceContentTypeUID()` | + +```ts +// Contentful: depth-based +const res = await client.getEntries({ content_type: 'blog', include: 2 }) +const authorName = res.items[0].fields.author.fields.name + +// Contentstack: explicit, references resolve to arrays +const res = await stack.contentType('blog').entry().query() + .includeReference('author', 'author.company') + .find() +const authorName = res.entries?.[0].author?.[0]?.name // note the [0] — references are arrays +``` + +> **Migration pitfall:** A single Contentful reference becomes a **single-element array** in +> Contentstack. Code that did `entry.fields.author.fields.name` becomes +> `entry.author?.[0]?.name`. Audit every dereference. + +Related include modifiers (verified in `entries.ts` / `entry.ts`): +`includeContentType()`, `includeEmbeddedItems()` (RTE embedded entries/assets), +`includeFallback()` (locale fallback), `includeMetadata()`, `includeBranch()`, +`includeSchema()`. + +--- + +## 8. Field-type mapping (for response handling) + +Contentful field `type` values (verified in `lib/entities/content-type-fields.ts`) map to +Contentstack `data_type` equivalents. Content modeling itself happens in the Contentstack UI / +Management API; this table helps the agent reason about **what a field will look like in the +response** and how to render it. + +| Contentful field `type` | Contentstack equivalent (`data_type`) | Response/handling notes | +|---|---|---| +| `Symbol` (short text) | `text` (single line) | plain string | +| `Text` (long text) | `text` (multiline) | plain string | +| `RichText` | `json` (JSON RTE) | different JSON shape → render with `@contentstack/utils` (§9) | +| `Integer` | `number` | number | +| `Number` (decimal) | `number` | number | +| `Date` | `isodate` | ISO date string | +| `Boolean` | `boolean` | boolean | +| `Location` (lat/lon) | `group` (lat/lng) | object shape differs | +| `Object` (JSON) | `json` | arbitrary JSON | +| `Link` → `Entry` | `reference` | array of resolved entries (with `includeReference`) | +| `Link` → `Asset` | `file` | asset object (see §10) | +| `Array` of `Symbol` | `text` (multiple) | array of strings | +| `Array` of `Link` | `reference` (multiple) | array of entries | +| `Array` of `Link` | `file` (multiple) | array of assets | +| (component/embedded) | `group` / `global_field` / `blocks` (Modular Blocks) | nested object/array | +| `ResourceLink` (cross-space) | reference / external | re-model as needed | +| `metadata.tags` | `tags` / Taxonomy | — | + +--- + +## 9. Rich text rendering + +This is a substantial change. **The JSON document shapes are different and the renderers are +different.** Contentful RichText is a `@contentful/rich-text-types` document; Contentstack +uses JSON RTE (Supercharged RTE) rendered via `@contentstack/utils`. + +| Concern | Contentful | Contentstack (`@contentstack/utils`) | +|---|---|---| +| Field type | `RichText` (document JSON) | JSON RTE (`json` data_type) | +| HTML string render | `documentToHtmlString(doc, options)` from `@contentful/rich-text-html-renderer` | `Utils.jsonToHTML({ entry, paths, renderOption })` | +| React / component render | `documentToReactComponents(doc, options)` from `@contentful/rich-text-react-renderer` (returns a **component tree**) | `@contentstack/utils` emits **HTML strings only** — there is no React/Vue/Angular component renderer. Inject the HTML (`dangerouslySetInnerHTML` / `v-html` / `[innerHTML]`), or use a dedicated JSON-RTE→component serializer for the framework. `Utils.render` does **not** return JSX. | +| Embedded entries/assets | `BLOCKS.EMBEDDED_ENTRY`, `INLINES.EMBEDDED_ENTRY`, `BLOCKS.EMBEDDED_ASSET` handled in `renderNode` | request with `.includeEmbeddedItems()`, then `Utils.render({ entry, renderOption })` | +| Node/mark customization | `options.renderNode` / `options.renderMark` keyed by `BLOCKS`/`INLINES`/`MARKS` | `renderOption` object keyed by node tag (`p`, `h1`, `a`, …), marks (`bold`), `block`, `inline`, `reference`, `display`, `default` | + +```ts +// Contentful +import { documentToHtmlString } from '@contentful/rich-text-html-renderer' +const html = documentToHtmlString(entry.fields.body) + +// Contentstack +import * as Utils from '@contentstack/utils' +const renderOption = { + p: (node, next) => `

${next(node.children)}

`, + h1: (node, next) => `

${next(node.children)}

`, + bold:(text) => `${text}`, + a: (node) => { + const txt = node.children.map((c) => c.text || '').join('') + return `${txt}` + }, +} +// For Supercharged/JSON RTE fields (MUTATES `entry` in place; returns void): +Utils.jsonToHTML({ entry, paths: ['body', 'group.rte_field'], renderOption }) +// For HTML-RTE embedded items (fetch with .includeEmbeddedItems() first): +// NOTE: the option key is `paths` (plural string[]), NOT `path`. Also mutates `entry` in place. +Utils.render({ entry, paths: ['body'], renderOption }) +// To render a raw RTE string/array and RECEIVE the HTML back (no mutation): Utils.renderContent(content, option) +``` + +> Steps for the agent: +> 1. Add `.includeEmbeddedItems()` to any entry/entries fetch that renders RTE with embeds. +> 2. Replace `documentToHtmlString(entry.fields.body)` with +> `Utils.jsonToHTML({ entry, paths: ['body'], renderOption })` (note: it mutates/returns +> HTML keyed onto the field path; pass the whole `entry`, not just the field value). +> 3. Translate `renderNode`/`renderMark` handlers into the `renderOption` shape. + +--- + +## 10. Assets & image transformations + +### 10.1 Asset shape + +| Contentful | Contentstack | +|---|---| +| `asset.fields.file.url` (often protocol-relative `//...`) | `asset.url` (absolute) | +| `asset.fields.file.fileName` | `asset.filename` | +| `asset.fields.file.contentType` | `asset.content_type` | +| `asset.fields.file.details.size` | `asset.file_size` | +| `asset.fields.file.details.image.{width,height}` | use `.includeDimension()` on asset fetch → `asset.dimension` | +| `asset.fields.title` | `asset.title` | +| `asset.sys.id` | `asset.uid` | + +```ts +// Contentful +const url = 'https:' + asset.fields.file.url +// Contentstack +const url = asset.url +``` + +### 10.2 Image API / transforms + +Both support URL-based image manipulation, but param names differ. Contentstack provides an +`ImageTransform` builder (`@contentstack/delivery-sdk`) and a `String.prototype.transform` +helper. + +| Transform | Contentful query param | Contentstack `ImageTransform` method | +|---|---|---| +| Width / height / resize | `?w=300&h=200` | `.resize({ width: 300, height: 200 })` | +| Format | `?fm=webp` | `.format(Format.WEBP)` | +| Quality | `?q=80` | `.quality(80)` | +| Fit / crop | `?fit=fill` / `?f=crop` | `.fit(FitBy.CROP)` / `.crop({...})` | +| Auto optimize | `?fm=webp` (manual) | `.auto()` | +| Background | `?bg=...` | `.bgColor('cccccc')` | +| DPR | `?dpr=2` | `.dpr(2)` | +| Blur / sharpen | `?blur=` / — | `.blur(n)` / `.sharpen(a,r,t)` | +| Orientation | `?or=` | `.orient(Orientation.RIGHT)` | +| Trim / pad / overlay / canvas | `?trim=` etc. | `.trim()`, `.padding()`, `.overlay({...})`, `.canvas({...})` | + +```ts +import { ImageTransform, Format } from '@contentstack/delivery-sdk' +const t = new ImageTransform().resize({ width: 300, height: 200 }).format(Format.WEBP).quality(80) +const optimized = asset.url.transform(t) // String.prototype.transform helper (loaded by the SDK) +``` + +> **Caveat (verified in source):** `Format` is a runtime value export, but at the package root +> `ImageTransform` is currently re-exported **type-only** (`export type { ImageTransform }` in +> `index.ts`). If `new ImageTransform()` from the package root fails at runtime, import the +> value class from the assets module (the SDK's own tests use +> `import { ImageTransform } from '@contentstack/delivery-sdk/dist/.../assets'`), or build the +> transform URL by appending the documented image-delivery query params directly +> (`?width=300&height=200&format=webp&quality=80`). The `String.prototype.transform` helper is +> installed when the SDK is imported (`import './common/string-extensions'`). + +--- + +## 11. Locales / localization + +| Concern | Contentful | Contentstack | +|---|---|---| +| Locale code style | `en-US`, `fr-FR` | `en-us`, `fr-fr` (lower-cased) | +| Per-request locale | `getEntries({ locale: 'fr-FR' })` | `.locale('fr-fr')` on entry/entries/asset | +| Default locale | `environment` default / init | `locale` in `stack({...})` or `stack.setLocale('fr-fr')` | +| All locales at once | `locale: '*'` → fields become `{ 'en-US': v }` maps | not the same; query per locale, or use fallback | +| Fallback when unpublished | (CDA returns default-locale content per settings) | explicit `.includeFallback()` | + +> **Pitfall:** With Contentful `locale: '*'`, `entry.fields.title` becomes an object keyed by +> locale. Apps relying on this multi-locale shape must be re-architected to query per locale +> in Contentstack (Contentstack returns single-locale flat entries). + +--- + +## 12. Pagination + +| Concern | Contentful | Contentstack | +|---|---|---| +| Offset pagination | `skip` + `limit`, read `res.total` | `.skip(n).limit(m)`, `.includeCount()` → `res.count` | +| Cursor pagination | `getEntriesWithCursor` (CMA) / `res.pages.next` | use sync API / offset; cursor not in delivery builder | +| Delta sync | `client.sync({ initial, nextSyncToken })` | `stack.sync({ ... })`; **TS keys are camelCase** (`paginationToken` / `syncToken`); `init: true` is auto-added when neither is present. Response uses snake_case (`sync_token`, `pagination_token`). | + +```ts +// Contentstack offset pagination with total +const page = await stack.contentType('blog').entry().query() + .includeCount().skip(20).limit(20).find() +const total = page.count +``` + +--- + +## 13. Common gotchas / migration pitfalls + +1. **`.fields.` removal.** Every `entry.fields.X` → `entry.X`. This is the most frequent edit. +2. **`sys` → flat metadata.** `entry.sys.id` → `entry.uid`; `sys.createdAt` → `created_at`; etc. +3. **References become arrays.** `entry.fields.ref.fields.x` → `entry.ref?.[0]?.x`. Even a + single reference resolves to a one-element array. +4. **References are not auto-resolved.** Replace `include: N` with explicit + `.includeReference('field', 'field.nested')`. Forgetting this leaves you with unresolved + reference stubs (`{ uid, _content_type_uid }`). +5. **Content type is in the path.** No `content_type` query param; use + `stack.contentType(uid)`. Multi-type Contentful queries must be split per type. +6. **`environment` is mandatory** in Contentstack init (Contentful defaults to `master`). +7. **Collection key rename.** `res.items` → `res.entries` (or `res.assets`). `res.total` → + `res.count` and only when `.includeCount()` is set. +8. **`.find()` vs `.fetch()`.** Collections/queries end in `.find()`; single objects end in + `.fetch()`. Mixing them up is a common error. +9. **Rich text is a different JSON dialect.** You cannot pass a Contentful RichText document to + `@contentstack/utils`; the underlying content must live in a Contentstack JSON RTE field. + The renderer API also differs (tag-keyed `renderOption` vs `BLOCKS`/`INLINES` `renderNode`). +10. **Locale casing.** Lower-case all locale codes (`en-US` → `en-us`). +11. **Asset URLs.** Contentful URLs are often protocol-relative (`//images.ctfassets.net/...`) + and need an added scheme; Contentstack URLs are absolute. +12. **Image transform params differ.** Don't carry over `?w=&h=&fit=` query strings verbatim; + rebuild with `ImageTransform` or the correct Contentstack image param names. +13. **OR queries.** Contentstack supports `.or(...)` natively; Contentful CDA often required + multiple requests — you can consolidate during migration. +14. **Typed responses.** Pass an interface to `fetch()` / `find()`. Extend `BaseEntry` + (from `@contentstack/delivery-sdk`) for entry types so `uid`, `locale`, `created_at`, etc. + are typed. +15. **`Utils.render` uses `paths` (plural array), not `path`.** And `jsonToHTML`/`render` + **mutate the entry in place** (return `void`) — pass the whole entry and read the field + afterward; use `Utils.renderContent(content, option)` if you need a returned HTML string. +16. **`@contentstack/utils` produces HTML strings, never components.** Code using + `documentToReactComponents` (a component tree) needs an HTML-injection strategy or a + JSON-RTE→component serializer — not a 1:1 swap. +17. **`ImageTransform` is type-only at the package root.** `new ImageTransform()` from + `@contentstack/delivery-sdk` may fail at runtime; build the image query string manually + (`?width=300&height=200&format=webp&quality=80`) or deep-import the value class. +18. **`stack.sync` keys are camelCase in TypeScript** (`paginationToken`, `syncToken`), even + though the response and some JSDoc examples use snake_case. +19. **Match the data-access approach.** Don't rewrite a GraphQL app to the REST SDK (or vice + versa). GraphQL → Contentstack GraphQL (§17); preview → Contentstack Live Preview (§18). + +--- + +## 14. Worked before/after example + +### Contentful (e.g. a Next.js / React data layer) +```ts +import { createClient } from 'contentful' +import { documentToHtmlString } from '@contentful/rich-text-html-renderer' + +const client = createClient({ + space: process.env.CF_SPACE!, + accessToken: process.env.CF_CDA_TOKEN!, + environment: 'master', +}) + +export async function getPost(slug: string) { + const res = await client.getEntries({ + content_type: 'blogPost', + 'fields.slug': slug, + include: 2, + limit: 1, + }) + const post = res.items[0] + return { + title: post.fields.title, + author: post.fields.author.fields.name, + coverUrl: 'https:' + post.fields.coverImage.fields.file.url, + bodyHtml: documentToHtmlString(post.fields.body), + } +} +``` + +### Contentstack (migrated) +```ts +import contentstack, { Region, BaseEntry } from '@contentstack/delivery-sdk' +import * as Utils from '@contentstack/utils' + +const stack = contentstack.stack({ + apiKey: process.env.CS_API_KEY!, + deliveryToken: process.env.CS_DELIVERY_TOKEN!, + environment: process.env.CS_ENVIRONMENT!, // required + region: Region.US, +}) + +interface BlogPost extends BaseEntry { + slug: string + author: Array<{ name: string }> // references resolve to arrays + cover_image: { url: string } // asset (file) field + body: any // JSON RTE +} + +const renderOption = { + p: (node, next) => `

${next(node.children)}

`, + bold: (text) => `${text}`, +} + +export async function getPost(slug: string) { + const res = await stack + .contentType('blog_post') + .entry() + .query() + .equalTo('slug', slug) + .includeReference('author') // explicit reference resolution + .includeEmbeddedItems() // needed for RTE embeds + .limit(1) + .find() + + const post = res.entries?.[0] + if (!post) return null + + Utils.jsonToHTML({ entry: post, paths: ['body'], renderOption }) // mutates post.body → HTML + + return { + title: post.title, // no .fields + author: post.author?.[0]?.name, // reference is an array + coverUrl: post.cover_image?.url, // absolute URL + bodyHtml: post.body, // rendered by jsonToHTML + } +} +``` + +--- + +## 15. Migration checklist (agent run order) + +0. **Detect** language, framework, and data-access approach(es) (§0.1). Confirm prerequisites: + the target stack already has the matching content model + published content, and you have a + Contentful-field-ID → Contentstack-field-UID map. If reads are GraphQL, follow §17; if the app + has Live Preview / draft mode, also follow §18. +1. **Dependencies:** remove `contentful*` + `@contentful/rich-text-*`; add + `@contentstack/delivery-sdk` + `@contentstack/utils` (+ `@contentstack/persistence-plugin` + only if cache policies are used). +2. **Env vars:** `CF_SPACE`/`CF_CDA_TOKEN` → `CS_API_KEY`/`CS_DELIVERY_TOKEN`/`CS_ENVIRONMENT` + (+ region/branch as needed). +3. **Client init:** `createClient(...)` → `contentstack.stack({...})`. +4. **Reads:** rewrite each `getEntry`/`getEntries`/`getAsset`/`getAssets`/`getContentType(s)` + to the builder form ending in `.fetch()`/`.find()` (§4). +5. **Queries:** translate every operator/sort/pagination/select param (§5). +6. **References:** replace `include: N` with explicit `includeReference(...)`; convert single + references to `?.[0]` access (§7). +7. **Field access:** strip `.fields.`; map `sys.*` → flat metadata (`uid`, `created_at`, …) (§6). +8. **Collections:** `res.items` → `res.entries`/`res.assets`; `res.total` → + `.includeCount()` + `res.count`. +9. **Rich text:** move RTE rendering to `@contentstack/utils` (`jsonToHTML`/`render`) with a + `renderOption`; add `.includeEmbeddedItems()` where embeds are rendered (§9). +10. **Assets/images:** `fields.file.url` → `url`; rebuild image transforms with + `ImageTransform` (§10). +11. **Locales:** lower-case codes; `locale` param → `.locale(...)`; add `.includeFallback()` + where Contentful relied on default-locale fallback (§11). +12. **Types:** introduce interfaces extending `BaseEntry` for `fetch()`/`find()`. +13. **Verify:** typecheck/build; smoke-test each migrated query against a real Contentstack + stack; confirm reference arrays, RTE HTML output, and image URLs render correctly. + +--- + +## 16. Quick API equivalence (condensed) + +``` +Contentful (contentful / CDA) Contentstack (@contentstack/delivery-sdk) +------------------------------------------ --------------------------------------------------- +createClient({ space, accessToken, env }) -> contentstack.stack({ apiKey, deliveryToken, environment, region }) +client.getEntry(id) -> stack.contentType(ct).entry(id).fetch() +client.getEntries({ content_type: ct }) -> stack.contentType(ct).entry().query().find() +client.getAsset(id) -> stack.asset(id).fetch() +client.getAssets(q) -> stack.asset().query()...find() +client.getContentType(id) -> stack.contentType(id).fetch() +client.getContentTypes() -> stack.contentType().find() +client.sync({ initial: true }) -> stack.sync({ ... }) +'fields.x': v -> .equalTo('x', v) | .where('x', QueryOperation.EQUALS, v) +'fields.x[gte]': v -> .greaterThanOrEqualTo('x', v) +order: '-fields.x' -> .orderByDescending('x') +skip / limit -> .skip(n) / .limit(m) +select: 'fields.a,fields.b' -> .only(['a','b']) +include: N -> .includeReference('a','a.b') +res.items / res.total -> res.entries / (res.count via .includeCount()) +entry.fields.x / entry.sys.id -> entry.x / entry.uid +documentToHtmlString(entry.fields.body) -> Utils.jsonToHTML({ entry, paths:['body'], renderOption }) +asset.fields.file.url -> asset.url +``` + +--- + +## 17. GraphQL migration (Contentful GraphQL → Contentstack GraphQL) + +If the app reads via the **Contentful GraphQL Content API** (signals: `graphql.contentful.com`, +`gql` tagged templates, `*.graphql` files, `@apollo/client` / `urql` / `graphql-request`, +GraphQL Code Generator), migrate to the **Contentstack GraphQL Content Delivery API** — do **not** +rewrite it to the REST Delivery SDK. **Keep the same GraphQL client library**; only the endpoint, +auth, schema, and query/fragment text change. The response-shape principles in §6 still apply +(flattened fields, `uid`/`created_at` metadata, references as nested objects). + +> The Contentstack GraphQL API is a separate service and is **not** part of `@contentstack/delivery-sdk`. +> The endpoint hosts, header names, and pagination/filter argument names below should be **verified +> against current Contentstack GraphQL documentation** for the target region before finalizing. + +### 17.1 Endpoint & auth + +| Concern | Contentful GraphQL | Contentstack GraphQL | +|---|---|---| +| Endpoint | `https://graphql.contentful.com/content/v1/spaces/{SPACE}/environments/{ENV}` | `https://graphql.contentstack.com/stacks/{API_KEY}?environment={ENV}` (regional hosts: `eu-graphql…`, `azure-na-graphql…`, `azure-eu-graphql…`, `gcp-na-graphql…`, `gcp-eu-graphql…`) | +| Auth | `Authorization: Bearer {CDA_or_CPA_TOKEN}` | header `access_token: {DELIVERY_TOKEN}` (API key is in the path; `environment` is a query param) | +| Preview | `graphql.contentful.com` + CPA Bearer token | preview host + `live_preview` hash / `preview_token` header (see §18) | + +### 17.2 Query shape + +Field/type names follow the **Contentstack content-type & field UIDs** (snake_case, e.g. +`blog_post`, `cover_image`), not Contentful's camelCase ids. + +```graphql +# Contentful +query { + blogPostCollection(limit: 10, where: { slug: "hello" }) { + total + items { title slug author { name } } + } +} + +# Contentstack (verify exact arg/field names against the stack's GraphQL schema) +query { + all_blog_post(limit: 10, where: { slug: "hello" }) { + total + items { + title + slug + author { ... on SysAssetOrEntry { /* reference: nest the referenced type's fields */ } } + } + } +} +``` + +| Concept | Contentful GraphQL | Contentstack GraphQL | +|---|---|---| +| Collection query | `xxxCollection` → `{ items, total }` | `all_` → `{ items, total }` | +| Single entry | `xxx(id: "...")` | `(uid: "...")` | +| References | nested `linkedFrom` / inline link selections | **nest the referenced type's fields** in the selection (the GraphQL analog of `includeReference`) | +| Filtering | `where: { field: value, field_gt: n }` | `where: { ... }` (operator/arg names differ — verify) | +| Pagination | `limit` + `skip` (`total`) | `limit` + `skip` (`total`) | +| Localization | `locale: "en-US"` arg | `locale: "en-us"` arg (lower-cased) | +| RTE field | `json` in response | `json` in response → render with `@contentstack/utils` (§9) | + +### 17.3 Steps for the agent +1. Repoint the GraphQL client (Apollo/urql/graphql-request) at the Contentstack endpoint and swap + `Authorization: Bearer` → `access_token` header + `environment` query param. +2. Rewrite every query/fragment to the Contentstack schema (collection names, field UIDs, reference + nesting, filter args, lower-cased locales). +3. Update response handling for the new shape (`items` arrays, flat fields, `uid`/`created_at`). +4. If **GraphQL Code Generator** is used, repoint it at the Contentstack schema/introspection and + regenerate types; fix call sites against the new generated types. +5. Render RTE/JSON fields with `@contentstack/utils` (§9), same as the REST path. + +--- + +## 18. Live Preview / draft mode migration + +If the source app implements **Contentful Preview** or **Live Preview**, reimplement equivalent +behavior with **Contentstack Live Preview**, matching the source's scope (which routes/components, +SSR vs client, click-to-edit vs. read-only preview). + +**Detect in the source:** `host: 'preview.contentful.com'`, a CPA / Content Preview token, a second +"preview" client, `@contentful/live-preview` (`ContentfulLivePreview.init`, `useContentfulLiveUpdates`, +`useContentfulInspectorMode`), Next.js `draftMode()` / preview API routes, or a `?preview=` route gate. + +### 18.1 SDK config & per-request hash (verified against `@contentstack/delivery-sdk` source) + +`live_preview` is a first-class field on `StackConfig` (`src/common/types.ts`), and the stack exposes +`livePreviewQuery(...)`: + +```ts +const stack = contentstack.stack({ + apiKey: process.env.CS_API_KEY!, + deliveryToken: process.env.CS_DELIVERY_TOKEN!, + environment: process.env.CS_ENVIRONMENT!, + live_preview: { + enable: true, // REQUIRED on the LivePreview type + preview_token: process.env.CS_PREVIEW_TOKEN!, // preferred (management_token is legacy) + host: 'rest-preview.contentstack.com',// regional preview host (verify per region) + }, +}) + +// Per request, apply the preview hash (from the URL / live-preview-utils) before fetching: +stack.livePreviewQuery({ + live_preview: hash, // the live_preview hash for the edited entry + contentTypeUid: 'blog_post', // also accepts content_type_uid + entryUid: 'blt...', // also accepts entry_uid + // preview_timestamp, release_id, include_applied_variants are also supported +}) +``` + +`LivePreview` type fields (verified): `enable: boolean` (required), `preview_token?`, +`management_token?` (legacy), `host?`, `live_preview?`, `contentTypeUid?`, `entryUid?`, +`include_applied_variants?`. In the **browser**, the SDK auto-reads `live_preview`, `release_id`, +and `preview_timestamp` from the page URL's query string during `stack(...)` init. + +### 18.2 Front-end real-time updates & click-to-edit (`@contentstack/live-preview-utils`) + +> Not part of the Delivery SDK and **not vendored in this workspace** — verify the exact API against +> current `@contentstack/live-preview-utils` docs. Add it as a dependency. + +| Contentful | Contentstack | +|---|---| +| `ContentfulLivePreview.init({...})` | `ContentstackLivePreview.init({ stackDetails: { apiKey, environment }, enable: true, ssr: false /* or true */ })` | +| `useContentfulLiveUpdates(entry)` (real-time merge) | `ContentstackLivePreview.onEntryChange(cb)` → re-run the fetch in `cb` | +| live-update hash plumbing | `ContentstackLivePreview.hash` (REST) / GraphQL hash helper → pass to `stack.livePreviewQuery({ live_preview: hash, ... })` | +| `useContentfulInspectorMode()` / `data-contentful-*` field tags (click-to-edit) | `Utils.addEditableTags(entry, contentTypeUid, true, locale)` from `@contentstack/utils` → emits `data-cslp` attributes the Live Preview UI uses for click-to-edit | +| CPA token | `preview_token` (+ `live_preview` host) | + +### 18.3 Steps for the agent +1. Add `live_preview` to the stack init (`enable: true` + `preview_token` + regional preview host). +2. Add `@contentstack/live-preview-utils`; call `ContentstackLivePreview.init(...)` where the source + called `ContentfulLivePreview.init(...)`. +3. Replace the source's live-update hook with `onEntryChange(...)` re-fetching, calling + `stack.livePreviewQuery({ live_preview, contentTypeUid, entryUid })` before each preview fetch. +4. If the source had click-to-edit (inspector mode), add `Utils.addEditableTags(...)` and render the + resulting `data-cslp` attributes on the same elements. +5. Preserve the existing preview gating/routing (e.g. Next.js `draftMode`, `?preview=` guard); only + swap the backend. Keep preview tokens server-side. + +--- + +## 19. Raw REST / `fetch` and framework source plugins + +Not every app uses an SDK. Migrate these in kind: + +- **Raw `fetch`/`axios` to `cdn.contentful.com`.** Repoint to the Contentstack Content Delivery + REST API: base `https://{region-cdn-host}/v3` (US `cdn.contentstack.io`; see §3 for regional hosts), + headers `api_key`, `access_token`, and `environment` as a query param. Endpoints: + `/v3/content_types/{ct_uid}/entries` (list), `/v3/content_types/{ct_uid}/entries/{uid}` (single), + `/v3/assets`. Query params map per §5 (`include[]=author`, `only[BASE][]=title`, `limit`, `skip`, + `include_count=true`, `locale`, `include_fallback=true`). Response keys per §6 (`entries`/`entry`, + `assets`/`asset`, `count`). Prefer adopting `@contentstack/delivery-sdk` if it doesn't fight the + app's architecture, but a like-for-like raw-fetch migration is valid. +- **Gatsby (`gatsby-source-contentful`).** Migrate to `@contentstack/gatsby-source-contentstack`: + swap the plugin + its options (`api_key`, `delivery_token`, `environment`, `regions`) in + `gatsby-config`, and rewrite GraphQL queries from `allContentfulXxx` to the Contentstack source + plugin's node types (verify node-type naming against the plugin docs). +- **Framework data-fetching idioms — keep them.** Migrate the data-layer call *inside* the app's + existing pattern; don't change where/how data is fetched: + - Next.js: `getStaticProps`/`getServerSideProps`/`generateStaticParams`/RSC `fetch` — keep the + same function, swap the client call. Keep ISR/caching tags. + - Remix `loader`, SvelteKit `load`, Nuxt `useAsyncData`/`asyncData`, Vue composables, Angular + services/resolvers — same: swap only the CMS call body. + - Module-singleton vs per-request client: instantiate the Contentstack stack the same way the + Contentful client was instantiated (one shared client module is the common case). diff --git a/skills/contentstack-migration-companion-beta/scripts/01_contentful_residue.sh b/skills/contentstack-migration-companion-beta/scripts/01_contentful_residue.sh new file mode 100755 index 0000000..b62f5f7 --- /dev/null +++ b/skills/contentstack-migration-companion-beta/scripts/01_contentful_residue.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# HARD GATE. No Contentful SDK/host/dependency may remain after migration. +. "$(dirname "$0")/_lib.sh" +begin "residue" "No leftover Contentful surface" + +# Source imports / requires +check "Contentful SDK import" "from[[:space:]]+['\"]contentful['\"]" +check "contentful-management import" "from[[:space:]]+['\"]contentful-management['\"]" +check "@contentful/* import" "from[[:space:]]+['\"]@contentful/" +check "contentful require()" "require\(['\"]contentful" +check "@contentful/rich-text import" "@contentful/rich-text" +check "Contentful rich-text renderers" "documentTo(HtmlString|ReactComponents)" + +# Hosts / asset domains +check "Contentful CDA host" "cdn\.contentful\.com" +check "Contentful preview host" "preview\.contentful\.com" +check "Contentful GraphQL host" "graphql\.contentful\.com" +check "Contentful asset domain" "ctfassets\.net" + +# Env var names +check "Contentful env vars" "(CONTENTFUL_[A-Z_]+|CF_SPACE|CF_CDA[A-Z_]*|CF_CPA[A-Z_]*|CTFL_[A-Z_]+)" "js,jsx,ts,tsx,mjs,cjs,vue,svelte,astro,env,example,local,sample,yml,yaml" + +# package.json must not depend on contentful, must depend on contentstack +check "contentful dependency in package.json" "\"(contentful|contentful-management|@contentful/[^\"]+)\"[[:space:]]*:" "json" +require_present "@contentstack/delivery-sdk (or @contentstack/* read SDK) dependency" "@contentstack/(delivery-sdk|management)" "json" + +finish diff --git a/skills/contentstack-migration-companion-beta/scripts/02_field_access.sh b/skills/contentstack-migration-companion-beta/scripts/02_field_access.sh new file mode 100755 index 0000000..eabf8b4 --- /dev/null +++ b/skills/contentstack-migration-companion-beta/scripts/02_field_access.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# HARD GATE. Contentful nests under sys/fields; Contentstack is flat. (doc §6, gotchas #1-2,7) +. "$(dirname "$0")/_lib.sh" +begin "field-access" "Flattened field & metadata access (no .fields./.sys.)" + +# Strong residue: Contentful field/metadata addressing +check ".fields. field access (drop the prefix)" "\.fields\." +check ".sys. access (map to flat uid/created_at/...)" "\.sys\.(id|createdAt|updatedAt|revision|version|locale|contentType)" +check "Contentful nested reference access (.fields.x.fields.y)" "\.fields\.[A-Za-z0-9_]+\.fields\." +check "metadata.tags (use entry.tags)" "\.metadata\.tags" + +# Collection-shape residue — softer (could be unrelated arrays); flagged for verification +check "'.items' collection key (Contentstack uses .entries/.assets)" "\.items\b" +check "'.total' count key (Contentstack uses .count via includeCount())" "\.total\b" +check "Contentful includes sidecar (.includes.Entry/.Asset)" "\.includes\.(Entry|Asset)" + +finish diff --git a/skills/contentstack-migration-companion-beta/scripts/03_sdk_init.sh b/skills/contentstack-migration-companion-beta/scripts/03_sdk_init.sh new file mode 100755 index 0000000..0f92311 --- /dev/null +++ b/skills/contentstack-migration-companion-beta/scripts/03_sdk_init.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# HARD GATE for REST SDK apps. Stack init must be correct & required fields present. (doc §3, gotcha #6) +. "$(dirname "$0")/_lib.sh" +begin "sdk-init" "Contentstack stack() initialization" + +uses_sdk="$(search "@contentstack/delivery-sdk")" +inits="$(files_with "contentstack\.stack\(")" + +if [ -z "$uses_sdk" ] && [ -z "$inits" ]; then + na "app does not use @contentstack/delivery-sdk (GraphQL/raw-REST app — see evals 09/build)" +fi + +if [ -z "$inits" ]; then + add_finding "@contentstack/delivery-sdk is imported but no 'contentstack.stack({...})' init found" +else + for f in $inits; do + grep -q 'environment' "$f" || add_finding "stack() init may be missing required 'environment': $f" + grep -Eq 'apiKey' "$f" || add_finding "stack() init missing 'apiKey' (Contentful used 'space'): $f" + grep -Eq 'deliveryToken' "$f"|| add_finding "stack() init missing 'deliveryToken' (Contentful used 'accessToken'): $f" + grep -Eq 'space:|accessToken:' "$f" && add_finding "Contentful init keys (space/accessToken) still present: $f" + done +fi + +# Credentials must come from env, not be hardcoded literals +check "Hardcoded apiKey literal (use process.env)" "apiKey[[:space:]]*:[[:space:]]*['\"][A-Za-z0-9]{6,}['\"]" +check "Hardcoded deliveryToken literal (use process.env)" "deliveryToken[[:space:]]*:[[:space:]]*['\"][A-Za-z0-9]{6,}['\"]" + +finish diff --git a/skills/contentstack-migration-companion-beta/scripts/04_query_builder.sh b/skills/contentstack-migration-companion-beta/scripts/04_query_builder.sh new file mode 100755 index 0000000..a3ee90f --- /dev/null +++ b/skills/contentstack-migration-companion-beta/scripts/04_query_builder.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Query builder correctness. (doc §4, §5, gotchas #5,8 + verified .only/.except placement) +. "$(dirname "$0")/_lib.sh" +begin "query-builder" "Query/operator/pagination translation" + +# Contentful query syntax that must be gone +check "Contentful bracket operator (e.g. fields.x[gte])" "\[(gte|lte|gt|lt|ne|nin|in|exists|match|all|near|within)\]" +check "Contentful 'content_type' query param (use stack.contentType(uid))" "content_type[[:space:]]*:" +check "Contentful reference depth 'include: N' (use includeReference)" "include[[:space:]]*:[[:space:]]*[0-9]" +check "Contentful 'order:' sort (use orderByAscending/Descending)" "order[[:space:]]*:[[:space:]]*['\"]" +check "Contentful 'select:' (use .only([...]))" "select[[:space:]]*:[[:space:]]*['\"]" + +# VERIFIED bug: .only()/.except() are NOT on .query() — they live on .entry()/.entries() +check ".only()/.except() chained after .query() (invalid — call before .query())" "\.query\([^)]*\)\.(only|except)\(" + +# Count must be requested explicitly +if [ -n "$(search "\.count\b")" ]; then + require_present ".includeCount() — code reads res.count but never calls includeCount()" "includeCount\(" +fi + +finish diff --git a/skills/contentstack-migration-companion-beta/scripts/05_references.sh b/skills/contentstack-migration-companion-beta/scripts/05_references.sh new file mode 100755 index 0000000..ca287c3 --- /dev/null +++ b/skills/contentstack-migration-companion-beta/scripts/05_references.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# References: not auto-resolved + resolve to ARRAYS. (doc §7, gotchas #3,4) +. "$(dirname "$0")/_lib.sh" +begin "references" "Reference resolution & array access" + +# Contentful link residue +check "Contentful Link stub (sys.linkType / linkType: 'Entry')" "(sys\.linkType|linkType[[:space:]]*:[[:space:]]*['\"](Entry|Asset))" +check "Contentful nested ref access (.fields.x.fields.y)" "\.fields\.[A-Za-z0-9_]+\.fields\." + +# If references are requested, surface the field UIDs so each dereference can be checked for [0]/array handling. +refs="$(search "includeReference\(")" +if [ -n "$refs" ]; then + echo " (info) includeReference() call sites — verify each resolved field is accessed as an ARRAY (entry.field?.[0]?.x, or .map(...)):" 1>&2 + # Pull referenced field names and flag object-style dereference (.field. not followed by [ , ?.[ , .map, .length, .forEach) + while IFS= read -r line; do + # crude extraction of the first quoted arg + fld="$(printf '%s' "$line" | grep -oE "includeReference\(['\"][A-Za-z0-9_]+" | head -1 | sed -E "s/.*['\"]//")" + [ -z "$fld" ] && continue + bad="$(search "\.${fld}\.[A-Za-z_]" )" + if [ -n "$bad" ]; then + add_finding "reference '${fld}' may be dereferenced as an object instead of an array (expected ${fld}?.[0]?.x):" + printf '%s\n' "$bad" | sed 's/^/ /' >> "$_TMP" + fi + done <<< "$(printf '%s\n' "$refs" | sort -u)" +fi + +finish diff --git a/skills/contentstack-migration-companion-beta/scripts/06_richtext.sh b/skills/contentstack-migration-companion-beta/scripts/06_richtext.sh new file mode 100755 index 0000000..8789c45 --- /dev/null +++ b/skills/contentstack-migration-companion-beta/scripts/06_richtext.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Rich text rendering. (doc §9, gotchas #9,15,16) +. "$(dirname "$0")/_lib.sh" +begin "richtext" "RTE rendering via @contentstack/utils" + +uses_rte="$(search "(jsonToHTML|renderContent|@contentstack/utils|documentTo(HtmlString|ReactComponents)|@contentful/rich-text)" )" +[ -z "$uses_rte" ] && na "no rich-text rendering detected" + +# Residue +check "Contentful rich-text renderer call" "documentTo(HtmlString|ReactComponents)" +check "@contentful/rich-text import" "@contentful/rich-text" + +# VERIFIED bug: Utils.render / jsonToHTML option key is `paths` (plural), not `path` +check "RTE render uses 'path:' (must be 'paths:' plural array)" "(jsonToHTML|[^A-Za-z]render)\([^)]*\bpath[[:space:]]*:" + +# Embeds require includeEmbeddedItems() +if [ -n "$(search "(jsonToHTML|[^A-Za-z]render)\(")" ]; then + require_present ".includeEmbeddedItems() — needed when RTE renders embedded entries/assets" "includeEmbeddedItems\(" +fi + +# utils emits HTML strings -> need an injection sink in component code +if [ -n "$(search "(jsonToHTML|renderContent|[^A-Za-z]render)\(")" ]; then + require_present "HTML injection sink (dangerouslySetInnerHTML / v-html / [innerHTML]) for rendered RTE HTML" "(dangerouslySetInnerHTML|v-html|\[innerHTML\]|innerHTML[[:space:]]*=)" +fi + +finish diff --git a/skills/contentstack-migration-companion-beta/scripts/07_assets.sh b/skills/contentstack-migration-companion-beta/scripts/07_assets.sh new file mode 100755 index 0000000..36b5a6b --- /dev/null +++ b/skills/contentstack-migration-companion-beta/scripts/07_assets.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Assets & image transforms. (doc §10, gotchas #11,12,17) +. "$(dirname "$0")/_lib.sh" +begin "assets" "Asset URL & image-transform translation" + +# Contentful asset shape residue +check "asset.fields.file.url (use asset.url)" "\.fields\.file\." +check "protocol-relative URL fix ('https:' + url)" "['\"]https?:['\"][[:space:]]*\+" +check "asset.fields.title/fileName (use asset.title/filename)" "\.fields\.(title|fileName|file)\b" + +# Contentful image API params carried over verbatim +check "Contentful image params (?w=/&h=/fm=/fit=) in URL strings" "[?&](w|h|fm|fit|q|bg|dpr|or)=" + +# Verified caveat: ImageTransform is exported TYPE-ONLY at the package root. +if [ -n "$(search "new[[:space:]]+ImageTransform\(")" ]; then + check "ImageTransform imported from package root (type-only — 'new ImageTransform()' may fail at runtime)" \ + "import[^;]*ImageTransform[^;]*@contentstack/delivery-sdk['\"]" +fi + +finish diff --git a/skills/contentstack-migration-companion-beta/scripts/08_locales.sh b/skills/contentstack-migration-companion-beta/scripts/08_locales.sh new file mode 100755 index 0000000..cf2958e --- /dev/null +++ b/skills/contentstack-migration-companion-beta/scripts/08_locales.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# Locales / localization. (doc §11, gotcha #10) +. "$(dirname "$0")/_lib.sh" +begin "locales" "Locale casing & fallback" + +# Applicability includes stray uppercase locale codes so they are never silently skipped. +uses_locale="$(search "(\.locale\(|locale[[:space:]]*:|setLocale\(|includeFallback\(|['\"][a-z]{2}-[A-Z]{2}['\"])")" +[ -z "$uses_locale" ] && na "no locale usage detected" + +# Contentstack locale codes are lower-cased; flag Contentful-style 'en-US'/'fr-FR' +check "Uppercase locale code (Contentstack uses lower-case, e.g. 'en-us')" "['\"][a-z]{2}-[A-Z]{2}['\"]" +# Multi-locale shape not supported the same way +check "locale: '*' (re-architect to per-locale queries)" "locale[[:space:]]*:[[:space:]]*['\"]\*['\"]" +# Prefer the builder method over a query param +check "Contentful 'locale:' param (use .locale(...))" "[^.]locale[[:space:]]*:[[:space:]]*['\"][a-zA-Z]" + +finish diff --git a/skills/contentstack-migration-companion-beta/scripts/09_graphql.sh b/skills/contentstack-migration-companion-beta/scripts/09_graphql.sh new file mode 100755 index 0000000..d9ad191 --- /dev/null +++ b/skills/contentstack-migration-companion-beta/scripts/09_graphql.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# CONDITIONAL. GraphQL Content API migration. (doc §17) +. "$(dirname "$0")/_lib.sh" +begin "graphql" "GraphQL endpoint/auth/schema migration" + +signal="$(search "(graphql\.contentful\.com|graphql\.contentstack\.com|@apollo/client|urql|graphql-request|gql\`)" "js,jsx,ts,tsx,mjs,cjs,vue,svelte,astro,graphql,gql")" +[ -z "$signal" ] && na "no GraphQL data-access detected" + +# Residue +check "Contentful GraphQL endpoint" "graphql\.contentful\.com" "js,jsx,ts,tsx,mjs,cjs,vue,svelte,astro,graphql,gql,json,env,example" +check "Contentful 'xxxCollection' query naming (Contentstack uses all_)" "[A-Za-z0-9_]+Collection[[:space:]]*\(" "js,jsx,ts,tsx,mjs,cjs,graphql,gql" +check "Authorization: Bearer header (Contentstack uses access_token header)" "Authorization['\"]?[[:space:]]*:[[:space:]]*[\`'\"]?Bearer" + +# Positive: must point at Contentstack GraphQL with the right auth +require_present "Contentstack GraphQL endpoint (graphql.contentstack.com)" "graphql\.contentstack\.com" "js,jsx,ts,tsx,mjs,cjs,vue,svelte,astro,graphql,gql,json,env,example" +require_present "access_token header for Contentstack GraphQL" "access_token" + +finish diff --git a/skills/contentstack-migration-companion-beta/scripts/10_livepreview.sh b/skills/contentstack-migration-companion-beta/scripts/10_livepreview.sh new file mode 100755 index 0000000..7bc4a2f --- /dev/null +++ b/skills/contentstack-migration-companion-beta/scripts/10_livepreview.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# CONDITIONAL. Live Preview / draft mode migration. (doc §18) +. "$(dirname "$0")/_lib.sh" +begin "live-preview" "Live Preview / draft mode reimplementation" + +cf_preview="$(search "(@contentful/live-preview|ContentfulLivePreview|useContentfulLiveUpdates|useContentfulInspectorMode|preview\.contentful\.com)")" +cs_preview="$(search "(@contentstack/live-preview-utils|ContentstackLivePreview|livePreviewQuery|live_preview)")" + +if [ -z "$cf_preview" ] && [ -z "$cs_preview" ]; then + na "no Live Preview / draft mode usage detected" +fi + +# Residue: Contentful preview must be gone +check "@contentful/live-preview import" "@contentful/live-preview" +check "ContentfulLivePreview usage" "ContentfulLivePreview" +check "useContentfulLiveUpdates hook" "useContentfulLiveUpdates" +check "useContentfulInspectorMode hook" "useContentfulInspectorMode" +check "Contentful preview host" "preview\.contentful\.com" + +# Positive: Contentstack Live Preview must be wired +require_present "@contentstack/live-preview-utils dependency/usage" "@contentstack/live-preview-utils" +require_present "ContentstackLivePreview.init(...)" "ContentstackLivePreview\.init" +require_present "live_preview config on stack init" "live_preview" +require_present "real-time updates via onEntryChange(...)" "onEntryChange\(" + +# If source had click-to-edit, expect edit tags +if [ -n "$(search "useContentfulInspectorMode")" ]; then + require_present "edit tags (addEditableTags / data-cslp) for click-to-edit parity" "(addEditableTags|data-cslp)" +fi + +# Preview tokens must not be hardcoded in client code +check "Hardcoded preview/management token literal" "(preview_token|management_token)[[:space:]]*:[[:space:]]*['\"][A-Za-z0-9]{6,}['\"]" + +finish diff --git a/skills/contentstack-migration-companion-beta/scripts/11_build_typecheck.sh b/skills/contentstack-migration-companion-beta/scripts/11_build_typecheck.sh new file mode 100755 index 0000000..c43bca7 --- /dev/null +++ b/skills/contentstack-migration-companion-beta/scripts/11_build_typecheck.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# HARD GATE & AUTHORITATIVE. Typecheck / lint / build must pass. (doc §15 step 13) +# Runs in the TARGET dir. Assumes deps are installed (run `npm ci` / `pnpm i` first if needed). +. "$(dirname "$0")/_lib.sh" +begin "build" "Typecheck / lint / build" + +[ -f "$TARGET/package.json" ] || na "no package.json (not a JS/TS app, or wrong target dir)" + +# Pick a package manager +PM="npm" +[ -f "$TARGET/pnpm-lock.yaml" ] && command -v pnpm >/dev/null 2>&1 && PM="pnpm" +[ -f "$TARGET/yarn.lock" ] && command -v yarn >/dev/null 2>&1 && PM="yarn" +[ -f "$TARGET/bun.lockb" ] && command -v bun >/dev/null 2>&1 && PM="bun" + +has_script() { grep -Eq "\"$1\"[[:space:]]*:" "$TARGET/package.json"; } +run() { # run LABEL CMD... + local label="$1"; shift + echo " --- $label: $* ---" >> "$_TMP" + if ( cd "$TARGET" && "$@" ) >>"$_TMP" 2>&1; then + echo " ✓ $label passed" ; return 0 + else + add_finding "$label FAILED (see output below)"; return 1 + fi +} + +ran_any=0 + +# Typecheck (preferred signal) +if [ -f "$TARGET/tsconfig.json" ]; then + ran_any=1 + if has_script "typecheck"; then run "typecheck" $PM run typecheck + elif command -v npx >/dev/null 2>&1; then run "tsc --noEmit" npx -y tsc --noEmit + fi +fi + +# Lint (non-fatal signal, but reported) +if has_script "lint"; then ran_any=1; run "lint" $PM run lint || true; fi + +# Build (authoritative) +if has_script "build"; then ran_any=1; run "build" $PM run build; fi + +[ "$ran_any" -eq 0 ] && na "no tsconfig/typecheck/lint/build script found to run" + +# Emit captured command output for triage +echo "### [$EVAL_NAME] $EVAL_TITLE" +if [ "$FINDINGS" -eq 0 ]; then + echo "STATUS: PASS (pkg manager: $PM)" + echo "SUMMARY_JSON: {\"eval\":\"build\",\"status\":\"PASS\",\"findings\":0}" + rm -f "$_TMP"; exit 0 +fi +echo "STATUS: FAIL (pkg manager: $PM) — $FINDINGS step(s) failed" +cat "$_TMP" +echo "SUMMARY_JSON: {\"eval\":\"build\",\"status\":\"FAIL\",\"findings\":$FINDINGS}" +rm -f "$_TMP"; exit 1 diff --git a/skills/contentstack-migration-companion-beta/scripts/12_secrets.sh b/skills/contentstack-migration-companion-beta/scripts/12_secrets.sh new file mode 100755 index 0000000..401ca50 --- /dev/null +++ b/skills/contentstack-migration-companion-beta/scripts/12_secrets.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# HARD GATE. No hardcoded credentials; tokens via env; example env updated. (doc §15 step 2) +. "$(dirname "$0")/_lib.sh" +begin "secrets" "No hardcoded credentials / env hygiene" + +# Hardcoded Contentstack credential literals in source +check "Hardcoded delivery/preview/management token literal" \ + "(deliveryToken|preview_token|management_token|access_token)[[:space:]]*[:=][[:space:]]*['\"][A-Za-z0-9_-]{8,}['\"]" +check "Hardcoded apiKey literal" "apiKey[[:space:]]*[:=][[:space:]]*['\"][A-Za-z0-9]{8,}['\"]" +# Contentstack token/key shapes appearing as literals +check "Contentstack-shaped token literal (cs.../blt...) in source" "['\"](cs[a-f0-9]{12,}|blt[a-z0-9]{12,})['\"]" + +# A committed .env with real values is a leak risk +if [ -f "$TARGET/.env" ]; then + if [ -f "$TARGET/.gitignore" ] && grep -Eq '(^|/)\.env($|[^.])' "$TARGET/.gitignore"; then :; else + add_finding ".env present but not clearly git-ignored — verify it is not committed" + fi +fi + +# Example env should advertise the new Contentstack vars +if ls "$TARGET"/.env.example "$TARGET"/.env.sample "$TARGET"/.env.local.example >/dev/null 2>&1; then + if ! grep -Eqr 'CS_(API_KEY|DELIVERY_TOKEN|ENVIRONMENT)|CONTENTSTACK_' "$TARGET"/.env.* 2>/dev/null; then + add_finding "example env file exists but lists no Contentstack (CS_*/CONTENTSTACK_*) variables" + fi +fi + +finish diff --git a/skills/contentstack-migration-companion-beta/scripts/13_todos_report.sh b/skills/contentstack-migration-companion-beta/scripts/13_todos_report.sh new file mode 100755 index 0000000..ca7c18e --- /dev/null +++ b/skills/contentstack-migration-companion-beta/scripts/13_todos_report.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# INFORMATIONAL. Surface migration TODOs and guessed UIDs for mandatory human review. +. "$(dirname "$0")/_lib.sh" +begin "todos" "Migration TODOs & guessed-UID review list" + +check "TODO(migration) markers" "TODO\(migration\)" +check "Generic migration FIXMEs" "FIXME|XXX|@migration" +check "Guessed/placeholder UIDs" "(GUESS|PLACEHOLDER|REPLACE_ME||your_[a-z_]*_uid)" + +report diff --git a/skills/contentstack-migration-companion-beta/scripts/_lib.sh b/skills/contentstack-migration-companion-beta/scripts/_lib.sh new file mode 100755 index 0000000..4c9edc8 --- /dev/null +++ b/skills/contentstack-migration-companion-beta/scripts/_lib.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +# Shared helpers for Contentful -> Contentstack migration evals. +# Each eval script sources this, then calls: begin / check / add_finding / require_present / finish | na +# +# Contract / exit codes (consumed by run-all.sh and by AI orchestrators): +# PASS -> exit 0 (no issues found) +# FAIL -> exit 1 (findings to review; for hard-gate evals this BLOCKS) +# ERROR -> exit 2 (the eval itself failed to run) +# N/A -> exit 3 (this approach is not used by the app; skip) +# REPORT -> exit 0 (informational only, e.g. TODO listing) +# +# Findings are signals for a human/AI to triage, not proof of a bug. Static greps +# can produce false positives — every FAIL must be reasoned about. The build and +# secrets evals are authoritative. + +set -uo pipefail + +TARGET="${TARGET:-${1:-.}}" +TARGET="${TARGET%/}" + +# Source-code file extensions to scan (language-agnostic across JS/TS frameworks). +CODE_EXT='js,jsx,ts,tsx,mjs,cjs,vue,svelte,astro' +EXCLUDE_DIRS='node_modules dist build .next .nuxt .svelte-kit out coverage .git .turbo .cache .vercel .output public/build' + +if command -v rg >/dev/null 2>&1; then HAVE_RG=1; else HAVE_RG=0; fi + +# search REGEX [GLOBS] -> prints "file:line:match" +search() { + local regex="$1"; local globs="${2:-$CODE_EXT}" + if [ "$HAVE_RG" -eq 1 ]; then + local exargs=(); local d + for d in $EXCLUDE_DIRS; do exargs+=(-g "!$d/**"); done + rg --no-heading --line-number --color never "${exargs[@]}" -g "*.{$globs}" -e "$regex" "$TARGET" 2>/dev/null + else + local inc=() exc=() e d + IFS=',' read -ra _exts <<< "$globs" + for e in "${_exts[@]}"; do inc+=(--include="*.$e"); done + for d in $EXCLUDE_DIRS; do exc+=(--exclude-dir="$d"); done + grep -rEn "${inc[@]}" "${exc[@]}" -e "$regex" "$TARGET" 2>/dev/null + fi +} + +# files_with REGEX [GLOBS] -> unique file list +files_with() { search "$1" "${2:-$CODE_EXT}" | cut -d: -f1 | sort -u; } + +# ---- reporting state ---- +EVAL_NAME=""; EVAL_TITLE=""; FINDINGS=0; _TMP="" +begin() { + EVAL_NAME="$1"; EVAL_TITLE="$2"; FINDINGS=0 + _TMP="$(mktemp)" || { echo "ERROR: mktemp failed"; exit 2; } +} + +# add_finding "message" — record one issue +add_finding() { printf ' ▸ %s\n' "$1" >> "$_TMP"; FINDINGS=$((FINDINGS+1)); } + +# check "description" REGEX [GLOBS] — flag every match of a pattern that should NOT exist +check() { + local desc="$1" regex="$2" globs="${3:-$CODE_EXT}" out n + out="$(search "$regex" "$globs")" + if [ -n "$out" ]; then + n="$(printf '%s\n' "$out" | grep -c .)" + { printf ' ▸ %s [%s hit(s)]\n' "$desc" "$n"; printf '%s\n' "$out" | sed 's/^/ /'; } >> "$_TMP" + FINDINGS=$((FINDINGS+n)) + fi +} + +# require_present "description" REGEX [GLOBS] — flag if a REQUIRED pattern is MISSING +require_present() { + local desc="$1" regex="$2" globs="${3:-$CODE_EXT}" + if [ -z "$(search "$regex" "$globs")" ]; then + add_finding "MISSING (expected to be present): $desc" + fi +} + +# na "reason" — mark eval not-applicable and exit +na() { + echo "### [$EVAL_NAME] $EVAL_TITLE" + echo "STATUS: N/A — $1" + echo "SUMMARY_JSON: {\"eval\":\"$EVAL_NAME\",\"status\":\"NA\",\"findings\":0}" + [ -n "$_TMP" ] && rm -f "$_TMP" + exit 3 +} + +# finish — print result and exit with PASS/FAIL code +finish() { + echo "### [$EVAL_NAME] $EVAL_TITLE" + if [ "$FINDINGS" -eq 0 ]; then + echo "STATUS: PASS" + echo "SUMMARY_JSON: {\"eval\":\"$EVAL_NAME\",\"status\":\"PASS\",\"findings\":0}" + rm -f "$_TMP"; exit 0 + fi + echo "STATUS: FAIL — $FINDINGS finding(s) to triage" + cat "$_TMP" + echo "SUMMARY_JSON: {\"eval\":\"$EVAL_NAME\",\"status\":\"FAIL\",\"findings\":$FINDINGS}" + rm -f "$_TMP"; exit 1 +} + +# report — informational eval; always exits 0 +report() { + echo "### [$EVAL_NAME] $EVAL_TITLE" + if [ "$FINDINGS" -eq 0 ]; then + echo "STATUS: REPORT — nothing to list" + else + echo "STATUS: REPORT — $FINDINGS item(s) for human review" + cat "$_TMP" + fi + echo "SUMMARY_JSON: {\"eval\":\"$EVAL_NAME\",\"status\":\"REPORT\",\"findings\":$FINDINGS}" + rm -f "$_TMP"; exit 0 +} diff --git a/skills/contentstack-migration-companion-beta/scripts/check_prereqs.py b/skills/contentstack-migration-companion-beta/scripts/check_prereqs.py new file mode 100644 index 0000000..5c8cbea --- /dev/null +++ b/skills/contentstack-migration-companion-beta/scripts/check_prereqs.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 +""" +check_prereqs.py — silent prerequisite evaluator for the CF→CS migration. + +Runs all checks in one pass, auto-installs missing CLIs (csdx, contentful), +and emits a single JSON summary to stdout. + +Exit codes: + 0 — all hard requirements met (some items may still need auth, flagged in JSON) + 1 — Node.js missing or too old (migration cannot proceed at all) +""" +import json +import os +import pathlib +import re +import subprocess +import sys +import urllib.request + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def run(cmd, env=None): + try: + r = subprocess.run( + cmd, + capture_output=True, + text=True, + env=env if env is not None else os.environ, + ) + return r.returncode, r.stdout.strip(), r.stderr.strip() + except FileNotFoundError: + return 1, "", f"{cmd[0]}: command not found" + + +def npm_install(pkg): + """Silently install a global npm package. Errors surfaced via the JSON result.""" + subprocess.run( + ["npm", "install", "-g", pkg], + capture_output=True, + ) + + +# --------------------------------------------------------------------------- +# Checks +# --------------------------------------------------------------------------- + +out = {} + +# ── Node.js ───────────────────────────────────────────────────────────────── +rc, ver, _ = run(["node", "--version"]) +if rc != 0: + out["node"] = {"ok": False, "error": "not_installed"} + print(json.dumps(out, indent=2)) + sys.exit(1) + +m = re.match(r"v(\d+)", ver) +major = int(m.group(1)) if m else 0 +out["node"] = {"ok": major >= 20, "version": ver, "major": major} +if major < 20: + # Hard blocker — emit result and exit 1 so the step file can surface the error + print(json.dumps(out, indent=2)) + sys.exit(1) + +# ── Contentstack CLI (csdx) ────────────────────────────────────────────────── +rc, ver, _ = run(["csdx", "--version"]) +if rc != 0: + print("Installing @contentstack/cli …", file=sys.stderr) + npm_install("@contentstack/cli") + rc, ver, _ = run(["csdx", "--version"]) +out["csdx"] = {"ok": rc == 0, "version": ver if rc == 0 else None} + +# ── Contentstack region ────────────────────────────────────────────────────── +rc, region_raw, _ = run(["csdx", "config:get:region"]) +region = region_raw.strip() if rc == 0 else "UNKNOWN" +out["cs_region"] = {"region": region} + +# ── Contentstack login + org UID ───────────────────────────────────────────── +rc, whoami, _ = run(["csdx", "auth:whoami"]) +logged_in_cs = rc == 0 and whoami and "No user" not in whoami and "not logged" not in whoami.lower() + +if logged_in_cs: + # Ensure cli-utilities is available, then read the oauth org UID + npm_install("@contentstack/cli-utilities") + rc2, npm_root, _ = run(["npm", "root", "-g"]) + node_env = {**os.environ, "NODE_PATH": npm_root.strip()} + uid_script = ( + "const {configHandler}=require('@contentstack/cli-utilities');" + "const t=configHandler.get('authorisationType');" + "const o=configHandler.get('oauthOrgUid');" + "const e=configHandler.get('email')||'';" + "if(t!=='OAUTH'||!o){process.exit(1);}" + "console.log(JSON.stringify({orgUid:o,email:e}));" + ) + rc3, uid_out, _ = run(["node", "-e", uid_script], env=node_env) + if rc3 == 0: + try: + uid_data = json.loads(uid_out) + out["cs_login"] = { + "ok": True, + "email": uid_data.get("email") or whoami, + "org_uid": uid_data.get("orgUid"), + } + except Exception: + out["cs_login"] = {"ok": True, "email": whoami, "org_uid": None} + else: + # Logged in but not via OAuth (or UID missing) — flag for re-auth + out["cs_login"] = { + "ok": True, + "email": whoami, + "org_uid": None, + "needs_oauth_reauth": True, + } +else: + out["cs_login"] = {"ok": False, "needs_login": True} + +# ── Contentful CLI ─────────────────────────────────────────────────────────── +rc, ver, _ = run(["contentful", "--version"]) +if rc != 0: + print("Installing contentful-cli …", file=sys.stderr) + npm_install("contentful-cli") + rc, ver, _ = run(["contentful", "--version"]) +out["contentful_cli"] = {"ok": rc == 0, "version": ver if rc == 0 else None} + +# ── Contentful login + spaces ──────────────────────────────────────────────── +rc, spaces_raw, spaces_err = run(["contentful", "space", "list"]) +auth_error = "You have to be logged in" in spaces_raw or "You have to be logged in" in spaces_err +logged_in_cf = rc == 0 and not auth_error + +if logged_in_cf: + identity = {"ok": True} + + # Resolve account identity from the stored management token + for p in ["~/.contentfulrc.json", "~/.config/contentful/config.json"]: + f = pathlib.Path(p).expanduser() + if not f.exists(): + continue + try: + d = json.loads(f.read_text()) + tok = ( + d.get("managementToken") + or d.get("cmaToken") + or d.get("management_token") + ) + if not tok: + continue + req = urllib.request.Request( + "https://api.contentful.com/users/me", + headers={ + "Authorization": f"Bearer {tok}", + "Content-Type": "application/json", + }, + ) + with urllib.request.urlopen(req, timeout=8) as resp: + user = json.loads(resp.read()) + identity["name"] = ( + f"{user.get('firstName', '')} {user.get('lastName', '')}".strip() + ) + identity["email"] = user.get("email", "") + break + except Exception: + pass + + out["contentful_login"] = identity + + # Parse spaces from the table output + spaces = [] + for line in spaces_raw.split("\n"): + if "│" in line: + cols = [c.strip() for c in re.split(r"│", line) if c.strip()] + if len(cols) >= 2 and cols[0] not in ("Space name", ""): + name = re.sub(r"\s*\[.*?\]", "", cols[0]).strip() + sid = cols[1] + if name and sid: + spaces.append({"name": name, "id": sid}) + out["contentful_spaces"] = spaces +else: + out["contentful_login"] = {"ok": False, "needs_login": True} + out["contentful_spaces"] = [] + +# ── Done ───────────────────────────────────────────────────────────────────── +print(json.dumps(out, indent=2)) +sys.exit(0) diff --git a/skills/contentstack-migration-companion-beta/scripts/log.sh b/skills/contentstack-migration-companion-beta/scripts/log.sh new file mode 100755 index 0000000..9232fd3 --- /dev/null +++ b/skills/contentstack-migration-companion-beta/scripts/log.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# Verbose session logger for the Contentful -> Contentstack migration. +# +# NOT a pass/fail eval — an append-only AUDIT TRAIL of everything the migration touched: +# user inputs, AI actions & communications, decisions, eval results, commands, and exceptions. +# Writes both a human-readable log and a machine-readable JSONL stream, plus per-command output. +# +# Usage: +# log.sh # append one entry +# log.sh run "