From 3c1571cd76d125854b8379dfb8edcb58c2f517a4 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 29 Apr 2026 16:58:20 +0900 Subject: [PATCH 01/15] feat(sdk-codemod): add v2 codemods for test-run arg, sdk-skills shim, and TailorPrincipal unification Add three codemods that the upgrade runner applies when migrating from 1.x to 2.x: - `v2/test-run-arg-input` strips the deprecated `{ "input": ... }` wrapper from `tailor-sdk function test-run --arg` JSON in package.json scripts, shell scripts, and Markdown code blocks. - `v2/sdk-skills-shim` rewrites `tailor-sdk-skills` invocations to `tailor-sdk skills install` across package.json, shell, YAML, and Markdown files. - `v2/principal-unify` renames TailorUser, TailorActor, and TailorInvoker to the unified TailorPrincipal, drops `unauthenticatedTailorUser` (replacing value references with `null`), and renames `user` to `caller` inside `createResolver` body parameters and member accesses. Generalize the fixture harness so each codemod can declare multiple `tests//` directories with arbitrary file extensions, and treat cases with no `expected.*` as no-change expectations. Wire the new transforms into the registry and tsdown entries, and ignore the fixture trees from oxlint and oxfmt to keep representative user code samples intact. --- .changeset/v2-codemods-phase1.md | 9 + .oxfmtrc.json | 1 + packages/sdk-codemod/.oxlintrc.json | 4 + .../codemods/v2/principal-unify/codemod.yaml | 7 + .../v2/principal-unify/scripts/transform.ts | 250 ++++++++++++++++++ .../principal-unify/tests/basic/expected.ts | 13 + .../v2/principal-unify/tests/basic/input.ts | 13 + .../tests/context-arg/expected.ts | 11 + .../tests/context-arg/input.ts | 11 + .../principal-unify/tests/no-match/input.ts | 5 + .../tests/type-imports/expected.ts | 7 + .../tests/type-imports/input.ts | 7 + .../tests/unauthenticated/expected.ts | 4 + .../tests/unauthenticated/input.ts | 4 + .../codemods/v2/sdk-skills-shim/codemod.yaml | 7 + .../v2/sdk-skills-shim/scripts/transform.ts | 59 +++++ .../tests/basic-package-json/expected.json | 7 + .../tests/basic-package-json/input.json | 7 + .../tests/basic-shell/expected.sh | 5 + .../tests/basic-shell/input.sh | 5 + .../tests/basic-yaml/expected.yml | 5 + .../tests/basic-yaml/input.yml | 5 + .../sdk-skills-shim/tests/no-match/input.sh | 2 + .../v2/test-run-arg-input/codemod.yaml | 7 + .../test-run-arg-input/scripts/transform.ts | 92 +++++++ .../tests/already-migrated/input.sh | 2 + .../tests/basic-markdown/expected.md | 7 + .../tests/basic-markdown/input.md | 7 + .../tests/basic-package-json/expected.json | 7 + .../tests/basic-package-json/input.json | 7 + .../tests/basic-shell/expected.sh | 5 + .../tests/basic-shell/input.sh | 5 + .../tests/multi-key/input.sh | 2 + packages/sdk-codemod/src/registry.ts | 31 +++ packages/sdk-codemod/src/transform.test.ts | 76 +++++- packages/sdk-codemod/tsdown.config.ts | 4 + 36 files changed, 686 insertions(+), 14 deletions(-) create mode 100644 .changeset/v2-codemods-phase1.md create mode 100644 packages/sdk-codemod/.oxlintrc.json create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/codemod.yaml create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/context-arg/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/context-arg/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/no-match/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/unauthenticated/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/unauthenticated/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/sdk-skills-shim/codemod.yaml create mode 100644 packages/sdk-codemod/codemods/v2/sdk-skills-shim/scripts/transform.ts create mode 100644 packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-package-json/expected.json create mode 100644 packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-package-json/input.json create mode 100644 packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-shell/expected.sh create mode 100644 packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-shell/input.sh create mode 100644 packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-yaml/expected.yml create mode 100644 packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-yaml/input.yml create mode 100644 packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/no-match/input.sh create mode 100644 packages/sdk-codemod/codemods/v2/test-run-arg-input/codemod.yaml create mode 100644 packages/sdk-codemod/codemods/v2/test-run-arg-input/scripts/transform.ts create mode 100644 packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/already-migrated/input.sh create mode 100644 packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/basic-markdown/expected.md create mode 100644 packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/basic-markdown/input.md create mode 100644 packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/basic-package-json/expected.json create mode 100644 packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/basic-package-json/input.json create mode 100644 packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/basic-shell/expected.sh create mode 100644 packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/basic-shell/input.sh create mode 100644 packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/multi-key/input.sh diff --git a/.changeset/v2-codemods-phase1.md b/.changeset/v2-codemods-phase1.md new file mode 100644 index 000000000..9e8b1d97d --- /dev/null +++ b/.changeset/v2-codemods-phase1.md @@ -0,0 +1,9 @@ +--- +"@tailor-platform/sdk-codemod": minor +--- + +Add three v2 codemods that the upgrade runner can apply when migrating across the 1.x → 2.x boundary: + +- `v2/test-run-arg-input` strips the deprecated `{ "input": ... }` wrapper from `tailor-sdk function test-run --arg` JSON inside `package.json` scripts, shell scripts, and Markdown code blocks. +- `v2/sdk-skills-shim` rewrites `tailor-sdk-skills` invocations to `tailor-sdk skills install` across `package.json`, shell, YAML, and Markdown files. +- `v2/principal-unify` renames `TailorUser` / `TailorActor` / `TailorInvoker` to the unified `TailorPrincipal`, drops `unauthenticatedTailorUser` (replacing value references with `null`), and renames `user` to `caller` inside `createResolver` body parameters and member accesses. diff --git a/.oxfmtrc.json b/.oxfmtrc.json index 9e663b90e..720b565ee 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -21,6 +21,7 @@ "example/tests/fixtures/", "example/seed/", "packages/tailor-proto/", + "packages/sdk-codemod/codemods/**/tests/", "generated/" ] } diff --git a/packages/sdk-codemod/.oxlintrc.json b/packages/sdk-codemod/.oxlintrc.json new file mode 100644 index 000000000..b089efdef --- /dev/null +++ b/packages/sdk-codemod/.oxlintrc.json @@ -0,0 +1,4 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "ignorePatterns": ["dist/", "codemods/**/tests/**"] +} diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/codemod.yaml b/packages/sdk-codemod/codemods/v2/principal-unify/codemod.yaml new file mode 100644 index 000000000..1dd94b1ac --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/codemod.yaml @@ -0,0 +1,7 @@ +name: "@tailor-platform/principal-unify" +version: "1.0.0" +description: "Unify TailorUser/TailorActor/TailorInvoker into TailorPrincipal and rename resolver body `user` → `caller`" +engine: jssg +language: typescript +since: "1.0.0" +until: "2.0.0" diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts new file mode 100644 index 000000000..6931896ff --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -0,0 +1,250 @@ +import { parse, Lang } from "@ast-grep/napi"; +import type { Edit, SgNode } from "@ast-grep/napi"; + +const TYPE_RENAME_MAP: Record = { + TailorUser: "TailorPrincipal", + TailorActor: "TailorPrincipal", + TailorInvoker: "TailorPrincipal", +}; + +const UNAUTHENTICATED = "unauthenticatedTailorUser"; + +function quickFilter(source: string): boolean { + if (!source.includes("@tailor-platform/sdk")) return false; + return ( + source.includes("TailorUser") || + source.includes("TailorActor") || + source.includes("TailorInvoker") || + source.includes(UNAUTHENTICATED) || + source.includes("createResolver") + ); +} + +function isInsideImportStatement(node: SgNode): boolean { + let current: SgNode | null = node.parent(); + while (current) { + if (current.kind() === "import_statement") return true; + current = current.parent(); + } + return false; +} + +interface ImportRewriteResult { + newText: string; + touched: boolean; +} + +function rebuildImportStatement(importStmt: SgNode): ImportRewriteResult { + const importText = importStmt.text(); + const isImportType = /^\s*import\s+type\b/.test(importText); + const trailingSemi = importText.trimEnd().endsWith(";") ? ";" : ""; + + const specifiers = importStmt.findAll({ rule: { kind: "import_specifier" } }); + const newSpecTexts: string[] = []; + const seenLocal = new Set(); + let touched = false; + + for (const spec of specifiers) { + const specText = spec.text(); + const idents = spec.children().filter((c: SgNode) => c.kind() === "identifier"); + if (idents.length === 0) { + newSpecTexts.push(specText); + continue; + } + const importedName = idents[0]!.text(); + const aliasNode = idents[1]; + const localName = aliasNode?.text() ?? importedName; + const isTypeOnly = /^\s*type\s+/.test(specText); + + const renamed = TYPE_RENAME_MAP[importedName]; + if (renamed) { + touched = true; + const finalLocal = aliasNode?.text() ?? renamed; + if (seenLocal.has(finalLocal)) continue; + seenLocal.add(finalLocal); + const asPart = aliasNode ? ` as ${aliasNode.text()}` : ""; + newSpecTexts.push(`${isTypeOnly ? "type " : ""}${renamed}${asPart}`); + } else if (importedName === UNAUTHENTICATED) { + touched = true; + } else { + if (seenLocal.has(localName)) continue; + seenLocal.add(localName); + newSpecTexts.push(specText); + } + } + + if (!touched) return { newText: importText, touched: false }; + if (newSpecTexts.length === 0) return { newText: "", touched: true }; + + const prefix = isImportType ? "import type " : "import "; + return { + newText: `${prefix}{ ${newSpecTexts.join(", ")} } from "@tailor-platform/sdk"${trailingSemi}`, + touched: true, + }; +} + +function hasLocalUserDeclaration(body: SgNode): boolean { + const declIdents = body.findAll({ + rule: { + kind: "identifier", + regex: "^user$", + inside: { kind: "variable_declarator" }, + }, + }); + if (declIdents.length > 0) return true; + + const declPatterns = body.findAll({ + rule: { + kind: "shorthand_property_identifier_pattern", + regex: "^user$", + inside: { kind: "variable_declarator" }, + }, + }); + return declPatterns.length > 0; +} + +function findResolverBodyArrow(call: SgNode): SgNode | null { + const args = call.field("arguments"); + if (!args) return null; + const objArg = args.children().find((c: SgNode) => c.kind() === "object"); + if (!objArg) return null; + + const pairs = objArg.findAll({ rule: { kind: "pair" } }); + for (const pair of pairs) { + const key = pair.field("key"); + if (key?.text() !== "body") continue; + const value = pair.field("value"); + if (!value) continue; + if (value.kind() === "arrow_function" || value.kind() === "function_expression") { + return value; + } + } + return null; +} + +function transformResolverBody(arrowNode: SgNode, edits: Edit[]): void { + const params = + arrowNode.field("parameters") ?? + arrowNode.field("parameter") ?? + arrowNode.children().find((c: SgNode) => c.kind() === "formal_parameters"); + const body = arrowNode.field("body"); + if (!params || !body) return; + + const userPatterns = params.findAll({ + rule: { kind: "shorthand_property_identifier_pattern", regex: "^user$" }, + }); + if (userPatterns.length > 0) { + if (hasLocalUserDeclaration(body)) return; + for (const p of userPatterns) { + edits.push(p.replace("caller")); + } + const refs = body.findAll({ rule: { kind: "identifier", regex: "^user$" } }); + for (const ref of refs) { + edits.push(ref.replace("caller")); + } + return; + } + + const firstParam = params + .children() + .find( + (c: SgNode) => + c.kind() === "required_parameter" || + c.kind() === "optional_parameter" || + c.kind() === "identifier" || + c.kind() === "object_pattern", + ); + if (!firstParam) return; + if (firstParam.kind() === "object_pattern") return; + + let paramIdent: SgNode | undefined; + if (firstParam.kind() === "identifier") { + paramIdent = firstParam; + } else { + const pattern = firstParam.field("pattern"); + if (pattern && pattern.kind() === "identifier") paramIdent = pattern; + } + if (!paramIdent) return; + + const ctxName = paramIdent.text(); + const propertyAccesses = body.findAll({ + rule: { kind: "property_identifier", regex: "^user$" }, + }); + for (const propId of propertyAccesses) { + const parent = propId.parent(); + if (!parent || parent.kind() !== "member_expression") continue; + const obj = parent.field("object"); + if (obj && obj.kind() === "identifier" && obj.text() === ctxName) { + edits.push(propId.replace("caller")); + } + } +} + +/** + * Migrate user/actor/invoker types and identifiers to the unified TailorPrincipal. + * + * - Renames `TailorUser` / `TailorActor` / `TailorInvoker` type references to `TailorPrincipal`. + * - Rewrites SDK imports to use `TailorPrincipal` (with dedupe) and drops `unauthenticatedTailorUser`. + * - Replaces value references to `unauthenticatedTailorUser` with `null`. + * - Renames `user` to `caller` inside `createResolver({ body })` parameters and bodies, and + * `.user` to `.caller` for non-destructured single-param resolver bodies. + * @param source - TypeScript source text. + * @returns Transformed source or null when nothing matched. + */ +export default function transform(source: string): string | null { + if (!quickFilter(source)) return null; + + const tree = parse(Lang.TypeScript, source).root(); + const edits: Edit[] = []; + + const typeIdents = tree.findAll({ + rule: { + kind: "type_identifier", + not: { inside: { kind: "import_statement" } }, + }, + }); + for (const id of typeIdents) { + const newName = TYPE_RENAME_MAP[id.text()]; + if (newName) edits.push(id.replace(newName)); + } + + const sdkImports = tree.findAll({ + rule: { + kind: "import_statement", + has: { kind: "string", regex: "^[\"']@tailor-platform/sdk[\"']$" }, + }, + }); + for (const importStmt of sdkImports) { + const { newText, touched } = rebuildImportStatement(importStmt); + if (touched) edits.push(importStmt.replace(newText)); + } + + const uauIds = tree.findAll({ + rule: { + kind: "identifier", + regex: `^${UNAUTHENTICATED}$`, + }, + }); + for (const id of uauIds) { + if (isInsideImportStatement(id)) continue; + edits.push(id.replace("null")); + } + + const resolverCalls = tree.findAll({ + rule: { + kind: "call_expression", + has: { + field: "function", + kind: "identifier", + regex: "^createResolver$", + }, + }, + }); + for (const call of resolverCalls) { + const arrow = findResolverBodyArrow(call); + if (arrow) transformResolverBody(arrow, edits); + } + + if (edits.length === 0) return null; + return tree.commitEdits(edits); +} diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/expected.ts new file mode 100644 index 000000000..5c1553ae6 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/expected.ts @@ -0,0 +1,13 @@ +import { createResolver, t, type TailorPrincipal } from "@tailor-platform/sdk"; + +export default createResolver({ + name: "getUser", + operation: "query", + input: t.object({ id: t.string() }), + output: t.object({ id: t.string() }), + body: ({ input, caller }) => { + return { id: caller.id }; + }, +}); + +export const helper = (u: TailorPrincipal) => u.id; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/input.ts new file mode 100644 index 000000000..76bb3630e --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/basic/input.ts @@ -0,0 +1,13 @@ +import { createResolver, t, type TailorUser } from "@tailor-platform/sdk"; + +export default createResolver({ + name: "getUser", + operation: "query", + input: t.object({ id: t.string() }), + output: t.object({ id: t.string() }), + body: ({ input, user }) => { + return { id: user.id }; + }, +}); + +export const helper = (u: TailorUser) => u.id; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/context-arg/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/context-arg/expected.ts new file mode 100644 index 000000000..997a81ed6 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/context-arg/expected.ts @@ -0,0 +1,11 @@ +import { createResolver, t } from "@tailor-platform/sdk"; + +export default createResolver({ + name: "show", + operation: "query", + output: t.object({ id: t.string(), type: t.string() }), + body: (ctx) => ({ + id: ctx.caller.id, + type: ctx.caller.type, + }), +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/context-arg/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/context-arg/input.ts new file mode 100644 index 000000000..41e93d0f2 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/context-arg/input.ts @@ -0,0 +1,11 @@ +import { createResolver, t } from "@tailor-platform/sdk"; + +export default createResolver({ + name: "show", + operation: "query", + output: t.object({ id: t.string(), type: t.string() }), + body: (ctx) => ({ + id: ctx.user.id, + type: ctx.user.type, + }), +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/no-match/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/no-match/input.ts new file mode 100644 index 000000000..f830fa51e --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/no-match/input.ts @@ -0,0 +1,5 @@ +import { defineConfig } from "@tailor-platform/sdk"; + +export default defineConfig({ + name: "demo", +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/expected.ts new file mode 100644 index 000000000..ee7750963 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/expected.ts @@ -0,0 +1,7 @@ +import type { TailorPrincipal } from "@tailor-platform/sdk"; + +export type Props = { + user: TailorPrincipal; + actor: TailorPrincipal; + invoker: TailorPrincipal; +}; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/input.ts new file mode 100644 index 000000000..d42dfc898 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/type-imports/input.ts @@ -0,0 +1,7 @@ +import type { TailorUser, TailorActor, TailorInvoker } from "@tailor-platform/sdk"; + +export type Props = { + user: TailorUser; + actor: TailorActor; + invoker: TailorInvoker; +}; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/unauthenticated/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/unauthenticated/expected.ts new file mode 100644 index 000000000..eaf4c5230 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/unauthenticated/expected.ts @@ -0,0 +1,4 @@ + + +export const fallback = null; +export const id = null.id; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/unauthenticated/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/unauthenticated/input.ts new file mode 100644 index 000000000..cef74d31e --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/unauthenticated/input.ts @@ -0,0 +1,4 @@ +import { unauthenticatedTailorUser } from "@tailor-platform/sdk"; + +export const fallback = unauthenticatedTailorUser; +export const id = unauthenticatedTailorUser.id; diff --git a/packages/sdk-codemod/codemods/v2/sdk-skills-shim/codemod.yaml b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/codemod.yaml new file mode 100644 index 000000000..07ab7bbff --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/codemod.yaml @@ -0,0 +1,7 @@ +name: "@tailor-platform/sdk-skills-shim" +version: "1.0.0" +description: "Replace deprecated `tailor-sdk-skills` binary with `tailor-sdk skills install`" +engine: jssg +language: text +since: "1.0.0" +until: "2.0.0" diff --git a/packages/sdk-codemod/codemods/v2/sdk-skills-shim/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/scripts/transform.ts new file mode 100644 index 000000000..e65e19393 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/scripts/transform.ts @@ -0,0 +1,59 @@ +import * as path from "pathe"; + +const SHIM_PATTERN = /\btailor-sdk-skills\b(?!-)/g; +const REPLACEMENT = "tailor-sdk skills install"; + +function replaceShim(value: string): string { + return value.replace(SHIM_PATTERN, REPLACEMENT); +} + +function transformText(source: string): string | null { + if (!SHIM_PATTERN.test(source)) return null; + SHIM_PATTERN.lastIndex = 0; + const updated = replaceShim(source); + return updated === source ? null : updated; +} + +function transformPackageJson(source: string): string | null { + let parsed: Record; + try { + parsed = JSON.parse(source) as Record; + } catch { + return null; + } + + let modified = false; + const scripts = parsed.scripts; + if (typeof scripts === "object" && scripts != null && !Array.isArray(scripts)) { + for (const [name, value] of Object.entries(scripts as Record)) { + if (typeof value !== "string") continue; + if (!value.includes("tailor-sdk-skills")) continue; + const updated = replaceShim(value); + if (updated !== value) { + (scripts as Record)[name] = updated; + modified = true; + } + } + } + + if (!modified) return null; + const trailing = source.endsWith("\n") ? "\n" : ""; + return JSON.stringify(parsed, null, 2) + trailing; +} + +/** + * Replace `tailor-sdk-skills` invocations with `tailor-sdk skills install`. + * + * The standalone `tailor-sdk-skills` binary is removed in v2; users must call + * the subcommand on the main `tailor-sdk` CLI instead. + * @param source - File contents + * @param filePath - Absolute path to the file (used to dispatch package.json vs text) + * @returns Transformed source or null when nothing matched. + */ +export default function transform(source: string, filePath: string): string | null { + if (!source.includes("tailor-sdk-skills")) return null; + + const ext = path.extname(filePath).toLowerCase(); + if (ext === ".json") return transformPackageJson(source); + return transformText(source); +} diff --git a/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-package-json/expected.json b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-package-json/expected.json new file mode 100644 index 000000000..fddd67a36 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-package-json/expected.json @@ -0,0 +1,7 @@ +{ + "name": "demo", + "scripts": { + "postinstall": "tailor-sdk skills install", + "skills": "pnpm exec tailor-sdk skills install" + } +} diff --git a/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-package-json/input.json b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-package-json/input.json new file mode 100644 index 000000000..6323614ad --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-package-json/input.json @@ -0,0 +1,7 @@ +{ + "name": "demo", + "scripts": { + "postinstall": "tailor-sdk-skills", + "skills": "pnpm exec tailor-sdk-skills" + } +} diff --git a/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-shell/expected.sh b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-shell/expected.sh new file mode 100644 index 000000000..9d6ee6bda --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-shell/expected.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +pnpm exec tailor-sdk skills install +npx tailor-sdk skills install --help diff --git a/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-shell/input.sh b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-shell/input.sh new file mode 100644 index 000000000..0b9e4c78b --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-shell/input.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +pnpm exec tailor-sdk-skills +npx tailor-sdk-skills --help diff --git a/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-yaml/expected.yml b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-yaml/expected.yml new file mode 100644 index 000000000..d410ac2fd --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-yaml/expected.yml @@ -0,0 +1,5 @@ +jobs: + install-skills: + runs-on: ubuntu-latest + steps: + - run: tailor-sdk skills install diff --git a/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-yaml/input.yml b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-yaml/input.yml new file mode 100644 index 000000000..3a4b36762 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/basic-yaml/input.yml @@ -0,0 +1,5 @@ +jobs: + install-skills: + runs-on: ubuntu-latest + steps: + - run: tailor-sdk-skills diff --git a/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/no-match/input.sh b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/no-match/input.sh new file mode 100644 index 000000000..a120be9a0 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/no-match/input.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +echo "no shim references here" diff --git a/packages/sdk-codemod/codemods/v2/test-run-arg-input/codemod.yaml b/packages/sdk-codemod/codemods/v2/test-run-arg-input/codemod.yaml new file mode 100644 index 000000000..4b76f3835 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/test-run-arg-input/codemod.yaml @@ -0,0 +1,7 @@ +name: "@tailor-platform/test-run-arg-input" +version: "1.0.0" +description: "Remove the deprecated {input: ...} wrapper from `function test-run --arg` JSON" +engine: jssg +language: text +since: "1.0.0" +until: "2.0.0" diff --git a/packages/sdk-codemod/codemods/v2/test-run-arg-input/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/test-run-arg-input/scripts/transform.ts new file mode 100644 index 000000000..eab68fc5c --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/test-run-arg-input/scripts/transform.ts @@ -0,0 +1,92 @@ +import * as path from "pathe"; + +const COMMAND_PATTERN = /\btailor-sdk\s+function\s+test-run\b/; +const SHELL_ARG_PATTERN = /(--arg|-a)(\s*=\s*|\s+)(['"`])((?:\\.|(?!\3)[^\\])*)\3/g; + +function isInputWrapper(parsed: unknown): parsed is { input: unknown } { + return ( + typeof parsed === "object" && + parsed !== null && + !Array.isArray(parsed) && + Object.keys(parsed).length === 1 && + "input" in parsed + ); +} + +function unwrapJsonString(body: string, quote: string): string | null { + const decoded = quote === '"' ? body.replace(/\\(["\\])/g, "$1") : body; + let parsed: unknown; + try { + parsed = JSON.parse(decoded); + } catch { + return null; + } + if (!isInputWrapper(parsed)) return null; + const inner = JSON.stringify(parsed.input); + if (quote === '"') { + return inner.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); + } + return inner; +} + +function transformShellLikeText(source: string): string | null { + if (!COMMAND_PATTERN.test(source)) return null; + + let modified = false; + const lines = source.split("\n"); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]!; + if (!COMMAND_PATTERN.test(line)) continue; + const replaced = line.replace(SHELL_ARG_PATTERN, (match, flag, sep, quote, body) => { + const unwrapped = unwrapJsonString(body, quote); + if (unwrapped == null) return match; + modified = true; + return `${flag}${sep}${quote}${unwrapped}${quote}`; + }); + lines[i] = replaced; + } + return modified ? lines.join("\n") : null; +} + +function transformPackageJson(source: string): string | null { + let parsed: Record; + try { + parsed = JSON.parse(source) as Record; + } catch { + return null; + } + const scripts = parsed.scripts; + if (typeof scripts !== "object" || scripts == null || Array.isArray(scripts)) return null; + + let modified = false; + for (const [name, value] of Object.entries(scripts as Record)) { + if (typeof value !== "string") continue; + if (!COMMAND_PATTERN.test(value)) continue; + const updated = transformShellLikeText(value); + if (updated != null) { + (scripts as Record)[name] = updated; + modified = true; + } + } + if (!modified) return null; + + const trailing = source.endsWith("\n") ? "\n" : ""; + return JSON.stringify(parsed, null, 2) + trailing; +} + +/** + * Strip `{ "input": ... }` wrappers from `tailor-sdk function test-run --arg` JSON. + * + * In v2 the resolver `--arg` JSON must be the input fields directly. Old format + * wrapped them under `input` and is removed. + * @param source - File contents + * @param filePath - Absolute path to the file (used to dispatch by extension) + * @returns Transformed source or null when nothing matched. + */ +export default function transform(source: string, filePath: string): string | null { + if (!source.includes("tailor-sdk")) return null; + + const ext = path.extname(filePath).toLowerCase(); + if (ext === ".json") return transformPackageJson(source); + return transformShellLikeText(source); +} diff --git a/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/already-migrated/input.sh b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/already-migrated/input.sh new file mode 100644 index 000000000..fe0c25f71 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/already-migrated/input.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +tailor-sdk function test-run resolvers/add.ts --arg '{"a":1,"b":2}' diff --git a/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/basic-markdown/expected.md b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/basic-markdown/expected.md new file mode 100644 index 000000000..ed6c2f721 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/basic-markdown/expected.md @@ -0,0 +1,7 @@ +# Test runner + +Run a resolver locally: + +```bash +tailor-sdk function test-run resolvers/add.ts -a '{"a":1,"b":2}' +``` diff --git a/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/basic-markdown/input.md b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/basic-markdown/input.md new file mode 100644 index 000000000..2f1b218d4 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/basic-markdown/input.md @@ -0,0 +1,7 @@ +# Test runner + +Run a resolver locally: + +```bash +tailor-sdk function test-run resolvers/add.ts -a '{"input":{"a":1,"b":2}}' +``` diff --git a/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/basic-package-json/expected.json b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/basic-package-json/expected.json new file mode 100644 index 000000000..c547fdf78 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/basic-package-json/expected.json @@ -0,0 +1,7 @@ +{ + "name": "demo", + "scripts": { + "seed": "tailor-sdk function test-run resolvers/seed.ts --arg '{\"users\":[]}'", + "build": "tsdown" + } +} diff --git a/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/basic-package-json/input.json b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/basic-package-json/input.json new file mode 100644 index 000000000..22c1c402f --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/basic-package-json/input.json @@ -0,0 +1,7 @@ +{ + "name": "demo", + "scripts": { + "seed": "tailor-sdk function test-run resolvers/seed.ts --arg '{\"input\":{\"users\":[]}}'", + "build": "tsdown" + } +} diff --git a/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/basic-shell/expected.sh b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/basic-shell/expected.sh new file mode 100644 index 000000000..4dbbba1ed --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/basic-shell/expected.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +tailor-sdk function test-run resolvers/add.ts --arg '{"a":1,"b":2}' +tailor-sdk function test-run resolvers/seed.ts -a '{"users":[]}' diff --git a/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/basic-shell/input.sh b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/basic-shell/input.sh new file mode 100644 index 000000000..9ade45e4f --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/basic-shell/input.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +tailor-sdk function test-run resolvers/add.ts --arg '{"input":{"a":1,"b":2}}' +tailor-sdk function test-run resolvers/seed.ts -a '{"input":{"users":[]}}' diff --git a/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/multi-key/input.sh b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/multi-key/input.sh new file mode 100644 index 000000000..cc71f5aa2 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/multi-key/input.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +tailor-sdk function test-run resolvers/add.ts --arg '{"input":{"a":1},"foo":2}' diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 950f8a7d9..9618370b4 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -16,6 +16,37 @@ const allCodemods: CodemodPackage[] = [ scriptPath: "v2/define-generators-to-plugins/scripts/transform.js", legacyPatterns: ["defineGenerators"], }, + { + id: "v2/test-run-arg-input", + name: "function test-run --arg input unwrap", + description: + "Strip the deprecated {input: ...} wrapper from `tailor-sdk function test-run --arg` JSON in scripts and docs", + since: "1.0.0", + until: "2.0.0", + scriptPath: "v2/test-run-arg-input/scripts/transform.js", + filePatterns: ["**/package.json", "**/*.{sh,bash,zsh}", "**/*.md"], + }, + { + id: "v2/sdk-skills-shim", + name: "tailor-sdk-skills → tailor-sdk skills install", + description: + "Replace deprecated `tailor-sdk-skills` invocations with `tailor-sdk skills install`", + since: "1.0.0", + until: "2.0.0", + scriptPath: "v2/sdk-skills-shim/scripts/transform.js", + filePatterns: ["**/package.json", "**/*.{sh,bash,zsh,yml,yaml}", "**/*.md"], + legacyPatterns: ["tailor-sdk-skills"], + }, + { + id: "v2/principal-unify", + name: "Unify TailorUser/TailorActor/TailorInvoker → TailorPrincipal", + description: + "Rename TailorUser/TailorActor/TailorInvoker to TailorPrincipal, drop unauthenticatedTailorUser, and rename resolver body `user` to `caller`", + since: "1.0.0", + until: "2.0.0", + scriptPath: "v2/principal-unify/scripts/transform.js", + legacyPatterns: ["TailorUser", "TailorActor", "TailorInvoker", "unauthenticatedTailorUser"], + }, ]; /** diff --git a/packages/sdk-codemod/src/transform.test.ts b/packages/sdk-codemod/src/transform.test.ts index 9e30b4c04..c2a919762 100644 --- a/packages/sdk-codemod/src/transform.test.ts +++ b/packages/sdk-codemod/src/transform.test.ts @@ -5,29 +5,77 @@ import type { TransformFn } from "./runner"; const CODEMODS_DIR = path.resolve(__dirname, "../codemods"); -/** - * Run a transform against its fixture files (input.ts → expected.ts). - * @param codemodPath - Relative path from the codemods root - */ -async function runFixtureTest(codemodPath: string): Promise { - const scriptPath = path.join(CODEMODS_DIR, codemodPath, "scripts/transform.ts"); - const inputPath = path.join(CODEMODS_DIR, codemodPath, "tests/basic/input.ts"); - const expectedPath = path.join(CODEMODS_DIR, codemodPath, "tests/basic/expected.ts"); +interface FixtureCase { + caseName: string; + caseDir: string; + inputFile: string; + expectedFile: string | null; +} - const input = await fs.promises.readFile(inputPath, "utf-8"); - const expected = await fs.promises.readFile(expectedPath, "utf-8"); +async function discoverCases(codemodPath: string): Promise { + const testsDir = path.join(CODEMODS_DIR, codemodPath, "tests"); + const entries = await fs.promises.readdir(testsDir, { withFileTypes: true }); + const cases: FixtureCase[] = []; + for (const entry of entries) { + if (!entry.isDirectory()) continue; + const caseDir = path.join(testsDir, entry.name); + const files = await fs.promises.readdir(caseDir); + const inputFile = files.find((f) => f.startsWith("input.")); + const expectedFile = files.find((f) => f.startsWith("expected.")); + if (!inputFile) { + throw new Error(`No input.* file found in fixture ${caseDir}`); + } + cases.push({ + caseName: entry.name, + caseDir, + inputFile, + expectedFile: expectedFile ?? null, + }); + } + + cases.sort((a, b) => a.caseName.localeCompare(b.caseName)); + return cases; +} + +async function runFixtureCases(codemodPath: string): Promise { + const scriptPath = path.join(CODEMODS_DIR, codemodPath, "scripts/transform.ts"); const mod = await import(scriptPath); const transform = mod.default as TransformFn; - const result = await transform(input, inputPath); + const cases = await discoverCases(codemodPath); + expect(cases.length, `expected at least one fixture under ${codemodPath}/tests`).toBeGreaterThan( + 0, + ); + + for (const c of cases) { + const inputPath = path.join(c.caseDir, c.inputFile); + const input = await fs.promises.readFile(inputPath, "utf-8"); + const result = await transform(input, inputPath); - expect(result).not.toBeNull(); - expect(result).toBe(expected); + if (c.expectedFile) { + const expected = await fs.promises.readFile(path.join(c.caseDir, c.expectedFile), "utf-8"); + expect(result, `${codemodPath}/${c.caseName}`).toBe(expected); + } else { + expect(result, `${codemodPath}/${c.caseName} (no expected.* → expect no change)`).toBeNull(); + } + } } describe("codemod transforms", () => { it("v2/define-generators-to-plugins transforms correctly", async () => { - await runFixtureTest("v2/define-generators-to-plugins"); + await runFixtureCases("v2/define-generators-to-plugins"); + }); + + it("v2/test-run-arg-input transforms correctly", async () => { + await runFixtureCases("v2/test-run-arg-input"); + }); + + it("v2/sdk-skills-shim transforms correctly", async () => { + await runFixtureCases("v2/sdk-skills-shim"); + }); + + it("v2/principal-unify transforms correctly", async () => { + await runFixtureCases("v2/principal-unify"); }); }); diff --git a/packages/sdk-codemod/tsdown.config.ts b/packages/sdk-codemod/tsdown.config.ts index 54bba9aba..0f2137ace 100644 --- a/packages/sdk-codemod/tsdown.config.ts +++ b/packages/sdk-codemod/tsdown.config.ts @@ -17,6 +17,10 @@ export default defineConfig([ entry: { "v2/define-generators-to-plugins/scripts/transform": "codemods/v2/define-generators-to-plugins/scripts/transform.ts", + "v2/test-run-arg-input/scripts/transform": + "codemods/v2/test-run-arg-input/scripts/transform.ts", + "v2/sdk-skills-shim/scripts/transform": "codemods/v2/sdk-skills-shim/scripts/transform.ts", + "v2/principal-unify/scripts/transform": "codemods/v2/principal-unify/scripts/transform.ts", }, format: ["esm"], target: "node18", From 7222ad0537cb6de153909f7a75b253aceb16cadc Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 29 Apr 2026 20:13:20 +0900 Subject: [PATCH 02/15] refactor(sdk-codemod): scope-aware shadow handling and import-removal cleanup in v2/principal-unify Resolve the two caveats noted on the codemod: - When `unauthenticatedTailorUser` import removal empties an SDK import statement, post-process the result to drop leading blank lines and collapse runs of three or more newlines down to one empty line. - For `createResolver` bodies that locally redeclare `user`, collect the byte ranges of the enclosing scopes and skip renaming `user` references within them. Param-level destructure and references outside the shadow zone are still rewritten to `caller`. Add a `shadow` fixture covering an inner-block redeclaration and trim the leading blank lines from the `unauthenticated` expected fixture. --- .../v2/principal-unify/scripts/transform.ts | 86 ++++++++++++++----- .../principal-unify/tests/shadow/expected.ts | 15 ++++ .../v2/principal-unify/tests/shadow/input.ts | 15 ++++ .../tests/unauthenticated/expected.ts | 2 - 4 files changed, 96 insertions(+), 22 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/shadow/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/shadow/input.ts diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index 6931896ff..770637918 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -83,24 +83,60 @@ function rebuildImportStatement(importStmt: SgNode): ImportRewriteResult { }; } -function hasLocalUserDeclaration(body: SgNode): boolean { - const declIdents = body.findAll({ - rule: { - kind: "identifier", - regex: "^user$", - inside: { kind: "variable_declarator" }, - }, - }); - if (declIdents.length > 0) return true; +const SCOPE_KINDS = new Set([ + "statement_block", + "function_body", + "for_statement", + "for_in_statement", + "for_of_statement", + "arrow_function", + "function_expression", + "function_declaration", + "method_definition", +]); - const declPatterns = body.findAll({ - rule: { - kind: "shorthand_property_identifier_pattern", - regex: "^user$", - inside: { kind: "variable_declarator" }, - }, - }); - return declPatterns.length > 0; +/** + * Collect byte ranges of scopes where `name` is locally redeclared. + * + * Any identifier reference whose start position falls inside one of these ranges + * is shadowed by the local binding and must not be renamed by the param rewrite. + * @param body - The arrow body node to scan. + * @param name - The identifier name to detect declarations of. + * @returns Sorted array of [startByte, endByte] ranges (half-open). + */ +function collectShadowBlockRanges(body: SgNode, name: string): Array<[number, number]> { + const ranges: Array<[number, number]> = []; + const declarations = [ + ...body.findAll({ + rule: { + kind: "identifier", + regex: `^${name}$`, + inside: { kind: "variable_declarator" }, + }, + }), + ...body.findAll({ + rule: { + kind: "shorthand_property_identifier_pattern", + regex: `^${name}$`, + inside: { kind: "variable_declarator" }, + }, + }), + ]; + + for (const decl of declarations) { + let scope: SgNode | null = decl.parent(); + while (scope && !SCOPE_KINDS.has(scope.kind())) { + scope = scope.parent(); + } + if (!scope) continue; + const range = scope.range(); + ranges.push([range.start.index, range.end.index]); + } + return ranges; +} + +function isInsideAnyRange(pos: number, ranges: Array<[number, number]>): boolean { + return ranges.some(([s, e]) => pos >= s && pos < e); } function findResolverBodyArrow(call: SgNode): SgNode | null { @@ -134,12 +170,14 @@ function transformResolverBody(arrowNode: SgNode, edits: Edit[]): void { rule: { kind: "shorthand_property_identifier_pattern", regex: "^user$" }, }); if (userPatterns.length > 0) { - if (hasLocalUserDeclaration(body)) return; + const shadowRanges = collectShadowBlockRanges(body, "user"); for (const p of userPatterns) { edits.push(p.replace("caller")); } const refs = body.findAll({ rule: { kind: "identifier", regex: "^user$" } }); for (const ref of refs) { + const pos = ref.range().start.index; + if (isInsideAnyRange(pos, shadowRanges)) continue; edits.push(ref.replace("caller")); } return; @@ -214,9 +252,12 @@ export default function transform(source: string): string | null { has: { kind: "string", regex: "^[\"']@tailor-platform/sdk[\"']$" }, }, }); + let importRemoved = false; for (const importStmt of sdkImports) { const { newText, touched } = rebuildImportStatement(importStmt); - if (touched) edits.push(importStmt.replace(newText)); + if (!touched) continue; + edits.push(importStmt.replace(newText)); + if (newText === "") importRemoved = true; } const uauIds = tree.findAll({ @@ -246,5 +287,10 @@ export default function transform(source: string): string | null { } if (edits.length === 0) return null; - return tree.commitEdits(edits); + let result = tree.commitEdits(edits); + + if (importRemoved) { + result = result.replace(/^[\t ]*\n+/, "").replace(/\n{3,}/g, "\n\n"); + } + return result; } diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/shadow/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/shadow/expected.ts new file mode 100644 index 000000000..bf9246993 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/shadow/expected.ts @@ -0,0 +1,15 @@ +import { createResolver, t } from "@tailor-platform/sdk"; + +export default createResolver({ + name: "shadow", + operation: "query", + input: t.object({ flag: t.bool() }), + output: t.object({ id: t.string() }), + body: ({ input, caller }) => { + if (input.flag) { + const user = { id: "fake" }; + return { id: user.id }; + } + return { id: caller.id }; + }, +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/shadow/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/shadow/input.ts new file mode 100644 index 000000000..d1e2a95b3 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/shadow/input.ts @@ -0,0 +1,15 @@ +import { createResolver, t } from "@tailor-platform/sdk"; + +export default createResolver({ + name: "shadow", + operation: "query", + input: t.object({ flag: t.bool() }), + output: t.object({ id: t.string() }), + body: ({ input, user }) => { + if (input.flag) { + const user = { id: "fake" }; + return { id: user.id }; + } + return { id: user.id }; + }, +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/unauthenticated/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/unauthenticated/expected.ts index eaf4c5230..89a90afb8 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/unauthenticated/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/unauthenticated/expected.ts @@ -1,4 +1,2 @@ - - export const fallback = null; export const id = null.id; From 452d3da7371bf44539c5295dd64b65aa3b3d6515 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 29 Apr 2026 20:43:48 +0900 Subject: [PATCH 03/15] fix(sdk-codemod): harden v2/principal-unify against breakage edge cases - Skip the null rewrite when unauthenticatedTailorUser is the object of a member expression so the codemod no longer emits null.id; the import is still dropped so the resulting type error points authors at the broken access. - Match @tailor-platform/sdk/test imports as well so the documented test helper unauthenticatedTailorUser is removed there too. - Restrict resolver-body destructuring rewrites to the top-level object pattern. Nested patterns like ({ input: { user } }) are left alone, and aliased pairs like ({ user: currentUser }) now rewrite the property key to caller while preserving the local binding. - Dedupe TailorPrincipal across multiple SDK import statements so a file that splits TailorUser and TailorActor across imports collapses to one TailorPrincipal binding. - Skip ctx.user rewrites that sit inside nested functions which re-bind the resolver context name (items.map((ctx) => ctx.user.id)). Add fixtures for each case and update the unauthenticated expected output, which previously asserted the broken null.id behavior. --- .changeset/v2-codemods-phase1.md | 2 +- .../v2/principal-unify/scripts/transform.ts | 172 ++++++++++++++---- .../tests/aliased-destructure/expected.ts | 8 + .../tests/aliased-destructure/input.ts | 8 + .../tests/dedupe-imports/expected.ts | 6 + .../tests/dedupe-imports/input.ts | 6 + .../tests/nested-ctx-shadow/expected.ts | 13 ++ .../tests/nested-ctx-shadow/input.ts | 13 ++ .../tests/nested-destructure/input.ts | 8 + .../unauthenticated-sdk-test/expected.ts | 1 + .../tests/unauthenticated-sdk-test/input.ts | 3 + .../tests/unauthenticated/expected.ts | 2 +- 12 files changed, 204 insertions(+), 38 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-destructure/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-destructure/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/dedupe-imports/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/dedupe-imports/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/nested-ctx-shadow/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/nested-ctx-shadow/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/nested-destructure/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/unauthenticated-sdk-test/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/unauthenticated-sdk-test/input.ts diff --git a/.changeset/v2-codemods-phase1.md b/.changeset/v2-codemods-phase1.md index 9e8b1d97d..918cea19e 100644 --- a/.changeset/v2-codemods-phase1.md +++ b/.changeset/v2-codemods-phase1.md @@ -6,4 +6,4 @@ Add three v2 codemods that the upgrade runner can apply when migrating across th - `v2/test-run-arg-input` strips the deprecated `{ "input": ... }` wrapper from `tailor-sdk function test-run --arg` JSON inside `package.json` scripts, shell scripts, and Markdown code blocks. - `v2/sdk-skills-shim` rewrites `tailor-sdk-skills` invocations to `tailor-sdk skills install` across `package.json`, shell, YAML, and Markdown files. -- `v2/principal-unify` renames `TailorUser` / `TailorActor` / `TailorInvoker` to the unified `TailorPrincipal`, drops `unauthenticatedTailorUser` (replacing value references with `null`), and renames `user` to `caller` inside `createResolver` body parameters and member accesses. +- `v2/principal-unify` renames `TailorUser` / `TailorActor` / `TailorInvoker` to the unified `TailorPrincipal`, drops `unauthenticatedTailorUser` (replacing standalone value references with `null`; member-access forms are left as-is so the resulting type error points authors at sites that need manual review), and renames `user` to `caller` inside `createResolver` body parameters and member accesses. diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index 770637918..63bc3ae87 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -29,15 +29,34 @@ function isInsideImportStatement(node: SgNode): boolean { return false; } +function isMemberExpressionObject(node: SgNode): boolean { + const parent = node.parent(); + if (!parent || parent.kind() !== "member_expression") return false; + const obj = parent.field("object"); + if (!obj) return false; + const r = node.range(); + const or = obj.range(); + return r.start.index === or.start.index && r.end.index === or.end.index; +} + interface ImportRewriteResult { newText: string; touched: boolean; } -function rebuildImportStatement(importStmt: SgNode): ImportRewriteResult { +function extractModuleSource(importText: string): string { + const m = importText.match(/from\s+(["'])([^"']+)\1/); + return m?.[2] ?? "@tailor-platform/sdk"; +} + +function rebuildImportStatement( + importStmt: SgNode, + globalEmittedRenamed: Set, +): ImportRewriteResult { const importText = importStmt.text(); const isImportType = /^\s*import\s+type\b/.test(importText); const trailingSemi = importText.trimEnd().endsWith(";") ? ";" : ""; + const sourceRaw = extractModuleSource(importText); const specifiers = importStmt.findAll({ rule: { kind: "import_specifier" } }); const newSpecTexts: string[] = []; @@ -61,7 +80,13 @@ function rebuildImportStatement(importStmt: SgNode): ImportRewriteResult { touched = true; const finalLocal = aliasNode?.text() ?? renamed; if (seenLocal.has(finalLocal)) continue; + // Cross-statement dedupe for non-aliased renames so a file with + // `import { TailorUser } from "@tailor-platform/sdk"` and + // `import { TailorActor } from "@tailor-platform/sdk"` does not collapse to + // two duplicate `import { TailorPrincipal } ...` lines. + if (!aliasNode && globalEmittedRenamed.has(renamed)) continue; seenLocal.add(finalLocal); + if (!aliasNode) globalEmittedRenamed.add(renamed); const asPart = aliasNode ? ` as ${aliasNode.text()}` : ""; newSpecTexts.push(`${isTypeOnly ? "type " : ""}${renamed}${asPart}`); } else if (importedName === UNAUTHENTICATED) { @@ -78,7 +103,7 @@ function rebuildImportStatement(importStmt: SgNode): ImportRewriteResult { const prefix = isImportType ? "import type " : "import "; return { - newText: `${prefix}{ ${newSpecTexts.join(", ")} } from "@tailor-platform/sdk"${trailingSemi}`, + newText: `${prefix}{ ${newSpecTexts.join(", ")} } from "${sourceRaw}"${trailingSemi}`, touched: true, }; } @@ -95,6 +120,13 @@ const SCOPE_KINDS = new Set([ "method_definition", ]); +const NESTED_FN_KINDS = [ + "arrow_function", + "function_expression", + "function_declaration", + "method_definition", +]; + /** * Collect byte ranges of scopes where `name` is locally redeclared. * @@ -139,6 +171,53 @@ function isInsideAnyRange(pos: number, ranges: Array<[number, number]>): boolean return ranges.some(([s, e]) => pos >= s && pos < e); } +function functionRebindsName(fn: SgNode, name: string): boolean { + const single = fn.field("parameter"); + if (single && single.kind() === "identifier" && single.text() === name) return true; + const params = + fn.field("parameters") ?? fn.children().find((c: SgNode) => c.kind() === "formal_parameters"); + if (!params) return false; + for (const child of params.children()) { + const k = child.kind(); + if (k === "identifier" && child.text() === name) return true; + if (k === "required_parameter" || k === "optional_parameter") { + const pat = child.field("pattern"); + if (pat && pat.kind() === "identifier" && pat.text() === name) return true; + } + } + return false; +} + +/** + * Collect byte ranges of inner functions that re-bind `ctxName` as a parameter. + * + * Member-accesses to `ctxName.user` whose start byte falls inside any of these + * ranges refer to the inner function's parameter, not the resolver context, and + * must not be renamed. + * @param body - The resolver body node. + * @param ctxName - The context parameter identifier name. + * @param resolverArrow - The resolver's outer arrow/function expression to exclude. + */ +function collectCtxShadowRanges( + body: SgNode, + ctxName: string, + resolverArrow: SgNode, +): Array<[number, number]> { + const ranges: Array<[number, number]> = []; + const ar = resolverArrow.range(); + for (const k of NESTED_FN_KINDS) { + const fns = body.findAll({ rule: { kind: k } }); + for (const fn of fns) { + const r = fn.range(); + if (r.start.index === ar.start.index && r.end.index === ar.end.index) continue; + if (functionRebindsName(fn, ctxName)) { + ranges.push([r.start.index, r.end.index]); + } + } + } + return ranges; +} + function findResolverBodyArrow(call: SgNode): SgNode | null { const args = call.field("arguments"); if (!args) return null; @@ -166,23 +245,6 @@ function transformResolverBody(arrowNode: SgNode, edits: Edit[]): void { const body = arrowNode.field("body"); if (!params || !body) return; - const userPatterns = params.findAll({ - rule: { kind: "shorthand_property_identifier_pattern", regex: "^user$" }, - }); - if (userPatterns.length > 0) { - const shadowRanges = collectShadowBlockRanges(body, "user"); - for (const p of userPatterns) { - edits.push(p.replace("caller")); - } - const refs = body.findAll({ rule: { kind: "identifier", regex: "^user$" } }); - for (const ref of refs) { - const pos = ref.range().start.index; - if (isInsideAnyRange(pos, shadowRanges)) continue; - edits.push(ref.replace("caller")); - } - return; - } - const firstParam = params .children() .find( @@ -193,18 +255,48 @@ function transformResolverBody(arrowNode: SgNode, edits: Edit[]): void { c.kind() === "object_pattern", ); if (!firstParam) return; - if (firstParam.kind() === "object_pattern") return; - let paramIdent: SgNode | undefined; - if (firstParam.kind() === "identifier") { - paramIdent = firstParam; + let pattern: SgNode | undefined; + if (firstParam.kind() === "object_pattern" || firstParam.kind() === "identifier") { + pattern = firstParam; } else { - const pattern = firstParam.field("pattern"); - if (pattern && pattern.kind() === "identifier") paramIdent = pattern; + const inner = firstParam.field("pattern"); + if (inner) pattern = inner; } - if (!paramIdent) return; + if (!pattern) return; - const ctxName = paramIdent.text(); + if (pattern.kind() === "object_pattern") { + let renamedShorthandUser = false; + // Only iterate top-level pattern children so nested destructures like + // `({ input: { user } })` are not mistaken for the resolver context user. + for (const child of pattern.children()) { + const kind = child.kind(); + if (kind === "shorthand_property_identifier_pattern" && child.text() === "user") { + edits.push(child.replace("caller")); + renamedShorthandUser = true; + } else if (kind === "pair_pattern") { + const key = child.field("key"); + if (key && key.text() === "user") { + edits.push(key.replace("caller")); + } + } + } + if (renamedShorthandUser) { + const shadowRanges = collectShadowBlockRanges(body, "user"); + const refs = body.findAll({ rule: { kind: "identifier", regex: "^user$" } }); + for (const ref of refs) { + const pos = ref.range().start.index; + if (isInsideAnyRange(pos, shadowRanges)) continue; + edits.push(ref.replace("caller")); + } + } + return; + } + + // Single identifier param: rewrite `.user` → `.caller`, but skip + // member accesses that sit inside a nested function which re-binds ``. + const ctxName = pattern.text(); + const ctxShadowRanges = collectCtxShadowRanges(body, ctxName, arrowNode); const propertyAccesses = body.findAll({ rule: { kind: "property_identifier", regex: "^user$" }, }); @@ -212,9 +304,10 @@ function transformResolverBody(arrowNode: SgNode, edits: Edit[]): void { const parent = propId.parent(); if (!parent || parent.kind() !== "member_expression") continue; const obj = parent.field("object"); - if (obj && obj.kind() === "identifier" && obj.text() === ctxName) { - edits.push(propId.replace("caller")); - } + if (!(obj && obj.kind() === "identifier" && obj.text() === ctxName)) continue; + const pos = obj.range().start.index; + if (isInsideAnyRange(pos, ctxShadowRanges)) continue; + edits.push(propId.replace("caller")); } } @@ -222,10 +315,15 @@ function transformResolverBody(arrowNode: SgNode, edits: Edit[]): void { * Migrate user/actor/invoker types and identifiers to the unified TailorPrincipal. * * - Renames `TailorUser` / `TailorActor` / `TailorInvoker` type references to `TailorPrincipal`. - * - Rewrites SDK imports to use `TailorPrincipal` (with dedupe) and drops `unauthenticatedTailorUser`. - * - Replaces value references to `unauthenticatedTailorUser` with `null`. - * - Renames `user` to `caller` inside `createResolver({ body })` parameters and bodies, and - * `.user` to `.caller` for non-destructured single-param resolver bodies. + * - Rewrites SDK imports (including the `/test` subpath) to use `TailorPrincipal` (deduped + * across statements) and drops `unauthenticatedTailorUser`. + * - Replaces standalone references to `unauthenticatedTailorUser` with `null`. Member-access + * forms like `unauthenticatedTailorUser.id` are left alone on purpose so the resulting TS + * error after the import is removed points the author at the broken access. + * - Renames `user` to `caller` for top-level destructured resolver bodies (`{ input, user }`), + * handles aliased pairs (`{ user: currentUser }`) by rewriting only the property name, and + * rewrites `.user` for non-destructured single-param bodies — respecting variable + * shadowing in both directions. * @param source - TypeScript source text. * @returns Transformed source or null when nothing matched. */ @@ -249,12 +347,13 @@ export default function transform(source: string): string | null { const sdkImports = tree.findAll({ rule: { kind: "import_statement", - has: { kind: "string", regex: "^[\"']@tailor-platform/sdk[\"']$" }, + has: { kind: "string", regex: "^[\"']@tailor-platform/sdk(/test)?[\"']$" }, }, }); let importRemoved = false; + const globalEmittedRenamed = new Set(); for (const importStmt of sdkImports) { - const { newText, touched } = rebuildImportStatement(importStmt); + const { newText, touched } = rebuildImportStatement(importStmt, globalEmittedRenamed); if (!touched) continue; edits.push(importStmt.replace(newText)); if (newText === "") importRemoved = true; @@ -268,6 +367,7 @@ export default function transform(source: string): string | null { }); for (const id of uauIds) { if (isInsideImportStatement(id)) continue; + if (isMemberExpressionObject(id)) continue; edits.push(id.replace("null")); } diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-destructure/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-destructure/expected.ts new file mode 100644 index 000000000..3aa4c354f --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-destructure/expected.ts @@ -0,0 +1,8 @@ +import { createResolver, t } from "@tailor-platform/sdk"; + +export default createResolver({ + name: "n", + operation: "query", + output: t.string(), + body: ({ caller: currentUser }) => currentUser.id, +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-destructure/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-destructure/input.ts new file mode 100644 index 000000000..1a98f5ae2 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-destructure/input.ts @@ -0,0 +1,8 @@ +import { createResolver, t } from "@tailor-platform/sdk"; + +export default createResolver({ + name: "n", + operation: "query", + output: t.string(), + body: ({ user: currentUser }) => currentUser.id, +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/dedupe-imports/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/dedupe-imports/expected.ts new file mode 100644 index 000000000..e3443d47d --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/dedupe-imports/expected.ts @@ -0,0 +1,6 @@ +import { TailorPrincipal } from "@tailor-platform/sdk"; +import { foo } from "@tailor-platform/sdk"; + +export type X = TailorPrincipal; +export type Y = TailorPrincipal; +export const v = foo; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/dedupe-imports/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/dedupe-imports/input.ts new file mode 100644 index 000000000..73aa9a3fb --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/dedupe-imports/input.ts @@ -0,0 +1,6 @@ +import { TailorUser } from "@tailor-platform/sdk"; +import { TailorActor, foo } from "@tailor-platform/sdk"; + +export type X = TailorUser; +export type Y = TailorActor; +export const v = foo; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/nested-ctx-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/nested-ctx-shadow/expected.ts new file mode 100644 index 000000000..a645dde3d --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/nested-ctx-shadow/expected.ts @@ -0,0 +1,13 @@ +import { createResolver, t } from "@tailor-platform/sdk"; + +const items: Array<{ user: { id: string } }> = []; + +export default createResolver({ + name: "n", + operation: "query", + output: t.array(t.string()), + body: (ctx) => ({ + me: ctx.caller.id, + others: items.map((ctx) => ctx.user.id), + }), +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/nested-ctx-shadow/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/nested-ctx-shadow/input.ts new file mode 100644 index 000000000..37e1ffc95 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/nested-ctx-shadow/input.ts @@ -0,0 +1,13 @@ +import { createResolver, t } from "@tailor-platform/sdk"; + +const items: Array<{ user: { id: string } }> = []; + +export default createResolver({ + name: "n", + operation: "query", + output: t.array(t.string()), + body: (ctx) => ({ + me: ctx.user.id, + others: items.map((ctx) => ctx.user.id), + }), +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/nested-destructure/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/nested-destructure/input.ts new file mode 100644 index 000000000..a48c00f60 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/nested-destructure/input.ts @@ -0,0 +1,8 @@ +import { createResolver, t } from "@tailor-platform/sdk"; + +export default createResolver({ + name: "n", + operation: "query", + output: t.string(), + body: ({ input: { user } }) => user, +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/unauthenticated-sdk-test/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/unauthenticated-sdk-test/expected.ts new file mode 100644 index 000000000..f3ffcf102 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/unauthenticated-sdk-test/expected.ts @@ -0,0 +1 @@ +export const fallback = null; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/unauthenticated-sdk-test/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/unauthenticated-sdk-test/input.ts new file mode 100644 index 000000000..cd918d18e --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/unauthenticated-sdk-test/input.ts @@ -0,0 +1,3 @@ +import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test"; + +export const fallback = unauthenticatedTailorUser; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/unauthenticated/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/unauthenticated/expected.ts index 89a90afb8..987f524e6 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/unauthenticated/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/unauthenticated/expected.ts @@ -1,2 +1,2 @@ export const fallback = null; -export const id = null.id; +export const id = unauthenticatedTailorUser.id; From ae4b8c5ba4614cdebc86d3fa2ac6bea7d7d18b09 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 29 Apr 2026 20:54:40 +0900 Subject: [PATCH 04/15] fix(sdk-codemod): cover aliases, object-literal shorthand, ctx destructure, chained shell commands principal-unify: - Track local names introduced by aliased imports (e.g. unauthenticatedTailorUser as testUser) and rewrite their references to null too. - Include shorthand_property_identifier (object literal shorthand) in body rewrites so ({ user }) => ({ user }) becomes ({ caller }) => ({ caller }) instead of leaving the return value referencing an undefined binding. - Rewrite destructures that read from the resolver context, so const { user } = ctx becomes const { caller: user } = ctx, preserving the local binding name. test-run-arg-input: - Tokenize lines on unquoted shell command boundaries (;, &&, ||, |, &) and only run the {input: ...} unwrap on segments that actually contain tailor-sdk function test-run, so chained commands like `tailor-sdk function test-run ... --arg '{"input":...}' && other-cli --arg '{"input":...}'` no longer corrupt the unrelated argument. Add fixtures for each new case (aliased-unauthenticated, object-shorthand-return, ctx-destructure, chained-commands). --- .../v2/principal-unify/scripts/transform.ts | 77 +++++++++++++--- .../tests/aliased-unauthenticated/expected.ts | 1 + .../tests/aliased-unauthenticated/input.ts | 3 + .../tests/ctx-destructure/expected.ts | 11 +++ .../tests/ctx-destructure/input.ts | 11 +++ .../tests/object-shorthand-return/expected.ts | 8 ++ .../tests/object-shorthand-return/input.ts | 8 ++ .../test-run-arg-input/scripts/transform.ts | 87 +++++++++++++++++-- .../tests/chained-commands/expected.sh | 2 + .../tests/chained-commands/input.sh | 2 + 10 files changed, 191 insertions(+), 19 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-unauthenticated/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-unauthenticated/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/ctx-destructure/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/ctx-destructure/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/object-shorthand-return/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/object-shorthand-return/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/chained-commands/expected.sh create mode 100644 packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/chained-commands/input.sh diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index 63bc3ae87..2e314b486 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -49,9 +49,14 @@ function extractModuleSource(importText: string): string { return m?.[2] ?? "@tailor-platform/sdk"; } +function escapeRegex(s: string): string { + return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + function rebuildImportStatement( importStmt: SgNode, globalEmittedRenamed: Set, + unauthenticatedLocalNames: Set, ): ImportRewriteResult { const importText = importStmt.text(); const isImportType = /^\s*import\s+type\b/.test(importText); @@ -91,6 +96,10 @@ function rebuildImportStatement( newSpecTexts.push(`${isTypeOnly ? "type " : ""}${renamed}${asPart}`); } else if (importedName === UNAUTHENTICATED) { touched = true; + // Track the local binding so aliased forms like + // `import { unauthenticatedTailorUser as testUser } ...` get their references + // rewritten to `null` alongside the canonical name. + unauthenticatedLocalNames.add(localName); } else { if (seenLocal.has(localName)) continue; seenLocal.add(localName); @@ -284,7 +293,13 @@ function transformResolverBody(arrowNode: SgNode, edits: Edit[]): void { if (renamedShorthandUser) { const shadowRanges = collectShadowBlockRanges(body, "user"); const refs = body.findAll({ rule: { kind: "identifier", regex: "^user$" } }); - for (const ref of refs) { + // Object literal shorthand uses `shorthand_property_identifier` (no `_pattern` + // suffix), so include those too — otherwise `({ user }) => ({ user })` becomes + // `({ caller }) => ({ user })` and references an undefined variable. + const shortRefs = body.findAll({ + rule: { kind: "shorthand_property_identifier", regex: "^user$" }, + }); + for (const ref of [...refs, ...shortRefs]) { const pos = ref.range().start.index; if (isInsideAnyRange(pos, shadowRanges)) continue; edits.push(ref.replace("caller")); @@ -309,6 +324,37 @@ function transformResolverBody(arrowNode: SgNode, edits: Edit[]): void { if (isInsideAnyRange(pos, ctxShadowRanges)) continue; edits.push(propId.replace("caller")); } + + // Also rewrite destructures of the context, e.g. `const { user } = ctx;` → + // `const { caller: user } = ctx;`. Local bindings stay the same so existing + // body references keep working. + const ctxDestructures = body.findAll({ + rule: { + kind: "variable_declarator", + has: { + field: "value", + kind: "identifier", + regex: `^${escapeRegex(ctxName)}$`, + }, + }, + }); + for (const decl of ctxDestructures) { + const pos = decl.range().start.index; + if (isInsideAnyRange(pos, ctxShadowRanges)) continue; + const pat = decl.field("name"); + if (!pat || pat.kind() !== "object_pattern") continue; + for (const child of pat.children()) { + const k = child.kind(); + if (k === "shorthand_property_identifier_pattern" && child.text() === "user") { + edits.push(child.replace("caller: user")); + } else if (k === "pair_pattern") { + const key = child.field("key"); + if (key && key.text() === "user") { + edits.push(key.replace("caller")); + } + } + } + } } /** @@ -352,23 +398,30 @@ export default function transform(source: string): string | null { }); let importRemoved = false; const globalEmittedRenamed = new Set(); + const unauthenticatedLocalNames = new Set([UNAUTHENTICATED]); for (const importStmt of sdkImports) { - const { newText, touched } = rebuildImportStatement(importStmt, globalEmittedRenamed); + const { newText, touched } = rebuildImportStatement( + importStmt, + globalEmittedRenamed, + unauthenticatedLocalNames, + ); if (!touched) continue; edits.push(importStmt.replace(newText)); if (newText === "") importRemoved = true; } - const uauIds = tree.findAll({ - rule: { - kind: "identifier", - regex: `^${UNAUTHENTICATED}$`, - }, - }); - for (const id of uauIds) { - if (isInsideImportStatement(id)) continue; - if (isMemberExpressionObject(id)) continue; - edits.push(id.replace("null")); + for (const localName of unauthenticatedLocalNames) { + const ids = tree.findAll({ + rule: { + kind: "identifier", + regex: `^${escapeRegex(localName)}$`, + }, + }); + for (const id of ids) { + if (isInsideImportStatement(id)) continue; + if (isMemberExpressionObject(id)) continue; + edits.push(id.replace("null")); + } } const resolverCalls = tree.findAll({ diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-unauthenticated/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-unauthenticated/expected.ts new file mode 100644 index 000000000..f3ffcf102 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-unauthenticated/expected.ts @@ -0,0 +1 @@ +export const fallback = null; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-unauthenticated/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-unauthenticated/input.ts new file mode 100644 index 000000000..e3215215d --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-unauthenticated/input.ts @@ -0,0 +1,3 @@ +import { unauthenticatedTailorUser as testUser } from "@tailor-platform/sdk/test"; + +export const fallback = testUser; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/ctx-destructure/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/ctx-destructure/expected.ts new file mode 100644 index 000000000..00575ecc3 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/ctx-destructure/expected.ts @@ -0,0 +1,11 @@ +import { createResolver, t } from "@tailor-platform/sdk"; + +export default createResolver({ + name: "n", + operation: "query", + output: t.string(), + body: (ctx) => { + const { caller: user } = ctx; + return user.id; + }, +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/ctx-destructure/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/ctx-destructure/input.ts new file mode 100644 index 000000000..37319717e --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/ctx-destructure/input.ts @@ -0,0 +1,11 @@ +import { createResolver, t } from "@tailor-platform/sdk"; + +export default createResolver({ + name: "n", + operation: "query", + output: t.string(), + body: (ctx) => { + const { user } = ctx; + return user.id; + }, +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/object-shorthand-return/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/object-shorthand-return/expected.ts new file mode 100644 index 000000000..df5fa02c8 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/object-shorthand-return/expected.ts @@ -0,0 +1,8 @@ +import { createResolver, t } from "@tailor-platform/sdk"; + +export default createResolver({ + name: "n", + operation: "query", + output: t.object({ user: t.string() }), + body: ({ caller }) => ({ caller }), +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/object-shorthand-return/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/object-shorthand-return/input.ts new file mode 100644 index 000000000..f228fa4ee --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/object-shorthand-return/input.ts @@ -0,0 +1,8 @@ +import { createResolver, t } from "@tailor-platform/sdk"; + +export default createResolver({ + name: "n", + operation: "query", + output: t.object({ user: t.string() }), + body: ({ user }) => ({ user }), +}); diff --git a/packages/sdk-codemod/codemods/v2/test-run-arg-input/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/test-run-arg-input/scripts/transform.ts index eab68fc5c..f599505fb 100644 --- a/packages/sdk-codemod/codemods/v2/test-run-arg-input/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/test-run-arg-input/scripts/transform.ts @@ -29,6 +29,82 @@ function unwrapJsonString(body: string, quote: string): string | null { return inner; } +/** + * Apply the unwrap to one shell command segment. Quotes are tracked so command + * boundary characters inside strings (e.g. `'a;b'`) are not split. + */ +function applyUnwrapToSegment(segment: string): string { + return segment.replace(SHELL_ARG_PATTERN, (match, flag, sep, quote, body) => { + const unwrapped = unwrapJsonString(body, quote); + if (unwrapped == null) return match; + return `${flag}${sep}${quote}${unwrapped}${quote}`; + }); +} + +/** + * Walk the line splitting on unquoted shell command boundaries (`;`, `&&`, + * `||`, `|`, `&`) and only run the unwrap on segments that actually invoke + * `tailor-sdk function test-run`. Without this, a chained line like + * `tailor-sdk function test-run ... --arg '{"input":...}' && other-cli --arg '{"input":...}'` + * would have the unrelated `other-cli` argument unwrapped too. + */ +function transformShellLine(line: string): string { + if (!COMMAND_PATTERN.test(line)) return line; + + let result = ""; + let segBuf = ""; + let i = 0; + let quoteChar: string | null = null; + const N = line.length; + + const flushSegment = () => { + if (COMMAND_PATTERN.test(segBuf)) { + result += applyUnwrapToSegment(segBuf); + } else { + result += segBuf; + } + segBuf = ""; + }; + + while (i < N) { + const ch = line[i]!; + if (quoteChar) { + if (ch === "\\" && quoteChar !== "'" && i + 1 < N) { + segBuf += line.slice(i, i + 2); + i += 2; + continue; + } + if (ch === quoteChar) quoteChar = null; + segBuf += ch; + i++; + continue; + } + if (ch === '"' || ch === "'" || ch === "`") { + quoteChar = ch; + segBuf += ch; + i++; + continue; + } + const two = line.slice(i, i + 2); + if (two === "&&" || two === "||") { + flushSegment(); + result += two; + i += 2; + continue; + } + if (ch === ";" || ch === "|" || ch === "&") { + flushSegment(); + result += ch; + i++; + continue; + } + segBuf += ch; + i++; + } + flushSegment(); + return result; +} + function transformShellLikeText(source: string): string | null { if (!COMMAND_PATTERN.test(source)) return null; @@ -36,14 +112,11 @@ function transformShellLikeText(source: string): string | null { const lines = source.split("\n"); for (let i = 0; i < lines.length; i++) { const line = lines[i]!; - if (!COMMAND_PATTERN.test(line)) continue; - const replaced = line.replace(SHELL_ARG_PATTERN, (match, flag, sep, quote, body) => { - const unwrapped = unwrapJsonString(body, quote); - if (unwrapped == null) return match; + const transformed = transformShellLine(line); + if (transformed !== line) { + lines[i] = transformed; modified = true; - return `${flag}${sep}${quote}${unwrapped}${quote}`; - }); - lines[i] = replaced; + } } return modified ? lines.join("\n") : null; } diff --git a/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/chained-commands/expected.sh b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/chained-commands/expected.sh new file mode 100644 index 000000000..189b400c6 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/chained-commands/expected.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +tailor-sdk function test-run resolvers/add.ts --arg '{"a":1}' && other-cli --arg '{"input":{"keep":true}}' diff --git a/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/chained-commands/input.sh b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/chained-commands/input.sh new file mode 100644 index 000000000..9267e7313 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/chained-commands/input.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +tailor-sdk function test-run resolvers/add.ts --arg '{"input":{"a":1}}' && other-cli --arg '{"input":{"keep":true}}' From 734dc160134d006ea93e6a29f07a5a664981c68f Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 29 Apr 2026 21:08:31 +0900 Subject: [PATCH 05/15] fix(sdk-codemod): preserve output schema, scope renames to SDK imports, handle version pins and continuations principal-unify: - Object literal shorthand (`{ user }` in a return value) is now expanded to `{ user: caller }` instead of being collapsed to `{ caller }`, so the resolver's emitted output schema stays the same after the binding rename. - Type renames are restricted to TailorUser/TailorActor/TailorInvoker that are imported from `@tailor-platform/sdk` (or `/test`) without an alias. Local types like `import { TailorUser } from "./domain"` are left alone, and aliased SDK imports keep working through the alias. - The unauthenticated-import rewrite is now driven by names actually imported from the SDK and skips identifier references that fall inside scopes which rebind the same name (variable declarations or function parameters), so shadowing arrow params do not get replaced with `null` and produce `(null) => null`. sdk-skills-shim: - The shim regex now consumes the optional `@version` suffix used by `npx tailor-sdk-skills@latest` / `pnpm dlx tailor-sdk-skills@1.2.3` and an optional ` install` subcommand, so the rewrite no longer leaves `@latest` attached to `tailor-sdk skills install`. test-run-arg-input: - Backslash-newline line continuations are folded into a single logical line via a sentinel before transformation, so a multi-line invocation where `tailor-sdk function test-run` and `--arg '{"input":...}'` sit on separate physical lines still has the wrapper unwrapped. Add fixtures (local-tailor-types, unauthenticated-shadowed, version-qualified, multiline-shell) and update object-shorthand-return to assert the new output shape. --- .../v2/principal-unify/scripts/transform.ts | 109 +++++++++++++++--- .../tests/local-tailor-types/input.ts | 13 +++ .../tests/object-shorthand-return/expected.ts | 2 +- .../unauthenticated-shadowed/expected.ts | 2 + .../tests/unauthenticated-shadowed/input.ts | 4 + .../v2/sdk-skills-shim/scripts/transform.ts | 8 +- .../tests/version-qualified/expected.sh | 4 + .../tests/version-qualified/input.sh | 4 + .../test-run-arg-input/scripts/transform.ts | 16 ++- .../tests/multiline-shell/expected.sh | 3 + .../tests/multiline-shell/input.sh | 3 + 11 files changed, 150 insertions(+), 18 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/local-tailor-types/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/unauthenticated-shadowed/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/unauthenticated-shadowed/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/version-qualified/expected.sh create mode 100644 packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/version-qualified/input.sh create mode 100644 packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/multiline-shell/expected.sh create mode 100644 packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/multiline-shell/input.sh diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index 2e314b486..c7cd2ecbb 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -227,6 +227,53 @@ function collectCtxShadowRanges( return ranges; } +/** + * Collect every byte range across the file where `name` is locally re-bound, + * so identifier references inside the range are treated as shadowed. + * + * Combines variable declarations (var/let/const, including object-pattern + * shorthand declarations) with function-parameter bindings. + */ +function collectAllShadowRanges(root: SgNode, name: string): Array<[number, number]> { + const ranges: Array<[number, number]> = []; + const escaped = escapeRegex(name); + + const declarations = [ + ...root.findAll({ + rule: { + kind: "identifier", + regex: `^${escaped}$`, + inside: { kind: "variable_declarator" }, + }, + }), + ...root.findAll({ + rule: { + kind: "shorthand_property_identifier_pattern", + regex: `^${escaped}$`, + inside: { kind: "variable_declarator" }, + }, + }), + ]; + for (const decl of declarations) { + let scope: SgNode | null = decl.parent(); + while (scope && !SCOPE_KINDS.has(scope.kind())) scope = scope.parent(); + if (!scope) continue; + const range = scope.range(); + ranges.push([range.start.index, range.end.index]); + } + + for (const k of NESTED_FN_KINDS) { + const fns = root.findAll({ rule: { kind: k } }); + for (const fn of fns) { + if (functionRebindsName(fn, name)) { + const range = fn.range(); + ranges.push([range.start.index, range.end.index]); + } + } + } + return ranges; +} + function findResolverBodyArrow(call: SgNode): SgNode | null { const args = call.field("arguments"); if (!args) return null; @@ -292,17 +339,26 @@ function transformResolverBody(arrowNode: SgNode, edits: Edit[]): void { } if (renamedShorthandUser) { const shadowRanges = collectShadowBlockRanges(body, "user"); + // Plain identifier references to the renamed binding (e.g. `user.id`). const refs = body.findAll({ rule: { kind: "identifier", regex: "^user$" } }); - // Object literal shorthand uses `shorthand_property_identifier` (no `_pattern` - // suffix), so include those too — otherwise `({ user }) => ({ user })` becomes - // `({ caller }) => ({ user })` and references an undefined variable. + for (const ref of refs) { + const pos = ref.range().start.index; + if (isInsideAnyRange(pos, shadowRanges)) continue; + edits.push(ref.replace("caller")); + } + // Object literal shorthand (kind: `shorthand_property_identifier`, no + // `_pattern` suffix) is both the key and the value. Rewriting it to + // `caller` would silently change the resolver's output schema from a + // `user` field to a `caller` field. Expand to `user: caller` instead so + // the emitted shape stays the same while the value side reads from the + // renamed local binding. const shortRefs = body.findAll({ rule: { kind: "shorthand_property_identifier", regex: "^user$" }, }); - for (const ref of [...refs, ...shortRefs]) { + for (const ref of shortRefs) { const pos = ref.range().start.index; if (isInsideAnyRange(pos, shadowRanges)) continue; - edits.push(ref.replace("caller")); + edits.push(ref.replace("user: caller")); } } return; @@ -379,6 +435,30 @@ export default function transform(source: string): string | null { const tree = parse(Lang.TypeScript, source).root(); const edits: Edit[] = []; + const sdkImports = tree.findAll({ + rule: { + kind: "import_statement", + has: { kind: "string", regex: "^[\"']@tailor-platform/sdk(/test)?[\"']$" }, + }, + }); + + // Only rewrite type identifiers that are imported from the SDK without an + // alias. A local `import type { TailorUser } from './domain'` must stay alone + // even when the file also imports something else from the SDK. + const sdkRenameSourceNames = new Set(); + for (const importStmt of sdkImports) { + const specs = importStmt.findAll({ rule: { kind: "import_specifier" } }); + for (const spec of specs) { + const idents = spec.children().filter((c: SgNode) => c.kind() === "identifier"); + if (idents.length === 0) continue; + const importedName = idents[0]!.text(); + const aliasNode = idents[1]; + if (TYPE_RENAME_MAP[importedName] && !aliasNode) { + sdkRenameSourceNames.add(importedName); + } + } + } + const typeIdents = tree.findAll({ rule: { kind: "type_identifier", @@ -386,19 +466,17 @@ export default function transform(source: string): string | null { }, }); for (const id of typeIdents) { - const newName = TYPE_RENAME_MAP[id.text()]; - if (newName) edits.push(id.replace(newName)); + if (!sdkRenameSourceNames.has(id.text())) continue; + const newName = TYPE_RENAME_MAP[id.text()]!; + edits.push(id.replace(newName)); } - const sdkImports = tree.findAll({ - rule: { - kind: "import_statement", - has: { kind: "string", regex: "^[\"']@tailor-platform/sdk(/test)?[\"']$" }, - }, - }); let importRemoved = false; const globalEmittedRenamed = new Set(); - const unauthenticatedLocalNames = new Set([UNAUTHENTICATED]); + // Populated only with names actually imported from the SDK (canonical or + // alias). A file with a local `unauthenticatedTailorUser` declaration that + // doesn't come from `@tailor-platform/sdk` is intentionally not rewritten. + const unauthenticatedLocalNames = new Set(); for (const importStmt of sdkImports) { const { newText, touched } = rebuildImportStatement( importStmt, @@ -411,6 +489,7 @@ export default function transform(source: string): string | null { } for (const localName of unauthenticatedLocalNames) { + const shadowRanges = collectAllShadowRanges(tree, localName); const ids = tree.findAll({ rule: { kind: "identifier", @@ -420,6 +499,8 @@ export default function transform(source: string): string | null { for (const id of ids) { if (isInsideImportStatement(id)) continue; if (isMemberExpressionObject(id)) continue; + const pos = id.range().start.index; + if (isInsideAnyRange(pos, shadowRanges)) continue; edits.push(id.replace("null")); } } diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/local-tailor-types/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/local-tailor-types/input.ts new file mode 100644 index 000000000..0d6bc3a3d --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/local-tailor-types/input.ts @@ -0,0 +1,13 @@ +import { createResolver } from "@tailor-platform/sdk"; +import type { TailorUser } from "./domain"; + +export type Props = { + user: TailorUser; +}; + +export default createResolver({ + name: "n", + operation: "query", + output: { id: "string" } as never, + body: () => null, +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/object-shorthand-return/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/object-shorthand-return/expected.ts index df5fa02c8..ad214cfb3 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/tests/object-shorthand-return/expected.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/object-shorthand-return/expected.ts @@ -4,5 +4,5 @@ export default createResolver({ name: "n", operation: "query", output: t.object({ user: t.string() }), - body: ({ caller }) => ({ caller }), + body: ({ caller }) => ({ user: caller }), }); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/unauthenticated-shadowed/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/unauthenticated-shadowed/expected.ts new file mode 100644 index 000000000..bb11a2ace --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/unauthenticated-shadowed/expected.ts @@ -0,0 +1,2 @@ +export const ids = [1].map((testUser) => testUser); +export const fallback = null; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/unauthenticated-shadowed/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/unauthenticated-shadowed/input.ts new file mode 100644 index 000000000..efe2cae7a --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/unauthenticated-shadowed/input.ts @@ -0,0 +1,4 @@ +import { unauthenticatedTailorUser as testUser } from "@tailor-platform/sdk/test"; + +export const ids = [1].map((testUser) => testUser); +export const fallback = testUser; diff --git a/packages/sdk-codemod/codemods/v2/sdk-skills-shim/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/scripts/transform.ts index e65e19393..bdac5661a 100644 --- a/packages/sdk-codemod/codemods/v2/sdk-skills-shim/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/scripts/transform.ts @@ -1,6 +1,12 @@ import * as path from "pathe"; -const SHIM_PATTERN = /\btailor-sdk-skills\b(?!-)/g; +// Match the deprecated binary plus the optional `@version` suffix that +// package-manager run commands can add (`npx tailor-sdk-skills@latest`, +// `pnpm dlx tailor-sdk-skills@1.2.3`) and the optional ` install` subcommand, +// so the rewrite drops the version pin and avoids leaving `@latest` attached +// to the new subcommand. `[ \t]+` (not `\s+`) prevents the optional-install +// alternative from greedily reaching across newlines into the next command. +const SHIM_PATTERN = /\btailor-sdk-skills(?:@[^\s'"`]+)?(?:[ \t]+install)?\b(?!-)/g; const REPLACEMENT = "tailor-sdk skills install"; function replaceShim(value: string): string { diff --git a/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/version-qualified/expected.sh b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/version-qualified/expected.sh new file mode 100644 index 000000000..8022201c7 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/version-qualified/expected.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +npx tailor-sdk skills install +pnpm dlx tailor-sdk skills install --help +tailor-sdk-skills-helper run diff --git a/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/version-qualified/input.sh b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/version-qualified/input.sh new file mode 100644 index 000000000..0d977850d --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/sdk-skills-shim/tests/version-qualified/input.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +npx tailor-sdk-skills@latest +pnpm dlx tailor-sdk-skills@1.2.3 install --help +tailor-sdk-skills-helper run diff --git a/packages/sdk-codemod/codemods/v2/test-run-arg-input/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/test-run-arg-input/scripts/transform.ts index f599505fb..112b52f67 100644 --- a/packages/sdk-codemod/codemods/v2/test-run-arg-input/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/test-run-arg-input/scripts/transform.ts @@ -105,11 +105,22 @@ function transformShellLine(line: string): string { return result; } +// Sentinel used to fold `\` continuations into a single logical line +// before splitting on `\n`. Picked from the Unicode private-use area so it +// cannot collide with realistic source text. +const JOIN_MARKER = "<>"; + function transformShellLikeText(source: string): string | null { if (!COMMAND_PATTERN.test(source)) return null; + // Treat backslash-newline as a line continuation so a multi-line invocation + // like `tailor-sdk function test-run resolvers/x.ts \\\n --arg '{"input":...}'` + // is recognized as a single command. The original `\\\n` is restored after + // transformation so line breaks remain in place. + const joined = source.replace(/\\\n/g, JOIN_MARKER); + let modified = false; - const lines = source.split("\n"); + const lines = joined.split("\n"); for (let i = 0; i < lines.length; i++) { const line = lines[i]!; const transformed = transformShellLine(line); @@ -118,7 +129,8 @@ function transformShellLikeText(source: string): string | null { modified = true; } } - return modified ? lines.join("\n") : null; + if (!modified) return null; + return lines.join("\n").split(JOIN_MARKER).join("\\\n"); } function transformPackageJson(source: string): string | null { diff --git a/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/multiline-shell/expected.sh b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/multiline-shell/expected.sh new file mode 100644 index 000000000..378aaa5b0 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/multiline-shell/expected.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +tailor-sdk function test-run resolvers/add.ts \ + --arg '{"a":1}' diff --git a/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/multiline-shell/input.sh b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/multiline-shell/input.sh new file mode 100644 index 000000000..1a9ae0a8f --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/multiline-shell/input.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +tailor-sdk function test-run resolvers/add.ts \ + --arg '{"input":{"a":1}}' From 897ac7705361a93a180c126defdb312934ade514 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 29 Apr 2026 21:16:50 +0900 Subject: [PATCH 06/15] fix(sdk-codemod): handle caller collisions, aliased createResolver, and defaulted user destructures principal-unify: - Detect when the resolver pattern or body already binds `caller` (as another destructure entry, a let/const declaration, or a function parameter) and skip the rewrite for that resolver. The previous code would emit duplicate bindings or silently re-target an unrelated value. - Resolve the local name(s) of `createResolver` from the SDK import block, so aliased forms like `import { createResolver as makeResolver } ...` are now migrated, and unrelated local helpers named `createResolver` (when the SDK import does not actually bring it in) are left alone. Calls inside scopes that shadow the binding are also skipped. - Handle the `{ user = fallback }` defaulted destructure (`object_assignment_pattern`) by renaming the inner shorthand identifier and preserving the default expression. Add fixtures (caller-collision, aliased-create-resolver, defaulted-destructure) covering each branch. --- .../v2/principal-unify/scripts/transform.ts | 106 ++++++++++++++++-- .../tests/aliased-create-resolver/expected.ts | 8 ++ .../tests/aliased-create-resolver/input.ts | 8 ++ .../tests/caller-collision/input.ts | 11 ++ .../tests/defaulted-destructure/expected.ts | 10 ++ .../tests/defaulted-destructure/input.ts | 10 ++ 6 files changed, 141 insertions(+), 12 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-create-resolver/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-create-resolver/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/caller-collision/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/defaulted-destructure/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/defaulted-destructure/input.ts diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index c7cd2ecbb..d044dd081 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -293,6 +293,52 @@ function findResolverBodyArrow(call: SgNode): SgNode | null { return null; } +/** + * Look for any binding named `caller` in the resolver body or pattern. When + * one exists, renaming `user` → `caller` would either shadow it, collide with + * a duplicate `let`/`const`, or alias an unrelated value, so the codemod + * leaves the body alone for manual migration instead. + */ +function hasCallerBindingConflict(pattern: SgNode, body: SgNode): boolean { + for (const child of pattern.children()) { + const k = child.kind(); + if (k === "shorthand_property_identifier_pattern" && child.text() === "caller") return true; + if (k === "pair_pattern") { + const value = child.field("value"); + if (value && value.kind() === "identifier" && value.text() === "caller") return true; + } + if (k === "object_assignment_pattern") { + const inner = child + .children() + .find((c: SgNode) => c.kind() === "shorthand_property_identifier_pattern"); + if (inner && inner.text() === "caller") return true; + } + } + const decls = body.findAll({ + rule: { + kind: "identifier", + regex: "^caller$", + inside: { kind: "variable_declarator" }, + }, + }); + if (decls.length > 0) return true; + const shortDecls = body.findAll({ + rule: { + kind: "shorthand_property_identifier_pattern", + regex: "^caller$", + inside: { kind: "variable_declarator" }, + }, + }); + if (shortDecls.length > 0) return true; + for (const k of NESTED_FN_KINDS) { + const fns = body.findAll({ rule: { kind: k } }); + for (const fn of fns) { + if (functionRebindsName(fn, "caller")) return true; + } + } + return false; +} + function transformResolverBody(arrowNode: SgNode, edits: Edit[]): void { const params = arrowNode.field("parameters") ?? @@ -322,6 +368,8 @@ function transformResolverBody(arrowNode: SgNode, edits: Edit[]): void { if (!pattern) return; if (pattern.kind() === "object_pattern") { + if (hasCallerBindingConflict(pattern, body)) return; + let renamedShorthandUser = false; // Only iterate top-level pattern children so nested destructures like // `({ input: { user } })` are not mistaken for the resolver context user. @@ -335,6 +383,16 @@ function transformResolverBody(arrowNode: SgNode, edits: Edit[]): void { if (key && key.text() === "user") { edits.push(key.replace("caller")); } + } else if (kind === "object_assignment_pattern") { + // `{ user = fallback }` — the inner shorthand is the binding; default + // expression is preserved. + const inner = child + .children() + .find((c: SgNode) => c.kind() === "shorthand_property_identifier_pattern"); + if (inner && inner.text() === "user") { + edits.push(inner.replace("caller")); + renamedShorthandUser = true; + } } } if (renamedShorthandUser) { @@ -505,19 +563,43 @@ export default function transform(source: string): string | null { } } - const resolverCalls = tree.findAll({ - rule: { - kind: "call_expression", - has: { - field: "function", - kind: "identifier", - regex: "^createResolver$", + // Resolve which local names refer to the SDK's `createResolver` so aliased + // imports like `import { createResolver as makeResolver } ...` are migrated + // and unrelated local helpers named `createResolver` (when the SDK import + // does not actually bring `createResolver` in) are not. + const createResolverLocalNames = new Set(); + for (const importStmt of sdkImports) { + const specs = importStmt.findAll({ rule: { kind: "import_specifier" } }); + for (const spec of specs) { + const idents = spec.children().filter((c: SgNode) => c.kind() === "identifier"); + if (idents.length === 0) continue; + const importedName = idents[0]!.text(); + const aliasNode = idents[1]; + if (importedName === "createResolver") { + createResolverLocalNames.add(aliasNode?.text() ?? importedName); + } + } + } + for (const localName of createResolverLocalNames) { + const shadowRanges = collectAllShadowRanges(tree, localName); + const calls = tree.findAll({ + rule: { + kind: "call_expression", + has: { + field: "function", + kind: "identifier", + regex: `^${escapeRegex(localName)}$`, + }, }, - }, - }); - for (const call of resolverCalls) { - const arrow = findResolverBodyArrow(call); - if (arrow) transformResolverBody(arrow, edits); + }); + for (const call of calls) { + const callee = call.field("function"); + if (!callee) continue; + const pos = callee.range().start.index; + if (isInsideAnyRange(pos, shadowRanges)) continue; + const arrow = findResolverBodyArrow(call); + if (arrow) transformResolverBody(arrow, edits); + } } if (edits.length === 0) return null; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-create-resolver/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-create-resolver/expected.ts new file mode 100644 index 000000000..696045d1a --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-create-resolver/expected.ts @@ -0,0 +1,8 @@ +import { createResolver as makeResolver, t } from "@tailor-platform/sdk"; + +export default makeResolver({ + name: "n", + operation: "query", + output: t.string(), + body: ({ caller }) => caller.id, +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-create-resolver/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-create-resolver/input.ts new file mode 100644 index 000000000..e208a238a --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-create-resolver/input.ts @@ -0,0 +1,8 @@ +import { createResolver as makeResolver, t } from "@tailor-platform/sdk"; + +export default makeResolver({ + name: "n", + operation: "query", + output: t.string(), + body: ({ user }) => user.id, +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/caller-collision/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/caller-collision/input.ts new file mode 100644 index 000000000..ab63c0483 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/caller-collision/input.ts @@ -0,0 +1,11 @@ +import { createResolver, t } from "@tailor-platform/sdk"; + +export default createResolver({ + name: "n", + operation: "query", + output: t.string(), + body: ({ user }) => { + const caller = "anonymous"; + return user.id ?? caller; + }, +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/defaulted-destructure/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/defaulted-destructure/expected.ts new file mode 100644 index 000000000..5f726a55a --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/defaulted-destructure/expected.ts @@ -0,0 +1,10 @@ +import { createResolver, t } from "@tailor-platform/sdk"; + +const fallback = { id: "anon" }; + +export default createResolver({ + name: "n", + operation: "query", + output: t.string(), + body: ({ caller = fallback }) => caller.id, +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/defaulted-destructure/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/defaulted-destructure/input.ts new file mode 100644 index 000000000..82c4086bb --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/defaulted-destructure/input.ts @@ -0,0 +1,10 @@ +import { createResolver, t } from "@tailor-platform/sdk"; + +const fallback = { id: "anon" }; + +export default createResolver({ + name: "n", + operation: "query", + output: t.string(), + body: ({ user = fallback }) => user.id, +}); From 71805295c987755c14721de13b54e63ebff58c2f Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 29 Apr 2026 21:44:19 +0900 Subject: [PATCH 07/15] fix(sdk-codemod): tighten body-rename shadows, ctx redeclaration, caller key conflict, arg continuations principal-unify: - Body-identifier rewrites in the destructured-resolver path now go through collectAllShadowRanges, so a nested arrow that re-binds `user` as its own parameter (e.g. `items.map((user) => user.id)`) keeps its inner reference pointing at the inner binding instead of being incorrectly renamed. - collectCtxShadowRanges also treats `var ctx = ...` / `let ctx = ...` style re-bindings of the resolver context name as shadows, so subsequent destructures of the rebound variable are not mistaken for the resolver context. - hasCallerBindingConflict additionally checks the property `key` of pair patterns, so `({ user, caller: x })` is detected as conflicting with the shorthand rename instead of producing a duplicate-key destructure. test-run-arg-input: - The shell-line argument regex accepts the JOIN_MARKER as part of the separator group, so a backslash-newline continuation between `--arg` and the quoted JSON still matches and the wrapper is unwrapped. - The marker itself is now plain ASCII (`SDK_CODEMOD_JOIN`) so the regex can reference it as a literal alternative without escaping or stray bytes. Add fixtures (param-shadow-user, ctx-rebound, caller-key-conflict, multiline-arg-continuation) covering each branch. --- .../v2/principal-unify/scripts/transform.ts | 64 +++++++------------ .../tests/caller-key-conflict/input.ts | 8 +++ .../tests/ctx-rebound/input.ts | 13 ++++ .../tests/param-shadow-user/expected.ts | 10 +++ .../tests/param-shadow-user/input.ts | 10 +++ .../test-run-arg-input/scripts/transform.ts | 8 ++- .../multiline-arg-continuation/expected.sh | 3 + .../tests/multiline-arg-continuation/input.sh | 3 + 8 files changed, 76 insertions(+), 43 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/caller-key-conflict/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/ctx-rebound/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/param-shadow-user/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/param-shadow-user/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/multiline-arg-continuation/expected.sh create mode 100644 packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/multiline-arg-continuation/input.sh diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index d044dd081..f98a332f6 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -136,46 +136,6 @@ const NESTED_FN_KINDS = [ "method_definition", ]; -/** - * Collect byte ranges of scopes where `name` is locally redeclared. - * - * Any identifier reference whose start position falls inside one of these ranges - * is shadowed by the local binding and must not be renamed by the param rewrite. - * @param body - The arrow body node to scan. - * @param name - The identifier name to detect declarations of. - * @returns Sorted array of [startByte, endByte] ranges (half-open). - */ -function collectShadowBlockRanges(body: SgNode, name: string): Array<[number, number]> { - const ranges: Array<[number, number]> = []; - const declarations = [ - ...body.findAll({ - rule: { - kind: "identifier", - regex: `^${name}$`, - inside: { kind: "variable_declarator" }, - }, - }), - ...body.findAll({ - rule: { - kind: "shorthand_property_identifier_pattern", - regex: `^${name}$`, - inside: { kind: "variable_declarator" }, - }, - }), - ]; - - for (const decl of declarations) { - let scope: SgNode | null = decl.parent(); - while (scope && !SCOPE_KINDS.has(scope.kind())) { - scope = scope.parent(); - } - if (!scope) continue; - const range = scope.range(); - ranges.push([range.start.index, range.end.index]); - } - return ranges; -} - function isInsideAnyRange(pos: number, ranges: Array<[number, number]>): boolean { return ranges.some(([s, e]) => pos >= s && pos < e); } @@ -224,6 +184,21 @@ function collectCtxShadowRanges( } } } + // Also treat re-binding via `var ctx = ...` / `let ctx = ...` as a shadow. + // We only check direct identifier-named declarators here — pattern-style + // bindings (`const { ctx } = something`) that happen to share the name are + // unrelated to the context parameter. + const declarators = body.findAll({ rule: { kind: "variable_declarator" } }); + for (const decl of declarators) { + const nameNode = decl.field("name"); + if (!nameNode || nameNode.kind() !== "identifier") continue; + if (nameNode.text() !== ctxName) continue; + let scope: SgNode | null = decl.parent(); + while (scope && !SCOPE_KINDS.has(scope.kind())) scope = scope.parent(); + if (!scope) continue; + const range = scope.range(); + ranges.push([range.start.index, range.end.index]); + } return ranges; } @@ -304,6 +279,10 @@ function hasCallerBindingConflict(pattern: SgNode, body: SgNode): boolean { const k = child.kind(); if (k === "shorthand_property_identifier_pattern" && child.text() === "caller") return true; if (k === "pair_pattern") { + // The property key can also collide: `{ user, caller: x }` after the + // shorthand rename becomes `{ caller, caller: x }`, a duplicate key. + const key = child.field("key"); + if (key && key.text() === "caller") return true; const value = child.field("value"); if (value && value.kind() === "identifier" && value.text() === "caller") return true; } @@ -396,7 +375,10 @@ function transformResolverBody(arrowNode: SgNode, edits: Edit[]): void { } } if (renamedShorthandUser) { - const shadowRanges = collectShadowBlockRanges(body, "user"); + // Use the broader shadow-range collector here so a nested arrow that + // re-binds `user` as a parameter (e.g. `items.map((user) => user.id)`) + // does not get its inner reference incorrectly renamed to `caller`. + const shadowRanges = collectAllShadowRanges(body, "user"); // Plain identifier references to the renamed binding (e.g. `user.id`). const refs = body.findAll({ rule: { kind: "identifier", regex: "^user$" } }); for (const ref of refs) { diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/caller-key-conflict/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/caller-key-conflict/input.ts new file mode 100644 index 000000000..d20cb11f1 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/caller-key-conflict/input.ts @@ -0,0 +1,8 @@ +import { createResolver, t } from "@tailor-platform/sdk"; + +export default createResolver({ + name: "n", + operation: "query", + output: t.string(), + body: ({ user, caller: x }) => user.id ?? x, +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/ctx-rebound/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/ctx-rebound/input.ts new file mode 100644 index 000000000..3e8ffd6ea --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/ctx-rebound/input.ts @@ -0,0 +1,13 @@ +import { createResolver, t } from "@tailor-platform/sdk"; + +declare function getOther(): { user: { id: string } }; + +export default createResolver({ + name: "n", + operation: "query", + output: t.string(), + body: (ctx) => { + var ctx = getOther(); + return ctx.user.id; + }, +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/param-shadow-user/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/param-shadow-user/expected.ts new file mode 100644 index 000000000..a60459ab3 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/param-shadow-user/expected.ts @@ -0,0 +1,10 @@ +import { createResolver, t } from "@tailor-platform/sdk"; + +const items: Array<{ id: string }> = []; + +export default createResolver({ + name: "n", + operation: "query", + output: t.array(t.string()), + body: ({ caller }) => items.map((user) => user.id), +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/param-shadow-user/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/param-shadow-user/input.ts new file mode 100644 index 000000000..40c8a994b --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/param-shadow-user/input.ts @@ -0,0 +1,10 @@ +import { createResolver, t } from "@tailor-platform/sdk"; + +const items: Array<{ id: string }> = []; + +export default createResolver({ + name: "n", + operation: "query", + output: t.array(t.string()), + body: ({ user }) => items.map((user) => user.id), +}); diff --git a/packages/sdk-codemod/codemods/v2/test-run-arg-input/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/test-run-arg-input/scripts/transform.ts index 112b52f67..3eb5d8ca6 100644 --- a/packages/sdk-codemod/codemods/v2/test-run-arg-input/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/test-run-arg-input/scripts/transform.ts @@ -1,7 +1,11 @@ import * as path from "pathe"; const COMMAND_PATTERN = /\btailor-sdk\s+function\s+test-run\b/; -const SHELL_ARG_PATTERN = /(--arg|-a)(\s*=\s*|\s+)(['"`])((?:\\.|(?!\3)[^\\])*)\3/g; +// The separator group accepts the JOIN_MARKER literal so a backslash-newline +// continuation between `--arg` and the quoted JSON (joined into the line via +// the marker by `transformShellLikeText`) still matches. +const SHELL_ARG_PATTERN = + /(--arg|-a)(\s*=\s*|(?:\s|SDK_CODEMOD_JOIN)+)(['"`])((?:\\.|(?!\3)[^\\])*)\3/g; function isInputWrapper(parsed: unknown): parsed is { input: unknown } { return ( @@ -108,7 +112,7 @@ function transformShellLine(line: string): string { // Sentinel used to fold `\` continuations into a single logical line // before splitting on `\n`. Picked from the Unicode private-use area so it // cannot collide with realistic source text. -const JOIN_MARKER = "<>"; +const JOIN_MARKER = "SDK_CODEMOD_JOIN"; function transformShellLikeText(source: string): string | null { if (!COMMAND_PATTERN.test(source)) return null; diff --git a/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/multiline-arg-continuation/expected.sh b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/multiline-arg-continuation/expected.sh new file mode 100644 index 000000000..21e4c82fa --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/multiline-arg-continuation/expected.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +tailor-sdk function test-run resolvers/x.ts --arg \ + '{"a":1}' diff --git a/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/multiline-arg-continuation/input.sh b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/multiline-arg-continuation/input.sh new file mode 100644 index 000000000..18ecf6e11 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/multiline-arg-continuation/input.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +tailor-sdk function test-run resolvers/x.ts --arg \ + '{"input":{"a":1}}' From 4f78bb6cb2f96001d9329945369af8b3bc4c00cf Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 29 Apr 2026 21:58:39 +0900 Subject: [PATCH 08/15] fix(sdk-codemod): make principal-unify shadow detection field-precise and pattern-aware principal-unify: - collectAllShadowRanges previously used `inside: { kind: "variable_declarator" }`, which also matched value-side identifier references such as `user` in `const userId = user.id`. Walking up to the enclosing scope from such a match marked the entire body as shadowed, so every body rename was silently suppressed and the codemod produced `({ caller }) => { const x = user.id; ... }` instead. The scan now iterates `variable_declarator` nodes directly and only consults `field("name")`. - Added a shared `patternBindsName` helper that recurses through `object_pattern`, `array_pattern`, `object_assignment_pattern`, `pair_pattern`, `assignment_pattern`, and `rest_pattern` so destructured parameters like `({ user }) => ...` and `(({ user, items }) => items.map(({ user }) => user))` are recognised as re-bindings of `user`. functionRebindsName uses the same helper for both single-arrow params and `formal_parameters` children. test-run-arg-input: - Updated the JOIN_MARKER comment to describe the actual ASCII strategy instead of the inaccurate "Unicode private-use area" claim left over from an earlier iteration. Add fixtures (body-with-derived-const, nested-destructure-shadow) covering the previously-broken behaviour. --- .../v2/principal-unify/scripts/transform.ts | 68 +++++++++++++------ .../tests/body-with-derived-const/expected.ts | 11 +++ .../tests/body-with-derived-const/input.ts | 11 +++ .../nested-destructure-shadow/expected.ts | 10 +++ .../tests/nested-destructure-shadow/input.ts | 10 +++ .../test-run-arg-input/scripts/transform.ts | 5 +- 6 files changed, 93 insertions(+), 22 deletions(-) create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/body-with-derived-const/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/body-with-derived-const/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/nested-destructure-shadow/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/nested-destructure-shadow/input.ts diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index f98a332f6..7dcc29554 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -140,18 +140,54 @@ function isInsideAnyRange(pos: number, ranges: Array<[number, number]>): boolean return ranges.some(([s, e]) => pos >= s && pos < e); } +function patternBindsName(pat: SgNode, name: string): boolean { + const k = pat.kind(); + if (k === "identifier") return pat.text() === name; + if (k === "object_pattern") { + for (const child of pat.children()) { + const ck = child.kind(); + if (ck === "shorthand_property_identifier_pattern" && child.text() === name) return true; + if (ck === "pair_pattern") { + const value = child.field("value"); + if (value && patternBindsName(value, name)) return true; + } + if (ck === "object_assignment_pattern") { + const inner = child + .children() + .find((c: SgNode) => c.kind() === "shorthand_property_identifier_pattern"); + if (inner && inner.text() === name) return true; + } + if (ck === "rest_pattern") { + const inner = child.children().find((c: SgNode) => c.kind() === "identifier"); + if (inner && inner.text() === name) return true; + } + } + } else if (k === "array_pattern") { + for (const child of pat.children()) { + if (patternBindsName(child, name)) return true; + } + } else if (k === "assignment_pattern") { + const left = pat.field("left"); + if (left && patternBindsName(left, name)) return true; + } + return false; +} + function functionRebindsName(fn: SgNode, name: string): boolean { const single = fn.field("parameter"); - if (single && single.kind() === "identifier" && single.text() === name) return true; + if (single && patternBindsName(single, name)) return true; const params = fn.field("parameters") ?? fn.children().find((c: SgNode) => c.kind() === "formal_parameters"); if (!params) return false; for (const child of params.children()) { const k = child.kind(); if (k === "identifier" && child.text() === name) return true; + if (k === "object_pattern" || k === "array_pattern") { + if (patternBindsName(child, name)) return true; + } if (k === "required_parameter" || k === "optional_parameter") { const pat = child.field("pattern"); - if (pat && pat.kind() === "identifier" && pat.text() === name) return true; + if (pat && patternBindsName(pat, name)) return true; } } return false; @@ -211,25 +247,17 @@ function collectCtxShadowRanges( */ function collectAllShadowRanges(root: SgNode, name: string): Array<[number, number]> { const ranges: Array<[number, number]> = []; - const escaped = escapeRegex(name); - const declarations = [ - ...root.findAll({ - rule: { - kind: "identifier", - regex: `^${escaped}$`, - inside: { kind: "variable_declarator" }, - }, - }), - ...root.findAll({ - rule: { - kind: "shorthand_property_identifier_pattern", - regex: `^${escaped}$`, - inside: { kind: "variable_declarator" }, - }, - }), - ]; - for (const decl of declarations) { + // Field-precise scan over `variable_declarator` nodes — only the binding + // pattern (`name` field) counts, not value-side identifier references. + // `inside: { kind: "variable_declarator" }` would also match `user` in + // `const x = user.id`, which would shadow the entire enclosing scope and + // suppress every body rename. + const declarators = root.findAll({ rule: { kind: "variable_declarator" } }); + for (const decl of declarators) { + const nameNode = decl.field("name"); + if (!nameNode) continue; + if (!patternBindsName(nameNode, name)) continue; let scope: SgNode | null = decl.parent(); while (scope && !SCOPE_KINDS.has(scope.kind())) scope = scope.parent(); if (!scope) continue; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-with-derived-const/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-with-derived-const/expected.ts new file mode 100644 index 000000000..a0c293c7e --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-with-derived-const/expected.ts @@ -0,0 +1,11 @@ +import { createResolver, t } from "@tailor-platform/sdk"; + +export default createResolver({ + name: "n", + operation: "query", + output: t.string(), + body: ({ caller }) => { + const userId = caller.id; + return userId; + }, +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-with-derived-const/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-with-derived-const/input.ts new file mode 100644 index 000000000..b5f7518a2 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/body-with-derived-const/input.ts @@ -0,0 +1,11 @@ +import { createResolver, t } from "@tailor-platform/sdk"; + +export default createResolver({ + name: "n", + operation: "query", + output: t.string(), + body: ({ user }) => { + const userId = user.id; + return userId; + }, +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/nested-destructure-shadow/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/nested-destructure-shadow/expected.ts new file mode 100644 index 000000000..bd3efce01 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/nested-destructure-shadow/expected.ts @@ -0,0 +1,10 @@ +import { createResolver, t } from "@tailor-platform/sdk"; + +const items: Array<{ user: { id: string } }> = []; + +export default createResolver({ + name: "n", + operation: "query", + output: t.array(t.string()), + body: ({ caller }) => items.map(({ user }) => user.id).concat(caller.id), +}); diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/nested-destructure-shadow/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/nested-destructure-shadow/input.ts new file mode 100644 index 000000000..ec04f1582 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/nested-destructure-shadow/input.ts @@ -0,0 +1,10 @@ +import { createResolver, t } from "@tailor-platform/sdk"; + +const items: Array<{ user: { id: string } }> = []; + +export default createResolver({ + name: "n", + operation: "query", + output: t.array(t.string()), + body: ({ user }) => items.map(({ user }) => user.id).concat(user.id), +}); diff --git a/packages/sdk-codemod/codemods/v2/test-run-arg-input/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/test-run-arg-input/scripts/transform.ts index 3eb5d8ca6..046b38222 100644 --- a/packages/sdk-codemod/codemods/v2/test-run-arg-input/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/test-run-arg-input/scripts/transform.ts @@ -110,8 +110,9 @@ function transformShellLine(line: string): string { } // Sentinel used to fold `\` continuations into a single logical line -// before splitting on `\n`. Picked from the Unicode private-use area so it -// cannot collide with realistic source text. +// before splitting on `\n`. Plain ASCII so the SHELL_ARG_PATTERN can reference +// it directly as a separator alternative; the chosen token is unlikely to +// appear inside a `tailor-sdk function test-run` invocation. const JOIN_MARKER = "SDK_CODEMOD_JOIN"; function transformShellLikeText(source: string): string | null { From 7a807e84cf183e5f9282adc4934ca9054ef8cddf Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 29 Apr 2026 22:06:41 +0900 Subject: [PATCH 09/15] refactor(sdk-codemod): hoist JOIN_MARKER and extract iterateImportSpecs test-run-arg-input: - Build SHELL_ARG_PATTERN via new RegExp with the JOIN_MARKER constant interpolated, instead of duplicating the literal token inside the regex source. The marker is now defined in one place. principal-unify: - Extract iterateImportSpecs generator that yields each specifier's imported name, alias node, and effective local name. The three import walks (rebuildImportStatement, sdkRenameSourceNames collection, createResolverLocalNames collection) consume it so the spec parsing logic lives in one place. --- .../v2/principal-unify/scripts/transform.ts | 56 +++++++++++-------- .../test-run-arg-input/scripts/transform.ts | 19 ++++--- 2 files changed, 44 insertions(+), 31 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index 7dcc29554..89d2e2eae 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -53,6 +53,35 @@ function escapeRegex(s: string): string { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } +interface ImportSpec { + spec: SgNode; + importedName: string; + aliasNode: SgNode | undefined; + localName: string; +} + +/** + * Yield each import specifier in `importStmt` along with its imported name and + * optional alias. `import { Foo as Bar }` produces `{ importedName: "Foo", + * aliasNode: , localName: "Bar" }`; `import { Foo }` produces + * `{ importedName: "Foo", aliasNode: undefined, localName: "Foo" }`. + */ +function* iterateImportSpecs(importStmt: SgNode): Generator { + const specs = importStmt.findAll({ rule: { kind: "import_specifier" } }); + for (const spec of specs) { + const idents = spec.children().filter((c: SgNode) => c.kind() === "identifier"); + if (idents.length === 0) continue; + const importedName = idents[0]!.text(); + const aliasNode = idents[1]; + yield { + spec, + importedName, + aliasNode, + localName: aliasNode?.text() ?? importedName, + }; + } +} + function rebuildImportStatement( importStmt: SgNode, globalEmittedRenamed: Set, @@ -63,21 +92,12 @@ function rebuildImportStatement( const trailingSemi = importText.trimEnd().endsWith(";") ? ";" : ""; const sourceRaw = extractModuleSource(importText); - const specifiers = importStmt.findAll({ rule: { kind: "import_specifier" } }); const newSpecTexts: string[] = []; const seenLocal = new Set(); let touched = false; - for (const spec of specifiers) { + for (const { spec, importedName, aliasNode, localName } of iterateImportSpecs(importStmt)) { const specText = spec.text(); - const idents = spec.children().filter((c: SgNode) => c.kind() === "identifier"); - if (idents.length === 0) { - newSpecTexts.push(specText); - continue; - } - const importedName = idents[0]!.text(); - const aliasNode = idents[1]; - const localName = aliasNode?.text() ?? importedName; const isTypeOnly = /^\s*type\s+/.test(specText); const renamed = TYPE_RENAME_MAP[importedName]; @@ -515,12 +535,7 @@ export default function transform(source: string): string | null { // even when the file also imports something else from the SDK. const sdkRenameSourceNames = new Set(); for (const importStmt of sdkImports) { - const specs = importStmt.findAll({ rule: { kind: "import_specifier" } }); - for (const spec of specs) { - const idents = spec.children().filter((c: SgNode) => c.kind() === "identifier"); - if (idents.length === 0) continue; - const importedName = idents[0]!.text(); - const aliasNode = idents[1]; + for (const { importedName, aliasNode } of iterateImportSpecs(importStmt)) { if (TYPE_RENAME_MAP[importedName] && !aliasNode) { sdkRenameSourceNames.add(importedName); } @@ -579,14 +594,9 @@ export default function transform(source: string): string | null { // does not actually bring `createResolver` in) are not. const createResolverLocalNames = new Set(); for (const importStmt of sdkImports) { - const specs = importStmt.findAll({ rule: { kind: "import_specifier" } }); - for (const spec of specs) { - const idents = spec.children().filter((c: SgNode) => c.kind() === "identifier"); - if (idents.length === 0) continue; - const importedName = idents[0]!.text(); - const aliasNode = idents[1]; + for (const { importedName, localName } of iterateImportSpecs(importStmt)) { if (importedName === "createResolver") { - createResolverLocalNames.add(aliasNode?.text() ?? importedName); + createResolverLocalNames.add(localName); } } } diff --git a/packages/sdk-codemod/codemods/v2/test-run-arg-input/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/test-run-arg-input/scripts/transform.ts index 046b38222..ed2c0c3a1 100644 --- a/packages/sdk-codemod/codemods/v2/test-run-arg-input/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/test-run-arg-input/scripts/transform.ts @@ -1,11 +1,20 @@ import * as path from "pathe"; const COMMAND_PATTERN = /\btailor-sdk\s+function\s+test-run\b/; + +// Sentinel used to fold `\` continuations into a single logical line +// before splitting on `\n`. Plain ASCII so the SHELL_ARG_PATTERN can reference +// it directly as a separator alternative; the chosen token is unlikely to +// appear inside a `tailor-sdk function test-run` invocation. +const JOIN_MARKER = "SDK_CODEMOD_JOIN"; + // The separator group accepts the JOIN_MARKER literal so a backslash-newline // continuation between `--arg` and the quoted JSON (joined into the line via // the marker by `transformShellLikeText`) still matches. -const SHELL_ARG_PATTERN = - /(--arg|-a)(\s*=\s*|(?:\s|SDK_CODEMOD_JOIN)+)(['"`])((?:\\.|(?!\3)[^\\])*)\3/g; +const SHELL_ARG_PATTERN = new RegExp( + `(--arg|-a)(\\s*=\\s*|(?:\\s|${JOIN_MARKER})+)(['"\`])((?:\\\\.|(?!\\3)[^\\\\])*)\\3`, + "g", +); function isInputWrapper(parsed: unknown): parsed is { input: unknown } { return ( @@ -109,12 +118,6 @@ function transformShellLine(line: string): string { return result; } -// Sentinel used to fold `\` continuations into a single logical line -// before splitting on `\n`. Plain ASCII so the SHELL_ARG_PATTERN can reference -// it directly as a separator alternative; the chosen token is unlikely to -// appear inside a `tailor-sdk function test-run` invocation. -const JOIN_MARKER = "SDK_CODEMOD_JOIN"; - function transformShellLikeText(source: string): string | null { if (!COMMAND_PATTERN.test(source)) return null; From d409266d40f48ee94d5208212205d9df591e5fcf Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 29 Apr 2026 22:09:16 +0900 Subject: [PATCH 10/15] refactor(sdk-codemod): drive quickFilter from TYPE_RENAME_MAP and share scope-walk helper principal-unify: - quickFilter now derives its needle list from Object.keys(TYPE_RENAME_MAP) plus the unauthenticated/createResolver tokens, so adding another renamed type only needs the map entry. - collectAllShadowRanges and collectCtxShadowRanges shared the same declarator-to-enclosing-scope walk; extracted enclosingScopeRange and consume it from both call sites. --- .../v2/principal-unify/scripts/transform.ts | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts index 89d2e2eae..af16cd7f0 100644 --- a/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/principal-unify/scripts/transform.ts @@ -9,15 +9,11 @@ const TYPE_RENAME_MAP: Record = { const UNAUTHENTICATED = "unauthenticatedTailorUser"; +const QUICK_FILTER_NEEDLES = [...Object.keys(TYPE_RENAME_MAP), UNAUTHENTICATED, "createResolver"]; + function quickFilter(source: string): boolean { if (!source.includes("@tailor-platform/sdk")) return false; - return ( - source.includes("TailorUser") || - source.includes("TailorActor") || - source.includes("TailorInvoker") || - source.includes(UNAUTHENTICATED) || - source.includes("createResolver") - ); + return QUICK_FILTER_NEEDLES.some((needle) => source.includes(needle)); } function isInsideImportStatement(node: SgNode): boolean { @@ -160,6 +156,18 @@ function isInsideAnyRange(pos: number, ranges: Array<[number, number]>): boolean return ranges.some(([s, e]) => pos >= s && pos < e); } +/** + * Walk up from `decl` and return the byte range of its enclosing scope, or + * null if no recognized scope ancestor exists. + */ +function enclosingScopeRange(decl: SgNode): [number, number] | null { + let scope: SgNode | null = decl.parent(); + while (scope && !SCOPE_KINDS.has(scope.kind())) scope = scope.parent(); + if (!scope) return null; + const range = scope.range(); + return [range.start.index, range.end.index]; +} + function patternBindsName(pat: SgNode, name: string): boolean { const k = pat.kind(); if (k === "identifier") return pat.text() === name; @@ -249,11 +257,8 @@ function collectCtxShadowRanges( const nameNode = decl.field("name"); if (!nameNode || nameNode.kind() !== "identifier") continue; if (nameNode.text() !== ctxName) continue; - let scope: SgNode | null = decl.parent(); - while (scope && !SCOPE_KINDS.has(scope.kind())) scope = scope.parent(); - if (!scope) continue; - const range = scope.range(); - ranges.push([range.start.index, range.end.index]); + const range = enclosingScopeRange(decl); + if (range) ranges.push(range); } return ranges; } @@ -278,11 +283,8 @@ function collectAllShadowRanges(root: SgNode, name: string): Array<[number, numb const nameNode = decl.field("name"); if (!nameNode) continue; if (!patternBindsName(nameNode, name)) continue; - let scope: SgNode | null = decl.parent(); - while (scope && !SCOPE_KINDS.has(scope.kind())) scope = scope.parent(); - if (!scope) continue; - const range = scope.range(); - ranges.push([range.start.index, range.end.index]); + const range = enclosingScopeRange(decl); + if (range) ranges.push(range); } for (const k of NESTED_FN_KINDS) { From 8833ca4846e10751e8dfc9adf861ca317d2e2950 Mon Sep 17 00:00:00 2001 From: dqn Date: Wed, 29 Apr 2026 22:16:58 +0900 Subject: [PATCH 11/15] test(sdk-codemod): cover --arg= form, double-quoted shell args, and aliased type imports test-run-arg-input: - equals-form fixture exercises the SHELL_ARG_PATTERN \s*=\s* separator branch (tailor-sdk function test-run --arg='{"input":...}'), which the prior fixtures only hit through the whitespace separator. - double-quoted-shell fixture exercises shell-context double-quoted JSON with backslash-escaped quotes, which was previously only exercised via package.json scripts. principal-unify: - aliased-type-import fixture exercises the aliased TailorUser/TailorActor/ TailorInvoker import path, asserting the alias is preserved (`TailorPrincipal as MyUser`) and the body's MyUser type references stay untouched. Also exclude `packages/sdk-codemod/codemods/**/tests/**` from the lefthook format hook to match the existing `.oxfmtrc.json` and `packages/sdk-codemod/.oxlintrc.json` ignore patterns. Without this entry, committing only fixture files makes oxfmt receive an empty staged-files list and exit with "Expected at least one target file." --- lefthook.yml | 1 + .../v2/principal-unify/tests/aliased-type-import/expected.ts | 5 +++++ .../v2/principal-unify/tests/aliased-type-import/input.ts | 5 +++++ .../test-run-arg-input/tests/double-quoted-shell/expected.sh | 2 ++ .../v2/test-run-arg-input/tests/double-quoted-shell/input.sh | 2 ++ .../v2/test-run-arg-input/tests/equals-form/expected.sh | 2 ++ .../v2/test-run-arg-input/tests/equals-form/input.sh | 2 ++ 7 files changed, 19 insertions(+) create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-type-import/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-type-import/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/double-quoted-shell/expected.sh create mode 100644 packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/double-quoted-shell/input.sh create mode 100644 packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/equals-form/expected.sh create mode 100644 packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/equals-form/input.sh diff --git a/lefthook.yml b/lefthook.yml index 198ae673d..f7e220d19 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -25,6 +25,7 @@ pre-commit: - "example/generated/**" - "example/seed/**" - "packages/tailor-proto/**" + - "packages/sdk-codemod/codemods/**/tests/**" run: pnpm oxfmt --check {staged_files} checks: diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-type-import/expected.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-type-import/expected.ts new file mode 100644 index 000000000..d3498e2ca --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-type-import/expected.ts @@ -0,0 +1,5 @@ +import { type TailorPrincipal as MyUser } from "@tailor-platform/sdk"; + +export type Props = { + caller: MyUser; +}; diff --git a/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-type-import/input.ts b/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-type-import/input.ts new file mode 100644 index 000000000..d3611f901 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/principal-unify/tests/aliased-type-import/input.ts @@ -0,0 +1,5 @@ +import { type TailorUser as MyUser } from "@tailor-platform/sdk"; + +export type Props = { + caller: MyUser; +}; diff --git a/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/double-quoted-shell/expected.sh b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/double-quoted-shell/expected.sh new file mode 100644 index 000000000..8fcce5ee3 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/double-quoted-shell/expected.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +tailor-sdk function test-run resolvers/add.ts --arg "{\"a\":1}" diff --git a/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/double-quoted-shell/input.sh b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/double-quoted-shell/input.sh new file mode 100644 index 000000000..536cf9719 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/double-quoted-shell/input.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +tailor-sdk function test-run resolvers/add.ts --arg "{\"input\":{\"a\":1}}" diff --git a/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/equals-form/expected.sh b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/equals-form/expected.sh new file mode 100644 index 000000000..3008a9930 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/equals-form/expected.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +tailor-sdk function test-run resolvers/add.ts --arg='{"a":1}' diff --git a/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/equals-form/input.sh b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/equals-form/input.sh new file mode 100644 index 000000000..cbce5b7ad --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/test-run-arg-input/tests/equals-form/input.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +tailor-sdk function test-run resolvers/add.ts --arg='{"input":{"a":1}}' From 4d005606f06564d9dd2e4dc5f0581022080b1eb6 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 9 May 2026 17:39:42 +0900 Subject: [PATCH 12/15] feat(sdk-codemod): add v2/apply-to-deploy codemod Rewrite `tailor-sdk apply` invocations in package.json scripts, shell scripts, CI YAML, and Markdown to the v2-recommended `tailor-sdk deploy` alias. Optional `@version` pins are preserved. --- .changeset/v2-codemods-phase1.md | 1 + .../codemods/v2/apply-to-deploy/codemod.yaml | 7 ++ .../v2/apply-to-deploy/scripts/transform.ts | 66 +++++++++++++++++++ .../tests/basic-package-json/expected.json | 9 +++ .../tests/basic-package-json/input.json | 9 +++ .../tests/basic-shell/expected.sh | 6 ++ .../tests/basic-shell/input.sh | 6 ++ .../tests/basic-yaml/expected.yml | 11 ++++ .../tests/basic-yaml/input.yml | 11 ++++ .../apply-to-deploy/tests/no-match/input.sh | 9 +++ .../tests/version-qualified/expected.sh | 5 ++ .../tests/version-qualified/input.sh | 5 ++ packages/sdk-codemod/src/registry.ts | 10 +++ packages/sdk-codemod/src/transform.test.ts | 4 ++ packages/sdk-codemod/tsdown.config.ts | 1 + 15 files changed, 160 insertions(+) create mode 100644 packages/sdk-codemod/codemods/v2/apply-to-deploy/codemod.yaml create mode 100644 packages/sdk-codemod/codemods/v2/apply-to-deploy/scripts/transform.ts create mode 100644 packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-package-json/expected.json create mode 100644 packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-package-json/input.json create mode 100644 packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-shell/expected.sh create mode 100644 packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-shell/input.sh create mode 100644 packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-yaml/expected.yml create mode 100644 packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-yaml/input.yml create mode 100644 packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/no-match/input.sh create mode 100644 packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/version-qualified/expected.sh create mode 100644 packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/version-qualified/input.sh diff --git a/.changeset/v2-codemods-phase1.md b/.changeset/v2-codemods-phase1.md index 918cea19e..d46f15775 100644 --- a/.changeset/v2-codemods-phase1.md +++ b/.changeset/v2-codemods-phase1.md @@ -7,3 +7,4 @@ Add three v2 codemods that the upgrade runner can apply when migrating across th - `v2/test-run-arg-input` strips the deprecated `{ "input": ... }` wrapper from `tailor-sdk function test-run --arg` JSON inside `package.json` scripts, shell scripts, and Markdown code blocks. - `v2/sdk-skills-shim` rewrites `tailor-sdk-skills` invocations to `tailor-sdk skills install` across `package.json`, shell, YAML, and Markdown files. - `v2/principal-unify` renames `TailorUser` / `TailorActor` / `TailorInvoker` to the unified `TailorPrincipal`, drops `unauthenticatedTailorUser` (replacing standalone value references with `null`; member-access forms are left as-is so the resulting type error points authors at sites that need manual review), and renames `user` to `caller` inside `createResolver` body parameters and member accesses. +- `v2/apply-to-deploy` rewrites `tailor-sdk apply` invocations in `package.json` scripts, shell scripts, CI YAML, and Markdown to the v2-recommended `tailor-sdk deploy` alias. Optional `@version` pins (`tailor-sdk@latest`, `tailor-sdk@1.45.2`) are preserved. diff --git a/packages/sdk-codemod/codemods/v2/apply-to-deploy/codemod.yaml b/packages/sdk-codemod/codemods/v2/apply-to-deploy/codemod.yaml new file mode 100644 index 000000000..f9a072de5 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/apply-to-deploy/codemod.yaml @@ -0,0 +1,7 @@ +name: "@tailor-platform/apply-to-deploy" +version: "1.0.0" +description: "Rewrite `tailor-sdk apply` invocations to `tailor-sdk deploy` (the v2 recommended name)" +engine: jssg +language: text +since: "1.0.0" +until: "2.0.0" diff --git a/packages/sdk-codemod/codemods/v2/apply-to-deploy/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/apply-to-deploy/scripts/transform.ts new file mode 100644 index 000000000..bf50fec76 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/apply-to-deploy/scripts/transform.ts @@ -0,0 +1,66 @@ +import * as path from "pathe"; + +// Match `tailor-sdk apply` plus the optional `@version` suffix that +// package-manager run commands can add (`npx tailor-sdk@latest apply`, +// `pnpm dlx tailor-sdk@1.45.2 apply`). The version pin is preserved because +// `apply` and `deploy` are the same subcommand on the same binary. +// `(?![-\w])` excludes both word continuation (`applyConfig`) and dash-suffixed +// names (`apply-foo`) so a hypothetical sibling subcommand is not rewritten. +const APPLY_PATTERN = /\btailor-sdk(@[^\s'"`]+)?(\s+)apply(?![-\w])/g; + +function replaceApply(value: string): string { + return value.replace( + APPLY_PATTERN, + (_match, ver: string | undefined, sep: string) => `tailor-sdk${ver ?? ""}${sep}deploy`, + ); +} + +function transformText(source: string): string | null { + if (!APPLY_PATTERN.test(source)) return null; + APPLY_PATTERN.lastIndex = 0; + const updated = replaceApply(source); + return updated === source ? null : updated; +} + +function transformPackageJson(source: string): string | null { + let parsed: Record; + try { + parsed = JSON.parse(source) as Record; + } catch { + return null; + } + + let modified = false; + const scripts = parsed.scripts; + if (typeof scripts === "object" && scripts != null && !Array.isArray(scripts)) { + for (const [name, value] of Object.entries(scripts as Record)) { + if (typeof value !== "string") continue; + if (!value.includes("tailor-sdk")) continue; + const updated = replaceApply(value); + if (updated !== value) { + (scripts as Record)[name] = updated; + modified = true; + } + } + } + + if (!modified) return null; + const trailing = source.endsWith("\n") ? "\n" : ""; + return JSON.stringify(parsed, null, 2) + trailing; +} + +/** + * Replace `tailor-sdk apply` invocations with `tailor-sdk deploy`. + * + * `deploy` is a v1 alias of `apply` and the recommended name going forward. + * @param source - File contents + * @param filePath - Absolute path to the file (used to dispatch package.json vs text) + * @returns Transformed source or null when nothing matched. + */ +export default function transform(source: string, filePath: string): string | null { + if (!source.includes("tailor-sdk")) return null; + + const ext = path.extname(filePath).toLowerCase(); + if (ext === ".json") return transformPackageJson(source); + return transformText(source); +} diff --git a/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-package-json/expected.json b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-package-json/expected.json new file mode 100644 index 000000000..cb68f1c59 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-package-json/expected.json @@ -0,0 +1,9 @@ +{ + "name": "my-app", + "version": "1.0.0", + "scripts": { + "deploy:dev": "tailor-sdk deploy --profile dev", + "deploy:prod": "tailor-sdk deploy --profile prod -y", + "build": "tsc" + } +} diff --git a/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-package-json/input.json b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-package-json/input.json new file mode 100644 index 000000000..26cdfa98a --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-package-json/input.json @@ -0,0 +1,9 @@ +{ + "name": "my-app", + "version": "1.0.0", + "scripts": { + "deploy:dev": "tailor-sdk apply --profile dev", + "deploy:prod": "tailor-sdk apply --profile prod -y", + "build": "tsc" + } +} diff --git a/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-shell/expected.sh b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-shell/expected.sh new file mode 100644 index 000000000..c3705af3f --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-shell/expected.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail + +pnpm exec tailor-sdk deploy --dry-run +npx tailor-sdk deploy -y --profile prod +bunx tailor-sdk deploy --no-cache diff --git a/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-shell/input.sh b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-shell/input.sh new file mode 100644 index 000000000..f197add5e --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-shell/input.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail + +pnpm exec tailor-sdk apply --dry-run +npx tailor-sdk apply -y --profile prod +bunx tailor-sdk apply --no-cache diff --git a/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-yaml/expected.yml b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-yaml/expected.yml new file mode 100644 index 000000000..8c20a131a --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-yaml/expected.yml @@ -0,0 +1,11 @@ +name: Deploy +on: + push: + branches: [main] +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: pnpm install + - run: pnpm exec tailor-sdk deploy --profile prod -y diff --git a/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-yaml/input.yml b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-yaml/input.yml new file mode 100644 index 000000000..c149c181c --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/basic-yaml/input.yml @@ -0,0 +1,11 @@ +name: Deploy +on: + push: + branches: [main] +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: pnpm install + - run: pnpm exec tailor-sdk apply --profile prod -y diff --git a/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/no-match/input.sh b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/no-match/input.sh new file mode 100644 index 000000000..004f8c9d8 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/no-match/input.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Should not match: bare `apply` without `tailor-sdk` prefix is a generic word +echo "How to apply this configuration" +# Should not match: hypothetical sibling subcommand starting with `apply-` +pnpm exec tailor-sdk apply-foo +# Should not match: word continuation +pnpm exec tailor-sdk applyConfig diff --git a/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/version-qualified/expected.sh b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/version-qualified/expected.sh new file mode 100644 index 000000000..0dfe8ee88 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/version-qualified/expected.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +npx tailor-sdk@latest deploy --dry-run +pnpm dlx tailor-sdk@1.45.2 deploy -y diff --git a/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/version-qualified/input.sh b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/version-qualified/input.sh new file mode 100644 index 000000000..020bd1b89 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/apply-to-deploy/tests/version-qualified/input.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +npx tailor-sdk@latest apply --dry-run +pnpm dlx tailor-sdk@1.45.2 apply -y diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 9618370b4..17d8184b9 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -47,6 +47,16 @@ const allCodemods: CodemodPackage[] = [ scriptPath: "v2/principal-unify/scripts/transform.js", legacyPatterns: ["TailorUser", "TailorActor", "TailorInvoker", "unauthenticatedTailorUser"], }, + { + id: "v2/apply-to-deploy", + name: "tailor-sdk apply → tailor-sdk deploy", + description: + "Rewrite `tailor-sdk apply` invocations in package.json scripts, shell scripts, CI configs, and docs to the v2-recommended `tailor-sdk deploy` alias", + since: "1.0.0", + until: "2.0.0", + scriptPath: "v2/apply-to-deploy/scripts/transform.js", + filePatterns: ["**/package.json", "**/*.{sh,bash,zsh,yml,yaml}", "**/*.md"], + }, ]; /** diff --git a/packages/sdk-codemod/src/transform.test.ts b/packages/sdk-codemod/src/transform.test.ts index c2a919762..df7be4fa6 100644 --- a/packages/sdk-codemod/src/transform.test.ts +++ b/packages/sdk-codemod/src/transform.test.ts @@ -78,4 +78,8 @@ describe("codemod transforms", () => { it("v2/principal-unify transforms correctly", async () => { await runFixtureCases("v2/principal-unify"); }); + + it("v2/apply-to-deploy transforms correctly", async () => { + await runFixtureCases("v2/apply-to-deploy"); + }); }); diff --git a/packages/sdk-codemod/tsdown.config.ts b/packages/sdk-codemod/tsdown.config.ts index 0f2137ace..2c5e5da89 100644 --- a/packages/sdk-codemod/tsdown.config.ts +++ b/packages/sdk-codemod/tsdown.config.ts @@ -21,6 +21,7 @@ export default defineConfig([ "codemods/v2/test-run-arg-input/scripts/transform.ts", "v2/sdk-skills-shim/scripts/transform": "codemods/v2/sdk-skills-shim/scripts/transform.ts", "v2/principal-unify/scripts/transform": "codemods/v2/principal-unify/scripts/transform.ts", + "v2/apply-to-deploy/scripts/transform": "codemods/v2/apply-to-deploy/scripts/transform.ts", }, format: ["esm"], target: "node18", From da6657ca02c647eafd584fd6360a4110c12a5423 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 9 May 2026 18:05:13 +0900 Subject: [PATCH 13/15] feat(sdk-codemod): add v2/cli-rename codemod Apply v2 CLI naming conventions to user code: collapse the `crash-report` multi-word command to `crashreport` and rewrite camelCase long options (`--executionId`, `--executorName`, `--jobId`) to their kebab-case form across package.json scripts, shell, CI YAML, and Markdown. Optional `@version` pins on the binary are preserved. --- .changeset/v2-codemods-phase1.md | 1 + .../codemods/v2/cli-rename/codemod.yaml | 7 ++ .../v2/cli-rename/scripts/transform.ts | 89 +++++++++++++++++++ .../tests/basic-package-json/expected.json | 9 ++ .../tests/basic-package-json/input.json | 9 ++ .../cli-rename/tests/basic-shell/expected.sh | 7 ++ .../v2/cli-rename/tests/basic-shell/input.sh | 7 ++ .../cli-rename/tests/basic-yaml/expected.yml | 10 +++ .../v2/cli-rename/tests/basic-yaml/input.yml | 10 +++ .../v2/cli-rename/tests/no-match/input.sh | 11 +++ .../tests/version-qualified/expected.sh | 5 ++ .../tests/version-qualified/input.sh | 5 ++ packages/sdk-codemod/src/registry.ts | 10 +++ packages/sdk-codemod/src/transform.test.ts | 4 + packages/sdk-codemod/tsdown.config.ts | 1 + 15 files changed, 185 insertions(+) create mode 100644 packages/sdk-codemod/codemods/v2/cli-rename/codemod.yaml create mode 100644 packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts create mode 100644 packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-package-json/expected.json create mode 100644 packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-package-json/input.json create mode 100644 packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh create mode 100644 packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh create mode 100644 packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/expected.yml create mode 100644 packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/input.yml create mode 100644 packages/sdk-codemod/codemods/v2/cli-rename/tests/no-match/input.sh create mode 100644 packages/sdk-codemod/codemods/v2/cli-rename/tests/version-qualified/expected.sh create mode 100644 packages/sdk-codemod/codemods/v2/cli-rename/tests/version-qualified/input.sh diff --git a/.changeset/v2-codemods-phase1.md b/.changeset/v2-codemods-phase1.md index d46f15775..fc4c1e844 100644 --- a/.changeset/v2-codemods-phase1.md +++ b/.changeset/v2-codemods-phase1.md @@ -8,3 +8,4 @@ Add three v2 codemods that the upgrade runner can apply when migrating across th - `v2/sdk-skills-shim` rewrites `tailor-sdk-skills` invocations to `tailor-sdk skills install` across `package.json`, shell, YAML, and Markdown files. - `v2/principal-unify` renames `TailorUser` / `TailorActor` / `TailorInvoker` to the unified `TailorPrincipal`, drops `unauthenticatedTailorUser` (replacing standalone value references with `null`; member-access forms are left as-is so the resulting type error points authors at sites that need manual review), and renames `user` to `caller` inside `createResolver` body parameters and member accesses. - `v2/apply-to-deploy` rewrites `tailor-sdk apply` invocations in `package.json` scripts, shell scripts, CI YAML, and Markdown to the v2-recommended `tailor-sdk deploy` alias. Optional `@version` pins (`tailor-sdk@latest`, `tailor-sdk@1.45.2`) are preserved. +- `v2/cli-rename` applies the v2 CLI naming conventions: `tailor-sdk crash-report` becomes `tailor-sdk crashreport`, and the camelCase long options `--executionId`, `--executorName`, `--jobId` become `--execution-id`, `--executor-name`, `--job-id` across `package.json` scripts, shell scripts, CI YAML, and Markdown. diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/codemod.yaml b/packages/sdk-codemod/codemods/v2/cli-rename/codemod.yaml new file mode 100644 index 000000000..2937f57aa --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/cli-rename/codemod.yaml @@ -0,0 +1,7 @@ +name: "@tailor-platform/cli-rename" +version: "1.0.0" +description: "Apply v2 CLI naming conventions: single-word command names and kebab-case option names" +engine: jssg +language: text +since: "1.0.0" +until: "2.0.0" diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts new file mode 100644 index 000000000..bf6d3fc4c --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts @@ -0,0 +1,89 @@ +import * as path from "pathe"; + +// Map of v1 multi-word command names to their v2 single-word replacements. +const COMMAND_RENAMES: ReadonlyArray = [["crash-report", "crashreport"]]; + +// Map of v1 camelCase option names to their v2 kebab-case replacements. +const OPTION_RENAMES: ReadonlyArray = [ + ["executionId", "execution-id"], + ["executorName", "executor-name"], + ["jobId", "job-id"], +]; + +const COMMAND_PATTERN = new RegExp( + `\\btailor-sdk(@[^\\s'"\`]+)?(\\s+)(${COMMAND_RENAMES.map(([from]) => from).join("|")})\\b`, + "g", +); + +// Lookahead `(?![-\w])` excludes camelCase or dash-suffixed extensions +// (`--executionIdExtra`, `--executionId-foo`) so unrelated long flags are not +// rewritten by accident. +const OPTION_PATTERN = new RegExp( + `(--)(${OPTION_RENAMES.map(([from]) => from).join("|")})(?![-\\w])`, + "g", +); + +const COMMAND_MAP = new Map(COMMAND_RENAMES); +const OPTION_MAP = new Map(OPTION_RENAMES); + +function replaceAll(value: string): string { + const renamedCommands = value.replace( + COMMAND_PATTERN, + (_match, ver: string | undefined, sep: string, cmd: string) => + `tailor-sdk${ver ?? ""}${sep}${COMMAND_MAP.get(cmd) ?? cmd}`, + ); + return renamedCommands.replace( + OPTION_PATTERN, + (_match, dashes: string, opt: string) => `${dashes}${OPTION_MAP.get(opt) ?? opt}`, + ); +} + +function transformText(source: string): string | null { + if (!COMMAND_PATTERN.test(source) && !OPTION_PATTERN.test(source)) return null; + COMMAND_PATTERN.lastIndex = 0; + OPTION_PATTERN.lastIndex = 0; + const updated = replaceAll(source); + return updated === source ? null : updated; +} + +function transformPackageJson(source: string): string | null { + let parsed: Record; + try { + parsed = JSON.parse(source) as Record; + } catch { + return null; + } + + let modified = false; + const scripts = parsed.scripts; + if (typeof scripts === "object" && scripts != null && !Array.isArray(scripts)) { + for (const [name, value] of Object.entries(scripts as Record)) { + if (typeof value !== "string") continue; + const updated = replaceAll(value); + if (updated !== value) { + (scripts as Record)[name] = updated; + modified = true; + } + } + } + + if (!modified) return null; + const trailing = source.endsWith("\n") ? "\n" : ""; + return JSON.stringify(parsed, null, 2) + trailing; +} + +/** + * Apply v2 CLI naming conventions: + * - Multi-word commands collapse into a single word (`crash-report` → `crashreport`). + * - camelCase options become kebab-case (`--executionId` → `--execution-id`). + * + * Optional `@version` pins on the binary (`tailor-sdk@latest`) are preserved. + * @param source - File contents + * @param filePath - Absolute path to the file (used to dispatch package.json vs text) + * @returns Transformed source or null when nothing matched. + */ +export default function transform(source: string, filePath: string): string | null { + const ext = path.extname(filePath).toLowerCase(); + if (ext === ".json") return transformPackageJson(source); + return transformText(source); +} diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-package-json/expected.json b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-package-json/expected.json new file mode 100644 index 000000000..048382c0c --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-package-json/expected.json @@ -0,0 +1,9 @@ +{ + "name": "my-app", + "version": "1.0.0", + "scripts": { + "report:tail": "tailor-sdk crashreport tail --job-id latest", + "exec:show": "tailor-sdk executor show --executor-name onUserCreated --execution-id abc", + "build": "tsc" + } +} diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-package-json/input.json b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-package-json/input.json new file mode 100644 index 000000000..10462a341 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-package-json/input.json @@ -0,0 +1,9 @@ +{ + "name": "my-app", + "version": "1.0.0", + "scripts": { + "report:tail": "tailor-sdk crash-report tail --jobId latest", + "exec:show": "tailor-sdk executor show --executorName onUserCreated --executionId abc", + "build": "tsc" + } +} diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh new file mode 100644 index 000000000..3d8d6aabf --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +pnpm exec tailor-sdk crashreport list +pnpm exec tailor-sdk executor jobs --execution-id abc123 +pnpm exec tailor-sdk workflow show --executor-name onUserCreated +pnpm exec tailor-sdk workflow logs --job-id=def456 diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh new file mode 100644 index 000000000..5eb978c61 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +pnpm exec tailor-sdk crash-report list +pnpm exec tailor-sdk executor jobs --executionId abc123 +pnpm exec tailor-sdk workflow show --executorName onUserCreated +pnpm exec tailor-sdk workflow logs --jobId=def456 diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/expected.yml b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/expected.yml new file mode 100644 index 000000000..a5f0ad54d --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/expected.yml @@ -0,0 +1,10 @@ +name: Show executor logs +on: workflow_dispatch +jobs: + logs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: pnpm install + - run: pnpm exec tailor-sdk crashreport tail --job-id latest + - run: pnpm exec tailor-sdk executor show --executor-name onUserCreated --execution-id abc diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/input.yml b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/input.yml new file mode 100644 index 000000000..d83964742 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/input.yml @@ -0,0 +1,10 @@ +name: Show executor logs +on: workflow_dispatch +jobs: + logs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: pnpm install + - run: pnpm exec tailor-sdk crash-report tail --jobId latest + - run: pnpm exec tailor-sdk executor show --executorName onUserCreated --executionId abc diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/no-match/input.sh b/packages/sdk-codemod/codemods/v2/cli-rename/tests/no-match/input.sh new file mode 100644 index 000000000..bccacf1ae --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/no-match/input.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Should not match: command name is part of a longer word +pnpm exec tailor-sdk crash-reporter list +# Should not match: option is a prefix of a different flag +pnpm exec tailor-sdk executor show --executionIdExtra value +# Should not match: option has dash continuation, not a hit +pnpm exec tailor-sdk executor show --executionId-foo value +# Should not match: bare crash-report not preceded by tailor-sdk +echo "Generated crash-report uploaded" diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/version-qualified/expected.sh b/packages/sdk-codemod/codemods/v2/cli-rename/tests/version-qualified/expected.sh new file mode 100644 index 000000000..8c47d0e8f --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/version-qualified/expected.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +npx tailor-sdk@latest crashreport list +pnpm dlx tailor-sdk@1.45.2 crashreport tail --job-id latest diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/version-qualified/input.sh b/packages/sdk-codemod/codemods/v2/cli-rename/tests/version-qualified/input.sh new file mode 100644 index 000000000..4417b3507 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/version-qualified/input.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +npx tailor-sdk@latest crash-report list +pnpm dlx tailor-sdk@1.45.2 crash-report tail --jobId latest diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 17d8184b9..0c20faa6a 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -57,6 +57,16 @@ const allCodemods: CodemodPackage[] = [ scriptPath: "v2/apply-to-deploy/scripts/transform.js", filePatterns: ["**/package.json", "**/*.{sh,bash,zsh,yml,yaml}", "**/*.md"], }, + { + id: "v2/cli-rename", + name: "v2 CLI rename (single-word commands, kebab-case options)", + description: + "Rewrite `tailor-sdk crash-report` → `crashreport` and camelCase long options (`--executionId`, `--executorName`, `--jobId`) to their kebab-case form across package.json scripts, shell scripts, CI configs, and docs", + since: "1.0.0", + until: "2.0.0", + scriptPath: "v2/cli-rename/scripts/transform.js", + filePatterns: ["**/package.json", "**/*.{sh,bash,zsh,yml,yaml}", "**/*.md"], + }, ]; /** diff --git a/packages/sdk-codemod/src/transform.test.ts b/packages/sdk-codemod/src/transform.test.ts index df7be4fa6..5a934a1e4 100644 --- a/packages/sdk-codemod/src/transform.test.ts +++ b/packages/sdk-codemod/src/transform.test.ts @@ -82,4 +82,8 @@ describe("codemod transforms", () => { it("v2/apply-to-deploy transforms correctly", async () => { await runFixtureCases("v2/apply-to-deploy"); }); + + it("v2/cli-rename transforms correctly", async () => { + await runFixtureCases("v2/cli-rename"); + }); }); diff --git a/packages/sdk-codemod/tsdown.config.ts b/packages/sdk-codemod/tsdown.config.ts index 2c5e5da89..91b7da2d5 100644 --- a/packages/sdk-codemod/tsdown.config.ts +++ b/packages/sdk-codemod/tsdown.config.ts @@ -22,6 +22,7 @@ export default defineConfig([ "v2/sdk-skills-shim/scripts/transform": "codemods/v2/sdk-skills-shim/scripts/transform.ts", "v2/principal-unify/scripts/transform": "codemods/v2/principal-unify/scripts/transform.ts", "v2/apply-to-deploy/scripts/transform": "codemods/v2/apply-to-deploy/scripts/transform.ts", + "v2/cli-rename/scripts/transform": "codemods/v2/cli-rename/scripts/transform.ts", }, format: ["esm"], target: "node18", From d9e0ed69338a5f6fd25216c9b97df7fa1760f4bc Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 9 May 2026 18:54:10 +0900 Subject: [PATCH 14/15] refactor(sdk-codemod): trim v2/cli-rename to crashreport-only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the camelCase long option transform (`--executionId` → `--execution-id`, etc.). The targeted identifiers are positional arguments in the SDK CLI, not long flags, so user scripts don't carry them and the transform had no real-world target. The transform now only rewrites `tailor-sdk crash-report` → `tailor-sdk crashreport`. Update fixtures, registry description, and changeset bullet to match. Add a no-match case asserting the camelCase positional tokens are intentionally left alone. --- .changeset/v2-codemods-phase1.md | 2 +- .../v2/cli-rename/scripts/transform.ts | 36 +++++-------------- .../tests/basic-package-json/expected.json | 4 +-- .../tests/basic-package-json/input.json | 4 +-- .../cli-rename/tests/basic-shell/expected.sh | 4 +-- .../v2/cli-rename/tests/basic-shell/input.sh | 4 +-- .../cli-rename/tests/basic-yaml/expected.yml | 8 ++--- .../v2/cli-rename/tests/basic-yaml/input.yml | 8 ++--- .../v2/cli-rename/tests/no-match/input.sh | 7 ++-- .../tests/version-qualified/expected.sh | 2 +- .../tests/version-qualified/input.sh | 2 +- packages/sdk-codemod/src/registry.ts | 4 +-- 12 files changed, 31 insertions(+), 54 deletions(-) diff --git a/.changeset/v2-codemods-phase1.md b/.changeset/v2-codemods-phase1.md index fc4c1e844..d2d97c482 100644 --- a/.changeset/v2-codemods-phase1.md +++ b/.changeset/v2-codemods-phase1.md @@ -8,4 +8,4 @@ Add three v2 codemods that the upgrade runner can apply when migrating across th - `v2/sdk-skills-shim` rewrites `tailor-sdk-skills` invocations to `tailor-sdk skills install` across `package.json`, shell, YAML, and Markdown files. - `v2/principal-unify` renames `TailorUser` / `TailorActor` / `TailorInvoker` to the unified `TailorPrincipal`, drops `unauthenticatedTailorUser` (replacing standalone value references with `null`; member-access forms are left as-is so the resulting type error points authors at sites that need manual review), and renames `user` to `caller` inside `createResolver` body parameters and member accesses. - `v2/apply-to-deploy` rewrites `tailor-sdk apply` invocations in `package.json` scripts, shell scripts, CI YAML, and Markdown to the v2-recommended `tailor-sdk deploy` alias. Optional `@version` pins (`tailor-sdk@latest`, `tailor-sdk@1.45.2`) are preserved. -- `v2/cli-rename` applies the v2 CLI naming conventions: `tailor-sdk crash-report` becomes `tailor-sdk crashreport`, and the camelCase long options `--executionId`, `--executorName`, `--jobId` become `--execution-id`, `--executor-name`, `--job-id` across `package.json` scripts, shell scripts, CI YAML, and Markdown. +- `v2/cli-rename` rewrites `tailor-sdk crash-report` invocations to the v2 single-word `tailor-sdk crashreport` form across `package.json` scripts, shell scripts, CI YAML, and Markdown. Optional `@version` pins are preserved. diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts index bf6d3fc4c..fbe72152c 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts +++ b/packages/sdk-codemod/codemods/v2/cli-rename/scripts/transform.ts @@ -3,45 +3,24 @@ import * as path from "pathe"; // Map of v1 multi-word command names to their v2 single-word replacements. const COMMAND_RENAMES: ReadonlyArray = [["crash-report", "crashreport"]]; -// Map of v1 camelCase option names to their v2 kebab-case replacements. -const OPTION_RENAMES: ReadonlyArray = [ - ["executionId", "execution-id"], - ["executorName", "executor-name"], - ["jobId", "job-id"], -]; - const COMMAND_PATTERN = new RegExp( `\\btailor-sdk(@[^\\s'"\`]+)?(\\s+)(${COMMAND_RENAMES.map(([from]) => from).join("|")})\\b`, "g", ); -// Lookahead `(?![-\w])` excludes camelCase or dash-suffixed extensions -// (`--executionIdExtra`, `--executionId-foo`) so unrelated long flags are not -// rewritten by accident. -const OPTION_PATTERN = new RegExp( - `(--)(${OPTION_RENAMES.map(([from]) => from).join("|")})(?![-\\w])`, - "g", -); - const COMMAND_MAP = new Map(COMMAND_RENAMES); -const OPTION_MAP = new Map(OPTION_RENAMES); function replaceAll(value: string): string { - const renamedCommands = value.replace( + return value.replace( COMMAND_PATTERN, (_match, ver: string | undefined, sep: string, cmd: string) => `tailor-sdk${ver ?? ""}${sep}${COMMAND_MAP.get(cmd) ?? cmd}`, ); - return renamedCommands.replace( - OPTION_PATTERN, - (_match, dashes: string, opt: string) => `${dashes}${OPTION_MAP.get(opt) ?? opt}`, - ); } function transformText(source: string): string | null { - if (!COMMAND_PATTERN.test(source) && !OPTION_PATTERN.test(source)) return null; + if (!COMMAND_PATTERN.test(source)) return null; COMMAND_PATTERN.lastIndex = 0; - OPTION_PATTERN.lastIndex = 0; const updated = replaceAll(source); return updated === source ? null : updated; } @@ -73,11 +52,14 @@ function transformPackageJson(source: string): string | null { } /** - * Apply v2 CLI naming conventions: - * - Multi-word commands collapse into a single word (`crash-report` → `crashreport`). - * - camelCase options become kebab-case (`--executionId` → `--execution-id`). + * Apply v2 CLI naming conventions: multi-word commands collapse into a single + * word (`crash-report` → `crashreport`). Optional `@version` pins on the binary + * (`tailor-sdk@latest`) are preserved. * - * Optional `@version` pins on the binary (`tailor-sdk@latest`) are preserved. + * Long options (`--executionId`, `--executorName`, `--jobId`) and the + * positional argument keys with the same names are intentionally not rewritten: + * those tokens are positional in the SDK CLI and never appear as long flags in + * user scripts, so a transform here would have no real-world target. * @param source - File contents * @param filePath - Absolute path to the file (used to dispatch package.json vs text) * @returns Transformed source or null when nothing matched. diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-package-json/expected.json b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-package-json/expected.json index 048382c0c..c8a36feda 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-package-json/expected.json +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-package-json/expected.json @@ -2,8 +2,8 @@ "name": "my-app", "version": "1.0.0", "scripts": { - "report:tail": "tailor-sdk crashreport tail --job-id latest", - "exec:show": "tailor-sdk executor show --executor-name onUserCreated --execution-id abc", + "report:tail": "tailor-sdk crashreport list", + "report:send": "tailor-sdk crashreport send --file ./latest.crash.log", "build": "tsc" } } diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-package-json/input.json b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-package-json/input.json index 10462a341..fe43c4b44 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-package-json/input.json +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-package-json/input.json @@ -2,8 +2,8 @@ "name": "my-app", "version": "1.0.0", "scripts": { - "report:tail": "tailor-sdk crash-report tail --jobId latest", - "exec:show": "tailor-sdk executor show --executorName onUserCreated --executionId abc", + "report:tail": "tailor-sdk crash-report list", + "report:send": "tailor-sdk crash-report send --file ./latest.crash.log", "build": "tsc" } } diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh index 3d8d6aabf..274b44990 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/expected.sh @@ -2,6 +2,4 @@ set -euo pipefail pnpm exec tailor-sdk crashreport list -pnpm exec tailor-sdk executor jobs --execution-id abc123 -pnpm exec tailor-sdk workflow show --executor-name onUserCreated -pnpm exec tailor-sdk workflow logs --job-id=def456 +pnpm exec tailor-sdk crashreport send --file ./latest.crash.log diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh index 5eb978c61..6202aa2aa 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-shell/input.sh @@ -2,6 +2,4 @@ set -euo pipefail pnpm exec tailor-sdk crash-report list -pnpm exec tailor-sdk executor jobs --executionId abc123 -pnpm exec tailor-sdk workflow show --executorName onUserCreated -pnpm exec tailor-sdk workflow logs --jobId=def456 +pnpm exec tailor-sdk crash-report send --file ./latest.crash.log diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/expected.yml b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/expected.yml index a5f0ad54d..0cddd575f 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/expected.yml +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/expected.yml @@ -1,10 +1,10 @@ -name: Show executor logs +name: Tail crash reports on: workflow_dispatch jobs: - logs: + tail: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: pnpm install - - run: pnpm exec tailor-sdk crashreport tail --job-id latest - - run: pnpm exec tailor-sdk executor show --executor-name onUserCreated --execution-id abc + - run: pnpm exec tailor-sdk crashreport list + - run: pnpm exec tailor-sdk crashreport send --file latest.crash.log diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/input.yml b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/input.yml index d83964742..83ab2b5f5 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/input.yml +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/basic-yaml/input.yml @@ -1,10 +1,10 @@ -name: Show executor logs +name: Tail crash reports on: workflow_dispatch jobs: - logs: + tail: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: pnpm install - - run: pnpm exec tailor-sdk crash-report tail --jobId latest - - run: pnpm exec tailor-sdk executor show --executorName onUserCreated --executionId abc + - run: pnpm exec tailor-sdk crash-report list + - run: pnpm exec tailor-sdk crash-report send --file latest.crash.log diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/no-match/input.sh b/packages/sdk-codemod/codemods/v2/cli-rename/tests/no-match/input.sh index bccacf1ae..a17ea544a 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/no-match/input.sh +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/no-match/input.sh @@ -3,9 +3,8 @@ set -euo pipefail # Should not match: command name is part of a longer word pnpm exec tailor-sdk crash-reporter list -# Should not match: option is a prefix of a different flag -pnpm exec tailor-sdk executor show --executionIdExtra value -# Should not match: option has dash continuation, not a hit -pnpm exec tailor-sdk executor show --executionId-foo value # Should not match: bare crash-report not preceded by tailor-sdk echo "Generated crash-report uploaded" +# Should not match: positional/long-form camelCase identifiers are out of scope +pnpm exec tailor-sdk function logs --executionId abc +pnpm exec tailor-sdk executor jobs my-executor --jobId xyz diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/version-qualified/expected.sh b/packages/sdk-codemod/codemods/v2/cli-rename/tests/version-qualified/expected.sh index 8c47d0e8f..5edd7a651 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/version-qualified/expected.sh +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/version-qualified/expected.sh @@ -2,4 +2,4 @@ set -euo pipefail npx tailor-sdk@latest crashreport list -pnpm dlx tailor-sdk@1.45.2 crashreport tail --job-id latest +pnpm dlx tailor-sdk@1.45.2 crashreport send --file ./latest.crash.log diff --git a/packages/sdk-codemod/codemods/v2/cli-rename/tests/version-qualified/input.sh b/packages/sdk-codemod/codemods/v2/cli-rename/tests/version-qualified/input.sh index 4417b3507..1296a3364 100644 --- a/packages/sdk-codemod/codemods/v2/cli-rename/tests/version-qualified/input.sh +++ b/packages/sdk-codemod/codemods/v2/cli-rename/tests/version-qualified/input.sh @@ -2,4 +2,4 @@ set -euo pipefail npx tailor-sdk@latest crash-report list -pnpm dlx tailor-sdk@1.45.2 crash-report tail --jobId latest +pnpm dlx tailor-sdk@1.45.2 crash-report send --file ./latest.crash.log diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 0c20faa6a..c5bdcd268 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -59,9 +59,9 @@ const allCodemods: CodemodPackage[] = [ }, { id: "v2/cli-rename", - name: "v2 CLI rename (single-word commands, kebab-case options)", + name: "v2 CLI rename (single-word commands)", description: - "Rewrite `tailor-sdk crash-report` → `crashreport` and camelCase long options (`--executionId`, `--executorName`, `--jobId`) to their kebab-case form across package.json scripts, shell scripts, CI configs, and docs", + "Rewrite `tailor-sdk crash-report` invocations to the v2 single-word `tailor-sdk crashreport` form across package.json scripts, shell scripts, CI configs, and docs", since: "1.0.0", until: "2.0.0", scriptPath: "v2/cli-rename/scripts/transform.js", From 3b605d61ab4659ff3002a603b0f9f72449944464 Mon Sep 17 00:00:00 2001 From: dqn Date: Sat, 9 May 2026 19:02:45 +0900 Subject: [PATCH 15/15] feat(sdk-codemod): add v2/auth-invoker-unwrap codemod Replace `auth.invoker("name")` calls with the bare `"name"` string literal and drop the `auth` import when no other reference remains. Calls whose argument is not a literal string (`auth.invoker(variable)`, template literals) are intentionally left untouched. Mirrors the deprecation in tailor-platform/sdk#971: importing `auth` from `tailor.config.ts` into runtime files pulls Node-only modules into the bundle. --- .changeset/v2-codemods-phase1.md | 1 + .../v2/auth-invoker-unwrap/codemod.yaml | 7 + .../auth-invoker-unwrap/scripts/transform.ts | 185 ++++++++++++++++++ .../tests/basic-resolver/expected.ts | 21 ++ .../tests/basic-resolver/input.ts | 22 +++ .../multi-import-drops-only-auth/expected.ts | 6 + .../multi-import-drops-only-auth/input.ts | 6 + .../tests/multi-import-keeps-auth/expected.ts | 8 + .../tests/multi-import-keeps-auth/input.ts | 8 + .../tests/no-match/input.ts | 9 + .../tests/non-literal-arg-untouched/input.ts | 9 + packages/sdk-codemod/src/registry.ts | 10 + packages/sdk-codemod/src/transform.test.ts | 4 + packages/sdk-codemod/tsdown.config.ts | 2 + 14 files changed, 298 insertions(+) create mode 100644 packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/codemod.yaml create mode 100644 packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/basic-resolver/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/basic-resolver/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-drops-only-auth/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-drops-only-auth/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-keeps-auth/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-keeps-auth/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/no-match/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/non-literal-arg-untouched/input.ts diff --git a/.changeset/v2-codemods-phase1.md b/.changeset/v2-codemods-phase1.md index d2d97c482..c06c874b2 100644 --- a/.changeset/v2-codemods-phase1.md +++ b/.changeset/v2-codemods-phase1.md @@ -9,3 +9,4 @@ Add three v2 codemods that the upgrade runner can apply when migrating across th - `v2/principal-unify` renames `TailorUser` / `TailorActor` / `TailorInvoker` to the unified `TailorPrincipal`, drops `unauthenticatedTailorUser` (replacing standalone value references with `null`; member-access forms are left as-is so the resulting type error points authors at sites that need manual review), and renames `user` to `caller` inside `createResolver` body parameters and member accesses. - `v2/apply-to-deploy` rewrites `tailor-sdk apply` invocations in `package.json` scripts, shell scripts, CI YAML, and Markdown to the v2-recommended `tailor-sdk deploy` alias. Optional `@version` pins (`tailor-sdk@latest`, `tailor-sdk@1.45.2`) are preserved. - `v2/cli-rename` rewrites `tailor-sdk crash-report` invocations to the v2 single-word `tailor-sdk crashreport` form across `package.json` scripts, shell scripts, CI YAML, and Markdown. Optional `@version` pins are preserved. +- `v2/auth-invoker-unwrap` replaces `auth.invoker("name")` calls with the bare `"name"` string literal and drops the `auth` import when it has no other reference. Calls whose argument is not a literal string (`auth.invoker(variable)`, template literals) are left untouched so the author can decide. diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/codemod.yaml b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/codemod.yaml new file mode 100644 index 000000000..fae6d759f --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/codemod.yaml @@ -0,0 +1,7 @@ +name: "@tailor-platform/auth-invoker-unwrap" +version: "1.0.0" +description: 'Replace `auth.invoker("name")` with the bare string `"name"` and drop the no-longer-needed `auth` import' +engine: jssg +language: typescript +since: "1.0.0" +until: "2.0.0" diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts new file mode 100644 index 000000000..63bbd413b --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/scripts/transform.ts @@ -0,0 +1,185 @@ +import { parse, Lang } from "@ast-grep/napi"; +import type { Edit, SgNode } from "@ast-grep/napi"; + +const QUICK_FILTER_NEEDLE = "auth.invoker"; + +function quickFilter(source: string): boolean { + return source.includes(QUICK_FILTER_NEEDLE); +} + +function isInsideImportStatement(node: SgNode): boolean { + let current: SgNode | null = node.parent(); + while (current) { + if (current.kind() === "import_statement") return true; + current = current.parent(); + } + return false; +} + +interface InvokerCall { + /** The full `auth.invoker(...)` call expression node. */ + callNode: SgNode; + /** The string literal argument node, including its surrounding quotes. */ + argText: string; + /** Byte range covered by this call expression. */ + range: [number, number]; +} + +/** + * Find every `auth.invoker()` call in `root`. Calls whose + * argument is not a literal string (e.g. `auth.invoker(name)`, + * `auth.invoker(\`x\${y}\`)`) are intentionally ignored: only the literal form + * is safely replaceable. + */ +function findInvokerCalls(root: SgNode): InvokerCall[] { + const matches = root.findAll({ rule: { pattern: "auth.invoker($NAME)" } }); + const out: InvokerCall[] = []; + for (const match of matches) { + const arg = match.getMatch("NAME"); + if (!arg) continue; + if (arg.kind() !== "string") continue; + const r = match.range(); + out.push({ callNode: match, argText: arg.text(), range: [r.start.index, r.end.index] }); + } + return out; +} + +/** + * Count `auth` identifier references that are not part of an import statement + * and not part of any of the `auth.invoker(...)` calls already scheduled for + * replacement. A non-zero return means the `auth` import must be preserved. + */ +function countRemainingAuthRefs( + root: SgNode, + scheduledCallRanges: Array<[number, number]>, +): number { + const idents = root.findAll({ rule: { kind: "identifier", regex: "^auth$" } }); + let count = 0; + for (const node of idents) { + if (isInsideImportStatement(node)) continue; + const r = node.range(); + const start = r.start.index; + const inScheduled = scheduledCallRanges.some(([s, e]) => start >= s && start < e); + if (inScheduled) continue; + count++; + } + return count; +} + +interface ImportSpec { + spec: SgNode; + importedName: string; + localName: string; +} + +function* iterateImportSpecs(importStmt: SgNode): Generator { + const specs = importStmt.findAll({ rule: { kind: "import_specifier" } }); + for (const spec of specs) { + const idents = spec.children().filter((c: SgNode) => c.kind() === "identifier"); + if (idents.length === 0) continue; + const importedName = idents[0]!.text(); + const aliasNode = idents[1]; + yield { + spec, + importedName, + localName: aliasNode?.text() ?? importedName, + }; + } +} + +/** + * Build an Edit that removes the `auth` specifier from `importStmt`. Returns + * null if the statement does not import `auth`. When `auth` is the only + * specifier the entire import line is removed (including a trailing newline); + * otherwise just the `auth,` / `, auth` fragment is dropped. + */ +function buildAuthImportRemovalEdit(source: string, importStmt: SgNode): Edit | null { + const specs = Array.from(iterateImportSpecs(importStmt)); + const authSpec = specs.find((s) => s.localName === "auth" && s.importedName === "auth"); + if (!authSpec) return null; + + if (specs.length === 1) { + const r = importStmt.range(); + let end = r.end.index; + while (end < source.length && (source[end] === "\n" || source[end] === "\r")) end++; + return { + startPos: r.start.index, + endPos: end, + insertedText: "", + }; + } + + const r = authSpec.spec.range(); + let start = r.start.index; + let end = r.end.index; + // Eat one neighbor `,` (and adjacent whitespace) so the resulting list stays + // syntactically valid. + while (end < source.length && (source[end] === " " || source[end] === "\t")) end++; + if (source[end] === ",") { + end++; + while (end < source.length && (source[end] === " " || source[end] === "\t")) end++; + return { startPos: start, endPos: end, insertedText: "" }; + } + while (start > 0 && (source[start - 1] === " " || source[start - 1] === "\t")) start--; + if (source[start - 1] === ",") { + start--; + while (start > 0 && (source[start - 1] === " " || source[start - 1] === "\t")) start--; + return { startPos: start, endPos: end, insertedText: "" }; + } + return { startPos: r.start.index, endPos: r.end.index, insertedText: "" }; +} + +function findAuthImports(root: SgNode): SgNode[] { + const stmts = root.findAll({ rule: { kind: "import_statement" } }); + return stmts.filter((stmt) => { + for (const { localName, importedName } of iterateImportSpecs(stmt)) { + if (localName === "auth" && importedName === "auth") return true; + } + return false; + }); +} + +/** + * Replace `auth.invoker("name")` calls with the bare `"name"` string literal. + * If no other `auth` references remain after the rewrite, drop the `auth` + * specifier (or the entire import line when `auth` was its sole specifier). + * + * `auth.invoker()` was deprecated in favor of passing the machine user name + * directly; carrying the `auth` import only for `.invoker()` would otherwise + * pull config-layer (Node-only) modules into runtime bundles. + * @param source - File contents + * @param filePath - Absolute path to the file (kept for the runner signature) + * @returns Transformed source or null when nothing matched. + */ +export default function transform(source: string, _filePath: string): string | null { + if (!quickFilter(source)) return null; + + const lang = source.includes("") ? Lang.Tsx : Lang.TypeScript; + const root = parse(lang, source).root(); + + const calls = findInvokerCalls(root); + if (calls.length === 0) return null; + + const edits: Edit[] = calls.map((c) => c.callNode.replace(c.argText)); + + const remaining = countRemainingAuthRefs( + root, + calls.map((c) => c.range), + ); + if (remaining === 0) { + for (const importStmt of findAuthImports(root)) { + const edit = buildAuthImportRemovalEdit(source, importStmt); + if (edit) edits.push(edit); + } + } + + if (edits.length === 0) return null; + + let result = root.commitEdits(edits); + + // Normalize: drop the leading blank line that an import removal at the top + // of the file leaves behind, and collapse runs of 3+ newlines. + result = result.replace(/^[\t ]*\n+/, "").replace(/\n{3,}/g, "\n\n"); + + return result === source ? null : result; +} diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/basic-resolver/expected.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/basic-resolver/expected.ts new file mode 100644 index 000000000..70ea71c4b --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/basic-resolver/expected.ts @@ -0,0 +1,21 @@ +import { createResolver, t } from "@tailor-platform/sdk"; +import orderProcessingWorkflow from "../workflows/order-processing"; + +export default createResolver({ + name: "triggerWorkflow", + type: "Mutation", + input: { + orderId: t.string().description("Order ID"), + customerId: t.string().description("Customer ID for the order"), + }, + body: async ({ input }) => { + const workflowRunId = await orderProcessingWorkflow.trigger( + { + orderId: input.orderId, + customerId: input.customerId, + }, + { authInvoker: "manager-machine-user" }, + ); + return workflowRunId; + }, +}); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/basic-resolver/input.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/basic-resolver/input.ts new file mode 100644 index 000000000..e0fedfc84 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/basic-resolver/input.ts @@ -0,0 +1,22 @@ +import { createResolver, t } from "@tailor-platform/sdk"; +import { auth } from "../tailor.config"; +import orderProcessingWorkflow from "../workflows/order-processing"; + +export default createResolver({ + name: "triggerWorkflow", + type: "Mutation", + input: { + orderId: t.string().description("Order ID"), + customerId: t.string().description("Customer ID for the order"), + }, + body: async ({ input }) => { + const workflowRunId = await orderProcessingWorkflow.trigger( + { + orderId: input.orderId, + customerId: input.customerId, + }, + { authInvoker: auth.invoker("manager-machine-user") }, + ); + return workflowRunId; + }, +}); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-drops-only-auth/expected.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-drops-only-auth/expected.ts new file mode 100644 index 000000000..be4be98b8 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-drops-only-auth/expected.ts @@ -0,0 +1,6 @@ +import { db } from "../tailor.config"; + +export const cfg = { + authInvoker: "kiosk", + table: db.type("Order"), +}; diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-drops-only-auth/input.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-drops-only-auth/input.ts new file mode 100644 index 000000000..b9f79afd2 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-drops-only-auth/input.ts @@ -0,0 +1,6 @@ +import { auth, db } from "../tailor.config"; + +export const cfg = { + authInvoker: auth.invoker("kiosk"), + table: db.type("Order"), +}; diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-keeps-auth/expected.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-keeps-auth/expected.ts new file mode 100644 index 000000000..9db289de7 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-keeps-auth/expected.ts @@ -0,0 +1,8 @@ +import { auth, db } from "../tailor.config"; + +export const cfg = { + authInvoker: "kiosk", + // `auth` is still referenced below, so the import must be preserved. + ownerType: auth.machineUser, + table: db.type("Order"), +}; diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-keeps-auth/input.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-keeps-auth/input.ts new file mode 100644 index 000000000..2381e1fd6 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/multi-import-keeps-auth/input.ts @@ -0,0 +1,8 @@ +import { auth, db } from "../tailor.config"; + +export const cfg = { + authInvoker: auth.invoker("kiosk"), + // `auth` is still referenced below, so the import must be preserved. + ownerType: auth.machineUser, + table: db.type("Order"), +}; diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/no-match/input.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/no-match/input.ts new file mode 100644 index 000000000..a0d997203 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/no-match/input.ts @@ -0,0 +1,9 @@ +import { createResolver, t } from "@tailor-platform/sdk"; + +// No `auth.invoker(...)` call here, the codemod should be a no-op. +export default createResolver({ + name: "noop", + type: "Query", + input: {}, + body: () => ({ ok: true }), +}); diff --git a/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/non-literal-arg-untouched/input.ts b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/non-literal-arg-untouched/input.ts new file mode 100644 index 000000000..feee54fe2 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/auth-invoker-unwrap/tests/non-literal-arg-untouched/input.ts @@ -0,0 +1,9 @@ +import { auth } from "../tailor.config"; + +const machineUserName = "kiosk"; + +export const cfg = { + // The argument is not a literal string, so the call is left intact and the + // `auth` import stays. + authInvoker: auth.invoker(machineUserName), +}; diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index c5bdcd268..ddd4bf315 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -67,6 +67,16 @@ const allCodemods: CodemodPackage[] = [ scriptPath: "v2/cli-rename/scripts/transform.js", filePatterns: ["**/package.json", "**/*.{sh,bash,zsh,yml,yaml}", "**/*.md"], }, + { + id: "v2/auth-invoker-unwrap", + name: 'auth.invoker("name") → "name"', + description: + 'Replace `auth.invoker("name")` calls with the bare `"name"` string and drop the `auth` import when no other reference remains. The `auth.invoker()` helper is deprecated in v2 because importing `auth` from `tailor.config.ts` into runtime files pulls Node-only modules into the bundle.', + since: "1.0.0", + until: "2.0.0", + scriptPath: "v2/auth-invoker-unwrap/scripts/transform.js", + legacyPatterns: ["auth.invoker"], + }, ]; /** diff --git a/packages/sdk-codemod/src/transform.test.ts b/packages/sdk-codemod/src/transform.test.ts index 5a934a1e4..cef6eb1ac 100644 --- a/packages/sdk-codemod/src/transform.test.ts +++ b/packages/sdk-codemod/src/transform.test.ts @@ -86,4 +86,8 @@ describe("codemod transforms", () => { it("v2/cli-rename transforms correctly", async () => { await runFixtureCases("v2/cli-rename"); }); + + it("v2/auth-invoker-unwrap transforms correctly", async () => { + await runFixtureCases("v2/auth-invoker-unwrap"); + }); }); diff --git a/packages/sdk-codemod/tsdown.config.ts b/packages/sdk-codemod/tsdown.config.ts index 91b7da2d5..c6c799ed5 100644 --- a/packages/sdk-codemod/tsdown.config.ts +++ b/packages/sdk-codemod/tsdown.config.ts @@ -23,6 +23,8 @@ export default defineConfig([ "v2/principal-unify/scripts/transform": "codemods/v2/principal-unify/scripts/transform.ts", "v2/apply-to-deploy/scripts/transform": "codemods/v2/apply-to-deploy/scripts/transform.ts", "v2/cli-rename/scripts/transform": "codemods/v2/cli-rename/scripts/transform.ts", + "v2/auth-invoker-unwrap/scripts/transform": + "codemods/v2/auth-invoker-unwrap/scripts/transform.ts", }, format: ["esm"], target: "node18",