@tailor-platform/sdk-codemod 0.3.0-next.2 → 0.3.0-next.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -5,18 +5,22 @@ 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 { Lang, parse } from "@ast-grep/napi";
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";
14
16
  //#region src/migration-doc.ts
15
17
  /**
16
18
  * Classify how much of a migration the codemod automates.
17
19
  * - `Automatic`: a transform fully covers it, with no residual to flag.
18
20
  * - `Partially automatic`: a transform covers the common cases but flags
19
- * residuals (via `legacyPatterns`/`suspiciousPatterns`/`prompt`) to finish.
21
+ * residuals (via `legacyPatterns`/`sourceStringLegacyPatterns`/
22
+ * `sourceTextLegacyPatterns`/`suspiciousPatterns`/
23
+ * `sourceStringSuspiciousPatterns`/`prompt`) to finish.
20
24
  * - `Manual`: no transform; the change is migrated by hand (optionally guided
21
25
  * by a `prompt`). Whether a person or an LLM does it does not matter here.
22
26
  * @param codemod - The codemod registry entry
@@ -24,11 +28,94 @@ import picomatch from "picomatch";
24
28
  */
25
29
  function automationLevel(codemod) {
26
30
  if (!codemod.scriptPath) return "Manual";
27
- return (codemod.legacyPatterns?.length ?? 0) > 0 || (codemod.suspiciousPatterns?.length ?? 0) > 0 || codemod.prompt != null ? "Partially automatic" : "Automatic";
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";
28
32
  }
29
33
  //#endregion
30
34
  //#region src/registry.ts
31
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";
32
119
  /** All registered codemods, in registration order. */
