Skip to content

feat(cli): add upgrade command with @tailor-platform/sdk-codemod package#893

Closed
toiroakr wants to merge 41 commits intomainfrom
feat/codemod-infra-v3
Closed

feat(cli): add upgrade command with @tailor-platform/sdk-codemod package#893
toiroakr wants to merge 41 commits intomainfrom
feat/codemod-infra-v3

Conversation

@toiroakr
Copy link
Copy Markdown
Contributor

@toiroakr toiroakr commented Apr 2, 2026

Summary

  • Add tailor-sdk upgrade --from <version> command for automated SDK migration
  • New @tailor-platform/sdk-codemod package: standalone codemod runner using @ast-grep/napi directly
  • In-memory transform chaining via reduce() pattern — multiple codemods see each other's output, dry-run is accurate even with sequential transforms on the same file
  • SDK detects target version from node_modules, invokes npx @tailor-platform/sdk-codemod with --from/--to
  • First codemod: defineGenerators()definePlugins() with explicit plugin imports
  • Dry-run mode with diff preview
  • 21 tests across both packages (registry, runner, transform, service, version detection)

Architecture

tailor-sdk upgrade --from 1.33.0 [--dry-run]
  ├── detectInstalledVersion() → --to (new version from node_modules)
  └── spawnSync npx @tailor-platform/sdk-codemod@<ver>
        --from 1.33.0 --to 2.0.0 --target . [--dry-run]
        ├── registry: select applicable codemods
        ├── runner: @ast-grep/napi parse → reduce() chain transforms → write/diff
        └── stdout: JSON result → SDK parses and displays

Key design: in-memory transform chaining

Unlike codemod.com's workflow engine (which breaks dry-run for sequential transforms on the same file), this implementation uses @ast-grep/napi directly:

for each file:
  current = original source
  for each transform:
    root = parse(lang, current)
    result = transform(root)
    if result: current = result
  if current ≠ original: record as modified

This ensures dry-run produces correct diffs even when codemod B depends on codemod A's output.

New Package: @tailor-platform/sdk-codemod

packages/sdk-codemod/
  src/index.ts       # CLI (politty): --from, --to, --target, --dry-run
  src/registry.ts    # Codemod registry + version filtering
  src/runner.ts      # @ast-grep/napi parse + reduce() chain + diff generation
  codemods/v2/       # Transform scripts + test fixtures

Open with Devin

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 2, 2026

🦋 Changeset detected

Latest commit: 4941b02

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@tailor-platform/sdk Minor
@tailor-platform/sdk-codemod Patch
@tailor-platform/create-sdk Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 2, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@tailor-platform/create-sdk@893

commit: 015aaed

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@toiroakr toiroakr force-pushed the feat/codemod-infra-v3 branch from 9d4fcd9 to 23c6684 Compare April 3, 2026 05:12
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor

@dqn dqn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up fixes: #909

@github-actions

This comment has been minimized.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 8, 2026

⚡ pkg.pr.new

Publishing commit: 4941b02

@tailor-platform/sdk

pnpm add https://pkg.pr.new/@tailor-platform/sdk@893
pnpm dlx https://pkg.pr.new/@tailor-platform/sdk@893 --help

@tailor-platform/create-sdk

pnpm add https://pkg.pr.new/@tailor-platform/create-sdk@893
pnpm dlx https://pkg.pr.new/@tailor-platform/create-sdk@893 my-app

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@toiroakr toiroakr marked this pull request as ready for review April 8, 2026 05:52
@toiroakr toiroakr requested a review from remiposo as a code owner April 8, 2026 05:52
@toiroakr toiroakr assigned toiroakr and remiposo and unassigned toiroakr Apr 8, 2026
@claude
Copy link
Copy Markdown

claude Bot commented Apr 8, 2026

📖 Docs Consistency Check

No inconsistencies found between documentation and implementation.

Checked areas:

CLI Command Implementation vs Documentation

  • ✅ Command name: upgrade matches across all files
  • ✅ Command description: "Run codemods to upgrade your project to a newer SDK version." is consistent
  • ✅ Options match between src/cli/commands/upgrade/index.ts and docs/cli/upgrade.md:
    • --from <FROM> (required, string) - SDK version before upgrade
    • --dry-run / -d (optional, boolean, default: false) - Preview mode
    • --path <PATH> (optional, string, default: ".") - Project directory

CLI Reference

  • docs/cli-reference.md properly updated with new "Upgrade Commands" section
  • ✅ Command index entry correctly links to docs/cli/upgrade.md

CLI Documentation Configuration

  • src/cli/docs.test.ts properly configured with upgrade command for auto-generation
  • ✅ Uses defaultRender template as expected

