@tailor-platform/sdk-codemod 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # @tailor-platform/sdk-codemod
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#2088](https://github.com/tailor-platform/sdk/pull/2088) [`c9f91d9`](https://github.com/tailor-platform/sdk/commit/c9f91d944e4b041250979ec4438c36cabf818e14) Thanks [@dqn](https://github.com/dqn)! - The `--branch` option of `tailor setup branch` is renamed to `--target`, so it no longer collides with the subcommand name. The old spelling keeps working as a deprecated alias until v3 and prints a deprecation warning when used; `tailor upgrade` offers the `v3/setup-branch-flag-rename` codemod to rewrite `setup branch --branch` invocations across package.json scripts, shell and Windows scripts, YAML, Markdown, and JavaScript/TypeScript sources. The `--branch` option of `setup tag`, `setup preview`, and `setup coordinate` is unchanged.
8
+
9
+ ### Patch Changes
10
+
11
+ - [#2137](https://github.com/tailor-platform/sdk/pull/2137) [`d38497a`](https://github.com/tailor-platform/sdk/commit/d38497ac214f547483028e2622d53dbf1414ecb2) Thanks [@dqn](https://github.com/dqn)! - Keep LLM review detection working in multi-major upgrades: each codemod's review detector and suspicious patterns now inspect the file as of that codemod's position in the transform chain, so a later codemod's rewrite can no longer silently hide an earlier codemod's review findings.
12
+
13
+ - [#2138](https://github.com/tailor-platform/sdk/pull/2138) [`870c8cd`](https://github.com/tailor-platform/sdk/commit/870c8cdaa007adcbb8f565fe9b4d238d98c00e6d) Thanks [@dqn](https://github.com/dqn)! - Stop exporting internal declarations that were only used within their own module.
14
+
15
+ - [#2089](https://github.com/tailor-platform/sdk/pull/2089) [`90948e6`](https://github.com/tailor-platform/sdk/commit/90948e6f6e1f3624a9c30595b3ff3a46ffbbced0) Thanks [@toiroakr](https://github.com/toiroakr)! - Add a targeted hint to the `Remote schema drift detected` error when every reported drift is a missing script hash — the pattern left by an environment last deployed with the pre-v2 CLI, which never wrote script hashes. The hint points at `migration sync <N>`, which is already listed as one of the general resolution options. The v2 migration guide (`docs/migration/v2.md`) now also documents that the first `tailor deploy` against such an environment needs a `migration sync` first.
16
+
17
+ ## 0.6.0
18
+
19
+ ### Minor Changes
20
+
21
+ - [#2075](https://github.com/tailor-platform/sdk/pull/2075) [`bd0e397`](https://github.com/tailor-platform/sdk/commit/bd0e39720015248e6ebb2c31efca49f9238b7060) Thanks [@dqn](https://github.com/dqn)! - `tailor function test-run` is renamed to `tailor function run`. The old name keeps working as a deprecated alias until v3 and prints a deprecation warning when used; `tailor upgrade` offers the `v3/function-test-run-rename` codemod to rewrite `function test-run` invocations across package.json scripts, shell and Windows scripts, YAML, Markdown, and JavaScript/TypeScript sources.
22
+
23
+ ### Patch Changes
24
+
25
+ - [#2031](https://github.com/tailor-platform/sdk/pull/2031) [`ecc18ee`](https://github.com/tailor-platform/sdk/commit/ecc18eebc4e5c03c78e362a9b17bacc31bc88a31) Thanks [@renovate](https://github.com/apps/renovate)! - fix(deps): update dependency @ast-grep/napi to v0.45.1
26
+
27
+ - [#2065](https://github.com/tailor-platform/sdk/pull/2065) [`5a3a0e1`](https://github.com/tailor-platform/sdk/commit/5a3a0e1ce8bfadb769dc4540ef257e944f0c077e) Thanks [@toiroakr](https://github.com/toiroakr)! - Raise the minimum supported Node.js version to 22.18.0 (from 22.15.0).
28
+
29
+ `tailor seed validate` crashed on Node 22.15.0–22.17.x with `Expected a string, an ArrayBuffer, or a TypedArray to be returned for the "source" from the "load" hook but got null`. This is a Node.js bug ([nodejs/node#58607](https://github.com/nodejs/node/issues/58607)): requiring a `node:`-scheme-only builtin (`node:sqlite`, used internally by the seed validator) while both a synchronous `resolve` and `load` hook are registered via `module.registerHooks()` crashes the loader on those versions. The SDK always registers both hooks, so any project on Node 22.15.0–22.17.x hit this. Node fixed it upstream in 22.18.0 ([nodejs/node#58612](https://github.com/nodejs/node/pull/58612)); this release raises `engines.node` to match, since Node 22.15.0–22.17.x never actually supported `tailor seed validate`.
30
+
3
31
  ## 0.5.0
4
32
 
5
33
  ### Minor Changes
@@ -197,7 +197,8 @@ function rewriteImportTypeReferences(root, edits, editedRanges) {
197
197
  function transform(source, _filePath) {
198
198
  if (!quickFilter(source)) return null;
199
199
  const filePath = _filePath?.toLowerCase();
200
- const root = parse(filePath?.endsWith(".tsx") || filePath?.endsWith(".jsx") ? Lang.Tsx : filePath ? Lang.TypeScript : source.includes("</") || source.includes("/>") ? Lang.Tsx : Lang.TypeScript, source).root();
200
+ const lang = filePath?.endsWith(".tsx") || filePath?.endsWith(".jsx") ? Lang.Tsx : filePath ? Lang.TypeScript : source.includes("</") || source.includes("/>") ? Lang.Tsx : Lang.TypeScript;
201
+ const root = parse(lang, source).root();
201
202
  const edits = [];
202
203
  const editedRanges = /* @__PURE__ */ new Set();
203
204
  const { localTypeRenames, namespaceNames } = collectSdkImports(root, edits, editedRanges);
@@ -224,7 +224,8 @@ function renameQuotedKey(node) {
224
224
  function transformAuthInvoker(source, _filePath, options = {}) {
225
225
  if (!quickFilter(source)) return null;
226
226
  const renameOptionKeys = options.renameOptionKeys ?? true;
227
- const root = parse(source.includes("</") || source.includes("/>") ? Lang.Tsx : Lang.TypeScript, source).root();
227
+ const lang = source.includes("</") || source.includes("/>") ? Lang.Tsx : Lang.TypeScript;
228
+ const root = parse(lang, source).root();
228
229
  const calls = findInvokerCalls(root).filter((c) => isSupportedInvokerValueCall(c.callNode));
229
230
  const edits = calls.map((c) => c.callNode.replace(c.argText));
230
231
  if (renameOptionKeys) {
@@ -44,7 +44,8 @@ function isExecuteScriptArg(stringifyCall) {
44
44
  */
45
45
  function transform(source, _filePath) {
46
46
  if (!quickFilter(source)) return null;
47
- const root = parse(source.includes("</") || source.includes("/>") ? Lang.Tsx : Lang.TypeScript, source).root();
47
+ const lang = source.includes("</") || source.includes("/>") ? Lang.Tsx : Lang.TypeScript;
48
+ const root = parse(lang, source).root();
48
49
  const edits = [];
49
50
  for (const match of root.findAll({ rule: { pattern: "JSON.stringify($X)" } })) {
50
51
  if (!isExecuteScriptArg(match)) continue;
@@ -43,7 +43,8 @@ function hasNonNamedBinding(importStmt) {
43
43
  */
44
44
  function transform(source, _filePath) {
45
45
  if (!source.includes(CLI_MODULE)) return null;
46
- const root = parse(source.includes("</") || source.includes("/>") ? Lang.Tsx : Lang.TypeScript, source).root();
46
+ const lang = source.includes("</") || source.includes("/>") ? Lang.Tsx : Lang.TypeScript;
47
+ const root = parse(lang, source).root();
47
48
  const edits = [];
48
49
  const importStmts = root.findAll({ rule: {
49
50
  kind: "import_statement",
@@ -699,18 +699,19 @@ function transformResolverBody(arrowNode, edits, parseContext) {
699
699
  const principalAliasBindings = [];
700
700
  for (const child of pattern.children()) {
701
701
  const kind = child.kind();
702
- if (kind === "shorthand_property_identifier_pattern" && child.text() === "user") if (aliasRenamedUser) {
703
- edits.push(child.replace("caller: user"));
704
- aliasedShorthandUser = true;
705
- principalAliasBindings.push({
706
- name: "user",
707
- bindingStart: child.range().start.index
708
- });
709
- } else {
710
- edits.push(child.replace("caller"));
711
- renamedShorthandUser = true;
712
- }
713
- else if (kind === "pair_pattern") {
702
+ if (kind === "shorthand_property_identifier_pattern" && child.text() === "user") {
703
+ if (aliasRenamedUser) {
704
+ edits.push(child.replace("caller: user"));
705
+ aliasedShorthandUser = true;
706
+ principalAliasBindings.push({
707
+ name: "user",
708
+ bindingStart: child.range().start.index
709
+ });
710
+ } else {
711
+ edits.push(child.replace("caller"));
712
+ renamedShorthandUser = true;
713
+ }
714
+ } else if (kind === "pair_pattern") {
714
715
  const key = child.field("key");
715
716
  if (key && key.text() === "user") {
716
717
  edits.push(key.replace("caller"));
@@ -722,16 +723,18 @@ function transformResolverBody(arrowNode, edits, parseContext) {
722
723
  }
723
724
  } else if (kind === "object_assignment_pattern") {
724
725
  const inner = child.children().find((c) => c.kind() === "shorthand_property_identifier_pattern");
725
- if (inner && inner.text() === "user") if (aliasRenamedUser) {
726
- edits.push(inner.replace("caller: user"));
727
- aliasedShorthandUser = true;
728
- principalAliasBindings.push({
729
- name: "user",
730
- bindingStart: inner.range().start.index
731
- });
732
- } else {
733
- edits.push(inner.replace("caller"));
734
- renamedShorthandUser = true;
726
+ if (inner && inner.text() === "user") {
727
+ if (aliasRenamedUser) {
728
+ edits.push(inner.replace("caller: user"));
729
+ aliasedShorthandUser = true;
730
+ principalAliasBindings.push({
731
+ name: "user",
732
+ bindingStart: inner.range().start.index
733
+ });
734
+ } else {
735
+ edits.push(inner.replace("caller"));
736
+ renamedShorthandUser = true;
737
+ }
735
738
  }
736
739
  }
737
740
  }
@@ -1061,33 +1064,36 @@ function transformPrincipalCallbackParam(fn, edits, typeContext) {
1061
1064
  let renamedShorthandUser = false;
1062
1065
  for (const child of pattern.children()) {
1063
1066
  const kind = child.kind();
1064
- if (kind === "shorthand_property_identifier_pattern" && child.text() === "user") if (aliasRenamedUser) {
1065
- edits.push(child.replace("invoker: user"));
1066
- aliasedShorthandUser = true;
1067
- principalAliasBindings.push({
1068
- name: "user",
1069
- bindingStart: child.range().start.index
1070
- });
1071
- } else {
1072
- edits.push(child.replace("invoker"));
1073
- renamedShorthandUser = true;
1074
- }
1075
- else if (kind === "pair_pattern") {
1076
- const key = child.field("key");
1077
- if (key?.text() === "user") edits.push(key.replace("invoker"));
1078
- } else if (kind === "object_assignment_pattern") {
1079
- const inner = child.children().find((c) => c.kind() === "shorthand_property_identifier_pattern");
1080
- if (inner?.text() === "user") if (aliasRenamedUser) {
1081
- edits.push(inner.replace("invoker: user"));
1067
+ if (kind === "shorthand_property_identifier_pattern" && child.text() === "user") {
1068
+ if (aliasRenamedUser) {
1069
+ edits.push(child.replace("invoker: user"));
1082
1070
  aliasedShorthandUser = true;
1083
1071
  principalAliasBindings.push({
1084
1072
  name: "user",
1085
- bindingStart: inner.range().start.index
1073
+ bindingStart: child.range().start.index
1086
1074
  });
1087
1075
  } else {
1088
- edits.push(inner.replace("invoker"));
1076
+ edits.push(child.replace("invoker"));
1089
1077
  renamedShorthandUser = true;
1090
1078
  }
1079
+ } else if (kind === "pair_pattern") {
1080
+ const key = child.field("key");
1081
+ if (key?.text() === "user") edits.push(key.replace("invoker"));
1082
+ } else if (kind === "object_assignment_pattern") {
1083
+ const inner = child.children().find((c) => c.kind() === "shorthand_property_identifier_pattern");
1084
+ if (inner?.text() === "user") {
1085
+ if (aliasRenamedUser) {
1086
+ edits.push(inner.replace("invoker: user"));
1087
+ aliasedShorthandUser = true;
1088
+ principalAliasBindings.push({
1089
+ name: "user",
1090
+ bindingStart: inner.range().start.index
1091
+ });
1092
+ } else {
1093
+ edits.push(inner.replace("invoker"));
1094
+ renamedShorthandUser = true;
1095
+ }
1096
+ }
1091
1097
  }
1092
1098
  }
1093
1099
  if (!renamedShorthandUser) {
@@ -24,7 +24,8 @@ function isInsideImportStatement(node) {
24
24
  function transform(source, _filePath) {
25
25
  if (!Object.keys(RENAMES).some((name) => source.includes(name))) return null;
26
26
  if (!source.includes(SDK_MODULE)) return null;
27
- const root = parse(source.includes("</") || source.includes("/>") ? Lang.Tsx : Lang.TypeScript, source).root();
27
+ const lang = source.includes("</") || source.includes("/>") ? Lang.Tsx : Lang.TypeScript;
28
+ const root = parse(lang, source).root();
28
29
  const edits = [];
29
30
  const needsBodyRename = /* @__PURE__ */ new Set();
30
31
  const importStmts = root.findAll({ rule: {
@@ -0,0 +1,48 @@
1
+ import * as path from "pathe";
2
+ //#region codemods/v3/function-test-run-rename/scripts/transform.ts
3
+ const TOKEN_SEPARATOR = "(?:[ \\t]|\\\\\\r?\\n)+";
4
+ const COMMAND_PATTERN = new RegExp(`(?<![\\w.-])(tailor(?:-sdk)?(?:\\.(?:cmd|ps1|exe))?${TOKEN_SEPARATOR}function${TOKEN_SEPARATOR})test-run(?![\\w.-])`, "g");
5
+ function replaceCommand(value) {
6
+ return value.replace(COMMAND_PATTERN, "$1run");
7
+ }
8
+ function transformText(source) {
9
+ const updated = replaceCommand(source);
10
+ return updated === source ? null : updated;
11
+ }
12
+ function transformPackageJson(source) {
13
+ let parsed;
14
+ try {
15
+ parsed = JSON.parse(source);
16
+ } catch {
17
+ return null;
18
+ }
19
+ let modified = false;
20
+ const scripts = parsed.scripts;
21
+ if (typeof scripts === "object" && scripts != null && !Array.isArray(scripts)) for (const [name, value] of Object.entries(scripts)) {
22
+ if (typeof value !== "string") continue;
23
+ const updated = replaceCommand(value);
24
+ if (updated !== value) {
25
+ scripts[name] = updated;
26
+ modified = true;
27
+ }
28
+ }
29
+ if (!modified) return null;
30
+ const trailing = source.endsWith("\n") ? "\n" : "";
31
+ return JSON.stringify(parsed, null, 2) + trailing;
32
+ }
33
+ /**
34
+ * Rename `tailor function test-run` invocations to `tailor function run`.
35
+ *
36
+ * `test-run` remains as a deprecated alias until v3, where only `run` is
37
+ * recognized.
38
+ * @param source - File contents
39
+ * @param filePath - Absolute path to the file (used to dispatch package.json vs text)
40
+ * @returns Transformed source or null when nothing matched.
41
+ */
42
+ function transform(source, filePath) {
43
+ if (!source.includes("test-run")) return null;
44
+ if (path.extname(filePath).toLowerCase() === ".json") return transformPackageJson(source);
45
+ return transformText(source);
46
+ }
47
+ //#endregion
48
+ export { transform as default };
@@ -0,0 +1,76 @@
1
+ import * as path from "pathe";
2
+ //#region codemods/v3/setup-branch-flag-rename/scripts/transform.ts
3
+ const TOKEN_SEPARATOR = "(?:[ \\t]|\\\\\\r?\\n)+";
4
+ const COMMAND_START = new RegExp(`(?<![\\w.-])tailor(?:-sdk)?(?:\\.(?:cmd|ps1|exe))?${TOKEN_SEPARATOR}setup${TOKEN_SEPARATOR}branch(?![\\w.-])`, "g");
5
+ const NEXT_TOKEN = new RegExp(`${TOKEN_SEPARATOR}("[^"\\n]*"|'[^'\\n]*'|[^\\s;&|]+)`, "y");
6
+ function replaceCommand(value) {
7
+ let result = "";
8
+ let cursor = 0;
9
+ COMMAND_START.lastIndex = 0;
10
+ for (let match = COMMAND_START.exec(value); match; match = COMMAND_START.exec(value)) {
11
+ let pos = COMMAND_START.lastIndex;
12
+ result += value.slice(cursor, pos);
13
+ NEXT_TOKEN.lastIndex = pos;
14
+ for (let token = NEXT_TOKEN.exec(value); token; token = NEXT_TOKEN.exec(value)) {
15
+ const chunk = token[0];
16
+ const word = token[1];
17
+ if (word.startsWith("#")) break;
18
+ const quoted = /^(['"])(--branch(?:=.*)?)\1$/.exec(word);
19
+ const quote = quoted ? quoted[1] : "";
20
+ const bare = quoted ? quoted[2] : word;
21
+ if (bare === "--branch" || bare.startsWith("--branch=")) {
22
+ const separator = chunk.slice(0, chunk.length - word.length);
23
+ result += `${separator}${quote}--target${bare.slice(8)}${quote}`;
24
+ } else result += chunk;
25
+ pos = NEXT_TOKEN.lastIndex;
26
+ if (/\$\(|`|<\(|>\(/.test(word)) break;
27
+ }
28
+ cursor = pos;
29
+ COMMAND_START.lastIndex = pos;
30
+ }
31
+ result += value.slice(cursor);
32
+ return result;
33
+ }
34
+ function transformText(source) {
35
+ const updated = replaceCommand(source);
36
+ return updated === source ? null : updated;
37
+ }
38
+ function transformPackageJson(source) {
39
+ let parsed;
40
+ try {
41
+ parsed = JSON.parse(source);
42
+ } catch {
43
+ return null;
44
+ }
45
+ let modified = false;
46
+ const scripts = parsed.scripts;
47
+ if (typeof scripts === "object" && scripts != null && !Array.isArray(scripts)) for (const [name, value] of Object.entries(scripts)) {
48
+ if (typeof value !== "string") continue;
49
+ const updated = replaceCommand(value);
50
+ if (updated !== value) {
51
+ scripts[name] = updated;
52
+ modified = true;
53
+ }
54
+ }
55
+ if (!modified) return null;
56
+ const trailing = source.endsWith("\n") ? "\n" : "";
57
+ return JSON.stringify(parsed, null, 2) + trailing;
58
+ }
59
+ /**
60
+ * Rename the `--branch` option of `tailor setup branch` invocations to
61
+ * `--target`.
62
+ *
63
+ * `--branch` remains as a deprecated alias until v3, where only
64
+ * `--target` is recognized. `setup tag`, `setup preview`, and
65
+ * `setup coordinate` keep their own `--branch` option unchanged.
66
+ * @param source - File contents
67
+ * @param filePath - Absolute path to the file (used to dispatch package.json vs text)
68
+ * @returns Transformed source or null when nothing matched.
69
+ */
70
+ function transform(source, filePath) {
71
+ if (!source.includes("--branch")) return null;
72
+ if (path.extname(filePath).toLowerCase() === ".json") return transformPackageJson(source);
73
+ return transformText(source);
74
+ }
75
+ //#endregion
76
+ export { transform as default };
package/dist/index.js CHANGED
@@ -123,6 +123,7 @@ const V2_NEXT_6 = "2.0.0-next.6";
123
123
  const V2_NEXT_7 = "2.0.0-next.7";
124
124
  const V2_NEXT_9 = "2.0.0-next.9";
125
125
  const V2_NEXT_11 = "2.0.0-next.11";
126
+ const SETUP_BRANCH_RESIDUAL_FLAG = /(?:(?<![\w.-])tailor(?:-sdk)?(?:\.(?:cmd|ps1|exe))?|@tailor-platform\/sdk(?:@\S{1,32})?)[\s\\^`]{1,16}setup[\s\\^`]{1,16}branch\b(?:'[^'\n]*'|"[^"\n]*"|[^\n;&|'"#]|[\\^`]\r?\n)*(?:[ \t]|[\\^`]\r?\n)--branch(?![\w-])/;
126
127
  /** All registered codemods, in registration order. */
127
128
  const allCodemods = [
128
129
  {
@@ -816,7 +817,11 @@ const allCodemods = [
816
817
  " them to StartWorkflowOptions / ExecJobFunctionOptions.",
817
818
  "- An invoker option passed via a variable or spread (not a literal object) —",
818
819
  " the codemod only inspects literal object arguments; rename the invoker key",
819
- " to authInvoker in the options object's own definition."
820
+ " to authInvoker in the options object's own definition.",
821
+ "- A renamed triggerJobFunction call whose target is another workflow job —",
822
+ " rewrite it further to that job's own .start() method (e.g. worker.start(args)).",
823
+ " Calling execJobFunction directly is not detected as a build-time dependency",
824
+ " and fails the build."
820
825
  ].join("\n")
821
826
  },
822
827
  {
@@ -849,7 +854,45 @@ const allCodemods = [
849
854
  "- mockWorkflow().startJobFunction in tests — assert on the execJobFunction vi.fn",
850
855
  " instead; the alias was the same mock function.",
851
856
  "- A file that already imports ExecJobFunctionOptions alongside the removed type —",
852
- " rename the remaining references by hand and drop the duplicate specifier."
857
+ " rename the remaining references by hand and drop the duplicate specifier.",
858
+ "- A renamed call whose target is another workflow job — rewrite it further to",
859
+ " that job's own .start() method (e.g. worker.start(args)). Calling",
860
+ " execJobFunction directly is not detected as a build-time dependency and fails",
861
+ " the build."
862
+ ].join("\n")
863
+ },
864
+ {
865
+ id: "v3/remove-workflow-exec-job-function",
866
+ name: "workflow.execJobFunction (imported) removed — use job.start()",
867
+ description: "`execJobFunction` on the `workflow` value imported from @tailor-platform/sdk/runtime(/workflow) is removed in v3. Calling it directly from a workflow job body to reach another job is not detected as a build-time dependency and has no working use; the target job's own `.start()` method is the only supported way to call it. This does not affect the ambient `tailor.workflow.execJobFunction` global — that's what `.start()` itself compiles down to at build time, and it remains fully supported.",
868
+ since: "1.0.0",
869
+ until: "3.0.0",
870
+ filePatterns: ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"],
871
+ suspiciousPatterns: ["execJobFunction"],
872
+ examples: [{
873
+ before: "import { workflow } from \"@tailor-platform/sdk/runtime\";\n\nawait workflow.execJobFunction(\"worker\", { id: 1 });",
874
+ after: "import { worker } from \"./jobs/worker\";\n\nawait worker.start({ id: 1 });"
875
+ }],
876
+ prompt: [
877
+ "workflow.execJobFunction — the value imported from",
878
+ "@tailor-platform/sdk/runtime or @tailor-platform/sdk/runtime/workflow — is",
879
+ "removed in v3. Replace each call with the target job's own .start() method:",
880
+ "import the WorkflowJob the call names and call <job>.start(args, options)",
881
+ "instead of workflow.execJobFunction(\"<job-name>\", args, options).",
882
+ "",
883
+ "This only removes the re-export from @tailor-platform/sdk/runtime. It does",
884
+ "not affect the ambient tailor.workflow.execJobFunction global, which stays",
885
+ "fully supported and is what .start() itself compiles down to at build time.",
886
+ "This codemod does not rewrite ambient tailor.workflow.execJobFunction(...)",
887
+ "call sites, since removing the import re-export does not affect them — but",
888
+ "if such a call site sits inside workflow job source and calls another job",
889
+ "by name, a separate build-time check already rejects it; migrate that call",
890
+ "to the target job's own .start() method too.",
891
+ "",
892
+ "If the job name passed to execJobFunction is not a string literal (a truly",
893
+ "dynamic dispatch), there is currently no supported replacement — .start()",
894
+ "only targets a statically known job. Flag this case for a human instead of",
895
+ "guessing a rewrite."
853
896
  ].join("\n")
854
897
  },
855
898
  {
@@ -1303,7 +1346,7 @@ const allCodemods = [
1303
1346
  {
1304
1347
  id: "v2/seed-exec-to-cli-plugin",
1305
1348
  name: "Generated seed exec.mjs → tailor seed CLI plugin",
1306
- description: "`seedPlugin` no longer generates the `exec.mjs` seed runner. Seeding and validation move to the `tailor seed` commands provided by the `@tailor-platform/sdk-plugin-seed` CLI plugin: install it as a devDependency, replace `node <distPath>/exec.mjs` invocations with `tailor seed apply` and `node <distPath>/exec.mjs validate` with `tailor seed validate`, and delete the stale generated `<distPath>/exec.mjs` file. Seed data and schema generation (`data/*.jsonl`, `data/*.schema.ts`) is unchanged, and the `tailor seed apply` options mirror the old script (`--machine-user`, `--namespace`, `--skip-idp`, `--truncate`, `--yes`, type-name arguments), plus a new `--upsert` flag to update existing rows instead of failing on duplicate ids.",
1349
+ description: "`seedPlugin` no longer generates the `exec.mjs` seed runner. Seeding and validation move to the `tailor seed` commands provided by the `@tailor-platform/sdk-plugin-seed` CLI plugin: install it as a devDependency, replace `node <distPath>/exec.mjs` invocations with `tailor seed apply` and `node <distPath>/exec.mjs validate` with `tailor seed validate`, and delete the stale generated `<distPath>/exec.mjs` file. Seed data and schema generation (`data/*.jsonl`, `data/*.schema.ts`) is unchanged, and the `tailor seed apply` options mirror the old script (`--machine-user`, `--namespace`, `--skip-idp`, `--truncate`, `--yes`, entity-name arguments), plus a new `--upsert` flag to update existing rows instead of failing on duplicate ids.",
1307
1350
  since: "1.0.0",
1308
1351
  until: "2.0.0",
1309
1352
  prereleaseUntil: V2_NEXT_9,
@@ -1328,8 +1371,8 @@ const allCodemods = [
1328
1371
  "- Install @tailor-platform/sdk-plugin-seed as a devDependency next to",
1329
1372
  " @tailor-platform/sdk.",
1330
1373
  "- Replace `node <distPath>/exec.mjs [options] [types...]` invocations with",
1331
- " `tailor seed apply [options] [types...]` (same options: --machine-user/-m,",
1332
- " --namespace/-n, --skip-idp, --truncate, --yes, and type-name arguments,",
1374
+ " `tailor seed apply [options] [entities...]` (same options: --machine-user/-m,",
1375
+ " --namespace/-n, --skip-idp, --truncate, --yes, and entity-name arguments,",
1333
1376
  " plus a new --upsert flag to update existing rows instead of failing on",
1334
1377
  " duplicate ids).",
1335
1378
  "- Replace `node <distPath>/exec.mjs validate [path]` with",
@@ -1450,7 +1493,7 @@ const allCodemods = [
1450
1493
  {
1451
1494
  id: "v2/node-minimum-22-15-0",
1452
1495
  name: "Node.js minimum version raised to 22.15.0",
1453
- description: "v2 requires Node.js **22.15.0** or later. This is the first version that includes `module.registerHooks()`, which the SDK uses to register its TypeScript loader hook synchronously in the main thread. No source change is required; ensure your environment runs Node.js 22.15.0+.",
1496
+ description: "v2 requires Node.js **22.15.0** or later. This is the first version that includes `module.registerHooks()`, which the SDK uses to register its TypeScript loader hook synchronously in the main thread. The actual floor is now **22.18.0**: Node 22.15.0–22.17.x has a bug that crashes `tailor seed validate` when requiring `node:`-scheme-only builtins such as `node:sqlite`, fixed upstream by [nodejs/node#58612](https://github.com/nodejs/node/pull/58612) in 22.18.0. No source change is required; ensure your environment runs Node.js 22.18.0+.",
1454
1497
  since: "1.0.0",
1455
1498
  until: "2.0.0",
1456
1499
  notice: true
@@ -1463,6 +1506,14 @@ const allCodemods = [
1463
1506
  until: "2.0.0",
1464
1507
  notice: true
1465
1508
  },
1509
+ {
1510
+ id: "v2/tailordb-script-hash-migration-sync",
1511
+ name: "First v2 deploy to a v1-deployed environment needs migration sync",
1512
+ description: "The pre-v2 CLI never wrote a script hash into deployed schemas, so the first `tailor deploy` against an environment last deployed with it reports `Remote schema drift detected` with every scripted type showing `has no script hash on remote`. Run `tailor tailordb migration sync <current migration number>` once for that environment to write the missing hashes, then `tailor deploy` as usual. Preview/PR workspaces don't hit this, since they're built with v2 from the start. No source change is required.",
1513
+ since: "1.0.0",
1514
+ until: "2.0.0",
1515
+ notice: true
1516
+ },
1466
1517
  {
1467
1518
  id: "v2/dts-env-value-types",
1468
1519
  name: "tailor.d.ts Env uses value types instead of literal values",
@@ -1496,6 +1547,65 @@ const allCodemods = [
1496
1547
  "editing `tailor.d.ts`: it is generated and will be overwritten, and embedding",
1497
1548
  "env values there is what leaked configured secrets into version control."
1498
1549
  ].join("\n")
1550
+ },
1551
+ {
1552
+ id: "v3/function-test-run-rename",
1553
+ name: "function test-run → function run",
1554
+ description: "Rename `tailor function test-run` invocations to `tailor function run`. `test-run` remains as a deprecated alias until it is removed in v3.",
1555
+ since: "1.22.0",
1556
+ until: "3.0.0",
1557
+ scriptPath: "v3/function-test-run-rename/scripts/transform.js",
1558
+ filePatterns: [
1559
+ "**/package.json",
1560
+ "**/*.{sh,bash,zsh,ps1,cmd,bat,yml,yaml}",
1561
+ "**/*.md",
1562
+ "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"
1563
+ ],
1564
+ legacyPatterns: [/\bfunction[\s\\]{1,16}test-run(?![\w.-])/],
1565
+ sourceStringLegacyPatterns: [/\bfunction[\s\\]{1,16}test-run(?![\w.-])/],
1566
+ examples: [{
1567
+ lang: "sh",
1568
+ before: "tailor function test-run resolvers/add.ts --arg '{\"a\":1,\"b\":2}'",
1569
+ after: "tailor function run resolvers/add.ts --arg '{\"a\":1,\"b\":2}'"
1570
+ }],
1571
+ prompt: [
1572
+ "The `tailor function test-run` subcommand is renamed to `tailor function run`;",
1573
+ "the old name is removed in v3. Replace any remaining `function test-run`",
1574
+ "invocations the codemod did not rewrite (e.g. wrapped across lines or invoked",
1575
+ "through a package runner such as `npx @tailor-platform/sdk`) with",
1576
+ "`function run`. Leave prose that merely mentions the old subcommand name",
1577
+ "unchanged unless it documents a command to type."
1578
+ ].join("\n")
1579
+ },
1580
+ {
1581
+ id: "v3/setup-branch-flag-rename",
1582
+ name: "setup branch --branch → --target",
1583
+ description: "Rename the `--branch` option of `tailor setup branch` invocations to `--target`. `--branch` remains as a deprecated alias until it is removed in v3. The `--branch` option of `setup tag`, `setup preview`, and `setup coordinate` is unchanged.",
1584
+ since: "1.72.0",
1585
+ until: "3.0.0",
1586
+ scriptPath: "v3/setup-branch-flag-rename/scripts/transform.js",
1587
+ filePatterns: [
1588
+ "**/package.json",
1589
+ "**/*.{sh,bash,zsh,ps1,cmd,bat,yml,yaml}",
1590
+ "**/*.md",
1591
+ "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"
1592
+ ],
1593
+ legacyPatterns: [SETUP_BRANCH_RESIDUAL_FLAG],
1594
+ sourceStringLegacyPatterns: [SETUP_BRANCH_RESIDUAL_FLAG],
1595
+ examples: [{
1596
+ lang: "sh",
1597
+ before: "tailor setup branch --name my-app-stg --branch main",
1598
+ after: "tailor setup branch --name my-app-stg --target main"
1599
+ }],
1600
+ prompt: [
1601
+ "The `--branch` option of `tailor setup branch` is renamed to `--target`;",
1602
+ "the old spelling is removed in v3. Replace any remaining `--branch` options of",
1603
+ "`setup branch` invocations the codemod did not rewrite (e.g. wrapped across",
1604
+ "lines or invoked through a package runner such as `npx @tailor-platform/sdk`)",
1605
+ "with `--target`. Do not touch the `--branch` option of `setup tag`,",
1606
+ "`setup preview`, or `setup coordinate`, which keeps its name, and leave prose",
1607
+ "that merely mentions the option unchanged unless it documents a command to type."
1608
+ ].join("\n")
1499
1609
  }
1500
1610
  ];
1501
1611
  /**
@@ -1996,6 +2106,27 @@ function legacyPatternWarnings(relative, content, sourceStringContent, sourceTex
1996
2106
  return [`${relative}: contains ${Array.from(found).join(", ")} but was not migrated automatically (rule: ${lt.id}). Manual migration may be needed.`];
1997
2107
  });
1998
2108
  }
2109
+ function lineRemapper(before, after) {
2110
+ const hunks = structuredPatch("", "", before, after, "", "", { context: 0 }).hunks;
2111
+ const lastLine = Math.max(1, after.split("\n").length - (after.endsWith("\n") ? 1 : 0));
2112
+ const map = (line) => {
2113
+ let offset = 0;
2114
+ for (const hunk of hunks) {
2115
+ if (line < hunk.oldStart) break;
2116
+ if (hunk.oldLines === 0) {
2117
+ offset += hunk.newLines;
2118
+ continue;
2119
+ }
2120
+ if (line < hunk.oldStart + hunk.oldLines) {
2121
+ if (hunk.newLines === 0) return hunk.newStart;
2122
+ return Math.min(hunk.newStart + (line - hunk.oldStart), hunk.newStart + hunk.newLines - 1);
2123
+ }
2124
+ offset += hunk.newLines - hunk.oldLines;
2125
+ }
2126
+ return line + offset;
2127
+ };
2128
+ return (line) => Math.min(lastLine, Math.max(1, map(line)));
2129
+ }
1999
2130
  function compareReviewFindings(a, b) {
2000
2131
  return a.file.localeCompare(b.file) || a.line - b.line || a.message.localeCompare(b.message) || a.excerpt.localeCompare(b.excerpt);
2001
2132
  }
@@ -2003,6 +2134,9 @@ function compareReviewFindings(a, b) {
2003
2134
  * Run multiple codemods on a project directory using in-memory chaining.
2004
2135
  * Each file is processed through all transforms whose filePatterns match it.
2005
2136
  * Later transforms see earlier transforms' output — even in dry-run mode.
2137
+ * Review detection (reviewFindings and suspicious patterns) runs against each
2138
+ * codemod's own position in the transform chain, so a later codemod's rewrite
2139
+ * cannot hide an earlier codemod's findings.
2006
2140
  *
2007
2141
  * In dry-run mode, colorized diffs are printed to stderr.
2008
2142
  * @param codemods - Codemod packages to run (with resolved script paths)
@@ -2048,25 +2182,41 @@ async function runCodemods(codemods, targetPath, dryRun) {
2048
2182
  continue;
2049
2183
  }
2050
2184
  let current = original;
2185
+ const reviewTargets = [];
2051
2186
  for (const lt of matchedTransforms) {
2052
- if (!lt.transform) continue;
2053
- const result = await lt.transform(current, absolute);
2054
- if (result != null) {
2055
- current = result;
2056
- appliedCodemodIds.add(lt.id);
2187
+ if (lt.transform) {
2188
+ const result = await lt.transform(current, absolute);
2189
+ if (result != null) {
2190
+ current = result;
2191
+ appliedCodemodIds.add(lt.id);
2192
+ }
2057
2193
  }
2194
+ if (lt.prompt) reviewTargets.push({
2195
+ lt,
2196
+ snapshot: current
2197
+ });
2058
2198
  }
2059
2199
  if (current !== original) {
2060
2200
  filesModified.push(absolute);
2061
2201
  if (dryRun) printDiff(absolute, original, current);
2062
2202
  else await fs.promises.writeFile(absolute, current, "utf-8");
2063
2203
  }
2064
- const residualContent = contentForResidualMatching(relative, current);
2065
- const sourceStringContent = sourceStringContentForResidualMatching(relative, current);
2204
+ const residualBySnapshot = /* @__PURE__ */ new Map();
2205
+ const residualFor = (content) => {
2206
+ let entry = residualBySnapshot.get(content);
2207
+ if (!entry) {
2208
+ entry = {
2209
+ residual: contentForResidualMatching(relative, content),
2210
+ sourceString: sourceStringContentForResidualMatching(relative, content)
2211
+ };
2212
+ residualBySnapshot.set(content, entry);
2213
+ }
2214
+ return entry;
2215
+ };
2216
+ const { residual: residualContent, sourceString: sourceStringContent } = residualFor(current);
2066
2217
  const sourceTextContent = sourceTextContentForResidualMatching(relative, current);
2067
2218
  warnings.push(...legacyPatternWarnings(relative, residualContent, sourceStringContent, sourceTextContent, matchedTransforms));
2068
- for (const lt of matchedTransforms) {
2069
- if (!lt.prompt) continue;
2219
+ for (const { lt, snapshot } of reviewTargets) {
2070
2220
  const filesForReview = () => {
2071
2221
  let files = suspiciousByCodemod.get(lt.id);
2072
2222
  if (!files) {
@@ -2076,7 +2226,14 @@ async function runCodemods(codemods, targetPath, dryRun) {
2076
2226
  return files;
2077
2227
  };
2078
2228
  if (lt.reviewFindings) {
2079
- const findings = await lt.reviewFindings(current, absolute, relative);
2229
+ let findings = await lt.reviewFindings(snapshot, absolute, relative);
2230
+ if (snapshot !== current && findings.length > 0) {
2231
+ const remap = lineRemapper(snapshot, current);
2232
+ findings = findings.map((finding) => ({
2233
+ ...finding,
2234
+ line: remap(finding.line)
2235
+ }));
2236
+ }
2080
2237
  if (findings.length > 0) {
2081
2238
  const files = filesForReview();
2082
2239
  for (const finding of findings) files.add(finding.file);
@@ -2088,7 +2245,9 @@ async function runCodemods(codemods, targetPath, dryRun) {
2088
2245
  existing.push(...findings);
2089
2246
  }
2090
2247
  }
2091
- if (lt.suspiciousPatterns.some((p) => matchResidualPattern(residualContent, p) !== null) || sourceStringContent != null && lt.sourceStringSuspiciousPatterns.some((p) => matchResidualPattern(sourceStringContent, p) !== null)) filesForReview().add(relative);
2248
+ if (lt.suspiciousPatterns.length === 0 && lt.sourceStringSuspiciousPatterns.length === 0) continue;
2249
+ const { residual, sourceString } = residualFor(snapshot);
2250
+ if (lt.suspiciousPatterns.some((p) => matchResidualPattern(residual, p) !== null) || sourceString != null && lt.sourceStringSuspiciousPatterns.some((p) => matchResidualPattern(sourceString, p) !== null)) filesForReview().add(relative);
2092
2251
  }
2093
2252
  }
2094
2253
  const llmReviews = [];
@@ -2219,7 +2378,7 @@ function printLlmReview(review) {
2219
2378
  }
2220
2379
  process.stderr.write(`\nPrompt for an LLM:\n${review.prompt.trim()}\n`);
2221
2380
  }
2222
- runMain(defineCommand({
2381
+ const main = defineCommand({
2223
2382
  name: packageName,
2224
2383
  description: packageJson.description ?? "Codemod runner for Tailor Platform SDK upgrades",
2225
2384
  subCommands: { list: listCommand },
@@ -2301,6 +2460,7 @@ human-readable form, so \`stdout\` stays pure JSON for piping.`,
2301
2460
  process.stdout.write(JSON.stringify(output) + "\n");
2302
2461
  if (output.errors.length > 0) process.exit(1);
2303
2462
  }
2304
- }), { version: packageVersion });
2463
+ });
2464
+ runMain(main, { version: packageVersion });
2305
2465
  //#endregion
2306
2466
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tailor-platform/sdk-codemod",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Codemod runner for Tailor Platform SDK upgrades",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -16,7 +16,7 @@
16
16
  ],
17
17
  "type": "module",
18
18
  "dependencies": {
19
- "@ast-grep/napi": "0.45.0",
19
+ "@ast-grep/napi": "0.45.1",
20
20
  "diff": "9.0.0",
21
21
  "pathe": "2.0.3",
22
22
  "picomatch": "4.0.5",
@@ -29,8 +29,9 @@
29
29
  "@types/node": "24.13.3",
30
30
  "@types/picomatch": "4.0.3",
31
31
  "@types/semver": "7.8.0",
32
- "eslint-plugin-zod": "4.9.0",
33
- "oxlint": "1.76.0",
32
+ "eslint-plugin-zod": "4.9.1",
33
+ "oxlint": "1.78.0",
34
+ "oxlint-tsgolint": "7.0.2001",
34
35
  "tsdown": "0.22.14",
35
36
  "typescript": "6.0.3",
36
37
  "vitest": "4.1.10",
@@ -38,12 +39,13 @@
38
39
  },
39
40
  "engines": {
40
41
  "bun": ">=1.2.0",
41
- "node": ">=22.15.0"
42
+ "node": ">=22.18.0"
42
43
  },
43
44
  "scripts": {
44
45
  "build": "tsdown",
45
- "lint": "oxlint .",
46
- "lint:fix": "oxlint . --fix",
46
+ "lint": "oxlint --type-aware .",
47
+ "lint:fix": "oxlint --type-aware . --fix",
48
+ "knip": "knip",
47
49
  "typecheck": "tsc --noEmit",
48
50
  "test": "vitest",
49
51
  "prepublish": "pnpm run build",