33
120
  const allCodemods = [
34
121
  {
@@ -37,6 +124,7 @@ const allCodemods = [
37
124
  description: "Migrate defineGenerators() tuple syntax to definePlugins() with explicit plugin imports",
38
125
  since: "1.0.0",
39
126
  until: "2.0.0",
127
+ prereleaseUntil: V2_NEXT_1,
40
128
  scriptPath: "v2/define-generators-to-plugins/scripts/transform.js",
41
129
  legacyPatterns: ["defineGenerators"],
42
130
  examples: [{
@@ -68,6 +156,7 @@ const allCodemods = [
68
156
  description: "Rewrite deprecated plugin re-export imports (kyselyTypePlugin, enumConstantsPlugin, fileUtilsPlugin, seedPlugin) from `@tailor-platform/sdk/cli` to their dedicated plugin subpaths",
69
157
  since: "1.0.0",
70
158
  until: "2.0.0",
159
+ prereleaseUntil: V2_NEXT_1,
71
160
  scriptPath: "v2/plugin-cli-import/scripts/transform.js",
72
161
  examples: [{
73
162
  before: "import { kyselyTypePlugin } from \"@tailor-platform/sdk/cli\";",
@@ -77,9 +166,10 @@ const allCodemods = [
77
166
  {
78
167
  id: "v2/test-run-arg-input",
79
168
  name: "function test-run --arg input unwrap",
80
- description: "Strip the deprecated {input: ...} wrapper from `tailor-sdk function test-run --arg` JSON in scripts and docs",
169
+ description: "Strip the deprecated {input: ...} wrapper from `tailor function test-run --arg` JSON in scripts and docs",
81
170
  since: "1.0.0",
82
171
  until: "2.0.0",
172
+ prereleaseUntil: V2_NEXT_1,
83
173
  scriptPath: "v2/test-run-arg-input/scripts/transform.js",
84
174
  filePatterns: [
85
175
  "**/package.json",
@@ -88,16 +178,17 @@ const allCodemods = [
88
178
  ],
89
179
  examples: [{
90
180
  lang: "sh",
91
- before: "tailor-sdk function test-run resolvers/add.ts --arg '{\"input\":{\"a\":1}}'",
92
- after: "tailor-sdk function test-run resolvers/add.ts --arg '{\"a\":1}'"
181
+ before: "tailor function test-run resolvers/add.ts --arg '{\"input\":{\"a\":1}}'",
182
+ after: "tailor function test-run resolvers/add.ts --arg '{\"a\":1}'"
93
183
  }]
94
184
  },
95
185
  {
96
186
  id: "v2/sdk-skills-shim",
97
- name: "tailor-sdk-skills → tailor-sdk skills install",
98
- description: "Replace deprecated `tailor-sdk-skills` invocations with `tailor-sdk skills install`",
187
+ name: "tailor-sdk-skills → tailor skills add",
188
+ description: "Replace deprecated `tailor-sdk-skills` invocations with `tailor skills add`",
99
189
  since: "1.0.0",
100
190
  until: "2.0.0",
191
+ prereleaseUntil: V2_NEXT_1,
101
192
  scriptPath: "v2/sdk-skills-shim/scripts/transform.js",
102
193
  filePatterns: [
103
194
  "**/package.json",
@@ -108,13 +199,13 @@ const allCodemods = [
108
199
  examples: [{
109
200
  lang: "sh",
110
201
  before: "npx tailor-sdk-skills",
111
- after: "tailor-sdk skills install"
202
+ after: "tailor skills add"
112
203
  }],
113
204
  prompt: [
114
- "The standalone tailor-sdk-skills binary is removed in v2; call the skills install",
115
- "subcommand on the main tailor-sdk CLI instead. Replace any remaining",
205
+ "The standalone tailor-sdk-skills binary is removed in v2; call the skills add",
206
+ "subcommand on the main tailor CLI instead. Replace any remaining",
116
207
  "tailor-sdk-skills invocations the codemod did not rewrite with",
117
- "`tailor-sdk skills install`."
208
+ "`tailor skills add`."
118
209
  ].join("\n")
119
210
  },
120
211
  {
@@ -123,6 +214,7 @@ const allCodemods = [
123
214
  description: "Rename TailorUser/TailorActor/TailorActorType/TailorInvoker to TailorPrincipal, drop unauthenticatedTailorUser, rename resolver body `user` to `caller`, and rename TailorDB callback `user` to `invoker`",
124
215
  since: "1.0.0",
125
216
  until: "2.0.0",
217
+ prereleaseUntil: V2_NEXT_2,
126
218
  scriptPath: "v2/principal-unify/scripts/transform.js",
127
219
  legacyPatterns: [
128
220
  "TailorUser",
@@ -162,12 +254,40 @@ const allCodemods = [
162
254
  "Use TailorPrincipal for the unified user/actor/invoker type."
163
255
  ].join("\n")
164
256
  },
257
+ {
258
+ id: "v2/auth-attributes-rename",
259
+ name: "AttributeMap → Attributes",
260
+ description: "Rename auth attribute module augmentation and related SDK type names from `AttributeMap` to `Attributes`",
261
+ since: "1.0.0",
262
+ until: "2.0.0",
263
+ scriptPath: "v2/auth-attributes-rename/scripts/transform.js",
264
+ legacyPatterns: [
265
+ "AttributeMap",
266
+ "interface AttributeMap",
267
+ "UserAttributeMap",
268
+ "InferredAttributeMap"
269
+ ],
270
+ examples: [{
271
+ caption: "Module augmentation uses `Attributes`:",
272
+ before: "declare module \"@tailor-platform/sdk\" {\n interface AttributeMap {\n role: string;\n }\n}",
273
+ after: "declare module \"@tailor-platform/sdk\" {\n interface Attributes {\n role: string;\n }\n}"
274
+ }],
275
+ prompt: [
276
+ "In Tailor SDK v2, the auth attribute type API is renamed from `AttributeMap`",
277
+ "to `Attributes`; related SDK types are renamed to `UserAttributes` and",
278
+ "`InferredAttributes`. The codemod rewrites SDK imports, re-exports,",
279
+ "namespace-qualified references, import() type references, and module",
280
+ "augmentations. Review any remaining matches manually and leave unrelated",
281
+ "local names or deploy/proto wire field names unchanged."
282
+ ].join("\n")
283
+ },
165
284
  {
166
285
  id: "v2/apply-to-deploy",
167
286
  name: "tailor-sdk apply → tailor-sdk deploy",
168
287
  description: "Rewrite `tailor-sdk apply` invocations in package.json scripts, shell scripts, CI configs, and docs to the canonical v2 `tailor-sdk deploy` command",
169
288
  since: "1.0.0",
170
289
  until: "2.0.0",
290
+ prereleaseUntil: V2_NEXT_1,
171
291
  scriptPath: "v2/apply-to-deploy/scripts/transform.js",
172
292
  filePatterns: [
173
293
  "**/package.json",
@@ -187,6 +307,7 @@ const allCodemods = [
187
307
  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",
188
308
  since: "1.0.0",
189
309
  until: "2.0.0",
310
+ prereleaseUntil: V2_NEXT_1,
190
311
  scriptPath: "v2/cli-rename/scripts/transform.js",
191
312
  filePatterns: [
192
313
  "**/package.json",
@@ -206,12 +327,98 @@ const allCodemods = [
206
327
  "happen to use `--machineuser` alone."
207
328
  ].join("\n")
208
329
  },
330
+ {
331
+ id: "v2/env-var-rename",
332
+ name: "SDK environment variable rename",
333
+ description: "Rewrite unambiguous removed SDK environment variable names to their v2 `TAILOR_*` names and flag generic names for manual review",
334
+ since: "1.0.0",
335
+ until: "2.0.0",
336
+ scriptPath: "v2/env-var-rename/scripts/transform.js",
337
+ filePatterns: [
338
+ "**/package.json",
339
+ "**/.env",
340
+ "**/.env.*",
341
+ "**/*.{env,sh,bash,zsh,yml,yaml,json,md}",
342
+ "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"
343
+ ],
344
+ legacyPatterns: [
345
+ "TAILOR_PLATFORM_SDK_CONFIG_PATH",
346
+ "TAILOR_PLATFORM_SDK_DTS_PATH",
347
+ "TAILOR_PLATFORM_SDK_ALLOW_CI_ID_INJECTION",
348
+ "TAILOR_PLATFORM_SDK_BUILD_ONLY",
349
+ "TAILOR_SDK_OUTPUT_DIR",
350
+ "TAILOR_SDK_SKILLS_SOURCE",
351
+ "TAILOR_SDK_VERSION",
352
+ "PLATFORM_URL",
353
+ "PLATFORM_OAUTH2_CLIENT_ID",
354
+ "TAILOR_ENABLE_INLINE_SOURCEMAP",
355
+ "TAILOR_PLATFORM_QUERY_NEWLINE_ON_ENTER",
356
+ "LOG_LEVEL",
357
+ "TAILOR_TOKEN"
358
+ ],
359
+ sourceStringLegacyPatterns: [
360
+ "PLATFORM_URL",
361
+ "PLATFORM_OAUTH2_CLIENT_ID",
362
+ "LOG_LEVEL"
363
+ ],
364
+ examples: [{
365
+ lang: "sh",
366
+ before: "TAILOR_PLATFORM_SDK_BUILD_ONLY=true tailor-sdk deploy",
367
+ after: "TAILOR_DEPLOY_BUILD_ONLY=true tailor-sdk deploy"
368
+ }, {
369
+ before: "const token = process.env.TAILOR_TOKEN;",
370
+ after: "const token = process.env.TAILOR_PLATFORM_TOKEN;"
371
+ }],
372
+ prompt: [
373
+ "Review any remaining removed SDK environment variable names after the codemod",
374
+ "runs. The codemod intentionally leaves generic names such as `LOG_LEVEL`,",
375
+ "`PLATFORM_URL`, and `PLATFORM_OAUTH2_CLIENT_ID` for manual review because",
376
+ "they can configure non-SDK tools. Replace only actual SDK usages with their",
377
+ "v2 names. If a remaining match is an unrelated local identifier, fixture",
378
+ "label, or historical documentation that intentionally does not configure the",
379
+ "SDK, leave it unchanged."
380
+ ].join("\n")
381
+ },
382
+ {
383
+ id: "v2/auth-invoker-call-unwrap",
384
+ name: "auth.invoker(\"name\") → \"name\"",
385
+ 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.",
386
+ since: "1.0.0",
387
+ until: "2.0.0",
388
+ prereleaseUntil: V2_NEXT_1,
389
+ scriptPath: "v2/auth-invoker-call-unwrap/scripts/transform.js",
390
+ suspiciousPatterns: ["auth.invoker"],
391
+ reviewSupersededBy: ["v2/auth-invoker-unwrap"],
392
+ prompt: [
393
+ "In Tailor SDK v2 the auth.invoker() helper is removed; an invoker is now the",
394
+ "machine user name passed directly as a string. The codemod already rewrote the",
395
+ "statically identified SDK option form authInvoker: auth.invoker(\"name\") to authInvoker: \"name\". These files still contain",
396
+ "auth.invoker(...) calls that need manual review.",
397
+ "",
398
+ "For each remaining auth.invoker(<expr>) call:",
399
+ "1. Replace the whole call with <expr> only where the target option expects a",
400
+ " machine user name string; platform/runtime authInvoker payloads still expect",
401
+ " the object form.",
402
+ "2. Keep the authInvoker key when targeting SDK versions before the invoker",
403
+ " option rename; later v2 targets run a separate codemod for that key rename.",
404
+ "3. After removing every auth.invoker usage in a file, delete the now-unused auth",
405
+ " import (keeping it pulls Node-only config modules into runtime bundles); leave",
406
+ " the import if auth is still referenced elsewhere.",
407
+ "",
408
+ "Do not change behavior beyond the auth.invoker() removal."
409
+ ].join("\n"),
410
+ examples: [{
411
+ before: "createResolver({ authInvoker: auth.invoker(\"manager\") });",
412
+ after: "createResolver({ authInvoker: \"manager\" });"
413
+ }]
414
+ },
209
415
  {
210
416
  id: "v2/auth-invoker-unwrap",
211
417
  name: "auth.invoker(\"name\") → invoker: \"name\"",
212
418
  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 `.trigger()` 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.",
213
419
  since: "1.0.0",
214
420
  until: "2.0.0",
421
+ prereleaseUntil: V2_NEXT_2,
215
422
  scriptPath: "v2/auth-invoker-unwrap/scripts/transform.js",
216
423
  suspiciousPatterns: [
217
424
  "auth.invoker",
@@ -254,12 +461,46 @@ const allCodemods = [
254
461
  after: "createResolver({ invoker: \"manager\" });"
255
462
  }]
256
463
  },
464
+ {
465
+ id: "v2/auth-connection-token-helper",
466
+ name: "auth.getConnectionToken() → runtime authconnection",
467
+ 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.",
468
+ since: "1.0.0",
469
+ until: "2.0.0",
470
+ prereleaseUntil: V2_NEXT_2,
471
+ scriptPath: "v2/auth-connection-token-helper/scripts/transform.js",
472
+ filePatterns: ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"],
473
+ examples: [{
474
+ before: "import { auth } from \"../tailor.config\";\n\nconst token = await auth.getConnectionToken(\"google\");",
475
+ after: "import { authconnection } from \"@tailor-platform/sdk/runtime\";\n\nconst token = await authconnection.getConnectionToken(\"google\");"
476
+ }],
477
+ prompt: [
478
+ "In Tailor SDK v2 the auth.getConnectionToken() helper returned by defineAuth()",
479
+ "is removed. Runtime code should call authconnection.getConnectionToken(...) from",
480
+ "@tailor-platform/sdk/runtime instead of importing auth from tailor.config.ts.",
481
+ "",
482
+ "For each getConnectionToken usage where <receiver> is a defineAuth() result",
483
+ "imported from tailor.config.ts:",
484
+ "1. Replace <receiver>.getConnectionToken(<expr>) calls with",
485
+ " authconnection.getConnectionToken(<expr>).",
486
+ "2. Update non-call references, including <receiver>.getConnectionToken,",
487
+ " <receiver>[\"getConnectionToken\"], and destructuring from <receiver>, to",
488
+ " reference authconnection instead.",
489
+ "3. Add or reuse `import { authconnection } from \"@tailor-platform/sdk/runtime\"`.",
490
+ "4. Remove the auth import from tailor.config.ts only when no other auth reference",
491
+ " remains in the file.",
492
+ "",
493
+ "Leave usages unchanged when the receiver is already the runtime authconnection",
494
+ "wrapper or global tailor.authconnection."
495
+ ].join("\n")
496
+ },
257
497
  {
258
498
  id: "v2/tailordb-namespace",
259
499
  name: "Tailordb → tailordb (lowercase ambient namespace)",
260
500
  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\"`.",
261
501
  since: "1.0.0",
262
502
  until: "2.0.0",
503
+ prereleaseUntil: V2_NEXT_1,
263
504
  scriptPath: "v2/tailordb-namespace/scripts/transform.js",
264
505
  legacyPatterns: ["Tailordb."],
265
506
  examples: [{
@@ -283,6 +524,7 @@ const allCodemods = [
283
524
  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.",
284
525
  since: "1.0.0",
285
526
  until: "2.0.0",
527
+ prereleaseUntil: V2_NEXT_2,
286
528
  scriptPath: "v2/execute-script-arg/scripts/transform.js",
287
529
  filePatterns: ["**/*.{ts,tsx,mts,cts,mjs,cjs,js}"],
288
530
  suspiciousPatterns: [[
@@ -309,12 +551,26 @@ const allCodemods = [
309
551
  after: "await executeScript({ ...opts, arg: { a: 1 } });"
310
552
  }]
311
553
  },
554
+ {
555
+ id: "v2/wait-point-rename",
556
+ name: "defineWaitPoint/defineWaitPoints → createWaitPoint/createWaitPoints",
557
+ 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.",
558
+ since: "1.0.0",
559
+ until: "2.0.0",
560
+ scriptPath: "v2/wait-point-rename/scripts/transform.js",
561
+ legacyPatterns: ["defineWaitPoint", "defineWaitPoints"],
562
+ examples: [{
563
+ before: "import { defineWaitPoints } from \"@tailor-platform/sdk\";\n\nexport const { approval } = defineWaitPoints((define) => ({\n approval: define<{ message: string }, { approved: boolean }>(),\n}));",
564
+ after: "import { createWaitPoints } from \"@tailor-platform/sdk\";\n\nexport const { approval } = createWaitPoints((define) => ({\n approval: define<{ message: string }, { approved: boolean }>(),\n}));"
565
+ }]
566
+ },
312
567
  {
313
568
  id: "v2/open-download-stream",
314
569
  name: "openDownloadStream → downloadStream",
315
570
  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.",
316
571
  since: "1.0.0",
317
572
  until: "2.0.0",
573
+ prereleaseUntil: V2_NEXT_2,
318
574
  filePatterns: ["**/*.{ts,tsx,mts,cts,mjs,cjs,js}"],
319
575
  suspiciousPatterns: ["openDownloadStream", "openFileDownloadStream"],
320
576
  examples: [{
@@ -331,14 +587,18 @@ const allCodemods = [
331
587
  {
332
588
  id: "v2/runtime-globals-opt-in",
333
589
  name: "Ambient runtime globals are opt-in",
334
- description: "Importing `@tailor-platform/sdk` no longer activates the ambient `tailor.*` / `tailordb.*` global declarations. Normal SDK development does not need them — use the SDK APIs and the typed wrappers from `@tailor-platform/sdk/runtime`. 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.)",
590
+ 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.)",
335
591
  since: "1.0.0",
336
592
  until: "2.0.0",
337
- filePatterns: ["**/*.{ts,tsx,mts,cts}"],
593
+ prereleaseUntil: V2_NEXT_1,
594
+ scriptPath: "v2/runtime-globals-opt-in/scripts/transform.js",
595
+ filePatterns: ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"],
338
596
  suspiciousPatterns: [
339
597
  "tailor.context",
340
598
  "tailor.iconv",
341
599
  "tailor.idp",
600
+ "tailor.secretmanager",
601
+ "tailor.authconnection",
342
602
  "tailor.workflow",
343
603
  "tailor[",
344
604
  "tailordb.Client",
@@ -351,6 +611,21 @@ const allCodemods = [
351
611
  "TailorErrorMessage",
352
612
  "TailorErrors"
353
613
  ],
614
+ sourceStringSuspiciousPatterns: [
615
+ "new tailor.idp.Client",
616
+ /[=(:,[]\s*tailor\.idp\.Client\b/,
617
+ /(?:(?:=>|[=(:,<{]|\[)\s*|\b(?:return|await|typeof)\s+)tailor\.(?:authconnection|context|iconv|idp|secretmanager|workflow)(?:\.[A-Za-z_$][\w$]*)?\b/,
618
+ /\btailor\.(?:authconnection|context|iconv|idp|secretmanager|workflow)\.[A-Za-z_$][\w$]*\s*\(/,
619
+ "tailor[",
620
+ /\btailordb\.file\.[A-Za-z_$][\w$]*\s*\(/,
621
+ /(?:(?:=>|[=(:,<{]|\[)\s*|\b(?:return|await|typeof)\s+)tailordb\.file\b/,
622
+ /(?:\bnew\s+|(?:=>|[=(:,<{]|\[)\s*|\b(?:return|await|typeof)\s+)tailordb\.(?:Client|CommandType|QueryResult)\b/,
623
+ /<\s*tailordb\.(?:Client|CommandType|QueryResult)\b/,
624
+ "tailordb[",
625
+ /(?:\bnew\s+|\bthrow\s+|\binstanceof\s+)Tailor(?:DBFileError|Errors|ErrorMessage)\b/,
626
+ /(?:[:=<]\s*|\bas\s+)Tailor(?:DBFileError|Errors|ErrorMessage|ErrorItem)\b/,
627
+ /[:<]\s*TailorErrorItem\b/
628
+ ],
354
629
  examples: [{
355
630
  caption: "Preferred: switch to the typed wrappers from `@tailor-platform/sdk/runtime` and drop the ambient globals:",
356
631
  before: "const client = new tailor.idp.Client();",
@@ -364,10 +639,11 @@ const allCodemods = [
364
639
  "The v2 SDK no longer enables ambient Tailor runtime globals from",
365
640
  "`@tailor-platform/sdk`. For each flagged file that uses `tailor.*`,",
366
641
  "`tailordb.*`, or Tailor runtime error globals, prefer migrating to the",
367
- "typed wrappers from `@tailor-platform/sdk/runtime` (e.g. replace",
368
- "`new tailor.idp.Client()` with `import { idp } from \"@tailor-platform/sdk/runtime\"`",
369
- "and `new idp.Client({ namespace })`). The wrappers are self-contained, so the",
370
- "ambient globals are no longer needed.",
642
+ "typed wrappers from `@tailor-platform/sdk/runtime`. The codemod already",
643
+ "rewrites direct `new tailor.idp.Client(...)` calls to `new idp.Client(...)`",
644
+ "when the file has no conflicting `tailor` or `idp` binding. For any remaining",
645
+ "`tailor.idp.Client` references, either resolve the binding collision and use",
646
+ "`idp.Client`, or keep the ambient global deliberately.",
371
647
  "",
372
648
  "Only when the file must keep referencing the bare `tailor.*` names directly,",
373
649
  "opt into the global declarations instead by adding one of these:",
@@ -376,7 +652,9 @@ const allCodemods = [
376
652
  " the relevant tsconfig compilerOptions",
377
653
  "",
378
654
  "Leave files unchanged when the matching name is local, imported from another",
379
- "module, or appears only in comments or strings."
655
+ "module, or appears only in comments or prose strings. Embedded code strings",
656
+ "that use runtime globals are review-only findings; do not insert imports inside",
657
+ "string literals."
380
658
  ].join("\n")
381
659
  },
382
660
  {
@@ -385,6 +663,7 @@ const allCodemods = [
385
663
  description: "Workflow job `.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 trigger responses with `mockWorkflow()` (`setJobHandler` / `enqueueResult`, assert via `triggeredJobs`), or use `runWorkflowLocally()` for a full-chain local run.",
386
664
  since: "1.0.0",
387
665
  until: "2.0.0",
666
+ prereleaseUntil: V2_NEXT_1,
388
667
  suspiciousPatterns: [".trigger("],
389
668
  examples: [{
390
669
  caption: "Tests must mock the workflow runtime instead of running bodies locally:",
@@ -405,6 +684,7 @@ const allCodemods = [
405
684
  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.",
406
685
  since: "1.0.0",
407
686
  until: "2.0.0",
687
+ prereleaseUntil: V2_NEXT_2,
408
688
  notice: true
409
689
  },
410
690
  {
@@ -413,12 +693,87 @@ const allCodemods = [
413
693
  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.",
414
694
  since: "1.0.0",
415
695
  until: "2.0.0",
696
+ prereleaseUntil: V2_NEXT_1,
416
697
  notice: true
417
698
  },
418
699
  {
419
700
  id: "v2/function-logs-content-hash",
420
701
  name: "function logs require a content hash for source mapping",
421
- description: "`tailor-sdk 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.",
702
+ 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.",
703
+ since: "1.0.0",
704
+ until: "2.0.0",
705
+ prereleaseUntil: V2_NEXT_1,
706
+ notice: true
707
+ },
708
+ {
709
+ id: "v2/rename-bin",
710
+ name: "tailor-sdk binary → tailor",
711
+ 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.",
712
+ since: "1.0.0",
713
+ until: "2.0.0",
714
+ scriptPath: "v2/rename-bin/scripts/transform.js",
715
+ filePatterns: [
716
+ "**/package.json",
717
+ "**/*.{sh,bash,zsh,yml,yaml}",
718
+ "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}",
719
+ "**/*.md"
720
+ ],
721
+ legacyPatterns: ["tailor-sdk"],
722
+ sourceStringLegacyPatterns: [
723
+ RENAME_BIN_SOURCE_LEGACY_PATTERN,
724
+ RENAME_BIN_QUOTED_SOURCE_LEGACY_PATTERN,
725
+ RENAME_BIN_QUOTED_LEGACY_COMMAND_PATTERN
726
+ ],
727
+ sourceTextLegacyPatterns: [
728
+ RENAME_BIN_SOURCE_LEGACY_PATTERN,
729
+ RENAME_BIN_QUOTED_SOURCE_LEGACY_PATTERN,
730
+ RENAME_BIN_QUOTED_LEGACY_COMMAND_PATTERN
731
+ ],
732
+ examples: [{
733
+ lang: "sh",
734
+ before: "tailor-sdk deploy\nnpx tailor-sdk@latest login",
735
+ after: "tailor deploy\nnpx @tailor-platform/sdk@latest login"
736
+ }],
737
+ prompt: [
738
+ "Rename any remaining `tailor-sdk` binary invocations to `tailor`. Only rewrite",
739
+ "the binary name — leave `.tailor-sdk` directory paths and `create-tailor-sdk`",
740
+ "package references unchanged."
741
+ ].join("\n")
742
+ },
743
+ {
744
+ id: "v2/tailor-output-ignore-dir",
745
+ name: ".tailor-sdk ignore entries → .tailor",
746
+ 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.",
747
+ since: "1.0.0",
748
+ until: "2.0.0",
749
+ scriptPath: "v2/tailor-output-ignore-dir/scripts/transform.js",
750
+ filePatterns: [
751
+ "**/.gitignore",
752
+ "**/.npmignore",
753
+ "**/.dockerignore",
754
+ "**/gitignore",
755
+ "**/npmignore",
756
+ "**/dockerignore",
757
+ "**/_gitignore",
758
+ "**/_npmignore",
759
+ "**/_dockerignore",
760
+ "**/__dot__gitignore",
761
+ "**/__dot__npmignore",
762
+ "**/__dot__dockerignore",
763
+ "**/*.gitignore",
764
+ "**/*.npmignore",
765
+ "**/*.dockerignore"
766
+ ],
767
+ examples: [{
768
+ lang: "gitignore",
769
+ before: ".tailor-sdk/",
770
+ after: ".tailor/"
771
+ }]
772
+ },
773
+ {
774
+ id: "v2/node-minimum-22-15-0",
775
+ name: "Node.js minimum version raised to 22.15.0",
776
+ 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+.",
422
777
  since: "1.0.0",
423
778
  until: "2.0.0",
424
779
  notice: true
@@ -432,9 +787,32 @@ const allCodemods = [
432
787
  function resolveCodemodScript(scriptPath) {
433
788
  return path.resolve(CODEMODS_ROOT, scriptPath);
434
789
  }
790
+ function reachesCodemodBoundary(toVersion, codemod) {
791
+ if (gte(toVersion, codemod.until)) return true;
792
+ if (codemod.prereleaseUntil === void 0 || !gte(toVersion, codemod.prereleaseUntil)) return false;
793
+ const target = parse(toVersion);
794
+ const boundary = parse(codemod.until);
795
+ return target.prerelease.length > 0 && target.major === boundary.major && target.minor === boundary.minor && target.patch === boundary.patch;
796
+ }
797
+ function effectiveCodemodBoundary(codemod) {
798
+ return codemod.prereleaseUntil ?? codemod.until;
799
+ }
800
+ function assertCodemodBoundaries(codemods) {
801
+ for (const codemod of codemods) {
802
+ const boundary = parse(codemod.until);
803
+ if (boundary === null) throw new Error(`Codemod ${codemod.id} until must be a valid semver version: ${codemod.until}`);
804
+ if (boundary.prerelease.length > 0) throw new Error(`Codemod ${codemod.id} until must be a stable version: ${codemod.until}`);
805
+ if (codemod.prereleaseUntil === void 0) continue;
806
+ const prereleaseBoundary = parse(codemod.prereleaseUntil);
807
+ if (prereleaseBoundary === null) throw new Error(`Codemod ${codemod.id} prereleaseUntil must be a valid semver version: ${codemod.prereleaseUntil}`);
808
+ if (prereleaseBoundary.prerelease.length === 0) throw new Error(`Codemod ${codemod.id} prereleaseUntil must be a prerelease version: ${codemod.prereleaseUntil}`);
809
+ 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}`);
810
+ }
811
+ }
435
812
  /**
436
813
  * Get codemod packages applicable for a version range.
437
- * A codemod applies when: since <= fromVersion < until <= toVersion
814
+ * A codemod applies when: since <= fromVersion < boundary <= toVersion.
815
+ * A target prerelease reaches `until` only when the codemod declares `prereleaseUntil`.
438
816
  * @param fromVersion - Current SDK version (semver)
439
817
  * @param toVersion - Target SDK version (semver)
440
818
  * @returns Array of applicable codemod packages in registration order
@@ -442,20 +820,21 @@ function resolveCodemodScript(scriptPath) {
442
820
  function getApplicableCodemods(fromVersion, toVersion) {
443
821
  if (!valid(fromVersion)) throw new Error(`Invalid fromVersion: ${fromVersion}`);
444
822
  if (!valid(toVersion)) throw new Error(`Invalid toVersion: ${toVersion}`);
445
- return allCodemods.filter((codemod) => gte(fromVersion, codemod.since) && lt(fromVersion, codemod.until) && gte(toVersion, codemod.until));
823
+ assertCodemodBoundaries(allCodemods);
824
+ return allCodemods.filter((codemod) => gte(fromVersion, codemod.since) && lt(fromVersion, effectiveCodemodBoundary(codemod)) && reachesCodemodBoundary(toVersion, codemod));
446
825
  }
447
826
  //#endregion
448
827
  //#region src/runner.ts
449
828
  /** Default file patterns for TypeScript files. */
450
829
  const DEFAULT_FILE_PATTERNS = ["**/*.{ts,tsx,mts,cts}"];
451
830
  /** Directory names always excluded from file scanning. */
452
- const EXCLUDE_DIRS = new Set([
831
+ const EXCLUDE_DIRS = /* @__PURE__ */ new Set([
453
832
  "node_modules",
454
833
  "dist",
455
834
  ".git"
456
835
  ]);
457
- const ALLOWED_DOT_DIRS = new Set([".github", ".circleci"]);
458
- const SOURCE_EXTENSIONS = new Set([
836
+ const ALLOWED_DOT_DIRS = /* @__PURE__ */ new Set([".github", ".circleci"]);
837
+ const SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
459
838
  ".ts",
460
839
  ".tsx",
461
840
  ".mts",
@@ -465,12 +844,36 @@ const SOURCE_EXTENSIONS = new Set([
465
844
  ".mjs",
466
845
  ".cjs"
467
846
  ]);
468
- const MASKED_SOURCE_NODE_KINDS = new Set([
847
+ const SOURCE_STRING_FRAGMENT_SEPARATOR = "\0";
848
+ const MASKED_SOURCE_NODE_KINDS = /* @__PURE__ */ new Set([
469
849
  "comment",
470
850
  "string",
471
851
  "regex",
472
- "string_fragment"
852
+ "string_fragment",
853
+ "jsx_text"
854
+ ]);
855
+ const SOURCE_VALUE_FLAGS = /* @__PURE__ */ new Set([
856
+ "--env-file-if-exists",
857
+ "--env-file",
858
+ "--profile",
859
+ "--config",
860
+ "--workspace-id",
861
+ "--arg",
862
+ "--query",
863
+ "--file",
864
+ "--name",
865
+ "--namespace",
866
+ "--dir",
867
+ "-e",
868
+ "-p",
869
+ "-c",
870
+ "-w",
871
+ "-a",
872
+ "-q",
873
+ "-f",
874
+ "-n"
473
875
  ]);
876
+ const SOURCE_CLI_BINARY_RE = /^(?:(?:.*[\\/])?tailor(?:\.(?:cmd|ps1|exe))?|(?:.*[\\/])?tailor-sdk(?:@[^\s'"`;|&)]+)?(?:\.(?:cmd|ps1|exe))?|@tailor-platform\/sdk(?:@[^\s'"`;|&)]+)?)$/;
474
877
  function shouldSkipDirectory(name) {
475
878
  return EXCLUDE_DIRS.has(name) || name.startsWith(".") && !ALLOWED_DOT_DIRS.has(name);
476
879
  }
@@ -514,25 +917,229 @@ function printDiff(filePath, before, after) {
514
917
  * Load a transform module from a TypeScript file path.
515
918
  * Expects the module to have a default export that is a TransformFn.
516
919
  * @param scriptPath - Absolute path to the transform script
517
- * @returns The transform function
920
+ * @returns The transform function and optional review detector
518
921
  */
519
- async function loadTransform(scriptPath) {
922
+ async function loadTransformModule(scriptPath) {
520
923
  const mod = await import(url.pathToFileURL(scriptPath).href);
521
924
  if (typeof mod.default !== "function") throw new Error(`Transform at ${scriptPath} does not have a default export function`);
522
- return mod.default;
925
+ return {
926
+ transform: mod.default,
927
+ reviewFindings: typeof mod.reviewFindings === "function" ? mod.reviewFindings : void 0
928
+ };
523
929
  }
524
930
  function contentForResidualMatching(relative, content) {
525
931
  const ext = path.extname(relative).toLowerCase();
526
932
  return SOURCE_EXTENSIONS.has(ext) ? maskSourceNonCode(relative, content) : content;
527
933
  }
934
+ function sourceStringFragmentGapForResidualMatching(gap) {
935
+ if (/^\\["']$/.test(gap)) return gap.slice(1);
936
+ return /^(?:\\(?:[nrtvf]|\r\n|\r|\n)|\s)+$/.test(gap) ? " " : SOURCE_STRING_FRAGMENT_SEPARATOR;
937
+ }
938
+ function sourceStringNodeContentForResidualMatching(node, content) {
939
+ const parts = [];
940
+ let previousFragmentEnd = null;
941
+ for (const child of node.children()) {
942
+ if (child.kind() !== "string_fragment") continue;
943
+ const range = child.range();
944
+ if (previousFragmentEnd != null && range.start.index > previousFragmentEnd) parts.push(sourceStringFragmentGapForResidualMatching(content.slice(previousFragmentEnd, range.start.index)));
945
+ parts.push(child.text());
946
+ previousFragmentEnd = range.end.index;
947
+ }
948
+ return parts.length === 0 ? null : parts.join("");
949
+ }
950
+ function sourceStringContentForResidualMatching(relative, content) {
951
+ const ext = path.extname(relative).toLowerCase();
952
+ if (!SOURCE_EXTENSIONS.has(ext)) return null;
953
+ let root;
954
+ try {
955
+ root = parse$1(sourceLang(relative), content).root();
956
+ } catch {
957
+ return null;
958
+ }
959
+ const sourceStrings = [];
960
+ const visit = (node) => {
961
+ if (node.kind() === "arguments") {
962
+ const value = sourceArgumentsCommandContent(node, content);
963
+ if (value != null) sourceStrings.push(value);
964
+ }
965
+ if (node.kind() === "array") {
966
+ const value = sourceArrayCommandContent(node, content);
967
+ if (value != null) sourceStrings.push(value);
968
+ }
969
+ const kind = node.kind();
970
+ if (kind === "string" || kind === "template_string") {
971
+ if (isSourceTailorSdkValueArgument(node, content)) return;
972
+ const sourceString = sourceStringNodeContentForResidualMatching(node, content);
973
+ if (sourceString != null) sourceStrings.push(sourceString);
974
+ }
975
+ for (const child of node.children()) {
976
+ if (child.kind() === "string_fragment") continue;
977
+ visit(child);
978
+ }
979
+ };
980
+ visit(root);
981
+ return sourceStrings.join(SOURCE_STRING_FRAGMENT_SEPARATOR);
982
+ }
983
+ function isConstVariableDeclarator(node) {
984
+ return node.parent()?.children().some((child) => child.kind() === "const") ?? false;
985
+ }
986
+ function sourceTextContentForResidualMatching(relative, content) {
987
+ const ext = path.extname(relative).toLowerCase();
988
+ if (!SOURCE_EXTENSIONS.has(ext)) return null;
989
+ let root;
990
+ try {
991
+ root = parse$1(sourceLang(relative), content).root();
992
+ } catch {
993
+ return null;
994
+ }
995
+ const fragments = [];
996
+ const visit = (node) => {
997
+ if (node.kind() === "comment" || node.kind() === "jsx_text") {
998
+ fragments.push(node.text());
999
+ return;
1000
+ }
1001
+ for (const child of node.children()) visit(child);
1002
+ };
1003
+ visit(root);
1004
+ return fragments.join(SOURCE_STRING_FRAGMENT_SEPARATOR);
1005
+ }
1006
+ function sourceArgumentsCommandContent(node, source) {
1007
+ const args = sourceArrayElements(node);
1008
+ const executable = args[0] == null ? null : sourceStringLikeNodeContent(args[0], source);
1009
+ const argv = args[1];
1010
+ if (executable == null || argv?.kind() !== "array") return null;
1011
+ const values = sourceArrayCommandValues(argv, source);
1012
+ return values.length === 0 ? null : [executable, ...values].join(" ");
1013
+ }
1014
+ function sourceArrayCommandContent(node, source) {
1015
+ const values = sourceArrayCommandValues(node, source);
1016
+ return values.length < 2 ? null : values.join(" ");
1017
+ }
1018
+ function sourceArrayCommandValues(node, source) {
1019
+ const values = [];
1020
+ for (const element of sourceArrayElements(node)) {
1021
+ if (isSourceValueArgument(element, source)) continue;
1022
+ const value = sourceStringLikeNodeContent(element, source);
1023
+ if (value != null) values.push(value);
1024
+ }
1025
+ return values;
1026
+ }
1027
+ function isSourceTailorSdkValueArgument(fragment, source) {
1028
+ const text = fragment.kind() === "string_fragment" ? fragment.text() : sourceStringLikeNodeContent(fragment, source);
1029
+ return text != null && text.includes("tailor-sdk") && isSourceValueArgument(fragment, source);
1030
+ }
1031
+ function isSyntaxOnlyNode(node) {
1032
+ const kind = node.kind();
1033
+ return kind === "[" || kind === "]" || kind === "(" || kind === ")" || kind === "," || kind === "comment";
1034
+ }
1035
+ function sourceArrayElements(node) {
1036
+ return node.children().filter((child) => !isSyntaxOnlyNode(child));
1037
+ }
1038
+ function nodeRangeKey(node) {
1039
+ const range = node.range();
1040
+ return `${range.start.index}:${range.end.index}`;
1041
+ }
1042
+ function sourceStringLikeNodeContent(node, source) {
1043
+ const directValue = sourceStringNodeContent(node, source);
1044
+ if (directValue != null) return directValue;
1045
+ return node.kind() === "identifier" ? sourceScopedStringVariableContent(node, source) : null;
1046
+ }
1047
+ function sourceScopedStringVariableContent(identifier, source) {
1048
+ const name = identifier.text();
1049
+ const before = identifier.range().start.index;
1050
+ let current = identifier.parent();
1051
+ while (current != null) {
1052
+ if (isSourceScopeNode(current)) {
1053
+ const value = findSourceStringVariableInScope(current, name, before, source);
1054
+ if (value != null) return value;
1055
+ }
1056
+ current = current.parent();
1057
+ }
1058
+ return null;
1059
+ }
1060
+ function findSourceStringVariableInScope(scope, name, before, source) {
1061
+ let value = null;
1062
+ const visit = (node) => {
1063
+ if (node !== scope && isSourceScopeNode(node)) return;
1064
+ if (node.kind() === "variable_declarator" && node.range().end.index < before) {
1065
+ const declarationValue = sourceStringVariableDeclarationValue(node, name, source);
1066
+ if (declarationValue != null) value = declarationValue;
1067
+ return;
1068
+ }
1069
+ for (const child of node.children()) visit(child);
1070
+ };
1071
+ visit(scope);
1072
+ return value;
1073
+ }
1074
+ function sourceStringVariableDeclarationValue(node, name, source) {
1075
+ if (!isConstVariableDeclarator(node)) return null;
1076
+ const children = node.children();
1077
+ if (children.find((child) => child.kind() === "identifier")?.text() !== name) return null;
1078
+ const initializer = children.findLast((child) => sourceConstInitializerContent(child, source) != null);
1079
+ return initializer == null ? null : sourceConstInitializerContent(initializer, source);
1080
+ }
1081
+ function sourceConstInitializerContent(node, source) {
1082
+ const directValue = sourceStringNodeContent(node, source);
1083
+ if (directValue != null) return directValue;
1084
+ if (node.kind() !== "as_expression" && node.kind() !== "satisfies_expression" && node.kind() !== "parenthesized_expression") return null;
1085
+ for (const child of node.children()) {
1086
+ const childValue = sourceConstInitializerContent(child, source);
1087
+ if (childValue != null) return childValue;
1088
+ }
1089
+ return null;
1090
+ }
1091
+ function isSourceScopeNode(node) {
1092
+ const kind = node.kind();
1093
+ return kind === "program" || kind === "statement_block" || kind === "function_declaration" || kind === "arrow_function" || kind === "method_definition";
1094
+ }
1095
+ function sourceStringNodeContent(node, source) {
1096
+ const kind = node.kind();
1097
+ if (kind !== "string" && kind !== "template_string") return null;
1098
+ if (kind === "template_string" && node.children().some((child) => child.kind() === "template_substitution")) return null;
1099
+ const range = node.range();
1100
+ return source.slice(range.start.index + 1, range.end.index - 1);
1101
+ }
1102
+ function isSourceValueArgument(fragment, source) {
1103
+ const stringNode = fragment.kind() === "string_fragment" ? fragment.parent() : fragment;
1104
+ if (stringNode == null) return false;
1105
+ const parent = stringNode.parent();
1106
+ if (parent?.kind() !== "array") return false;
1107
+ const elements = sourceArrayElements(parent);
1108
+ const index = elements.findIndex((element) => nodeRangeKey(element) === nodeRangeKey(stringNode));
1109
+ if (index <= 0) return false;
1110
+ if (!isTailorCliArgumentArray(parent, index, source)) return false;
1111
+ const previous = sourceStringLikeNodeContent(elements[index - 1], source);
1112
+ return previous != null && SOURCE_VALUE_FLAGS.has(previous.split("=", 1)[0]) && !previous.includes("=");
1113
+ }
1114
+ function isTailorCliArgumentArray(arrayNode, index, source) {
1115
+ const argumentsNode = arrayNode.parent();
1116
+ if (argumentsNode?.kind() === "arguments") {
1117
+ const callArgs = sourceArrayElements(argumentsNode);
1118
+ const executable = callArgs[0] == null ? null : sourceStringLikeNodeContent(callArgs[0], source);
1119
+ if (executable != null && SOURCE_CLI_BINARY_RE.test(executable)) return true;
1120
+ }
1121
+ return sourceArrayElements(arrayNode).slice(0, index).some((element) => {
1122
+ const value = sourceStringLikeNodeContent(element, source);
1123
+ return value != null && SOURCE_CLI_BINARY_RE.test(value);
1124
+ });
1125
+ }
528
1126
  function sourceLang(relative) {
529
1127
  const ext = path.extname(relative).toLowerCase();
530
- return ext === ".tsx" || ext === ".jsx" ? Lang.Tsx : Lang.TypeScript;
1128
+ return ext === ".tsx" || ext === ".jsx" || ext === ".js" ? Lang.Tsx : Lang.TypeScript;
1129
+ }
1130
+ function isProcessEnvSubscriptKey(node) {
1131
+ const stringNode = node.kind() === "string_fragment" ? node.parent() : node;
1132
+ if (stringNode == null) return false;
1133
+ const stringNodeKind = stringNode.kind();
1134
+ if (stringNodeKind !== "string" && stringNodeKind !== "template_string") return false;
1135
+ const parent = stringNode.parent();
1136
+ return parent?.kind() === "subscript_expression" && /^process\.env\s*\[/.test(parent.text());
531
1137
  }
532
1138
  function collectMaskedRanges(root) {
533
1139
  const ranges = [];
534
1140
  const visit = (node) => {
535
1141
  if (MASKED_SOURCE_NODE_KINDS.has(node.kind())) {
1142
+ if (isProcessEnvSubscriptKey(node)) return;
536
1143
  const range = node.range();
537
1144
  ranges.push([range.start.index, range.end.index]);
538
1145
  return;
@@ -545,7 +1152,7 @@ function collectMaskedRanges(root) {
545
1152
  function maskSourceNonCode(relative, content) {
546
1153
  let ranges;
547
1154
  try {
548
- ranges = collectMaskedRanges(parse(sourceLang(relative), content).root());
1155
+ ranges = collectMaskedRanges(parse$1(sourceLang(relative), content).root());
549
1156
  } catch {
550
1157
  return content;
551
1158
  }
@@ -581,13 +1188,31 @@ function matchResidualPattern(content, pattern) {
581
1188
  if (!Array.isArray(pattern)) return matchesPattern(content, pattern) ? patternLabel(pattern) : null;
582
1189
  return pattern.every((p) => matchesPattern(content, p)) ? pattern.map((p) => patternLabel(p)).join(" + ") : null;
583
1190
  }
584
- function legacyPatternWarnings(relative, content, transforms) {
1191
+ function matchResidualPatternFragment(content, pattern) {
1192
+ for (const fragment of content.split(SOURCE_STRING_FRAGMENT_SEPARATOR)) {
1193
+ const label = matchResidualPattern(fragment, pattern);
1194
+ if (label != null) return label;
1195
+ }
1196
+ return null;
1197
+ }
1198
+ function legacyPatternWarnings(relative, content, sourceStringContent, sourceTextContent, transforms) {
585
1199
  return transforms.flatMap((lt) => {
586
- const found = lt.legacyPatterns.map((p) => matchResidualPattern(content, p)).filter((label) => label !== null);
587
- if (found.length === 0) return [];
588
- return [`${relative}: contains ${found.join(", ")} but was not migrated automatically (rule: ${lt.id}). Manual migration may be needed.`];
1200
+ const found = new Set(lt.legacyPatterns.map((p) => matchResidualPattern(content, p)).filter((label) => label !== null));
1201
+ if (sourceStringContent != null) for (const pattern of lt.sourceStringLegacyPatterns) {
1202
+ const label = matchResidualPatternFragment(sourceStringContent, pattern);
1203
+ if (label != null) found.add(label);
1204
+ }
1205
+ if (sourceTextContent != null) for (const pattern of lt.sourceTextLegacyPatterns) {
1206
+ const label = matchResidualPatternFragment(sourceTextContent, pattern);
1207
+ if (label != null) found.add(label);
1208
+ }
1209
+ if (found.size === 0) return [];
1210
+ return [`${relative}: contains ${Array.from(found).join(", ")} but was not migrated automatically (rule: ${lt.id}). Manual migration may be needed.`];
589
1211
  });
590
1212
  }
1213
+ function compareReviewFindings(a, b) {
1214
+ return a.file.localeCompare(b.file) || a.line - b.line || a.message.localeCompare(b.message) || a.excerpt.localeCompare(b.excerpt);
1215
+ }
591
1216
  /**
592
1217
  * Run multiple codemods on a project directory using in-memory chaining.
593
1218
  * Each file is processed through all transforms whose filePatterns match it.
@@ -603,13 +1228,19 @@ async function runCodemods(codemods, targetPath, dryRun) {
603
1228
  const loaded = [];
604
1229
  for (const { codemod, scriptPath } of codemods) {
605
1230
  const patterns = codemod.filePatterns ?? DEFAULT_FILE_PATTERNS;
1231
+ const loadedModule = scriptPath ? await loadTransformModule(scriptPath) : void 0;
606
1232
  loaded.push({
607
1233
  id: codemod.id,
608
- transform: scriptPath ? await loadTransform(scriptPath) : void 0,
1234
+ transform: loadedModule?.transform,
1235
+ reviewFindings: loadedModule?.reviewFindings,
609
1236
  matches: picomatch(patterns, { dot: true }),
610
1237
  legacyPatterns: codemod.legacyPatterns ?? [],
1238
+ sourceStringLegacyPatterns: codemod.sourceStringLegacyPatterns ?? [],
1239
+ sourceTextLegacyPatterns: codemod.sourceTextLegacyPatterns ?? [],
611
1240
  suspiciousPatterns: codemod.suspiciousPatterns ?? [],
612
- prompt: codemod.prompt
1241
+ sourceStringSuspiciousPatterns: codemod.sourceStringSuspiciousPatterns ?? [],
1242
+ prompt: codemod.prompt,
1243
+ reviewSupersededBy: codemod.reviewSupersededBy ?? []
613
1244
  });
614
1245
  }
615
1246
  const filesModified = [];
@@ -617,6 +1248,7 @@ async function runCodemods(codemods, targetPath, dryRun) {
617
1248
  const appliedCodemodIds = /* @__PURE__ */ new Set();
618
1249
  const seen = /* @__PURE__ */ new Set();
619
1250
  const suspiciousByCodemod = /* @__PURE__ */ new Map();
1251
+ const findingsByCodemod = /* @__PURE__ */ new Map();
620
1252
  for await (const relative of walkFiles(targetPath)) {
621
1253
  const absolute = path.resolve(targetPath, relative);
622
1254
  if (seen.has(absolute)) continue;
@@ -644,26 +1276,51 @@ async function runCodemods(codemods, targetPath, dryRun) {
644
1276
  else await fs.promises.writeFile(absolute, current, "utf-8");
645
1277
  }
646
1278
  const residualContent = contentForResidualMatching(relative, current);
647
- warnings.push(...legacyPatternWarnings(relative, residualContent, matchedTransforms));
1279
+ const sourceStringContent = sourceStringContentForResidualMatching(relative, current);
1280
+ const sourceTextContent = sourceTextContentForResidualMatching(relative, current);
1281
+ warnings.push(...legacyPatternWarnings(relative, residualContent, sourceStringContent, sourceTextContent, matchedTransforms));
648
1282
  for (const lt of matchedTransforms) {
649
- if (!lt.prompt || lt.suspiciousPatterns.length === 0) continue;
650
- if (lt.suspiciousPatterns.some((p) => matchResidualPattern(residualContent, p) !== null)) {
651
- const files = suspiciousByCodemod.get(lt.id) ?? [];
652
- files.push(relative);
653
- suspiciousByCodemod.set(lt.id, files);
1283
+ if (!lt.prompt) continue;
1284
+ const filesForReview = () => {
1285
+ let files = suspiciousByCodemod.get(lt.id);
1286
+ if (!files) {
1287
+ files = /* @__PURE__ */ new Set();
1288
+ suspiciousByCodemod.set(lt.id, files);
1289
+ }
1290
+ return files;
1291
+ };
1292
+ if (lt.reviewFindings) {
1293
+ const findings = await lt.reviewFindings(current, absolute, relative);
1294
+ if (findings.length > 0) {
1295
+ const files = filesForReview();
1296
+ for (const finding of findings) files.add(finding.file);
1297
+ let existing = findingsByCodemod.get(lt.id);
1298
+ if (!existing) {
1299
+ existing = [];
1300
+ findingsByCodemod.set(lt.id, existing);
1301
+ }
1302
+ existing.push(...findings);
1303
+ }
654
1304
  }
1305
+ if (lt.suspiciousPatterns.some((p) => matchResidualPattern(residualContent, p) !== null) || sourceStringContent != null && lt.sourceStringSuspiciousPatterns.some((p) => matchResidualPattern(sourceStringContent, p) !== null)) filesForReview().add(relative);
655
1306
  }
656
1307
  }
657
1308
  const llmReviews = [];
1309
+ const loadedIds = new Set(loaded.map((lt) => lt.id));
658
1310
  for (const lt of loaded) {
659
1311
  if (!lt.prompt) continue;
660
- if (lt.suspiciousPatterns.length > 0) {
1312
+ if (lt.reviewSupersededBy.some((id) => loadedIds.has(id))) continue;
1313
+ if (lt.suspiciousPatterns.length > 0 || lt.sourceStringSuspiciousPatterns.length > 0 || lt.reviewFindings) {
661
1314
  const files = suspiciousByCodemod.get(lt.id);
662
- if (files) llmReviews.push({
663
- codemodId: lt.id,
664
- prompt: lt.prompt,
665
- files: files.toSorted()
666
- });
1315
+ if (files) {
1316
+ const findings = findingsByCodemod.get(lt.id)?.toSorted(compareReviewFindings);
1317
+ llmReviews.push({
1318
+ codemodId: lt.id,
1319
+ prompt: lt.prompt,
1320
+ files: Array.from(files).toSorted(),
1321
+ ...findings && findings.length > 0 ? { findings } : {}
1322
+ });
1323
+ }
667
1324
  } else if (lt.legacyPatterns.length === 0) llmReviews.push({
668
1325
  codemodId: lt.id,
669
1326
  prompt: lt.prompt,
@@ -679,12 +1336,64 @@ async function runCodemods(codemods, targetPath, dryRun) {
679
1336
  };
680
1337
  }
681
1338
  //#endregion
1339
+ //#region src/runner-metadata.ts
1340
+ const SOURCE_PACKAGE_PATH = "packages/sdk-codemod";
1341
+ const LOCAL_BUILD_COMMAND = "pnpm --dir packages/sdk-codemod build";
1342
+ function createRunnerMetadata({ packageName, packageVersion, packageRoot, readGit = readGitOutput, realpath = safeRealpath }) {
1343
+ const metadata = {
1344
+ packageName,
1345
+ packageVersion
1346
+ };
1347
+ const gitRoot = readGit(packageRoot, ["rev-parse", "--show-toplevel"]);
1348
+ if (!gitRoot) return metadata;
1349
+ if (path.normalize(path.relative(realpath(gitRoot), realpath(packageRoot))).replaceAll("\\", "/") !== SOURCE_PACKAGE_PATH) return metadata;
1350
+ const gitCommit = readGit(packageRoot, [
1351
+ "rev-parse",
1352
+ "--verify",
1353
+ "HEAD"
1354
+ ]);
1355
+ if (!gitCommit) return metadata;
1356
+ return {
1357
+ ...metadata,
1358
+ gitCommit,
1359
+ localBuildCommand: LOCAL_BUILD_COMMAND
1360
+ };
1361
+ }
1362
+ function readGitOutput(cwd, args) {
1363
+ try {
1364
+ return execFileSync("git", [
1365
+ "-C",
1366
+ cwd,
1367
+ ...args
1368
+ ], {
1369
+ encoding: "utf-8",
1370
+ stdio: [
1371
+ "ignore",
1372
+ "pipe",
1373
+ "ignore"
1374
+ ]
1375
+ }).trim() || void 0;
1376
+ } catch {
1377
+ return;
1378
+ }
1379
+ }
1380
+ function safeRealpath(value) {
1381
+ try {
1382
+ return realpathSync(value);
1383
+ } catch {
1384
+ return path.resolve(value);
1385
+ }
1386
+ }
1387
+ //#endregion
682
1388
  //#region src/index.ts
683
- const packageJson = await readPackageJSON(path.dirname(fileURLToPath(import.meta.url)) + "/..");
1389
+ const packageRoot = path.dirname(fileURLToPath(import.meta.url)) + "/..";
1390
+ const packageJson = await readPackageJSON(packageRoot);
1391
+ const packageName = packageJson.name ?? "sdk-codemod";
1392
+ const packageVersion = packageJson.version ?? "0.0.0";
684
1393
  const listCommand = defineCommand({
685
1394
  name: "list",
686
1395
  description: "List the available codemod rules (id, name, kind, version range).",
687
- args: z.object({}).strict(),
1396
+ args: z.strictObject({}),
688
1397
  run: () => {
689
1398
  const rules = allCodemods.map((codemod) => ({
690
1399
  id: codemod.id,
@@ -706,11 +1415,26 @@ const listCommand = defineCommand({
706
1415
  function printLlmReview(review) {
707
1416
  const scope = review.files.length > 0 ? "the codemod cannot safely migrate these automatically" : "review the project for this manual change";
708
1417
  process.stderr.write(`\nšŸ¤– LLM-assisted review suggested (${review.codemodId}) — ${scope}:\n`);
709
- for (const file of review.files) process.stderr.write(` - ${file}\n`);
1418
+ const findingsByFile = /* @__PURE__ */ new Map();
1419
+ for (const finding of review.findings ?? []) {
1420
+ let findings = findingsByFile.get(finding.file);
1421
+ if (!findings) {
1422
+ findings = [];
1423
+ findingsByFile.set(finding.file, findings);
1424
+ }
1425
+ findings.push(finding);
1426
+ }
1427
+ for (const file of review.files) {
1428
+ process.stderr.write(` - ${file}\n`);
1429
+ for (const finding of findingsByFile.get(file) ?? []) {
1430
+ process.stderr.write(` - line ${finding.line}: ${finding.message}\n`);
1431
+ process.stderr.write(` ${finding.excerpt}\n`);
1432
+ }
1433
+ }
710
1434
  process.stderr.write(`\nPrompt for an LLM:\n${review.prompt.trim()}\n`);
711
1435
  }
712
1436
  runMain(defineCommand({
713
- name: packageJson.name ?? "sdk-codemod",
1437
+ name: packageName,
714
1438
  description: packageJson.description ?? "Codemod runner for Tailor Platform SDK upgrades",
715
1439
  subCommands: { list: listCommand },
716
1440
  notes: `Applies the codemods matching the \`--from\`/\`--to\` version range to the
@@ -719,8 +1443,11 @@ runMain(defineCommand({
719
1443
  - \`filesModified\`: files a codemod changed
720
1444
  - \`warnings\`: files that may still need manual migration
721
1445
  - \`llmReviews\`: changes the codemods could not fully migrate on their own. Each
722
- entry has the affected \`files\` and a \`prompt\` — hand the prompt and files to
723
- an LLM (or follow it yourself) to finish those cases.
1446
+ entry has the affected \`files\`, optional file-local \`findings\`, and a
1447
+ \`prompt\` — hand the prompt and files to an LLM (or follow it yourself) to
1448
+ finish those cases.
1449
+ - \`runner\`: exact codemod runner identity. Local source builds include the
1450
+ repository commit and the build command used to produce \`dist/index.js\`.
724
1451
 
725
1452
  Progress, warnings, and the LLM-review prompts are also printed to \`stderr\` in
726
1453
  human-readable form, so \`stdout\` stays pure JSON for piping.`,
@@ -731,7 +1458,7 @@ human-readable form, so \`stdout\` stays pure JSON for piping.`,
731
1458
  cmd: "--from 1.64.0 --to 2.0.0 --dry-run",
732
1459
  desc: "Preview the changes and any LLM-review prompts without writing files"
733
1460
  }],
734
- args: z.object({
1461
+ args: z.strictObject({
735
1462
  from: arg(z.string(), { description: "Source SDK version (the version before upgrade)" }),
736
1463
  to: arg(z.string(), { description: "Target SDK version (the version after upgrade)" }),
737
1464
  target: arg(z.string().default("."), { description: "Project directory to transform" }),
@@ -739,12 +1466,18 @@ human-readable form, so \`stdout\` stays pure JSON for piping.`,
739
1466
  alias: "d",
740
1467
  description: "Preview changes without modifying files"
741
1468
  })
742
- }).strict(),
1469
+ }),
743
1470
  run: async (args) => {
744
1471
  const targetPath = path.resolve(args.target);
745
1472
  const dryRun = args["dry-run"];
1473
+ const runner = createRunnerMetadata({
1474
+ packageName,
1475
+ packageVersion,
1476
+ packageRoot
1477
+ });
746
1478
  const codemods = getApplicableCodemods(args.from, args.to);
747
1479
  const output = {
1480
+ runner,
748
1481
  codemodsApplied: 0,
749
1482
  codemodsSkipped: 0,
750
1483
  filesModified: [],
@@ -782,6 +1515,6 @@ human-readable form, so \`stdout\` stays pure JSON for piping.`,
782
1515
  process.stdout.write(JSON.stringify(output) + "\n");
783
1516
  if (output.errors.length > 0) process.exit(1);
784
1517
  }
785
- }), { version: packageJson.version });
1518
+ }), { version: packageVersion });
786
1519
  //#endregion
787
1520
  export {};