upgrade

upgrade lists and runs codemods: automated migrations that move your app across a breaking change in @customerio/pluma-components. Codemods are grouped by the @customerio/pluma-components version each change lands in, so you can see which ones the versions you are crossing need.

When to use it

Run upgrade when you bump @customerio/pluma-components across a version that has codemods, before you fix anything by hand. Each codemod covers one breaking change; what it migrates, and what it leaves for you, is in each codemod's own README. A codemod doesn't replace reading the release notes: changes without a codemod still need migrating by hand. See When upgrading for the steps.

Usage

pluma-cli upgrade --list [--json]
pluma-cli upgrade --codemod <id> <react|ember> <path...> [--dry] [--force]
                  [--report <path>] [--ledger <path> | --no-ledger] [--baseline]

After --codemod <id>, pass the framework (react or ember), then one or more paths, then any flags. Flags and paths are forwarded to the codemod, so its own options (--dry, and — for React — standard jscodeshift flags) work as documented in each codemod's README. The flags in the table below are the CLI's own and mean the same for every codemod. None of them ever reaches jscodeshift, which reads a flag it doesn't know as taking a value and would drop the path after it. The codemods built on the shared dispatcher — every 5.0.0 one among them — go further: a bare flag that is neither the CLI's nor jscodeshift's own, or a path that doesn't exist, stops the run with exit status 2 before anything is written, and a file jscodeshift fails to transform makes the run exit non-zero. An option meant for the transform goes through in the --name=value form, which can't swallow the argument after it. --silent is refused while the run ledger is on, because it hides the summary the ledger is checked against.

Flags

table
FlagDescription
--listList available codemods, grouped by target @customerio/pluma-components version.
--codemod <id>Run the codemod with this id (see --list).
--jsonWith --list, print machine-readable JSON.
--dry, -dPreview changes without writing them.
--forceBypass a codemod's re-run safety check. Only codemods that have one take it; it never changes which files are transformed.
--report <p>Where to write the findings report (default .pluma/codemod-report.json). Also --report=<p>.
--ledger <p>Where the run ledger lives (default .pluma/codemod-ledger.json). Also --ledger=<p>.
--no-ledgerNeither read nor write the run ledger.
--baselineRecord every site a run would change as already migrated, and change no files. Only for codemods that keep a run ledger.
-h, --helpShow help.

Examples

List the codemods

pluma-cli upgrade --list
Codemods by @customerio/pluma-components version:

2.170.0
  banner-description-prop  [react, ember]
    Move existing Banner children/yields into the new description prop or named block.

2.204.0
  flex-to-stack  [react, ember]
    Replace Flex with Stack or InlineStack, remapping alignment onto the axis props.

3.0.0
  button-variant-rename  [react, ember]
    Rename Button variant props: tertiary→secondary, subtle→tertiary.
  text-size-shorthand-rename  [react, ember]
    Rename Text/Heading/Paragraph short-hand size props: s→sm, m→md, l→lg (and product-ps text style→product-psm).
  css-token-rename  [react, ember]
    Rename the CSS custom properties the token rename moved: --font-size-s/m/l→sm/md/lg, --text-product-ps→product-psm, and the Banner/Snackbar error names→critical (both the --pluma-* and the bare @customerio/pluma-css spelling).
  size-longhand-rename  [react, ember]
    Rename long-hand size props (medium→md, small→sm, large→lg) on Button, ButtonGroup, ProgressBar, SegmentedControl, Select, TextField, Toggle, and friends.
  event-handler-rename  [react, ember]
    Rename the deprecated onChange handler to onCheckedChange on Checkbox, Radio, and OptionCard.
  primitive-rename  [react, ember]
    Rename the primitive components: PopoverPrimitive→PlainPopover, ModalPrimitive→PlainModal, SelectPrimitive→SelectController (and their subcomponents/types).
  deprecated-props-removal  [react, ember]
    Migrate removed deprecated props: Button/Snackbar/Link isError, isDanger→isCritical on Button and the dropdown/filters components that inherit it, Popover content, Banner/Snackbar variant "warning"→"caution" and "error"→"critical".
  icon-deprecations  [react, ember]
    Migrate deprecated icons: rename/merge icon names (including error→critical, warning→caution), fold v1-only sizes (xxs/xs→sm), and remove the Icon sizeVersion prop.
  label-icon-to-coin  [react, ember]
    Migrate the removed LabelIcon component to Coin with a child Icon.
  toggle-button-to-button  [react, ember]
    Migrate the removed ToggleButton component to Button (defaults to variant="tertiary").
  test-helper-get-rename  [react, ember]
    Migrate the removed PlumaTestHelpers.get test helper to find (review notes flag sites that should use extract).
  data-table-tanstack-v9  [react, ember]
    Migrate DataTable to TanStack Table v9: Ember flexRenderWrapper→flexRenderComponent (@props→@options), sortingFn→sortFn, table.getState()→table.store.state, and the createColumnHelper features type parameter.
  label-to-badge  [react, ember]
    Replace the deprecated Label with Badge, and Badge isOutline/isEmphasized with variant and color values.

