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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
959 changes: 959 additions & 0 deletions skills/contentstack-migration-companion-beta/SKILL.md

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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.<meta> 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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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_<ct_uid>)" "[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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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|<content_type_uid>|your_[a-z_]*_uid)"

report
Loading
Loading