@tailor-platform/sdk-codemod 0.3.0-next.1 ā 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/CHANGELOG.md +174 -2
- package/dist/codemods/ast-grep-helpers-Bfn39biW.js +171 -0
- package/dist/codemods/v2/apply-to-deploy/scripts/transform.js +17 -1
- package/dist/codemods/v2/auth-attributes-rename/scripts/transform.js +213 -0
- package/dist/codemods/v2/auth-connection-token-helper/scripts/transform.js +243 -0
- package/dist/codemods/v2/auth-invoker-call-unwrap/scripts/transform.js +7 -0
- package/dist/codemods/v2/auth-invoker-unwrap/scripts/transform.js +108 -13
- package/dist/codemods/v2/define-generators-to-plugins/scripts/transform.js +2 -1
- package/dist/codemods/v2/env-var-rename/scripts/transform.js +88 -0
- package/dist/codemods/v2/execute-script-arg/scripts/transform.js +60 -0
- package/dist/codemods/v2/principal-unify/scripts/transform.js +1556 -45
- package/dist/codemods/v2/rename-bin/scripts/transform.js +1087 -0
- package/dist/codemods/v2/runtime-globals-opt-in/scripts/transform.js +103 -0
- package/dist/codemods/v2/sdk-skills-shim/scripts/transform.js +3 -3
- package/dist/codemods/v2/tailor-output-ignore-dir/scripts/transform.js +14 -0
- package/dist/codemods/v2/wait-point-rename/scripts/transform.js +126 -0
- package/dist/index.js +1236 -40
- package/package.json +10 -9
package/dist/index.js
CHANGED
|
@@ -5,13 +5,118 @@ 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 { realpathSync } from "node:fs";
|
|
11
|
+
import { Lang, parse as parse$1 } from "@ast-grep/napi";
|
|
10
12
|
import chalk from "chalk";
|
|
11
13
|
import { structuredPatch } from "diff";
|
|
12
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
|
|
13
34
|
//#region src/registry.ts
|
|
14
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
|
+
/** All registered codemods, in registration order. */
|
|
15
120
|
const allCodemods = [
|
|
16
121
|
{
|
|
17
122
|
id: "v2/define-generators-to-plugins",
|
|
@@ -19,8 +124,31 @@ const allCodemods = [
|
|
|
19
124
|
description: "Migrate defineGenerators() tuple syntax to definePlugins() with explicit plugin imports",
|
|
20
125
|
since: "1.0.0",
|
|
21
126
|
until: "2.0.0",
|
|
127
|
+
prereleaseUntil: V2_NEXT_1,
|
|
22
128
|
scriptPath: "v2/define-generators-to-plugins/scripts/transform.js",
|
|
23
|
-
legacyPatterns: ["defineGenerators"]
|
|
129
|
+
legacyPatterns: ["defineGenerators"],
|
|
130
|
+
examples: [{
|
|
131
|
+
before: [
|
|
132
|
+
"import { defineGenerators } from \"@tailor-platform/sdk\";",
|
|
133
|
+
"",
|
|
134
|
+
"export const generators = defineGenerators(",
|
|
135
|
+
" [\"@tailor-platform/kysely-type\", { distPath: \"db.ts\" }],",
|
|
136
|
+
");"
|
|
137
|
+
].join("\n"),
|
|
138
|
+
after: [
|
|
139
|
+
"import { definePlugins } from \"@tailor-platform/sdk\";",
|
|
140
|
+
"import { kyselyTypePlugin } from \"@tailor-platform/sdk/plugin/kysely-type\";",
|
|
141
|
+
"",
|
|
142
|
+
"export const generators = definePlugins(kyselyTypePlugin({ distPath: \"db.ts\" }));"
|
|
143
|
+
].join("\n")
|
|
144
|
+
}],
|
|
145
|
+
prompt: [
|
|
146
|
+
"defineGenerators() is replaced by definePlugins() in v2. The codemod rewrites the",
|
|
147
|
+
"known plugin tuples (kysely-type, enum-constants, file-utils, seed). For any",
|
|
148
|
+
"remaining defineGenerators([...]) the codemod left in place ā a plugin it does not",
|
|
149
|
+
"know, or a non-tuple/spread form ā convert it to definePlugins(pluginFn(config)),",
|
|
150
|
+
"importing the matching plugin from its @tailor-platform/sdk/plugin/<name> subpath."
|
|
151
|
+
].join("\n")
|
|
24
152
|
},
|
|
25
153
|
{
|
|
26
154
|
id: "v2/plugin-cli-import",
|
|
@@ -28,48 +156,130 @@ const allCodemods = [
|
|
|
28
156
|
description: "Rewrite deprecated plugin re-export imports (kyselyTypePlugin, enumConstantsPlugin, fileUtilsPlugin, seedPlugin) from `@tailor-platform/sdk/cli` to their dedicated plugin subpaths",
|
|
29
157
|
since: "1.0.0",
|
|
30
158
|
until: "2.0.0",
|
|
31
|
-
|
|
159
|
+
prereleaseUntil: V2_NEXT_1,
|
|
160
|
+
scriptPath: "v2/plugin-cli-import/scripts/transform.js",
|
|
161
|
+
examples: [{
|
|
162
|
+
before: "import { kyselyTypePlugin } from \"@tailor-platform/sdk/cli\";",
|
|
163
|
+
after: "import { kyselyTypePlugin } from \"@tailor-platform/sdk/plugin/kysely-type\";"
|
|
164
|
+
}]
|
|
32
165
|
},
|
|
33
166
|
{
|
|
34
167
|
id: "v2/test-run-arg-input",
|
|
35
168
|
name: "function test-run --arg input unwrap",
|
|
36
|
-
description: "Strip the deprecated {input: ...} wrapper from `tailor
|
|
169
|
+
description: "Strip the deprecated {input: ...} wrapper from `tailor function test-run --arg` JSON in scripts and docs",
|
|
37
170
|
since: "1.0.0",
|
|
38
171
|
until: "2.0.0",
|
|
172
|
+
prereleaseUntil: V2_NEXT_1,
|
|
39
173
|
scriptPath: "v2/test-run-arg-input/scripts/transform.js",
|
|
40
174
|
filePatterns: [
|
|
41
175
|
"**/package.json",
|
|
42
176
|
"**/*.{sh,bash,zsh}",
|
|
43
177
|
"**/*.md"
|
|
44
|
-
]
|
|
178
|
+
],
|
|
179
|
+
examples: [{
|
|
180
|
+
lang: "sh",
|
|
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}'"
|
|
183
|
+
}]
|
|
45
184
|
},
|
|
46
185
|
{
|
|
47
186
|
id: "v2/sdk-skills-shim",
|
|
48
|
-
name: "tailor-sdk-skills ā tailor
|
|
49
|
-
description: "Replace deprecated `tailor-sdk-skills` invocations with `tailor
|
|
187
|
+
name: "tailor-sdk-skills ā tailor skills add",
|
|
188
|
+
description: "Replace deprecated `tailor-sdk-skills` invocations with `tailor skills add`",
|
|
50
189
|
since: "1.0.0",
|
|
51
190
|
until: "2.0.0",
|
|
191
|
+
prereleaseUntil: V2_NEXT_1,
|
|
52
192
|
scriptPath: "v2/sdk-skills-shim/scripts/transform.js",
|
|
53
193
|
filePatterns: [
|
|
54
194
|
"**/package.json",
|
|
55
195
|
"**/*.{sh,bash,zsh,yml,yaml}",
|
|
56
196
|
"**/*.md"
|
|
57
197
|
],
|
|
58
|
-
legacyPatterns: ["tailor-sdk-skills"]
|
|
198
|
+
legacyPatterns: ["tailor-sdk-skills"],
|
|
199
|
+
examples: [{
|
|
200
|
+
lang: "sh",
|
|
201
|
+
before: "npx tailor-sdk-skills",
|
|
202
|
+
after: "tailor skills add"
|
|
203
|
+
}],
|
|
204
|
+
prompt: [
|
|
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",
|
|
207
|
+
"tailor-sdk-skills invocations the codemod did not rewrite with",
|
|
208
|
+
"`tailor skills add`."
|
|
209
|
+
].join("\n")
|
|
59
210
|
},
|
|
60
211
|
{
|
|
61
212
|
id: "v2/principal-unify",
|
|
62
|
-
name: "Unify TailorUser/TailorActor/TailorInvoker ā TailorPrincipal",
|
|
63
|
-
description: "Rename TailorUser/TailorActor/TailorInvoker to TailorPrincipal, drop unauthenticatedTailorUser,
|
|
213
|
+
name: "Unify TailorUser/TailorActor/TailorActorType/TailorInvoker ā TailorPrincipal",
|
|
214
|
+
description: "Rename TailorUser/TailorActor/TailorActorType/TailorInvoker to TailorPrincipal, drop unauthenticatedTailorUser, rename resolver body `user` to `caller`, and rename TailorDB callback `user` to `invoker`",
|
|
64
215
|
since: "1.0.0",
|
|
65
216
|
until: "2.0.0",
|
|
217
|
+
prereleaseUntil: V2_NEXT_2,
|
|
66
218
|
scriptPath: "v2/principal-unify/scripts/transform.js",
|
|
67
219
|
legacyPatterns: [
|
|
68
220
|
"TailorUser",
|
|
69
221
|
"TailorActor",
|
|
222
|
+
"TailorActorType",
|
|
70
223
|
"TailorInvoker",
|
|
71
224
|
"unauthenticatedTailorUser"
|
|
72
|
-
]
|
|
225
|
+
],
|
|
226
|
+
suspiciousPatterns: [
|
|
227
|
+
"caller?.",
|
|
228
|
+
"context.user",
|
|
229
|
+
"context.invoker ?? context.user",
|
|
230
|
+
"ResolverContext"
|
|
231
|
+
],
|
|
232
|
+
examples: [{
|
|
233
|
+
caption: "Type references unify under `TailorPrincipal`:",
|
|
234
|
+
before: "import type { TailorUser } from \"@tailor-platform/sdk\";",
|
|
235
|
+
after: "import type { TailorPrincipal } from \"@tailor-platform/sdk\";"
|
|
236
|
+
}, {
|
|
237
|
+
caption: "The resolver body `user` becomes `caller`:",
|
|
238
|
+
before: "body: ({ input, user }) => user.id,",
|
|
239
|
+
after: "body: ({ input, caller }) => caller.id,"
|
|
240
|
+
}],
|
|
241
|
+
prompt: [
|
|
242
|
+
"Finish the cases the codemod left for manual migration:",
|
|
243
|
+
"- Rename user -> caller in resolver bodies the codemod skipped because a `caller`",
|
|
244
|
+
" binding already exists or renaming would shadow/collide with another value.",
|
|
245
|
+
"- Replace member-access on the removed unauthenticatedTailorUser (e.g.",
|
|
246
|
+
" unauthenticatedTailorUser.id); the codemod only replaced standalone references",
|
|
247
|
+
" with null and left member access to surface a type error.",
|
|
248
|
+
"- Review helper adapters that still accept or read `context.user`; v2 resolver",
|
|
249
|
+
" context uses nullable `caller` and `invoker`, so project-specific helper",
|
|
250
|
+
" semantics for anonymous callers and command invokers must be chosen explicitly.",
|
|
251
|
+
"- Review `caller?.` values passed to APIs that require non-null values. If the",
|
|
252
|
+
" resolver requires authentication, throw or otherwise narrow before the call;",
|
|
253
|
+
" if anonymous callers are allowed, keep the nullable flow explicit.",
|
|
254
|
+
"Use TailorPrincipal for the unified user/actor/invoker type."
|
|
255
|
+
].join("\n")
|
|
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")
|
|
73
283
|
},
|
|
74
284
|
{
|
|
75
285
|
id: "v2/apply-to-deploy",
|
|
@@ -77,12 +287,19 @@ const allCodemods = [
|
|
|
77
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",
|
|
78
288
|
since: "1.0.0",
|
|
79
289
|
until: "2.0.0",
|
|
290
|
+
prereleaseUntil: V2_NEXT_1,
|
|
80
291
|
scriptPath: "v2/apply-to-deploy/scripts/transform.js",
|
|
81
292
|
filePatterns: [
|
|
82
293
|
"**/package.json",
|
|
83
294
|
"**/*.{sh,bash,zsh,yml,yaml}",
|
|
295
|
+
"**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}",
|
|
84
296
|
"**/*.md"
|
|
85
|
-
]
|
|
297
|
+
],
|
|
298
|
+
examples: [{
|
|
299
|
+
lang: "sh",
|
|
300
|
+
before: "tailor-sdk apply --profile prod",
|
|
301
|
+
after: "tailor-sdk deploy --profile prod"
|
|
302
|
+
}]
|
|
86
303
|
},
|
|
87
304
|
{
|
|
88
305
|
id: "v2/cli-rename",
|
|
@@ -90,31 +307,476 @@ const allCodemods = [
|
|
|
90
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",
|
|
91
308
|
since: "1.0.0",
|
|
92
309
|
until: "2.0.0",
|
|
310
|
+
prereleaseUntil: V2_NEXT_1,
|
|
93
311
|
scriptPath: "v2/cli-rename/scripts/transform.js",
|
|
94
312
|
filePatterns: [
|
|
95
313
|
"**/package.json",
|
|
96
314
|
"**/*.{sh,bash,zsh,yml,yaml}",
|
|
97
315
|
"**/*.md"
|
|
98
316
|
],
|
|
99
|
-
legacyPatterns: ["tailor-sdk crash-report", "--machineuser"]
|
|
317
|
+
legacyPatterns: ["tailor-sdk crash-report", "--machineuser"],
|
|
318
|
+
examples: [{
|
|
319
|
+
lang: "sh",
|
|
320
|
+
before: "tailor-sdk crash-report list\ntailor-sdk login --machineuser",
|
|
321
|
+
after: "tailor-sdk crashreport list\ntailor-sdk login --machine-user"
|
|
322
|
+
}],
|
|
323
|
+
prompt: [
|
|
324
|
+
"Apply the v2 CLI renames the codemod did not reach (only `tailor-sdk`-prefixed",
|
|
325
|
+
"invocations are rewritten): `tailor-sdk crash-report` -> `tailor-sdk crashreport`",
|
|
326
|
+
"and the `--machineuser` option -> `--machine-user`. Leave unrelated commands that",
|
|
327
|
+
"happen to use `--machineuser` alone."
|
|
328
|
+
].join("\n")
|
|
100
329
|
},
|
|
101
330
|
{
|
|
102
|
-
id: "v2/
|
|
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",
|
|
103
384
|
name: "auth.invoker(\"name\") ā \"name\"",
|
|
104
|
-
description: "Replace `auth.invoker(\"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.",
|
|
105
386
|
since: "1.0.0",
|
|
106
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
|
+
},
|
|
415
|
+
{
|
|
416
|
+
id: "v2/auth-invoker-unwrap",
|
|
417
|
+
name: "auth.invoker(\"name\") ā invoker: \"name\"",
|
|
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.",
|
|
419
|
+
since: "1.0.0",
|
|
420
|
+
until: "2.0.0",
|
|
421
|
+
prereleaseUntil: V2_NEXT_2,
|
|
107
422
|
scriptPath: "v2/auth-invoker-unwrap/scripts/transform.js",
|
|
108
|
-
|
|
423
|
+
suspiciousPatterns: [
|
|
424
|
+
"auth.invoker",
|
|
425
|
+
"authInvoker:",
|
|
426
|
+
"authInvoker :",
|
|
427
|
+
"authInvoker?",
|
|
428
|
+
"{ authInvoker",
|
|
429
|
+
", authInvoker",
|
|
430
|
+
"\n authInvoker",
|
|
431
|
+
"\n authInvoker",
|
|
432
|
+
"\n authInvoker",
|
|
433
|
+
"\"authInvoker\":",
|
|
434
|
+
"\"authInvoker\" :",
|
|
435
|
+
"\"authInvoker\"?",
|
|
436
|
+
"'authInvoker':",
|
|
437
|
+
"'authInvoker' :",
|
|
438
|
+
"'authInvoker'?"
|
|
439
|
+
],
|
|
440
|
+
prompt: [
|
|
441
|
+
"In Tailor SDK v2 the auth.invoker() helper is removed; an invoker is now the",
|
|
442
|
+
"machine user name passed directly as a string. The codemod already rewrote the",
|
|
443
|
+
"statically identified SDK option form authInvoker: auth.invoker(\"name\") to invoker: \"name\" and renamed supported authInvoker option keys. These files still contain",
|
|
444
|
+
"auth.invoker(...) calls or authInvoker keys that need manual review.",
|
|
445
|
+
"",
|
|
446
|
+
"For each remaining auth.invoker(<expr>) call:",
|
|
447
|
+
"1. Replace the whole call with <expr> only where the target option expects a",
|
|
448
|
+
" machine user name string; platform/runtime authInvoker payloads still expect",
|
|
449
|
+
" the object form.",
|
|
450
|
+
"2. Rename remaining authInvoker option keys to invoker only for SDK resolver,",
|
|
451
|
+
" executor, workflow.trigger(), or startWorkflow() options. Keep platform/runtime",
|
|
452
|
+
" payload keys such as tailor.workflow.triggerWorkflow(..., { authInvoker: ... }).",
|
|
453
|
+
"3. After removing every auth.invoker usage in a file, delete the now-unused auth",
|
|
454
|
+
" import (keeping it pulls Node-only config modules into runtime bundles); leave",
|
|
455
|
+
" the import if auth is still referenced elsewhere.",
|
|
456
|
+
"",
|
|
457
|
+
"Do not change behavior beyond the SDK option rename and auth.invoker() removal."
|
|
458
|
+
].join("\n"),
|
|
459
|
+
examples: [{
|
|
460
|
+
before: "createResolver({ invoker: auth.invoker(\"manager\") });",
|
|
461
|
+
after: "createResolver({ invoker: \"manager\" });"
|
|
462
|
+
}]
|
|
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")
|
|
109
496
|
},
|
|
110
497
|
{
|
|
111
498
|
id: "v2/tailordb-namespace",
|
|
112
499
|
name: "Tailordb ā tailordb (lowercase ambient namespace)",
|
|
113
|
-
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`.",
|
|
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\"`.",
|
|
114
501
|
since: "1.0.0",
|
|
115
502
|
until: "2.0.0",
|
|
503
|
+
prereleaseUntil: V2_NEXT_1,
|
|
116
504
|
scriptPath: "v2/tailordb-namespace/scripts/transform.js",
|
|
117
|
-
legacyPatterns: ["Tailordb."]
|
|
505
|
+
legacyPatterns: ["Tailordb."],
|
|
506
|
+
examples: [{
|
|
507
|
+
before: "const command: Tailordb.CommandType = \"SELECT\";",
|
|
508
|
+
after: "import \"@tailor-platform/sdk/runtime/globals\";\nconst command: tailordb.CommandType = \"SELECT\";"
|
|
509
|
+
}],
|
|
510
|
+
prompt: [
|
|
511
|
+
"The capital-cased Tailordb ambient namespace is removed in v2; use the lowercase",
|
|
512
|
+
"tailordb.* namespace from @tailor-platform/sdk/runtime/globals. The codemod rewrites",
|
|
513
|
+
"the known members (QueryResult, CommandType, Client). Rewrite any other remaining",
|
|
514
|
+
"Tailordb.* reference to its tailordb.* equivalent (and confirm the member still",
|
|
515
|
+
"exists on the lowercase namespace).",
|
|
516
|
+
"Also add `import \"@tailor-platform/sdk/runtime/globals\"` at the top of each file",
|
|
517
|
+
"that contains any tailordb.* type reference ā v2 no longer activates ambient",
|
|
518
|
+
"declarations automatically on SDK import."
|
|
519
|
+
].join("\n")
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
id: "v2/execute-script-arg",
|
|
523
|
+
name: "executeScript arg JSON.stringify ā value",
|
|
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.",
|
|
525
|
+
since: "1.0.0",
|
|
526
|
+
until: "2.0.0",
|
|
527
|
+
prereleaseUntil: V2_NEXT_2,
|
|
528
|
+
scriptPath: "v2/execute-script-arg/scripts/transform.js",
|
|
529
|
+
filePatterns: ["**/*.{ts,tsx,mts,cts,mjs,cjs,js}"],
|
|
530
|
+
suspiciousPatterns: [[
|
|
531
|
+
"executeScript",
|
|
532
|
+
"JSON.stringify",
|
|
533
|
+
/\barg\s*[:=]|["']arg["']\s*(?::|\]\s*[:=])/
|
|
534
|
+
]],
|
|
535
|
+
prompt: [
|
|
536
|
+
"In Tailor SDK v2 the executeScript() arg option takes a JSON-serializable value",
|
|
537
|
+
"and is serialized internally, so a pre-stringified argument double-encodes. The",
|
|
538
|
+
"codemod already rewrote the direct form arg: JSON.stringify(X) to arg: X. Review",
|
|
539
|
+
"the executeScript calls in these files for cases it could not rewrite ā where the",
|
|
540
|
+
"arg value is reached indirectly, for example:",
|
|
541
|
+
"- a variable holding a JSON.stringify(...) result (const s = JSON.stringify(x); ... arg: s)",
|
|
542
|
+
"- JSON.stringify(x, null, 2) or another multi-argument form",
|
|
543
|
+
"- an options object built or spread dynamically",
|
|
544
|
+
"",
|
|
545
|
+
"For each such call, pass the underlying value directly as arg (drop the",
|
|
546
|
+
"JSON.stringify wrapper) so executeScript serializes it once. Leave calls that",
|
|
547
|
+
"already pass a plain value unchanged."
|
|
548
|
+
].join("\n"),
|
|
549
|
+
examples: [{
|
|
550
|
+
before: "await executeScript({ ...opts, arg: JSON.stringify({ a: 1 }) });",
|
|
551
|
+
after: "await executeScript({ ...opts, arg: { a: 1 } });"
|
|
552
|
+
}]
|
|
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
|
+
},
|
|
567
|
+
{
|
|
568
|
+
id: "v2/open-download-stream",
|
|
569
|
+
name: "openDownloadStream ā downloadStream",
|
|
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.",
|
|
571
|
+
since: "1.0.0",
|
|
572
|
+
until: "2.0.0",
|
|
573
|
+
prereleaseUntil: V2_NEXT_2,
|
|
574
|
+
filePatterns: ["**/*.{ts,tsx,mts,cts,mjs,cjs,js}"],
|
|
575
|
+
suspiciousPatterns: ["openDownloadStream", "openFileDownloadStream"],
|
|
576
|
+
examples: [{
|
|
577
|
+
before: "const res = await openDownloadStream(namespace, typeName, fieldName, recordId);",
|
|
578
|
+
after: "const res = await downloadStream(namespace, typeName, fieldName, recordId);"
|
|
579
|
+
}],
|
|
580
|
+
prompt: [
|
|
581
|
+
"The openDownloadStream file-streaming API is removed in v2. Replace every call to",
|
|
582
|
+
"openDownloadStream with downloadStream (same arguments). If you used the generated",
|
|
583
|
+
"openFileDownloadStream helper, switch to downloadFileStream, which calls",
|
|
584
|
+
"downloadStream and returns FileDownloadStreamResponse."
|
|
585
|
+
].join("\n")
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
id: "v2/runtime-globals-opt-in",
|
|
589
|
+
name: "Ambient runtime globals are opt-in",
|
|
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.)",
|
|
591
|
+
since: "1.0.0",
|
|
592
|
+
until: "2.0.0",
|
|
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}"],
|
|
596
|
+
suspiciousPatterns: [
|
|
597
|
+
"tailor.context",
|
|
598
|
+
"tailor.iconv",
|
|
599
|
+
"tailor.idp",
|
|
600
|
+
"tailor.secretmanager",
|
|
601
|
+
"tailor.authconnection",
|
|
602
|
+
"tailor.workflow",
|
|
603
|
+
"tailor[",
|
|
604
|
+
"tailordb.Client",
|
|
605
|
+
"tailordb.CommandType",
|
|
606
|
+
"tailordb.QueryResult",
|
|
607
|
+
"tailordb.file",
|
|
608
|
+
"tailordb[",
|
|
609
|
+
"TailorDBFileError",
|
|
610
|
+
"TailorErrorItem",
|
|
611
|
+
"TailorErrorMessage",
|
|
612
|
+
"TailorErrors"
|
|
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
|
+
],
|
|
629
|
+
examples: [{
|
|
630
|
+
caption: "Preferred: switch to the typed wrappers from `@tailor-platform/sdk/runtime` and drop the ambient globals:",
|
|
631
|
+
before: "const client = new tailor.idp.Client();",
|
|
632
|
+
after: "import { idp } from \"@tailor-platform/sdk/runtime\";\nconst client = new idp.Client({ namespace: \"my-namespace\" });"
|
|
633
|
+
}, {
|
|
634
|
+
caption: "Fallback: only if you must keep referencing the bare `tailor.*` names, opt into the global declarations:",
|
|
635
|
+
before: "const client = new tailor.idp.Client();",
|
|
636
|
+
after: "import \"@tailor-platform/sdk/runtime/globals\";\nconst client = new tailor.idp.Client();"
|
|
637
|
+
}],
|
|
638
|
+
prompt: [
|
|
639
|
+
"The v2 SDK no longer enables ambient Tailor runtime globals from",
|
|
640
|
+
"`@tailor-platform/sdk`. For each flagged file that uses `tailor.*`,",
|
|
641
|
+
"`tailordb.*`, or Tailor runtime error globals, prefer migrating to the",
|
|
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.",
|
|
647
|
+
"",
|
|
648
|
+
"Only when the file must keep referencing the bare `tailor.*` names directly,",
|
|
649
|
+
"opt into the global declarations instead by adding one of these:",
|
|
650
|
+
"- per-file: `import \"@tailor-platform/sdk/runtime/globals\";`",
|
|
651
|
+
"- project-wide: `\"types\": [\"@tailor-platform/sdk/runtime/globals\"]` in",
|
|
652
|
+
" the relevant tsconfig compilerOptions",
|
|
653
|
+
"",
|
|
654
|
+
"Leave files unchanged when the matching name is local, imported from another",
|
|
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."
|
|
658
|
+
].join("\n")
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
id: "v2/workflow-trigger-dispatch",
|
|
662
|
+
name: "Workflow .trigger() and trigger tests",
|
|
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.",
|
|
664
|
+
since: "1.0.0",
|
|
665
|
+
until: "2.0.0",
|
|
666
|
+
prereleaseUntil: V2_NEXT_1,
|
|
667
|
+
suspiciousPatterns: [".trigger("],
|
|
668
|
+
examples: [{
|
|
669
|
+
caption: "Tests must mock the workflow runtime instead of running bodies locally:",
|
|
670
|
+
before: "const result = await orderJob.trigger({ id });\nexpect(result.status).toBe(\"done\");",
|
|
671
|
+
after: "using wf = mockWorkflow();\nwf.setJobHandler((jobName) => (jobName === \"order-job\" ? { status: \"done\" } : null));\nconst result = await orderJob.trigger({ id });\nexpect(result.status).toBe(\"done\");"
|
|
672
|
+
}],
|
|
673
|
+
prompt: [
|
|
674
|
+
"Workflow job .trigger() now uses the platform workflow runtime instead of running",
|
|
675
|
+
"the job body locally. In tests, acquire `using wf = mockWorkflow()` and provide",
|
|
676
|
+
"trigger responses (setJobHandler / enqueueResult), or use runWorkflowLocally() for a",
|
|
677
|
+
"full-chain local run; an unmocked trigger now throws. Outside tests, treat the",
|
|
678
|
+
"trigger result as the job output directly (no Promise wrapper to unwrap)."
|
|
679
|
+
].join("\n")
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
id: "v2/cli-token-keyring-storage",
|
|
683
|
+
name: "CLI tokens stored in the OS keyring",
|
|
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.",
|
|
685
|
+
since: "1.0.0",
|
|
686
|
+
until: "2.0.0",
|
|
687
|
+
prereleaseUntil: V2_NEXT_2,
|
|
688
|
+
notice: true
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
id: "v2/cli-users-by-subject",
|
|
692
|
+
name: "CLI users keyed by subject ID",
|
|
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.",
|
|
694
|
+
since: "1.0.0",
|
|
695
|
+
until: "2.0.0",
|
|
696
|
+
prereleaseUntil: V2_NEXT_1,
|
|
697
|
+
notice: true
|
|
698
|
+
},
|
|
699
|
+
{
|
|
700
|
+
id: "v2/function-logs-content-hash",
|
|
701
|
+
name: "function logs require a content hash for source mapping",
|
|
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+.",
|
|
777
|
+
since: "1.0.0",
|
|
778
|
+
until: "2.0.0",
|
|
779
|
+
notice: true
|
|
118
780
|
}
|
|
119
781
|
];
|
|
120
782
|
/**
|
|
@@ -125,9 +787,32 @@ const allCodemods = [
|
|
|
125
787
|
function resolveCodemodScript(scriptPath) {
|
|
126
788
|
return path.resolve(CODEMODS_ROOT, scriptPath);
|
|
127
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
|
+
}
|
|
128
812
|
/**
|
|
129
813
|
* Get codemod packages applicable for a version range.
|
|
130
|
-
* A codemod applies when: since <= fromVersion <
|
|
814
|
+
* A codemod applies when: since <= fromVersion < boundary <= toVersion.
|
|
815
|
+
* A target prerelease reaches `until` only when the codemod declares `prereleaseUntil`.
|
|
131
816
|
* @param fromVersion - Current SDK version (semver)
|
|
132
817
|
* @param toVersion - Target SDK version (semver)
|
|
133
818
|
* @returns Array of applicable codemod packages in registration order
|
|
@@ -135,19 +820,60 @@ function resolveCodemodScript(scriptPath) {
|
|
|
135
820
|
function getApplicableCodemods(fromVersion, toVersion) {
|
|
136
821
|
if (!valid(fromVersion)) throw new Error(`Invalid fromVersion: ${fromVersion}`);
|
|
137
822
|
if (!valid(toVersion)) throw new Error(`Invalid toVersion: ${toVersion}`);
|
|
138
|
-
|
|
823
|
+
assertCodemodBoundaries(allCodemods);
|
|
824
|
+
return allCodemods.filter((codemod) => gte(fromVersion, codemod.since) && lt(fromVersion, effectiveCodemodBoundary(codemod)) && reachesCodemodBoundary(toVersion, codemod));
|
|
139
825
|
}
|
|
140
826
|
//#endregion
|
|
141
827
|
//#region src/runner.ts
|
|
142
828
|
/** Default file patterns for TypeScript files. */
|
|
143
829
|
const DEFAULT_FILE_PATTERNS = ["**/*.{ts,tsx,mts,cts}"];
|
|
144
830
|
/** Directory names always excluded from file scanning. */
|
|
145
|
-
const EXCLUDE_DIRS = new Set([
|
|
831
|
+
const EXCLUDE_DIRS = /* @__PURE__ */ new Set([
|
|
146
832
|
"node_modules",
|
|
147
833
|
"dist",
|
|
148
834
|
".git"
|
|
149
835
|
]);
|
|
150
|
-
const ALLOWED_DOT_DIRS = new Set([".github", ".circleci"]);
|
|
836
|
+
const ALLOWED_DOT_DIRS = /* @__PURE__ */ new Set([".github", ".circleci"]);
|
|
837
|
+
const SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
838
|
+
".ts",
|
|
839
|
+
".tsx",
|
|
840
|
+
".mts",
|
|
841
|
+
".cts",
|
|
842
|
+
".js",
|
|
843
|
+
".jsx",
|
|
844
|
+
".mjs",
|
|
845
|
+
".cjs"
|
|
846
|
+
]);
|
|
847
|
+
const SOURCE_STRING_FRAGMENT_SEPARATOR = "\0";
|
|
848
|
+
const MASKED_SOURCE_NODE_KINDS = /* @__PURE__ */ new Set([
|
|
849
|
+
"comment",
|
|
850
|
+
"string",
|
|
851
|
+
"regex",
|
|
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"
|
|
875
|
+
]);
|
|
876
|
+
const SOURCE_CLI_BINARY_RE = /^(?:(?:.*[\\/])?tailor(?:\.(?:cmd|ps1|exe))?|(?:.*[\\/])?tailor-sdk(?:@[^\s'"`;|&)]+)?(?:\.(?:cmd|ps1|exe))?|@tailor-platform\/sdk(?:@[^\s'"`;|&)]+)?)$/;
|
|
151
877
|
function shouldSkipDirectory(name) {
|
|
152
878
|
return EXCLUDE_DIRS.has(name) || name.startsWith(".") && !ALLOWED_DOT_DIRS.has(name);
|
|
153
879
|
}
|
|
@@ -191,20 +917,302 @@ function printDiff(filePath, before, after) {
|
|
|
191
917
|
* Load a transform module from a TypeScript file path.
|
|
192
918
|
* Expects the module to have a default export that is a TransformFn.
|
|
193
919
|
* @param scriptPath - Absolute path to the transform script
|
|
194
|
-
* @returns The transform function
|
|
920
|
+
* @returns The transform function and optional review detector
|
|
195
921
|
*/
|
|
196
|
-
async function
|
|
922
|
+
async function loadTransformModule(scriptPath) {
|
|
197
923
|
const mod = await import(url.pathToFileURL(scriptPath).href);
|
|
198
924
|
if (typeof mod.default !== "function") throw new Error(`Transform at ${scriptPath} does not have a default export function`);
|
|
199
|
-
return
|
|
925
|
+
return {
|
|
926
|
+
transform: mod.default,
|
|
927
|
+
reviewFindings: typeof mod.reviewFindings === "function" ? mod.reviewFindings : void 0
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
function contentForResidualMatching(relative, content) {
|
|
931
|
+
const ext = path.extname(relative).toLowerCase();
|
|
932
|
+
return SOURCE_EXTENSIONS.has(ext) ? maskSourceNonCode(relative, content) : content;
|
|
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
|
+
}
|
|
1126
|
+
function sourceLang(relative) {
|
|
1127
|
+
const ext = path.extname(relative).toLowerCase();
|
|
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());
|
|
200
1137
|
}
|
|
201
|
-
function
|
|
1138
|
+
function collectMaskedRanges(root) {
|
|
1139
|
+
const ranges = [];
|
|
1140
|
+
const visit = (node) => {
|
|
1141
|
+
if (MASKED_SOURCE_NODE_KINDS.has(node.kind())) {
|
|
1142
|
+
if (isProcessEnvSubscriptKey(node)) return;
|
|
1143
|
+
const range = node.range();
|
|
1144
|
+
ranges.push([range.start.index, range.end.index]);
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
for (const child of node.children()) visit(child);
|
|
1148
|
+
};
|
|
1149
|
+
visit(root);
|
|
1150
|
+
return ranges;
|
|
1151
|
+
}
|
|
1152
|
+
function maskSourceNonCode(relative, content) {
|
|
1153
|
+
let ranges;
|
|
1154
|
+
try {
|
|
1155
|
+
ranges = collectMaskedRanges(parse$1(sourceLang(relative), content).root());
|
|
1156
|
+
} catch {
|
|
1157
|
+
return content;
|
|
1158
|
+
}
|
|
1159
|
+
ranges = ranges.toSorted(([a], [b]) => a - b);
|
|
1160
|
+
const chars = content.split("");
|
|
1161
|
+
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] = " ";
|
|
1162
|
+
return chars.join("");
|
|
1163
|
+
}
|
|
1164
|
+
function isIdentifierChar(char) {
|
|
1165
|
+
return char != null && /^[A-Za-z0-9_$]$/.test(char);
|
|
1166
|
+
}
|
|
1167
|
+
function matchesPattern(content, pattern) {
|
|
1168
|
+
if (typeof pattern === "string") {
|
|
1169
|
+
const checkLeft = isIdentifierChar(pattern[0]);
|
|
1170
|
+
const checkRight = isIdentifierChar(pattern.at(-1));
|
|
1171
|
+
let index = content.indexOf(pattern);
|
|
1172
|
+
while (index !== -1) {
|
|
1173
|
+
const before = index > 0 ? content[index - 1] : void 0;
|
|
1174
|
+
const after = content[index + pattern.length];
|
|
1175
|
+
if ((!checkLeft || !isIdentifierChar(before)) && (!checkRight || !isIdentifierChar(after))) return true;
|
|
1176
|
+
index = content.indexOf(pattern, index + 1);
|
|
1177
|
+
}
|
|
1178
|
+
return false;
|
|
1179
|
+
}
|
|
1180
|
+
pattern.lastIndex = 0;
|
|
1181
|
+
return pattern.test(content);
|
|
1182
|
+
}
|
|
1183
|
+
function patternLabel(pattern) {
|
|
1184
|
+
return typeof pattern === "string" ? pattern : pattern.toString();
|
|
1185
|
+
}
|
|
1186
|
+
/** Resolve a residual pattern against content, returning its label when matched. */
|
|
1187
|
+
function matchResidualPattern(content, pattern) {
|
|
1188
|
+
if (!Array.isArray(pattern)) return matchesPattern(content, pattern) ? patternLabel(pattern) : null;
|
|
1189
|
+
return pattern.every((p) => matchesPattern(content, p)) ? pattern.map((p) => patternLabel(p)).join(" + ") : null;
|
|
1190
|
+
}
|
|
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) {
|
|
202
1199
|
return transforms.flatMap((lt) => {
|
|
203
|
-
const found = lt.legacyPatterns.
|
|
204
|
-
if (
|
|
205
|
-
|
|
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.`];
|
|
206
1211
|
});
|
|
207
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
|
+
}
|
|
208
1216
|
/**
|
|
209
1217
|
* Run multiple codemods on a project directory using in-memory chaining.
|
|
210
1218
|
* Each file is processed through all transforms whose filePatterns match it.
|
|
@@ -220,17 +1228,27 @@ async function runCodemods(codemods, targetPath, dryRun) {
|
|
|
220
1228
|
const loaded = [];
|
|
221
1229
|
for (const { codemod, scriptPath } of codemods) {
|
|
222
1230
|
const patterns = codemod.filePatterns ?? DEFAULT_FILE_PATTERNS;
|
|
1231
|
+
const loadedModule = scriptPath ? await loadTransformModule(scriptPath) : void 0;
|
|
223
1232
|
loaded.push({
|
|
224
1233
|
id: codemod.id,
|
|
225
|
-
transform:
|
|
1234
|
+
transform: loadedModule?.transform,
|
|
1235
|
+
reviewFindings: loadedModule?.reviewFindings,
|
|
226
1236
|
matches: picomatch(patterns, { dot: true }),
|
|
227
|
-
legacyPatterns: codemod.legacyPatterns ?? []
|
|
1237
|
+
legacyPatterns: codemod.legacyPatterns ?? [],
|
|
1238
|
+
sourceStringLegacyPatterns: codemod.sourceStringLegacyPatterns ?? [],
|
|
1239
|
+
sourceTextLegacyPatterns: codemod.sourceTextLegacyPatterns ?? [],
|
|
1240
|
+
suspiciousPatterns: codemod.suspiciousPatterns ?? [],
|
|
1241
|
+
sourceStringSuspiciousPatterns: codemod.sourceStringSuspiciousPatterns ?? [],
|
|
1242
|
+
prompt: codemod.prompt,
|
|
1243
|
+
reviewSupersededBy: codemod.reviewSupersededBy ?? []
|
|
228
1244
|
});
|
|
229
1245
|
}
|
|
230
1246
|
const filesModified = [];
|
|
231
1247
|
const warnings = [];
|
|
232
1248
|
const appliedCodemodIds = /* @__PURE__ */ new Set();
|
|
233
1249
|
const seen = /* @__PURE__ */ new Set();
|
|
1250
|
+
const suspiciousByCodemod = /* @__PURE__ */ new Map();
|
|
1251
|
+
const findingsByCodemod = /* @__PURE__ */ new Map();
|
|
234
1252
|
for await (const relative of walkFiles(targetPath)) {
|
|
235
1253
|
const absolute = path.resolve(targetPath, relative);
|
|
236
1254
|
if (seen.has(absolute)) continue;
|
|
@@ -245,6 +1263,7 @@ async function runCodemods(codemods, targetPath, dryRun) {
|
|
|
245
1263
|
}
|
|
246
1264
|
let current = original;
|
|
247
1265
|
for (const lt of matchedTransforms) {
|
|
1266
|
+
if (!lt.transform) continue;
|
|
248
1267
|
const result = await lt.transform(current, absolute);
|
|
249
1268
|
if (result != null) {
|
|
250
1269
|
current = result;
|
|
@@ -256,22 +1275,190 @@ async function runCodemods(codemods, targetPath, dryRun) {
|
|
|
256
1275
|
if (dryRun) printDiff(absolute, original, current);
|
|
257
1276
|
else await fs.promises.writeFile(absolute, current, "utf-8");
|
|
258
1277
|
}
|
|
259
|
-
|
|
1278
|
+
const residualContent = contentForResidualMatching(relative, current);
|
|
1279
|
+
const sourceStringContent = sourceStringContentForResidualMatching(relative, current);
|
|
1280
|
+
const sourceTextContent = sourceTextContentForResidualMatching(relative, current);
|
|
1281
|
+
warnings.push(...legacyPatternWarnings(relative, residualContent, sourceStringContent, sourceTextContent, matchedTransforms));
|
|
1282
|
+
for (const lt of matchedTransforms) {
|
|
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
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
if (lt.suspiciousPatterns.some((p) => matchResidualPattern(residualContent, p) !== null) || sourceStringContent != null && lt.sourceStringSuspiciousPatterns.some((p) => matchResidualPattern(sourceStringContent, p) !== null)) filesForReview().add(relative);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
const llmReviews = [];
|
|
1309
|
+
const loadedIds = new Set(loaded.map((lt) => lt.id));
|
|
1310
|
+
for (const lt of loaded) {
|
|
1311
|
+
if (!lt.prompt) continue;
|
|
1312
|
+
if (lt.reviewSupersededBy.some((id) => loadedIds.has(id))) continue;
|
|
1313
|
+
if (lt.suspiciousPatterns.length > 0 || lt.sourceStringSuspiciousPatterns.length > 0 || lt.reviewFindings) {
|
|
1314
|
+
const files = suspiciousByCodemod.get(lt.id);
|
|
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
|
+
}
|
|
1324
|
+
} else if (lt.legacyPatterns.length === 0) llmReviews.push({
|
|
1325
|
+
codemodId: lt.id,
|
|
1326
|
+
prompt: lt.prompt,
|
|
1327
|
+
files: []
|
|
1328
|
+
});
|
|
260
1329
|
}
|
|
261
1330
|
return {
|
|
262
1331
|
changed: filesModified.length > 0,
|
|
263
1332
|
filesModified,
|
|
264
1333
|
warnings,
|
|
265
|
-
appliedCodemodIds
|
|
1334
|
+
appliedCodemodIds,
|
|
1335
|
+
llmReviews
|
|
266
1336
|
};
|
|
267
1337
|
}
|
|
268
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
|
|
269
1388
|
//#region src/index.ts
|
|
270
|
-
const
|
|
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";
|
|
1393
|
+
const listCommand = defineCommand({
|
|
1394
|
+
name: "list",
|
|
1395
|
+
description: "List the available codemod rules (id, name, kind, version range).",
|
|
1396
|
+
args: z.strictObject({}),
|
|
1397
|
+
run: () => {
|
|
1398
|
+
const rules = allCodemods.map((codemod) => ({
|
|
1399
|
+
id: codemod.id,
|
|
1400
|
+
name: codemod.name,
|
|
1401
|
+
kind: codemod.notice ? "Notice" : automationLevel(codemod),
|
|
1402
|
+
since: codemod.since,
|
|
1403
|
+
until: codemod.until
|
|
1404
|
+
}));
|
|
1405
|
+
for (const rule of rules) process.stderr.write(` ${rule.id} [${rule.kind}] ${rule.name}\n`);
|
|
1406
|
+
process.stdout.write(JSON.stringify(rules) + "\n");
|
|
1407
|
+
}
|
|
1408
|
+
});
|
|
1409
|
+
/**
|
|
1410
|
+
* Print an LLM-assisted review task to stderr: the flagged files plus the
|
|
1411
|
+
* codemod's migration prompt, ready to hand to an LLM for the cases the
|
|
1412
|
+
* deterministic transform could not complete on its own.
|
|
1413
|
+
* @param review - The review task (codemod id, prompt, files)
|
|
1414
|
+
*/
|
|
1415
|
+
function printLlmReview(review) {
|
|
1416
|
+
const scope = review.files.length > 0 ? "the codemod cannot safely migrate these automatically" : "review the project for this manual change";
|
|
1417
|
+
process.stderr.write(`\nš¤ LLM-assisted review suggested (${review.codemodId}) ā ${scope}:\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
|
+
}
|
|
1434
|
+
process.stderr.write(`\nPrompt for an LLM:\n${review.prompt.trim()}\n`);
|
|
1435
|
+
}
|
|
271
1436
|
runMain(defineCommand({
|
|
272
|
-
name:
|
|
1437
|
+
name: packageName,
|
|
273
1438
|
description: packageJson.description ?? "Codemod runner for Tailor Platform SDK upgrades",
|
|
274
|
-
|
|
1439
|
+
subCommands: { list: listCommand },
|
|
1440
|
+
notes: `Applies the codemods matching the \`--from\`/\`--to\` version range to the
|
|
1441
|
+
\`--target\` directory, then writes a JSON summary to \`stdout\`:
|
|
1442
|
+
|
|
1443
|
+
- \`filesModified\`: files a codemod changed
|
|
1444
|
+
- \`warnings\`: files that may still need manual migration
|
|
1445
|
+
- \`llmReviews\`: changes the codemods could not fully migrate on their own. Each
|
|
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\`.
|
|
1451
|
+
|
|
1452
|
+
Progress, warnings, and the LLM-review prompts are also printed to \`stderr\` in
|
|
1453
|
+
human-readable form, so \`stdout\` stays pure JSON for piping.`,
|
|
1454
|
+
examples: [{
|
|
1455
|
+
cmd: "--from 1.64.0 --to 2.0.0",
|
|
1456
|
+
desc: "Apply every codemod for the 1.64.0 -> 2.0.0 upgrade to the current project"
|
|
1457
|
+
}, {
|
|
1458
|
+
cmd: "--from 1.64.0 --to 2.0.0 --dry-run",
|
|
1459
|
+
desc: "Preview the changes and any LLM-review prompts without writing files"
|
|
1460
|
+
}],
|
|
1461
|
+
args: z.strictObject({
|
|
275
1462
|
from: arg(z.string(), { description: "Source SDK version (the version before upgrade)" }),
|
|
276
1463
|
to: arg(z.string(), { description: "Target SDK version (the version after upgrade)" }),
|
|
277
1464
|
target: arg(z.string().default("."), { description: "Project directory to transform" }),
|
|
@@ -279,17 +1466,24 @@ runMain(defineCommand({
|
|
|
279
1466
|
alias: "d",
|
|
280
1467
|
description: "Preview changes without modifying files"
|
|
281
1468
|
})
|
|
282
|
-
})
|
|
1469
|
+
}),
|
|
283
1470
|
run: async (args) => {
|
|
284
1471
|
const targetPath = path.resolve(args.target);
|
|
285
1472
|
const dryRun = args["dry-run"];
|
|
1473
|
+
const runner = createRunnerMetadata({
|
|
1474
|
+
packageName,
|
|
1475
|
+
packageVersion,
|
|
1476
|
+
packageRoot
|
|
1477
|
+
});
|
|
286
1478
|
const codemods = getApplicableCodemods(args.from, args.to);
|
|
287
1479
|
const output = {
|
|
1480
|
+
runner,
|
|
288
1481
|
codemodsApplied: 0,
|
|
289
1482
|
codemodsSkipped: 0,
|
|
290
1483
|
filesModified: [],
|
|
291
1484
|
warnings: [],
|
|
292
|
-
errors: []
|
|
1485
|
+
errors: [],
|
|
1486
|
+
llmReviews: []
|
|
293
1487
|
};
|
|
294
1488
|
if (codemods.length === 0) {
|
|
295
1489
|
process.stdout.write(JSON.stringify(output) + "\n");
|
|
@@ -297,7 +1491,7 @@ runMain(defineCommand({
|
|
|
297
1491
|
}
|
|
298
1492
|
const codemodEntries = codemods.map((codemod) => ({
|
|
299
1493
|
codemod,
|
|
300
|
-
scriptPath: resolveCodemodScript(codemod.scriptPath)
|
|
1494
|
+
scriptPath: codemod.scriptPath ? resolveCodemodScript(codemod.scriptPath) : void 0
|
|
301
1495
|
}));
|
|
302
1496
|
for (const { codemod } of codemodEntries) process.stderr.write(`Running: ${codemod.name} - ${codemod.description}\n`);
|
|
303
1497
|
try {
|
|
@@ -306,8 +1500,10 @@ runMain(defineCommand({
|
|
|
306
1500
|
output.codemodsSkipped = codemods.length - result.appliedCodemodIds.size;
|
|
307
1501
|
output.filesModified = result.filesModified;
|
|
308
1502
|
output.warnings = result.warnings;
|
|
1503
|
+
output.llmReviews = result.llmReviews;
|
|
309
1504
|
if (result.changed) process.stderr.write(` ${result.filesModified.length} file(s) modified\n`);
|
|
310
1505
|
else process.stderr.write(" No changes needed\n");
|
|
1506
|
+
for (const review of output.llmReviews) printLlmReview(review);
|
|
311
1507
|
} catch (error) {
|
|
312
1508
|
const message = error instanceof Error ? error.message : String(error);
|
|
313
1509
|
output.errors.push({
|
|
@@ -319,6 +1515,6 @@ runMain(defineCommand({
|
|
|
319
1515
|
process.stdout.write(JSON.stringify(output) + "\n");
|
|
320
1516
|
if (output.errors.length > 0) process.exit(1);
|
|
321
1517
|
}
|
|
322
|
-
}), { version:
|
|
1518
|
+
}), { version: packageVersion });
|
|
323
1519
|
//#endregion
|
|
324
1520
|
export {};
|