@tailor-platform/sdk-codemod 0.3.7 → 0.4.0-next.9

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.
Files changed (24) hide show
  1. package/CHANGELOG.md +267 -0
  2. package/dist/codemods/ast-grep-helpers-CXtWn3RB.js +171 -0
  3. package/dist/codemods/v2/apply-to-deploy/scripts/transform.js +22 -4
  4. package/dist/codemods/v2/auth-attributes-rename/scripts/transform.js +213 -0
  5. package/dist/codemods/v2/auth-connection-token-helper/scripts/transform.js +243 -0
  6. package/dist/codemods/v2/auth-invoker-call-unwrap/scripts/transform.js +7 -0
  7. package/dist/codemods/v2/auth-invoker-unwrap/scripts/transform.js +108 -13
  8. package/dist/codemods/v2/cli-rename/scripts/transform.js +373 -14
  9. package/dist/codemods/v2/db-type-to-table/scripts/transform.js +383 -0
  10. package/dist/codemods/v2/env-var-rename/scripts/transform.js +88 -0
  11. package/dist/codemods/v2/erd-site-to-plugin/scripts/transform.js +195 -0
  12. package/dist/codemods/v2/execute-script-arg/scripts/transform.js +60 -0
  13. package/dist/codemods/v2/forward-relation-name/scripts/transform.js +115 -0
  14. package/dist/codemods/v2/principal-unify/scripts/transform.js +1555 -44
  15. package/dist/codemods/v2/rename-bin/scripts/transform.js +1087 -0
  16. package/dist/codemods/v2/runtime-globals-opt-in/scripts/transform.js +103 -0
  17. package/dist/codemods/v2/runtime-subpath-namespace/scripts/transform.js +792 -0
  18. package/dist/codemods/v2/sdk-skills-shim/scripts/transform.js +3 -3
  19. package/dist/codemods/v2/tailor-output-ignore-dir/scripts/transform.js +14 -0
  20. package/dist/codemods/v2/tailordb-namespace/scripts/transform.js +5 -4
  21. package/dist/codemods/v2/wait-point-rename/scripts/transform.js +126 -0
  22. package/dist/codemods/v2/workflow-trigger-rename/scripts/transform.js +122 -0
  23. package/dist/index.js +1713 -51
  24. package/package.json +5 -4
package/dist/index.js CHANGED
@@ -5,14 +5,123 @@ import * as path from "pathe";
5
5
  import { readPackageJSON } from "pkg-types";
6
6
  import { arg, defineCommand, runMain } from "politty";
7
7
  import { z } from "zod";
8
- import { gte, lt, valid } from "semver";
8
+ import { gte, lt, parse, valid } from "semver";
9
9
  import * as fs from "node:fs";
10
- import { glob } from "node:fs/promises";
10
+ import { realpathSync } from "node:fs";
11
+ import { Lang, parse as parse$1 } from "@ast-grep/napi";
11
12
  import chalk from "chalk";
12
13
  import { structuredPatch } from "diff";
13
14
  import picomatch from "picomatch";
15
+ import { execFileSync } from "node:child_process";
16
+ //#region src/migration-doc.ts
17
+ /**
18
+ * Classify how much of a migration the codemod automates.
19
+ * - `Automatic`: a transform fully covers it, with no residual to flag.
20
+ * - `Partially automatic`: a transform covers the common cases but flags
21
+ * residuals (via `legacyPatterns`/`sourceStringLegacyPatterns`/
22
+ * `sourceTextLegacyPatterns`/`suspiciousPatterns`/
23
+ * `sourceStringSuspiciousPatterns`/`prompt`) to finish.
24
+ * - `Manual`: no transform; the change is migrated by hand (optionally guided
25
+ * by a `prompt`). Whether a person or an LLM does it does not matter here.
26
+ * @param codemod - The codemod registry entry
27
+ * @returns The automation level
28
+ */
29
+ function automationLevel(codemod) {
30
+ if (!codemod.scriptPath) return "Manual";
31
+ return (codemod.legacyPatterns?.length ?? 0) > 0 || (codemod.sourceStringLegacyPatterns?.length ?? 0) > 0 || (codemod.sourceTextLegacyPatterns?.length ?? 0) > 0 || (codemod.suspiciousPatterns?.length ?? 0) > 0 || (codemod.sourceStringSuspiciousPatterns?.length ?? 0) > 0 || codemod.prompt != null ? "Partially automatic" : "Automatic";
32
+ }
33
+ //#endregion
14
34
  //#region src/registry.ts
15
35
  const CODEMODS_ROOT = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), "codemods");
36
+ const RENAME_BIN_SOURCE_VALUE_FLAGS = [
37
+ "--env-file-if-exists",
38
+ "--env-file",
39
+ "--profile",
40
+ "--config",
41
+ "--workspace-id",
42
+ "--arg",
43
+ "--query",
44
+ "--file",
45
+ "--name",
46
+ "--namespace",
47
+ "--dir",
48
+ "-e",
49
+ "-p",
50
+ "-c",
51
+ "-w",
52
+ "-a",
53
+ "-q",
54
+ "-f",
55
+ "-n"
56
+ ];
57
+ const RENAME_BIN_SOURCE_COMMANDS = [
58
+ "api",
59
+ "apply",
60
+ "authconnection",
61
+ "completion",
62
+ "crash-report",
63
+ "crashreport",
64
+ "deploy",
65
+ "executor",
66
+ "function",
67
+ "generate",
68
+ "init",
69
+ "login",
70
+ "logout",
71
+ "machineuser",
72
+ "oauth2client",
73
+ "open",
74
+ "organization",
75
+ "profile",
76
+ "query",
77
+ "remove",
78
+ "secret",
79
+ "setup",
80
+ "show",
81
+ "skills",
82
+ "staticwebsite",
83
+ "tailordb",
84
+ "upgrade",
85
+ "user",
86
+ "workflow",
87
+ "workspace"
88
+ ];
89
+ function escapeRegExp(value) {
90
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
91
+ }
92
+ const RENAME_BIN_SOURCE_VALUE_GUARDS = RENAME_BIN_SOURCE_VALUE_FLAGS.flatMap((flag) => {
93
+ const escaped = escapeRegExp(flag);
94
+ return [`(?<!${escaped}\\s+)`, `(?<!${escaped}=)`];
95
+ }).join("");
96
+ const RENAME_BIN_SOURCE_COMMAND_OR_FLAG = `(?:--?[\\w-]+|${RENAME_BIN_SOURCE_COMMANDS.join("|")})`;
97
+ const RENAME_BIN_SOURCE_COMMAND_TOKEN = "tailor-sdk(?:(?:\\.(?:cmd|ps1|exe))|(?:@[^\\s'\"`;|&)]+))?(?![\\w-])";
98
+ const RENAME_BIN_SOURCE_LEGACY_PATTERN = new RegExp([
99
+ "(?<![.\\w-])",
100
+ "(?<![\"'])",
101
+ "(?<!\\\\[\"'])",
102
+ RENAME_BIN_SOURCE_VALUE_GUARDS,
103
+ RENAME_BIN_SOURCE_COMMAND_TOKEN,
104
+ `(?=\\s*(?:$|${RENAME_BIN_SOURCE_COMMAND_OR_FLAG}\\b))`
105
+ ].join(""));
106
+ const RENAME_BIN_QUOTED_SOURCE_LEGACY_PATTERN = new RegExp([
107
+ "(?:^|[\\s;&|\\x00])(?:sh|bash|zsh)\\s+-\\w*c\\w*\\s+\\\\?[\"']",
108
+ RENAME_BIN_SOURCE_COMMAND_TOKEN,
109
+ `(?=\\s*(?:$|${RENAME_BIN_SOURCE_COMMAND_OR_FLAG}\\b))`
110
+ ].join(""));
111
+ const RENAME_BIN_QUOTED_LEGACY_COMMAND_PATTERN = new RegExp([
112
+ RENAME_BIN_SOURCE_VALUE_GUARDS,
113
+ "[\"']",
114
+ RENAME_BIN_SOURCE_COMMAND_TOKEN,
115
+ "(?=\\s*(?:apply\\b|crash-report\\b|[^\"'`]*\\s--machineuser\\b))"
116
+ ].join(""));
117
+ const V2_NEXT_1 = "2.0.0-next.1";
118
+ const V2_NEXT_2 = "2.0.0-next.2";
119
+ const V2_NEXT_4 = "2.0.0-next.4";
120
+ const V2_NEXT_5 = "2.0.0-next.5";
121
+ const V2_NEXT_6 = "2.0.0-next.6";
122
+ const V2_NEXT_7 = "2.0.0-next.7";
123
+ const V2_NEXT_9 = "2.0.0-next.9";
124
+ /** All registered codemods, in registration order. */
16
125
  const allCodemods = [
17
126
  {
18
127
  id: "v2/define-generators-to-plugins",
@@ -20,8 +129,31 @@ const allCodemods = [
20
129
  description: "Migrate defineGenerators() tuple syntax to definePlugins() with explicit plugin imports",
21
130
  since: "1.0.0",
22
131
  until: "2.0.0",
132
+ prereleaseUntil: V2_NEXT_1,
23
133
  scriptPath: "v2/define-generators-to-plugins/scripts/transform.js",
24
- legacyPatterns: ["defineGenerators"]
134
+ legacyPatterns: ["defineGenerators"],
135
+ examples: [{
136
+ before: [
137
+ "import { defineGenerators } from \"@tailor-platform/sdk\";",
138
+ "",
139
+ "export const generators = defineGenerators(",
140
+ " [\"@tailor-platform/kysely-type\", { distPath: \"db.ts\" }],",
141
+ ");"
142
+ ].join("\n"),
143
+ after: [
144
+ "import { definePlugins } from \"@tailor-platform/sdk\";",
145
+ "import { kyselyTypePlugin } from \"@tailor-platform/sdk/plugin/kysely-type\";",
146
+ "",
147
+ "export const generators = definePlugins(kyselyTypePlugin({ distPath: \"db.ts\" }));"
148
+ ].join("\n")
149
+ }],
150
+ prompt: [
151
+ "defineGenerators() is replaced by definePlugins() in v2. The codemod rewrites the",
152
+ "known plugin tuples (kysely-type, enum-constants, file-utils, seed). For any",
153
+ "remaining defineGenerators([...]) the codemod left in place — a plugin it does not",
154
+ "know, or a non-tuple/spread form — convert it to definePlugins(pluginFn(config)),",
155
+ "importing the matching plugin from its @tailor-platform/sdk/plugin/<name> subpath."
156
+ ].join("\n")
25
157
  },
26
158
  {
27
159
  id: "v2/plugin-cli-import",
@@ -29,92 +161,1066 @@ const allCodemods = [
29
161
  description: "Rewrite deprecated plugin re-export imports (kyselyTypePlugin, enumConstantsPlugin, fileUtilsPlugin, seedPlugin) from `@tailor-platform/sdk/cli` to their dedicated plugin subpaths",
30
162
  since: "1.0.0",
31
163
  until: "2.0.0",
32
- scriptPath: "v2/plugin-cli-import/scripts/transform.js"
164
+ prereleaseUntil: V2_NEXT_1,
165
+ scriptPath: "v2/plugin-cli-import/scripts/transform.js",
166
+ examples: [{
167
+ before: "import { kyselyTypePlugin } from \"@tailor-platform/sdk/cli\";",
168
+ after: "import { kyselyTypePlugin } from \"@tailor-platform/sdk/plugin/kysely-type\";"
169
+ }]
33
170
  },
34
171
  {
35
172
  id: "v2/test-run-arg-input",
36
173
  name: "function test-run --arg input unwrap",
37
- description: "Strip the deprecated {input: ...} wrapper from `tailor-sdk function test-run --arg` JSON in scripts and docs",
174
+ description: "Strip the deprecated {input: ...} wrapper from `tailor function test-run --arg` JSON in scripts and docs",
38
175
  since: "1.0.0",
39
176
  until: "2.0.0",
177
+ prereleaseUntil: V2_NEXT_1,
40
178
  scriptPath: "v2/test-run-arg-input/scripts/transform.js",
41
179
  filePatterns: [
42
180
  "**/package.json",
43
181
  "**/*.{sh,bash,zsh}",
44
182
  "**/*.md"
45
- ]
183
+ ],
184
+ examples: [{
185
+ lang: "sh",
186
+ before: "tailor function test-run resolvers/add.ts --arg '{\"input\":{\"a\":1}}'",
187
+ after: "tailor function test-run resolvers/add.ts --arg '{\"a\":1}'"
188
+ }]
46
189
  },
47
190
  {
48
191
  id: "v2/sdk-skills-shim",
49
- name: "tailor-sdk-skills → tailor-sdk skills install",
50
- description: "Replace deprecated `tailor-sdk-skills` invocations with `tailor-sdk skills install`",
192
+ name: "tailor-sdk-skills → tailor skills add",
193
+ description: "Replace deprecated `tailor-sdk-skills` invocations with `tailor skills add`",
51
194
  since: "1.0.0",
52
195
  until: "2.0.0",
196
+ prereleaseUntil: V2_NEXT_1,
53
197
  scriptPath: "v2/sdk-skills-shim/scripts/transform.js",
54
198
  filePatterns: [
55
199
  "**/package.json",
56
200
  "**/*.{sh,bash,zsh,yml,yaml}",
57
201
  "**/*.md"
58
202
  ],
59
- legacyPatterns: ["tailor-sdk-skills"]
203
+ legacyPatterns: ["tailor-sdk-skills"],
204
+ examples: [{
205
+ lang: "sh",
206
+ before: "npx tailor-sdk-skills",
207
+ after: "tailor skills add"
208
+ }],
209
+ prompt: [
210
+ "The standalone tailor-sdk-skills binary is removed in v2; call the skills add",
211
+ "subcommand on the main tailor CLI instead. Replace any remaining",
212
+ "tailor-sdk-skills invocations the codemod did not rewrite with",
213
+ "`tailor skills add`."
214
+ ].join("\n")
60
215
  },
61
216
  {
62
217
  id: "v2/principal-unify",
63
- name: "Unify TailorUser/TailorActor/TailorInvoker → TailorPrincipal",
64
- description: "Rename TailorUser/TailorActor/TailorInvoker to TailorPrincipal, drop unauthenticatedTailorUser, and rename resolver body `user` to `caller`",
218
+ name: "Unify TailorUser/TailorActor/TailorActorType/TailorInvoker → TailorPrincipal",
219
+ description: "Rename TailorUser/TailorActor/TailorActorType/TailorInvoker to TailorPrincipal, drop unauthenticatedTailorUser, rename resolver body `user` to `caller`, and rename TailorDB callback `user` to `invoker`",
65
220
  since: "1.0.0",
66
221
  until: "2.0.0",
222
+ prereleaseUntil: V2_NEXT_2,
67
223
  scriptPath: "v2/principal-unify/scripts/transform.js",
68
224
  legacyPatterns: [
69
225
  "TailorUser",
70
226
  "TailorActor",
227
+ "TailorActorType",
71
228
  "TailorInvoker",
72
229
  "unauthenticatedTailorUser"
73
- ]
230
+ ],
231
+ suspiciousPatterns: [
232
+ "caller?.",
233
+ "context.user",
234
+ "context.invoker ?? context.user",
235
+ "ResolverContext"
236
+ ],
237
+ examples: [{
238
+ caption: "Type references unify under `TailorPrincipal`:",
239
+ before: "import type { TailorUser } from \"@tailor-platform/sdk\";",
240
+ after: "import type { TailorPrincipal } from \"@tailor-platform/sdk\";"
241
+ }, {
242
+ caption: "The resolver body `user` becomes `caller`:",
243
+ before: "body: ({ input, user }) => user.id,",
244
+ after: "body: ({ input, caller }) => caller.id,"
245
+ }],
246
+ prompt: [
247
+ "Finish the cases the codemod left for manual migration:",
248
+ "- Rename user -> caller in resolver bodies the codemod skipped because a `caller`",
249
+ " binding already exists or renaming would shadow/collide with another value.",
250
+ "- Replace member-access on the removed unauthenticatedTailorUser (e.g.",
251
+ " unauthenticatedTailorUser.id); the codemod only replaced standalone references",
252
+ " with null and left member access to surface a type error.",
253
+ "- Review helper adapters that still accept or read `context.user`; v2 resolver",
254
+ " context uses nullable `caller` and `invoker`, so project-specific helper",
255
+ " semantics for anonymous callers and command invokers must be chosen explicitly.",
256
+ "- Review `caller?.` values passed to APIs that require non-null values. If the",
257
+ " resolver requires authentication, throw or otherwise narrow before the call;",
258
+ " if anonymous callers are allowed, keep the nullable flow explicit.",
259
+ "Use TailorPrincipal for the unified user/actor/invoker type."
260
+ ].join("\n")
261
+ },
262
+ {
263
+ id: "v2/auth-attributes-rename",
264
+ name: "AttributeMap → Attributes",
265
+ description: "Rename auth attribute module augmentation and related SDK type names from `AttributeMap` to `Attributes`",
266
+ since: "1.0.0",
267
+ until: "2.0.0",
268
+ scriptPath: "v2/auth-attributes-rename/scripts/transform.js",
269
+ legacyPatterns: [
270
+ "AttributeMap",
271
+ "interface AttributeMap",
272
+ "UserAttributeMap",
273
+ "InferredAttributeMap"
274
+ ],
275
+ examples: [{
276
+ caption: "Module augmentation uses `Attributes`:",
277
+ before: "declare module \"@tailor-platform/sdk\" {\n interface AttributeMap {\n role: string;\n }\n}",
278
+ after: "declare module \"@tailor-platform/sdk\" {\n interface Attributes {\n role: string;\n }\n}"
279
+ }],
280
+ prompt: [
281
+ "In Tailor SDK v2, the auth attribute type API is renamed from `AttributeMap`",
282
+ "to `Attributes`; related SDK types are renamed to `UserAttributes` and",
283
+ "`InferredAttributes`. The codemod rewrites SDK imports, re-exports,",
284
+ "namespace-qualified references, import() type references, and module",
285
+ "augmentations. Review any remaining matches manually and leave unrelated",
286
+ "local names or deploy/proto wire field names unchanged."
287
+ ].join("\n")
74
288
  },
75
289
  {
76
290
  id: "v2/apply-to-deploy",
77
291
  name: "tailor-sdk apply → tailor-sdk deploy",
78
- description: "Rewrite `tailor-sdk apply` invocations in package.json scripts, shell scripts, CI configs, and docs to the v2-recommended `tailor-sdk deploy` alias",
292
+ description: "Rewrite `tailor-sdk apply` invocations in package.json scripts, shell scripts, CI configs, and docs to the canonical v2 `tailor-sdk deploy` command",
79
293
  since: "1.0.0",
80
294
  until: "2.0.0",
295
+ prereleaseUntil: V2_NEXT_1,
81
296
  scriptPath: "v2/apply-to-deploy/scripts/transform.js",
82
297
  filePatterns: [
83
298
  "**/package.json",
84
299
  "**/*.{sh,bash,zsh,yml,yaml}",
300
+ "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}",
85
301
  "**/*.md"
86
- ]
302
+ ],
303
+ examples: [{
304
+ lang: "sh",
305
+ before: "tailor-sdk apply --profile prod",
306
+ after: "tailor-sdk deploy --profile prod"
307
+ }]
87
308
  },
88
309
  {
89
310
  id: "v2/cli-rename",
90
- name: "v2 CLI rename (single-word commands)",
91
- description: "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",
311
+ name: "v2 CLI rename",
312
+ description: "Rewrite `tailor-sdk crash-report` to `tailor-sdk crashreport` and `--machineuser` to `--machine-user` across package.json scripts, shell scripts, CI configs, and docs",
92
313
  since: "1.0.0",
93
314
  until: "2.0.0",
315
+ prereleaseUntil: V2_NEXT_1,
94
316
  scriptPath: "v2/cli-rename/scripts/transform.js",
95
317
  filePatterns: [
96
318
  "**/package.json",
97
319
  "**/*.{sh,bash,zsh,yml,yaml}",
98
320
  "**/*.md"
99
- ]
321
+ ],
322
+ legacyPatterns: ["tailor-sdk crash-report", "--machineuser"],
323
+ examples: [{
324
+ lang: "sh",
325
+ before: "tailor-sdk crash-report list\ntailor-sdk login --machineuser",
326
+ after: "tailor-sdk crashreport list\ntailor-sdk login --machine-user"
327
+ }],
328
+ prompt: [
329
+ "Apply the v2 CLI renames the codemod did not reach (only `tailor-sdk`-prefixed",
330
+ "invocations are rewritten): `tailor-sdk crash-report` -> `tailor-sdk crashreport`",
331
+ "and the `--machineuser` option -> `--machine-user`. Leave unrelated commands that",
332
+ "happen to use `--machineuser` alone."
333
+ ].join("\n")
100
334
  },
101
335
  {
102
- id: "v2/auth-invoker-unwrap",
336
+ id: "v2/env-var-rename",
337
+ name: "SDK environment variable rename",
338
+ description: "Rewrite unambiguous removed SDK environment variable names to their v2 `TAILOR_*` names and flag generic names for manual review",
339
+ since: "1.0.0",
340
+ until: "2.0.0",
341
+ scriptPath: "v2/env-var-rename/scripts/transform.js",
342
+ filePatterns: [
343
+ "**/package.json",
344
+ "**/.env",
345
+ "**/.env.*",
346
+ "**/*.{env,sh,bash,zsh,yml,yaml,json,md}",
347
+ "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"
348
+ ],
349
+ legacyPatterns: [
350
+ "TAILOR_PLATFORM_SDK_CONFIG_PATH",
351
+ "TAILOR_PLATFORM_SDK_DTS_PATH",
352
+ "TAILOR_PLATFORM_SDK_ALLOW_CI_ID_INJECTION",
353
+ "TAILOR_PLATFORM_SDK_BUILD_ONLY",
354
+ "TAILOR_SDK_OUTPUT_DIR",
355
+ "TAILOR_SDK_SKILLS_SOURCE",
356
+ "TAILOR_SDK_VERSION",
357
+ "PLATFORM_URL",
358
+ "PLATFORM_OAUTH2_CLIENT_ID",
359
+ "TAILOR_ENABLE_INLINE_SOURCEMAP",
360
+ "TAILOR_PLATFORM_QUERY_NEWLINE_ON_ENTER",
361
+ "LOG_LEVEL",
362
+ "TAILOR_TOKEN"
363
+ ],
364
+ sourceStringLegacyPatterns: [
365
+ "PLATFORM_URL",
366
+ "PLATFORM_OAUTH2_CLIENT_ID",
367
+ "LOG_LEVEL"
368
+ ],
369
+ examples: [{
370
+ lang: "sh",
371
+ before: "TAILOR_PLATFORM_SDK_BUILD_ONLY=true tailor-sdk deploy",
372
+ after: "TAILOR_DEPLOY_BUILD_ONLY=true tailor-sdk deploy"
373
+ }, {
374
+ before: "const token = process.env.TAILOR_TOKEN;",
375
+ after: "const token = process.env.TAILOR_PLATFORM_TOKEN;"
376
+ }],
377
+ prompt: [
378
+ "Review any remaining removed SDK environment variable names after the codemod",
379
+ "runs. The codemod intentionally leaves generic names such as `LOG_LEVEL`,",
380
+ "`PLATFORM_URL`, and `PLATFORM_OAUTH2_CLIENT_ID` for manual review because",
381
+ "they can configure non-SDK tools. Replace only actual SDK usages with their",
382
+ "v2 names. If a remaining match is an unrelated local identifier, fixture",
383
+ "label, or historical documentation that intentionally does not configure the",
384
+ "SDK, leave it unchanged."
385
+ ].join("\n")
386
+ },
387
+ {
388
+ id: "v2/auth-invoker-call-unwrap",
103
389
  name: "auth.invoker(\"name\") → \"name\"",
104
- 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.",
390
+ description: "Replace statically identified SDK `auth.invoker(\"name\")` option values with the bare `\"name\"` string while preserving the `authInvoker` key for SDK versions before the option rename.",
391
+ since: "1.0.0",
392
+ until: "2.0.0",
393
+ prereleaseUntil: V2_NEXT_1,
394
+ scriptPath: "v2/auth-invoker-call-unwrap/scripts/transform.js",
395
+ suspiciousPatterns: ["auth.invoker"],
396
+ reviewSupersededBy: ["v2/auth-invoker-unwrap"],
397
+ prompt: [
398
+ "In Tailor SDK v2 the auth.invoker() helper is removed; an invoker is now the",
399
+ "machine user name passed directly as a string. The codemod already rewrote the",
400
+ "statically identified SDK option form authInvoker: auth.invoker(\"name\") to authInvoker: \"name\". These files still contain",
401
+ "auth.invoker(...) calls that need manual review.",
402
+ "",
403
+ "For each remaining auth.invoker(<expr>) call:",
404
+ "1. Replace the whole call with <expr> only where the target option expects a",
405
+ " machine user name string; platform/runtime authInvoker payloads still expect",
406
+ " the object form.",
407
+ "2. Keep the authInvoker key when targeting SDK versions before the invoker",
408
+ " option rename; later v2 targets run a separate codemod for that key rename.",
409
+ "3. After removing every auth.invoker usage in a file, delete the now-unused auth",
410
+ " import (keeping it pulls Node-only config modules into runtime bundles); leave",
411
+ " the import if auth is still referenced elsewhere.",
412
+ "",
413
+ "Do not change behavior beyond the auth.invoker() removal."
414
+ ].join("\n"),
415
+ examples: [{
416
+ before: "createResolver({ authInvoker: auth.invoker(\"manager\") });",
417
+ after: "createResolver({ authInvoker: \"manager\" });"
418
+ }]
419
+ },
420
+ {
421
+ id: "v2/auth-invoker-unwrap",
422
+ name: "auth.invoker(\"name\") → invoker: \"name\"",
423
+ description: "Rename statically identified SDK `authInvoker` options to `invoker`, replace `auth.invoker(\"name\")` there with the bare `\"name\"` string, and drop the `auth` import when no other reference remains. Ambiguous workflow `.start()` calls are left for manual review. The `auth.invoker()` helper is removed in v2 because importing `auth` from `tailor.config.ts` into runtime files pulls Node-only modules into the bundle.",
105
424
  since: "1.0.0",
106
425
  until: "2.0.0",
426
+ prereleaseUntil: V2_NEXT_2,
107
427
  scriptPath: "v2/auth-invoker-unwrap/scripts/transform.js",
108
- legacyPatterns: ["auth.invoker"]
428
+ suspiciousPatterns: [
429
+ "auth.invoker",
430
+ "authInvoker:",
431
+ "authInvoker :",
432
+ "authInvoker?",
433
+ "{ authInvoker",
434
+ ", authInvoker",
435
+ "\n authInvoker",
436
+ "\n authInvoker",
437
+ "\n authInvoker",
438
+ "\"authInvoker\":",
439
+ "\"authInvoker\" :",
440
+ "\"authInvoker\"?",
441
+ "'authInvoker':",
442
+ "'authInvoker' :",
443
+ "'authInvoker'?"
444
+ ],
445
+ prompt: [
446
+ "In Tailor SDK v2 the auth.invoker() helper is removed; an invoker is now the",
447
+ "machine user name passed directly as a string. The codemod already rewrote the",
448
+ "statically identified SDK option form authInvoker: auth.invoker(\"name\") to invoker: \"name\" and renamed supported authInvoker option keys. These files still contain",
449
+ "auth.invoker(...) calls or authInvoker keys that need manual review.",
450
+ "",
451
+ "For each remaining auth.invoker(<expr>) call:",
452
+ "1. Replace the whole call with <expr> only where the target option expects a",
453
+ " machine user name string; platform/runtime authInvoker payloads still expect",
454
+ " the object form.",
455
+ "2. Rename remaining authInvoker option keys to invoker only for SDK resolver,",
456
+ " executor, workflow.start(), or startWorkflow() options. Keep platform/runtime",
457
+ " payload keys such as tailor.workflow.startWorkflow(..., { authInvoker: ... }).",
458
+ "3. After removing every auth.invoker usage in a file, delete the now-unused auth",
459
+ " import (keeping it pulls Node-only config modules into runtime bundles); leave",
460
+ " the import if auth is still referenced elsewhere.",
461
+ "",
462
+ "Do not change behavior beyond the SDK option rename and auth.invoker() removal."
463
+ ].join("\n"),
464
+ examples: [{
465
+ before: "createResolver({ invoker: auth.invoker(\"manager\") });",
466
+ after: "createResolver({ invoker: \"manager\" });"
467
+ }]
468
+ },
469
+ {
470
+ id: "v2/auth-connection-token-helper",
471
+ name: "auth.getConnectionToken() → runtime authconnection",
472
+ description: "The deprecated `auth.getConnectionToken()` helper returned by `defineAuth()` is removed in v2. Use `authconnection.getConnectionToken(...)` from `@tailor-platform/sdk/runtime` in resolvers, executors, and workflows instead.",
473
+ since: "1.0.0",
474
+ until: "2.0.0",
475
+ prereleaseUntil: V2_NEXT_2,
476
+ scriptPath: "v2/auth-connection-token-helper/scripts/transform.js",
477
+ filePatterns: ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"],
478
+ examples: [{
479
+ before: "import { auth } from \"../tailor.config\";\n\nconst token = await auth.getConnectionToken(\"google\");",
480
+ after: "import { authconnection } from \"@tailor-platform/sdk/runtime\";\n\nconst token = await authconnection.getConnectionToken(\"google\");"
481
+ }],
482
+ prompt: [
483
+ "In Tailor SDK v2 the auth.getConnectionToken() helper returned by defineAuth()",
484
+ "is removed. Runtime code should call authconnection.getConnectionToken(...) from",
485
+ "@tailor-platform/sdk/runtime instead of importing auth from tailor.config.ts.",
486
+ "",
487
+ "For each getConnectionToken usage where <receiver> is a defineAuth() result",
488
+ "imported from tailor.config.ts:",
489
+ "1. Replace <receiver>.getConnectionToken(<expr>) calls with",
490
+ " authconnection.getConnectionToken(<expr>).",
491
+ "2. Update non-call references, including <receiver>.getConnectionToken,",
492
+ " <receiver>[\"getConnectionToken\"], and destructuring from <receiver>, to",
493
+ " reference authconnection instead.",
494
+ "3. Add or reuse `import { authconnection } from \"@tailor-platform/sdk/runtime\"`.",
495
+ "4. Remove the auth import from tailor.config.ts only when no other auth reference",
496
+ " remains in the file.",
497
+ "",
498
+ "Leave usages unchanged when the receiver is already the runtime authconnection",
499
+ "wrapper or global tailor.authconnection."
500
+ ].join("\n")
501
+ },
502
+ {
503
+ id: "v2/runtime-subpath-namespace",
504
+ name: "Runtime subpath imports use namespace objects",
505
+ description: "Rewrite `@tailor-platform/sdk/runtime/*` namespace-star and flat value imports to self-named namespace imports, and aggregate `file.deleteFile` calls to `file.delete`. `TailorContextAPI` and `TailorWorkflowAPI` now describe SDK wrappers; direct platform globals use `PlatformContextAPI` and `PlatformWorkflowAPI`.",
506
+ since: "1.0.0",
507
+ until: "2.0.0",
508
+ prereleaseUntil: V2_NEXT_4,
509
+ scriptPath: "v2/runtime-subpath-namespace/scripts/transform.js",
510
+ filePatterns: ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"],
511
+ legacyPatterns: [
512
+ "@tailor-platform/sdk/runtime/iconv",
513
+ "@tailor-platform/sdk/runtime/secretmanager",
514
+ "@tailor-platform/sdk/runtime/authconnection",
515
+ "@tailor-platform/sdk/runtime/idp",
516
+ "@tailor-platform/sdk/runtime/workflow",
517
+ "@tailor-platform/sdk/runtime/context",
518
+ "@tailor-platform/sdk/runtime/file",
519
+ "@tailor-platform/sdk/runtime/aigateway"
520
+ ],
521
+ examples: [
522
+ {
523
+ before: "import * as iconv from \"@tailor-platform/sdk/runtime/iconv\";\niconv.convert(value, \"UTF-8\", \"Shift_JIS\");",
524
+ after: "import { iconv } from \"@tailor-platform/sdk/runtime/iconv\";\niconv.convert(value, \"UTF-8\", \"Shift_JIS\");"
525
+ },
526
+ {
527
+ before: "import { get } from \"@tailor-platform/sdk/runtime/aigateway\";\nconst gateway = await get(\"main\");",
528
+ after: "import { aigateway } from \"@tailor-platform/sdk/runtime/aigateway\";\nconst gateway = await aigateway.get(\"main\");"
529
+ },
530
+ {
531
+ before: "import { file } from \"@tailor-platform/sdk/runtime\";\nawait file.deleteFile(\"ns\", \"Doc\", \"blob\", \"record-id\");",
532
+ after: "import { file } from \"@tailor-platform/sdk/runtime\";\nawait file.delete(\"ns\", \"Doc\", \"blob\", \"record-id\");"
533
+ }
534
+ ],
535
+ prompt: [
536
+ "In Tailor SDK v2, runtime subpath modules export only a self-named namespace",
537
+ "object (for example, `iconv` from `@tailor-platform/sdk/runtime/iconv`).",
538
+ "Default and flat value imports such as",
539
+ "`import { get } from \"@tailor-platform/sdk/runtime/aigateway\"` are removed.",
540
+ "The codemod rewrites straightforward namespace-star imports and flat named value",
541
+ "imports. It also rewrites direct `file.deleteFile` calls on the aggregate runtime",
542
+ "namespace to `file.delete`. Destructured aggregate `deleteFile` references require",
543
+ "manual migration. Review any remaining runtime imports manually, especially when",
544
+ "a local binding or nested scope shadows an imported value, or when",
545
+ "type-position namespace member references need explicit top-level type imports.",
546
+ "For direct platform globals, replace `TailorContextAPI` and `TailorWorkflowAPI`",
547
+ "type references with `PlatformContextAPI` and `PlatformWorkflowAPI` respectively."
548
+ ].join("\n")
109
549
  },
110
550
  {
111
551
  id: "v2/tailordb-namespace",
112
552
  name: "Tailordb → tailordb (lowercase ambient namespace)",
113
- description: "Rewrite references to the deprecated capital-cased `Tailordb` ambient namespace (`Tailordb.QueryResult`, `Tailordb.CommandType`, `Tailordb.Client`, `typeof Tailordb.Client`) to the new lowercase `tailordb.*` namespace re-published by the SDK in place of `@tailor-platform/function-types`.",
553
+ description: "Rewrite references to the removed capital-cased `Tailordb` ambient namespace (`Tailordb.QueryResult`, `Tailordb.CommandType`, `Tailordb.Client`, `typeof Tailordb.Client`) to the lowercase `tailordb.*` namespace exposed by `@tailor-platform/sdk/runtime/globals`. Because v2 no longer activates ambient declarations automatically, each file that contains `tailordb.*` references after the rewrite must also add `import \"@tailor-platform/sdk/runtime/globals\"`.",
114
554
  since: "1.0.0",
115
555
  until: "2.0.0",
556
+ prereleaseUntil: V2_NEXT_1,
116
557
  scriptPath: "v2/tailordb-namespace/scripts/transform.js",
117
- legacyPatterns: ["Tailordb."]
558
+ legacyPatterns: ["Tailordb."],
559
+ examples: [{
560
+ before: "const command: Tailordb.CommandType = \"SELECT\";",
561
+ after: "import \"@tailor-platform/sdk/runtime/globals\";\nconst command: tailordb.CommandType = \"SELECT\";"
562
+ }],
563
+ prompt: [
564
+ "The capital-cased Tailordb ambient namespace is removed in v2; use the lowercase",
565
+ "tailordb.* namespace from @tailor-platform/sdk/runtime/globals. The codemod rewrites",
566
+ "the known members (QueryResult, CommandType, Client). Rewrite any other remaining",
567
+ "Tailordb.* reference to its tailordb.* equivalent (and confirm the member still",
568
+ "exists on the lowercase namespace).",
569
+ "Also add `import \"@tailor-platform/sdk/runtime/globals\"` at the top of each file",
570
+ "that contains any tailordb.* type reference — v2 no longer activates ambient",
571
+ "declarations automatically on SDK import."
572
+ ].join("\n")
573
+ },
574
+ {
575
+ id: "v2/db-type-to-table",
576
+ name: "db.type() → db.table()",
577
+ description: "Rename TailorDB schema builder calls from `db.type()` to `db.table()`. TailorDB schema definitions now use table terminology in SDK projects.",
578
+ since: "1.0.0",
579
+ until: "2.0.0",
580
+ prereleaseUntil: V2_NEXT_4,
581
+ scriptPath: "v2/db-type-to-table/scripts/transform.js",
582
+ filePatterns: ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"],
583
+ legacyPatterns: ["db.type"],
584
+ examples: [{
585
+ before: "import { db } from \"@tailor-platform/sdk\";\n\nexport const user = db.type(\"User\", {\n name: db.string(),\n});",
586
+ after: "import { db } from \"@tailor-platform/sdk\";\n\nexport const user = db.table(\"User\", {\n name: db.string(),\n});"
587
+ }],
588
+ prompt: [
589
+ "In Tailor SDK v2, TailorDB schema definitions use db.table(...) instead of",
590
+ "db.type(...). The codemod rewrites member accesses on db imported from",
591
+ "@tailor-platform/sdk, including aliases such as `import { db as schema }`.",
592
+ "It flags destructured builder aliases such as `const { type } = db` and",
593
+ "local builder aliases such as `const schema = db`, `schema = db`, or",
594
+ "`function make(schema = db) { ... }` for manual review because the local",
595
+ "alias may require call-site renaming.",
596
+ "Review any remaining db.type references and rename SDK TailorDB schema builder",
597
+ "calls to db.table. Leave unrelated local objects with a .type() method unchanged."
598
+ ].join("\n")
599
+ },
600
+ {
601
+ id: "v2/forward-relation-name",
602
+ name: "TailorDB forward relation names derive from field names",
603
+ description: "Review TailorDB relations that omit `toward.as`. Their forward GraphQL field names now derive from the relation field name with a trailing `ID`, `Id`, or `id` removed, instead of from the target table name.",
604
+ since: "1.0.0",
605
+ until: "2.0.0",
606
+ prereleaseUntil: V2_NEXT_5,
607
+ scriptPath: "v2/forward-relation-name/scripts/transform.js",
608
+ filePatterns: ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"],
609
+ suspiciousPatterns: [
610
+ /\.relation\b(?!\s*\()/,
611
+ /\{[^}\n]*\brelation\b[^}\n]*\}\s*=/,
612
+ /\[\s*["']relation["']\s*\]/
613
+ ],
614
+ examples: [{
615
+ caption: "Preserve the v1 GraphQL field name by making it explicit:",
616
+ before: [
617
+ "ownerId: db.uuid().relation({",
618
+ " type: \"n-1\",",
619
+ " toward: { type: user },",
620
+ "}),"
621
+ ].join("\n"),
622
+ after: [
623
+ "ownerId: db.uuid().relation({",
624
+ " type: \"n-1\",",
625
+ " toward: { type: user, as: \"user\" },",
626
+ "}),"
627
+ ].join("\n")
628
+ }],
629
+ prompt: [
630
+ "Tailor SDK v2 derives a default forward GraphQL relation name from the source",
631
+ "field name by removing a trailing ID, Id, or id. V1 derived it from the target",
632
+ "table name. Review each reported non-self relation that omits toward.as.",
633
+ "",
634
+ "If consumers must keep using the v1 GraphQL field name, inspect the v1 schema and",
635
+ "copy that exact field name into toward.as. Otherwise, update GraphQL operations",
636
+ "and consumer code to use the new field-based name. No change is needed when the old",
637
+ "and new names are identical. Relations with a guaranteed non-empty toward.as,",
638
+ "self-relations, and keyOnly relations are unchanged. For an empty or dynamic",
639
+ "toward.as, determine whether its runtime value can be falsy; if so, treat the",
640
+ "relation as using the default name.",
641
+ "",
642
+ "A relation field without a trailing ID, Id, or id would default to its own scalar",
643
+ "field name and therefore conflict. Give that relation an explicit toward.as."
644
+ ].join("\n")
645
+ },
646
+ {
647
+ id: "v2/execute-script-arg",
648
+ name: "executeScript arg JSON.stringify → value",
649
+ description: "Unwrap `JSON.stringify(...)` passed as the `executeScript` `arg` option. In v2 `arg` takes a JSON-serializable value and is serialized internally, so a pre-stringified argument double-encodes.",
650
+ since: "1.0.0",
651
+ until: "2.0.0",
652
+ prereleaseUntil: V2_NEXT_2,
653
+ scriptPath: "v2/execute-script-arg/scripts/transform.js",
654
+ filePatterns: ["**/*.{ts,tsx,mts,cts,mjs,cjs,js}"],
655
+ suspiciousPatterns: [[
656
+ "executeScript",
657
+ "JSON.stringify",
658
+ /\barg\s*[:=]|["']arg["']\s*(?::|\]\s*[:=])/
659
+ ]],
660
+ prompt: [
661
+ "In Tailor SDK v2 the executeScript() arg option takes a JSON-serializable value",
662
+ "and is serialized internally, so a pre-stringified argument double-encodes. The",
663
+ "codemod already rewrote the direct form arg: JSON.stringify(X) to arg: X. Review",
664
+ "the executeScript calls in these files for cases it could not rewrite — where the",
665
+ "arg value is reached indirectly, for example:",
666
+ "- a variable holding a JSON.stringify(...) result (const s = JSON.stringify(x); ... arg: s)",
667
+ "- JSON.stringify(x, null, 2) or another multi-argument form",
668
+ "- an options object built or spread dynamically",
669
+ "",
670
+ "For each such call, pass the underlying value directly as arg (drop the",
671
+ "JSON.stringify wrapper) so executeScript serializes it once. Leave calls that",
672
+ "already pass a plain value unchanged."
673
+ ].join("\n"),
674
+ examples: [{
675
+ before: "await executeScript({ ...opts, arg: JSON.stringify({ a: 1 }) });",
676
+ after: "await executeScript({ ...opts, arg: { a: 1 } });"
677
+ }]
678
+ },
679
+ {
680
+ id: "v2/wait-point-rename",
681
+ name: "defineWaitPoint/defineWaitPoints → createWaitPoint/createWaitPoints",
682
+ description: "Rename `defineWaitPoint` and `defineWaitPoints` to `createWaitPoint` and `createWaitPoints`. The functions create runtime instances with `.wait()` / `.resolve()` methods, so the `create*` prefix is used consistently.",
683
+ since: "1.0.0",
684
+ until: "2.0.0",
685
+ scriptPath: "v2/wait-point-rename/scripts/transform.js",
686
+ legacyPatterns: ["defineWaitPoint", "defineWaitPoints"],
687
+ examples: [{
688
+ before: "import { defineWaitPoints } from \"@tailor-platform/sdk\";\n\nexport const { approval } = defineWaitPoints((define) => ({\n approval: define<{ message: string }, { approved: boolean }>(),\n}));",
689
+ after: "import { createWaitPoints } from \"@tailor-platform/sdk\";\n\nexport const { approval } = createWaitPoints((define) => ({\n approval: define<{ message: string }, { approved: boolean }>(),\n}));"
690
+ }]
691
+ },
692
+ {
693
+ id: "v2/workflow-trigger-rename",
694
+ name: "workflow.triggerWorkflow/triggerJobFunction/resumeWorkflow → startWorkflow/startJobFunction/resumeWorkflowExecution",
695
+ description: "Rename tailor.workflow call sites from the pre-alignment triggerWorkflow/triggerJobFunction/resumeWorkflow names to the canonical startWorkflow/startJobFunction/resumeWorkflowExecution names, on both the ambient tailor.workflow global and a workflow value imported from @tailor-platform/sdk/runtime(/workflow). For a renamed triggerWorkflow call, also renames a literal `invoker` option key to `authInvoker` — startWorkflow's options expect the platform shape directly, unlike the removed triggerWorkflow wrapper, which converted invoker to authInvoker internally.",
696
+ since: "1.0.0",
697
+ until: "2.0.0",
698
+ prereleaseUntil: V2_NEXT_6,
699
+ scriptPath: "v2/workflow-trigger-rename/scripts/transform.js",
700
+ filePatterns: ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"],
701
+ legacyPatterns: [
702
+ "triggerWorkflow",
703
+ "triggerJobFunction",
704
+ "resumeWorkflow"
705
+ ],
706
+ examples: [{
707
+ before: "import { workflow } from \"@tailor-platform/sdk/runtime\";\n\nawait workflow.triggerWorkflow(\"myWorkflow\", { data: \"value\" });",
708
+ after: "import { workflow } from \"@tailor-platform/sdk/runtime\";\n\nawait workflow.startWorkflow(\"myWorkflow\", { data: \"value\" });"
709
+ }, {
710
+ caption: "A literal invoker option is renamed to authInvoker:",
711
+ before: "await workflow.triggerWorkflow(\"myWorkflow\", { data: \"value\" }, { invoker: myInvoker });",
712
+ after: "await workflow.startWorkflow(\"myWorkflow\", { data: \"value\" }, { authInvoker: myInvoker });"
713
+ }],
714
+ prompt: [
715
+ "The pre-alignment tailor.workflow names triggerWorkflow, triggerJobFunction, and",
716
+ "resumeWorkflow are removed from the SDK's type surface in v2; use the canonical",
717
+ "startWorkflow, startJobFunction, and resumeWorkflowExecution names instead. The",
718
+ "codemod rewrites direct member-access call sites on the ambient tailor.workflow",
719
+ "global and on a workflow value imported from @tailor-platform/sdk/runtime or",
720
+ "@tailor-platform/sdk/runtime/workflow (including aliased imports). It skips a",
721
+ "file entirely when a local declaration shadows the workflow import or the",
722
+ "ambient tailor name, to avoid rewriting an unrelated same-named value — review",
723
+ "those manually.",
724
+ "",
725
+ "For a renamed triggerWorkflow call, the codemod also renames a literal invoker",
726
+ "option key (including shorthand { invoker }) to authInvoker, since startWorkflow",
727
+ "expects the platform's authInvoker shape directly while triggerWorkflow's removed",
728
+ "wrapper converted invoker to authInvoker internally.",
729
+ "",
730
+ "Also review, and migrate by hand:",
731
+ "- Destructured references (e.g. const { triggerWorkflow } = workflow) — the",
732
+ " codemod only rewrites direct member-access calls.",
733
+ "- Imported TriggerWorkflowOptions / TriggerJobFunctionOptions types — rename",
734
+ " them to StartWorkflowOptions / StartJobFunctionOptions.",
735
+ "- An invoker option passed via a variable or spread (not a literal object) —",
736
+ " the codemod only inspects literal object arguments; rename the invoker key",
737
+ " to authInvoker in the options object's own definition."
738
+ ].join("\n")
739
+ },
740
+ {
741
+ id: "v2/open-download-stream",
742
+ name: "openDownloadStream → downloadStream",
743
+ description: "The deprecated `openDownloadStream` file-streaming API is removed in v2. Use `downloadStream` for streamed file downloads. The generated file utilities now emit `downloadFileStream` (which calls `downloadStream` and returns `FileDownloadStreamResponse`) instead of the removed `openFileDownloadStream` helper.",
744
+ since: "1.0.0",
745
+ until: "2.0.0",
746
+ prereleaseUntil: V2_NEXT_2,
747
+ filePatterns: ["**/*.{ts,tsx,mts,cts,mjs,cjs,js}"],
748
+ suspiciousPatterns: ["openDownloadStream", "openFileDownloadStream"],
749
+ examples: [{
750
+ before: "const res = await openDownloadStream(namespace, typeName, fieldName, recordId);",
751
+ after: "const res = await downloadStream(namespace, typeName, fieldName, recordId);"
752
+ }],
753
+ prompt: [
754
+ "The openDownloadStream file-streaming API is removed in v2. Replace every call to",
755
+ "openDownloadStream with downloadStream (same arguments). If you used the generated",
756
+ "openFileDownloadStream helper, switch to downloadFileStream, which calls",
757
+ "downloadStream and returns FileDownloadStreamResponse."
758
+ ].join("\n")
759
+ },
760
+ {
761
+ id: "v2/runtime-globals-opt-in",
762
+ name: "Ambient runtime globals are opt-in",
763
+ description: "Importing `@tailor-platform/sdk` no longer activates the ambient `tailor.*` / `tailordb.*` global declarations. The codemod rewrites simple direct `new tailor.idp.Client(...)` calls to the typed `idp.Client` wrapper from `@tailor-platform/sdk/runtime`; broader runtime global usage remains review-only. Only if you relied on the ambient globals directly, add `import \"@tailor-platform/sdk/runtime/globals\"`. (The capital-cased `Tailordb.*` namespace is removed separately — see the `Tailordb → tailordb` codemod.)",
764
+ since: "1.0.0",
765
+ until: "2.0.0",
766
+ prereleaseUntil: V2_NEXT_1,
767
+ scriptPath: "v2/runtime-globals-opt-in/scripts/transform.js",
768
+ filePatterns: ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"],
769
+ suspiciousPatterns: [
770
+ "tailor.context",
771
+ "tailor.iconv",
772
+ "tailor.idp",
773
+ "tailor.secretmanager",
774
+ "tailor.authconnection",
775
+ "tailor.workflow",
776
+ "tailor[",
777
+ "tailordb.Client",
778
+ "tailordb.CommandType",
779
+ "tailordb.QueryResult",
780
+ "tailordb.file",
781
+ "tailordb[",
782
+ "TailorDBFileError",
783
+ "TailorErrorItem",
784
+ "TailorErrorMessage",
785
+ "TailorErrors"
786
+ ],
787
+ sourceStringSuspiciousPatterns: [
788
+ "new tailor.idp.Client",
789
+ /[=(:,[]\s*tailor\.idp\.Client\b/,
790
+ /(?:(?:=>|[=(:,<{]|\[)\s*|\b(?:return|await|typeof)\s+)tailor\.(?:authconnection|context|iconv|idp|secretmanager|workflow)(?:\.[A-Za-z_$][\w$]*)?\b/,
791
+ /\btailor\.(?:authconnection|context|iconv|idp|secretmanager|workflow)\.[A-Za-z_$][\w$]*\s*\(/,
792
+ "tailor[",
793
+ /\btailordb\.file\.[A-Za-z_$][\w$]*\s*\(/,
794
+ /(?:(?:=>|[=(:,<{]|\[)\s*|\b(?:return|await|typeof)\s+)tailordb\.file\b/,
795
+ /(?:\bnew\s+|(?:=>|[=(:,<{]|\[)\s*|\b(?:return|await|typeof)\s+)tailordb\.(?:Client|CommandType|QueryResult)\b/,
796
+ /<\s*tailordb\.(?:Client|CommandType|QueryResult)\b/,
797
+ "tailordb[",
798
+ /(?:\bnew\s+|\bthrow\s+|\binstanceof\s+)Tailor(?:DBFileError|Errors|ErrorMessage)\b/,
799
+ /(?:[:=<]\s*|\bas\s+)Tailor(?:DBFileError|Errors|ErrorMessage|ErrorItem)\b/,
800
+ /[:<]\s*TailorErrorItem\b/
801
+ ],
802
+ examples: [{
803
+ caption: "Preferred: switch to the typed wrappers from `@tailor-platform/sdk/runtime` and drop the ambient globals:",
804
+ before: "const client = new tailor.idp.Client();",
805
+ after: "import { idp } from \"@tailor-platform/sdk/runtime\";\nconst client = new idp.Client({ namespace: \"my-namespace\" });"
806
+ }, {
807
+ caption: "Fallback: only if you must keep referencing the bare `tailor.*` names, opt into the global declarations:",
808
+ before: "const client = new tailor.idp.Client();",
809
+ after: "import \"@tailor-platform/sdk/runtime/globals\";\nconst client = new tailor.idp.Client();"
810
+ }],
811
+ prompt: [
812
+ "The v2 SDK no longer enables ambient Tailor runtime globals from",
813
+ "`@tailor-platform/sdk`. For each flagged file that uses `tailor.*`,",
814
+ "`tailordb.*`, or Tailor runtime error globals, prefer migrating to the",
815
+ "typed wrappers from `@tailor-platform/sdk/runtime`. The codemod already",
816
+ "rewrites direct `new tailor.idp.Client(...)` calls to `new idp.Client(...)`",
817
+ "when the file has no conflicting `tailor` or `idp` binding. For any remaining",
818
+ "`tailor.idp.Client` references, either resolve the binding collision and use",
819
+ "`idp.Client`, or keep the ambient global deliberately.",
820
+ "",
821
+ "Only when the file must keep referencing the bare `tailor.*` names directly,",
822
+ "opt into the global declarations instead by adding one of these:",
823
+ "- per-file: `import \"@tailor-platform/sdk/runtime/globals\";`",
824
+ "- project-wide: `\"types\": [\"@tailor-platform/sdk/runtime/globals\"]` in",
825
+ " the relevant tsconfig compilerOptions",
826
+ "",
827
+ "Leave files unchanged when the matching name is local, imported from another",
828
+ "module, or appears only in comments or prose strings. Embedded code strings",
829
+ "that use runtime globals are review-only findings; do not insert imports inside",
830
+ "string literals."
831
+ ].join("\n")
832
+ },
833
+ {
834
+ id: "v2/workflow-trigger-dispatch",
835
+ name: "Workflow job start() and start tests",
836
+ description: "Workflow job `.start()` (previously `.trigger()`) now aligns with the platform runtime: it returns the job result directly instead of a Promise wrapper, and tests no longer run job bodies locally. Mock start responses with `mockWorkflow()` (`setJobHandler` / `enqueueResult`, assert via `startedJobs`), or use `runWorkflowLocally()` for a full-chain local run.",
837
+ since: "1.0.0",
838
+ until: "2.0.0",
839
+ prereleaseUntil: V2_NEXT_1,
840
+ suspiciousPatterns: [".trigger("],
841
+ examples: [{
842
+ caption: "Tests must mock the workflow runtime instead of running bodies locally:",
843
+ before: "const result = await orderJob.start({ id });\nexpect(result.status).toBe(\"done\");",
844
+ after: "using wf = mockWorkflow();\nwf.setJobHandler((jobName) => (jobName === \"order-job\" ? { status: \"done\" } : null));\nconst result = await orderJob.start({ id });\nexpect(result.status).toBe(\"done\");"
845
+ }],
846
+ prompt: [
847
+ "Workflow job .start() now uses the platform workflow runtime instead of running",
848
+ "the job body locally. In tests, acquire `using wf = mockWorkflow()` and provide",
849
+ "start responses (setJobHandler / enqueueResult), or use runWorkflowLocally() for a",
850
+ "full-chain local run; an unmocked start now throws. Outside tests, treat the",
851
+ "start result as the job output directly (no Promise wrapper to unwrap)."
852
+ ].join("\n")
853
+ },
854
+ {
855
+ id: "v2/workflow-start-rename",
856
+ name: "Workflow.trigger()/WorkflowJob.trigger() → .start()",
857
+ description: "Rename `Workflow.trigger()` (returned by `createWorkflow()`) and `WorkflowJob.trigger()` (returned by `createWorkflowJob()`) to `.start()`, aligning the SDK's ergonomic verb with the platform's `start*` RPC vocabulary. No codemod ships for this rename: distinguishing a workflow/job `.trigger()` call from an unrelated object's own `.trigger()` method requires resolving the receiver back to a `createWorkflow`/`createWorkflowJob` result across files, which the SDK's own CLI bundler already does for build-time rewriting. Reusing that logic in a standalone script is a nontrivial lift, and — unlike the bundler, which fails loudly when it cannot rewrite a call — a codemod false positive would silently rewrite an unrelated `.trigger()` call with no error. For the call-site volume this rename typically involves, manual review guided by the prompt below is the safer trade-off.",
858
+ since: "1.0.0",
859
+ until: "2.0.0",
860
+ prereleaseUntil: V2_NEXT_7,
861
+ filePatterns: ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"],
862
+ suspiciousPatterns: [".trigger("],
863
+ examples: [{
864
+ before: ["const inventory = checkInventory.trigger({ orderId: input.orderId });", "const workflowRunId = await orderProcessingWorkflow.trigger(args, { invoker: \"manager\" });"].join("\n"),
865
+ after: ["const inventory = checkInventory.start({ orderId: input.orderId });", "const workflowRunId = await orderProcessingWorkflow.start(args, { invoker: \"manager\" });"].join("\n")
866
+ }],
867
+ prompt: [
868
+ "In Tailor SDK v2, the ergonomic .trigger() method on a createWorkflow() or",
869
+ "createWorkflowJob() result is renamed to .start(). This is unrelated to the",
870
+ "separate tailor.workflow.triggerWorkflow/triggerJobFunction/resumeWorkflow removal",
871
+ "(see the workflow-trigger-rename codemod) — this rename targets the SDK's own",
872
+ "ergonomic wrapper, not the low-level platform call.",
873
+ "",
874
+ "For each flagged `.trigger(` call in these files:",
875
+ "1. Confirm the receiver is a workflow or job object — typically a local const",
876
+ " assigned from createWorkflow(...)/createWorkflowJob(...), a named import of one,",
877
+ " or the default import of a workflow module. Skip receivers that are unrelated",
878
+ " objects with their own .trigger() method (state machines, event emitters, etc.).",
879
+ "2. Rename the call from .trigger(...) to .start(...); the argument list is unchanged.",
880
+ "3. Update any mock/test code that reads WorkflowJob['trigger'] / Workflow['trigger']",
881
+ " as a type, or that mocks the ergonomic method via a wrapper — for example,",
882
+ " `wf.job(definition)` / `wf.workflow(definition)` from mockWorkflow() now return a",
883
+ " mock of the `.start` method.",
884
+ "4. Update prose/docs/comments that say \"trigger the workflow/job\" to \"start\" only",
885
+ " where they describe this SDK verb specifically, not unrelated event terminology."
886
+ ].join("\n")
887
+ },
888
+ {
889
+ id: "v2/cli-token-keyring-storage",
890
+ name: "CLI tokens stored in the OS keyring",
891
+ description: "CLI login tokens are stored in the OS keyring by default when available, falling back to the platform config file when it is not. No source change is required; re-login if you need tokens moved into the keyring.",
892
+ since: "1.0.0",
893
+ until: "2.0.0",
894
+ prereleaseUntil: V2_NEXT_2,
895
+ notice: true
896
+ },
897
+ {
898
+ id: "v2/cli-users-by-subject",
899
+ name: "CLI users keyed by subject ID",
900
+ description: "The CLI stores human users by their stable subject ID instead of email (email is kept for display). Legacy email-keyed entries are migrated automatically on the next login or token refresh. No source change is required.",
901
+ since: "1.0.0",
902
+ until: "2.0.0",
903
+ prereleaseUntil: V2_NEXT_1,
904
+ notice: true
905
+ },
906
+ {
907
+ id: "v2/function-logs-content-hash",
908
+ name: "function logs require a content hash for source mapping",
909
+ description: "`tailor function logs` maps stack traces against the function bundle only when the execution recorded a `contentHash`. Executions without one now show raw stack traces instead of mapped frames. No source change is required.",
910
+ since: "1.0.0",
911
+ until: "2.0.0",
912
+ prereleaseUntil: V2_NEXT_1,
913
+ notice: true
914
+ },
915
+ {
916
+ id: "v2/rename-bin",
917
+ name: "tailor-sdk binary → tailor",
918
+ description: "Rename the CLI binary from `tailor-sdk` to `tailor` in package.json scripts, shell scripts, CI workflows, source files, generated declaration comments, and documentation. Does not rename `.tailor-sdk` directory paths or the `create-tailor-sdk` scaffolding package. Note: v2 also changes the default generated output directory from `.tailor-sdk/` to `.tailor/` and the setup lock file from `.github/tailor-sdk.lock` to `.github/tailor.lock`. Run `mv .tailor-sdk .tailor` to migrate the generated output directory (preserves auth connection state and other local files). Run `git mv .github/tailor-sdk.lock .github/tailor.lock` if the old lock file exists; without it `tailor setup check` will treat all managed workflows as missing. Exact ignore-file entries for `.tailor-sdk/` are handled by the generated-output ignore codemod. If your CI workflows were generated by `tailor setup`, re-run `tailor setup` afterwards so they pin tailor-platform/actions v2 — the v1 actions invoke the removed `tailor-sdk` bin.",
919
+ since: "1.0.0",
920
+ until: "2.0.0",
921
+ scriptPath: "v2/rename-bin/scripts/transform.js",
922
+ filePatterns: [
923
+ "**/package.json",
924
+ "**/*.{sh,bash,zsh,yml,yaml}",
925
+ "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}",
926
+ "**/*.md"
927
+ ],
928
+ legacyPatterns: ["tailor-sdk"],
929
+ sourceStringLegacyPatterns: [
930
+ RENAME_BIN_SOURCE_LEGACY_PATTERN,
931
+ RENAME_BIN_QUOTED_SOURCE_LEGACY_PATTERN,
932
+ RENAME_BIN_QUOTED_LEGACY_COMMAND_PATTERN
933
+ ],
934
+ sourceTextLegacyPatterns: [
935
+ RENAME_BIN_SOURCE_LEGACY_PATTERN,
936
+ RENAME_BIN_QUOTED_SOURCE_LEGACY_PATTERN,
937
+ RENAME_BIN_QUOTED_LEGACY_COMMAND_PATTERN
938
+ ],
939
+ examples: [{
940
+ lang: "sh",
941
+ before: "tailor-sdk deploy\nnpx tailor-sdk@latest login",
942
+ after: "tailor deploy\nnpx @tailor-platform/sdk@latest login"
943
+ }],
944
+ prompt: [
945
+ "Rename any remaining `tailor-sdk` binary invocations to `tailor`. Only rewrite",
946
+ "the binary name — leave `.tailor-sdk` directory paths and `create-tailor-sdk`",
947
+ "package references unchanged."
948
+ ].join("\n")
949
+ },
950
+ {
951
+ id: "v2/tailor-output-ignore-dir",
952
+ name: ".tailor-sdk ignore entries → .tailor",
953
+ description: "Rewrite exact ignore-file entries for the v1 generated output directory from `.tailor-sdk` to the v2 `.tailor` directory. Other `.tailor-sdk` paths and prose are left unchanged.",
954
+ since: "1.0.0",
955
+ until: "2.0.0",
956
+ scriptPath: "v2/tailor-output-ignore-dir/scripts/transform.js",
957
+ filePatterns: [
958
+ "**/.gitignore",
959
+ "**/.npmignore",
960
+ "**/.dockerignore",
961
+ "**/gitignore",
962
+ "**/npmignore",
963
+ "**/dockerignore",
964
+ "**/_gitignore",
965
+ "**/_npmignore",
966
+ "**/_dockerignore",
967
+ "**/__dot__gitignore",
968
+ "**/__dot__npmignore",
969
+ "**/__dot__dockerignore",
970
+ "**/*.gitignore",
971
+ "**/*.npmignore",
972
+ "**/*.dockerignore"
973
+ ],
974
+ examples: [{
975
+ lang: "gitignore",
976
+ before: ".tailor-sdk/",
977
+ after: ".tailor/"
978
+ }]
979
+ },
980
+ {
981
+ id: "v2/tailordb-validate-simplify",
982
+ name: "ValidateFn simplification and type-level validate",
983
+ description: "Field-level `ValidateFn` is simplified from `(args: { value, data, invoker }) => boolean` to `(args: { value }) => string | void` — the function now returns the error message directly instead of a separate `[fn, message]` tuple. The `ValidateConfig` tuple form and `Validators<F>` record syntax on `db.type().validate()` are removed. Type-level validation uses `db.type().validate((args, issues) => void)` with `{ newRecord, oldRecord, invoker }` args and an `issues(field, message)` callback for cross-field rules.",
984
+ since: "1.0.0",
985
+ until: "2.0.0",
986
+ prereleaseUntil: V2_NEXT_5,
987
+ suspiciousPatterns: [
988
+ "ValidateConfig",
989
+ "Validators<",
990
+ "ValidatorsBase",
991
+ ".validate("
992
+ ],
993
+ examples: [{
994
+ caption: "Field-level validate: return an error message string instead of a boolean (tuple form removed):",
995
+ before: ".validate(\n [({ value }) => value.length > 5, \"Name must be longer than 5 characters\"],\n)",
996
+ after: ".validate(({ value }) =>\n value.length <= 5 ? \"Name must be longer than 5 characters\" : undefined,\n)"
997
+ }, {
998
+ caption: "Type-level validate: per-field record syntax replaced by a single function with `issues()` callback:",
999
+ before: ".validate({\n name: [({ value }) => value.length > 5, \"Name must be longer than 5\"],\n})",
1000
+ after: ".validate(({ newRecord }, issues) => {\n if (newRecord.name && newRecord.name.length <= 5) {\n issues(\"name\", \"Name must be longer than 5\");\n }\n})"
1001
+ }],
1002
+ prompt: [
1003
+ "The v2 SDK simplifies field validation and introduces type-level validation.",
1004
+ "",
1005
+ "Field-level `.validate()` changes:",
1006
+ "- Signature: `(args: { value, data, invoker }) => boolean` → `(args: { value }) => string | void`",
1007
+ "- The function now returns the error message string directly (or undefined/void to pass)",
1008
+ " instead of returning a boolean with the message in a separate tuple.",
1009
+ "- The `[fn, errorMessage]` tuple form (`ValidateConfig`) is removed.",
1010
+ "- `data` and `invoker` are no longer available in field-level validators.",
1011
+ " Use type-level `.validate()` for cross-field or invoker-dependent rules.",
1012
+ "",
1013
+ "Type-level `.validate()` on `db.type()` changes:",
1014
+ "- Old: `.validate({ fieldName: fn | [fn, msg] | fn[] })` (per-field record, `Validators<F>` type)",
1015
+ "- New: `.validate((args, issues) => void)` (single function, `TypeValidateFn<F>` type)",
1016
+ "- Args: `{ newRecord, oldRecord, invoker }` — `newRecord` is the record after hooks run",
1017
+ "- Call `issues(field, message)` to report validation errors; `field` supports dotted paths",
1018
+ "- Move per-field validators that need `data`/`invoker` to the type-level function",
1019
+ "",
1020
+ "For each remaining `ValidateConfig`, `Validators<`, or old-signature `.validate()` usage:",
1021
+ "1. Rewrite field-level validators to return the error string directly",
1022
+ "2. Move cross-field / invoker-dependent validators to the type-level function",
1023
+ "3. Remove unused `ValidateConfig` / `Validators` type imports"
1024
+ ].join("\n")
1025
+ },
1026
+ {
1027
+ id: "v2/tailordb-hook-redesign",
1028
+ name: "TailorDB hook redesign: field-level args and type-level hooks",
1029
+ description: "Field-level `HookFn` args change from `{ value, data, invoker }` to create `{ input, invoker, now }` / update `{ input, oldValue, invoker, now }` — `value` is renamed to `input`, matching the `input` arg on type-level hooks (same pre-hook data, narrowed to one field); `data` (the full record) is removed; `oldValue` (previous field value) is added for update hooks only; `now` (operation timestamp) is shared across all hooks. Type-level hooks on `db.type().hooks()` change from per-field mapping `{ fieldName: { create, update } }` (`Hooks<F>`) to a single `{ create, update }` object (`TypeHook<F>`) — create hooks take `{ input, invoker, now }`, update hooks take `{ input, oldRecord, invoker, now }` (oldRecord is always non-null). Both return partial field overrides.",
1030
+ since: "1.0.0",
1031
+ until: "2.0.0",
1032
+ prereleaseUntil: V2_NEXT_5,
1033
+ suspiciousPatterns: [
1034
+ "Hooks<",
1035
+ "HookFn<",
1036
+ "Hook<",
1037
+ ".hooks("
1038
+ ],
1039
+ examples: [{
1040
+ caption: "Field-level hooks: `value` renamed to `input`, `data` replaced by `oldValue` and `now`; use `now` instead of `new Date()`:",
1041
+ before: "db.datetime().hooks({\n create: ({ value }) => value ?? new Date(),\n update: () => new Date(),\n})",
1042
+ after: "db.datetime().hooks({\n create: ({ input, now }) => input ?? now,\n update: ({ now }) => now,\n})"
1043
+ }, {
1044
+ caption: "Type-level hooks: per-field mapping replaced by single create/update functions:",
1045
+ before: ".hooks({\n fullAddress: {\n create: ({ data }) => `${data.postalCode} ${data.address}`,\n update: ({ data }) => `${data.postalCode} ${data.address}`,\n },\n})",
1046
+ after: ".hooks({\n create: ({ input }) => ({\n fullAddress: `${input.postalCode} ${input.address}`,\n }),\n update: ({ input }) => ({\n fullAddress: `${input.postalCode} ${input.address}`,\n }),\n})"
1047
+ }],
1048
+ prompt: [
1049
+ "The v2 SDK redesigns TailorDB hooks at both field and type levels.",
1050
+ "",
1051
+ "Field-level `.hooks()` on individual fields:",
1052
+ "- Create args: `{ value, data, invoker }` → `{ input, invoker, now }` (no `oldValue`)",
1053
+ "- Update args: `{ value, data, invoker }` → `{ input, oldValue, invoker, now }`",
1054
+ "- `value` is renamed to `input`, matching the type-level hook's `input` arg — both are",
1055
+ " the same pre-hook data, at different granularity",
1056
+ "- `data` (full record) is removed; update hooks get `oldValue` (previous field value) instead",
1057
+ "- `now` provides the operation timestamp — use `now` instead of `new Date()`",
1058
+ "- If a field-level hook needs the full record (other fields), move it to a type-level hook",
1059
+ "",
1060
+ "Type-level `.hooks()` on `db.type()`:",
1061
+ "- Old: `.hooks({ fieldName: { create: fn, update: fn } })` (per-field mapping, `Hooks<F>` type)",
1062
+ "- New: `.hooks({ create: fn, update: fn })` (single object, `TypeHook<F>` type)",
1063
+ "- Each function: `({ input, oldRecord, invoker, now }) => ({ fieldName: value, ... })`",
1064
+ "- `input` is the pre-hook input (may have nullish values for optional/defaulted fields)",
1065
+ "- Create hooks do not receive `oldRecord`; update hooks receive `oldRecord` (always non-null)",
1066
+ "- Return an object with only the fields to override; unmentioned fields are unchanged",
1067
+ "",
1068
+ "Migration steps for each `.hooks()` call on a `db.type()`:",
1069
+ "1. If the old per-field hooks only use `value`/`invoker` and don't reference `data`,",
1070
+ " convert them to field-level hooks with the new args (`value` → `input`, plus `oldValue`, `now`)",
1071
+ "2. If the old hooks reference `data` (cross-field access), convert to a type-level hook",
1072
+ " using `input`/`oldRecord`",
1073
+ "3. Remove unused `Hooks<F>` / `HookFn<>` type imports"
1074
+ ].join("\n")
1075
+ },
1076
+ {
1077
+ id: "v2/erd-site-to-plugin",
1078
+ name: "`db.<namespace>.erdSite` → `tailordbErdPlugin({ sites })`",
1079
+ description: "Move the TailorDB `erdSite` setting from `db.<namespace>` in tailor.config.ts into `tailordbErdPlugin({ sites })` from `@tailor-platform/sdk-plugin-tailordb-erd`, registered via definePlugins(). The core config schema no longer accepts `erdSite`; the `tailor tailordb erd` commands read the target static website from the plugin configuration and validate each site name against `staticWebsites`. Install `@tailor-platform/sdk-plugin-tailordb-erd` as a dev dependency: the migrated config imports it, so config loading fails with a module-not-found error until it is installed.",
1080
+ since: "1.0.0",
1081
+ until: "2.0.0",
1082
+ prereleaseUntil: V2_NEXT_9,
1083
+ scriptPath: "v2/erd-site-to-plugin/scripts/transform.js",
1084
+ legacyPatterns: ["erdSite:"],
1085
+ sourceStringLegacyPatterns: ["erdSite"],
1086
+ suspiciousPatterns: [
1087
+ "erdSite:",
1088
+ /\berdSite\s*[,}]/,
1089
+ "tailordbErdPlugin"
1090
+ ],
1091
+ sourceStringSuspiciousPatterns: ["erdSite"],
1092
+ examples: [{
1093
+ before: [
1094
+ "export default defineConfig({",
1095
+ " db: {",
1096
+ " tailordb: {",
1097
+ " files: [\"./tailordb/*.ts\"],",
1098
+ " erdSite: \"my-erd-site\",",
1099
+ " },",
1100
+ " },",
1101
+ "});"
1102
+ ].join("\n"),
1103
+ after: [
1104
+ "import { tailordbErdPlugin } from \"@tailor-platform/sdk-plugin-tailordb-erd\";",
1105
+ "",
1106
+ "export default defineConfig({",
1107
+ " db: {",
1108
+ " tailordb: {",
1109
+ " files: [\"./tailordb/*.ts\"],",
1110
+ " },",
1111
+ " },",
1112
+ "});",
1113
+ "",
1114
+ "export const plugins = definePlugins(",
1115
+ " tailordbErdPlugin({ sites: { tailordb: \"my-erd-site\" } }),",
1116
+ ");"
1117
+ ].join("\n")
1118
+ }],
1119
+ prompt: [
1120
+ "In Tailor SDK v2 the TailorDB `erdSite` setting is removed from the core config",
1121
+ "schema; the ERD deploy target is configured on the ERD CLI plugin instead. The",
1122
+ "codemod rewrites literal `db.<namespace>.erdSite` entries inside top-level",
1123
+ "defineConfig() calls into a `tailordbErdPlugin({ sites: { <namespace>: <value> } })`",
1124
+ "argument of definePlugins(), importing it from @tailor-platform/sdk-plugin-tailordb-erd.",
1125
+ "",
1126
+ "First, for every config that now registers tailordbErdPlugin, make sure",
1127
+ "@tailor-platform/sdk-plugin-tailordb-erd is installed as a dev dependency — the",
1128
+ "migrated config imports it, so config loading fails with ERR_MODULE_NOT_FOUND",
1129
+ "until it is installed.",
1130
+ "",
1131
+ "For any remaining `erdSite` config keys the codemod did not rewrite — a db config",
1132
+ "built dynamically or passed via a variable, quoted or computed keys, spread",
1133
+ "properties, a defineConfig() call inside a factory function, or a file that",
1134
+ "already registers tailordbErdPlugin — move the namespace → static-website-name",
1135
+ "mapping into tailordbErdPlugin({ sites }) and delete the `erdSite` key. For",
1136
+ "factory-built configs, keep any referenced parameters or locals in scope when",
1137
+ "moving the value to the module-level definePlugins() export. Each site name",
1138
+ "must match a static website defined in staticWebsites. Leave unrelated",
1139
+ "identifiers that merely contain the name (e.g. a defineStaticWebSite variable",
1140
+ "named erdSite) unchanged."
1141
+ ].join("\n")
1142
+ },
1143
+ {
1144
+ id: "v2/generate-watch-flag",
1145
+ name: "generate --watch flag removed",
1146
+ description: "Review and remove `tailor generate --watch` / `-W` invocations and the `watch` option on `GenerateOptions`. The flag, its dependency watcher, and the self-restart-on-change logic are removed; `generate` now always performs a single generation pass.",
1147
+ since: "1.0.0",
1148
+ until: "2.0.0",
1149
+ prereleaseUntil: V2_NEXT_6,
1150
+ filePatterns: [
1151
+ "**/package.json",
1152
+ "**/*.{sh,bash,zsh,yml,yaml}",
1153
+ "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}",
1154
+ "**/*.md"
1155
+ ],
1156
+ suspiciousPatterns: [/\bgenerate\b[^\n]*(?:--watch\b|\s-W\b)/, [/\bgenerate\s*\(/, /\bwatch\s*:/]],
1157
+ examples: [{
1158
+ lang: "sh",
1159
+ caption: "The --watch/-W flag no longer exists; re-run generate after each change:",
1160
+ before: "tailor generate --watch",
1161
+ after: "tailor generate"
1162
+ }],
1163
+ prompt: [
1164
+ "Tailor SDK v2 removes the `generate --watch` (`-W`) flag along with the",
1165
+ "dependency watcher and self-restart logic that powered it. `tailor generate`",
1166
+ "now always runs a single generation pass and exits.",
1167
+ "",
1168
+ "For each flagged `tailor generate ... --watch` / `-W` invocation (package.json",
1169
+ "scripts, shell scripts, CI configs, or docs), drop the flag and re-run",
1170
+ "`tailor generate` after each change instead. If automatic regeneration on file",
1171
+ "change is still needed, wrap the command with a general-purpose file watcher",
1172
+ "(e.g. `chokidar-cli`, `nodemon`) at the project level.",
1173
+ "",
1174
+ "For programmatic use of `generate()` from `@tailor-platform/sdk/cli`, remove the",
1175
+ "`watch` field from the `GenerateOptions` argument — the function now performs a",
1176
+ "single generation pass and resolves once it completes."
1177
+ ].join("\n")
1178
+ },
1179
+ {
1180
+ id: "v2/seed-exec-to-cli-plugin",
1181
+ name: "Generated seed exec.mjs → tailor seed CLI plugin",
1182
+ 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).",
1183
+ since: "1.0.0",
1184
+ until: "2.0.0",
1185
+ prereleaseUntil: V2_NEXT_9,
1186
+ filePatterns: ["**/package.json", "**/*.{sh,yml,yaml,md,mjs,ts}"],
1187
+ suspiciousPatterns: ["exec.mjs"],
1188
+ sourceStringSuspiciousPatterns: ["exec.mjs"],
1189
+ examples: [{
1190
+ before: "\"seed\": \"node ./seed/exec.mjs\",\n\"seed:validate\": \"node ./seed/exec.mjs validate\"",
1191
+ after: "\"seed\": \"tailor seed apply\",\n\"seed:validate\": \"tailor seed validate\"",
1192
+ lang: "jsonc"
1193
+ }],
1194
+ prompt: [
1195
+ "seedPlugin no longer generates the exec.mjs seed runner in v2. The tailor seed",
1196
+ "CLI plugin (@tailor-platform/sdk-plugin-seed) replaces it:",
1197
+ "",
1198
+ "- Install @tailor-platform/sdk-plugin-seed as a devDependency next to",
1199
+ " @tailor-platform/sdk.",
1200
+ "- Replace `node <distPath>/exec.mjs [options] [types...]` invocations with",
1201
+ " `tailor seed apply [options] [types...]` (same options: --machine-user/-m,",
1202
+ " --namespace/-n, --skip-idp, --truncate, --yes, and type-name arguments).",
1203
+ "- Replace `node <distPath>/exec.mjs validate [path]` with",
1204
+ " `tailor seed validate [path]`.",
1205
+ "- Delete the stale generated `<distPath>/exec.mjs` file; keep the data/",
1206
+ " directory (JSONL data and generated schemas) as-is."
1207
+ ].join("\n")
1208
+ },
1209
+ {
1210
+ id: "v2/node-minimum-22-15-0",
1211
+ name: "Node.js minimum version raised to 22.15.0",
1212
+ 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+.",
1213
+ since: "1.0.0",
1214
+ until: "2.0.0",
1215
+ notice: true
1216
+ },
1217
+ {
1218
+ id: "v2/remove-legacy-bundle-cleanup",
1219
+ name: "Legacy bundle artifact cleanup removed from deploy",
1220
+ description: "`tailor deploy` no longer deletes on-disk bundle artifacts (`.entry.js` files, workflow-job bundles, and the `hooks-validate-scripts/` directory) left in the SDK output directory (`.tailor` by default) by SDK versions that predate the current in-memory bundling approach. Current bundlers no longer write these files. No source change is required; if such stale files remain from a very old SDK version, delete only those specific files/directories manually — do not delete the output directory itself, since it also holds deploy state (e.g. `secrets-state/`, `*.context.json`) that existing secrets and Auth Connections depend on.",
1221
+ since: "1.0.0",
1222
+ until: "2.0.0",
1223
+ notice: true
118
1224
  }
119
1225
  ];
120
1226
  /**
@@ -125,9 +1231,33 @@ const allCodemods = [
125
1231
  function resolveCodemodScript(scriptPath) {
126
1232
  return path.resolve(CODEMODS_ROOT, scriptPath);
127
1233
  }
1234
+ function reachesCodemodBoundary(toVersion, codemod) {
1235
+ if (gte(toVersion, codemod.until)) return true;
1236
+ if (codemod.prereleaseUntil === void 0 || codemod.prereleaseUntil === "pending" || !gte(toVersion, codemod.prereleaseUntil)) return false;
1237
+ const target = parse(toVersion);
1238
+ const boundary = parse(codemod.until);
1239
+ return target.prerelease.length > 0 && target.major === boundary.major && target.minor === boundary.minor && target.patch === boundary.patch;
1240
+ }
1241
+ function effectiveCodemodBoundary(codemod) {
1242
+ if (codemod.prereleaseUntil === "pending") return codemod.until;
1243
+ return codemod.prereleaseUntil ?? codemod.until;
1244
+ }
1245
+ function assertCodemodBoundaries(codemods) {
1246
+ for (const codemod of codemods) {
1247
+ const boundary = parse(codemod.until);
1248
+ if (boundary === null) throw new Error(`Codemod ${codemod.id} until must be a valid semver version: ${codemod.until}`);
1249
+ if (boundary.prerelease.length > 0) throw new Error(`Codemod ${codemod.id} until must be a stable version: ${codemod.until}`);
1250
+ if (codemod.prereleaseUntil === void 0 || codemod.prereleaseUntil === "pending") continue;
1251
+ const prereleaseBoundary = parse(codemod.prereleaseUntil);
1252
+ if (prereleaseBoundary === null) throw new Error(`Codemod ${codemod.id} prereleaseUntil must be a valid semver version: ${codemod.prereleaseUntil}`);
1253
+ if (prereleaseBoundary.prerelease.length === 0) throw new Error(`Codemod ${codemod.id} prereleaseUntil must be a prerelease version: ${codemod.prereleaseUntil}`);
1254
+ if (prereleaseBoundary.major !== boundary.major || prereleaseBoundary.minor !== boundary.minor || prereleaseBoundary.patch !== boundary.patch) throw new Error(`Codemod ${codemod.id} prereleaseUntil must target the same version as until: ${codemod.prereleaseUntil}`);
1255
+ }
1256
+ }
128
1257
  /**
129
1258
  * Get codemod packages applicable for a version range.
130
- * A codemod applies when: since <= fromVersion < until <= toVersion
1259
+ * A codemod applies when: since <= fromVersion < boundary <= toVersion.
1260
+ * A target prerelease reaches `until` only when the codemod declares `prereleaseUntil`.
131
1261
  * @param fromVersion - Current SDK version (semver)
132
1262
  * @param toVersion - Target SDK version (semver)
133
1263
  * @returns Array of applicable codemod packages in registration order
@@ -135,7 +1265,8 @@ function resolveCodemodScript(scriptPath) {
135
1265
  function getApplicableCodemods(fromVersion, toVersion) {
136
1266
  if (!valid(fromVersion)) throw new Error(`Invalid fromVersion: ${fromVersion}`);
137
1267
  if (!valid(toVersion)) throw new Error(`Invalid toVersion: ${toVersion}`);
138
- return allCodemods.filter((codemod) => gte(fromVersion, codemod.since) && lt(fromVersion, codemod.until) && gte(toVersion, codemod.until));
1268
+ assertCodemodBoundaries(allCodemods);
1269
+ return allCodemods.filter((codemod) => gte(fromVersion, codemod.since) && lt(fromVersion, effectiveCodemodBoundary(codemod)) && reachesCodemodBoundary(toVersion, codemod));
139
1270
  }
140
1271
  //#endregion
141
1272
  //#region src/runner.ts
@@ -147,6 +1278,68 @@ const EXCLUDE_DIRS = /* @__PURE__ */ new Set([
147
1278
  "dist",
148
1279
  ".git"
149
1280
  ]);
1281
+ const ALLOWED_DOT_DIRS = /* @__PURE__ */ new Set([".github", ".circleci"]);
1282
+ const SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
1283
+ ".ts",
1284
+ ".tsx",
1285
+ ".mts",
1286
+ ".cts",
1287
+ ".js",
1288
+ ".jsx",
1289
+ ".mjs",
1290
+ ".cjs"
1291
+ ]);
1292
+ const SOURCE_STRING_FRAGMENT_SEPARATOR = "\0";
1293
+ const MASKED_SOURCE_NODE_KINDS = /* @__PURE__ */ new Set([
1294
+ "comment",
1295
+ "string",
1296
+ "regex",
1297
+ "string_fragment",
1298
+ "jsx_text"
1299
+ ]);
1300
+ const SOURCE_VALUE_FLAGS = /* @__PURE__ */ new Set([
1301
+ "--env-file-if-exists",
1302
+ "--env-file",
1303
+ "--profile",
1304
+ "--config",
1305
+ "--workspace-id",
1306
+ "--arg",
1307
+ "--query",
1308
+ "--file",
1309
+ "--name",
1310
+ "--namespace",
1311
+ "--dir",
1312
+ "-e",
1313
+ "-p",
1314
+ "-c",
1315
+ "-w",
1316
+ "-a",
1317
+ "-q",
1318
+ "-f",
1319
+ "-n"
1320
+ ]);
1321
+ const SOURCE_CLI_BINARY_RE = /^(?:(?:.*[\\/])?tailor(?:\.(?:cmd|ps1|exe))?|(?:.*[\\/])?tailor-sdk(?:@[^\s'"`;|&)]+)?(?:\.(?:cmd|ps1|exe))?|@tailor-platform\/sdk(?:@[^\s'"`;|&)]+)?)$/;
1322
+ function shouldSkipDirectory(name) {
1323
+ return EXCLUDE_DIRS.has(name) || name.startsWith(".") && !ALLOWED_DOT_DIRS.has(name);
1324
+ }
1325
+ async function* walkFiles(root, relativeDir = "") {
1326
+ const absoluteDir = path.join(root, relativeDir);
1327
+ let entries;
1328
+ try {
1329
+ entries = await fs.promises.readdir(absoluteDir, { withFileTypes: true });
1330
+ } catch {
1331
+ return;
1332
+ }
1333
+ for (const entry of entries) {
1334
+ const relative = relativeDir ? path.join(relativeDir, entry.name) : entry.name;
1335
+ if (entry.isDirectory()) {
1336
+ if (shouldSkipDirectory(entry.name)) continue;
1337
+ yield* walkFiles(root, relative);
1338
+ continue;
1339
+ }
1340
+ if (entry.isFile()) yield relative;
1341
+ }
1342
+ }
150
1343
  /**
151
1344
  * Print a colorized unified diff for a single file to stderr.
152
1345
  * @param filePath - Absolute path to the file
@@ -169,12 +1362,301 @@ function printDiff(filePath, before, after) {
169
1362
  * Load a transform module from a TypeScript file path.
170
1363
  * Expects the module to have a default export that is a TransformFn.
171
1364
  * @param scriptPath - Absolute path to the transform script
172
- * @returns The transform function
1365
+ * @returns The transform function and optional review detector
173
1366
  */
174
- async function loadTransform(scriptPath) {
1367
+ async function loadTransformModule(scriptPath) {
175
1368
  const mod = await import(url.pathToFileURL(scriptPath).href);
176
1369
  if (typeof mod.default !== "function") throw new Error(`Transform at ${scriptPath} does not have a default export function`);
177
- return mod.default;
1370
+ return {
1371
+ transform: mod.default,
1372
+ reviewFindings: typeof mod.reviewFindings === "function" ? mod.reviewFindings : void 0
1373
+ };
1374
+ }
1375
+ function contentForResidualMatching(relative, content) {
1376
+ const ext = path.extname(relative).toLowerCase();
1377
+ return SOURCE_EXTENSIONS.has(ext) ? maskSourceNonCode(relative, content) : content;
1378
+ }
1379
+ function sourceStringFragmentGapForResidualMatching(gap) {
1380
+ if (/^\\["']$/.test(gap)) return gap.slice(1);
1381
+ return /^(?:\\(?:[nrtvf]|\r\n|\r|\n)|\s)+$/.test(gap) ? " " : SOURCE_STRING_FRAGMENT_SEPARATOR;
1382
+ }
1383
+ function sourceStringNodeContentForResidualMatching(node, content) {
1384
+ const parts = [];
1385
+ let previousFragmentEnd = null;
1386
+ for (const child of node.children()) {
1387
+ if (child.kind() !== "string_fragment") continue;
1388
+ const range = child.range();
1389
+ if (previousFragmentEnd != null && range.start.index > previousFragmentEnd) parts.push(sourceStringFragmentGapForResidualMatching(content.slice(previousFragmentEnd, range.start.index)));
1390
+ parts.push(child.text());
1391
+ previousFragmentEnd = range.end.index;
1392
+ }
1393
+ return parts.length === 0 ? null : parts.join("");
1394
+ }
1395
+ function sourceStringContentForResidualMatching(relative, content) {
1396
+ const ext = path.extname(relative).toLowerCase();
1397
+ if (!SOURCE_EXTENSIONS.has(ext)) return null;
1398
+ let root;
1399
+ try {
1400
+ root = parse$1(sourceLang(relative), content).root();
1401
+ } catch {
1402
+ return null;
1403
+ }
1404
+ const sourceStrings = [];
1405
+ const visit = (node) => {
1406
+ if (node.kind() === "arguments") {
1407
+ const value = sourceArgumentsCommandContent(node, content);
1408
+ if (value != null) sourceStrings.push(value);
1409
+ }
1410
+ if (node.kind() === "array") {
1411
+ const value = sourceArrayCommandContent(node, content);
1412
+ if (value != null) sourceStrings.push(value);
1413
+ }
1414
+ const kind = node.kind();
1415
+ if (kind === "string" || kind === "template_string") {
1416
+ if (isSourceTailorSdkValueArgument(node, content)) return;
1417
+ const sourceString = sourceStringNodeContentForResidualMatching(node, content);
1418
+ if (sourceString != null) sourceStrings.push(sourceString);
1419
+ }
1420
+ for (const child of node.children()) {
1421
+ if (child.kind() === "string_fragment") continue;
1422
+ visit(child);
1423
+ }
1424
+ };
1425
+ visit(root);
1426
+ return sourceStrings.join(SOURCE_STRING_FRAGMENT_SEPARATOR);
1427
+ }
1428
+ function isConstVariableDeclarator(node) {
1429
+ return node.parent()?.children().some((child) => child.kind() === "const") ?? false;
1430
+ }
1431
+ function sourceTextContentForResidualMatching(relative, content) {
1432
+ const ext = path.extname(relative).toLowerCase();
1433
+ if (!SOURCE_EXTENSIONS.has(ext)) return null;
1434
+ let root;
1435
+ try {
1436
+ root = parse$1(sourceLang(relative), content).root();
1437
+ } catch {
1438
+ return null;
1439
+ }
1440
+ const fragments = [];
1441
+ const visit = (node) => {
1442
+ if (node.kind() === "comment" || node.kind() === "jsx_text") {
1443
+ fragments.push(node.text());
1444
+ return;
1445
+ }
1446
+ for (const child of node.children()) visit(child);
1447
+ };
1448
+ visit(root);
1449
+ return fragments.join(SOURCE_STRING_FRAGMENT_SEPARATOR);
1450
+ }
1451
+ function sourceArgumentsCommandContent(node, source) {
1452
+ const args = sourceArrayElements(node);
1453
+ const executable = args[0] == null ? null : sourceStringLikeNodeContent(args[0], source);
1454
+ const argv = args[1];
1455
+ if (executable == null || argv?.kind() !== "array") return null;
1456
+ const values = sourceArrayCommandValues(argv, source);
1457
+ return values.length === 0 ? null : [executable, ...values].join(" ");
1458
+ }
1459
+ function sourceArrayCommandContent(node, source) {
1460
+ const values = sourceArrayCommandValues(node, source);
1461
+ return values.length < 2 ? null : values.join(" ");
1462
+ }
1463
+ function sourceArrayCommandValues(node, source) {
1464
+ const values = [];
1465
+ for (const element of sourceArrayElements(node)) {
1466
+ if (isSourceValueArgument(element, source)) continue;
1467
+ const value = sourceStringLikeNodeContent(element, source);
1468
+ if (value != null) values.push(value);
1469
+ }
1470
+ return values;
1471
+ }
1472
+ function isSourceTailorSdkValueArgument(fragment, source) {
1473
+ const text = fragment.kind() === "string_fragment" ? fragment.text() : sourceStringLikeNodeContent(fragment, source);
1474
+ return text != null && text.includes("tailor-sdk") && isSourceValueArgument(fragment, source);
1475
+ }
1476
+ function isSyntaxOnlyNode(node) {
1477
+ const kind = node.kind();
1478
+ return kind === "[" || kind === "]" || kind === "(" || kind === ")" || kind === "," || kind === "comment";
1479
+ }
1480
+ function sourceArrayElements(node) {
1481
+ return node.children().filter((child) => !isSyntaxOnlyNode(child));
1482
+ }
1483
+ function nodeRangeKey(node) {
1484
+ const range = node.range();
1485
+ return `${range.start.index}:${range.end.index}`;
1486
+ }
1487
+ function sourceStringLikeNodeContent(node, source) {
1488
+ const directValue = sourceStringNodeContent(node, source);
1489
+ if (directValue != null) return directValue;
1490
+ return node.kind() === "identifier" ? sourceScopedStringVariableContent(node, source) : null;
1491
+ }
1492
+ function sourceScopedStringVariableContent(identifier, source) {
1493
+ const name = identifier.text();
1494
+ const before = identifier.range().start.index;
1495
+ let current = identifier.parent();
1496
+ while (current != null) {
1497
+ if (isSourceScopeNode(current)) {
1498
+ const value = findSourceStringVariableInScope(current, name, before, source);
1499
+ if (value != null) return value;
1500
+ }
1501
+ current = current.parent();
1502
+ }
1503
+ return null;
1504
+ }
1505
+ function findSourceStringVariableInScope(scope, name, before, source) {
1506
+ let value = null;
1507
+ const visit = (node) => {
1508
+ if (node !== scope && isSourceScopeNode(node)) return;
1509
+ if (node.kind() === "variable_declarator" && node.range().end.index < before) {
1510
+ const declarationValue = sourceStringVariableDeclarationValue(node, name, source);
1511
+ if (declarationValue != null) value = declarationValue;
1512
+ return;
1513
+ }
1514
+ for (const child of node.children()) visit(child);
1515
+ };
1516
+ visit(scope);
1517
+ return value;
1518
+ }
1519
+ function sourceStringVariableDeclarationValue(node, name, source) {
1520
+ if (!isConstVariableDeclarator(node)) return null;
1521
+ const children = node.children();
1522
+ if (children.find((child) => child.kind() === "identifier")?.text() !== name) return null;
1523
+ const initializer = children.findLast((child) => sourceConstInitializerContent(child, source) != null);
1524
+ return initializer == null ? null : sourceConstInitializerContent(initializer, source);
1525
+ }
1526
+ function sourceConstInitializerContent(node, source) {
1527
+ const directValue = sourceStringNodeContent(node, source);
1528
+ if (directValue != null) return directValue;
1529
+ if (node.kind() !== "as_expression" && node.kind() !== "satisfies_expression" && node.kind() !== "parenthesized_expression") return null;
1530
+ for (const child of node.children()) {
1531
+ const childValue = sourceConstInitializerContent(child, source);
1532
+ if (childValue != null) return childValue;
1533
+ }
1534
+ return null;
1535
+ }
1536
+ function isSourceScopeNode(node) {
1537
+ const kind = node.kind();
1538
+ return kind === "program" || kind === "statement_block" || kind === "function_declaration" || kind === "arrow_function" || kind === "method_definition";
1539
+ }
1540
+ function sourceStringNodeContent(node, source) {
1541
+ const kind = node.kind();
1542
+ if (kind !== "string" && kind !== "template_string") return null;
1543
+ if (kind === "template_string" && node.children().some((child) => child.kind() === "template_substitution")) return null;
1544
+ const range = node.range();
1545
+ return source.slice(range.start.index + 1, range.end.index - 1);
1546
+ }
1547
+ function isSourceValueArgument(fragment, source) {
1548
+ const stringNode = fragment.kind() === "string_fragment" ? fragment.parent() : fragment;
1549
+ if (stringNode == null) return false;
1550
+ const parent = stringNode.parent();
1551
+ if (parent?.kind() !== "array") return false;
1552
+ const elements = sourceArrayElements(parent);
1553
+ const index = elements.findIndex((element) => nodeRangeKey(element) === nodeRangeKey(stringNode));
1554
+ if (index <= 0) return false;
1555
+ if (!isTailorCliArgumentArray(parent, index, source)) return false;
1556
+ const previous = sourceStringLikeNodeContent(elements[index - 1], source);
1557
+ return previous != null && SOURCE_VALUE_FLAGS.has(previous.split("=", 1)[0]) && !previous.includes("=");
1558
+ }
1559
+ function isTailorCliArgumentArray(arrayNode, index, source) {
1560
+ const argumentsNode = arrayNode.parent();
1561
+ if (argumentsNode?.kind() === "arguments") {
1562
+ const callArgs = sourceArrayElements(argumentsNode);
1563
+ const executable = callArgs[0] == null ? null : sourceStringLikeNodeContent(callArgs[0], source);
1564
+ if (executable != null && SOURCE_CLI_BINARY_RE.test(executable)) return true;
1565
+ }
1566
+ return sourceArrayElements(arrayNode).slice(0, index).some((element) => {
1567
+ const value = sourceStringLikeNodeContent(element, source);
1568
+ return value != null && SOURCE_CLI_BINARY_RE.test(value);
1569
+ });
1570
+ }
1571
+ function sourceLang(relative) {
1572
+ const ext = path.extname(relative).toLowerCase();
1573
+ return ext === ".tsx" || ext === ".jsx" || ext === ".js" ? Lang.Tsx : Lang.TypeScript;
1574
+ }
1575
+ function isProcessEnvSubscriptKey(node) {
1576
+ const stringNode = node.kind() === "string_fragment" ? node.parent() : node;
1577
+ if (stringNode == null) return false;
1578
+ const stringNodeKind = stringNode.kind();
1579
+ if (stringNodeKind !== "string" && stringNodeKind !== "template_string") return false;
1580
+ const parent = stringNode.parent();
1581
+ return parent?.kind() === "subscript_expression" && /^process\.env\s*\[/.test(parent.text());
1582
+ }
1583
+ function collectMaskedRanges(root) {
1584
+ const ranges = [];
1585
+ const visit = (node) => {
1586
+ if (MASKED_SOURCE_NODE_KINDS.has(node.kind())) {
1587
+ if (isProcessEnvSubscriptKey(node)) return;
1588
+ const range = node.range();
1589
+ ranges.push([range.start.index, range.end.index]);
1590
+ return;
1591
+ }
1592
+ for (const child of node.children()) visit(child);
1593
+ };
1594
+ visit(root);
1595
+ return ranges;
1596
+ }
1597
+ function maskSourceNonCode(relative, content) {
1598
+ let ranges;
1599
+ try {
1600
+ ranges = collectMaskedRanges(parse$1(sourceLang(relative), content).root());
1601
+ } catch {
1602
+ return content;
1603
+ }
1604
+ ranges = ranges.toSorted(([a], [b]) => a - b);
1605
+ const chars = content.split("");
1606
+ for (const [start, end] of ranges) for (let i = start; i < end && i < chars.length; i++) if (chars[i] !== "\n" && chars[i] !== "\r") chars[i] = " ";
1607
+ return chars.join("");
1608
+ }
1609
+ function isIdentifierChar(char) {
1610
+ return char != null && /^[A-Za-z0-9_$]$/.test(char);
1611
+ }
1612
+ function matchesPattern(content, pattern) {
1613
+ if (typeof pattern === "string") {
1614
+ const checkLeft = isIdentifierChar(pattern[0]);
1615
+ const checkRight = isIdentifierChar(pattern.at(-1));
1616
+ let index = content.indexOf(pattern);
1617
+ while (index !== -1) {
1618
+ const before = index > 0 ? content[index - 1] : void 0;
1619
+ const after = content[index + pattern.length];
1620
+ if ((!checkLeft || !isIdentifierChar(before)) && (!checkRight || !isIdentifierChar(after))) return true;
1621
+ index = content.indexOf(pattern, index + 1);
1622
+ }
1623
+ return false;
1624
+ }
1625
+ pattern.lastIndex = 0;
1626
+ return pattern.test(content);
1627
+ }
1628
+ function patternLabel(pattern) {
1629
+ return typeof pattern === "string" ? pattern : pattern.toString();
1630
+ }
1631
+ /** Resolve a residual pattern against content, returning its label when matched. */
1632
+ function matchResidualPattern(content, pattern) {
1633
+ if (!Array.isArray(pattern)) return matchesPattern(content, pattern) ? patternLabel(pattern) : null;
1634
+ return pattern.every((p) => matchesPattern(content, p)) ? pattern.map((p) => patternLabel(p)).join(" + ") : null;
1635
+ }
1636
+ function matchResidualPatternFragment(content, pattern) {
1637
+ for (const fragment of content.split(SOURCE_STRING_FRAGMENT_SEPARATOR)) {
1638
+ const label = matchResidualPattern(fragment, pattern);
1639
+ if (label != null) return label;
1640
+ }
1641
+ return null;
1642
+ }
1643
+ function legacyPatternWarnings(relative, content, sourceStringContent, sourceTextContent, transforms) {
1644
+ return transforms.flatMap((lt) => {
1645
+ const found = new Set(lt.legacyPatterns.map((p) => matchResidualPattern(content, p)).filter((label) => label !== null));
1646
+ if (sourceStringContent != null) for (const pattern of lt.sourceStringLegacyPatterns) {
1647
+ const label = matchResidualPatternFragment(sourceStringContent, pattern);
1648
+ if (label != null) found.add(label);
1649
+ }
1650
+ if (sourceTextContent != null) for (const pattern of lt.sourceTextLegacyPatterns) {
1651
+ const label = matchResidualPatternFragment(sourceTextContent, pattern);
1652
+ if (label != null) found.add(label);
1653
+ }
1654
+ if (found.size === 0) return [];
1655
+ return [`${relative}: contains ${Array.from(found).join(", ")} but was not migrated automatically (rule: ${lt.id}). Manual migration may be needed.`];
1656
+ });
1657
+ }
1658
+ function compareReviewFindings(a, b) {
1659
+ return a.file.localeCompare(b.file) || a.line - b.line || a.message.localeCompare(b.message) || a.excerpt.localeCompare(b.excerpt);
178
1660
  }
179
1661
  /**
180
1662
  * Run multiple codemods on a project directory using in-memory chaining.
@@ -191,26 +1673,33 @@ async function runCodemods(codemods, targetPath, dryRun) {
191
1673
  const loaded = [];
192
1674
  for (const { codemod, scriptPath } of codemods) {
193
1675
  const patterns = codemod.filePatterns ?? DEFAULT_FILE_PATTERNS;
1676
+ const loadedModule = scriptPath ? await loadTransformModule(scriptPath) : void 0;
194
1677
  loaded.push({
195
1678
  id: codemod.id,
196
- transform: await loadTransform(scriptPath),
197
- matches: picomatch(patterns),
198
- legacyPatterns: codemod.legacyPatterns ?? []
1679
+ transform: loadedModule?.transform,
1680
+ reviewFindings: loadedModule?.reviewFindings,
1681
+ matches: picomatch(patterns, { dot: true }),
1682
+ legacyPatterns: codemod.legacyPatterns ?? [],
1683
+ sourceStringLegacyPatterns: codemod.sourceStringLegacyPatterns ?? [],
1684
+ sourceTextLegacyPatterns: codemod.sourceTextLegacyPatterns ?? [],
1685
+ suspiciousPatterns: codemod.suspiciousPatterns ?? [],
1686
+ sourceStringSuspiciousPatterns: codemod.sourceStringSuspiciousPatterns ?? [],
1687
+ prompt: codemod.prompt,
1688
+ reviewSupersededBy: codemod.reviewSupersededBy ?? []
199
1689
  });
200
1690
  }
201
- const allPatterns = /* @__PURE__ */ new Set();
202
- for (const { codemod } of codemods) for (const p of codemod.filePatterns ?? DEFAULT_FILE_PATTERNS) allPatterns.add(p);
203
1691
  const filesModified = [];
204
1692
  const warnings = [];
205
1693
  const appliedCodemodIds = /* @__PURE__ */ new Set();
206
1694
  const seen = /* @__PURE__ */ new Set();
207
- for (const pattern of allPatterns) for await (const relative of glob(pattern, {
208
- cwd: targetPath,
209
- exclude: (name) => EXCLUDE_DIRS.has(name)
210
- })) {
1695
+ const suspiciousByCodemod = /* @__PURE__ */ new Map();
1696
+ const findingsByCodemod = /* @__PURE__ */ new Map();
1697
+ for await (const relative of walkFiles(targetPath)) {
211
1698
  const absolute = path.resolve(targetPath, relative);
212
1699
  if (seen.has(absolute)) continue;
213
1700
  seen.add(absolute);
1701
+ const matchedTransforms = loaded.filter((lt) => lt.matches(relative));
1702
+ if (matchedTransforms.length === 0) continue;
214
1703
  let original;
215
1704
  try {
216
1705
  original = await fs.promises.readFile(absolute, "utf-8");
@@ -218,10 +1707,8 @@ async function runCodemods(codemods, targetPath, dryRun) {
218
1707
  continue;
219
1708
  }
220
1709
  let current = original;
221
- const matchedTransforms = [];
222
- for (const lt of loaded) {
223
- if (!lt.matches(relative)) continue;
224
- matchedTransforms.push(lt);
1710
+ for (const lt of matchedTransforms) {
1711
+ if (!lt.transform) continue;
225
1712
  const result = await lt.transform(current, absolute);
226
1713
  if (result != null) {
227
1714
  current = result;
@@ -232,25 +1719,191 @@ async function runCodemods(codemods, targetPath, dryRun) {
232
1719
  filesModified.push(absolute);
233
1720
  if (dryRun) printDiff(absolute, original, current);
234
1721
  else await fs.promises.writeFile(absolute, current, "utf-8");
235
- } else for (const lt of matchedTransforms) {
236
- const found = lt.legacyPatterns.filter((p) => original.includes(p));
237
- if (found.length > 0) warnings.push(`${relative}: contains ${found.join(", ")} but was not migrated automatically (rule: ${lt.id}). Manual migration may be needed.`);
1722
+ }
1723
+ const residualContent = contentForResidualMatching(relative, current);
1724
+ const sourceStringContent = sourceStringContentForResidualMatching(relative, current);
1725
+ const sourceTextContent = sourceTextContentForResidualMatching(relative, current);
1726
+ warnings.push(...legacyPatternWarnings(relative, residualContent, sourceStringContent, sourceTextContent, matchedTransforms));
1727
+ for (const lt of matchedTransforms) {
1728
+ if (!lt.prompt) continue;
1729
+ const filesForReview = () => {
1730
+ let files = suspiciousByCodemod.get(lt.id);
1731
+ if (!files) {
1732
+ files = /* @__PURE__ */ new Set();
1733
+ suspiciousByCodemod.set(lt.id, files);
1734
+ }
1735
+ return files;
1736
+ };
1737
+ if (lt.reviewFindings) {
1738
+ const findings = await lt.reviewFindings(current, absolute, relative);
1739
+ if (findings.length > 0) {
1740
+ const files = filesForReview();
1741
+ for (const finding of findings) files.add(finding.file);
1742
+ let existing = findingsByCodemod.get(lt.id);
1743
+ if (!existing) {
1744
+ existing = [];
1745
+ findingsByCodemod.set(lt.id, existing);
1746
+ }
1747
+ existing.push(...findings);
1748
+ }
1749
+ }
1750
+ if (lt.suspiciousPatterns.some((p) => matchResidualPattern(residualContent, p) !== null) || sourceStringContent != null && lt.sourceStringSuspiciousPatterns.some((p) => matchResidualPattern(sourceStringContent, p) !== null)) filesForReview().add(relative);
238
1751
  }
239
1752
  }
1753
+ const llmReviews = [];
1754
+ const loadedIds = new Set(loaded.map((lt) => lt.id));
1755
+ for (const lt of loaded) {
1756
+ if (!lt.prompt) continue;
1757
+ if (lt.reviewSupersededBy.some((id) => loadedIds.has(id))) continue;
1758
+ if (lt.suspiciousPatterns.length > 0 || lt.sourceStringSuspiciousPatterns.length > 0 || lt.reviewFindings) {
1759
+ const files = suspiciousByCodemod.get(lt.id);
1760
+ if (files) {
1761
+ const findings = findingsByCodemod.get(lt.id)?.toSorted(compareReviewFindings);
1762
+ llmReviews.push({
1763
+ codemodId: lt.id,
1764
+ prompt: lt.prompt,
1765
+ files: Array.from(files).toSorted(),
1766
+ ...findings && findings.length > 0 ? { findings } : {}
1767
+ });
1768
+ }
1769
+ } else if (lt.legacyPatterns.length === 0) llmReviews.push({
1770
+ codemodId: lt.id,
1771
+ prompt: lt.prompt,
1772
+ files: []
1773
+ });
1774
+ }
240
1775
  return {
241
1776
  changed: filesModified.length > 0,
242
1777
  filesModified,
243
1778
  warnings,
244
- appliedCodemodIds
1779
+ appliedCodemodIds,
1780
+ llmReviews
245
1781
  };
246
1782
  }
247
1783
  //#endregion
1784
+ //#region src/runner-metadata.ts
1785
+ const SOURCE_PACKAGE_PATH = "packages/sdk-codemod";
1786
+ const LOCAL_BUILD_COMMAND = "pnpm --dir packages/sdk-codemod build";
1787
+ function createRunnerMetadata({ packageName, packageVersion, packageRoot, readGit = readGitOutput, realpath = safeRealpath }) {
1788
+ const metadata = {
1789
+ packageName,
1790
+ packageVersion
1791
+ };
1792
+ const gitRoot = readGit(packageRoot, ["rev-parse", "--show-toplevel"]);
1793
+ if (!gitRoot) return metadata;
1794
+ if (path.normalize(path.relative(realpath(gitRoot), realpath(packageRoot))).replaceAll("\\", "/") !== SOURCE_PACKAGE_PATH) return metadata;
1795
+ const gitCommit = readGit(packageRoot, [
1796
+ "rev-parse",
1797
+ "--verify",
1798
+ "HEAD"
1799
+ ]);
1800
+ if (!gitCommit) return metadata;
1801
+ return {
1802
+ ...metadata,
1803
+ gitCommit,
1804
+ localBuildCommand: LOCAL_BUILD_COMMAND
1805
+ };
1806
+ }
1807
+ function readGitOutput(cwd, args) {
1808
+ try {
1809
+ return execFileSync("git", [
1810
+ "-C",
1811
+ cwd,
1812
+ ...args
1813
+ ], {
1814
+ encoding: "utf-8",
1815
+ stdio: [
1816
+ "ignore",
1817
+ "pipe",
1818
+ "ignore"
1819
+ ]
1820
+ }).trim() || void 0;
1821
+ } catch {
1822
+ return;
1823
+ }
1824
+ }
1825
+ function safeRealpath(value) {
1826
+ try {
1827
+ return realpathSync(value);
1828
+ } catch {
1829
+ return path.resolve(value);
1830
+ }
1831
+ }
1832
+ //#endregion
248
1833
  //#region src/index.ts
249
- const packageJson = await readPackageJSON(path.dirname(fileURLToPath(import.meta.url)) + "/..");
1834
+ const packageRoot = path.dirname(fileURLToPath(import.meta.url)) + "/..";
1835
+ const packageJson = await readPackageJSON(packageRoot);
1836
+ const packageName = packageJson.name ?? "sdk-codemod";
1837
+ const packageVersion = packageJson.version ?? "0.0.0";
1838
+ const listCommand = defineCommand({
1839
+ name: "list",
1840
+ description: "List the available codemod rules (id, name, kind, version range).",
1841
+ args: z.strictObject({}),
1842
+ run: () => {
1843
+ const rules = allCodemods.map((codemod) => ({
1844
+ id: codemod.id,
1845
+ name: codemod.name,
1846
+ kind: codemod.notice ? "Notice" : automationLevel(codemod),
1847
+ since: codemod.since,
1848
+ until: codemod.until
1849
+ }));
1850
+ for (const rule of rules) process.stderr.write(` ${rule.id} [${rule.kind}] ${rule.name}\n`);
1851
+ process.stdout.write(JSON.stringify(rules) + "\n");
1852
+ }
1853
+ });
1854
+ /**
1855
+ * Print an LLM-assisted review task to stderr: the flagged files plus the
1856
+ * codemod's migration prompt, ready to hand to an LLM for the cases the
1857
+ * deterministic transform could not complete on its own.
1858
+ * @param review - The review task (codemod id, prompt, files)
1859
+ */
1860
+ function printLlmReview(review) {
1861
+ const scope = review.files.length > 0 ? "the codemod cannot safely migrate these automatically" : "review the project for this manual change";
1862
+ process.stderr.write(`\nšŸ¤– LLM-assisted review suggested (${review.codemodId}) — ${scope}:\n`);
1863
+ const findingsByFile = /* @__PURE__ */ new Map();
1864
+ for (const finding of review.findings ?? []) {
1865
+ let findings = findingsByFile.get(finding.file);
1866
+ if (!findings) {
1867
+ findings = [];
1868
+ findingsByFile.set(finding.file, findings);
1869
+ }
1870
+ findings.push(finding);
1871
+ }
1872
+ for (const file of review.files) {
1873
+ process.stderr.write(` - ${file}\n`);
1874
+ for (const finding of findingsByFile.get(file) ?? []) {
1875
+ process.stderr.write(` - line ${finding.line}: ${finding.message}\n`);
1876
+ process.stderr.write(` ${finding.excerpt}\n`);
1877
+ }
1878
+ }
1879
+ process.stderr.write(`\nPrompt for an LLM:\n${review.prompt.trim()}\n`);
1880
+ }
250
1881
  runMain(defineCommand({
251
- name: packageJson.name ?? "sdk-codemod",
1882
+ name: packageName,
252
1883
  description: packageJson.description ?? "Codemod runner for Tailor Platform SDK upgrades",
253
- args: z.object({
1884
+ subCommands: { list: listCommand },
1885
+ notes: `Applies the codemods matching the \`--from\`/\`--to\` version range to the
1886
+ \`--target\` directory, then writes a JSON summary to \`stdout\`:
1887
+
1888
+ - \`filesModified\`: files a codemod changed
1889
+ - \`warnings\`: files that may still need manual migration
1890
+ - \`llmReviews\`: changes the codemods could not fully migrate on their own. Each
1891
+ entry has the affected \`files\`, optional file-local \`findings\`, and a
1892
+ \`prompt\` — hand the prompt and files to an LLM (or follow it yourself) to
1893
+ finish those cases.
1894
+ - \`runner\`: exact codemod runner identity. Local source builds include the
1895
+ repository commit and the build command used to produce \`dist/index.js\`.
1896
+
1897
+ Progress, warnings, and the LLM-review prompts are also printed to \`stderr\` in
1898
+ human-readable form, so \`stdout\` stays pure JSON for piping.`,
1899
+ examples: [{
1900
+ cmd: "--from 1.64.0 --to 2.0.0",
1901
+ desc: "Apply every codemod for the 1.64.0 -> 2.0.0 upgrade to the current project"
1902
+ }, {
1903
+ cmd: "--from 1.64.0 --to 2.0.0 --dry-run",
1904
+ desc: "Preview the changes and any LLM-review prompts without writing files"
1905
+ }],
1906
+ args: z.strictObject({
254
1907
  from: arg(z.string(), { description: "Source SDK version (the version before upgrade)" }),
255
1908
  to: arg(z.string(), { description: "Target SDK version (the version after upgrade)" }),
256
1909
  target: arg(z.string().default("."), { description: "Project directory to transform" }),
@@ -258,17 +1911,24 @@ runMain(defineCommand({
258
1911
  alias: "d",
259
1912
  description: "Preview changes without modifying files"
260
1913
  })
261
- }).strict(),
1914
+ }),
262
1915
  run: async (args) => {
263
1916
  const targetPath = path.resolve(args.target);
264
1917
  const dryRun = args["dry-run"];
1918
+ const runner = createRunnerMetadata({
1919
+ packageName,
1920
+ packageVersion,
1921
+ packageRoot
1922
+ });
265
1923
  const codemods = getApplicableCodemods(args.from, args.to);
266
1924
  const output = {
1925
+ runner,
267
1926
  codemodsApplied: 0,
268
1927
  codemodsSkipped: 0,
269
1928
  filesModified: [],
270
1929
  warnings: [],
271
- errors: []
1930
+ errors: [],
1931
+ llmReviews: []
272
1932
  };
273
1933
  if (codemods.length === 0) {
274
1934
  process.stdout.write(JSON.stringify(output) + "\n");
@@ -276,7 +1936,7 @@ runMain(defineCommand({
276
1936
  }
277
1937
  const codemodEntries = codemods.map((codemod) => ({
278
1938
  codemod,
279
- scriptPath: resolveCodemodScript(codemod.scriptPath)
1939
+ scriptPath: codemod.scriptPath ? resolveCodemodScript(codemod.scriptPath) : void 0
280
1940
  }));
281
1941
  for (const { codemod } of codemodEntries) process.stderr.write(`Running: ${codemod.name} - ${codemod.description}\n`);
282
1942
  try {
@@ -285,8 +1945,10 @@ runMain(defineCommand({
285
1945
  output.codemodsSkipped = codemods.length - result.appliedCodemodIds.size;
286
1946
  output.filesModified = result.filesModified;
287
1947
  output.warnings = result.warnings;
1948
+ output.llmReviews = result.llmReviews;
288
1949
  if (result.changed) process.stderr.write(` ${result.filesModified.length} file(s) modified\n`);
289
1950
  else process.stderr.write(" No changes needed\n");
1951
+ for (const review of output.llmReviews) printLlmReview(review);
290
1952
  } catch (error) {
291
1953
  const message = error instanceof Error ? error.message : String(error);
292
1954
  output.errors.push({
@@ -298,6 +1960,6 @@ runMain(defineCommand({
298
1960
  process.stdout.write(JSON.stringify(output) + "\n");
299
1961
  if (output.errors.length > 0) process.exit(1);
300
1962
  }
301
- }), { version: packageJson.version });
1963
+ }), { version: packageVersion });
302
1964
  //#endregion
303
1965
  export {};