@tailor-platform/sdk-codemod 0.3.0-next.0 → 0.3.0-next.2
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 +86 -0
- package/dist/codemods/v2/apply-to-deploy/scripts/transform.js +22 -4
- package/dist/codemods/v2/auth-invoker-unwrap/scripts/transform.js +99 -11
- package/dist/codemods/v2/cli-rename/scripts/transform.js +373 -14
- package/dist/codemods/v2/execute-script-arg/scripts/transform.js +60 -0
- package/dist/codemods/v2/principal-unify/scripts/transform.js +1131 -43
- package/dist/codemods/v2/tailordb-namespace/scripts/transform.js +5 -4
- package/dist/index.js +521 -37
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -7,12 +7,29 @@ import { arg, defineCommand, runMain } from "politty";
|
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { gte, lt, valid } from "semver";
|
|
9
9
|
import * as fs from "node:fs";
|
|
10
|
-
import {
|
|
10
|
+
import { Lang, parse } from "@ast-grep/napi";
|
|
11
11
|
import chalk from "chalk";
|
|
12
12
|
import { structuredPatch } from "diff";
|
|
13
13
|
import picomatch from "picomatch";
|
|
14
|
+
//#region src/migration-doc.ts
|
|
15
|
+
/**
|
|
16
|
+
* Classify how much of a migration the codemod automates.
|
|
17
|
+
* - `Automatic`: a transform fully covers it, with no residual to flag.
|
|
18
|
+
* - `Partially automatic`: a transform covers the common cases but flags
|
|
19
|
+
* residuals (via `legacyPatterns`/`suspiciousPatterns`/`prompt`) to finish.
|
|
20
|
+
* - `Manual`: no transform; the change is migrated by hand (optionally guided
|
|
21
|
+
* by a `prompt`). Whether a person or an LLM does it does not matter here.
|
|
22
|
+
* @param codemod - The codemod registry entry
|
|
23
|
+
* @returns The automation level
|
|
24
|
+
*/
|
|
25
|
+
function automationLevel(codemod) {
|
|
26
|
+
if (!codemod.scriptPath) return "Manual";
|
|
27
|
+
return (codemod.legacyPatterns?.length ?? 0) > 0 || (codemod.suspiciousPatterns?.length ?? 0) > 0 || codemod.prompt != null ? "Partially automatic" : "Automatic";
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
14
30
|
//#region src/registry.ts
|
|
15
31
|
const CODEMODS_ROOT = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), "codemods");
|
|
32
|
+
/** All registered codemods, in registration order. */
|
|
16
33
|
const allCodemods = [
|
|
17
34
|
{
|
|
18
35
|
id: "v2/define-generators-to-plugins",
|
|
@@ -21,7 +38,29 @@ const allCodemods = [
|
|
|
21
38
|
since: "1.0.0",
|
|
22
39
|
until: "2.0.0",
|
|
23
40
|
scriptPath: "v2/define-generators-to-plugins/scripts/transform.js",
|
|
24
|
-
legacyPatterns: ["defineGenerators"]
|
|
41
|
+
legacyPatterns: ["defineGenerators"],
|
|
42
|
+
examples: [{
|
|
43
|
+
before: [
|
|
44
|
+
"import { defineGenerators } from \"@tailor-platform/sdk\";",
|
|
45
|
+
"",
|
|
46
|
+
"export const generators = defineGenerators(",
|
|
47
|
+
" [\"@tailor-platform/kysely-type\", { distPath: \"db.ts\" }],",
|
|
48
|
+
");"
|
|
49
|
+
].join("\n"),
|
|
50
|
+
after: [
|
|
51
|
+
"import { definePlugins } from \"@tailor-platform/sdk\";",
|
|
52
|
+
"import { kyselyTypePlugin } from \"@tailor-platform/sdk/plugin/kysely-type\";",
|
|
53
|
+
"",
|
|
54
|
+
"export const generators = definePlugins(kyselyTypePlugin({ distPath: \"db.ts\" }));"
|
|
55
|
+
].join("\n")
|
|
56
|
+
}],
|
|
57
|
+
prompt: [
|
|
58
|
+
"defineGenerators() is replaced by definePlugins() in v2. The codemod rewrites the",
|
|
59
|
+
"known plugin tuples (kysely-type, enum-constants, file-utils, seed). For any",
|
|
60
|
+
"remaining defineGenerators([...]) the codemod left in place — a plugin it does not",
|
|
61
|
+
"know, or a non-tuple/spread form — convert it to definePlugins(pluginFn(config)),",
|
|
62
|
+
"importing the matching plugin from its @tailor-platform/sdk/plugin/<name> subpath."
|
|
63
|
+
].join("\n")
|
|
25
64
|
},
|
|
26
65
|
{
|
|
27
66
|
id: "v2/plugin-cli-import",
|
|
@@ -29,7 +68,11 @@ const allCodemods = [
|
|
|
29
68
|
description: "Rewrite deprecated plugin re-export imports (kyselyTypePlugin, enumConstantsPlugin, fileUtilsPlugin, seedPlugin) from `@tailor-platform/sdk/cli` to their dedicated plugin subpaths",
|
|
30
69
|
since: "1.0.0",
|
|
31
70
|
until: "2.0.0",
|
|
32
|
-
scriptPath: "v2/plugin-cli-import/scripts/transform.js"
|
|
71
|
+
scriptPath: "v2/plugin-cli-import/scripts/transform.js",
|
|
72
|
+
examples: [{
|
|
73
|
+
before: "import { kyselyTypePlugin } from \"@tailor-platform/sdk/cli\";",
|
|
74
|
+
after: "import { kyselyTypePlugin } from \"@tailor-platform/sdk/plugin/kysely-type\";"
|
|
75
|
+
}]
|
|
33
76
|
},
|
|
34
77
|
{
|
|
35
78
|
id: "v2/test-run-arg-input",
|
|
@@ -42,7 +85,12 @@ const allCodemods = [
|
|
|
42
85
|
"**/package.json",
|
|
43
86
|
"**/*.{sh,bash,zsh}",
|
|
44
87
|
"**/*.md"
|
|
45
|
-
]
|
|
88
|
+
],
|
|
89
|
+
examples: [{
|
|
90
|
+
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}'"
|
|
93
|
+
}]
|
|
46
94
|
},
|
|
47
95
|
{
|
|
48
96
|
id: "v2/sdk-skills-shim",
|
|
@@ -56,39 +104,87 @@ const allCodemods = [
|
|
|
56
104
|
"**/*.{sh,bash,zsh,yml,yaml}",
|
|
57
105
|
"**/*.md"
|
|
58
106
|
],
|
|
59
|
-
legacyPatterns: ["tailor-sdk-skills"]
|
|
107
|
+
legacyPatterns: ["tailor-sdk-skills"],
|
|
108
|
+
examples: [{
|
|
109
|
+
lang: "sh",
|
|
110
|
+
before: "npx tailor-sdk-skills",
|
|
111
|
+
after: "tailor-sdk skills install"
|
|
112
|
+
}],
|
|
113
|
+
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",
|
|
116
|
+
"tailor-sdk-skills invocations the codemod did not rewrite with",
|
|
117
|
+
"`tailor-sdk skills install`."
|
|
118
|
+
].join("\n")
|
|
60
119
|
},
|
|
61
120
|
{
|
|
62
121
|
id: "v2/principal-unify",
|
|
63
|
-
name: "Unify TailorUser/TailorActor/TailorInvoker → TailorPrincipal",
|
|
64
|
-
description: "Rename TailorUser/TailorActor/TailorInvoker to TailorPrincipal, drop unauthenticatedTailorUser,
|
|
122
|
+
name: "Unify TailorUser/TailorActor/TailorActorType/TailorInvoker → TailorPrincipal",
|
|
123
|
+
description: "Rename TailorUser/TailorActor/TailorActorType/TailorInvoker to TailorPrincipal, drop unauthenticatedTailorUser, rename resolver body `user` to `caller`, and rename TailorDB callback `user` to `invoker`",
|
|
65
124
|
since: "1.0.0",
|
|
66
125
|
until: "2.0.0",
|
|
67
126
|
scriptPath: "v2/principal-unify/scripts/transform.js",
|
|
68
127
|
legacyPatterns: [
|
|
69
128
|
"TailorUser",
|
|
70
129
|
"TailorActor",
|
|
130
|
+
"TailorActorType",
|
|
71
131
|
"TailorInvoker",
|
|
72
132
|
"unauthenticatedTailorUser"
|
|
73
|
-
]
|
|
133
|
+
],
|
|
134
|
+
suspiciousPatterns: [
|
|
135
|
+
"caller?.",
|
|
136
|
+
"context.user",
|
|
137
|
+
"context.invoker ?? context.user",
|
|
138
|
+
"ResolverContext"
|
|
139
|
+
],
|
|
140
|
+
examples: [{
|
|
141
|
+
caption: "Type references unify under `TailorPrincipal`:",
|
|
142
|
+
before: "import type { TailorUser } from \"@tailor-platform/sdk\";",
|
|
143
|
+
after: "import type { TailorPrincipal } from \"@tailor-platform/sdk\";"
|
|
144
|
+
}, {
|
|
145
|
+
caption: "The resolver body `user` becomes `caller`:",
|
|
146
|
+
before: "body: ({ input, user }) => user.id,",
|
|
147
|
+
after: "body: ({ input, caller }) => caller.id,"
|
|
148
|
+
}],
|
|
149
|
+
prompt: [
|
|
150
|
+
"Finish the cases the codemod left for manual migration:",
|
|
151
|
+
"- Rename user -> caller in resolver bodies the codemod skipped because a `caller`",
|
|
152
|
+
" binding already exists or renaming would shadow/collide with another value.",
|
|
153
|
+
"- Replace member-access on the removed unauthenticatedTailorUser (e.g.",
|
|
154
|
+
" unauthenticatedTailorUser.id); the codemod only replaced standalone references",
|
|
155
|
+
" with null and left member access to surface a type error.",
|
|
156
|
+
"- Review helper adapters that still accept or read `context.user`; v2 resolver",
|
|
157
|
+
" context uses nullable `caller` and `invoker`, so project-specific helper",
|
|
158
|
+
" semantics for anonymous callers and command invokers must be chosen explicitly.",
|
|
159
|
+
"- Review `caller?.` values passed to APIs that require non-null values. If the",
|
|
160
|
+
" resolver requires authentication, throw or otherwise narrow before the call;",
|
|
161
|
+
" if anonymous callers are allowed, keep the nullable flow explicit.",
|
|
162
|
+
"Use TailorPrincipal for the unified user/actor/invoker type."
|
|
163
|
+
].join("\n")
|
|
74
164
|
},
|
|
75
165
|
{
|
|
76
166
|
id: "v2/apply-to-deploy",
|
|
77
167
|
name: "tailor-sdk apply → tailor-sdk deploy",
|
|
78
|
-
description: "Rewrite `tailor-sdk apply` invocations in package.json scripts, shell scripts, CI configs, and docs to the v2
|
|
168
|
+
description: "Rewrite `tailor-sdk apply` invocations in package.json scripts, shell scripts, CI configs, and docs to the canonical v2 `tailor-sdk deploy` command",
|
|
79
169
|
since: "1.0.0",
|
|
80
170
|
until: "2.0.0",
|
|
81
171
|
scriptPath: "v2/apply-to-deploy/scripts/transform.js",
|
|
82
172
|
filePatterns: [
|
|
83
173
|
"**/package.json",
|
|
84
174
|
"**/*.{sh,bash,zsh,yml,yaml}",
|
|
175
|
+
"**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}",
|
|
85
176
|
"**/*.md"
|
|
86
|
-
]
|
|
177
|
+
],
|
|
178
|
+
examples: [{
|
|
179
|
+
lang: "sh",
|
|
180
|
+
before: "tailor-sdk apply --profile prod",
|
|
181
|
+
after: "tailor-sdk deploy --profile prod"
|
|
182
|
+
}]
|
|
87
183
|
},
|
|
88
184
|
{
|
|
89
185
|
id: "v2/cli-rename",
|
|
90
|
-
name: "v2 CLI rename
|
|
91
|
-
description: "Rewrite `tailor-sdk crash-report`
|
|
186
|
+
name: "v2 CLI rename",
|
|
187
|
+
description: "Rewrite `tailor-sdk crash-report` to `tailor-sdk crashreport` and `--machineuser` to `--machine-user` across package.json scripts, shell scripts, CI configs, and docs",
|
|
92
188
|
since: "1.0.0",
|
|
93
189
|
until: "2.0.0",
|
|
94
190
|
scriptPath: "v2/cli-rename/scripts/transform.js",
|
|
@@ -96,25 +192,236 @@ const allCodemods = [
|
|
|
96
192
|
"**/package.json",
|
|
97
193
|
"**/*.{sh,bash,zsh,yml,yaml}",
|
|
98
194
|
"**/*.md"
|
|
99
|
-
]
|
|
195
|
+
],
|
|
196
|
+
legacyPatterns: ["tailor-sdk crash-report", "--machineuser"],
|
|
197
|
+
examples: [{
|
|
198
|
+
lang: "sh",
|
|
199
|
+
before: "tailor-sdk crash-report list\ntailor-sdk login --machineuser",
|
|
200
|
+
after: "tailor-sdk crashreport list\ntailor-sdk login --machine-user"
|
|
201
|
+
}],
|
|
202
|
+
prompt: [
|
|
203
|
+
"Apply the v2 CLI renames the codemod did not reach (only `tailor-sdk`-prefixed",
|
|
204
|
+
"invocations are rewritten): `tailor-sdk crash-report` -> `tailor-sdk crashreport`",
|
|
205
|
+
"and the `--machineuser` option -> `--machine-user`. Leave unrelated commands that",
|
|
206
|
+
"happen to use `--machineuser` alone."
|
|
207
|
+
].join("\n")
|
|
100
208
|
},
|
|
101
209
|
{
|
|
102
210
|
id: "v2/auth-invoker-unwrap",
|
|
103
|
-
name: "auth.invoker(\"name\") → \"name\"",
|
|
104
|
-
description: "
|
|
211
|
+
name: "auth.invoker(\"name\") → invoker: \"name\"",
|
|
212
|
+
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.",
|
|
105
213
|
since: "1.0.0",
|
|
106
214
|
until: "2.0.0",
|
|
107
215
|
scriptPath: "v2/auth-invoker-unwrap/scripts/transform.js",
|
|
108
|
-
|
|
216
|
+
suspiciousPatterns: [
|
|
217
|
+
"auth.invoker",
|
|
218
|
+
"authInvoker:",
|
|
219
|
+
"authInvoker :",
|
|
220
|
+
"authInvoker?",
|
|
221
|
+
"{ authInvoker",
|
|
222
|
+
", authInvoker",
|
|
223
|
+
"\n authInvoker",
|
|
224
|
+
"\n authInvoker",
|
|
225
|
+
"\n authInvoker",
|
|
226
|
+
"\"authInvoker\":",
|
|
227
|
+
"\"authInvoker\" :",
|
|
228
|
+
"\"authInvoker\"?",
|
|
229
|
+
"'authInvoker':",
|
|
230
|
+
"'authInvoker' :",
|
|
231
|
+
"'authInvoker'?"
|
|
232
|
+
],
|
|
233
|
+
prompt: [
|
|
234
|
+
"In Tailor SDK v2 the auth.invoker() helper is removed; an invoker is now the",
|
|
235
|
+
"machine user name passed directly as a string. The codemod already rewrote the",
|
|
236
|
+
"statically identified SDK option form authInvoker: auth.invoker(\"name\") to invoker: \"name\" and renamed supported authInvoker option keys. These files still contain",
|
|
237
|
+
"auth.invoker(...) calls or authInvoker keys that need manual review.",
|
|
238
|
+
"",
|
|
239
|
+
"For each remaining auth.invoker(<expr>) call:",
|
|
240
|
+
"1. Replace the whole call with <expr> only where the target option expects a",
|
|
241
|
+
" machine user name string; platform/runtime authInvoker payloads still expect",
|
|
242
|
+
" the object form.",
|
|
243
|
+
"2. Rename remaining authInvoker option keys to invoker only for SDK resolver,",
|
|
244
|
+
" executor, workflow.trigger(), or startWorkflow() options. Keep platform/runtime",
|
|
245
|
+
" payload keys such as tailor.workflow.triggerWorkflow(..., { authInvoker: ... }).",
|
|
246
|
+
"3. After removing every auth.invoker usage in a file, delete the now-unused auth",
|
|
247
|
+
" import (keeping it pulls Node-only config modules into runtime bundles); leave",
|
|
248
|
+
" the import if auth is still referenced elsewhere.",
|
|
249
|
+
"",
|
|
250
|
+
"Do not change behavior beyond the SDK option rename and auth.invoker() removal."
|
|
251
|
+
].join("\n"),
|
|
252
|
+
examples: [{
|
|
253
|
+
before: "createResolver({ invoker: auth.invoker(\"manager\") });",
|
|
254
|
+
after: "createResolver({ invoker: \"manager\" });"
|
|
255
|
+
}]
|
|
109
256
|
},
|
|
110
257
|
{
|
|
111
258
|
id: "v2/tailordb-namespace",
|
|
112
259
|
name: "Tailordb → tailordb (lowercase ambient namespace)",
|
|
113
|
-
description: "Rewrite references to the
|
|
260
|
+
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
261
|
since: "1.0.0",
|
|
115
262
|
until: "2.0.0",
|
|
116
263
|
scriptPath: "v2/tailordb-namespace/scripts/transform.js",
|
|
117
|
-
legacyPatterns: ["Tailordb."]
|
|
264
|
+
legacyPatterns: ["Tailordb."],
|
|
265
|
+
examples: [{
|
|
266
|
+
before: "const command: Tailordb.CommandType = \"SELECT\";",
|
|
267
|
+
after: "import \"@tailor-platform/sdk/runtime/globals\";\nconst command: tailordb.CommandType = \"SELECT\";"
|
|
268
|
+
}],
|
|
269
|
+
prompt: [
|
|
270
|
+
"The capital-cased Tailordb ambient namespace is removed in v2; use the lowercase",
|
|
271
|
+
"tailordb.* namespace from @tailor-platform/sdk/runtime/globals. The codemod rewrites",
|
|
272
|
+
"the known members (QueryResult, CommandType, Client). Rewrite any other remaining",
|
|
273
|
+
"Tailordb.* reference to its tailordb.* equivalent (and confirm the member still",
|
|
274
|
+
"exists on the lowercase namespace).",
|
|
275
|
+
"Also add `import \"@tailor-platform/sdk/runtime/globals\"` at the top of each file",
|
|
276
|
+
"that contains any tailordb.* type reference — v2 no longer activates ambient",
|
|
277
|
+
"declarations automatically on SDK import."
|
|
278
|
+
].join("\n")
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
id: "v2/execute-script-arg",
|
|
282
|
+
name: "executeScript arg JSON.stringify → value",
|
|
283
|
+
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
|
+
since: "1.0.0",
|
|
285
|
+
until: "2.0.0",
|
|
286
|
+
scriptPath: "v2/execute-script-arg/scripts/transform.js",
|
|
287
|
+
filePatterns: ["**/*.{ts,tsx,mts,cts,mjs,cjs,js}"],
|
|
288
|
+
suspiciousPatterns: [[
|
|
289
|
+
"executeScript",
|
|
290
|
+
"JSON.stringify",
|
|
291
|
+
/\barg\s*[:=]|["']arg["']\s*(?::|\]\s*[:=])/
|
|
292
|
+
]],
|
|
293
|
+
prompt: [
|
|
294
|
+
"In Tailor SDK v2 the executeScript() arg option takes a JSON-serializable value",
|
|
295
|
+
"and is serialized internally, so a pre-stringified argument double-encodes. The",
|
|
296
|
+
"codemod already rewrote the direct form arg: JSON.stringify(X) to arg: X. Review",
|
|
297
|
+
"the executeScript calls in these files for cases it could not rewrite — where the",
|
|
298
|
+
"arg value is reached indirectly, for example:",
|
|
299
|
+
"- a variable holding a JSON.stringify(...) result (const s = JSON.stringify(x); ... arg: s)",
|
|
300
|
+
"- JSON.stringify(x, null, 2) or another multi-argument form",
|
|
301
|
+
"- an options object built or spread dynamically",
|
|
302
|
+
"",
|
|
303
|
+
"For each such call, pass the underlying value directly as arg (drop the",
|
|
304
|
+
"JSON.stringify wrapper) so executeScript serializes it once. Leave calls that",
|
|
305
|
+
"already pass a plain value unchanged."
|
|
306
|
+
].join("\n"),
|
|
307
|
+
examples: [{
|
|
308
|
+
before: "await executeScript({ ...opts, arg: JSON.stringify({ a: 1 }) });",
|
|
309
|
+
after: "await executeScript({ ...opts, arg: { a: 1 } });"
|
|
310
|
+
}]
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
id: "v2/open-download-stream",
|
|
314
|
+
name: "openDownloadStream → downloadStream",
|
|
315
|
+
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
|
+
since: "1.0.0",
|
|
317
|
+
until: "2.0.0",
|
|
318
|
+
filePatterns: ["**/*.{ts,tsx,mts,cts,mjs,cjs,js}"],
|
|
319
|
+
suspiciousPatterns: ["openDownloadStream", "openFileDownloadStream"],
|
|
320
|
+
examples: [{
|
|
321
|
+
before: "const res = await openDownloadStream(namespace, typeName, fieldName, recordId);",
|
|
322
|
+
after: "const res = await downloadStream(namespace, typeName, fieldName, recordId);"
|
|
323
|
+
}],
|
|
324
|
+
prompt: [
|
|
325
|
+
"The openDownloadStream file-streaming API is removed in v2. Replace every call to",
|
|
326
|
+
"openDownloadStream with downloadStream (same arguments). If you used the generated",
|
|
327
|
+
"openFileDownloadStream helper, switch to downloadFileStream, which calls",
|
|
328
|
+
"downloadStream and returns FileDownloadStreamResponse."
|
|
329
|
+
].join("\n")
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
id: "v2/runtime-globals-opt-in",
|
|
333
|
+
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.)",
|
|
335
|
+
since: "1.0.0",
|
|
336
|
+
until: "2.0.0",
|
|
337
|
+
filePatterns: ["**/*.{ts,tsx,mts,cts}"],
|
|
338
|
+
suspiciousPatterns: [
|
|
339
|
+
"tailor.context",
|
|
340
|
+
"tailor.iconv",
|
|
341
|
+
"tailor.idp",
|
|
342
|
+
"tailor.workflow",
|
|
343
|
+
"tailor[",
|
|
344
|
+
"tailordb.Client",
|
|
345
|
+
"tailordb.CommandType",
|
|
346
|
+
"tailordb.QueryResult",
|
|
347
|
+
"tailordb.file",
|
|
348
|
+
"tailordb[",
|
|
349
|
+
"TailorDBFileError",
|
|
350
|
+
"TailorErrorItem",
|
|
351
|
+
"TailorErrorMessage",
|
|
352
|
+
"TailorErrors"
|
|
353
|
+
],
|
|
354
|
+
examples: [{
|
|
355
|
+
caption: "Preferred: switch to the typed wrappers from `@tailor-platform/sdk/runtime` and drop the ambient globals:",
|
|
356
|
+
before: "const client = new tailor.idp.Client();",
|
|
357
|
+
after: "import { idp } from \"@tailor-platform/sdk/runtime\";\nconst client = new idp.Client({ namespace: \"my-namespace\" });"
|
|
358
|
+
}, {
|
|
359
|
+
caption: "Fallback: only if you must keep referencing the bare `tailor.*` names, opt into the global declarations:",
|
|
360
|
+
before: "const client = new tailor.idp.Client();",
|
|
361
|
+
after: "import \"@tailor-platform/sdk/runtime/globals\";\nconst client = new tailor.idp.Client();"
|
|
362
|
+
}],
|
|
363
|
+
prompt: [
|
|
364
|
+
"The v2 SDK no longer enables ambient Tailor runtime globals from",
|
|
365
|
+
"`@tailor-platform/sdk`. For each flagged file that uses `tailor.*`,",
|
|
366
|
+
"`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.",
|
|
371
|
+
"",
|
|
372
|
+
"Only when the file must keep referencing the bare `tailor.*` names directly,",
|
|
373
|
+
"opt into the global declarations instead by adding one of these:",
|
|
374
|
+
"- per-file: `import \"@tailor-platform/sdk/runtime/globals\";`",
|
|
375
|
+
"- project-wide: `\"types\": [\"@tailor-platform/sdk/runtime/globals\"]` in",
|
|
376
|
+
" the relevant tsconfig compilerOptions",
|
|
377
|
+
"",
|
|
378
|
+
"Leave files unchanged when the matching name is local, imported from another",
|
|
379
|
+
"module, or appears only in comments or strings."
|
|
380
|
+
].join("\n")
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
id: "v2/workflow-trigger-dispatch",
|
|
384
|
+
name: "Workflow .trigger() and trigger tests",
|
|
385
|
+
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
|
+
since: "1.0.0",
|
|
387
|
+
until: "2.0.0",
|
|
388
|
+
suspiciousPatterns: [".trigger("],
|
|
389
|
+
examples: [{
|
|
390
|
+
caption: "Tests must mock the workflow runtime instead of running bodies locally:",
|
|
391
|
+
before: "const result = await orderJob.trigger({ id });\nexpect(result.status).toBe(\"done\");",
|
|
392
|
+
after: "using wf = mockWorkflow();\nwf.setJobHandler((jobName) => (jobName === \"order-job\" ? { status: \"done\" } : null));\nconst result = await orderJob.trigger({ id });\nexpect(result.status).toBe(\"done\");"
|
|
393
|
+
}],
|
|
394
|
+
prompt: [
|
|
395
|
+
"Workflow job .trigger() now uses the platform workflow runtime instead of running",
|
|
396
|
+
"the job body locally. In tests, acquire `using wf = mockWorkflow()` and provide",
|
|
397
|
+
"trigger responses (setJobHandler / enqueueResult), or use runWorkflowLocally() for a",
|
|
398
|
+
"full-chain local run; an unmocked trigger now throws. Outside tests, treat the",
|
|
399
|
+
"trigger result as the job output directly (no Promise wrapper to unwrap)."
|
|
400
|
+
].join("\n")
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
id: "v2/cli-token-keyring-storage",
|
|
404
|
+
name: "CLI tokens stored in the OS keyring",
|
|
405
|
+
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
|
+
since: "1.0.0",
|
|
407
|
+
until: "2.0.0",
|
|
408
|
+
notice: true
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
id: "v2/cli-users-by-subject",
|
|
412
|
+
name: "CLI users keyed by subject ID",
|
|
413
|
+
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
|
+
since: "1.0.0",
|
|
415
|
+
until: "2.0.0",
|
|
416
|
+
notice: true
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
id: "v2/function-logs-content-hash",
|
|
420
|
+
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.",
|
|
422
|
+
since: "1.0.0",
|
|
423
|
+
until: "2.0.0",
|
|
424
|
+
notice: true
|
|
118
425
|
}
|
|
119
426
|
];
|
|
120
427
|
/**
|
|
@@ -147,6 +454,44 @@ const EXCLUDE_DIRS = new Set([
|
|
|
147
454
|
"dist",
|
|
148
455
|
".git"
|
|
149
456
|
]);
|
|
457
|
+
const ALLOWED_DOT_DIRS = new Set([".github", ".circleci"]);
|
|
458
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
459
|
+
".ts",
|
|
460
|
+
".tsx",
|
|
461
|
+
".mts",
|
|
462
|
+
".cts",
|
|
463
|
+
".js",
|
|
464
|
+
".jsx",
|
|
465
|
+
".mjs",
|
|
466
|
+
".cjs"
|
|
467
|
+
]);
|
|
468
|
+
const MASKED_SOURCE_NODE_KINDS = new Set([
|
|
469
|
+
"comment",
|
|
470
|
+
"string",
|
|
471
|
+
"regex",
|
|
472
|
+
"string_fragment"
|
|
473
|
+
]);
|
|
474
|
+
function shouldSkipDirectory(name) {
|
|
475
|
+
return EXCLUDE_DIRS.has(name) || name.startsWith(".") && !ALLOWED_DOT_DIRS.has(name);
|
|
476
|
+
}
|
|
477
|
+
async function* walkFiles(root, relativeDir = "") {
|
|
478
|
+
const absoluteDir = path.join(root, relativeDir);
|
|
479
|
+
let entries;
|
|
480
|
+
try {
|
|
481
|
+
entries = await fs.promises.readdir(absoluteDir, { withFileTypes: true });
|
|
482
|
+
} catch {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
for (const entry of entries) {
|
|
486
|
+
const relative = relativeDir ? path.join(relativeDir, entry.name) : entry.name;
|
|
487
|
+
if (entry.isDirectory()) {
|
|
488
|
+
if (shouldSkipDirectory(entry.name)) continue;
|
|
489
|
+
yield* walkFiles(root, relative);
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
if (entry.isFile()) yield relative;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
150
495
|
/**
|
|
151
496
|
* Print a colorized unified diff for a single file to stderr.
|
|
152
497
|
* @param filePath - Absolute path to the file
|
|
@@ -176,6 +521,73 @@ async function loadTransform(scriptPath) {
|
|
|
176
521
|
if (typeof mod.default !== "function") throw new Error(`Transform at ${scriptPath} does not have a default export function`);
|
|
177
522
|
return mod.default;
|
|
178
523
|
}
|
|
524
|
+
function contentForResidualMatching(relative, content) {
|
|
525
|
+
const ext = path.extname(relative).toLowerCase();
|
|
526
|
+
return SOURCE_EXTENSIONS.has(ext) ? maskSourceNonCode(relative, content) : content;
|
|
527
|
+
}
|
|
528
|
+
function sourceLang(relative) {
|
|
529
|
+
const ext = path.extname(relative).toLowerCase();
|
|
530
|
+
return ext === ".tsx" || ext === ".jsx" ? Lang.Tsx : Lang.TypeScript;
|
|
531
|
+
}
|
|
532
|
+
function collectMaskedRanges(root) {
|
|
533
|
+
const ranges = [];
|
|
534
|
+
const visit = (node) => {
|
|
535
|
+
if (MASKED_SOURCE_NODE_KINDS.has(node.kind())) {
|
|
536
|
+
const range = node.range();
|
|
537
|
+
ranges.push([range.start.index, range.end.index]);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
for (const child of node.children()) visit(child);
|
|
541
|
+
};
|
|
542
|
+
visit(root);
|
|
543
|
+
return ranges;
|
|
544
|
+
}
|
|
545
|
+
function maskSourceNonCode(relative, content) {
|
|
546
|
+
let ranges;
|
|
547
|
+
try {
|
|
548
|
+
ranges = collectMaskedRanges(parse(sourceLang(relative), content).root());
|
|
549
|
+
} catch {
|
|
550
|
+
return content;
|
|
551
|
+
}
|
|
552
|
+
ranges = ranges.toSorted(([a], [b]) => a - b);
|
|
553
|
+
const chars = content.split("");
|
|
554
|
+
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] = " ";
|
|
555
|
+
return chars.join("");
|
|
556
|
+
}
|
|
557
|
+
function isIdentifierChar(char) {
|
|
558
|
+
return char != null && /^[A-Za-z0-9_$]$/.test(char);
|
|
559
|
+
}
|
|
560
|
+
function matchesPattern(content, pattern) {
|
|
561
|
+
if (typeof pattern === "string") {
|
|
562
|
+
const checkLeft = isIdentifierChar(pattern[0]);
|
|
563
|
+
const checkRight = isIdentifierChar(pattern.at(-1));
|
|
564
|
+
let index = content.indexOf(pattern);
|
|
565
|
+
while (index !== -1) {
|
|
566
|
+
const before = index > 0 ? content[index - 1] : void 0;
|
|
567
|
+
const after = content[index + pattern.length];
|
|
568
|
+
if ((!checkLeft || !isIdentifierChar(before)) && (!checkRight || !isIdentifierChar(after))) return true;
|
|
569
|
+
index = content.indexOf(pattern, index + 1);
|
|
570
|
+
}
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
pattern.lastIndex = 0;
|
|
574
|
+
return pattern.test(content);
|
|
575
|
+
}
|
|
576
|
+
function patternLabel(pattern) {
|
|
577
|
+
return typeof pattern === "string" ? pattern : pattern.toString();
|
|
578
|
+
}
|
|
579
|
+
/** Resolve a residual pattern against content, returning its label when matched. */
|
|
580
|
+
function matchResidualPattern(content, pattern) {
|
|
581
|
+
if (!Array.isArray(pattern)) return matchesPattern(content, pattern) ? patternLabel(pattern) : null;
|
|
582
|
+
return pattern.every((p) => matchesPattern(content, p)) ? pattern.map((p) => patternLabel(p)).join(" + ") : null;
|
|
583
|
+
}
|
|
584
|
+
function legacyPatternWarnings(relative, content, transforms) {
|
|
585
|
+
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.`];
|
|
589
|
+
});
|
|
590
|
+
}
|
|
179
591
|
/**
|
|
180
592
|
* Run multiple codemods on a project directory using in-memory chaining.
|
|
181
593
|
* Each file is processed through all transforms whose filePatterns match it.
|
|
@@ -193,24 +605,24 @@ async function runCodemods(codemods, targetPath, dryRun) {
|
|
|
193
605
|
const patterns = codemod.filePatterns ?? DEFAULT_FILE_PATTERNS;
|
|
194
606
|
loaded.push({
|
|
195
607
|
id: codemod.id,
|
|
196
|
-
transform: await loadTransform(scriptPath),
|
|
197
|
-
matches: picomatch(patterns),
|
|
198
|
-
legacyPatterns: codemod.legacyPatterns ?? []
|
|
608
|
+
transform: scriptPath ? await loadTransform(scriptPath) : void 0,
|
|
609
|
+
matches: picomatch(patterns, { dot: true }),
|
|
610
|
+
legacyPatterns: codemod.legacyPatterns ?? [],
|
|
611
|
+
suspiciousPatterns: codemod.suspiciousPatterns ?? [],
|
|
612
|
+
prompt: codemod.prompt
|
|
199
613
|
});
|
|
200
614
|
}
|
|
201
|
-
const allPatterns = /* @__PURE__ */ new Set();
|
|
202
|
-
for (const { codemod } of codemods) for (const p of codemod.filePatterns ?? DEFAULT_FILE_PATTERNS) allPatterns.add(p);
|
|
203
615
|
const filesModified = [];
|
|
204
616
|
const warnings = [];
|
|
205
617
|
const appliedCodemodIds = /* @__PURE__ */ new Set();
|
|
206
618
|
const seen = /* @__PURE__ */ new Set();
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
exclude: (name) => EXCLUDE_DIRS.has(name)
|
|
210
|
-
})) {
|
|
619
|
+
const suspiciousByCodemod = /* @__PURE__ */ new Map();
|
|
620
|
+
for await (const relative of walkFiles(targetPath)) {
|
|
211
621
|
const absolute = path.resolve(targetPath, relative);
|
|
212
622
|
if (seen.has(absolute)) continue;
|
|
213
623
|
seen.add(absolute);
|
|
624
|
+
const matchedTransforms = loaded.filter((lt) => lt.matches(relative));
|
|
625
|
+
if (matchedTransforms.length === 0) continue;
|
|
214
626
|
let original;
|
|
215
627
|
try {
|
|
216
628
|
original = await fs.promises.readFile(absolute, "utf-8");
|
|
@@ -218,10 +630,8 @@ async function runCodemods(codemods, targetPath, dryRun) {
|
|
|
218
630
|
continue;
|
|
219
631
|
}
|
|
220
632
|
let current = original;
|
|
221
|
-
const matchedTransforms
|
|
222
|
-
|
|
223
|
-
if (!lt.matches(relative)) continue;
|
|
224
|
-
matchedTransforms.push(lt);
|
|
633
|
+
for (const lt of matchedTransforms) {
|
|
634
|
+
if (!lt.transform) continue;
|
|
225
635
|
const result = await lt.transform(current, absolute);
|
|
226
636
|
if (result != null) {
|
|
227
637
|
current = result;
|
|
@@ -232,24 +642,95 @@ async function runCodemods(codemods, targetPath, dryRun) {
|
|
|
232
642
|
filesModified.push(absolute);
|
|
233
643
|
if (dryRun) printDiff(absolute, original, current);
|
|
234
644
|
else await fs.promises.writeFile(absolute, current, "utf-8");
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
|
|
645
|
+
}
|
|
646
|
+
const residualContent = contentForResidualMatching(relative, current);
|
|
647
|
+
warnings.push(...legacyPatternWarnings(relative, residualContent, matchedTransforms));
|
|
648
|
+
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);
|
|
654
|
+
}
|
|
238
655
|
}
|
|
239
656
|
}
|
|
657
|
+
const llmReviews = [];
|
|
658
|
+
for (const lt of loaded) {
|
|
659
|
+
if (!lt.prompt) continue;
|
|
660
|
+
if (lt.suspiciousPatterns.length > 0) {
|
|
661
|
+
const files = suspiciousByCodemod.get(lt.id);
|
|
662
|
+
if (files) llmReviews.push({
|
|
663
|
+
codemodId: lt.id,
|
|
664
|
+
prompt: lt.prompt,
|
|
665
|
+
files: files.toSorted()
|
|
666
|
+
});
|
|
667
|
+
} else if (lt.legacyPatterns.length === 0) llmReviews.push({
|
|
668
|
+
codemodId: lt.id,
|
|
669
|
+
prompt: lt.prompt,
|
|
670
|
+
files: []
|
|
671
|
+
});
|
|
672
|
+
}
|
|
240
673
|
return {
|
|
241
674
|
changed: filesModified.length > 0,
|
|
242
675
|
filesModified,
|
|
243
676
|
warnings,
|
|
244
|
-
appliedCodemodIds
|
|
677
|
+
appliedCodemodIds,
|
|
678
|
+
llmReviews
|
|
245
679
|
};
|
|
246
680
|
}
|
|
247
681
|
//#endregion
|
|
248
682
|
//#region src/index.ts
|
|
249
683
|
const packageJson = await readPackageJSON(path.dirname(fileURLToPath(import.meta.url)) + "/..");
|
|
684
|
+
const listCommand = defineCommand({
|
|
685
|
+
name: "list",
|
|
686
|
+
description: "List the available codemod rules (id, name, kind, version range).",
|
|
687
|
+
args: z.object({}).strict(),
|
|
688
|
+
run: () => {
|
|
689
|
+
const rules = allCodemods.map((codemod) => ({
|
|
690
|
+
id: codemod.id,
|
|
691
|
+
name: codemod.name,
|
|
692
|
+
kind: codemod.notice ? "Notice" : automationLevel(codemod),
|
|
693
|
+
since: codemod.since,
|
|
694
|
+
until: codemod.until
|
|
695
|
+
}));
|
|
696
|
+
for (const rule of rules) process.stderr.write(` ${rule.id} [${rule.kind}] ${rule.name}\n`);
|
|
697
|
+
process.stdout.write(JSON.stringify(rules) + "\n");
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
/**
|
|
701
|
+
* Print an LLM-assisted review task to stderr: the flagged files plus the
|
|
702
|
+
* codemod's migration prompt, ready to hand to an LLM for the cases the
|
|
703
|
+
* deterministic transform could not complete on its own.
|
|
704
|
+
* @param review - The review task (codemod id, prompt, files)
|
|
705
|
+
*/
|
|
706
|
+
function printLlmReview(review) {
|
|
707
|
+
const scope = review.files.length > 0 ? "the codemod cannot safely migrate these automatically" : "review the project for this manual change";
|
|
708
|
+
process.stderr.write(`\n🤖 LLM-assisted review suggested (${review.codemodId}) — ${scope}:\n`);
|
|
709
|
+
for (const file of review.files) process.stderr.write(` - ${file}\n`);
|
|
710
|
+
process.stderr.write(`\nPrompt for an LLM:\n${review.prompt.trim()}\n`);
|
|
711
|
+
}
|
|
250
712
|
runMain(defineCommand({
|
|
251
713
|
name: packageJson.name ?? "sdk-codemod",
|
|
252
714
|
description: packageJson.description ?? "Codemod runner for Tailor Platform SDK upgrades",
|
|
715
|
+
subCommands: { list: listCommand },
|
|
716
|
+
notes: `Applies the codemods matching the \`--from\`/\`--to\` version range to the
|
|
717
|
+
\`--target\` directory, then writes a JSON summary to \`stdout\`:
|
|
718
|
+
|
|
719
|
+
- \`filesModified\`: files a codemod changed
|
|
720
|
+
- \`warnings\`: files that may still need manual migration
|
|
721
|
+
- \`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.
|
|
724
|
+
|
|
725
|
+
Progress, warnings, and the LLM-review prompts are also printed to \`stderr\` in
|
|
726
|
+
human-readable form, so \`stdout\` stays pure JSON for piping.`,
|
|
727
|
+
examples: [{
|
|
728
|
+
cmd: "--from 1.64.0 --to 2.0.0",
|
|
729
|
+
desc: "Apply every codemod for the 1.64.0 -> 2.0.0 upgrade to the current project"
|
|
730
|
+
}, {
|
|
731
|
+
cmd: "--from 1.64.0 --to 2.0.0 --dry-run",
|
|
732
|
+
desc: "Preview the changes and any LLM-review prompts without writing files"
|
|
733
|
+
}],
|
|
253
734
|
args: z.object({
|
|
254
735
|
from: arg(z.string(), { description: "Source SDK version (the version before upgrade)" }),
|
|
255
736
|
to: arg(z.string(), { description: "Target SDK version (the version after upgrade)" }),
|
|
@@ -268,7 +749,8 @@ runMain(defineCommand({
|
|
|
268
749
|
codemodsSkipped: 0,
|
|
269
750
|
filesModified: [],
|
|
270
751
|
warnings: [],
|
|
271
|
-
errors: []
|
|
752
|
+
errors: [],
|
|
753
|
+
llmReviews: []
|
|
272
754
|
};
|
|
273
755
|
if (codemods.length === 0) {
|
|
274
756
|
process.stdout.write(JSON.stringify(output) + "\n");
|
|
@@ -276,7 +758,7 @@ runMain(defineCommand({
|
|
|
276
758
|
}
|
|
277
759
|
const codemodEntries = codemods.map((codemod) => ({
|
|
278
760
|
codemod,
|
|
279
|
-
scriptPath: resolveCodemodScript(codemod.scriptPath)
|
|
761
|
+
scriptPath: codemod.scriptPath ? resolveCodemodScript(codemod.scriptPath) : void 0
|
|
280
762
|
}));
|
|
281
763
|
for (const { codemod } of codemodEntries) process.stderr.write(`Running: ${codemod.name} - ${codemod.description}\n`);
|
|
282
764
|
try {
|
|
@@ -285,8 +767,10 @@ runMain(defineCommand({
|
|
|
285
767
|
output.codemodsSkipped = codemods.length - result.appliedCodemodIds.size;
|
|
286
768
|
output.filesModified = result.filesModified;
|
|
287
769
|
output.warnings = result.warnings;
|
|
770
|
+
output.llmReviews = result.llmReviews;
|
|
288
771
|
if (result.changed) process.stderr.write(` ${result.filesModified.length} file(s) modified\n`);
|
|
289
772
|
else process.stderr.write(" No changes needed\n");
|
|
773
|
+
for (const review of output.llmReviews) printLlmReview(review);
|
|
290
774
|
} catch (error) {
|
|
291
775
|
const message = error instanceof Error ? error.message : String(error);
|
|
292
776
|
output.errors.push({
|