5.0.0
  theme-tokens-rename  [react, ember]
    Rename themeVars and --pluma-* CSS custom property reads to the semantic color layer (surface planes, scopes, text/mark roles, rebuilt palette steps), and defaultTheme to lightTheme. Hover, pressed and selected reads go to the 5.0 state recipes rather than the resting rung; sites that need a human land in the findings report.
  text-icon-color-rename  [react, ember]
    Rename Text/Icon color values (base/bold/active to primary, subtle/minimal/placeholder to secondary), Box backgroundColor and Coin borderColor values to the semantic layer.
  color-variant-rename  [react, ember]
    Rename palette hue values (grey to gray, raspberry to pink, clementine to orange, plum to fuchsia, purple to violet) on Badge, Coin, StatusIndicator, PageHeaderIconTile, TimelineItem and ConditionViewerItem, and Badge accent/*-outline colors to variant + color.

Run one with:
  pluma-cli upgrade --codemod <id> <react|ember> [--dry] <path...>

List the codemods as JSON

pluma-cli upgrade --list --json
[
  {
    "id": "button-variant-rename",
    "version": "3.0.0",
    "title": "Rename Button variant props: tertiary→secondary, subtle→tertiary.",
    "frameworks": [
      "react",
      "ember"
    ],
    "rerunGuard": true
  },
  {
    "id": "text-size-shorthand-rename",
    "version": "3.0.0",
    "title": "Rename Text/Heading/Paragraph short-hand size props: s→sm, m→md, l→lg (and product-ps text style→product-psm).",
    "frameworks": [
      "react",
      "ember"
    ]
  },
  {
    "id": "css-token-rename",
    "version": "3.0.0",
    "title": "Rename the CSS custom properties the token rename moved: --font-size-s/m/l→sm/md/lg, --text-product-ps→product-psm, and the Banner/Snackbar error names→critical (both the --pluma-* and the bare @customerio/pluma-css spelling).",
    "frameworks": [
      "react",
      "ember"
    ]
  },
  {
    "id": "size-longhand-rename",
    "version": "3.0.0",
    "title": "Rename long-hand size props (medium→md, small→sm, large→lg) on Button, ButtonGroup, ProgressBar, SegmentedControl, Select, TextField, Toggle, and friends.",
    "frameworks": [
      "react",
      "ember"
    ]
  },
  {
    "id": "event-handler-rename",
    "version": "3.0.0",
    "title": "Rename the deprecated onChange handler to onCheckedChange on Checkbox, Radio, and OptionCard.",
    "frameworks": [
      "react",
      "ember"
    ]
  },
  {
    "id": "primitive-rename",
    "version": "3.0.0",
    "title": "Rename the primitive components: PopoverPrimitive→PlainPopover, ModalPrimitive→PlainModal, SelectPrimitive→SelectController (and their subcomponents/types).",
    "frameworks": [
      "react",
      "ember"
    ]
  },
  {
    "id": "deprecated-props-removal",
    "version": "3.0.0",
    "title": "Migrate removed deprecated props: Button/Snackbar/Link isError, isDanger→isCritical on Button and the dropdown/filters components that inherit it, Popover content, Banner/Snackbar variant "warning"→"caution" and "error"→"critical".",
    "frameworks": [
      "react",
      "ember"
    ]
  },
  {
    "id": "icon-deprecations",
    "version": "3.0.0",
    "title": "Migrate deprecated icons: rename/merge icon names (including error→critical, warning→caution), fold v1-only sizes (xxs/xs→sm), and remove the Icon sizeVersion prop.",
    "frameworks": [
      "react",
      "ember"
    ]
  },
  {
    "id": "label-icon-to-coin",
    "version": "3.0.0",
    "title": "Migrate the removed LabelIcon component to Coin with a child Icon.",
    "frameworks": [
      "react",
      "ember"
    ]
  },
  {
    "id": "toggle-button-to-button",
    "version": "3.0.0",
    "title": "Migrate the removed ToggleButton component to Button (defaults to variant="tertiary").",
    "frameworks": [
      "react",
      "ember"
    ]
  },
  {
    "id": "test-helper-get-rename",
    "version": "3.0.0",
    "title": "Migrate the removed PlumaTestHelpers.get test helper to find (review notes flag sites that should use extract).",
    "frameworks": [
      "react",
      "ember"
    ]
  },
  {
    "id": "data-table-tanstack-v9",
    "version": "3.0.0",
    "title": "Migrate DataTable to TanStack Table v9: Ember flexRenderWrapper→flexRenderComponent (@props→@options), sortingFn→sortFn, table.getState()→table.store.state, and the createColumnHelper features type parameter.",
    "frameworks": [
      "react",
      "ember"
    ]
  },
  {
    "id": "banner-description-prop",
    "version": "2.170.0",
    "title": "Move existing Banner children/yields into the new description prop or named block.",
    "frameworks": [
      "react",
      "ember"
    ]
  },
  {
    "id": "label-to-badge",
    "version": "3.0.0",
    "title": "Replace the deprecated Label with Badge, and Badge isOutline/isEmphasized with variant and color values.",
    "frameworks": [
      "react",
      "ember"
    ]
  },
  {
    "id": "flex-to-stack",
    "version": "2.204.0",
    "title": "Replace Flex with Stack or InlineStack, remapping alignment onto the axis props.",
    "frameworks": [
      "react",
      "ember"
    ]
  },
  {
    "id": "theme-tokens-rename",
    "version": "5.0.0",
    "title": "Rename themeVars and --pluma-* CSS custom property reads to the semantic color layer (surface planes, scopes, text/mark roles, rebuilt palette steps), and defaultTheme to lightTheme. Hover, pressed and selected reads go to the 5.0 state recipes rather than the resting rung; sites that need a human land in the findings report.",
    "frameworks": [
      "react",
      "ember"
    ],
    "rerunGuard": true,
    "ledger": true
  },
  {
    "id": "text-icon-color-rename",
    "version": "5.0.0",
    "title": "Rename Text/Icon color values (base/bold/active to primary, subtle/minimal/placeholder to secondary), Box backgroundColor and Coin borderColor values to the semantic layer.",
    "frameworks": [
      "react",
      "ember"
    ],
    "ledger": true
  },
  {
    "id": "color-variant-rename",
    "version": "5.0.0",
    "title": "Rename palette hue values (grey to gray, raspberry to pink, clementine to orange, plum to fuchsia, purple to violet) on Badge, Coin, StatusIndicator, PageHeaderIconTile, TimelineItem and ConditionViewerItem, and Badge accent/*-outline colors to variant + color.",
    "frameworks": [
      "react",
      "ember"
    ],
    "ledger": true
  }
]

Run a codemod

Always preview with --dry first, then run it for real:

pluma-cli upgrade --codemod button-variant-rename react --dry ./src
pluma-cli upgrade --codemod button-variant-rename react ./src

The run prints the codemod's own progress and a "needs manual review" summary. A codemod that records findings ends by printing how many rows the findings report holds, split by class, and where it went. A codemod that keeps a run ledger writes it to .pluma/codemod-ledger.json unless --dry or --no-ledger is set.

JSON output

--list --json prints an array with one object per codemod:

table
KeyValue
idThe id --codemod takes.
versionThe @customerio/pluma-components version whose breaking change the codemod migrates.
titleA one-line summary of what it changes.
frameworksThe frameworks it can transform: react, ember.
rerunGuardtrue when the codemod has a re-run safety check that --force bypasses. Absent otherwise.
ledgertrue when the codemod keeps a run ledger. Absent otherwise.

A codemod run has no JSON output; its results are the changed files, the findings report and the run ledger.

Troubleshooting

Unknown codemod

Unknown codemod: "button-variant".

Available codemods:
  button-variant-rename
  ...

The id is misspelled, or the codemod isn't in the version of pluma-cli you ran. The command exits with status 2 and lists every id it knows. Run with @latest to get the newest codemods, and copy the id from pluma-cli upgrade --list.

Missing framework or path

Expected a framework (react or ember) after --codemod button-variant-rename, got "./src".

The framework comes straight after the codemod id, before the paths: pluma-cli upgrade --codemod button-variant-rename react ./src. The command exits with status 2. A run with no path at all prints jscodeshift's usage instead of migrating anything, and a codemod built on the shared dispatcher refuses a path that doesn't exist with No such file or directory: <path>.

A flag jscodeshift would swallow

Unknown flag: --verbose-ish. It would have been passed to jscodeshift, which reads an unknown flag as taking a value and drops the argument after it. To hand an option to the transform, attach its value: --verbose-ish=<value>.
Nothing was changed.

jscodeshift reads a flag it doesn't know as taking a value and would drop the path after it, so a codemod built on the shared dispatcher stops the run with exit status 2 before anything is written. Check the flag's spelling against the flags table and the codemod's README. An option meant for the transform goes through in the --name=value form, which can't swallow the argument after it.

--silent is refused

--silent hides the jscodeshift summary the run ledger is checked against. Drop it, or pass --no-ledger.

A codemod that keeps a run ledger checks it against the summary jscodeshift prints, and --silent (-s) hides that summary. The run stops with exit status 2. Drop --silent, or pass --no-ledger if you really mean to run without the ledger.

When upgrading

  1. Bump @customerio/pluma-components.
  2. Run pluma-cli upgrade --list and note the codemods whose version falls in the range you're crossing.
  3. Run each with --dry first, review the diff and the "needs manual review" summary, then run for real and commit — with the .pluma/ directory the run wrote.
  4. Work the findings report before you ship — see below.

The findings report

A codemod prints two kinds of note as it runs: warnings (a token it could not rewrite) and review notes (a rewrite worth a second look). Both scroll past. A run that touches fifty files prints hundreds of lines, and the migrations that shook this out showed exactly what happens next: the notes were read once and the sites they pointed at shipped unfixed.

Some codemods therefore also write a findings report — one JSON file per run, at .pluma/codemod-report.json (see The .pluma/ directory) unless --report <path> says otherwise. Both halves of a React run (the stylesheet pass and the JS pass) write into the same file, and so does a pre-flight check that found something the run went ahead over. At the end of the run the CLI prints how many rows it holds, split by class, and where it went.

{
	"version": 1,
	"codemod": "theme-tokens-rename",
	"framework": "react",
	"dryRun": false,
	"generatedAt": "2026-09-17T02:11:04.918Z",
	"summary": {
		"total": 61,
		"rewritten": 48,
		"fixByHand": 13,
		"byClass": { "interaction-state": 44, "plane-collapse": 3, "overlay-base": 1, "palette-shift": 13 },
	},
	"findings": [
		{
			"class": "interaction-state",
			"action": "rewritten",
			"file": "src/features/home/recents-row.css.ts",
			"line": 91,
			"selector": "&:hover",
			"detectedFrom": "&:hover",
			"property": "backgroundColor",
			"token": "color.surface-subtle",
			"mappedTo": "color.overlay-tint",
			"rewrittenTo": "layerHover()",
			"reason": "A hover state, detected from the selector. …",
		},
	],
}

Every row is one site. The two fields to sort on:

table
FieldMeaning
classWhich problem this is. Codemod-specific; see the codemod's own README.
actionrewritten — the codemod changed the source and you should check the result.
fix-by-hand — the source is untouched, or — where leaving the old name would
have broken the file — renamed with the colour decision still open. The right answer
needed a look at the element, and a plausible wrong colour is worse than a work item.
This row is the task.
renamed-only — the name was migrated so the file stays valid, but the site still
needs a hand fix. Used where leaving the old name would break silently rather than
loudly — a stylesheet has no typecheck, and an undefined custom property simply
vanishes. These also print a warning during the run.

A fix-by-hand row does not always mean the codemod declined a rewrite. Some classes describe a site the codemod was right to leave alone and a reader still has to judge — theme-tokens-rename's token-revival is one, where the line is already valid 5.0 and the question is what it used to paint. The codemod's own README says which of its classes are which.

Filter the hand-work list with jq:

jq -r '.findings[] | select(.action != "rewritten") | "(.file):(.line)  (.action)  (.reason)"' .pluma/codemod-report.json

rewritten rows are excluded because nothing is owed on them, not because they're finished — the codemod made a call there and the result still wants a look. Read those too, by class; the codemod's own README says which ones are worth the time.

The run ledger

Most codemods are safe to run twice. A few are not: theme-tokens-rename renames color.navigation-background to color.surface-base and color.surface-base to color.surface-content, so a second pass over code the first one migrated shifts it one rung further. And a branch migrated to a new major rarely stays still — it keeps merging in code written against the old one, often into files it already migrated. After that merge, a file reads a surface-base the codemod wrote (5.0, the page plane) next to a surface-base main just added (4.x, the white panel), and nothing in the text says which is which.

So the 5.0 codemods (theme-tokens-rename, text-icon-color-rename, color-variant-rename) keep a run ledger: .pluma/codemod-ledger.json, next to the findings report unless --ledger <path> says otherwise. Commit it with the migrated files. A later run over the same files leaves every site it records exactly as it is, and migrates the rest — the new code — even inside a file it migrated before.

What is recorded

Not every rewrite: only the sites a second run would change. After writing a file, the codemod runs over its own output once more, and whatever that pass would still change is a site the next run must leave alone. So the ledger needs no knowledge of any codemod's tables, and a codemod that never rewrites its own output records nothing. text-icon-color-rename and color-variant-rename are like that; theme-tokens-rename is not, and records every cascade site it writes. Each run recomputes the rows for the files it visits from those files as it leaves them, so the ledger always describes the tree as it stands. A row for a site the run never reached — with the bare pass off, say — is kept while the site is still in the file. A run stopped with Ctrl-C records the files it wrote before it stopped.

How a site is identified

A site is keyed by its tokens and what surrounds them, never by a line number:

  • text — the tokens a re-run would change, joined: surface-base, --pluma-palette-blue-600.
  • rerun — what a re-run would write there. For reading only; it is not part of the key.
  • line — a hash of the line the site sits on.
  • before / after — a 4-character hash of each of the 16 tokens either side, nearest first, ending in ^ / $ where the file does.

The code is read as tokens — words and punctuation, whitespace ignored — with , and ; skipped and " read as '. So indentation, re-wrapping, quote style, trailing commas and semicolons change nothing but, at most, the line hash; lines added above or below a site, and a block a merge moved, carry the site with them. Matching measures how far out each side still agrees, both as an unbroken run from the site and as a longest common subsequence (so a line inserted right next to a site costs only that line), and weighs a word the file rarely uses — a style's name, a selector — above punctuation, which is the same everywhere.

The trade-off is duplication: two sites with the same text on identical lines in identical surroundings can't be told apart by anything in the file. When a file holds more such copies than the ledger recorded, one of them is new and nothing says which, so none of them is rewritten. Each lands in the findings report as a ledger-ambiguous row, fix-by-hand, and so does a site whose surroundings changed too much for the ledger to be sure. Every uncertain case leaves the source as it is and says so, on every run until you migrate the new copies by hand and then, before any other run, record the file with --baseline <path>.

The file

{
	"version": 1,
	"about": "Written by @customerio/pluma-cli: …",
	"context": 16,
	"codemods": {
		"theme-tokens-rename": {
			"src/nav.css.ts": [
				{
					"text": "surface-base",
					"rerun": "surface-content",
					"line": "0ef0717a",
					"before": "bb58 1e5c 6dd0 3a52 9952 05a7 5a0c 60ba 28ed 26ec 2160 fea8 b42f 5171 bb58 d988",
					"after": "bb58 4ff4 6dd0 05a7 9952 3a52 b8a5 1e5c bb58 0397 bb58 4ff4 5377 05a7 9952 3a52",
				},
			],
			"src/text.css.ts": [],
		},
	},
}
table
FieldMeaning
versionThe format. A CLI refuses a ledger newer than it understands rather than ignore its rows.
contextHow many tokens either side each row records.
codemodsRows per codemod, then per file — the path from the ledger's root, with forward slashes.
hashesPer codemod and file, a hash of the content its rows were recorded against.

A ledger in a .pluma/ directory records paths from the directory that holds .pluma/ — the repository root, for the default ledger — so they read the same whichever directory a run starts in. A ledger anywhere else records paths from its own directory.

A file with an empty list holds nothing a re-run would change, but reads the new names: it is listed so theme-tokens-rename's re-run guard knows it is the codemod's own output (see below). The file is deterministic — codemods, files and rows sorted, one row per line, no timestamps — so a re-run diffs as the rows that actually changed. Rows for files that no longer exist are dropped. If a merge conflicts in it, keep both sides' rows; the order doesn't matter.

Flags

  • --dry reads the ledger (so the preview is what a real run would write) and never writes it.
  • --no-ledger neither reads nor writes it: the codemod behaves exactly as it did before the ledger existed.
  • --baseline changes no file. It records every site a run would change as already migrated, and lists them. That is how a tree migrated before the ledger existed — or code written against the new API by hand, which reads exactly like code still to migrate — is brought under it. Run it on the branch before merging main in, then merge and run as normal, and run it again before each later merge whenever new-API code has been written by hand since. Check the list: a recorded site is never migrated.
  • --force still only bypasses a codemod's re-run guard. The ledger applies under it; --no-ledger is the flag that turns the ledger off.

theme-tokens-rename's re-run guard, which refuses a tree that already reads 5.0-only names, skips every file the ledger lists — so re-running over a tree it migrated is no longer refused. A listed file that changed since its rows were recorded is named in a warning instead; it never refuses the run, and its sites without a row are migrated. Files the ledger has no record of are scanned as before, except that where the ledger lists files under the same paths, 5.0 names in a single unlisted file refuse the run — record it with --baseline <path> if it is already 5.0. See its README.

What it can't do

  • It only knows what a codemod wrote, or what --baseline recorded. Code migrated by hand, or written against the new API since, is invisible to it until it is baselined.
  • It is keyed by path. theme-tokens-rename carries the rows of a migrated file that was moved or renamed unchanged. Moved and edited, or pulled out into a new file, the run is refused: record the new file with --baseline <path>, or, if it is not the same code, drop the old rows with --forget <old path>.
  • A copy is new code to it. A migrated block copied into a new file is migrated again, unless it reads 5.0-only names.
  • So is an un-commented line. Un-comment your own migrated line next to it, and the next run migrates it.
  • A killed run records nothing. Ctrl-C, SIGTERM or a closed terminal keep the rows for what the run wrote; SIGKILL loses that run's rows.

Adding a codemod

  1. Create src/codemods/<target-version>/<id>/ containing a bin.mjs dispatcher (bin.mjs <react|ember> <path…>), the react.cjs and/or ember.mjs transforms, a README.md, and a tests/ suite. Use an existing codemod as the template.
  2. Register it in src/registry.ts.
  3. Add a changeset so the new codemod ships in the next @customerio/pluma-cli release.