diff --git a/.github/build.yml b/.github/workflows/build.yml similarity index 100% rename from .github/build.yml rename to .github/workflows/build.yml diff --git a/README.md b/README.md index 7024458..a6cbbbd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Contentstack Skills -A bundle of 20 ready-to-use [Contentstack](https://www.contentstack.com) agent skills for AI coding tools, covering CMS implementation, Brand Kit guidance, delivery SDK development, Developer Hub app architecture, and Launch automation. +A bundle of 21 ready-to-use [Contentstack](https://www.contentstack.com) agent skills for AI coding tools, covering CMS implementation, Brand Kit guidance, delivery SDK development, migration workflows, Developer Hub app architecture, and Launch automation. ## What's in here? @@ -71,6 +71,7 @@ npx skills add @ | `dx-migrate-js-to-ts-sdk` | Migrate JS to TS SDK | Developer Experience | Migration from the JavaScript Contentstack SDK to the TypeScript Delivery SDK, including API mapping, rewrites, and unsupported pattern callouts. | | `launch-sync-environment-variables-from-env-example` | Sync Launch environment variables from .env.example | Launch | Comparing a local `.env.example` with Launch environment variables and patching missing keys without printing secrets. | | `launch-trigger-and-monitor-launch-deployments` | Trigger and Monitor Launch Deployments | Launch | Triggering Launch deployments, polling deployment status, retrieving failure logs, and summarizing likely causes and next steps. | +| `migration-companion` | Contentstack Migration Companion | Developer Experience | Guided Contentful-to-Contentstack migration workflow covering prerequisites, content migration, code migration, bundled validation scripts, and completion reporting. | ## How it works diff --git a/codex/AGENTS.md b/codex/AGENTS.md index 1e8fc8f..e86f870 100644 --- a/codex/AGENTS.md +++ b/codex/AGENTS.md @@ -5,6 +5,7 @@ Use the table below to route a user request to the right skill. Each row maps a | When the user asks… | Skill | |----------------------|-------| | Use when users ask about Contentstack Brand Kit, Voice Profiles, Knowledge Vault, brand voice, tone, style rules, on-brand AI generation, setup, governance, or API usage. Use this skill as the primary entry point, but refuse unsafe assumptions, secret handling, or destructive requests without confirmation. | [Brand Kit Assistant](./brand-kit-assistant/SKILL.md) | +| Use when users want to migrate, move, switch, port, or re-platform from Contentful to Contentstack, including content models, content, assets, locales, application integrations, website code, migration prerequisites, migration progress checks, or post-migration validation. | [contentstack-migration-companion](./migration-companion/SKILL.md) | | Use when a user wants to migrate Contentstack Delivery SDK code from JavaScript to TypeScript. | [dx-migrate-js-to-ts-sdk](./dx-migrate-js-to-ts-sdk/SKILL.md) | | Use when a user asks for Contentstack Delivery SDK code, query examples, helper functions, SDK setup, stack initialization, reference inclusion, filtering, sorting, pagination, typed entry fetching, asset fetching, Live Preview setup, Visual Builder support, SSR preview handling, or debugging SDK query chains. | [dx-delivery-sdk](./dx-delivery-sdk/SKILL.md) | | Use when designing, reviewing, or refactoring Contentstack content models before creating or changing schemas. | [Contentstack Data Modeling Best Practices](./cms-data-modeling-best-practices/SKILL.md) | diff --git a/codex/brand-kit-assistant/references/brand-kit-management-api-reference.md b/codex/brand-kit-assistant/references/brand-kit-management-api-reference.md new file mode 100644 index 0000000..0270709 --- /dev/null +++ b/codex/brand-kit-assistant/references/brand-kit-management-api-reference.md @@ -0,0 +1,31 @@ +Operate Contentstack Brand Kit and Voice Profile APIs with correct headers, payloads, and safety checks. Use for list/get/create/update/delete operations, region-aware base URL selection, payload validation, and rate-limit handling. + +## Safety +- Never expose Brand Kit API tokens or other secrets. +- Never delete Brand Kits. +- Require explicit user confirmation before deleting a Voice Profile. +- Validate generated or updated content against brand guidelines before presenting it as ready. +- Stop and ask for missing required IDs, headers, or region details. +- Treat 429 responses as rate-limit events and avoid aggressive retries. + +## Inputs +- Operation type: list, get, create, update, delete +- Target resource: Brand Kit or Voice Profile +- Region / base URL +- organization_uid and authtoken +- brand_kit_uid for Voice Profile operations +- brand_kit_uid or voice_profile_uid for targeted get/update/delete operations +- Name, description, linked stack API keys, communication_style, insights, and sample content as needed + +## Behavior +- Select the correct regional base URL. +- Include authtoken and organization_uid for Brand Kit endpoints. +- Include authtoken and brand_kit_uid for Voice Profile endpoints. +- Validate required IDs and payload fields before calling the API. +- For create/update, support brand_kit.name, optional brand_kit.description, and optional brand_kit.api_keys. +- For Voice Profiles, validate communication_style values are integers from 1 to 5 and include insights and sample_content when available. +- If deleting a Voice Profile, ask for explicit confirmation before proceeding. +- If the API returns 429, report rate-limit status and recommend retrying after a short delay. + +## Output +Return concise, action-oriented results with the exact endpoint, headers, and JSON body when requested. Summarize validation issues, returned IDs, and changed fields clearly. Do not include secrets. \ No newline at end of file diff --git a/codex/brand-kit-assistant/references/knowledge-vault-api-reference-skill.md b/codex/brand-kit-assistant/references/knowledge-vault-api-reference-skill.md new file mode 100644 index 0000000..4e988d9 --- /dev/null +++ b/codex/brand-kit-assistant/references/knowledge-vault-api-reference-skill.md @@ -0,0 +1,29 @@ +Operate Contentstack Knowledge Vault through the API and mapped MCP tools with region-aware endpoints, required headers, payload validation, and safe write/delete handling. Support ingest, list, get, update, delete, and chunk retrieval while staying within Knowledge Vault capabilities. + +## Safety +- Never expose Brand Kit API tokens or other secrets. +- Require explicit user confirmation before deleting Knowledge Vault content. +- Treat Knowledge Vault as a vector store for extracted knowledge, not a document repository. +- Do not imply original-file retrieval or binary asset access. +- Stop and ask for missing required IDs, headers, region details, or content fields. +- Treat 429 responses as rate-limit events and avoid aggressive retries. + +## Inputs +- Operation type: ingest/add, list, get, update, delete, get chunks, or MCP-mapped equivalent +- Region / base URL +- brand_kit_uid +- content_uid for get, update, or delete +- Content text to ingest or update +- Optional metadata such as title, category, date, or external ID + +## Behavior +- Select the correct regional base URL. +- Include authtoken and brand_kit_uid headers. +- Validate required IDs and payload fields before calling the API. +- Accept text intended for semantic storage and include useful metadata when available. +- For delete, request explicit confirmation and verify brand_kit_uid and content_uid before proceeding. +- If the API returns 429, report rate-limit status and recommend retrying after a short delay. +- Support mapped MCP operations where available. + +## Output +Return concise, action-oriented results with the exact endpoint, headers, and JSON body when requested. Summarize validation issues, returned IDs, and changed fields clearly. Do not include secrets. \ No newline at end of file diff --git a/codex/developer-hub-app-architect/references/developer-hub-coding-reference.md b/codex/developer-hub-app-architect/references/developer-hub-coding-reference.md new file mode 100644 index 0000000..f99d4d5 --- /dev/null +++ b/codex/developer-hub-app-architect/references/developer-hub-coding-reference.md @@ -0,0 +1,860 @@ +# Contentstack Developer Hub & Custom Apps Guide + +Complete guide for building custom apps and extensions for Contentstack Developer Hub. This guide covers the App SDK, UI locations, implementation patterns, and best practices for AI coding assistants. + +## What is Contentstack Developer Hub? + +[Contentstack Developer Hub](https://www.contentstack.com/docs/developers/developer-hub) is a platform that allows developers to build custom applications that extend and enhance the Contentstack CMS experience. It provides APIs, SDKs, and tools to create apps that integrate seamlessly into Contentstack's user interface. + +### Key Features + +- **App Framework**: Complete framework for building custom apps with React, TypeScript, and modern tooling +- **UI Locations**: Multiple integration points within Contentstack's interface (sidebars, dashboards, custom fields, etc.) +- **SDK Integration**: JavaScript SDK (`@contentstack/app-sdk`) for interacting with Contentstack's APIs and UI +- **Marketplace**: Ability to publish apps to Contentstack's marketplace or keep them private +- **OAuth Integration**: Secure authentication and authorization for apps +- **App Hosting**: Options for external hosting or Contentstack-managed hosting + +### Types of Apps + +1. **Marketplace Apps**: Public apps available to all Contentstack users +2. **Private Apps**: Organization-specific apps for internal use +3. **Machine-to-Machine Apps**: Apps that interact with Contentstack APIs without user interface + +--- + +## Key Concepts + +### App Manifest + +The app manifest is configured in the Contentstack Developer Hub platform (not in the codebase). It defines: + +- App metadata (name, description, icon) +- UI locations where the app appears +- Routing configuration (paths to your app's routes) +- Hosting information (base URL) +- Visibility settings (public/private) + +The manifest configuration is done through the Developer Hub UI when creating or managing your app. + +### UI Locations + +UI locations are specific places in Contentstack's interface where your app can appear: + +| Location | Path | Use Case | Description | +| ------------------ | -------------------- | -------------------- | -------------------------------------------------------- | +| **Asset Sidebar** | `/asset-sidebar` | Actions on assets | Sidebar in the Asset Library when viewing/editing assets | +| **Entry Sidebar** | `/entry-sidebar` | Actions on entries | Sidebar when editing content entries | +| **Custom Field** | `/custom-field` | Custom input fields | Custom field type for content types | +| **Dashboard** | `/stack-dashboard` | Dashboard widgets | Widget on the stack dashboard | +| **App Config** | `/app-configuration` | App settings | Configuration page for app settings | +| **Full Page** | `/full-page` | Standalone pages | Standalone full-page app | +| **Field Modifier** | `/field-modifier` | Modify field values | Modifies field values programmatically | +| **RTE Location** | `/json-rte.js` | Rich text extensions | Rich text editor extensions | + +### App SDK + +The `@contentstack/app-sdk` provides: + +- **SDK Initialization**: `ContentstackAppSDK.init()` - Initializes the SDK and establishes connection +- **Location Access**: Access to current UI location and its data +- **Asset Operations**: Methods to read, upload, and replace assets +- **Entry Operations**: Methods to read and modify content entries +- **Configuration**: Access to app configuration settings +- **Frame Management**: Control iframe dimensions and auto-resizing +- **API Proxy**: Make external API calls through Contentstack's secure proxy + +**Basic SDK Usage:** + +```typescript +import ContentstackAppSDK from "@contentstack/app-sdk"; + +// Initialize SDK +const appSdk = await ContentstackAppSDK.init(); + +// Access current location +const location = appSdk.location; + +// Access configuration +const config = await appSdk.getConfig(); + +// Access stack data +const stack = appSdk.stack; +``` + +--- + +## Architecture Overview + +### How Apps Work + +Developer Hub apps run in an **iframe** within Contentstack's interface. The app is a standalone React application that communicates with Contentstack via the App SDK. + +### Initialization Flow + +1. **App Loads**: React app loads in Contentstack's iframe +2. **SDK Initialization**: App calls `ContentstackAppSDK.init()` +3. **Token Validation**: App verifies authentication token from URL +4. **Location Detection**: SDK identifies which UI location is active +5. **Context Setup**: SDK instance and config are provided via React Context +6. **Component Rendering**: UI components access SDK via hooks + +### Data Flow + +``` +Contentstack UI (iframe) + ↓ +MarketplaceAppProvider + ↓ +ContentstackAppSDK.init() + ↓ +React Context (appSdk, appConfig) + ↓ +Location Component (AssetSidebar, EntrySidebar, etc.) + ↓ +useAppSdk() hook + ↓ +Location-specific operations (getData, setData, etc.) +``` + +--- + +## Prerequisites + +- React and TypeScript knowledge +- Contentstack account with Developer Hub access +- Node.js v18+ +- Understanding of iframe communication patterns + +--- + +## Quick Start + +### Use the Official Boilerplate (Recommended) + +The [Contentstack Marketplace App Boilerplate](https://github.com/contentstack/marketplace-app-boilerplate) provides: + +- **Pre-configured SDK Setup**: Ready-to-use `MarketplaceAppProvider` for SDK initialization +- **Custom Hooks**: Pre-built hooks like `useAppSdk`, `useAppLocation`, `useAppConfig` +- **Routing Structure**: React Router setup with lazy loading for optimal performance +- **UI Location Templates**: Scaffolding for all available UI locations +- **Testing Infrastructure**: E2E testing setup with Playwright +- **Build Configuration**: Vite configuration optimized for Contentstack apps +- **TypeScript Support**: Full TypeScript setup with proper type definitions + +```bash +# Clone the official boilerplate +git clone https://github.com/contentstack/marketplace-app-boilerplate +cd marketplace-app-boilerplate +npm install +npm run dev +``` + +### Project Structure + +``` +src/ +├── containers/ +│ ├── App/ # Main app component with routing +│ ├── AssetSidebarWidget/ # Asset sidebar location +│ ├── EntrySidebar/ # Entry sidebar location +│ ├── CustomField/ # Custom field location +│ ├── Dashboard/ # Dashboard widget location +│ └── AppConfiguration/ # App configuration page +├── common/ +│ ├── providers/ # React context providers +│ │ └── MarketplaceAppProvider.tsx # SDK initialization +│ ├── hooks/ # Custom React hooks +│ │ ├── useAppSdk.tsx # Access to SDK instance +│ │ └── useAppLocation.ts # Current UI location +│ └── contexts/ # React contexts +│ └── marketplaceContext.ts +└── components/ # Reusable components + └── ErrorBoundary.tsx +``` + +--- + +## SDK Initialization + +### Provider Setup + +The SDK must be initialized once at the app root level: + +```typescript +// providers/MarketplaceAppProvider.tsx +import { useEffect, useState } from "react"; +import ContentstackAppSDK from "@contentstack/app-sdk"; + +export function MarketplaceAppProvider({ children }) { + const [appSdk, setAppSdk] = useState(null); + const [config, setConfig] = useState(null); + const [failed, setFailed] = useState(false); + + useEffect(() => { + ContentstackAppSDK.init() + .then(async (sdk) => { + setAppSdk(sdk); + const appConfig = await sdk.getConfig(); + setConfig(appConfig); + }) + .catch(() => { + setFailed(true); + }); + }, []); + + if (failed) return
Failed to initialize SDK
; + if (!appSdk) return
Loading...
; + + return ( + + {children} + + ); +} +``` + +### Using the SDK Hook + +```typescript +// hooks/useAppSdk.tsx +import { useContext } from "react"; +import { MarketplaceContext } from "../contexts/marketplaceContext"; + +export function useAppSdk() { + const { appSdk } = useContext(MarketplaceContext); + if (!appSdk) { + throw new Error("useAppSdk must be used within MarketplaceAppProvider"); + } + return appSdk; +} +``` + +### App Root Setup + +```typescript +// App.tsx +import { MarketplaceAppProvider } from "./providers/MarketplaceAppProvider"; +import { BrowserRouter } from "react-router-dom"; + +function App() { + return ( + + {/* Your routes */} + + ); +} +``` + +--- + +## UI Location Implementations + +### Asset Sidebar Widget + +The Asset Sidebar appears when viewing or editing an asset in Contentstack's Asset Library. + +```typescript +// containers/AssetSidebarWidget/AssetSidebar.tsx +import { useState, useEffect } from "react"; +import { useAppSdk } from "@/common/hooks/useAppSdk"; + +export function AssetSidebar() { + const appSdk = useAppSdk(); + const [asset, setAsset] = useState(null); + const [loading, setLoading] = useState(true); + + // Get asset sidebar location + const assetSidebar = (appSdk?.location as any)?.AssetSidebarWidget; + + useEffect(() => { + if (assetSidebar) { + // Get current asset data + assetSidebar.getData().then((data) => { + setAsset(data); + setLoading(false); + }); + } + }, [assetSidebar]); + + // Replace asset with new file + const replaceAsset = async (file: File) => { + try { + await assetSidebar.replaceAsset(file); + // Refresh asset data + const updated = await assetSidebar.getData(); + setAsset(updated); + } catch (error) { + console.error("Failed to replace asset:", error); + } + }; + + if (loading) return
Loading asset...
; + + return ( +
+

Asset: {asset?.title}

+

Type: {asset?.content_type}

+

Size: {asset?.file_size} bytes

+ {/* Your custom UI */} +
+ ); +} +``` + +**Available Methods:** + +- `getData()` - Get current asset data +- `replaceAsset(file)` - Replace asset with new file +- `onAssetChange(callback)` - Listen for asset changes + +### Entry Sidebar Widget + +The Entry Sidebar appears when editing a content entry. + +```typescript +// containers/EntrySidebar/EntrySidebar.tsx +import { useState, useEffect } from "react"; +import { useAppSdk } from "@/common/hooks/useAppSdk"; + +export function EntrySidebar() { + const appSdk = useAppSdk(); + const [entry, setEntry] = useState(null); + + const sidebar = appSdk?.location?.SidebarWidget; + + useEffect(() => { + if (sidebar) { + // Get entry data + sidebar.getData().then(setEntry); + + // Listen for save events + sidebar.onSave(() => { + console.log("Entry saved"); + // Refresh entry data + sidebar.getData().then(setEntry); + }); + + // Listen for publish events + sidebar.onPublish(() => { + console.log("Entry published"); + }); + } + }, [sidebar]); + + // Update field value + const setFieldValue = async (field: string, value: any) => { + try { + await sidebar.entry.setField(field, value); + // Refresh entry data + const updated = await sidebar.getData(); + setEntry(updated); + } catch (error) { + console.error("Failed to set field:", error); + } + }; + + if (!entry) return
Loading entry...
; + + return ( +
+

Entry: {entry?.title}

+

Content Type: {entry?.content_type_uid}

+ {/* Your custom UI */} +
+ ); +} +``` + +**Available Methods:** + +- `getData()` - Get current entry data +- `entry.setField(field, value)` - Update field value +- `entry.getField(field)` - Get field value +- `onSave(callback)` - Listen for save events +- `onPublish(callback)` - Listen for publish events + +### Custom Field + +Custom fields allow you to create custom input types for content types. + +```typescript +// containers/CustomField/CustomField.tsx +import { useState, useEffect } from "react"; +import { useAppSdk } from "@/common/hooks/useAppSdk"; + +export function CustomField() { + const appSdk = useAppSdk(); + const [value, setValue] = useState(""); + const [fieldConfig, setFieldConfig] = useState(null); + + const customField = appSdk?.location?.CustomField; + + useEffect(() => { + if (customField) { + // Get initial value + customField.field.getData().then(setValue); + + // Get field configuration + customField.field.getConfig().then(setFieldConfig); + } + }, [customField]); + + const handleChange = async (newValue: string) => { + setValue(newValue); + // Update field value + await customField.field.setData(newValue); + }; + + return ( +
+ + handleChange(e.target.value)} + placeholder={fieldConfig?.placeholder} + /> +
+ ); +} +``` + +**Available Methods:** + +- `field.getData()` - Get current field value +- `field.setData(value)` - Set field value +- `field.getConfig()` - Get field configuration +- `field.setFocus()` - Focus the field + +### Dashboard Widget + +Dashboard widgets appear on the stack dashboard. + +```typescript +// containers/Dashboard/Dashboard.tsx +import { useEffect, useState } from "react"; +import { useAppSdk } from "@/common/hooks/useAppSdk"; + +export function Dashboard() { + const appSdk = useAppSdk(); + const [stackInfo, setStackInfo] = useState(null); + + useEffect(() => { + if (appSdk) { + const stack = appSdk.stack; + setStackInfo({ + apiKey: stack.getApiKey(), + name: stack.getName(), + // ... other stack info + }); + } + }, [appSdk]); + + return ( +
+

Stack: {stackInfo?.name}

+

API Key: {stackInfo?.apiKey}

+ {/* Your dashboard widget UI */} +
+ ); +} +``` + +### App Configuration Page + +The App Configuration page allows users to configure app settings. + +```typescript +// containers/AppConfiguration/AppConfiguration.tsx +import { useState, useEffect } from "react"; +import { useAppSdk } from "@/common/hooks/useAppSdk"; + +export function AppConfiguration() { + const appSdk = useAppSdk(); + const [config, setConfig] = useState({ apiKey: "", enabled: false }); + const [saving, setSaving] = useState(false); + + const appConfig = appSdk?.location?.AppConfigWidget; + + useEffect(() => { + if (appConfig) { + // Load existing config + appConfig.getConfig().then(setConfig); + } + }, [appConfig]); + + const saveConfig = async () => { + setSaving(true); + try { + await appConfig.setConfig(config); + // Show success message + } catch (error) { + console.error("Failed to save config:", error); + } finally { + setSaving(false); + } + }; + + return ( +
+

App Configuration

+ + + +
+ ); +} +``` + +**Available Methods:** + +- `getConfig()` - Get app configuration +- `setConfig(config)` - Save app configuration + +--- + +## API Proxy + +Use `appSdk.api()` to make external API calls through Contentstack's secure proxy. This allows you to: + +- Keep API keys secure (stored in Developer Hub Advanced Settings) +- Avoid CORS issues +- Use variable substitution for credentials + +### Basic Usage + +```typescript +// Make API request through Contentstack proxy +const response = await appSdk.api("/external-api/endpoint", { + method: "POST", + headers: { + "x-api-key": "{{var.API_KEY}}", // Variable substitution + "Content-Type": "application/json", + }, + body: JSON.stringify(data), +}); + +const data = await response.json(); +``` + +### Variable Substitution + +Variables are configured in Developer Hub Advanced Settings and automatically substituted: + +```typescript +// In your code +headers: { + "x-api-key": "{{var.API_KEY}}", + "authorization": "Bearer {{var.ACCESS_TOKEN}}", +} + +// Contentstack replaces with actual values from Advanced Settings +``` + +### API Rewrites + +Configure URL rewrites in Developer Hub Advanced Settings: + +- **Pattern**: `/external-api/*` +- **Target**: `https://api.example.com/*` + +This allows you to use relative paths in your code while Contentstack proxies to the actual API. + +### Complete Example + +```typescript +async function callExternalAPI(prompt: string) { + const appSdk = useAppSdk(); + + try { + const response = await appSdk.api( + "/genai/gemini-2.5-flash-image:generateContent", + { + method: "POST", + headers: { + "x-goog-api-key": "{{var.GEMINI_API_KEY}}", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + contents: [ + { + parts: [ + { text: prompt }, + { inline_data: { mime_type: "image/jpeg", data: base64Image } }, + ], + }, + ], + }), + } + ); + + if (!response.ok) { + throw new Error(`API error: ${response.status}`); + } + + return await response.json(); + } catch (error) { + console.error("API call failed:", error); + throw error; + } +} +``` + +--- + +## Frame Management + +Control the iframe dimensions to ensure your app displays correctly: + +```typescript +const appSdk = useAppSdk(); + +// Auto-resize iframe based on content +appSdk.location.frame?.autoResizeFrame(); + +// Set specific height +appSdk.location.frame?.setFrameHeight(400); + +// Enable auto-resize with options +appSdk.location.frame?.enableAutoResizing({ + height: true, + width: false, +}); +``` + +**Best Practice**: Use `autoResizeFrame()` for dynamic content that changes height. + +--- + +## Accessing Configuration + +### Get App Configuration + +```typescript +const appSdk = useAppSdk(); + +// Get configuration (set in App Configuration location) +const config = await appSdk.getConfig(); +console.log(config.apiKey); +console.log(config.enabled); +``` + +### Get Stack Information + +```typescript +const appSdk = useAppSdk(); +const stack = appSdk.stack; + +console.log(stack.getApiKey()); +console.log(stack.getName()); +console.log(stack.getUid()); +``` + +--- + +## Development Workflow + +### 1. Local Development Setup + +```bash +# Clone boilerplate +git clone https://github.com/contentstack/marketplace-app-boilerplate +cd marketplace-app-boilerplate + +# Install dependencies +npm install + +# Start dev server +npm run dev +``` + +Runs on `http://localhost:3000` + +### 2. Configure in Developer Hub + +1. **Create App**: + + - Go to Contentstack Developer Hub + - Click "Create App" + - Fill in app details (name, description, icon) + +2. **Set Base URL**: + + - Set base URL to `http://localhost:3000` (for development) + - Or use tunneling service (ngrok, Cloudflare Tunnel) for HTTPS + +3. **Configure UI Locations**: + + - Add UI locations you want to support + - Set route paths (e.g., `/asset-sidebar`) + +4. **Configure Advanced Settings** (if needed): + - **API Rewrites**: Add rewrite rules for external APIs + - **Variables**: Add variables for API keys (accessed via `{{var.NAME}}`) + +### 3. Install and Test + +1. **Install App**: + + - Go to your test stack + - Navigate to Apps + - Install your app + +2. **Test Locations**: + - Open Asset Library → View asset → Check sidebar + - Edit entry → Check sidebar + - Go to Dashboard → Check widget + - Configure app settings → Check config page + +### 4. Building for Production + +```bash +npm run build +``` + +Outputs to `dist/` directory. + +### 5. Deploy + +1. Deploy `dist/` folder to hosting provider (Vercel, Netlify, AWS, etc.) +2. Update app configuration in Developer Hub with production URL +3. Update manifest settings if needed + +--- + +## Error Handling + +Always handle errors gracefully: + +```typescript +try { + const data = await assetSidebar.getData(); + setAsset(data); +} catch (error) { + console.error("Failed to get asset:", error); + // Show user-friendly error message + setError("Unable to load asset. Please try again."); +} +``` + +### Common Error Scenarios + +- **SDK not initialized**: Ensure `MarketplaceAppProvider` wraps your app +- **Location not available**: Check if location is configured in Developer Hub +- **API call failed**: Verify API rewrites and variables are configured +- **Permission denied**: Check app permissions in Developer Hub + +--- + +## Best Practices + +| Do | Don't | +| -------------------------------- | ---------------------------------- | +| Initialize SDK once at root | Re-initialize on every render | +| Handle loading states | Show blank screens | +| Handle errors gracefully | Let errors crash app | +| Use TypeScript | Skip type safety | +| Test in Contentstack iframe | Only test standalone | +| Use config for secrets | Hardcode API keys | +| Use API proxy for external calls | Make direct API calls from browser | +| Auto-resize iframe | Use fixed heights | +| Validate user input | Trust all input | +| Provide user feedback | Silent failures | + +--- + +## Testing + +### Unit Tests + +```typescript +// __tests__/AssetSidebar.test.tsx +import { render, screen } from "@testing-library/react"; +import { AssetSidebar } from "../AssetSidebar"; + +// Mock SDK +jest.mock("@/common/hooks/useAppSdk", () => ({ + useAppSdk: () => ({ + location: { + AssetSidebarWidget: { + getData: jest.fn().mockResolvedValue({ title: "Test Asset" }), + }, + }, + }), +})); + +test("renders asset title", async () => { + render(); + expect(await screen.findByText("Test Asset")).toBeInTheDocument(); +}); +``` + +### E2E Tests (Playwright) + +```typescript +// e2e/asset-sidebar.spec.ts +import { test, expect } from "@playwright/test"; + +test("asset sidebar loads", async ({ page }) => { + await page.goto("http://localhost:3000/asset-sidebar"); + await expect(page.locator("h2")).toContainText("Asset:"); +}); +``` + +--- + +## Troubleshooting + +### App Not Loading + +1. Check browser console for errors +2. Verify base URL is correct in Developer Hub +3. Check if app is installed on the stack +4. Verify HTTPS if using production URL + +### SDK Not Initializing + +1. Check if `MarketplaceAppProvider` wraps your app +2. Verify authentication token in URL +3. Check browser console for SDK errors +4. Ensure app is properly installed + +### Location Not Available + +1. Verify location is configured in Developer Hub manifest +2. Check route path matches configuration +3. Ensure location is enabled for the app + +### API Calls Failing + +1. Verify API rewrites are configured in Advanced Settings +2. Check variables are set correctly (`{{var.NAME}}`) +3. Verify rewrite patterns match your API paths +4. Check network tab for actual request URLs + +--- + +## Resources + +- [Contentstack Developer Hub Documentation](https://www.contentstack.com/docs/developers/developer-hub) +- [Contentstack App SDK Reference](https://www.contentstack.com/docs/developers/developer-hub/contentstack-app-development) +- [UI Locations Reference](https://www.contentstack.com/docs/developers/developer-hub/managing-ui-locations) +- [Marketplace App Boilerplate (GitHub)](https://github.com/contentstack/marketplace-app-boilerplate) +- [Marketplace App Boilerplate Documentation](https://www.contentstack.com/docs/developers/developer-hub/marketplace-app-boilerplate) \ No newline at end of file diff --git a/codex/dx-delivery-sdk/references/delivery-sdk-spec.md b/codex/dx-delivery-sdk/references/delivery-sdk-spec.md new file mode 100644 index 0000000..1ea01a3 --- /dev/null +++ b/codex/dx-delivery-sdk/references/delivery-sdk-spec.md @@ -0,0 +1,1157 @@ +--- +title: "TypeScript Delivery SDK API Reference" +description: "Reference guide for Contentstack's TypeScript Delivery SDK: Explore features and functions for seamless content delivery in your projects" +url: "https://www.contentstack.com/docs/developers/sdks/content-delivery-sdk/typescript/reference.md" +product: "Contentstack" +doc_type: "guide" +audience: + - developers + - admins +version: "current" +last_updated: "2026-02-27" +--- + +# TypeScript Delivery SDK API Reference + +## TypeScript Delivery SDK API Reference + +## Overview + +Contentstack offers the TypeScript Delivery SDK for building applications. Below, is an in-depth guide and valuable resources to initiate your journey with our TypeScript Delivery SDK. Additionally, the SDK supports the creating applications for Node.js and React Native environments. + +**Additional Resource**: To know more about the TypeScript Delivery SDK, refer to the [About TypeScript Delivery SDK](https://www.contentstack.com/docs/developers/sdks/content-delivery-sdk/typescript/about-typescript-delivery-sdk.md) and [Get Started with TypeScript Delivery SDK](https://www.contentstack.com/docs/developers/sdks/content-delivery-sdk/typescript/get-started-with-typescript-delivery-sdk.md) documentation. + +## Contentstack + +The Contentstack module contains the instance of a stack. To import Contentstack, refer to the code below: + +import contentstack from '@contentstack/delivery-sdk'; + +## Stack + +A [stack](https://www.contentstack.com/docs/developers/set-up-stack/about-stack.md) is a repository or a container that holds all the [entries](https://www.contentstack.com/docs/content-managers/author-content/about-entries.md)/[assets](https://www.contentstack.com/docs/content-managers/working-with-assets/about-assets.md) of your site. It allows multiple users to [create](https://www.contentstack.com/docs/content-managers/working-with-entries/create-an-entry.md), [edit](https://www.contentstack.com/docs/content-managers/working-with-entries/edit-an-entry.md), [approve](https://www.contentstack.com/docs/content-managers/use-workflows/send-an-entry-for-publish-or-unpublish-approval.md), and [publish](https://www.contentstack.com/docs/content-managers/publish-content.md) their content within a single space. + +The stack function initializes an instance of the Stack. To initialize a stack execute the following code: + +import contentstack from '@contentstack/delivery-sdk' +const stack = contentstack.stack({ apiKey: "apiKey", deliveryToken: "deliveryToken", environment: "environment" }); + +## LivePreviewConfig + +Configuration settings to enable live preview functionality and fetch real-time content data. + +Specifies whether to enable the live preview feature. + +Specifies the host domain used to retrieve live preview content. + +Token required to fetch live preview content from the stack. + +## Plugins + +When creating custom plugins, through this request, you can pass the details of your custom plugins. This facilitates their utilization in subsequent requests when retrieving details. + +To initializing a stack with plugins, refer to the code snippet below: + +// custom class for plugin +class CrossStackPlugin { + onRequest (request) { + // add request modifications + + return request + +} + async onResponse (request, response, data) { + // add response modifications here + + return response + +} +} +const Stack = Contentstack.stack({ + api_key, + delivery_token, + environment, + plugins: \[ + new CrossStackPlugin(), + \] +}); + +## Asset + +The Asset method by default creates an object for all assets of a stack. To retrieve a single asset, specify its UID. + +UID of the asset + +## ContentType + +The ContentType method retrieves all the content types of a stack. To retrieve a single contenttype, specify its UID. + +UID of the content type + +## setLocale + +The setLocale method sets the locale of the API server. + +Enter the locale code + +## sync + +The sync method syncs your Contentstack data with your app and ensures that the data is always up-to-date by providing delta updates. + +An object that supports ‘locale’, ‘start_date’, ‘content_type_uid’, and ‘type’ queries + +Specifies if the sync should be recursive + +API key of the stack + +Delivery token to retrieve data from the stack + +Environment name where content is published + +The Live preview configuration for the Contentstack API + +Name of the branch to fetch data from + +Sets the host of the API server +(example: "dev.contentstack.com") + +Region of the stack. You can choose from five regions: NA, EU, Azure NA, Azure EU, GCP NA, and GCP EU. + +Lets you specify which language to use as source content if the entry does not exist in the specified language. + +Specifies the caching strategy. Accepts a string value from the Policy enum. + +Defines where the cache is stored. Accepts localStorage or memoryStorage as string values. + +Sets the maximum age (in milliseconds) before the cache expires. + +Function to serialize data before storing it in the cache. + +Function to deserialize data when retrieving it from the cache. + +Set early access headers + +Method to enable custom logging in the SDK + +Add custom plugins to the SDK + +## Asset + +In Contentstack, any files (images, videos, PDFs, audio files, and so on) that you upload get stored in your repository for future use. This repository of uploaded files is called [assets](https://www.contentstack.com/docs/content-managers/author-content/about-assets.md). + +The Asset method by default creates an object for all assets of a stack. To retrieve a single asset, specify its UID. + +**Example:** + +import contentstack from '@contentstack/delivery-sdk'; + +import { BaseAsset } from '@contentstack/delivery-sdk'; + +const stack = contentstack.stack({ apiKey: "apiKey", deliveryToken: "deliveryToken", environment: "environment" }); + +interface BlogAsset extends BaseAsset { + + title: string; + + description: string; + + url: string; + + // Add other custom properties as needed + +} + +async function fetchAssets() { + + try { + + const result = await stack.asset(asset\_uid).fetch(); + + console.log('Assets Fetched:', assets); + +//Add your statements + + } catch (error) { + + console.error('Error fetching asset:', error); + + } + +} + +fetchAssets(); + +## fetch + +The fetch method retrieves the asset data of the specified asset. + +## includeBranch + +The includeBranch method includes the branch details in the response. + +## includeDimension + +The includeDimension method includes the dimensions (height and width) of the image in the result. + +## includeFallback + +The includeFallback method retrieves the entry in its fallback language. + +## locale + +The locale method retrieves the assets published in that locale. + +## relativeUrls + +The relativeUrls method includes the relative URLs of the asset in the result. + +## version + +The version method retrieves the specified version of the asset in the result. + +Version of the required asset + +## includeMetadata + +The includeMetadata method includes the metadata for getting metadata content for the entry. + +## assetFields + +The assetFields method determines the optional asset field groups to include in the response. + +**Note:** The assetFields method is supported only in the North America (NA) region. + +**Response Behavior:** + +- Retrieves only the requested asset metadata, keeping asset payloads smaller. +- Applies to published assets from the asset API. It applies to both single-asset responses and multi-asset responses. +- Does not filter which assets appear in the response or restrict them by file type (MIME). +- Requests optional metadata groups in addition to the core fields on BaseAsset, such as the file MIME type (content_type) and the folder flag (is_dir). It does not replace or modify those fields. + +**Note:** On asset objects, content_type represents the file’s MIME type, not a Contentstack CMS content type. + +**Supported field groups (values):** + +- user_defined_fields: Includes stack-defined custom fields on the asset (author-managed key-value data). +- embedded_metadata: Includes metadata extracted from the file (e.g, EXIF or IPTC). +- ai_generated_metadata: Includes AI-generated data (e.g., tags, descriptions, classifications). +- visual_markups: Includes annotation data (e.g., regions, notes, overlays) + +Keys that specify asset field groups to retrieve. Provide them as arguments before .fetch() or .find(). + +UID of the asset + +## Asset Collection + +The Asset Collection provides methods for filtering and retrieving assets stored in Contentstack. You can retrieve specific assets by UID, tags, or metadata. + +**Example:** + +const result = stack.asset().find() + +.then((assets) => console.log(assets)) + +.catch((error) => console.error("Error fetching assets:", error)); + +## addParams + +The addParam method adds a query parameter to the query. + +Add key-value pairs + +## find + +The find method retrieves all the assets of the stack. + +## includeBranch + +The includeBranch method includes the branch details in the result. + +## includeCount + +The includeCount method retrieves count and data of all the objects in the result. + +## includeDimension + +The includeDimension method includes the dimensions (height and width) of the image in the result + +## includeFallback + +The includeFallback method retrieves the entry in its fallback language. + +## locale + +The locale method retrieves the asset published in the specified locale. + +Locale of the asset + +## orderByAscending + +The orderByAscending method sorts the results in ascending order based on the specified field UID. + +Field UID to sort the results + +## orderByDescending + +The orderByDescending method sorts the results in descending order based on the specified key. + +Field UID to sort the results + +## param + +The param method adds query parameters to the URL. + +Add any param to include in the response + +Add the corresponding value of the param key + +## relativeUrls + +The relativeUrls method includes the relative URLs of all the assets in the result. + +## removeParam + +The removeParam method removes a query parameter from the query. + +Specify the param key you want to remove + +## version + +The version method retrieves a specific version of the asset in the result. + +Version number of the asset + +## where + +The where method filters the results based on the specified criteria. + +Specify the field the comparison is made from + +Specify the comparison criteria + +Specify the field the comparison is made to + +## includeMetadata + +The includeMetadata method includes the metadata for getting metadata content for the entry. + +## skip + +The skip method will skip a specific number of assets in the output. + +Enter the number of assets to be skipped. + +## limit + +The limit method will return a specific number of assets in the output. + +Enter the maximum number of assets to be returned. + +## ContentType + +A [content type](https://www.contentstack.com/docs/developers/create-content-types/about-content-types.md) is the structure or blueprint of a page or a section that your web or mobile property will display. It lets you define the overall schema of this blueprint by adding fields and setting its properties. + +**Example:** + +import { BaseContentType } from '@contentstack/delivery-sdk' + +interface BlogPost extends BaseContentType { + +text: string; + +// other custom props + +} + +async function fetchContentType() { + +try { + +const contentType = await stack.contentType("blog").fetch(); + +console.log(contentType); + +//Add your statements + + } catch (error) { + +console.error("Error fetching content type:", error); + +} + +} + +fetchContentType(); + +## entry + +The entry method creates an entry object for the specified entry. + +UID of the entry + +## fetch + +The fetch method retrieves the details for the specified content type. + +UID of the content type + +## ContentType Collection + +The ContentType Collection method retrieves a list of all content types available within a stack. It provides metadata and structural details for each content type but does not retrieve actual content entries. + +**Example:** + +const contentType = await stack.contentType().find(); + +## find + +The find method retrieves all the content types of the stack. + +## includeGlobalFieldSchema + +The includeGlobalFieldSchema method includes the schema of the global field in the response. + +## Entry + +An [Entry](https://www.contentstack.com/docs/content-managers/author-content/about-entries.md) is the actual piece of content created using one of the defined content types. To work with a single entry, specify its UID. + +**Example:** + +import contentstack from '@contentstack/delivery-sdk' + +import { BaseEntry } from '@contentstack/delivery-sdk' + +const stack = contentstack.stack({ apiKey: "apiKey", deliveryToken: "deliveryToken", environment: "environment" }); + +interface BlogPostEntry extends BaseEntry { + +// custom entry types + +} + +async function fetchEntry() { + + try { + +const result = await stack.contentType(contenttype_uid).entry(entry_uid).fetch(); + + console.log('Entry: ', result); + +//Add your statements + + } catch (error) { + + console.error('Error fetching entry:', error); + +} + +} + +fetchEntry(); + +## fetch + +The fetch method retrieves the details of a specific entry. + +## includeBranch + +The includeBranch method includes the branch details in the result. + +## includeFallback + +The includeFallback method retrieves the entry in its fallback language. + +## locale + +The locale method retrieves the entries published in that locale. + +Locale of the entry + +## addParams + +The addParam method adds a query parameter to the query. + +Add key-value pairs + +## except + +The except method excludes specific field(s) of an entry. + +UID of the field to exclude + +## find + +The find method retrieves the details of the specified entry. + +## skip + +The skip method will skip a specific number of entries in the output. + +Enter the number of entries to be skipped. + +## limit + +The limit method will return a specific number of entries in the output. + +Enter the maximum number of entries to be returned. + +## includeCount + +The includeCount method retrieves the count and data of objects in the result. + +## only + +The only method selects specific field(s) of an entry. + +UID of the field to select + +## orderByAscending + +The orderByAscending sorts the results in ascending order based on the specified field UID. + +Field UID to sort the results + +## orderByDescending + +The orderByDescending sorts the results in descending order based on the specified field UID. + +Field UID to sort the results + +## param + +The param method adds query parameters to the URL. + +Add any param to include in the response + +Add the corresponding value of the param key + +## query + +The query method retrieves the details of the entry on the basis of the queries applied. + +Query in object format + +## removeParam + +The removeParam method removes a query parameter from the query. + +Specify the param key you want to remove + +## where + +The where method filters the results based on the specified criteria. + +Specify the field the comparison is made from + +Specify the comparison criteria + +Specify the field the comparison is made to + +## includeMetadata + +The includeMetadata method includes the metadata for getting metadata content for the entry. + +## includeEmbeddedItems + +The includeEmbeddedItems method includes embedded objects (Entry and Assets) along with entry details + +## includeContentType + +The includeContentType method includes the details of the content type along with the Entry details. + +## includeReference + +The includeReference method retrieves the content of the referred entries in your response. + +UID of the reference field to include + +## Variants + +Variants are different versions of content designed to meet specific needs or target audiences. This feature allows content editors to create multiple variations of a single entry, each customized for a particular variant group or purpose. + +When Personalize creates a variant in the CMS, it assigns a "Variant Alias" to identify that specific variant. When fetching entry variants using the Delivery API, you can pass variant aliases in place of variant UIDs in the x-cs-variant-uid header. + +## assetFields + +The assetFields method specifies the optional asset field groups to include for assets returned with an entry. + +**Note:** The assetFields method is supported only in the North America (NA) region. + +**Response Behavior:** + +- Retrieves only the required asset metadata, keeping the entry payloads smaller when entries reference assets. +- Applies to published assets referenced or embedded on the entry. It applies to both single-entry and multi-entry responses. +- Adds optional metadata fields to each asset in the response. It does not filter assets or control file types. +- Requests optional metadata groups in addition to standard asset fields such as file MIME type (content_type) and folder flag (is_dir). It does not replace or modify core asset fields. + +**Note:** On asset objects, content_type represents the file’s MIME type, not a Contentstack CMS content type. + +**Supported field groups (values):** + +- user_defined_fields: Includes stack-defined custom fields on the asset (author-managed key-value data). +- embedded_metadata: Includes metadata extracted from the file (e.g, EXIF or IPTC). +- ai_generated_metadata: Includes AI-generated data (e.g., tags, descriptions, classifications). +- visual_markups: Includes annotation data (e.g., regions, notes, overlays) + +Keys that specify asset field groups to retrieve for assets in the entry response. Provide them as arguments before .fetch() or .find(). + +UID of the entry + +## Query + +These methods allow you to refine entry queries by applying conditions, filters, and relational data. You can filter entries based on specific field values, include referenced entries, and limit the number of results. + +**Example:** + +const query = stack.contentType("contentTypeUid").Entry().query(); + +## addParams + +The addParam method adds a query parameter to the query. + +Add key-value pairs + +## addQuery + +The addQuery method adds multiple query parameters to the query. + +Add filter query key + +Add the corresponding value to the filter query key + +## find + +The find method retrieves the details of the specified entry. + +## includeCount + +The includeCount method retrieves count and data of objects in the result. + +## orderByAscending + +The orderByAscending method sorts the results in ascending order based on the specified field UID. + +Field UID to sort the results + +## orderByDescending + +The orderByDescending method sorts the results in descending order based on the specified key. + +Field UID to sort the results + +## param + +The param method adds query parameters to the URL. + +Add any param to include in the response + +Add the corresponding value of the param key + +## queryOperator + +The queryOperator method retrieves the entries as per the given operator. + +Type of query operator to apply + +Query instances to apply the query to + +## removeParam + +The removeParam method removes a query parameter from the query. + +Specify the param key you want to remove + +## where + +The where method filters the results based on the specified criteria. + +Specify the field the comparison is made from + +Specify the comparison criteria + +Specify the field the comparison is made to + +## whereIn + +The whereIn method retrieves the entries that meet the query conditions made on referenced fields. + +UID of the reference field to query + +Query instance to include in the where clause + +## whereNotIn + +The whereNotIn method retrieves the entries that do not meet the query conditions made on referenced fields. + +UID of the reference field to query + +Query instance to include in the where clause + +## skip + +The skip method will skip a specific number of entries in the output. + +Enter the number of entries to be skipped. + +## limit + +The limit method will return a specific number of entries in the output. + +Enter the maximum number of entries to be returned. + +## or + +The or method retrieves the entries that meet either of the conditions specified. + +Array of query objects or raw queries + +## and + +The and method retrieves the entries that meet all the specified conditions. + +Array of query objects or raw queries + +## containedIn + +The containedIn method retrieves the entries that contain the conditions specified. + +UID of the field + +Array of values that are to be used to match or compare + +## notContainedIn + +The notContainedIn method retrieves the entries where the specified conditions are absent. + +UID of the field + +Array of values that are to be used to match or compare + +## equalTo + +The equalTo method retrieves entries that match the specified conditions exactly. + +UID of the field + +Array of values that are to be used to match or compare + +## exists + +The exists method retrieves the entries that satisfy the specified condition of existence. + +UID of the field + +## notExists + +The notExists method retrieves entries where the specified conditions are not met. + +UID of the field + +## getQuery + +The getQuery method retrieves the entries as per the specified query. + +UID of the field + +## greaterThan + +The greaterThan method retrieves the entries that are greater than the specified condition. + +UID of the field + +Array of values that are to be used to match or compare + +## greaterThanOrEqualTo + +The greaterThanOrEqualTo method retrieves entries that meet the specified condition of being greater than or equal to a certain value. + +UID of the field + +Array of values that are to be used to match or compare + +## lessThan + +The lessThan method retrieves the entries that are less than the specified condition. + +UID of the field + +Array of values that are to be used to match or compare + +## lessThanOrEqualTo + +The lessThanOrEqualTo method retrieves entries that meet the specified condition of being less than or equal to a certain value. + +UID of the field + +Array of values that are to be used to match or compare + +## referenceIn + +The referenceIn method retrieves the entries that are referenced. + +UID of the reference field + +RAW (JSON) queries + +## referenceNotIn + +The referenceNotIn method retrieves the entries where the referenced items are not included. + +UID of the reference field + +RAW (JSON) queries + +## regex + +The regex method retrieves entries that match a specified regular expression pattern. + +UID of the field + +Array of values that are to be used to match or compare + +Match or compare value in entry + +## search + +The search method retrieves the entries that match the specified search criteria. + +UID of the field + +## tags + +The tags method fetches the entries that are associated with specific tags. + +Array of tags + +## Taxonomy + +[Taxonomy](https://www.contentstack.com/docs/developers/taxonomy/about-taxonomy.md) helps you categorize pieces of content within your stack to facilitate easy navigation, search, and retrieval of information. + +**Note**: All methods in the Query section are applicable for taxonomy-based filtering as well. + +## equalAndBelow + +The equalAndBelow operation retrieves all entries for a specific taxonomy that match a specific term and all its descendant terms, requiring only the target term. + +Enter the UID of the taxonomy + +Enter the UID of the term + +Enter the level + +## below + +The below operation retrieves all entries for a specific taxonomy that match all of their descendant terms by specifying only the target term and a specific level. + +**Note:** If you don't specify the level, the default behavior is to retrieve terms up to level **10**. + +Enter the UID of the taxonomy + +Enter the UID of the term + +Enter the level + +## equalAndAbove + +The equalAndAbove operation retrieves all entries for a specific taxonomy that match a specific term and all its ancestor terms, requiring only the target term and a specified level + +**Note:** If you don't specify the level, the default behavior is to retrieve terms up to level **10**. + +Enter the UID of the taxonomy + +Enter the UID of the term + +Enter the level + +## above + +The equalAndAbove operation retrieves all entries for a specific The above operation retrieves all entries for a specific taxonomy that match only the parent terms of a specified target term, excluding the target term itself and a specified level. + +**Note:** If you don't specify the level, the default behavior is to retrieve terms up to level **10**. + +Enter the UID of the taxonomy + +Enter the UID of the term + +Enter the level + +## fetch + +The fetch method retrieves taxonomy data for the specified taxonomy UID. + +Specifies the UID of the taxonomy to fetch. + +## find + +The find method retrieves all published taxonomies in the stack. + +- Returns a response object containing: + - taxonomies: Array of taxonomy objects. + - count: Optional total number of taxonomies. +- Supports locale and query parameters when configured. + +## Terms + +Terms serve as the primary classification elements within a taxonomy, allowing you to establish hierarchical structures and incorporate them into entries. + +Use the Terms to fetch individual terms, list terms within a taxonomy, and traverse term hierarchies such as ancestors and descendants. + +**Note:** The Delivery SDK uses .term() (singular) to access term-level operations, whereas the JavaScript Management SDK uses .terms() (plural) for term-related methods. + +**Terms Methods Overview** + +Use the following methods to retrieve term data within a taxonomy: + +- **fetch**: Retrieves a single term by UID. +- **find**: Retrieves all terms within a specific taxonomy. +- **locales**: Retrieves all available localized versions of a single term. +- **ancestors**: Retrieves all ancestor terms of a single term, up to the root. +- **descendants**: Retrieves all descendant terms of a single term. + +import contentstack, { BaseTerm } from '@contentstack/delivery-sdk' + +interface BlogPostTerm extends BaseTerm { +// custom term types +} + +const stack = contentstack.stack({ apiKey: "apiKey", deliveryToken: "deliveryToken", environment: "environment" }); + +const data = await stack.taxonomy("taxonomy_uid").term("term_uid").fetch(); + +Specifies the term's UID to retrieve. + +## fetch + +Fetches the details of a single published term within a taxonomy. + +Only published terms are returned. This is enforced by the Delivery API and the delivery token used during stack initialization. + +If a locale is specified during stack initialization, it is applied automatically to this request. + +## find + +Fetches a list of all published terms within a specific taxonomy + +## locales + +Fetches the specified term across all locales configured in the stack. + +## ancestors + +Fetches all ancestors of a single published term, up to the root. + +## descendants + +Fetches all descendants of a single published term. + +## Global Fields + +A [Global field](https://www.contentstack.com/docs/developers/global-field/about-global-field.md) is a reusable field (or group of fields) that you can define once and reuse in any content type within your stack. This eliminates the need (and thereby time and efforts) to create the same set of fields repeatedly in multiple content types. + +**Example:** + +const globalField = stack.globalField('global_field_uid'); // For a single globalField with uid 'global_field_uid' + +## find + +The find method retrieves all the global fields of the stack. + +## fetch + +The fetch method retrieves the global field data of the specified global field. + +## includeBranch + +The includeBranch method includes the branch details in the result for single or multiple global fields. + +UID of the Global field + +## Pagination + +In a single instance, a query will retrieve only the first 100 items in the response. You can paginate and retrieve the rest of the items in batches using the skip and limit parameters in subsequent requests. + +**Example:** + +const query = stack.contentType("contentTypeUid").entry().query(); +const pagedResult = await query + .paginate() + .find(); +// OR +const pagedResult = await query + .paginate({ skip: 20, limit: 20 }) + .find(); + +## next + +The next method retrieves the next set of response values and skips the current number of responses. + +## previous + +The previous method retrieves the previous set of response values and skips the current number of responses. + +## ImageTransform + +Image transformations can be performed on images by specifying the desired parameters. The parameters control the specific transformations that will be applied to the image. + +**Example:** + +const url = 'www.example.com'; +const transformObj = new ImageTransform().bgColor('cccccc'); + +const transformURL = url.transform(transformObj); + +## auto + +The auto method enables the functionality that automates certain image optimization features. + +## bgColor + +The bgColor method sets a background color for the given image. + +Color of the background + +## blur + +The blur method allows you to decrease the focus and clarity of a given image. + +Set the blur intensity between 1 to 1000 + +## brightness + +The brightness method enables the functionality that automates certain image optimization features. + +Set the brightness of the image between -100 to 100 + +## canvas + +The canvas method allows you to increase the size of the canvas that surrounds an image. + +Specifies what params to use for creating canvas - DEFAULT, ASPECTRATIO, REGION, OFFSET + +Sets height of the canvas + +Sets width of the canvas + +Defines the X-axis position of the top left corner or horizontal offset + +Defines the Y-axis position of the top left corner or vertical offset + +## contrast + +The contrast method enables the functionality that automates certain image optimization features. + +Set the contrast of the image between -100 to 100 + +## crop + +The crop method allows you to remove pixels from an image by adjusting the height and width in the percentage value or aspect ratio. + +Specify the CropBy type using values DEFAULT, ASPECTRATIO, REGION, or OFFSET. + +Specify the width to resize the image to. + +The value can be in pixels (for example, 400) or in percentage (for example, 0.60 OR '60p') + +Specify the height to resize the image to. The value can be in pixels (for example, 400) or in percentage (for example, 0.60 OR '60p') + +For the CropBy Region, specify the X-axis position of the top left corner of the crop. For CropBy Offset, specify the horizontal offset of the crop region. + +For CropBy Region, specify the Y-axis position of the top left corner of the crop. For CropBy Offset, specify the vertical offset of the crop region. + +Ensures that the output image never returns an error due to the specified crop area being out of bounds. The output image is returned as an intersection of the source image and the defined crop area. + +Ensures crop is done using content-aware algorithms. Content-aware image cropping returns a cropped image that automatically fits the defined dimensions while intelligently including the most important components of the image. + +## dpr + +The dpr method lets you deliver images with appropriate size to devices that come with a defined device pixel ratio. + +Specify the device pixel ratio. The value should range between 1-10000 or 0.0 to 9999.999 + +## fit + +The fit method enables you to fit the given image properly within the specified height and width. + +Specifies fit type (Bounds or Crop) + +## format + +The format method lets you convert a given image from one format to another. + +Specify the format + +## frame + +The frame method retrieves the first frame from an animated GIF (Graphics Interchange Format) file that comprises a sequence of moving images. + +## orient + +The orient method allows you to rotate or flip an image in any direction. + +Type of Orientation. Values are DEFAULT, FLIP_HORIZONTAL, FLIP_HORIZONTAL_VERTICAL, FLIP_VERTICAL, FLIP_HORIZONTAL_LEFT, RIGHT, FLIP_HORIZONTAL_RIGHT, LEFT. + +## overlay + +The overlay method lets you place one image over another by specifying the relative URL of the image. + +URL of the image to overlay on base image + +Lets you define the position of the overlay image. Accepted values are TOP, BOTTOM, LEFT, RIGHT, MIDDLE, CENTER + +Lets you define how the overlay image will be repeated on the given image. Accepted values are X, Y, BOTH + +Lets you define the width of the overlay image. For pixels, use any whole number between 1 and 8192. For percentages, use any decimal number between 0.0 and 0.99 + +Lets you define the height of the overlay image. For pixels, use any whole number between 1 and 8192. For percentages, use any decimal number between 0.0 and 0.99 + +Lets you add extra pixels to the edges of an image. This is useful if you want to add whitespace or border to an image + +## padding + +The padding method lets you add extra pixels to the edges of an image's border or add whitespace. + +padding value in pixels or percentages + +## quality + +The quality method lets you control the compression level of images that have lossy file format. + +Quality range: 1 - 100 + +## resize + +The resize method lets you resize the image in terms of width, height, upscaling the image. + +Specifies the width to resize the image to. The value can be in pixels (for example, 400) or in percentage (for example, 0.60 OR '60p') + +Specifies the height to resize the image to.The value can be in pixels (for example, 400) or in percentage (for example, 0.60 OR '60p') + +The disable parameter disables the functionality that is enabled by default. As of now, there is only one value, i.e., upscale, that you can use with the disable parameter. + +## resizeFilter + +The resizeFilter method allows you to increase or decrease the number of pixels in a given image. + +Types of Filter to apply. Values are NEAREST, BILINEAR, BICUBIC, LANCZOS2, LANCZOS3. + +## saturation + +The saturation method allows you to increase or decrease the intensity of the colors in a given image. + +To set the saturation of image between -100 to 100 + +## sharpen + +The sharpen method allows you to increase the definition of the edges of objects in an image. + +Specifies the amount of contrast to be set for the image edges between the range \[0-10\] + +Specifies the radius of the image edges between the range \[1-1000\] + +Specifies the range of image edges that need to be ignored while sharpening between the range \[0-255\] + +## trim + +The trim method lets you trim an image from the edges. + +Specifies values for top, right, bottom, and left edges of an image. diff --git a/codex/migration-companion/SKILL.md b/codex/migration-companion/SKILL.md new file mode 100644 index 0000000..d3f4e15 --- /dev/null +++ b/codex/migration-companion/SKILL.md @@ -0,0 +1,947 @@ +# 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 `/.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=""`) 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. +- **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=":$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 ``." + +### 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=":$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 + ``, 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 | ✅ `` | | +| 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 +# Use the Node the checker validated (NODE_BIN_DIR from the prereq JSON). +export PATH=":$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=":$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:`** → 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 --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: /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/codex/migration-companion/references/CONTENTFUL_TO_CONTENTSTACK_MIGRATION_CONTEXT.md b/codex/migration-companion/references/CONTENTFUL_TO_CONTENTSTACK_MIGRATION_CONTEXT.md new file mode 100644 index 0000000..d19ab85 --- /dev/null +++ b/codex/migration-companion/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/codex/migration-companion/scripts/01_contentful_residue.sh b/codex/migration-companion/scripts/01_contentful_residue.sh new file mode 100755 index 0000000..b62f5f7 --- /dev/null +++ b/codex/migration-companion/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/codex/migration-companion/scripts/02_field_access.sh b/codex/migration-companion/scripts/02_field_access.sh new file mode 100755 index 0000000..eabf8b4 --- /dev/null +++ b/codex/migration-companion/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/codex/migration-companion/scripts/03_sdk_init.sh b/codex/migration-companion/scripts/03_sdk_init.sh new file mode 100755 index 0000000..0f92311 --- /dev/null +++ b/codex/migration-companion/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/codex/migration-companion/scripts/04_query_builder.sh b/codex/migration-companion/scripts/04_query_builder.sh new file mode 100755 index 0000000..a3ee90f --- /dev/null +++ b/codex/migration-companion/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/codex/migration-companion/scripts/05_references.sh b/codex/migration-companion/scripts/05_references.sh new file mode 100755 index 0000000..ca287c3 --- /dev/null +++ b/codex/migration-companion/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/codex/migration-companion/scripts/06_richtext.sh b/codex/migration-companion/scripts/06_richtext.sh new file mode 100755 index 0000000..8789c45 --- /dev/null +++ b/codex/migration-companion/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/codex/migration-companion/scripts/07_assets.sh b/codex/migration-companion/scripts/07_assets.sh new file mode 100755 index 0000000..36b5a6b --- /dev/null +++ b/codex/migration-companion/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/codex/migration-companion/scripts/08_locales.sh b/codex/migration-companion/scripts/08_locales.sh new file mode 100755 index 0000000..cf2958e --- /dev/null +++ b/codex/migration-companion/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/codex/migration-companion/scripts/09_graphql.sh b/codex/migration-companion/scripts/09_graphql.sh new file mode 100755 index 0000000..d9ad191 --- /dev/null +++ b/codex/migration-companion/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/codex/migration-companion/scripts/10_livepreview.sh b/codex/migration-companion/scripts/10_livepreview.sh new file mode 100755 index 0000000..7bc4a2f --- /dev/null +++ b/codex/migration-companion/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/codex/migration-companion/scripts/11_build_typecheck.sh b/codex/migration-companion/scripts/11_build_typecheck.sh new file mode 100755 index 0000000..c43bca7 --- /dev/null +++ b/codex/migration-companion/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/codex/migration-companion/scripts/12_secrets.sh b/codex/migration-companion/scripts/12_secrets.sh new file mode 100755 index 0000000..401ca50 --- /dev/null +++ b/codex/migration-companion/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/codex/migration-companion/scripts/13_todos_report.sh b/codex/migration-companion/scripts/13_todos_report.sh new file mode 100755 index 0000000..ca7c18e --- /dev/null +++ b/codex/migration-companion/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/codex/migration-companion/scripts/README.md b/codex/migration-companion/scripts/README.md new file mode 100644 index 0000000..ece6a48 --- /dev/null +++ b/codex/migration-companion/scripts/README.md @@ -0,0 +1,92 @@ +# Migration evals — Contentful → Contentstack + +Independent, parallel-runnable checks that confirm a migration was done correctly. Each script +targets one class of mistake from the migration mapping (`reference/CONTENTFUL_TO_CONTENTSTACK_MIGRATION_CONTEXT.md`) +and is **runnable on its own**, so an AI can fan them out (one agent per eval) or use the runner. + +## Run + +```bash +# all evals, in parallel, against the migrated app +bash run-all.sh /path/to/migrated-app + +# a subset (by number prefix) +bash run-all.sh /path/to/app 01 02 04 06 + +# skip the slow build eval (e.g. before deps are installed) +SKIP_BUILD=1 bash run-all.sh /path/to/app + +# one eval directly +bash 02_field_access.sh /path/to/app +``` + +Install deps in the target first (`npm ci` / `pnpm i`) so eval 11 (build/typecheck) is meaningful. +Uses `ripgrep` if present, else falls back to `grep`. + +## The evals + +| # | eval | Gate | Catches (doc ref) | +|---|------|------|-------| +| 01 | `residue` | **HARD** | Any leftover Contentful import/host/dep; missing Contentstack dep (§1, gotcha 1) | +| 02 | `field-access` | **HARD** | `.fields.` / `.sys.*` / `.items` / `.total` not flattened (§6, gotchas 1-2,7) | +| 03 | `sdk-init` | **HARD** | `stack()` missing required `environment`/`apiKey`/`deliveryToken`; old keys; hardcoded creds (§3, gotcha 6) | +| 04 | `query-builder` | review | Contentful `[gte]`/`content_type:`/`include:N`/`order:`/`select:`; `.only`/`.except` after `.query()` (§4-5, gotchas 5,8) | +| 05 | `references` | review | Link stubs; references dereferenced as objects not arrays (§7, gotchas 3-4) | +| 06 | `richtext` | review | `@contentful/rich-text`; `path:` vs `paths:`; missing `includeEmbeddedItems()`/HTML sink (§9, gotchas 9,15,16) | +| 07 | `assets` | review | `fields.file.url`; protocol-relative fix; Contentful image params; `ImageTransform` root import (§10, gotchas 11,12,17) | +| 08 | `locales` | review | Uppercase locale codes; `locale:'*'`; query-param locale (§11, gotcha 10) | +| 09 | `graphql` | review* | Contentful GraphQL endpoint/auth/`xxxCollection`; missing Contentstack endpoint/`access_token` (§17) | +| 10 | `live-preview` | review* | Leftover `@contentful/live-preview`; missing `live_preview`/`onEntryChange`/edit tags (§18) | +| 11 | `build` | **HARD** | Typecheck / lint / build failures (authoritative) (§15.13) | +| 12 | `secrets` | **HARD** | Hardcoded tokens/keys; un-ignored `.env`; example env not updated (§15.2) | +| 13 | `todos` | report | Lists `TODO(migration)` and guessed UIDs for mandatory human review | + +\* evals 09/10 return **N/A** (exit 3) when the app doesn't use that approach. + +## Session logger (`log.sh`) — verbose audit trail + +`log.sh` is **not** a pass/fail eval (the runner ignores it). It is an append-only audit log of the +whole migration — user inputs, AI actions & communications, decisions, eval results, commands, and +**every exception** — written to `/.migration/` as both `session.log` (human-readable) and +`session.jsonl` (machine-readable), with full per-command output under `commands/`. + +```bash +L=/evals/log.sh + +bash "$L" user-input "User asked: migrate Next.js app, GraphQL + live preview" +bash "$L" decision "Detected TS/Next.js/GraphQL(urql)+contentful live-preview" +bash "$L" ai-action "Rewrote lib/cms.ts queries to all_blog_post" +bash "$L" communication "Asked user to confirm field-UID map" +bash "$L" exception "coverImage UID unknown -> guessed cover_image (TODO)" + +# Run a command through the logger: captures stdout+stderr, preserves exit code, +# and auto-records a FAILED command as an exception: +bash "$L" run "install" -- npm ci +bash "$L" run "typecheck" -- npx tsc --noEmit + +bash "$L" summary # counts by type + exception list + recent entries +bash "$L" path # print the log directory +``` + +`run-all.sh` automatically records its verdict to this log when present. Recommended type values: +`user-input | ai-action | communication | decision | exception | command | eval | note`. + +## Exit codes + +`0` PASS · `1` FAIL (findings) · `2` ERROR (eval crashed) · `3` N/A (approach not used). +`run-all.sh` verdict: `0` PASS, `1` REVIEW (non-gate findings), `2` BLOCK (a hard gate failed). + +## How an AI should use these + +1. After migrating, run `run-all.sh ` (or spawn one agent per `NN_*.sh` for max parallelism). +2. **Hard-gate FAIL/ERROR (01,02,03,11,12) ⇒ the migration is not done.** Fix and re-run. +3. For review evals, **triage every finding** — static greps flag *suspects*, not proven bugs. + Open each `file:line`, decide true-positive vs false-positive, fix the real ones, and state why + the rest are safe. Do not dismiss findings silently. +4. The build eval is authoritative: a green typecheck/build is necessary but **not sufficient** — + it can't catch reference-array bugs, wrong field UIDs, or RTE output. Still smoke-test live + queries against the real Contentstack stack (doc §15.13). +5. Report results in the migration summary: per-eval status, triaged findings, and remaining TODOs. + +These checks are **necessary, not sufficient**. They guard against the known, mechanical mistakes; +they do not prove semantic correctness against your content model. diff --git a/codex/migration-companion/scripts/_lib.sh b/codex/migration-companion/scripts/_lib.sh new file mode 100755 index 0000000..4c9edc8 --- /dev/null +++ b/codex/migration-companion/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/codex/migration-companion/scripts/check_prereqs.py b/codex/migration-companion/scripts/check_prereqs.py new file mode 100644 index 0000000..17589e6 --- /dev/null +++ b/codex/migration-companion/scripts/check_prereqs.py @@ -0,0 +1,307 @@ +#!/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 glob +import json +import os +import pathlib +import re +import subprocess +import sys +import urllib.request + +# --------------------------------------------------------------------------- +# Environment +# --------------------------------------------------------------------------- +# +# All CLI subprocesses (node, npm, csdx, contentful) run under CLI_ENV. It +# starts as the inherited environment, but once we resolve the best available +# Node (see "Node.js" below) we prepend that Node's bin dir to PATH so every +# downstream tool uses it — even when an older /usr/local/bin/node would +# otherwise win in a non-interactive shell. +CLI_ENV = dict(os.environ) + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def run(cmd, env=None): + try: + r = subprocess.run( + cmd, + capture_output=True, + text=True, + env=env if env is not None else CLI_ENV, + ) + 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, + env=CLI_ENV, + ) + + +def extract_version(s): + """Pull the first semver-looking token out of a `--version` output.""" + m = re.search(r"(\d+\.\d+\.\d+[^\s]*)", s or "") + return m.group(1) if m else None + + +def npm_latest(pkg): + """Latest published version of a package on npm (None if npm is unreachable).""" + rc, ver, _ = run(["npm", "view", pkg, "version"]) + return ver.strip() if rc == 0 and ver.strip() else None + + +def node_version_tuple(node_bin): + """(major, minor, patch) version tuple for a node binary, or None if it won't run.""" + rc, ver, _ = run([node_bin, "--version"]) + if rc != 0: + return None, None + m = re.match(r"v?(\d+)\.(\d+)\.(\d+)", ver) + if not m: + return None, ver + return tuple(int(x) for x in m.groups()), ver + + +def discover_node_bins(): + """Every node binary we can find: PATH entries, nvm-installed versions, and + common system/homebrew locations. De-duplicated by real path.""" + candidates = [] + + # All `node` on PATH (handles shims and multiple managers). + rc, paths, _ = run(["which", "-a", "node"]) + if rc == 0: + candidates += [p for p in paths.splitlines() if p.strip()] + + # Every nvm-installed version (the default shell may not expose these). + nvm_dir = os.environ.get("NVM_DIR", os.path.expanduser("~/.nvm")) + candidates += glob.glob(os.path.join(nvm_dir, "versions", "node", "*", "bin", "node")) + + # Common fixed locations. + candidates += [ + "/opt/homebrew/bin/node", + "/usr/local/bin/node", + "/usr/bin/node", + ] + + seen, uniq = set(), [] + for c in candidates: + if not c or not os.path.exists(c): + continue + rp = os.path.realpath(c) + if rp in seen: + continue + seen.add(rp) + uniq.append(c) + return uniq + + +def pick_best_node(): + """Highest-version node binary available. Returns (version_tuple, version_str, + bin_path) or None if no node is found at all.""" + best = None + for b in discover_node_bins(): + tup, ver = node_version_tuple(b) + if tup is None: + continue + if best is None or tup > best[0]: + best = (tup, ver, b) + return best + + +def ensure_latest_cli(cmd_name, pkg): + """Ensure a global CLI is installed AND on the latest npm version. + + Installs when missing, upgrades when an older version is present. Returns a + dict suitable for the JSON summary: {ok, version, installed, latest, updated}. + """ + rc, raw, _ = run([cmd_name, "--version"]) + installed = extract_version(raw) if rc == 0 else None + latest = npm_latest(pkg) + updated = False + + if rc != 0: + # Not installed — install latest. + print(f"Installing {pkg} …", file=sys.stderr) + npm_install(f"{pkg}@latest") + rc, raw, _ = run([cmd_name, "--version"]) + installed = extract_version(raw) if rc == 0 else None + updated = rc == 0 + elif latest and installed and installed != latest: + # Installed but outdated — upgrade to latest. + print(f"Updating {pkg} {installed} → {latest} …", file=sys.stderr) + npm_install(f"{pkg}@latest") + rc, raw, _ = run([cmd_name, "--version"]) + installed = extract_version(raw) if rc == 0 else None + updated = rc == 0 + # else: latest is unknown (offline) or already current — leave as-is. + + return { + "ok": rc == 0, + "version": raw if rc == 0 else None, + "installed": installed, + "latest": latest, + "updated": updated, + } + + +# --------------------------------------------------------------------------- +# Checks +# --------------------------------------------------------------------------- + +out = {} + +# ── Node.js ───────────────────────────────────────────────────────────────── +# Pick the highest node available across PATH + nvm, not just whatever the +# non-interactive shell resolves first (which may be an old /usr/local/bin/node). +best = pick_best_node() +if best is None: + out["node"] = {"ok": False, "error": "not_installed"} + print(json.dumps(out, indent=2)) + sys.exit(1) + +node_tuple, ver, node_bin = best +major = node_tuple[0] +node_dir = os.path.dirname(node_bin) +out["node"] = { + "ok": major >= 20, + "version": ver, + "major": major, + "path": node_bin, + "bin_dir": node_dir, +} + +if major < 20: + # Hard blocker — emit result and exit 1 so the step file can surface the error. + # `path`/`version` reflect the BEST node we found, so the message is accurate. + print(json.dumps(out, indent=2)) + sys.exit(1) + +# Pin every downstream CLI (npm, csdx, contentful, node -e) to this node's bin +# dir so they don't fall back to an older node earlier on PATH. +CLI_ENV["PATH"] = node_dir + os.pathsep + CLI_ENV.get("PATH", "") + +# ── Contentstack CLI (csdx) — install if missing, upgrade if outdated ───────── +out["csdx"] = ensure_latest_cli("csdx", "@contentstack/cli") + +# ── 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 = {**CLI_ENV, "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_bin, "-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 — install if missing, upgrade if outdated ────────────────── +out["contentful_cli"] = ensure_latest_cli("contentful", "contentful-cli") + +# ── 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/codex/migration-companion/scripts/log.sh b/codex/migration-companion/scripts/log.sh new file mode 100755 index 0000000..9232fd3 --- /dev/null +++ b/codex/migration-companion/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 "