Implementation Details

  • ✅ Service implementation (src/cli/commands/upgrade/service.ts) correctly:
    • Detects installed SDK version from node_modules
    • Spawns @tailor-platform/sdk-codemod@latest with correct arguments
    • Parses JSON output matching RunOutput interface
    • Handles dry-run mode as documented
  • ✅ Test coverage confirms documented behavior

Codemod Package

  • @tailor-platform/sdk-codemod CLI accepts documented options (--from, --to, --target, --dry-run)
  • ✅ First codemod (defineGeneratorsdefinePlugins) properly configured with test fixtures

Notes

  • The docs/cli/upgrade.md documentation is minimal (auto-generated politty structure only). While not an inconsistency, it could benefit from:
    • Usage examples
    • List of available codemods (e.g., "Migrates defineGenerators() to definePlugins() for v2.0+")
    • Sample dry-run output explanation
    • However, this is an enhancement opportunity, not a docs/code mismatch

@claude
Copy link
Copy Markdown

claude Bot commented Apr 8, 2026

📖 Docs Consistency Check

⚠️ Inconsistencies Found

File Issue Suggested Fix
CLAUDE.md Lines 43, 79 reference defineGenerators() but example/tailor.config.ts uses definePlugins() Update CLAUDE.md to reflect current pattern or clarify both are supported
packages/sdk/docs/cli/upgrade.md Missing workflow explanation and context Add manual section explaining version auto-detection and typical workflow

Details

1. CLAUDE.md References Out-of-Date Pattern

Location: CLAUDE.md:43, 79

Issue: The documentation describes defineGenerators() as the pattern used in the example, but the actual implementation has moved to definePlugins():

  • CLAUDE.md line 43 says:

    • example/tailor.config.ts - Configuration with defineConfig, defineAuth, defineIdp, defineStaticWebSite, defineGenerators
  • CLAUDE.md lines 77-79 has a "Generators" section explaining defineGenerators() and referencing example/tailor.config.ts

  • Actual example/tailor.config.ts uses definePlugins() with explicit plugin imports (lines 1-11)

The codemod define-generators-to-plugins migrates FROM defineGenerators() TO definePlugins() for v1 → v2 upgrades, confirming that definePlugins() is the current/recommended pattern.

Recommendation: Update CLAUDE.md to:

  • Change line 43 to list definePlugins instead of defineGenerators
  • Update the "Generators" section (lines 77-79) to document definePlugins() as the primary pattern

2. upgrade.md Missing Workflow Explanation

Location: packages/sdk/docs/cli/upgrade.md

Issue: The documentation contains only auto-generated command reference with no explanation of:

  1. How the target version is determined (auto-detected from node_modules/@tailor-platform/sdk)
  2. Typical upgrade workflow
  3. What codemods do or when to use them

Current state: The file contains only politty-generated markers with options table - no manual content sections.

Comparison: Other commands like apply have manual sections after the auto-generated content (e.g., "Migration Handling" section in packages/sdk/docs/cli/application.md).

Implementation details (from packages/sdk/src/cli/commands/upgrade/service.ts:63-79):
The upgrade service auto-detects the target SDK version from node_modules rather than requiring a --to flag.

Recommendation: Add a manual section to upgrade.md after the auto-generated sections explaining:

  • Target version is auto-detected from installed @tailor-platform/sdk in node_modules
  • Typical workflow: upgrade SDK packages → run tailor-sdk upgrade --from <old-version>
  • What codemods do (automated code transformations for breaking changes)
  • Example usage scenario

Recommended Actions

  1. Update CLAUDE.md to reflect the current definePlugins() pattern used in examples
  2. Add workflow explanation section to packages/sdk/docs/cli/upgrade.md after the auto-generated content

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 5 additional findings.

Open in Devin Review

@toiroakr toiroakr force-pushed the feat/codemod-infra-v3 branch from 062df86 to cb02595 Compare April 9, 2026 13:16
Introduce `tailor-sdk upgrade` command that delegates code transformations
to codemod.com's jssg runtime instead of a custom codemod engine.

- CLI command with --to, --dry-run, --interactive, --path options
- Thin orchestrator: version detection → codemod selection → jssg execution
- Codemod registry with semver-based version filtering
- First codemod: defineGenerators → definePlugins migration
- Integration tests via `npx codemod jssg test`
toiroakr and others added 17 commits April 9, 2026 22:22
…x Windows npx

- Build transform scripts to dist/codemods/ via tsdown so Node 18/20 can
  import them without --experimental-strip-types
- Use @tailor-platform/sdk-codemod@latest instead of locking to the SDK
  version, since the codemod registry filters by --from/--to internally
