
Commerce App Migrate
- 28 installs
- 13 repo stars
- Updated August 4, 2026
- adobe/aio-commerce-sdk
commerce-app-migrate is a Claude Code skill that migrates an Adobe Commerce App Builder project from the Integration or Checkout Starter Kit to the App Management approach.
About
commerce-app-migrate migrates an Adobe Commerce App Builder project from the Integration Starter Kit or Checkout Starter Kit to the new App Management approach using @adobe/aio-commerce-lib-app. It orchestrates the full migration through detection, domain analysis, Q&A, config assembly, and execution, leaving the project ready to deploy. It supports --auto for non-interactive CI runs and --doc-scan-only to flag outdated README and env.dist content without modifying files.
- Migrates Integration or Checkout Starter Kit projects to App Management
- Orchestrates detection, domain analysis, Q&A, config assembly, and execution
- Offers --auto and --doc-scan-only modes for CI and doc-only scans
Commerce App Migrate by the numbers
- 28 all-time installs (skills.sh)
- Ranked #3,395 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
commerce-app-migrate capabilities & compatibility
- Capabilities
- commerce migration · starter kit upgrade · doc scan
- Use cases
- api development · refactoring
- Pricing
- Free
What commerce-app-migrate says it does
Migrate an Adobe Commerce App Builder project from the Integration Starter Kit or Checkout Starter Kit to the new App Management approach.
This skill orchestrates the full migration: detection → domain analysis → Q&A → config assembly → execution. It leaves the project fully migrated and ready to deploy.
npx skills add https://github.com/adobe/aio-commerce-sdk --skill commerce-app-migrateAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 28 |
|---|---|
| repo stars | ★ 13 |
| Last updated | August 4, 2026 |
| Repository | adobe/aio-commerce-sdk ↗ |
What it does
Migrate an Adobe Commerce App Builder project from a Starter Kit to the App Management approach.
Who is it for?
Upgrading an Integration Starter Kit or Checkout Starter Kit project to the App Management architecture with @adobe/aio-commerce-lib-app.
When should I use this skill?
A user wants to migrate an App Builder project from a Starter Kit to App Management or upgrade their Commerce extension architecture.
What you get
A fully migrated, deploy-ready App Management project with documentation recommendations.
- migrated app.commerce.config.ts
- documentation recommendations for README.md and env.dist
By the numbers
- 5 migration steps (detection, domain analysis, Q&A, config assembly, execution)
- README flagged by 5 patterns
- 3 modes (interactive, --auto, --doc-scan-only)
Files
Migrate to App Management
Use this skill when the user wants to migrate an Adobe Commerce App Builder project from the Integration Starter Kit or Checkout Starter Kit to the new App Management approach using @adobe/aio-commerce-lib-app.
This skill orchestrates the full migration: detection → domain analysis → Q&A → config assembly → execution. It leaves the project fully migrated and ready to deploy.
Confirmation protocol
At each step marked [await]: end your turn immediately, output nothing further, and wait for the developer's reply before proceeding.
Autonomous mode: If invoked with --auto or --yes, or the context indicates an automated pipeline (no interactive terminal), skip all [await] points and proceed directly to the next step.
Doc-scan-only mode: If invoked with --doc-scan-only, skip all migration steps after the Analyzer. After Step 1 completes (Analyzer returns a ProjectSnapshot):
- If
alreadyMigrated === false, output:
--doc-scan-only requires the project to already be migrated to App Management. No app.commerce.config.ts (or .js) was found.
Run /commerce-app-migrate (without --doc-scan-only) to perform the migration first.
Then stop.
- If
alreadyMigrated === true, apply any applicable Cross-cutting Warnings, then
dispatch the Executor in doc-scan-only mode. No files are modified. Do not proceed to Steps 2–5.
---
Preflight Check
Before doing anything else, verify the current directory looks like an App Builder project. Check that ALL of these exist:
app.config.yamlpackage.json- At least one of:
actions/directory ORsrc/directory ORactions-src/directory
(some projects compile TypeScript from actions-src/ to actions/; actions/ may be gitignored and absent in a fresh checkout)
If none of those pass, stop immediately and output:
This directory does not appear to be a Commerce App Builder project. Expected to find: app.config.yaml, package.json, and an actions/ or src/ directory.
Please run this skill from the root of your App Builder project.
Do not proceed further if the preflight fails.
---
Step 1: Run Analyzer Agent
Dispatch the Analyzer agent (defined in ${CLAUDE_SKILL_DIR}/agents/analyzer.md) using the Agent tool. The Analyzer reads the current directory and returns a ProjectSnapshot JSON object (schema defined in ${CLAUDE_SKILL_DIR}/shared/schema.md).
Before printing the summary, check these early-exit conditions:
If `alreadyMigrated === true`: Output:
This project appears to already be migrated to App Management. Found: app.commerce.config.ts (or app.commerce.config.js with ESM defineConfig)
Re-running migration would overwrite your existing configuration. If you want to re-generate specific sections, please specify which section to update: metadata / eventing / installation / adminUi / businessConfig
Then apply any applicable Cross-cutting Warnings (see subsection below).
Then dispatch the Executor agent (${CLAUDE_SKILL_DIR}/agents/executor.md) in doc-scan-only mode to produce documentation recommendations for the project:
- Pass
mode = "doc-scan-only"to the Executor - Pass the
ProjectSnapshotJSON from the Analyzer - Pass
assembled config = null(no new config to write)
The Executor will scan README.md and env.dist against the existing app.commerce.config.ts and print the "Documentation recommendations" report without modifying any files.
Do not proceed to Steps 2–5 (domain agents, Q&A, config assembly, full execution) unless the developer explicitly requests a specific section update.
After the Executor prints the documentation recommendations, stop. Do not continue to the "Detected project:" summary block or the migration confirmation prompt below.
After the Analyzer returns, print a human-readable summary:
Detected project: Type: <starterKitType> Starter Kit Auth mode: <authMode> (<paas = "PaaS/OAuth1" | saas = "SaaS/IMS" | dual = "Both PaaS + SaaS" | unknown = "Unknown">) Action packages: <comma-separated list of package names> (<count> packages) Onboarding: <comma-separated list of script paths with purposes, or "none"> Package manager: <packageManager>
Migration will include: <list domains where confidence !== "none">
Apply any applicable Cross-cutting Warnings (see subsection below).
If `hasMeshConfig === true`, also append:
⚠ API Mesh configuration detected (mesh.json). Mesh configuration cannot be migrated automatically and must be preserved manually.
Then ask:
Does this look correct? (yes / no — if no, describe what's wrong) Press Enter or type "yes" to proceed automatically.
[await]
Handle corrections:
- If developer corrects
starterKitType, update it in the ProjectSnapshot before proceeding - If developer corrects
authMode, update it - Re-print summary with corrections and ask again until confirmed
Handle unknown starterKitType: If starterKitType === "unknown", check extensionPointsInUse:
- If
"commerce/backend-ui/1"is present → this is an Admin UI SDK v1 extension. Proceed
with starterKitType = "unknown" — the admin-ui-sdk domain agent will migrate it to v2. Print a note: "Detected Admin UI SDK v1 (commerce/backend-ui/1). Migrating to v2 (commerce/backend-ui/2)."
- If all confidence values are
"none"ANDextensionPointsInUseis empty:
Output:
This project does not appear to be based on the Integration Starter Kit or Checkout Starter Kit. No event consumers, webhooks, or Admin UI SDK patterns were detected.
Migration can still generate a minimal app.commerce.config.ts with metadata only. Continue? (yes / no)
If developer says no, stop. If yes, proceed with empty domain results.
- Otherwise, ask the developer:
I couldn't determine which starter kit this project is based on. Is this an Integration Starter Kit or a Checkout Starter Kit? Options: [integration / checkout / adminUiSdk / custom]
Not sure? See the documentation: • Integration Starter Kit: https://developer.adobe.com/commerce/extensibility/starter-kit/integration/ • Checkout Starter Kit: https://developer.adobe.com/commerce/extensibility/starter-kit/checkout/
Update the ProjectSnapshot with their answer before proceeding.
Cross-cutting Warnings
Apply these whenever the corresponding field is set in the ProjectSnapshot, regardless of migration state. Append each matching block to whatever output is currently being built.
If `openWhiskTriggers` is non-empty, append:
⚠ OpenWhisk triggers detected (cannot be auto-migrated): <list each trigger description> These scheduled triggers have no direct equivalent in App Management.
Read ${CLAUDE_SKILL_DIR}/shared/migration-warnings.md (OpenWhisk Triggers section) and present the replacement options to the developer.
If `hasApiGateway === true`, append:
⚠ OpenWhisk API Gateway routes detected (apis: blocks in runtime manifest). These HTTP routes have no direct equivalent in App Management.
Read ${CLAUDE_SKILL_DIR}/shared/migration-warnings.md (API Gateway section) and present the migration options to the developer.
If `hasSequences === true`, append:
⚠ OpenWhisk sequences detected. Action sequences have no equivalent in App Management.
Read ${CLAUDE_SKILL_DIR}/shared/migration-warnings.md (Sequences section) and present the refactoring guidance to the developer.
---
Step 2: Dispatch Domain Agents in Parallel
For each domain where confidence !== "none", dispatch the corresponding agent at the same time using the Agent tool (all in one parallel call):
| confidence field | Agent file |
|---|---|
confidence.events !== "none" | ${CLAUDE_SKILL_DIR}/agents/events.md |
confidence.webhooks !== "none" | ${CLAUDE_SKILL_DIR}/agents/webhooks.md |
confidence.adminUiSdk !== "none" | ${CLAUDE_SKILL_DIR}/agents/admin-ui-sdk.md |
confidence.businessConfig !== "none" | ${CLAUDE_SKILL_DIR}/agents/business-config.md |
Each agent receives:
- The
ProjectSnapshotJSON - Instruction to read the relevant files in the current directory using their own Read tools
Collect all returned DomainResult objects. Each has domain, configFragment, and unresolvedQuestions fields (schema in ${CLAUDE_SKILL_DIR}/shared/schema.md).
---
Step 3: Grouped Q&A
Collect all unresolvedQuestions from every DomainResult. If there are no unresolved questions across all domains, skip to Step 4.
Present all questions in a single grouped session (format defined in ${CLAUDE_SKILL_DIR}/shared/questions.md):
- Group by domain
- For questions with
defaultvalues, show as confirmations: "(suggested: X)" - Number questions sequentially across domains
[await]
For each answer received:
- Apply it to the
configFragmentof the correspondingDomainResult - Use the question
idto locate the exact field to update
If the developer accepts a suggested value (presses Enter / says "yes"), use the default.
Special case — skip-by-default questions (where default is "no"): These questions ask whether to include an optional section that was omitted from the configFragment because required data was missing (e.g. a provider direction with no deployed action package). Interpret the developer's reply as:
"no"(or pressing Enter) → leave the section out of theconfigFragment; do nothing"yes"or any action string → add the section back; prompt for the runtime action name
if not already specified in the reply, then apply it to the configFragment
Autonomous mode — auto-accepted defaults summary:
In --auto mode, after applying all question defaults, print:
── Auto-accepted defaults ──────────────────────────────────────── The following questions were answered automatically (--auto mode): [ <domain> : <question id> = "<default value>" ] ← one line per question that had an explicit default; omit entire section if no questions existed →
Omit this section entirely if there were no unresolved questions across all domains.
---
Step 4: Assemble app.commerce.config.ts
Read package.json from the current directory to extract metadata.
Assemble the full config content by merging all configFragment objects:
import { defineConfig } from "@adobe/aio-commerce-lib-app/config";
export default defineConfig({
metadata: {
id: "<derived from package.json name>",
displayName: "<derived from package.json name>",
version: "<from package.json version or 1.0.0>",
description:
"<from package.json description or 'Commerce App Builder application'>",
},
// eventing: { ... } ← from events DomainResult, if present
// installation: { ... } ← from webhooks DomainResult, if present
// adminUi: { ... } ← from admin-ui-sdk DomainResult (migrated from v1), if present
// businessConfig: { ... } ← from business-config DomainResult, if present
});Metadata derivation rules:
id: Checkextension-manifest.jsonfirst — if it has anidfield and it is not
the same as the package name boilerplate, use it (apply same normalization: lowercase, replace non-alphanumeric with -, trim, max 50 chars). Otherwise take name from package.json. Strip npm scope (@scope/). Replace any non-alphanumeric characters (except -) with -. Lowercase the entire string. Trim leading/trailing dashes. Max 50 chars. Known ISK boilerplate names to skip in favor of extension-manifest.json: commerce-integration-starter-kit, starter-kit, commerce-checkout-starter-kit, aio-app-builder-template.
displayName: Checkextension-manifest.jsonfirst — if it has adisplayNamefield,
use it (truncated to 50 chars). Otherwise title-case the id (replace - with spaces). Max 50 chars.
version: Usepackage.jsonversion. Default:"1.0.0".description: Usepackage.jsondescriptionif present.
If the description exceeds 255 characters, truncate it to 252 characters and append "...". If absent or empty, check extension-manifest.json description field (truncate to 255 if needed). If neither has a description: "Commerce App Builder application".
productDependencies comment: If ProjectSnapshot.productDependencies is non-null (has minVersion and/or maxVersion): Insert this comment block in the assembled TypeScript, immediately AFTER the copyright header and BEFORE the import { defineConfig } line:
// Product version constraints (no App Management equivalent — for reference only):
// Adobe Commerce compatibility: >= <minVersion>, < <maxVersion>
// Contact Adobe Commerce Marketplace for guidance on version enforcement.Omit the < <maxVersion> part if maxVersion is null. Omit the >= <minVersion> part if minVersion is null. If both are null or productDependencies is null, omit the comment entirely.
Print the assembled TypeScript content to the terminal:
Here is the app.commerce.config.ts that will be created:
───────────────────────────────────────────────────────── import { defineConfig } from '@adobe/aio-commerce-lib-app/config'
export default defineConfig({ ...assembled content... }) ─────────────────────────────────────────────────────────
Does this look correct? (yes / no — if no, which section needs updating?) Press Enter or type "yes" to proceed automatically.
[await]
Handle rejection: If the developer says no, ask: "Which section needs updating? (metadata / eventing / installation / adminUi / businessConfig)"
Then ask the specific corrective question for that section, update the assembled config accordingly, re-print, and ask for confirmation again.
Repeat until the developer confirms. Do NOT restart the entire flow — only re-enter the Q&A for the specific section being corrected.
---
Step 5: Execute Migration
Dispatch the Executor agent (${CLAUDE_SKILL_DIR}/agents/executor.md) with:
1. The assembled app.commerce.config.ts TypeScript content as a string 2. The final ProjectSnapshot JSON
The Executor performs all file writes and CLI commands and prints the migration summary.
If the Executor reports an error, relay the error message and the step that failed. Do not attempt to roll back changes — the git branch created by the Executor provides a rollback point (git checkout main to abandon the migration).
---
Notes
- Domain agents (Step 2) run in parallel — dispatch all eligible agents in one Agent tool call
- Analyzer (Step 1) and Executor (Step 5) are sequential — Analyzer first, Executor last
- This skill runs in the developer's project directory, not the migration skill repository
- Documentation recommendations are produced by the Executor and cover:
- README.md sections flagged by 5 patterns: ISK onboarding commands, old env var references, outdated architecture diagrams, credential-family semantic matching (IMS/SaaS, PaaS/OAuth1, workspace), and env setup boilerplate (
cp env.dist .env) - env.dist entries grouped into three buckets: safe-to-remove (managed by App Management), review-manually (referenced in action source files, grouped by file), and onboarding-only (used only in onboarding scripts)
- Rule 9 catches unreferenced env.dist variables not matched by any other rule and flags them for manual review
- Category C (README) requires ≥ 5 flagged sections to emit the recommendations block
- `--auto` mode prints a summary of all Q&A questions answered automatically with their defaults (omitted if no questions existed)
- `productDependencies` version constraints from
extension-manifest.jsonare auto-inserted as a comment block immediately before theimport { defineConfig }line in the generatedapp.commerce.config.ts - Internal metadata fields (
_directionWarning,_source) may be added by domain agents toconfigFragmentobjects — the Executor strips these before writing files so they never appear in the output TypeScript
Supporting Files
Agent files dispatched by this skill:
- Analyzer
- Events
- Webhooks
- Admin UI SDK
- Business Config
- Executor
Shared reference files:
- Schema
- Questions format
- Migration warnings
- External references
Admin UI SDK Agent — v1 to v2 Migration
You are the Admin UI SDK migration agent for the App Management Migration skill. This agent is dispatched only when confidence.adminUiSdk !== "none".
You receive a ProjectSnapshot JSON. Your job is to read the existing Admin UI SDK v1 registration data and produce an adminUi (v2) config fragment for app.commerce.config.ts.
commerce/backend-ui/1 (the Admin UI SDK v1 extension point) is superseded by commerce/backend-ui/2. This agent migrates the v1 registration shape to the adminUi config key, which targets commerce/backend-ui/2.
Output ONLY valid JSON — no explanation, no markdown fences, no extra text.
---
Input
You will be given:
1. The ProjectSnapshot JSON 2. Read any of the following files that exist (use your Read tool):
- All contents of
src/commerce-backend-ui-1/directory (if present) - Any file named
registration.jsorregistration.jsonunderactions/ - Any file whose name contains
registrationunderactions/ - Any
ext.config.yamlfiles in the project root
---
Inference Rules
Finding v1 registration data
Look for a runtime action or file that exports or returns an Admin UI SDK v1 registration object. It will be a JS/JSON file containing an object with one or more of these top-level keys:
Supported v1 extension points:
menuItems— array of menu item definitionsorder— object with:massActions,gridColumns,viewButtons,customFeesproduct— object with:massActions,gridColumnscustomer— object with:massActions,gridColumnsbannerNotification— per-action success/error messages (folded into mass action and view buttonnotifications)
Static vs dynamic registration:
Static registration: If the file exports a static object (values are not computed from env vars, API calls, or runtime data), extract it directly.
Dynamic registration: If values are computed at runtime — reading from process.env or params, making API calls, or using template strings where interpolated values come from non-constant sources (function parameters, imported config, environment lookups) — the agent cannot safely infer static values. Add an unresolved question:
{ "id": "adminUi.registration.source", "prompt": "The Admin UI SDK registration in <file> is dynamically generated. Please provide the static registration object as JSON.", "default": "{}" }
Exception: Template strings whose interpolated values are module-level constants defined in the same file (e.g. ${BASE_PATH}/route where const BASE_PATH = '/api') are resolvable — substitute the constant and treat the result as static.
Multiple conflicting registration files: Add an unresolved question:
{ "id": "adminUi.registration.conflict", "prompt": "Multiple registration files found: <file1>, <file2>. Which one should be used?", "default": "<file1>" }
---
Mapping Rules: v1 → v2
Mass actions (order, product, customer)
V1 mass actions used displayIframe to distinguish iframe vs. worker handlers. Map them using the type discriminator in v2.
| V1 field | V2 field | Notes |
|---|---|---|
actionId | id | Rename |
label | label | Unchanged |
title | title | Unchanged (optional) |
confirm.title | confirm.title | Unchanged |
confirm.message | confirm.message | Unchanged |
path | path / runtimeAction | If type: "view": keep as path. If type: "worker": rename to runtimeAction. |
sandbox | sandboxPermissions | V1: space-separated string → V2: array of valid values (see below). Only on type: "view". |
selectionLimit | selectionLimit | Unchanged (order) |
productSelectLimit | selectionLimit | Rename (product) |
customerSelectLimit | selectionLimit | Rename (customer) |
displayIframe | type | true (or absent) → "view"; false → "worker" |
timeout | timeout | Keep if type: "worker"; omit if type: "view" |
`type` from `displayIframe`: Set type based on displayIframe:
displayIframe: trueordisplayIframeabsent →type: "view"displayIframe: false→type: "worker"
`runtimeAction` for worker mass actions: V1 path on a displayIframe: false mass action was a full HTTP URL to the runtime action endpoint, not a runtime action name. V2 runtimeAction requires a <package>/<action> name resolved by App Registry. Add an unresolved question and set runtimeAction to "<FILL_IN>":
{ "id": "adminUi.<entity>.massActions.<actionId>.runtimeAction", "prompt": "V1 mass action \"<actionId>\" was a worker (displayIframe: false) with path \"<path>\". V2 requires a runtimeAction name (e.g. \"<package>/<action-name>\"). What is the runtime action name?", "default": "" }
`sandbox` / `sandboxPermissions`: Valid values are "allow-downloads", "allow-modals", and "allow-popups". If the v1 value is a space-separated string, split it and keep only values from this list, discarding any others. If the v1 value is already an array, use it as-is (filtering out invalid values as above). If no valid values remain after filtering, omit sandboxPermissions entirely.
Banner notifications (bannerNotification): if a mass action has a matching entry in bannerNotification.massActions.{order|product|customer} (matched by actionId), fold successMessage → notifications.success and errorMessage → notifications.error on the corresponding v2 mass action entry.
Grid columns (order, product, customer)
V1 grid columns used the API Mesh (data.meshId) to fetch column data. V2 uses a runtimeAction instead. This gap requires manual intervention.
| V1 field | V2 field | Notes |
|---|---|---|
properties[].columnId | columns[].id | Rename |
properties[].label | columns[].label | Unchanged |
properties[].type | columns[].type | All v1 values are valid in v2; v2 also adds "datetime" |
properties[].align | columns[].align | Unchanged |
data.meshId | _(no equivalent)_ | Add unresolved question (see below) |
V2 grid columns also require label (grid title) and description (grid description) at the top level — these don't exist in V1. Add an unresolved question for each missing field.
Unresolved questions for each entity that has grid columns (replace <entity> with order, product, or customer and <meshId> with the actual value found):
Replace <meshId> with the actual data.meshId value. If data.meshId is absent, omit the (meshId: "...") parenthetical from the prompt.
{ "id": "adminUi.<entity>.gridColumns.runtimeAction", "prompt": "V1 grid columns for <entity> used API Mesh (meshId: \"<meshId>\"). V2 requires a runtimeAction that returns column data. What is the runtime action name for the <entity> grid columns?", "default": "" }
{ "id": "adminUi.<entity>.gridColumns.label", "prompt": "What label should be shown for the <entity> grid columns section?", "default": "<Entity> grid columns" }
{ "id": "adminUi.<entity>.gridColumns.description", "prompt": "What description should be shown for the <entity> grid columns?", "default": "Adds custom columns to the <entity> grid" }
Set runtimeAction to "<FILL_IN>" in the configFragment until the developer answers.
If properties is absent or an empty array, treat the entity as having no gridColumns — skip it entirely (no configFragment entry, no unresolved questions for that entity).
View buttons (order only)
V1 order view buttons (order.viewButtons) are now supported in v2. Like mass actions, the v1 displayIframe boolean is replaced by an explicit type discriminator.
| V1 field | V2 field | Notes |
|---|---|---|
buttonId | id | Rename |
label | label | Unchanged |
displayIframe | type | true (or absent) → "view"; false → "worker" |
path | path / runtimeAction | If type: "view": keep as path. If type: "worker": add unresolved question (see below). |
sandbox | sandboxPermissions | Same rules as mass actions: split string, filter valid values. Only on type: "view". |
level | level | Unchanged (optional) |
sortOrder | sortOrder | Unchanged (optional) |
confirm | confirm | V1 only had confirm.message; v2 adds optional confirm.title (new, omit from configFragment) |
timeout | timeout | Keep on type: "worker"; omit on type: "view" |
| _(absent)_ | description | New optional v2 field; omit from configFragment |
`type` from `displayIframe`: Same rule as mass actions:
displayIframe: trueor absent →type: "view"(keeppath)displayIframe: false→type: "worker"(seeruntimeActionbelow)
`runtimeAction` for worker buttons: V1 path on a displayIframe: false button was a developer-defined HTTP URL, not a runtime action name. V2 requires a <package>/<action> name resolved by App Registry. Add an unresolved question and set runtimeAction to "<FILL_IN>":
{ "id": "adminUi.order.viewButtons.<buttonId>.runtimeAction", "prompt": "V1 view button \"<buttonId>\" was a non-iframe button (displayIframe: false) with path \"<path>\". V2 requires a runtimeAction name (e.g. \"orders/<action-name>\"). What is the runtime action name?", "default": "" }
Missing `path` on view buttons: If a type: "view" button has no path, add an unresolved question and set path to "<FILL_IN>":
{ "id": "adminUi.order.viewButtons.<buttonId>.path", "prompt": "View button \"<buttonId>\" has no path. What iframe path should it open (e.g. \"#/my-view\")?", "default": "" }
Banner notifications (bannerNotification.orderViewButtons): if a view button has a matching entry in bannerNotification.orderViewButtons (matched by buttonId), fold successMessage → notifications.success and errorMessage → notifications.error on the corresponding v2 entry.
Menu items
V1 menuItems is an array that typically contains a section item (isSection: true) and a child menu item. V2 has a single menu object — skip any isSection: true entries and use the first non-section item.
| V1 field | V2 field | Notes |
|---|---|---|
id | id | Unchanged |
title | label | Rename |
page.title | pageTitle | Rename and flatten; omit if absent |
parent | parentMenu | Map only if value is a known Commerce menu ID (see below) |
sandbox | sandboxPermissions | Same rules as mass actions: split string, filter valid values |
sortOrder | _(dropped)_ | No v2 equivalent; omit |
isSection | _(dropped)_ | Section is auto-generated by v2; skip items with this flag |
| _(absent)_ | description | Required in v2; add unresolved question |
`parentMenu` mapping: V2 parentMenu is constrained to known Commerce Admin menu IDs: "sales", "catalog", "customers", "marketing", "content", "reports", "stores", "system".
- If the v1
parentvalue matches one of these IDs → setparentMenuto that value. - If the v1
parentreferences another item in the same registration (an app-created section) → omit `parentMenu`; v2 auto-generates the app section frommetadata.idandmetadata.displayName. - If
parentis absent → omitparentMenu.
Missing `id`: If the non-section menu item has no id, add an unresolved question and set id to "<FILL_IN>":
{ "id": "adminUi.menu.id", "prompt": "The first non-section menu item has no id. What id should the v2 menu use (e.g. \"dashboard\")?", "default": "" }
If multiple non-section menu items are present, use the first one and add:
{ "id": "adminUi.menu.multipleItems", "prompt": "V1 menuItems had <N> non-section entries. V2 supports only one top-level menu. Using the first item (id: \"<id>\"). The remaining items (<list ids>) cannot be migrated automatically — acknowledge and continue?", "default": "acknowledged" }
Required `description` for menu — add for every menu item:
{ "id": "adminUi.menu.description", "prompt": "V2 menu requires a description. What description should the menu have?", "default": "<title value from v1>" }
Set description to "<FILL_IN>" in the configFragment until the developer answers.
Custom fees (order only)
V1 order.customFees has no equivalent in adminUi. In v2, custom fees are implemented as a webhook on plugin.magento.out_of_process_totals_collector.api.get_total_modifications.custom_fees — a server-to-server mechanism that fires during cart totals collection and returns fees via a JSON Patch response. This is a manual migration that requires implementing a new webhook handler.
For each custom fee found, inform the developer that it requires manual migration.
See: Custom fees — Checkout Totals Collector
---
Handler Migration
After producing the configFragment, update the runtime action handler source files. Only worker entries require a handler — view entries (iframe) have no server handler.
Use your Write tool to update or create each handler file described below.
Worker mass actions
For each worker mass action in the v1 registration, find the existing handler file. The file is typically referenced by the path field in the v1 config (an HTTP URL whose path segment maps to an action under actions/), or discoverable via app.config.yaml or ext.config.yaml. Update it to use the v2 wire-contract builders:
import {
parseMassActionRequest,
okMassActionResponse,
massActionErrorResponse,
} from "@adobe/aio-commerce-sdk/admin-ui/mass-actions";
export async function main(params: unknown) {
const { gridType, ids } = parseMassActionRequest(params);
try {
// existing business logic — replace direct params access with the parsed values above
await processEntities(gridType, ids);
return okMassActionResponse();
} catch (error) {
return massActionErrorResponse(500, (error as Error).message);
}
}parseMassActionRequest(params)→{ requestId, gridType, ids }— validates the incoming shape; throwsCommerceSdkValidationErroron bad input.okMassActionResponse(body?)→ HTTP 200; optional body for logging.massActionErrorResponse(statusCode, message)→ non-2xx; body is{ message }.
Worker view buttons
For each worker view button (displayIframe: false) in the v1 registration, find the existing handler file and update it:
import {
parseOrderViewButtonRequest,
okOrderViewButtonResponse,
orderViewButtonErrorResponse,
} from "@adobe/aio-commerce-sdk/admin-ui/order-view-buttons";
export async function main(params: unknown) {
const { id, orderId } = parseOrderViewButtonRequest(params);
try {
// existing business logic — id is the buttonId, orderId is the order
await handleButton(id, orderId);
return okOrderViewButtonResponse();
} catch (error) {
return orderViewButtonErrorResponse(500, (error as Error).message);
}
}parseOrderViewButtonRequest(params)→{ requestId, id, orderId }—ididentifies which button was clicked (useful when one handler serves multiple buttons).okOrderViewButtonResponse()→ success; empty{}body.orderViewButtonErrorResponse(statusCode, message)→ error;{ message }body.
Grid columns (new handler)
V1 grid columns used API Mesh — there is no existing handler to migrate. Once the developer provides the runtimeAction name (via the adminUi.<entity>.gridColumns.runtimeAction unresolved question), scaffold a new handler under src/commerce-backend-ui-2/actions/<action>/index.ts:
import {
parseGridRequest,
okGridResponse,
errorGridResponse,
} from "@adobe/aio-commerce-sdk/admin-ui/grid-columns";
export async function main(params: unknown) {
const { gridType, ids } = parseGridRequest(params);
try {
// fetch column data for the visible entity ids
const rows = await fetchColumnData(gridType, ids);
// rows keyed by entity id; each value keyed by the column ids declared in config
return okGridResponse(rows);
} catch (error) {
return errorGridResponse(500, (error as Error).message);
}
}parseGridRequest(params)→{ requestId, gridType, ids }—idsare the visible entity IDs.okGridResponse(data, defaults?)→ success envelope. Row object keys must match theidvalues declared ingridColumns.columns, or cells render empty.errorGridResponse(statusCode, message)→ non-2xx error response.
Also register the action in src/commerce-backend-ui-2/ext.config.yaml under runtimeManifest:
runtimeManifest:
packages:
<package>: # must match the <package> portion of runtimeAction
actions:
<action>:
function: actions/<action>/index.js
web: "yes"
annotations:
require-adobe-auth: true
final: trueIf the developer has not yet answered the runtimeAction unresolved question, skip creating the handler file — leave a <FILL_IN> placeholder in the config and note that the handler must be scaffolded once the action name is confirmed.
---
Output
The domain field must be "adminUi" (v2 key).
Example when order mass actions and product grid columns are found:
{ "domain": "adminUi", "configFragment": { "adminUi": { "order": { "massActions": [ { "id": "export-orders", "label": "Export Orders", "type": "view", "path": "#/export", "confirm": { "title": "Export?", "message": "Export selected orders?" } } ] }, "product": { "gridColumns": { "label": "Product grid columns", "description": "Adds custom columns to the product grid", "runtimeAction": "<FILL_IN>", "columns": [ { "id": "inventory_status", "label": "Inventory Status", "type": "string", "align": "left" } ] } } } }, "unresolvedQuestions": [ { "id": "adminUi.product.gridColumns.runtimeAction", "prompt": "V1 grid columns for product used API Mesh (meshId: \"my-mesh-id\"). V2 requires a runtimeAction that returns column data. What is the runtime action name for the product grid columns?", "default": "" }, { "id": "adminUi.product.gridColumns.label", "prompt": "What label should be shown for the product grid columns section?", "default": "Product grid columns" }, { "id": "adminUi.product.gridColumns.description", "prompt": "What description should be shown for the product grid columns?", "default": "Adds custom columns to the product grid" } ] }
Example when a menu item is found:
{ "domain": "adminUi", "configFragment": { "adminUi": { "menu": { "id": "dashboard", "label": "My App", "description": "<FILL_IN>", "parentMenu": "sales" } } }, "unresolvedQuestions": [ { "id": "adminUi.menu.description", "prompt": "V2 menu requires a description. What description should the menu have?", "default": "My App" } ] }
Analyzer Agent — App Management Migration
You are the Analyzer agent for the App Management Migration skill. Your job is to read the App Builder starter kit project in the current directory and produce a ProjectSnapshot JSON object describing its structure.
Output ONLY valid JSON — no explanation, no markdown fences, no extra text.
---
Files to Read
Read ALL of the following. Skip files that don't exist (do not error).
1. app.config.yaml — packages, extensions block, included action configs 2. package.json — dependencies and scripts 3. env.dist — variable names for auth mode detection and envDistKeys population 4. .env — last resort; extract key names only via shell, do not read with Read tool 5. Every file under scripts/onboarding/ (list directory first, then read each file) 6. Every actions.config.yaml under actions/ (list directory recursively, read each) 7. install.yaml AND install.yml — existing extension point declarations (either extension may exist) 8. README.md — supplementary context 9. app.commerce.config.ts — check for already-migrated state 10. app.commerce.config.js — check for already-migrated state 11. mesh.json — API Mesh configuration presence 12. Any ext.config.yaml referenced via $include in the extensions: block 13. extension-manifest.json — preferred metadata source (id, displayName, description)
---
Detection Rules
alreadyMigrated
true—app.commerce.config.tsORapp.commerce.config.jsexists at the project rootfalse— neither file exists
starterKitType
Parse app.config.yaml. First resolve all packages:
- If
app.config.yamlhas ONLY anextensions:block (noapplication:block), follow each
$include path under extensions: to read the referenced ext.config.yaml files, then inspect all package names within those files.
- If
app.config.yamlhas anapplication:block, inspectapplication.runtimeManifest.packages. - If
app.config.yamlhas BOTH anapplication:block AND anextensions:block, inspect
packages from BOTH: read application.runtimeManifest.packages AND follow each $include path under extensions: to read all referenced ext.config.yaml files.
Signals:
"integration"— ANY of these package names present:product-commerce,
customer-commerce, order-commerce, stock-commerce, product-backoffice, customer-backoffice, order-backoffice, stock-backoffice, starter-kit OR any of these structural signals:
starter-kit-registrations.jsonexists at the project rootEVENTS_SCHEMA.jsonexists at the project rootscripts/onboarding/index.jsexists- Action directories follow the
actions/<entity>/commerce/+actions/<entity>/external/
bidirectional pattern for at least 2 entities (e.g. actions/product/commerce/, actions/product/external/, actions/order/commerce/, actions/order/external/)
"checkout"— ANY of these signals:events.config.yaml,payment-methods.yaml,shipping-carriers.yaml, ortax-integrations.yamlexists at the project root- Any of these npm scripts present in
package.json:configure-events,configure-commerce-events,create-payment-methods,create-tax-integrations,create-shipping-carriers,get-shipping-carriers,sync-oauth-credentials - Any action in any collected config file has annotation
raw-http: trueAND an input namedCOMMERCE_WEBHOOKS_PUBLIC_KEY - Any webhook method path in any config file contains
out_of_process scripts/create-shipping-carriers.jsORscripts/create-payment-methods.jsexistscommerce-checkout-starter-kitpresent in thenamefield ofpackage.json- If both signal sets appear, use
"integration" "unknown"— no signals from either set
authMode
Check in this order, stopping as soon as both signals are resolved:
1. app.config.yaml global inputs and any ext.config.yaml files — look for input variable names 2. env.dist — read the file and look for non-commented, non-empty variable name lines 3. .env — last resort only; do NOT read the file with Read. Instead run: grep -E '^[A-Z_]+=' .env | sed 's/=.*//' This extracts only variable names, never values.
COMMERCE_CONSUMER_KEYORAIO_COMMERCE_AUTH_INTEGRATION_CONSUMER_KEYpresent → PaaS signalOAUTH_CLIENT_IDORAIO_COMMERCE_AUTH_IMS_CLIENT_IDpresent → SaaS signal
Result:
"dual"— BOTH signals found in the same project (most common in real-world apps)"paas"— only PaaS signal found"saas"— only SaaS signal found"unknown"— neither found
envDistKeys
Using the content of env.dist already read for authMode detection:
For each line in env.dist:
- Skip lines that start with
#(comments) or are blank - Extract the portion of the line before the first
=character; trim surrounding whitespace - If the line contains no
=, use the entire trimmed line as the key name - Validate the candidate key name against the pattern
^[A-Z_][A-Z0-9_]*$ - If the key does NOT match (e.g. contains lowercase letters, spaces, or shell metacharacters), skip the line silently — do not include it in
envDistKeys
Store the resulting array of validated key names as envDistKeys.
Example — given this env.dist:
Auth
COMMERCE_CONSUMER_KEY= OAUTH_CLIENT_ID=abc
Events
AIO_EVENTS_PROVIDER_ID=xyz LOG_LEVEL=debug
→ "envDistKeys": ["COMMERCE_CONSUMER_KEY", "OAUTH_CLIENT_ID", "AIO_EVENTS_PROVIDER_ID", "LOG_LEVEL"]
If env.dist does not exist, output "envDistKeys": [].
envDistDuplicates
While processing the same env.dist lines for envDistKeys, count how many non-comment, non-blank lines contain each key name (i.e. how many times each key appears at the start of a non-comment line).
For each key whose count is greater than 1, add an entry to envDistDuplicates:
{ "<KEY>": <count> }
If no key appears more than once, output "envDistDuplicates": {}. If env.dist does not exist, output "envDistDuplicates": {}.
Example — given an env.dist where COMMERCE_CONSUMER_KEY appears on two separate lines:
→ "envDistDuplicates": { "COMMERCE_CONSUMER_KEY": 2 }
---
packageScripts
Using the package.json content already read for packageManager and starterKitType detection:
Extract the scripts object from package.json. Store each script name and its command string verbatim as packageScripts.
Example — given this package.json scripts section:
"scripts": { "onboard": "node scripts/onboarding/index.js", "configure-events": "node scripts/onboarding/subscribe.js", "build": "webpack" }
→ "packageScripts": { "onboard": "node scripts/onboarding/index.js", "configure-events": "node scripts/onboarding/subscribe.js", "build": "webpack" }
If package.json has no scripts key, output "packageScripts": {}.
packageManager
Check root directory for lockfiles:
pnpm-lock.yamlexists →"pnpm"yarn.lockexists →"yarn"bun.lockbexists →"bun"- Otherwise →
"npm"(including when no lockfile exists at all)
actionPackages
Collect packages from ALL config files:
From `application.runtimeManifest.packages` (if application: block exists):
- For each package entry, record
nameand resolve actions (see below)
From `extensions:` block (if exists):
- For each extension point (e.g.
commerce/backend-ui/1: $include: src/...ext.config.yaml):
read the referenced ext.config.yaml and collect all packages within it the same way
Action resolution for each package:
- If the action is defined inline: extract
name,functionpath, andwebas a boolean
(true if YAML has web: 'yes', false otherwise)
- If the package uses
$include: ./path/to/actions.config.yaml: read that file and extract
all actions from it
Build the array of { name, actions: [{ name, function, web }] } objects. The web field must be a JSON boolean (true/false), never a string.
onboardingScripts
List all files under scripts/onboarding/. For each file, emit an object with exactly two fields: path (relative path from project root) and purpose (one of the values below).
Classify purpose:
"event-provider"— filename containsproviderOR file content imports/calls
functions named createEventProvider, provider, or similar provider-creation APIs
"event-subscription"— filename containssubscribe,registration,
event-subscribe OR content calls createEventSubscription, register, or reads from starter-kit-registrations.json / events.json
"webhook"— filename containswebhookOR content callssubscribeWebhook,
registerWebhook, or imports @adobe/aio-commerce-lib-webhooks
"unknown"— none of the above patterns match
Also check scripts/commerce-event-subscribe/ (if it exists alongside scripts/onboarding/ or instead of it): list all files there and classify each as "event-subscription". This directory is used by many real-world ISK apps to hold the Commerce-side event subscription step that runs after provider creation.
If scripts/onboarding/ does not exist, check hooks/ as a fallback. If neither exists, check the top-level scripts/ directory for custom installation scripts: files whose names contain create-, setup-, configure-, register-, install-, or onboard- AND that are referenced as npm scripts in package.json. Classify these as "custom-installation". If no scripts directory exists at all, onboardingScripts is an empty array.
Special case for Integration Starter Kit: The scripts/onboarding/index.js script orchestrates provider creation, metadata, and event registrations all in one. Classify this file as "event-subscription" (it does everything).
extensionPointsInUse
Check app.config.yaml for an extensions: top-level key. List all child keys (e.g. commerce/backend-ui/1). If no extensions: block, return [].
Also check install.yaml AND install.yml if either exists: add any extensionPointId values not already in the list.
openWhiskTriggers
Scan ALL collected YAML config files (app.config.yaml and all referenced ext.config.yaml and actions.config.yaml files) for triggers: and rules: blocks.
For each trigger found, record a human-readable description: "<trigger-name> (<feed> — <interval or cron> if present)"
Examples:
"dailyTrigger (/whisk.system/alarms/alarm — 1440 min interval)""processScheduledEmailsTrigger (interval — 1 min)""ordercancel-every-five-minutes (/whisk.system/alarms/alarm — cron)"
Also scan each action entry for a cron: annotation field (a non-standard scheduling pattern used by some apps). Record each as: "<package-name>/<action-name> (cron annotation: <cron value>)"
Return [] if no triggers, rules, or cron annotations found.
hasMeshConfig
true—mesh.jsonexists at the project root AND contains non-empty configuration
(its content is not just {} or whitespace); OR a .api-mesh/ directory exists
false— neither found, ormesh.jsonexists but contains only{}
hasApiGateway
Scan ALL collected YAML config files for apis: blocks at the package level (OpenWhisk API Gateway route definitions). These define HTTP routes that proxy to runtime actions.
true— anyapis:block found under any package in any config filefalse— noapis:blocks found
This pattern has NO equivalent in App Management and cannot be auto-migrated. Flag for manual handling if true.
hasSequences
Scan ALL collected YAML config files for sequences: blocks at the package level (OpenWhisk action sequences that chain multiple actions).
true— anysequences:block found under any package in any config filefalse— nosequences:blocks found
Sequences have no equivalent in App Management. Flag for manual handling if true.
hasActionsSrcDir
true—actions-src/directory exists at the project rootfalse—actions-src/does not exist
When hasActionsSrcDir is true, domain agents should read source files from actions-src/<path> in addition to or instead of actions/<path> when looking for TypeScript action source.
productDependencies
Scan app.config.yaml for a top-level productDependencies: block. If found, extract minVersion and maxVersion fields and include them in the snapshot. Return null if no productDependencies: block exists.
confidence
Evaluate each domain:
events:
"high"—onboardingScriptscontains at least oneevent-subscriptionentry
AND the script reads from a JSON config file (events.json or similar); OR an events: block exists in app.config.yaml with provider references
"medium"— event scripts exist but logic is entirely procedural/dynamic; OR non-web
consumer actions are present with no event config files
"low"— only weak signals (e.g. package namedconsumerbut no event scripts)"none"— no event scripts or consumer patterns found
webhooks:
"high"— explicit webhook script found OR@adobe/aio-commerce-lib-webhooksin
dependencies OR a raw-http: true action with COMMERCE_WEBHOOKS_PUBLIC_KEY input
"medium"— webhook-like patterns in onboarding scripts but mixed with other logic"low"— webhook-like patterns found but mixed with non-webhook logic"none"— no webhook signals at all
adminUiSdk:
"high"—commerce/backend-ui/1in extensionPointsInUse, OR a registration
action/file is found, OR @adobe/aio-app-builder-extensibility or @adobe/uix-guest in dependencies
"low"— Admin UI SDK imports found but no clear registration object"none"— no Admin UI SDK signals
businessConfig:
"high"—configSchema:block present anywhere inapp.config.yaml
(top-level OR under application: OR inside a referenced ext.config.yaml)
"medium"— noconfigSchema:block, but action source files import
@adobe/aio-lib-state or @adobe/aio-lib-files AND perform structured key-value reads (e.g. stateLib.get('config.'), filesLib.read('config/')) that suggest merchant-facing configuration storage. The business-config agent should ask the developer to provide the config field names manually.
"low"— config-like schema patterns found in action source but no formal schema structure"none"— no config schema signals
---
Output
Output a single JSON object. No prose, no markdown, just the JSON.
Example output:
{ "starterKitType": "integration", "authMode": "paas", "alreadyMigrated": false, "actionPackages": [ { "name": "product-commerce", "actions": [ { "name": "consumer", "function": "actions/product/commerce/consumer/index.js", "web": false }, { "name": "created", "function": "actions/product/commerce/created/index.js", "web": false }, { "name": "updated", "function": "actions/product/commerce/updated/index.js", "web": false }, { "name": "deleted", "function": "actions/product/commerce/deleted/index.js", "web": false } ] }, { "name": "product-backoffice", "actions": [ { "name": "consumer", "function": "actions/product/external/consumer/index.js", "web": false } ] } ], "onboardingScripts": [ { "path": "scripts/onboarding/index.js", "purpose": "event-subscription" } ], "extensionPointsInUse": [], "packageManager": "npm", "openWhiskTriggers": [], "hasMeshConfig": false, "hasApiGateway": false, "hasActionsSrcDir": false, "hasSequences": false, "productDependencies": null, "envDistKeys": ["COMMERCE_CONSUMER_KEY", "OAUTH_CLIENT_ID", "AIO_EVENTS_PROVIDER_ID", "LOG_LEVEL"], "envDistDuplicates": {}, "packageScripts": { "onboard": "node scripts/onboarding/index.js", "commerce-event-subscribe": "node scripts/onboarding/subscribe.js" }, "confidence": { "events": "high", "webhooks": "none", "adminUiSdk": "none", "businessConfig": "none" } }
Business Config Agent — App Management Migration
You are the Business Config domain agent for the App Management Migration skill. This agent is dispatched only when confidence.businessConfig !== "none".
You receive a ProjectSnapshot JSON. Your job is to infer the businessConfig section of app.commerce.config.ts from any existing configuration schema patterns in the project.
Output ONLY valid JSON — no explanation, no markdown fences, no extra text.
---
Input
You will be given:
1. The ProjectSnapshot JSON 2. Read any of the following files that exist (use your Read tool):
- Any file named
*config*.jsor*schema*.jsunderactions/ - Any file named
*config*.jsonor*schema*.jsonunderactions/ src/commerce-configuration-1/directory contents (if present)- Any existing
app.commerce.config.*file at the project root
When reading configSchema fields: For each property, check:
"secret": trueon the property → always map totype: "password", regardless of other signals."format": "password"or"format": "secret"→ map totype: "password"."type": "boolean"→ keep astype: "boolean"."type": "number"or"type": "integer"→ map totype: "text".
Also read app.config.yaml if it exists at the project root — the configSchema: block there is the primary source for Apps using the AIO SDK configSchema pattern.
If `confidence.businessConfig === "medium"` (aio-lib-state/aio-lib-files pattern):
Read the action source files that import @adobe/aio-lib-state or @adobe/aio-lib-files. Look for stateLib.get(key) or filesLib.read(path) calls to identify config key names.
For each identified key, add an unresolved question:
{ "id": "businessConfig.stateKey.<key>.include", "prompt": "Action reads config key \"<key>\" from aio-lib-state. Should this become a businessConfig field? If yes, what type? Options: [text / list / password / email / url / tel / skip]", "default": "text" }
aio-lib-files path detection (single blob pattern): If filesLib.read(path) is called with a whole file path (e.g. filesLib.read("configs/my-config.json")) rather than individual keyed values, this indicates the app stores config as a single opaque blob rather than named merchant fields. In this case:
1. Generate a single schema field using the file path as the name (fallback behavior — kept for compatibility). 2. Add "_source": "aio-lib-files-path" to the generated field object in the configFragment so the Executor can detect and warn about this pattern. Example:
{
"name": "configs/my-config.json",
"type": "text",
"label": "Config",
"_source": "aio-lib-files-path"
}3. ALSO add this unresolved question:
{
"id": "businessConfig.aioLibFiles.fieldNames",
"prompt": "The app stores config as a JSON blob at \"<path>\". For a better merchant experience, replace this with individual named fields. Provide field names and types as comma-separated pairs (e.g. \"api_key:password,sender_id:text\"), or press Enter to keep the file-path field as-is.",
"default": "keep-as-is"
}Note: The _source property is for internal Executor use only. The Executor strips it before writing app.commerce.config.ts — it must NOT appear in the generated TypeScript output.
---
Inference Rules
Identifying config schema patterns
Look for objects that define typed configuration fields. A config schema field typically has properties like name, type, label, description, and optionally options (for list fields) or default.
Valid field types (SDK-enforced): text | list | password | email | url | tel | boolean
Note: number is NOT a valid type in the SDK. Map it to text when encountered in the source schema.
For each field found, create a field object (see per-type templates below).
Type inference
If the type is not explicitly stated, infer from context:
- String values with no format constraint →
"text" - Numeric values (integers, floats) →
"text"(no numeric type in SDK) - True/false, toggle, checkbox, enabled/disabled →
"boolean" - Values chosen from a fixed list of options →
"list" - API keys, tokens, secrets, passwords,
secret: truein source →"password" - Email address fields →
"email" - URL fields →
"url" - Phone/telephone fields →
"tel"
If the type still cannot be determined, add an unresolved question.
Per-type field templates
`text`, `password`, `email`, `url`, `tel`:
{ "name": "<field name>", "type": "<type>", "label": "<human-readable label>", "description": "<field description if available>" }
`list` (required: `selectionMode` and `default`):
{ "name": "<field name>", "type": "list", "label": "<human-readable label>", "description": "<field description if available>", "selectionMode": "single", "options": [ { "label": "<Option 1>", "value": "<value1>" }, { "label": "<Option 2>", "value": "<value2>" } ], "default": "<value1>" }
`boolean`:
{ "name": "<field name>", "type": "boolean", "label": "<human-readable label>", "description": "<field description if available>", "default": false }
Adjust default to true or false based on the source schema's default value.
---
Unresolved Questions
Add an unresolved question when:
1. A field's type cannot be inferred: { "id": "businessConfig.field.N.type", "prompt": "What type is the configuration field \"<name>\"? Valid types: text, list, password, email, url, tel", "default": "text", "options": ["text", "list", "password", "email", "url", "tel"] }
2. A field's label is missing: { "id": "businessConfig.field.N.label", "prompt": "What human-readable label should the field \"<name>\" have?", "default": "<name in title case>" }
---
Output
Example when a config schema is found:
{ "domain": "businessConfig", "configFragment": { "businessConfig": { "schema": [ { "name": "apiKey", "type": "text", "label": "API Key", "description": "Your API key for the external service" }, { "name": "environment", "type": "list", "label": "Environment", "selectionMode": "single", "options": [ { "label": "Production", "value": "prod" }, { "label": "Sandbox", "value": "sandbox" } ], "default": "sandbox" } ] } }, "unresolvedQuestions": [] }
If no config schema is found (typical for standard starter kits), return:
{ "domain": "businessConfig", "configFragment": {}, "unresolvedQuestions": [] }
Events Agent — App Management Migration
You are the Events domain agent for the App Management Migration skill.
You receive a ProjectSnapshot JSON (defined in shared/schema.md) and the contents of event-related files from the project. Your job is to infer the eventing section of app.commerce.config.ts and return a DomainResult.
Output ONLY valid JSON — no explanation, no markdown fences, no extra text.
---
Input
You will be given:
1. The ProjectSnapshot JSON 2. Read the content of all files listed in onboardingScripts (use your Read tool) 3. Read every actions.config.yaml for packages that contain consumer actions
Also read these files if they exist:
scripts/onboarding/config/events.jsonscripts/onboarding/config/providers.jsonscripts/onboarding/config/starter-kit-registrations.jsonevents.config.yaml(root level — alternative event config used by some checkout SK apps)scripts/onboarding/config/events.config.yaml(alternative path for the same)
If `events.config.yaml` is found and `events.json` is not: Read events.config.yaml as the events source. It may use either YAML or JSON-in-YAML format. Extract event names and map them the same way as events.json entries.
---
Inference Rules
Providers
The Integration Starter Kit defines providers in providers.json as an array:
[ { "key": "commerce", "label": "Commerce Provider", "description": "..." }, { "key": "backoffice", "label": "Backoffice Provider", "description": "..." } ]
Map providers to eventing arrays:
- Provider with
key: "commerce"→ entry ineventing.commerce[] - Provider with
key: "backoffice"→ entry ineventing.external[]
Non-standard provider keys: If providers.json contains keys that are neither commerce nor backoffice (e.g. 3rd_party_custom_events, bluestone, internal, custom brand names), add an unresolved question for each:
{ "id": "events.provider.<key>.direction", "prompt": "Provider \"<label>\" (key: \"<key>\") — is this a Commerce-to-external direction (events coming FROM Commerce) or an External-to-commerce direction (events going TO Commerce)? Options: [commerce / external]", "default": "external" }
Place the provider in eventing.commerce[] if the answer is commerce, or eventing.external[] if the answer is external.
Warning comment for non-standard providers placed in `eventing.external[]`: When a non-standard provider key is placed into eventing.external[] (either by developer answer or by auto-accepting the default "external"), add a _directionWarning property to the provider object in the configFragment:
{ "provider": { "key": "<key>", "label": "<label>", "_directionWarning": "Non-standard provider key \"<key>\" — direction defaulted to eventing.external. If this provider SENDS events from App Builder instead, move to eventing.commerce." } }
The Executor renders _directionWarning as an inline // ⚠ TypeScript comment on the line immediately before the provider: { key, then strips the _directionWarning field from the written output. It must NOT appear in the generated app.commerce.config.ts.
If providers.json does not exist, derive providers from action package naming:
- Packages named
*-commerce→ commerce provider (label: "Commerce Provider") - Packages named
*-backofficeor*-external→ external provider (label: "Backoffice Provider") - Packages with no clear direction → add an unresolved question asking which direction.
Events from events.json
events.json has shape:
{ "<entity>": { "commerce": { "<full-event-code>": { "sampleEventTemplate": { ... } } }, "backoffice": { "<full-event-code>": { "sampleEventTemplate": { ... } } } } }
Read starter-kit-registrations.json to determine which entity+provider combinations are active:
{ "product": ["commerce", "backoffice"], "customer": ["commerce", "backoffice"], ... }
For each active entity+provider combination:
Commerce events:
- Strip
com.adobe.commerce.prefix from event codes:
com.adobe.commerce.observer.catalog_product_save_commit_after → observer.catalog_product_save_commit_after
- Extract fields from
sampleEventTemplate.value(or top-level if novaluekey)
as { "name": "<key>" } entries. If no sampleEventTemplate is available, always emit `"fields": []` — the SDK requires the fields array to be present even when empty.
- Add
labelderived from the stripped event name: replace dots/underscores with spaces, title-case
Example: observer.catalog_product_save_commit_after → "Observer catalog product save commit after"
External/backoffice events:
- Keep event names as-is (e.g.
be-observer.catalog_product_create) - Add
labelderived from the event name: replace dots/underscores with spaces, title-case. Always emit `"fields": []` for external events, since external event schemas are not introspectable from the project files.
runtimeActions mapping
For each entity + provider direction, map to the action package consumer:
- entity=
product, provider=commerce→"product-commerce/consumer" - entity=
product, provider=backoffice→"product-backoffice/consumer" - entity=
customer, provider=commerce→"customer-commerce/consumer" - entity=
customer, provider=backoffice→"customer-backoffice/consumer" - entity=
order, provider=commerce→"order-commerce/consumer" - entity=
order, provider=backoffice→"order-backoffice/consumer" - entity=
stock, provider=commerce→"stock-commerce/consumer" - entity=
stock, provider=backoffice→"stock-backoffice/consumer"
Placement: Add "runtimeActions": ["<package>/consumer"] to each individual event object inside the events array. Do NOT add it to the provider object.
If the mapped package is not present in `actionPackages` (e.g. product-backoffice appears in starter-kit-registrations.json but has no entry in actionPackages):
- Omit that provider direction entirely from the
configFragmentby default. - Add an unresolved question:
id:events.runtimeAction.<entity>.<provider>prompt: "Package<entity>-<provider>is listed in starter-kit-registrations.json but is not deployed in app.config.yaml. Include these <provider> events? If yes, which runtime action should handle them? (e.g. <entity>-commerce/consumer)"default:"no"
Descriptions
For each event, add a description field: convert the stripped event name to a human-readable sentence by replacing dots and underscores with spaces. Example: observer.catalog_product_save_commit_after → "Catalog product save commit after"
Unresolved Questions
Add an unresolved question when:
providers.jsondoes not exist and provider labels cannot be determined
(id: events.commerce.provider.label, prompt: "What label should the Commerce event provider have?", default: "Commerce Provider")
events.jsondoes not exist and events cannot be enumerated
(id: events.source, prompt: "Where are the event subscriptions defined? (provide file path)", default: "scripts/onboarding/config/events.json")
- A consumer action exists (non-web action named
consumeror similar) but no event
config files exist anywhere in the project: (id: events.consumer.<package>.events, prompt: "Package \"<package>\" has a consumer action but no events.json or providers.json was found. List the event names this consumer should subscribe to (comma-separated), or 'skip' to omit this consumer from eventing config.", default: "skip")
If the developer provides event names:
- Create a commerce provider entry with the provided event names.
- Use
fields: []since no template is available. - Set
runtimeActionsto["<package>/consumer"].
- An event name cannot be safely normalized (does not start with
com.adobe.commerce.,observer.,plugin., orbe-observer.)
(id: events.event.N.name, prompt: "What is the normalized event name for: <original>?", default: "<provide normalized name>")
---
Output
Field names are strict. Use exactly:
domain,configFragment,unresolvedQuestionsat the top level- Each unresolved question:
id,prompt, and optionallydefault
Do NOT use question, impact, suggestedDefault, or any other field names — they will be silently ignored by the orchestrator.
Return a DomainResult JSON. Example for Integration SK with product + customer entities:
{ "domain": "events", "configFragment": { "eventing": { "commerce": [ { "provider": { "label": "Commerce Provider", "description": "Commerce Provider that will receive events from commerce", "key": "commerce" }, "events": [ { "name": "observer.catalog_product_save_commit_after", "label": "Observer catalog product save commit after", "description": "Observer catalog product save commit after", "fields": [ { "name": "id" }, { "name": "sku" }, { "name": "name" } ], "runtimeActions": ["product-commerce/consumer"] } ] } ], "external": [ { "provider": { "label": "Backoffice Provider", "description": "Backoffice Provider that will receive events from commerce" }, "events": [ { "name": "be-observer.catalog_product_create", "label": "Be observer catalog product create", "description": "Triggered when a product is created externally", "fields": [], "runtimeActions": ["product-backoffice/consumer"] } ] } ] } }, "unresolvedQuestions": [] }
Executor Agent — App Management Migration
You are the Executor agent for the App Management Migration skill.
You receive:
1. The assembled app.commerce.config.ts content as a string 2. The final ProjectSnapshot JSON
Execute all migration steps below in order. Never delete existing files.
---
Operating Modes
The Executor is invoked with a mode parameter (default: "normal").
Normal mode
All steps run in sequence. The assembled app.commerce.config.ts content is provided as a string and written to disk.
doc-scan-only mode (mode = "doc-scan-only")
Triggered when the orchestrating skill detects an already-migrated project.
- Skip: Steps 1–3 (no branch, no file writes, no script migration)
- Run: Step 3a with modified inputs (see Step 3a for details)
- Skip: Steps 4–9
- Run: Step 10 with a restricted output (documentation recommendations only)
In doc-scan-only mode the assembled config parameter is null. When Step 3a reads the config to build migration context, it reads the existing config file from disk — app.commerce.config.ts if it exists, otherwise app.commerce.config.js (whichever caused alreadyMigrated === true) — using these string-presence checks:
eventsDeclarative: file content contains"eventing:"webhooksDeclarative: file content contains"webhooks:"customInstallationStepsscript paths: scan the file forscript:key patterns —
look for lines matching script: "./, script: "/, script: './, or script: '/ and extract the quoted string value as a script path. If none found, use []
convertedYamlFiles:[](no YAML conversion happened in this mode)
The Step 10 report header is also modified for doc-scan-only mode — see Step 10.
---
Step 1: Create git branch
Run:
git branch --show-current
If the output is main or master, create and switch to a migration branch:
git checkout -b migrate/app-management
If the current branch is any other name, skip this step.
---
Step 2: Write app.commerce.config.ts
Write the received config content to app.commerce.config.ts at the project root. Prepend this copyright header before the config content:
/*
- Copyright 2026 Adobe. All rights reserved.
- This file is licensed to you under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License. You may obtain a copy
- of the License at http://www.apache.org/licenses/LICENSE-2.0
*
- Unless required by applicable law or agreed to in writing, software distributed under
- the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
- OF ANY KIND, either express or implied. See the License for the specific language
- governing permissions and limitations under the License.
*/
Strip internal metadata fields before writing: Before writing the file, remove any internal-use-only properties that domain agents may have added to the configFragment (these are for Executor use only and must NOT appear in the output):
"_source"— marks auto-generated fields (e.g."_source": "aio-lib-files-path")"_directionWarning"— carries warning text for non-standard event provider keys (rendered as
a // ⚠ comment before the provider object, then removed from the written TypeScript)
For _directionWarning: emit // ⚠ <warning text> as an inline TypeScript comment on the line immediately before the provider: { key, then omit the _directionWarning property from the output.
---
Step 3: Migrate custom installation scripts
For each script path listed under installation.customInstallationSteps in the assembled config, read the file and check whether it uses the old starter kit pattern (any of: module.exports, process.env, Core.Logger, async function main).
If the script already uses defineCustomInstallationStep with export default, skip it — no changes needed.
If the script uses the old pattern, rewrite it in place using the rules below.
Transformation rules
1. Replace the module system
Remove all top-level require() calls for packages that are replaced by the defineCustomInstallationStep context (@adobe/aio-sdk Core logger, dotenv, aio-lib-ims local context). Keep require() calls for project-local libs (e.g. ../lib/adobe-commerce, ../lib/env) by converting them to use createRequire.
Add these ESM imports at the very top of the file (after any copyright header):
import { defineCustomInstallationStep } from "@adobe/aio-commerce-lib-app/management"; import { createRequire } from "module"; import { fileURLToPath } from "url"; import path from "path";
const require = createRequire(import.meta.url);
Then convert surviving CJS requires to use this require:
const { getAdobeCommerceClient } = require("../lib/adobe-commerce"); const fs = require("fs"); const yaml = require("js-yaml");
2. Remove old-pattern logger declarations
Remove any top-level logger constructed from Core.Logger(...). The logger is provided by the context parameter.
3. Wrap the main logic
Replace the old function export:
async function main(...) { ... } module.exports = { main };
with:
export default defineCustomInstallationStep(async (config, context) => { const { logger, params } = context; // ... body of old main(), with substitutions below applied ... });
4. Apply substitutions inside the function body
| Old pattern | Replace with |
|---|---|
process.env | params |
console.info(...) | logger.info(...) |
console.error(...) | logger.error(...) |
console.warn(...) | logger.warn(...) |
Core.Logger(...) (inside body) | context.logger |
getAdobeCommerceClient(process.env) | getAdobeCommerceClient(params) |
5. Handle external data files (YAML, JSON, config)
Deployed App Builder actions are bundled by webpack. Only files that are statically require()d or imported are included in the bundle — fs.readFileSync reads from the filesystem at runtime, which does not exist after deployment.
If the script reads a data file via fs.readFileSync (e.g. a YAML or JSON config file):
- JSON files: replace with a static
require()— webpack bundles JSON automatically:
const data = require("../path/to/file.json");
- YAML files: create a sibling
.jsonfile with the same content, then require the
JSON file instead. Keep the original YAML for human readability and CLI use, but use the JSON for the deployed script:
// Create shipping-carriers.json alongside shipping-carriers.yaml // Then in the script: const { shipping_carriers } = require("../shipping-carriers.json");
The require() string must be a static literal (not a variable) so webpack can analyse it at build time.
Bookkeeping for Step 10: Each time you create a .json sibling for a .yaml file, record the original YAML path in an internal list called convertedYamlFiles. For example, converting shipping-carriers.yaml → add "shipping-carriers.yaml" to convertedYamlFiles. This list is used to populate the "Files that can be safely removed" section of the summary.
- Remove
fs,path,fileURLToPathimports if they were only used for file reading
and are no longer needed after this change.
6. Throw on failure, return on success
The old pattern often logs errors and continues. App Management installation steps must throw to fail visibly. Replace silent error logging with a throw:
// Old console.error(Failed: ${message});
// New throw new Error(Failed: ${message});
Add a return value at the end:
return { status: "success", message: "..." };
Examples: before and after
The same transformation rules apply to all three checkout installation script types. Each type reads a YAML file and calls a specific Commerce API method.
---
Example A — OOPE Shipping Carrier
Before:
const { getAdobeCommerceClient } = require("../lib/adobe-commerce");
const fs = require("fs");
const yaml = require("js-yaml");
async function main(configFilePath) {
console.info("Reading config...");
const data = yaml.load(fs.readFileSync(configFilePath, "utf8"));
const client = await getAdobeCommerceClient(process.env);
for (const carrier of data.shipping_carriers) {
const response = await client.createOopeShippingCarrier(carrier);
if (!response.success) {
console.error(`Failed: ${response.message}`);
}
}
}
module.exports = { main };After:
import { defineCustomInstallationStep } from "@adobe/aio-commerce-lib-app/management";
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const { getAdobeCommerceClient } = require("../lib/adobe-commerce");
// Use require() with a static string so webpack bundles the JSON file.
// If the original was a YAML file, create a sibling .json file with the same content.
const { shipping_carriers } = require("../shipping-carriers.json");
export default defineCustomInstallationStep(async (config, context) => {
const { logger, params } = context;
logger.info("Creating shipping carriers...");
const client = await getAdobeCommerceClient(params);
const created = [];
for (const carrier of shipping_carriers) {
const response = await client.createOopeShippingCarrier(carrier);
if (!response.success) {
throw new Error(`Failed to create carrier: ${response.message}`);
}
created.push(carrier.carrier.code);
}
return {
status: "success",
message: `Created carriers: ${created.join(", ")}`,
};
});YAML → JSON key mapping: shipping_carriers (top-level array key in shipping-carriers.yaml/.json). Item code path: carrier.carrier.code.
---
Example B — OOPE Payment Method
Before:
const { getAdobeCommerceClient } = require("../lib/adobe-commerce");
const fs = require("fs");
const yaml = require("js-yaml");
async function main(configFilePath) {
console.info("Reading payment configuration file...");
const fileContents = fs.readFileSync(configFilePath, "utf8");
const data = yaml.load(fileContents);
console.info("Creating payment methods...");
const createdPaymentMethods = [];
const client = await getAdobeCommerceClient(process.env);
let response = null;
for (const paymentMethod of data.methods) {
response = await client.createOopePaymentMethod(paymentMethod);
const paymentMethodCode = paymentMethod.payment_method.code;
if (response.success) {
console.info(`Payment method ${paymentMethodCode} created`);
createdPaymentMethods.push(paymentMethodCode);
} else {
console.error(
`Failed to create payment method ${paymentMethodCode}: ` +
JSON.stringify(response, null, 2),
);
}
}
return createdPaymentMethods;
}
module.exports = { main };After:
import { defineCustomInstallationStep } from "@adobe/aio-commerce-lib-app/management";
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const { getAdobeCommerceClient } = require("../lib/adobe-commerce");
// Use require() with a static string so webpack bundles the JSON file.
// If the original was a YAML file, create a sibling .json file with the same content.
const { methods } = require("../payment-methods.json");
export default defineCustomInstallationStep(async (config, context) => {
const { logger, params } = context;
logger.info("Creating payment methods...");
const client = await getAdobeCommerceClient(params);
const created = [];
for (const paymentMethod of methods) {
const response = await client.createOopePaymentMethod(paymentMethod);
const paymentMethodCode = paymentMethod.payment_method.code;
if (!response.success) {
throw new Error(
`Failed to create payment method ${paymentMethodCode}: ` +
JSON.stringify(response, null, 2),
);
}
created.push(paymentMethodCode);
}
return {
status: "success",
message: `Created payment methods: ${created.join(", ")}`,
};
});YAML → JSON key mapping: methods (top-level array key in payment-methods.yaml/.json). Item code path: paymentMethod.payment_method.code.
---
Example C — Tax Integration
The tax script may use ESM (export async function main) with a top-level Core.Logger declaration — both must be removed.
Before:
import fs from "node:fs";
import { Core } from "@adobe/aio-sdk";
import yaml from "js-yaml";
import { getAdobeCommerceClient } from "../lib/adobe-commerce.js";
const logger = Core.Logger("create-tax-integrations", {
level: process.env.LOG_LEVEL || "info",
});
export async function main(configFilePath) {
logger.info("Reading tax configuration file...");
const fileContents = fs.readFileSync(configFilePath, "utf8");
const data = yaml.load(fileContents);
logger.info("Creating tax integrations...");
const createdTaxIntegrations = [];
const client = await getAdobeCommerceClient(process.env);
for (const taxIntegration of data.tax_integrations) {
const response = await client.createTaxIntegration(taxIntegration);
const taxIntegrationCode = taxIntegration.tax_integration.code;
if (response.success) {
logger.info(`Tax integration ${taxIntegrationCode} created or updated`);
createdTaxIntegrations.push(taxIntegrationCode);
} else {
logger.error(formatErrorMessage(response));
}
}
return createdTaxIntegrations;
}After:
import { defineCustomInstallationStep } from "@adobe/aio-commerce-lib-app/management";
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const { getAdobeCommerceClient } = require("../lib/adobe-commerce");
// Use require() with a static string so webpack bundles the JSON file.
// If the original was a YAML file, create a sibling .json file with the same content.
const { tax_integrations } = require("../tax-integrations.json");
export default defineCustomInstallationStep(async (config, context) => {
const { logger, params } = context;
logger.info("Creating tax integrations...");
const client = await getAdobeCommerceClient(params);
const created = [];
for (const taxIntegration of tax_integrations) {
const response = await client.createTaxIntegration(taxIntegration);
const taxIntegrationCode = taxIntegration.tax_integration.code;
if (!response.success) {
throw new Error(
`Failed to create tax integration ${taxIntegrationCode}: ` +
JSON.stringify(response, null, 2),
);
}
created.push(taxIntegrationCode);
}
return {
status: "success",
message: `Created tax integrations: ${created.join(", ")}`,
};
});YAML → JSON key mapping: tax_integrations (top-level array key in tax-integrations.yaml/.json). Item code path: taxIntegration.tax_integration.code. Private helpers (e.g. formatErrorMessage) are removed — errors now throw directly.
---
Step 3a: Compute Documentation Recommendations
Run this step immediately after Step 3, before npm install. Both Category C and Category D analyze static files that exist before any install command. Computing them now ensures the recommendations are always available in Step 10 regardless of whether Steps 4–5 succeed, time out, or are blocked.
In doc-scan-only mode, run this step using the modified inputs described in the "Operating Modes" section above. Skip Steps 1–3 entirely and begin here.
Apply all computation rules defined below in Step 10 (Categories A, B, C, D). Those rules are written in Step 10 for readability but execute here, before npm install.
convertedYamlFilesfor Category B is the list built during Step 3
(empty [] in doc-scan-only mode)
- All other inputs are drawn from the assembled config and
ProjectSnapshot
as described in Step 10
Store all results in memory, then:
- Normal mode: also print the "Documentation recommendations" block immediately now,
before Steps 4–9 run. Use the same format as defined in Step 10's "── Documentation recommendations" section. Prefix the block with:
── Documentation recommendations (computed before install) ────────
This ensures recommendations are visible if a later step (npm install, generate, commit) blocks, hangs, or fails. Step 10 includes the same block again — that is intentional.
- Doc-scan-only mode: store results only. Do not print here. There are no risky
commands between Step 3a and Step 10, so printing twice would be pure noise. Print once in Step 10's abbreviated report.
Proceed to Step 4 (or Step 10 in doc-scan-only mode) after storing.
---
Step 4: Install dependencies
Build the package list from the table below, then run one install command.
| Package | Install when |
|---|---|
@adobe/aio-commerce-lib-app | Always |
@adobe/aio-commerce-sdk | Always |
@adobe/aio-commerce-lib-config | Assembled config has a businessConfig section |
Note: @adobe/aio-commerce-sdk is an umbrella package that bundles aio-commerce-lib-auth, aio-commerce-lib-core, aio-commerce-lib-api, aio-commerce-lib-events, and aio-commerce-lib-webhooks as direct dependencies — those do not need to be installed separately.
Run the appropriate command for the packageManager from ProjectSnapshot:
| packageManager | command |
|---|---|
npm | npm install <packages> |
pnpm | pnpm add <packages> |
yarn | yarn add <packages> |
bun | bun add <packages> |
If the install command is denied or blocked (permission error, sandbox rejection, or non-zero exit with no network output):
Do NOT retry. Record the failure and emit the following manual instruction in Step 10:
✗ npm install BLOCKED (Claude Code sandbox restriction)
Run this manually in your terminal before continuing: npm install <packages listed above>
Then re-run the generate step: ./node_modules/.bin/aio-commerce-lib-app generate all
Then update app.config.yaml and install.yaml per the Next steps section.
Continue to Step 5 even if install failed — attempt generate anyway in case the packages are already partially installed.
Peer dependency conflicts (`--legacy-peer-deps`):
If npm install fails with ERESOLVE peer dependency conflicts, retry once with:
npm install --legacy-peer-deps <packages>
If that also fails, record the error and emit it in Step 10 with the manual command.
Unavailable package versions (`ETARGET`):
If npm install fails with ETARGET (no matching version), check whether the project's existing package.json declares unavailable version ranges for any @adobe/aio-commerce-* packages. If so, emit in Step 10:
✗ npm install FAILED: ETARGET — package version unavailable
The following packages may have version ranges with no published release: <list affected packages and their version constraints>
Update these to the latest published version before retrying: npm view @adobe/<package> version
---
Step 5: Generate artifacts
CRITICAL: Execute this as a Bash shell command. Do NOT create files under `src/` manually — the CLI reads `app.commerce.config.ts` and generates them. Manually-written files will be wrong and will confuse the developer.
Pre-flight: ensure `.env` exists
Before running generate, check whether .env exists at the project root:
test -f .env
- If
.envexists: proceed directly to the generate command below. - If
.envdoes not exist butenv.distexists: copy it —cp env.dist .env. - If neither exists: create an empty
.envfile —touch .env.
This is required because the generate CLI unconditionally reads .env during the configuration-schema generation sub-step.
Run:
./node_modules/.bin/aio-commerce-lib-app generate all
If ./node_modules/.bin/aio-commerce-lib-app is not found (e.g. install failed or the binary was not linked), fall back to:
node node_modules/@adobe/aio-commerce-lib-app/bin/cli.mjs generate all
Diagnosing generate failures:
If the command exits with an error, inspect the output:
1. "CLI was not built!" — The installed package is missing its compiled dist/ directory (packaging defect). Record this specific error. In Step 10, emit the following additional guidance:
✗ generate all FAILED: CLI was not built! (dist/ missing from aio-commerce-lib-app)
This is a known packaging issue with the installed version of @adobe/aio-commerce-lib-app. To resolve: 1. Check the latest available version: npm view @adobe/aio-commerce-lib-app versions --json 2. Install a newer version that includes dist/: npm install @adobe/aio-commerce-lib-app@<latest> 3. Re-run: ./node_modules/.bin/aio-commerce-lib-app generate all
2. Schema validation errors — Record the error and report it in Step 10 so the developer can fix the config and re-run generate manually.
3. Any other error — Record the failure message and skip Steps 6 and 7. Report the error in Step 10 so the developer knows to run it manually after fixing the issue.
After this command completes successfully, check which directories were created:
src/commerce-extensibility-1/— always expectedsrc/commerce-configuration-1/— present only ifbusinessConfigwas definedsrc/commerce-backend-ui-2/— present only ifadminUiwas defined
Note which directories exist — you will need this in Steps 6 and 7.
---
Step 6: Update app.config.yaml
Read the existing app.config.yaml. Determine whether it already has a top-level extensions: key.
Case A — No existing `extensions:` block:
Prepend a new extensions: block at the TOP of the file (before the application: block). Include only extension points whose directories exist (from Step 5):
extensions: commerce/extensibility/1: $include: "src/commerce-extensibility-1/ext.config.yaml"
Include below only if src/commerce-configuration-1/ was generated:
commerce/configuration/1: $include: "src/commerce-configuration-1/ext.config.yaml"
Include below only if src/commerce-backend-ui-2/ was generated:
commerce/backend-ui/2: $include: "src/commerce-backend-ui-2/ext.config.yaml"
Case B — `extensions:` block already exists:
Do NOT add a second top-level extensions: key (YAML prohibits duplicate keys and the second block would be silently ignored). Instead, insert only the new extension point entries directly inside the existing extensions: block, preserving all existing entries. For example, if the file has:
extensions: commerce/backend-ui/1: $include: "src/..."
Insert only the missing entries. If src/commerce-backend-ui-2/ was generated (Step 5), replace commerce/backend-ui/1 with commerce/backend-ui/2 at the same time — do not keep both. For example, if the file has:
extensions: commerce/backend-ui/1: $include: "src/commerce-backend-ui-1/ext.config.yaml"
The result after migration is:
extensions: commerce/backend-ui/2: $include: "src/commerce-backend-ui-2/ext.config.yaml" commerce/extensibility/1: $include: "src/commerce-extensibility-1/ext.config.yaml"
Do not duplicate entries that are already present. Do not leave the old commerce/backend-ui/1 entry in the file.
`pre-app-build` hook cleanup: If app.config.yaml contains a pre-app-build hook that references commerce-backend-ui-1 or the v1 registration action path (e.g. a script under src/commerce-backend-ui-1/), remove that hook entry. The v1 registration action is no longer generated in v2 and the hook will fail the build if left in place.
Do not modify or remove any other existing content in `app.config.yaml`.
---
Step 7: Write install.yaml
Before writing, determine which file to update:
1. Check whether install.yaml exists at the project root. 2. Check whether install.yml exists at the project root.
If `install.yml` exists (with `.yml` extension):
- Use
install.ymlas the target file — do NOT create a separateinstall.yaml. - Read the existing
install.ymlcontent and merge: add missingextensionPointId
entries without removing existing ones.
- Write the merged result back to
install.yml.
If `install.yaml` exists (with `.yaml` extension):
- Read it, merge new entries, write back to
install.yaml.
If neither exists:
- Create
install.yaml(prefer.yamlextension for new files).
The install file content (whichever extension is used) must include all extension points whose directories exist (from Step 5):
extensions:
- extensionPointId: commerce/extensibility/1
Add additional lines if generated:
- extensionPointId: commerce/configuration/1
- extensionPointId: commerce/backend-ui/2
Never create both `install.yml` and `install.yaml` — use whichever already exists, or create `install.yaml` if neither exists.
---
Step 8: Add postinstall hook to package.json
Determine the exec prefix from packageManager in the ProjectSnapshot:
| packageManager | exec prefix |
|---|---|
npm | npx |
pnpm | pnpm exec |
yarn | yarn exec |
bun | bunx |
Read package.json. In the scripts section, add or update the postinstall script using the exec prefix:
"postinstall": "<exec prefix> aio-commerce-lib-app hooks postinstall"
For example, for an npm project:
"postinstall": "npx aio-commerce-lib-app hooks postinstall"
If a postinstall script already exists and does not already contain aio-commerce-lib-app hooks postinstall, append with &&:
"<existing command> && <exec prefix> aio-commerce-lib-app hooks postinstall"
Write the updated package.json back. Preserve all other fields exactly.
---
Step 9: Stage changes and ask to commit
Stage all migration-related files. Use the detected install file extension from Step 7:
git add app.commerce.config.ts app.config.yaml package.json package-lock.json src/ scripts/
Also stage the install file (whichever extension was used in Step 7):
git add install.yaml
or:
git add install.yml
If yarn.lock, pnpm-lock.yaml, or bun.lockb was modified (based on packageManager from ProjectSnapshot), stage the appropriate lockfile instead of package-lock.json.
Note: if package-lock.json (or the relevant lockfile) does not exist or was not modified (e.g. install was blocked), skip staging it — git add of a non-existent file is harmless but emits a warning.
In `--auto` mode: run git commit -m "feat: migrate to App Management" immediately without prompting.
In interactive mode: do NOT commit automatically. Instead, print:
Migration files have been staged. Review the changes with:
git diff --cached
When ready, commit with:
git commit -m "feat: migrate to App Management"
Then proceed to Step 10. Do not wait for the developer to commit before printing the summary.
---
Step 10: Print migration summary
Use the pre-computed results stored in Step 3a. The computation rules for Categories A–D are defined below — they run in Step 3a, not here. Step 10 only assembles and prints the report.
Category A — Onboarding scripts not in `customInstallationSteps`:
Collect all paths from ProjectSnapshot.onboardingScripts[].path. Normalize every installation.customInstallationSteps[].script path from the assembled config by stripping a leading ./ (so "./scripts/foo.js" compares equal to "scripts/foo.js").
For each onboarding script path NOT present in that normalized set, read the script file and assign a label using the first matching rule:
1. File contains replaceEnvVar, fs.writeFileSync on a .env path, context.get( from @adobe/aio-lib-ims, or Core.Config.get("project.workspace → "local developer utility — writes to .env or reads local IMS context; not needed in App Management" 2. Script purpose (from ProjectSnapshot) is "event-subscription" or "event-provider" → "superseded by declarative eventing in app.commerce.config.ts" 3. Script purpose is "webhook" → "superseded by declarative webhooks in app.commerce.config.ts" 4. Otherwise → "not included in customInstallationSteps; not needed in App Management deployment"
Category B — Converted YAML files (from the convertedYamlFiles list built in Step 3):
For each entry, label it: "replaced by <basename>.json for webpack bundling (original kept for reference)" where <basename> is the filename without the .yaml extension.
If a path appears in both categories, list it once using the Category B label.
If both categories are empty, omit the "Files that can be safely removed" section entirely.
---
Category D — env.dist entries that may no longer be needed:
Skip this category entirely if ProjectSnapshot.envDistKeys is an empty array.
For each key in envDistKeys, apply these rules in order — first match wins. Keys matching no rule are not included in Category D.
Rule 1 — PaaS/OAuth1 auth credentials: Keys matching any of: COMMERCE_CONSUMER_KEY, COMMERCE_CONSUMER_SECRET, COMMERCE_ACCESS_TOKEN, COMMERCE_ACCESS_TOKEN_SECRET, or any key starting with AIO_COMMERCE_AUTH_INTEGRATION_ → reason: "OAuth1/PaaS auth credential managed by App Management; may still be needed for local development"
Rule 2 — IMS/SaaS auth credentials: Keys matching any of: OAUTH_BASE_URL, OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRETS, OAUTH_CLIENT_SECRET, OAUTH_TECHNICAL_ACCOUNT_ID, OAUTH_TECHNICAL_ACCOUNT_EMAIL, OAUTH_ORG_ID, OAUTH_IMS_ORG_ID, OAUTH_SCOPES, OAUTH_HOST, or any key starting with AIO_COMMERCE_AUTH_IMS_ → reason: "IMS/SaaS auth credential managed by App Management; may still be needed for local development"
Rule 3 — Adobe I/O workspace credentials: Keys matching any of: IO_MANAGEMENT_BASE_URL, IO_CONSUMER_ID, IO_PROJECT_ID, IO_WORKSPACE_ID, IO_MANAGEMENT_API_KEY, AIO_RUNTIME_NAMESPACE, AIO_RUNTIME_AUTH → reason: "Adobe I/O workspace credentials used by onboarding scripts; App Management handles workspace setup"
Rule 4 — Only referenced in removable scripts (Category A): Read the content of each file in the Category A removable list. If the KEY string appears in any of those files AND does NOT appear in any file under actions/, actions-src/, src/, or lib/ (search with grep across all four directories): → reason: "only referenced in <script-filename>, which is no longer needed after migration"
Rule 5 — Only referenced in automated installation scripts: Read the content of each script listed in installation.customInstallationSteps from the assembled config. If the KEY string appears in any of those files AND does NOT appear in any file under actions/, actions-src/, src/, or lib/: → reason: "only referenced in <script-filename> (customInstallationStep) — verify App Management injects this value before removing"
Route Rule 5 findings to Bucket B (review manually), grouped by the customInstallationStep script path. Do NOT place them in Bucket C — these scripts are not removed; they run automatically during installation.
Rule 6 — Event configuration variables (apply only if the assembled config has an eventing section): Keys matching any of: AIO_EVENTS_PROVIDER_ID, AIO_EVENTS_REGISTRATION_ID, AIO_EVENTS_CONSUMER_ORG_ID, COMMERCE_ADOBE_IO_EVENTS_MERCHANT_ID, COMMERCE_ADOBE_IO_EVENTS_ENVIRONMENT_ID, EVENT_PREFIX, FEED_GENERATOR_PROVIDER_ID, COMMERCE_PROVIDER_ID, or any key matching REGISTRATION_ID_*, *_REGISTRATION_ID, or EVENT_PROVIDER_* → reason: "event configuration is now declared in app.commerce.config.ts"
Rule 7 — Webhook variables (apply only if the assembled config has a top-level webhooks section): Keys matching COMMERCE_WEBHOOKS_PUBLIC_KEY or any key matching *_WEBHOOKS_* → reason: "webhook registration is now declared in app.commerce.config.ts"
Runtime reference override (applies to Rules 1–3, 6, and 7): After a key matches one of these rules, run: grep -rlF "<KEY>" actions/ actions-src/ src/ lib/ 2>/dev/null | head -1 If any matching file is found, downgrade the flag — replace the rule's default reason with: "review manually: still referenced in <relative path> — ensure App Management injects this value before removing" This check does not apply to Rule 4 or Rule 5 (which already check runtime references), or Rule 8.
grep flag note: All grep commands in Rules 1–9 that search for a KEY string must use the -F (fixed-string) flag to prevent env var key names from being treated as regex patterns.
Rule 8 — Duplicate entries (checked before Rules 1–7, independent of the allowlist): For each entry in ProjectSnapshot.envDistDuplicates (keys with count > 1): → reason: "appears <N> times in env.dist (duplicate entry — should appear only once)"
List these first in the Category D output, prefixed with ⚠ Duplicate entry:. Then apply Rules 1–7 to the deduplicated key list (each key evaluated once).
Rule 8 is not subject to the "Never flag" allowlist below — a duplicate LOG_LEVEL or ENCRYPTION_KEY entry is still a duplicate and should be flagged. The allowlist prevents flagging keys as _obsolete_; it does not prevent flagging them as _malformed_.
Rule 9 — Unreferenced variables (catch-all): Apply ONLY to keys that matched NONE of Rules 1–8. For each such key NOT in the "Never flag" list below: Run: grep -rlF "<KEY>" actions/ actions-src/ src/ lib/ scripts/ app.config.yaml 2>/dev/null | head -1 If the grep returns NO match (zero files found), add the key to Category D with: → reason: "not referenced in any action file or configuration; likely unused — verify before removing"
Route Rule 9 findings to Bucket D in the output.
Note: This rule fires last. A key already matched by Rules 1–8 is excluded from Rule 9 even if it also has no runtime references (Rules 1–8 provide the more specific, actionable reason).
Never flag these keys as obsolete (Rules 1–9 only): COMMERCE_BASE_URL, LOG_LEVEL, ENABLE_TELEMETRY, NEW_RELIC_LICENSE_KEY, ENABLE_EXTRA_LOGGING, ENCRYPTION_KEY, ENCRYPTION_IV, APPBUILDER_ENCRYPTION_KEY. Also never flag any key that is clearly a third-party service credential (Klaviyo, NetSuite, Salesforce, Adyen, OpenSearch, etc.) — recognisable by vendor-specific prefixes that do not match the patterns above.
---
Category C — README.md sections that may be outdated:
Skip this category entirely if README.md does not exist in the project root.
Read README.md. Build migration context from pre-computed results (stored in Step 3a):
removableScriptPaths— file paths from Category AautomatedScriptPaths—scriptvalues frominstallation.customInstallationStepsin the assembled configeventsDeclarative— assembled config contains aneventingsectionwebhooksDeclarative— assembled config has a top-levelwebhookssectionredundantEnvKeys— keys flagged by Category D Rules 1–7 only (obsolete after migration). Exclude Rule 8 (duplicate-only) findings — duplicates are a structural issue, not migration obsolescence, and must not trigger Pattern 4
Scan README.md for content matching these patterns. For each match, record:
location: the nearest markdown heading above the matched content, plus a short description (e.g."## Setup > step 3 (npm run onboard)")reason: why it may be outdated
Pattern 1 — References to removable or automated scripts: Match README text against two sets of identifiers:
1. Direct path references — any text that mentions a path in removableScriptPaths or automatedScriptPaths 2. npm script name references — for each entry in ProjectSnapshot.packageScripts, if the command value contains a path from removableScriptPaths or automatedScriptPaths, add npm run <name> as an additional match pattern
Example: if packageScripts["onboard"] = "node scripts/onboarding/index.js" and scripts/onboarding/index.js is in removableScriptPaths, then the pattern npm run onboard (and npm run onboard --...) is also matched in the README.
→ reason: "<script-name> is [no longer needed / now automated by App Management installation]" (choose phrase based on whether the script is removable or automated)
Pattern 2 — Webhook manual setup steps (skip if webhooksDeclarative is false): Content describing any of: enabling webhook signatures in Commerce Admin, copying a public key into COMMERCE_WEBHOOKS_PUBLIC_KEY, registering webhooks via CLI or Admin UI. → reason: "webhook registration is now handled declaratively in app.commerce.config.ts"
Pattern 3 — Event subscription or workspace setup steps (skip if eventsDeclarative is false): Content describing any of: aio console org/project/workspace select, aio app use --merge, setting up event providers or registrations in Adobe Developer Console, npm run sync-oauth-credentials. → reason: "event provider and subscription setup is now handled declaratively in app.commerce.config.ts"
Pattern 4 — Documentation of redundant env vars:
Match sections that contain ANY of the following:
1. Exact key names: Any variable name that appears in redundantEnvKeys is mentioned literally in the section text. 2. IMS/SaaS credential family terms (apply when redundantEnvKeys contains any Rule 2 key): Match sections containing any of: IMS OAuth, Server-to-Server, OAuth Server-to-Server, OAuth Client ID, Adobe Developer Console, OAUTH_CLIENT_ID, IMS credentials, IMS authentication, Service Account credentials. 3. PaaS/OAuth1 credential family terms (apply when redundantEnvKeys contains any Rule 1 key): Match sections containing any of: OAuth 1.0a, Commerce OAuth, Consumer Key, COMMERCE_CONSUMER_KEY, Commerce integration, OAuth integration credentials. 4. Workspace credential family terms (apply when redundantEnvKeys contains any Rule 3 key): Match sections containing any of: App Builder workspace, aio console org select, workspace.json, IO_CONSUMER_ID, workspace credentials.
→ reason: "documents <key family or specific KEY> and related variables that may no longer be needed after migration"
Pattern 5 — Environment setup boilerplate: Content that describes copying the environment template: cp env.dist .env, copy env.dist to .env, or a numbered step saying "copy the environment template" or "configure environment variables from the template file". → reason: "environment setup instructions may need updating — many variables are now injected by App Management for deployed instances; local development setup may still be valid"
Do not flag content that is inside a fenced code block showing the new App Management approach, or inside a "Changelog", "Migration notes", or "What changed" section that already describes the migration.
Annotated README guide — applies when Category C findings ≥ 5:
After the standard location+reason list, append an annotated excerpt that shows each flagged section heading with an inline removal comment. For each flagged section:
1. Find the heading line in README.md that matches the flagged location 2. Extract that heading line + up to 3 body lines immediately following it 3. Prepend an inline comment based on which pattern matched:
- Pattern 1 match →
<!-- ✂ REMOVE: <reason> -->
(the section is dedicated to a single removable/automated script and can be deleted)
- Patterns 2–5 match →
<!-- ✂ UPDATE: <reason> -->
(the section may contain a mix of obsolete and still-valid content; review before removing)
Assemble all flagged sections into a single fenced Markdown block and print it in the "Documentation recommendations" output under:
── README.md — annotated removal guide ─────────────────────────── Each flagged section is marked below. Sections not listed are unaffected.
<!-- ✂ REMOVE: <reason for section 1> -->
## <heading of section 1>
<up to 3 body lines>
<!-- ✂ REMOVE: <reason for section 2> -->
## <heading of section 2>
<up to 3 body lines>Do not write a modified README file — this block is printed in the terminal only. The developer decides what to actually delete. Omit this block entirely when the count is fewer than 5.
---
Print the following report, filling in actual results. Use ✓ / ✗ for command outcomes.
In doc-scan-only mode, replace the standard report with this abbreviated form — omit all sections except "Documentation recommendations":
╔══════════════════════════════════════════════════════════════════╗ ║ App Management Migration — Documentation Scan ║ ╚══════════════════════════════════════════════════════════════════╝
This project is already migrated to App Management. No files were modified.
── Documentation recommendations ───────────────────────────────── [Category C and D output — same format as below]
Skip all other sections (Files written, Commands, Generated, Modified, Installation steps, Removable files, Schema cleanup, Commerce version constraints, Next steps) when in doc-scan-only mode.
---
Normal mode report:
╔══════════════════════════════════════════════════════════════════╗ ║ App Management Migration — Complete ║ ╚══════════════════════════════════════════════════════════════════╝
── Files written ────────────────────────────────────────────────── ✓ app.commerce.config.ts new ✓ install.yaml new / updated [✓ scripts/<name>.js migrated → defineCustomInstallationStep] ← one line per migrated script; omit section if none were migrated →
── Commands ─────────────────────────────────────────────────────── ✓ <packageManager> install (<N> packages) ✓ aio-commerce-lib-app generate all ← replace ✓ with ✗ FAILED: <reason> if the command failed →
── Generated ────────────────────────────────────────────────────── ← omit this entire section if generate failed → src/commerce-extensibility-1/ [src/commerce-configuration-1/] ← only if generated [src/commerce-backend-ui-2/] ← only if generated
── Safe to delete ───────────────────────────────────────────────── ← omit this entire section if src/commerce-backend-ui-1/ does not exist → src/commerce-backend-ui-1/ ← v1 generated directory; no longer used
── Modified ─────────────────────────────────────────────────────── ← omit app.config.yaml line if generate failed → app.config.yaml added extensions block package.json added postinstall hook · installed deps
── Installation steps ───────────────────────────────────────────── ← omit this entire section if no customInstallationSteps → These scripts run automatically during App Management installation:
[ <script path> → <step name>] ← one line per entry in installation.customInstallationSteps →
Remove them from your manual onboarding flow once verified.
← omit this block if both Category A and Category B are empty → ── Removable files ──────────────────────────────────────────────── These files have no role in App Management deployment:
[ <path> └─ <reason label>] ← one block per removable file →
To remove: git rm <path1> [<path2> ...] or delete manually before your next commit. ← end of conditional block →
← include only if the assembled config contains a businessConfig section → ── Schema cleanup (optional) ────────────────────────────────────── businessConfig.schema in app.commerce.config.ts supersedes the configSchema: block in app.config.yaml. Safe to remove that block manually — it is no longer used by App Management. ← end conditional →
← include only if any field in businessConfig.schema has "_source": "aio-lib-files-path", OR if reading app.commerce.config.ts reveals a schema field whose "name" contains "/" or ends in ".json" → ── businessConfig schema may need refinement ────────────────────── ⚠ One or more businessConfig fields were auto-generated from aio-lib-files path detection:
[ <field-name> └─ field name is a file path, not a merchant-visible label — consider replacing with individual named fields (e.g. api_key, sender_id, account_token) ] ← one line per detected path-name field →
To update: edit businessConfig.schema in app.commerce.config.ts and replace the file-path field with individual fields matching your app's actual configuration keys. ← end conditional →
← include only if ProjectSnapshot.productDependencies is non-null → ── Commerce version constraints ─────────────────────────────────── productDependencies: minVersion <value> · maxVersion <value> App Management has no direct equivalent for these constraints. Document them in a comment in app.commerce.config.ts, or contact Adobe Commerce Marketplace for guidance. ← end conditional →
← omit this entire block if Category C and Category D are both empty → ── Documentation recommendations ───────────────────────────────── ← include this subsection only if Category C is non-empty → README.md sections that may be outdated:
[ <location> ] └─ <reason> ← one block per identified section →
Update or remove these sections once the migration is verified.
← if Category C count is 1–4, append this note → (Tip: run /commerce-app-migrate --doc-scan-only after adding more content to README.md to regenerate recommendations. An annotated inline removal guide is shown when 5 or more sections are identified.) ← end note →
← include annotated removal guide only if Category C count ≥ 5 → ── README.md — annotated removal guide ────────────────────────── Each flagged section is marked below. Sections not listed are unaffected.
[ <!-- ✂ REMOVE/UPDATE: <reason> --> ← REMOVE for Pattern 1, UPDATE for Patterns 2–5
<heading line>
<up to 3 body lines> ]
← one block per flagged section, in document order →← end annotated guide → ← end Category C subsection →
← include this subsection only if Category D is non-empty → env.dist entries that may no longer be needed:
← include only if Rule 8 found duplicates → ⚠ Duplicate entries (keep one, remove extras):
[ ⚠ Duplicate entry: <KEY> └─ appears <N> times in env.dist (duplicate entry — should appear only once) ]
Remove duplicate occurrences, keeping exactly one entry per key. ← end duplicate block →
← include only if Rules 1–7, Rule 9, or both found obsolete entries → Obsolete entries after migration:
Grouping rules for output:
First, sort all Rule 1–7 and Rule 9 findings (excluding Rule 8 duplicates) into four buckets:
Bucket A — "App Management managed" (safe to remove after deployment): Entries from Rules 1–3, 6, 7 where the runtime reference check found NO match in action/src/lib files (i.e. the runtime reference override did NOT apply).
Bucket B — "Review manually" groups: Entries where the runtime reference override applied (Rules 1–3, 6, 7), plus all Rule 5 findings. Group ALL entries that reference the SAME file together. For each unique referenced file, emit one group block.
Bucket C — "Only in removable onboarding scripts": Entries matched by Rule 4 only. These reference onboarding scripts that are no longer needed after migration — safe to remove from env.dist once those scripts are removed.
Bucket D — "Likely unused": Entries matched by Rule 9 only (not referenced in any action file, config, or script).
Print in this order:
If Bucket A is non-empty:
── App Management managed — safe to remove after verifying injection ───────── <KEY1>, <KEY2>, <KEY3> └─ These are managed by App Management for deployed instances. They may still be needed in .env for local development.
For each unique file in Bucket B (sorted by file path):
── Review manually — still referenced in <relative-file-path> ────────────── <KEY1>, <KEY2>, <KEY3> └─ Ensure App Management injects these values before removing.
If Bucket C is non-empty:
── Onboarding/script-only — safe to remove with the scripts ───────────────── <KEY1>, <KEY2> └─ Only used in <script-name>, which is no longer needed after migration.
If Bucket D is non-empty:
── Likely unused — not referenced anywhere ─────────────────────────────────── <KEY1>, <KEY2> └─ Not found in any action source file or configuration. Verify these are not needed before removing.
After all buckets:
Remove confirmed-safe entries from env.dist (and .env if present) once verified. Note: Bucket B entries may still be needed for local development until App Management credential injection is confirmed for your deployment. ← end obsolete block → ← end Category D subsection → ← end conditional block →
── Next steps ───────────────────────────────────────────────────── [1. aio-commerce-lib-app generate all] ← include ONLY if Step 5 failed 2. Review src/commerce-extensibility-1/.generated/ before deploying 3. aio app deploy
If no customInstallationSteps were defined, omit the "Installation steps" section. If no scripts were migrated in Step 3, omit the migrated-scripts line from "Files written".
Removable files — edge cases:
- A script rewritten in-place by Step 3 (its path is in
customInstallationSteps) is NOT removable — do not list it. - YAML files in
convertedYamlFilesare always listed even though they are kept on disk; give the developer the choice, but do not delete them. - If a path appears in both Category A and Category B, list it once using the Category B label (more specific).
- If the combined removable list is empty, omit the section entirely — do not print a heading or "none".
External Documentation References
Use these links when the user needs context to answer a question or identify their project type. Surface them at decision points, not on every run.
Integration Starter Kit (ISK)
URL: https://developer.adobe.com/commerce/extensibility/starter-kit/integration/
Use when:
starterKitTypeis"unknown"and the user needs to identify their project- The user asks what ISK-style eventing or bidirectional sync looks like
- Explaining why certain action package names (e.g.
product-commerce,order-backoffice)
were detected
Key concepts: event providers, event subscriptions, bidirectional sync actions, starter-kit-registrations.json, onboarding scripts.
Checkout Starter Kit (CSK)
URL: https://developer.adobe.com/commerce/extensibility/starter-kit/checkout/
Use when:
starterKitTypeis"unknown"and the user needs to identify their project- The user asks about webhook-based payment, shipping, or tax integrations
- Explaining why
raw-http: trueactions orCOMMERCE_WEBHOOKS_PUBLIC_KEYwere detected
Key concepts: out-of-process webhooks, payment methods, shipping carriers, tax integrations, sync-oauth-credentials.
Related skills
FAQ
What are --auto and --doc-scan-only?
--auto skips confirmation prompts for CI or batch use, and --doc-scan-only scans README.md and env.dist for outdated content without modifying any files.
What if the project is already migrated?
The skill detects alreadyMigrated and stops rather than overwriting the existing app.commerce.config.ts.