# Migration Companion

> contentstack-migration-companion

- Skill: `contentstack/migration-companion` (Agent Skill, multi-file: 21 files)
- Install (CLI): `npx skillmds@latest add contentstack/migration-companion`
- Raw SKILL.md: https://api.skillmd.com/api/skills/contentstack/migration-companion/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: contentstack (https://skillmd.com/u/contentstack)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/contentstack/migration-companion

---

# contentstack-migration-companion


# contentstack-migration-companion

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 `<project>/.claude/skills/contentstack-migration-companion`,
  `~/.claude/skills/contentstack-migration-companion`, or a Windows path like
  `%USERPROFILE%\.claude\skills\contentstack-migration-companion`. 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/SKILL.md`), and for shell commands set it as
  a variable up front (`SKILL_DIR="<that absolute path>"`) so bundled scripts can be invoked as
  `"$SKILL_DIR/scripts/<name>"`. The bundled scripts self-locate their own siblings, so once you
  invoke them by absolute path they work regardless of your current directory.
- **Pin the Node version for the whole session.** A machine often has several Node versions
  (system, Homebrew, multiple nvm installs) and a non-interactive shell may resolve an old one
  (e.g. `/usr/local/bin/node` v14) ahead of the user's nvm default. The Step 1 prereq checker
  finds the **highest installed Node ≥ 20** and reports its directory as `node.bin_dir` in the
  JSON. Record that value as a concrete literal `NODE_BIN_DIR` and **prefix every `csdx`, `npm`,
  and `contentful` command for the rest of the migration** with it, e.g.
  `PATH="<NODE_BIN_DIR>:$PATH" csdx migrate:create …`. This guarantees the CLI runs on the
  Node the prereq check validated, not whatever an unconfigured shell picks first. If
  `node.bin_dir` is absent (older check output), fall back to the plain command.
- **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 `--source 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 `<SESSION_DIR>`."

### 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.

**Record the Node bin directory.** Capture `node.bin_dir` from the JSON as the concrete literal
`NODE_BIN_DIR`. Per the "Pin the Node version" principle in the overview, prefix every later
`csdx`, `npm`, and `contentful` command with `PATH="<NODE_BIN_DIR>:$PATH"` so the whole migration
runs on the Node the checker validated — not whatever an unconfigured non-interactive shell would
resolve first (the checker already picks the **highest installed Node ≥ 20**, scanning PATH and
all nvm installs). If `node.bin_dir` is missing, fall back to the plain command.

**Hard blocker:** If the script exits with code 1, Node.js is missing or too old. The reported
`node.version`/`node.path` reflect the *best* Node found anywhere on the machine, so the message is
accurate even when a newer Node exists but isn't on the default PATH. Stop immediately and tell the
user the exact problem:

- `node.error == "not_installed"` → "Node.js is not installed. Install it via `nvm install 22`
  or from nodejs.org, then try again."
- `node.ok == false` (e.g. `node.version == "v18.x"`) → "The newest Node I can find is
  `<version>`, but Node 20+ is required. Install a newer one with `nvm install 22 && nvm use 22`
  (or upgrade your system Node), 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                 | ✅ `<node.version>`                                   |                               |
| Contentstack CLI (csdx) | ✅ `<csdx.version>`                                   |                               |
| Contentstack region     | ⚙️ `<cs_region.region>`                               |                               |
| Contentstack login      | ✅ `<cs_login.email>`                                 | Org UID: `<cs_login.org_uid>` |
| Contentful CLI          | ✅ `<contentful_cli.version>`                         |                               |
| Contentful login        | ✅ `<contentful_login.name> <contentful_login.email>` |                               |

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 <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 <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 <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
# Use the Node the checker validated (NODE_BIN_DIR from the prereq JSON).
export PATH="<NODE_BIN_DIR>:$PATH"
$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"
```

(`export` only affects this one Eval block — shell state does not persist across commands, so
later steps must still prefix `PATH="<NODE_BIN_DIR>:$PATH"` per the overview principle.)

**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:<version>`** → already on the latest version; proceed to the Eval. (Stay
  silent — don't report "already up to date" to the user.)
- **`PLUGIN_INSTALLED:<version>`** → freshly installed. Briefly tell the user, then proceed.
- **`PLUGIN_UPDATED:<old>→<new>`** → uninstalled old version and installed latest. Briefly tell
  the user the plugin was updated, then proceed.
- **`PLUGIN_INSTALL_FAILED`** / **`PLUGIN_UPDATE_FAILED:<old>→<new>`** → 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 --source contentful \
  --space-id "$SPACE_ID" \
  --source-token "$CONTENTFUL_MANAGEMENT_TOKEN" \
  --org "$ORG_UID" \
  --download-assets \
  --output "$SESSION_DIR" \
  --workspace "$SESSION_DIR" \
  -y \
  2>&1 | tee "$SESSION_DIR/migrate-create.log"
```

Flag notes:

- `--space-id` / `--source-token` — Contentful source (token resolved in 3.0)
- `--org` — the org UID captured in Step 1; a new stack is created here
- `--source contentful` — declares Contentful as the migration source
- `--download-assets` — include asset binaries in the migration
- `--output "$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: <SESSION_DIR>/export.json
```

**Phase 2 — Convert** (transforms export into Contentstack bundle):

```
  validate   ✓  export.json
  extract    ✓  2 locales · 16 types
  transform  ✓  53 entries · 13 types  →  <SESSION_DIR>/bundle
  Bundle: <SESSION_DIR>/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: <SESSION_DIR>/logs
✓ Bundle metadata written: <SESSION_DIR>/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: <path>`               | Eval below         |
| `BUNDLE_DIR`    | `Bundle: <path> (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: <path>`             | Eval below         |
| `METADATA_PATH` | `✓ Bundle metadata written: <path>`                       | 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 "<STACK_NAME>" (<STACK_KEY>)
```

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 <REPO_URL>
> ```
>
> 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="<absolute path to this skill's install 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 <target> user-input "<what the user asked/answered>"` — every user input/communication.
- `log.sh <target> decision "<detection results, choices, assumptions, guessed UIDs>"`.
- `log.sh <target> ai-action "<files/queries you changed>"` and `communication "<what you told the user>"`.
- `log.sh <target> exception "<any error, blocker, or uncertainty you hit>"` — log all exceptions.
- Run shell commands through it so output + exit codes are captured and failures auto-log as
  exceptions: `log.sh <target> run "typecheck" -- npx tsc --noEmit`.

End with `log.sh <target> summary`. The log lands in `<target>/.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" <path-to-migrated-app>
   ```
   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 summar

…(truncated)