- Resolve npx.cmd on Windows (same pattern as skills-installer)
…use file URLs for import

- Return null when any defineGenerators argument is not in PLUGIN_MAP,
  preventing generation of invalid mixed tuple/plugin syntax
- Use pathToFileURL for dynamic import in runner to support Windows paths
- Document known limitations: aliased imports and same-file reference rename
…ename, walk parent node_modules

- Use fileURLToPath instead of URL.pathname for readPackageJSON to handle
  Windows paths and percent-encoded characters correctly
- Remove generators->plugins variable rename: loadConfig() discovers
  exports by value not name, so the rename is unnecessary and breaks
  same-file references
- Walk up directory tree in detectInstalledVersion to find hoisted deps
  in workspace setups
… replace Node 22 glob

- Skip files that don't import from @tailor-platform/sdk
- Skip files that already contain definePlugins to avoid duplicate identifiers
- Replace Node 22-only fs.glob with recursive readdir + picomatch for
  Node 18+ compatibility
…rface npx failures

- Remove blanket definePlugins early return so mixed generator+plugin
  configs can be migrated; deduplicate import specifiers instead
- Restrict step 2 identifier rename to call expressions only to prevent
  import-level duplicates
- Emit warnings in CodemodRunResult when files contain defineGenerators
  but were not migrated (custom generators, unsupported patterns)
- Check spawnSync exit status before JSON.parse to surface npm/registry
  failures with actionable error messages
…tions

- Check if import path already exists in the file before adding plugin
  import lines, preventing duplicate declarations in mixed configs
- Add TODO for --json structured output support in upgrade command
… bound, add structured output

- Use regex matching on function name in import statements instead of
  import path to avoid suppressing needed imports when a different symbol
  is already imported from the same module
- Add gte(fromVersion, codemod.since) to getApplicableCodemods filter so
  codemods are not applied to source versions older than their declared range
- Emit logger.out(output) for structured stdout data alongside the human
  summary on stderr, honoring the CLI --json contract
… cwd, capture stderr

- Check all SDK import statements globally for existing definePlugins
  instead of per-statement, fixing duplicate bindings when definePlugins
  is imported in a separate statement
- Set cwd to projectRoot for spawnSync so npm reads local .npmrc config
- Pipe stderr from sdk-codemod to capture actual error messages (e.g.
  invalid semver) instead of showing misleading network error suggestions
…ess path

The upgrade command pipes sdk-codemod's stderr so it can surface failures via
CLIError, but the success path never replayed the captured output. That
silently swallowed the colorized unified diff emitted by `printDiff` in
dry-run mode and the `Running: ...` / `N file(s) modified` progress messages.
Write `result.stderr` to `process.stderr` after the error checks so users see
the diagnostic output they rely on.
The import_statement query used an unanchored regex that could match
subpath imports like @tailor-platform/sdk/plugin/kysely-type, potentially
causing incorrect specifier handling.
…nning

Drop the Node 18-compatible readdir walk and use fs.promises.glob
directly, since the SDK requires Node 22+.
@toiroakr toiroakr force-pushed the feat/codemod-infra-v3 branch from cb02595 to 55d39d3 Compare April 9, 2026 13:25
@github-actions

This comment has been minimized.

devin-ai-integration[bot]

This comment was marked as resolved.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 9, 2026

Code Metrics Report (packages/sdk)

main (cc2a290) #893 (91a9703) +/-
Coverage 57.0% 57.2% +0.1%
Code to Test Ratio 1:0.4 1:0.4 +0.0
Details
  |                    | main (cc2a290) | #893 (91a9703) |  +/-  |
  |--------------------|----------------|----------------|-------|
+ | Coverage           |          57.0% |          57.2% | +0.1% |
  |   Files            |            337 |            340 |    +3 |
  |   Lines            |          11043 |          11106 |   +63 |
+ |   Covered          |           6299 |           6353 |   +54 |
+ | Code to Test Ratio |          1:0.4 |          1:0.4 |  +0.0 |
  |   Code             |          67313 |          67817 |  +504 |
+ |   Test             |          27541 |          27858 |  +317 |

Code coverage of files in pull request scope (33.3% → 71.2%)

Files Coverage +/- Status
packages/sdk/src/cli/commands/upgrade/index.ts 20.0% +20.0% added
packages/sdk/src/cli/commands/upgrade/service.ts 88.3% +88.3% added
packages/sdk/src/cli/commands/upgrade/version-detector.ts 100.0% +100.0% added
packages/sdk/src/cli/index.ts 33.3% 0.0% modified

SDK Configure Bundle Size

main (cc2a290) #893 (91a9703) +/-
configure-index-size 12.69KB 12.69KB 0KB
dependency-chunks-size 33.36KB 33.36KB 0KB
total-bundle-size 46.04KB 46.04KB 0KB

Runtime Performance

main (cc2a290) #893 (91a9703) +/-
Generate Median 2,574ms 2,386ms -188ms
Generate Max 2,658ms 2,413ms -245ms
Apply Build Median 2,624ms 2,422ms -202ms
Apply Build Max 2,651ms 2,454ms -197ms

Type Performance (instantiations)

main (cc2a290) #893 (91a9703) +/-
tailordb-basic 42,484 42,484 0
tailordb-optional 3,826 3,826 0
tailordb-relation 3,970 3,970 0
tailordb-validate 2,824 2,824 0
tailordb-hooks 5,689 5,689 0
tailordb-object 11,470 11,470 0
tailordb-enum 2,720 2,720 0
resolver-basic 9,239 9,239 0
resolver-nested 25,626 25,626 0
resolver-array 17,862 17,862 0
executor-schedule 4,244 4,244 0
executor-webhook 883 883 0
executor-record 4,746 4,746 0
executor-resolver 4,273 4,273 0
executor-operation-function 877 877 0
executor-operation-gql 879 879 0
executor-operation-webhook 898 898 0
executor-operation-workflow 2,290 2,290 0

Reported by octocov

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 13 additional findings in Devin Review.

Open in Devin Review

Comment thread packages/sdk-codemod/src/index.ts Outdated
@toiroakr toiroakr force-pushed the feat/codemod-infra-v3 branch from 10b4d2e to c132bd5 Compare April 9, 2026 14:25
@toiroakr
Copy link
Copy Markdown
Contributor Author

toiroakr commented Apr 9, 2026

Recreating with clean commit history

@toiroakr toiroakr closed this Apr 9, 2026
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 13 additional findings in Devin Review.

Open in Devin Review

Comment on lines +205 to +209
const sdkImportRegex = /^(import\s+.*from\s+["']@tailor-platform\/sdk["'];?)$/m;
const match = sdkImportRegex.exec(result);
if (match) {
const insertPos = (match.index ?? 0) + match[0].length;
result = result.slice(0, insertPos) + "\n" + importLines.join("\n") + result.slice(insertPos);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 SDK import regex fails to match multi-line imports, causing plugin imports to be prepended at file top

The sdkImportRegex at line 205 uses ^(import\s+.*from\s+["']@tailor-platform\/sdk["'];?)$ with the /m flag. Since .* does not match newlines, this regex cannot match multi-line SDK imports such as:

import {
  defineConfig,
  defineGenerators,
} from "@tailor-platform/sdk";

When the SDK import spans multiple lines, the regex returns no match and the fallback at line 212 prepends the new plugin imports at the very top of the file — before all other imports. This produces valid but poorly ordered output (e.g., import { kyselyTypePlugin } ... appearing above import * as path from "node:path").

Example of incorrect output for multi-line import

Input:

import {
  defineConfig,
  defineGenerators,
} from "@tailor-platform/sdk";
import config from "./tailor.config";

Actual output (plugin import prepended at top):

import { kyselyTypePlugin } from "@tailor-platform/sdk/plugin/kysely-type";
import {
  defineConfig,
  definePlugins,
} from "@tailor-platform/sdk";
import config from "./tailor.config";

Expected output (plugin import after SDK import):

import {
  defineConfig,
  definePlugins,
} from "@tailor-platform/sdk";
import { kyselyTypePlugin } from "@tailor-platform/sdk/plugin/kysely-type";
import config from "./tailor.config";
Prompt for agents
In packages/sdk-codemod/codemods/v2/define-generators-to-plugins/scripts/transform.ts, the sdkImportRegex on line 205 only matches single-line SDK imports because .* does not cross newlines even with the /m flag. When the SDK import spans multiple lines (e.g., import { defineConfig, definePlugins } from "@tailor-platform/sdk" spread across lines), the regex fails to match and the fallback prepends plugin imports at the top of the file.

To fix this, use a regex that can match multi-line imports. One approach is to use [\s\S] instead of .* to allow matching across newlines:
  const sdkImportRegex = /^(import\s+[\s\S]*?from\s+["']@tailor-platform\/sdk["'];?)$/m;

However, this could be fragile with greedy matching across lines. A more robust approach would be to find the last line containing 'from "@tailor-platform/sdk"' (or the SDK import path) and insert after that line, using a simple string search rather than a single-line regex. For example, find the index of the closing of the SDK import statement by searching for the from clause pattern line-by-line.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants