@synchronized-studio/cmsassets-agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,960 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ applyPlan,
4
+ createPlan,
5
+ createReport,
6
+ gitOps,
7
+ loadPlanFile,
8
+ savePlanFile,
9
+ saveReport,
10
+ scan,
11
+ verify
12
+ } from "./chunk-J6E3CMX6.js";
13
+ import "./chunk-645ASJ55.js";
14
+ import "./chunk-QGM4M3NI.js";
15
+
16
+ // src/cli.ts
17
+ import { existsSync as existsSync4 } from "fs";
18
+ import { dirname as pathDirname, resolve as resolve6 } from "path";
19
+ import { fileURLToPath } from "url";
20
+ import { defineCommand as defineCommand8, runMain } from "citty";
21
+ import { config as dotenvConfig } from "dotenv";
22
+
23
+ // src/commands/init.ts
24
+ import { createInterface } from "readline";
25
+ import { resolve } from "path";
26
+ import { defineCommand } from "citty";
27
+ import consola2 from "consola";
28
+ import pc from "picocolors";
29
+
30
+ // src/patcher/injectScript.ts
31
+ import { existsSync, readFileSync, writeFileSync } from "fs";
32
+ import { join } from "path";
33
+ import consola from "consola";
34
+ var SCRIPT_NAME = "cmsassets:transform";
35
+ var SCRIPT_VALUE = "npx @synchronized-studio/cmsassets-agent apply";
36
+ function injectNpmScript(projectRoot) {
37
+ const pkgPath = join(projectRoot, "package.json");
38
+ if (!existsSync(pkgPath)) {
39
+ return { added: false, reason: "No package.json found" };
40
+ }
41
+ const raw = readFileSync(pkgPath, "utf-8");
42
+ let pkg;
43
+ try {
44
+ pkg = JSON.parse(raw);
45
+ } catch {
46
+ return { added: false, reason: "Could not parse package.json" };
47
+ }
48
+ if (!pkg.scripts) pkg.scripts = {};
49
+ if (pkg.scripts[SCRIPT_NAME]) {
50
+ return { added: false, reason: "Script already exists" };
51
+ }
52
+ pkg.scripts[SCRIPT_NAME] = SCRIPT_VALUE;
53
+ const indentMatch = raw.match(/^(\s+)"/m);
54
+ const indent = indentMatch?.[1] ?? " ";
55
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, indent) + "\n", "utf-8");
56
+ consola.success(`Added "${SCRIPT_NAME}" script to package.json`);
57
+ return { added: true };
58
+ }
59
+
60
+ // src/commands/init.ts
61
+ function confirm(question) {
62
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
63
+ return new Promise((resolve7) => {
64
+ rl.question(question, (answer) => {
65
+ rl.close();
66
+ resolve7(answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes");
67
+ });
68
+ });
69
+ }
70
+ var initCommand = defineCommand({
71
+ meta: {
72
+ name: "init",
73
+ description: "One-command setup: scan project, install transformer, patch files, add npm script."
74
+ },
75
+ args: {
76
+ dir: {
77
+ type: "string",
78
+ description: "Project root directory",
79
+ default: "."
80
+ },
81
+ yes: {
82
+ type: "boolean",
83
+ description: "Skip confirmation prompt",
84
+ default: false
85
+ },
86
+ "dry-run": {
87
+ type: "boolean",
88
+ description: "Preview changes without writing files",
89
+ default: false
90
+ },
91
+ slug: {
92
+ type: "string",
93
+ description: "Tenant slug for CMS_ASSETS_URL"
94
+ },
95
+ cms: {
96
+ type: "string",
97
+ description: "Override CMS type"
98
+ },
99
+ repository: {
100
+ type: "string",
101
+ description: "Override Prismic repository name"
102
+ },
103
+ spaceId: {
104
+ type: "string",
105
+ description: "Override Contentful space ID"
106
+ },
107
+ "no-install": {
108
+ type: "boolean",
109
+ description: "Skip installing @synchronized-studio/response-transformer",
110
+ default: false
111
+ },
112
+ "no-script": {
113
+ type: "boolean",
114
+ description: "Skip adding cmsassets:transform npm script",
115
+ default: false
116
+ },
117
+ llm: {
118
+ type: "boolean",
119
+ description: "Enable LLM fallback for low-confidence patches when AST fails",
120
+ default: false
121
+ },
122
+ "include-tests": {
123
+ type: "boolean",
124
+ description: "Also patch test/fixture files",
125
+ default: false
126
+ }
127
+ },
128
+ async run({ args }) {
129
+ const root = resolve(args.dir);
130
+ consola2.log("");
131
+ consola2.log(pc.bold(" CMS Assets Agent \u2014 Init"));
132
+ consola2.log(pc.dim(" \u2500".repeat(25)));
133
+ consola2.log("");
134
+ const scanResult = scan(root);
135
+ if (args.cms) scanResult.cms.type = args.cms;
136
+ if (args.repository) scanResult.cms.params.repository = args.repository;
137
+ if (args.spaceId) scanResult.cms.params.spaceId = args.spaceId;
138
+ consola2.log(` ${pc.cyan("Framework:")} ${scanResult.framework.name} ${pc.dim(scanResult.framework.version)}`);
139
+ consola2.log(` ${pc.cyan("CMS:")} ${scanResult.cms.type}`);
140
+ consola2.log(` ${pc.cyan("Package mgr:")} ${scanResult.packageManager}`);
141
+ consola2.log("");
142
+ if (scanResult.injectionPoints.length === 0) {
143
+ consola2.warn(" No injection points found. Nothing to do.");
144
+ return;
145
+ }
146
+ consola2.log(` ${pc.bold("Injection points:")} ${scanResult.injectionPoints.length} found`);
147
+ consola2.log("");
148
+ for (const point of scanResult.injectionPoints) {
149
+ const color = point.confidence === "high" ? pc.green : point.confidence === "medium" ? pc.yellow : pc.red;
150
+ consola2.log(
151
+ ` ${color("\u25CF")} ${point.filePath} ${pc.dim(`[${point.confidence}]`)}`
152
+ );
153
+ }
154
+ consola2.log("");
155
+ if (!args.yes && !args["dry-run"]) {
156
+ const ok = await confirm(` Proceed with integration? ${pc.dim("(y/N)")} `);
157
+ if (!ok) {
158
+ consola2.log(" Aborted.");
159
+ return;
160
+ }
161
+ consola2.log("");
162
+ }
163
+ const plan = createPlan(scanResult);
164
+ if (args.slug) plan.env.placeholder = `https://${args.slug}.cmsassets.com`;
165
+ if (args.llm) plan.policies.allowLlmFallback = true;
166
+ if (args["dry-run"]) {
167
+ consola2.log(pc.bold(" Dry Run \u2014 planned changes:"));
168
+ consola2.log("");
169
+ consola2.log(` ${pc.cyan("Install:")} ${plan.install.command}`);
170
+ consola2.log(` ${pc.cyan("Env:")} ${plan.env.key}=${plan.env.placeholder}`);
171
+ consola2.log("");
172
+ for (const patch of plan.patches) {
173
+ const color = patch.confidence === "high" ? pc.green : patch.confidence === "medium" ? pc.yellow : pc.red;
174
+ consola2.log(` ${color("\u25CF")} ${patch.filePath} ${pc.dim(`[${patch.confidence}]`)}`);
175
+ consola2.log(` ${pc.dim(patch.wrapTarget.originalCode)}`);
176
+ consola2.log(` ${pc.green("\u2192 " + patch.wrapTarget.transformedCode)}`);
177
+ }
178
+ consola2.log("");
179
+ consola2.log(` ${pc.dim("Run without --dry-run to apply.")}`);
180
+ return;
181
+ }
182
+ let installed = false;
183
+ if (!args["no-install"]) {
184
+ consola2.info(`Installing: ${plan.install.command}`);
185
+ try {
186
+ const { execSync } = await import("child_process");
187
+ execSync(plan.install.command, { cwd: root, stdio: "inherit" });
188
+ installed = true;
189
+ } catch {
190
+ consola2.warn("Package install failed. You can install it manually later.");
191
+ }
192
+ }
193
+ consola2.info("Applying patches...");
194
+ const applyReport = await applyPlan(plan, {
195
+ dryRun: false,
196
+ includeTests: args["include-tests"]
197
+ });
198
+ applyReport.installed = installed;
199
+ let scriptAdded = false;
200
+ if (!args["no-script"]) {
201
+ const result = injectNpmScript(root);
202
+ scriptAdded = result.added;
203
+ }
204
+ const report = createReport({ apply: applyReport, plan, scan: scanResult });
205
+ saveReport(root, report);
206
+ const applied = applyReport.files.filter((f) => f.applied);
207
+ const skipped = applyReport.files.filter((f) => !f.applied);
208
+ consola2.log("");
209
+ consola2.log(pc.bold(" CMS Assets Agent \u2014 Init Complete"));
210
+ consola2.log(pc.dim(" \u2500".repeat(25)));
211
+ consola2.log("");
212
+ consola2.log(` ${pc.green("Applied:")} ${applied.length} files`);
213
+ consola2.log(` ${pc.yellow("Skipped:")} ${skipped.length} files`);
214
+ if (installed) consola2.log(` ${pc.green("Installed:")} @synchronized-studio/response-transformer`);
215
+ if (scriptAdded) consola2.log(` ${pc.green("Script:")} Added "cmsassets:transform" to package.json`);
216
+ consola2.log("");
217
+ for (const file of applied) {
218
+ consola2.log(` ${pc.green("\u2713")} ${file.filePath} ${pc.dim(`(${file.method}, ${file.durationMs}ms)`)}`);
219
+ }
220
+ for (const file of skipped) {
221
+ consola2.log(` ${pc.yellow("\u25CB")} ${file.filePath} ${pc.dim(`\u2014 ${file.reason}`)}`);
222
+ }
223
+ consola2.log("");
224
+ if (!installed) {
225
+ consola2.log(` ${pc.dim("Don't forget to install:")} ${pc.cyan(plan.install.command)}`);
226
+ }
227
+ if (!applyReport.envUpdated) {
228
+ consola2.log(` ${pc.dim("Set your env var:")} ${pc.cyan(`${plan.env.key}=${plan.env.placeholder}`)}`);
229
+ }
230
+ if (scriptAdded) {
231
+ consola2.log(` ${pc.dim("Re-run later with:")} ${pc.cyan("npm run cmsassets:transform")}`);
232
+ }
233
+ consola2.log("");
234
+ }
235
+ });
236
+
237
+ // src/commands/scan.ts
238
+ import { defineCommand as defineCommand2 } from "citty";
239
+ import consola3 from "consola";
240
+ import pc2 from "picocolors";
241
+ var scanCommand = defineCommand2({
242
+ meta: {
243
+ name: "scan",
244
+ description: "Scan current project for framework, CMS, and injection points."
245
+ },
246
+ args: {
247
+ dir: {
248
+ type: "string",
249
+ description: "Project root directory",
250
+ default: "."
251
+ },
252
+ json: {
253
+ type: "boolean",
254
+ description: "Output raw JSON instead of formatted text",
255
+ default: false
256
+ }
257
+ },
258
+ async run({ args }) {
259
+ const result = scan(args.dir);
260
+ if (args.json) {
261
+ console.log(JSON.stringify(result, null, 2));
262
+ return;
263
+ }
264
+ consola3.log("");
265
+ consola3.log(pc2.bold(" CMS Assets Agent \u2014 Scan Results"));
266
+ consola3.log(pc2.dim(" \u2500".repeat(25)));
267
+ consola3.log("");
268
+ consola3.log(` ${pc2.cyan("Framework:")} ${result.framework.name} ${pc2.dim(result.framework.version)}`);
269
+ if (result.framework.configFile) {
270
+ consola3.log(` ${pc2.dim("Config:")} ${result.framework.configFile}`);
271
+ }
272
+ consola3.log(` ${pc2.cyan("CMS:")} ${result.cms.type}`);
273
+ if (Object.keys(result.cms.params).length > 0) {
274
+ for (const [key, val] of Object.entries(result.cms.params)) {
275
+ consola3.log(` ${pc2.dim(key + ":")} ${val}`);
276
+ }
277
+ }
278
+ if (result.cms.detectedFrom.length > 0) {
279
+ consola3.log(` ${pc2.dim("Detected from:")} ${result.cms.detectedFrom.slice(0, 3).join(", ")}`);
280
+ }
281
+ consola3.log(` ${pc2.cyan("Package mgr:")} ${result.packageManager}`);
282
+ consola3.log("");
283
+ if (result.injectionPoints.length === 0) {
284
+ consola3.warn(" No injection points found.");
285
+ return;
286
+ }
287
+ consola3.log(` ${pc2.bold("Injection points:")} ${result.injectionPoints.length} found`);
288
+ consola3.log("");
289
+ for (let i = 0; i < result.injectionPoints.length; i++) {
290
+ const point = result.injectionPoints[i];
291
+ const confidenceColor = point.confidence === "high" ? pc2.green : point.confidence === "medium" ? pc2.yellow : pc2.red;
292
+ consola3.log(
293
+ ` ${pc2.bold(String(i + 1) + ".")} ${point.filePath} ${confidenceColor(`[${point.confidence.toUpperCase()}]`)} ${pc2.dim(`score: ${point.score}`)}`
294
+ );
295
+ consola3.log(` ${pc2.dim(`Line ${point.line}:`)} ${point.type}`);
296
+ for (const reason of point.reasons) {
297
+ consola3.log(` ${pc2.dim("\u2022")} ${reason}`);
298
+ }
299
+ consola3.log("");
300
+ }
301
+ }
302
+ });
303
+
304
+ // src/commands/plan.ts
305
+ import { defineCommand as defineCommand3 } from "citty";
306
+ import consola4 from "consola";
307
+ import pc3 from "picocolors";
308
+ var planCommand = defineCommand3({
309
+ meta: {
310
+ name: "plan",
311
+ description: "Generate a plan file (cmsassets-agent.plan.json) for review before applying."
312
+ },
313
+ args: {
314
+ dir: {
315
+ type: "string",
316
+ description: "Project root directory",
317
+ default: "."
318
+ },
319
+ json: {
320
+ type: "boolean",
321
+ description: "Output raw JSON to stdout instead of saving a file",
322
+ default: false
323
+ },
324
+ cms: {
325
+ type: "string",
326
+ description: "Override CMS type (prismic, contentful, sanity, shopify, cloudinary, imgix, generic)"
327
+ },
328
+ repository: {
329
+ type: "string",
330
+ description: "Override Prismic repository name"
331
+ },
332
+ spaceId: {
333
+ type: "string",
334
+ description: "Override Contentful space ID"
335
+ },
336
+ slug: {
337
+ type: "string",
338
+ description: "Your cmsassets.com tenant slug (for CMS_ASSETS_URL placeholder)"
339
+ }
340
+ },
341
+ async run({ args }) {
342
+ const scanResult = scan(args.dir);
343
+ if (args.cms) {
344
+ scanResult.cms.type = args.cms;
345
+ }
346
+ if (args.repository) {
347
+ scanResult.cms.params.repository = args.repository;
348
+ }
349
+ if (args.spaceId) {
350
+ scanResult.cms.params.spaceId = args.spaceId;
351
+ }
352
+ const plan = createPlan(scanResult);
353
+ if (args.slug) {
354
+ plan.env.placeholder = `https://${args.slug}.cmsassets.com`;
355
+ }
356
+ if (args.json) {
357
+ console.log(JSON.stringify(plan, null, 2));
358
+ return;
359
+ }
360
+ const filePath = savePlanFile(scanResult.projectRoot, plan);
361
+ consola4.log("");
362
+ consola4.log(pc3.bold(" CMS Assets Agent \u2014 Plan Generated"));
363
+ consola4.log(pc3.dim(" \u2500".repeat(25)));
364
+ consola4.log("");
365
+ consola4.log(` ${pc3.cyan("Plan file:")} ${filePath}`);
366
+ consola4.log(` ${pc3.cyan("Install:")} ${plan.install.command}`);
367
+ consola4.log(` ${pc3.cyan("Env var:")} ${plan.env.key}=${plan.env.placeholder}`);
368
+ consola4.log(` ${pc3.cyan("Patches:")} ${plan.patches.length} files`);
369
+ consola4.log("");
370
+ for (let i = 0; i < plan.patches.length; i++) {
371
+ const patch = plan.patches[i];
372
+ const confidenceColor = patch.confidence === "high" ? pc3.green : patch.confidence === "medium" ? pc3.yellow : pc3.red;
373
+ consola4.log(` ${pc3.bold(String(i + 1) + ".")} ${patch.filePath} ${confidenceColor(`[${patch.confidence.toUpperCase()}]`)}`);
374
+ consola4.log(` ${patch.description}`);
375
+ consola4.log(` ${pc3.dim("Line " + patch.wrapTarget.line + ":")} ${pc3.red(patch.wrapTarget.originalCode)}`);
376
+ consola4.log(` ${pc3.dim(" \u2192")} ${pc3.green(patch.wrapTarget.transformedCode)}`);
377
+ consola4.log("");
378
+ }
379
+ consola4.log(` ${pc3.dim("Review the plan, then run:")} ${pc3.cyan("cmsassets-agent apply")}`);
380
+ consola4.log("");
381
+ }
382
+ });
383
+
384
+ // src/commands/apply.ts
385
+ import { existsSync as existsSync2 } from "fs";
386
+ import { join as join2, resolve as resolve2 } from "path";
387
+ import { defineCommand as defineCommand4 } from "citty";
388
+ import consola5 from "consola";
389
+ import pc4 from "picocolors";
390
+ var applyCommand = defineCommand4({
391
+ meta: {
392
+ name: "apply",
393
+ description: "Apply the transformer integration (install package, patch files, update env)."
394
+ },
395
+ args: {
396
+ dir: {
397
+ type: "string",
398
+ description: "Project root directory",
399
+ default: "."
400
+ },
401
+ plan: {
402
+ type: "string",
403
+ description: "Path to a plan file (skips scan+plan phases)"
404
+ },
405
+ "dry-run": {
406
+ type: "boolean",
407
+ description: "Preview changes without writing files",
408
+ default: false
409
+ },
410
+ "include-tests": {
411
+ type: "boolean",
412
+ description: "Also patch test/fixture files",
413
+ default: false
414
+ },
415
+ "allow-dirty": {
416
+ type: "boolean",
417
+ description: "Allow running on a dirty git worktree",
418
+ default: false
419
+ },
420
+ install: {
421
+ type: "boolean",
422
+ description: "Install the transformer package (use --no-install to skip)",
423
+ default: true
424
+ },
425
+ "no-script": {
426
+ type: "boolean",
427
+ description: "Skip adding cmsassets:transform npm script",
428
+ default: false
429
+ },
430
+ "max-files": {
431
+ type: "string",
432
+ description: "Max number of files to auto-apply"
433
+ },
434
+ force: {
435
+ type: "boolean",
436
+ description: "Apply all detected patches (ignore max files threshold)",
437
+ default: false
438
+ },
439
+ cms: {
440
+ type: "string",
441
+ description: "Override CMS type"
442
+ },
443
+ repository: {
444
+ type: "string",
445
+ description: "Override Prismic repository name"
446
+ },
447
+ spaceId: {
448
+ type: "string",
449
+ description: "Override Contentful space ID"
450
+ },
451
+ slug: {
452
+ type: "string",
453
+ description: "Tenant slug for CMS_ASSETS_URL"
454
+ },
455
+ "create-pr": {
456
+ type: "boolean",
457
+ description: "Create a GitHub PR after applying",
458
+ default: false
459
+ },
460
+ "github-token": {
461
+ type: "string",
462
+ description: "GitHub token for PR creation"
463
+ },
464
+ llm: {
465
+ type: "boolean",
466
+ description: "Enable LLM fallback for low-confidence patches when AST fails",
467
+ default: false
468
+ },
469
+ "llm-all": {
470
+ type: "boolean",
471
+ description: "Try LLM for any failed AST patch (for testing). Requires --llm and OPENAI_API_KEY.",
472
+ default: false
473
+ },
474
+ "llm-only": {
475
+ type: "boolean",
476
+ description: "Skip AST, use LLM for all patches (testing only). Requires --llm and OPENAI_API_KEY.",
477
+ default: false
478
+ },
479
+ "ai-verify": {
480
+ type: "boolean",
481
+ description: "Run AI verification after patching to catch missed transforms. Requires OPENAI_API_KEY.",
482
+ default: false
483
+ },
484
+ "ai-model": {
485
+ type: "string",
486
+ description: "OpenAI model for AI verification",
487
+ default: "gpt-4o"
488
+ },
489
+ "ai-max-iterations": {
490
+ type: "string",
491
+ description: "Max AI fix attempts per file",
492
+ default: "3"
493
+ }
494
+ },
495
+ async run({ args }) {
496
+ const root = resolve2(args.dir);
497
+ if (gitOps.isGitRepo(root) && !args["allow-dirty"] && !gitOps.isClean(root)) {
498
+ consola5.error(
499
+ "Git worktree is dirty. Commit or stash changes first, or use --allow-dirty."
500
+ );
501
+ process.exit(1);
502
+ }
503
+ let plan;
504
+ if (args.plan) {
505
+ const planPath = resolve2(args.plan);
506
+ const loaded = loadPlanFile(planPath);
507
+ if (!loaded) {
508
+ consola5.error(`Invalid plan file: ${planPath}`);
509
+ process.exit(1);
510
+ }
511
+ plan = loaded;
512
+ } else {
513
+ const defaultPlanPath = join2(root, "cmsassets-agent.plan.json");
514
+ if (existsSync2(defaultPlanPath)) {
515
+ consola5.info(`Found existing plan file: ${defaultPlanPath}`);
516
+ const loaded = loadPlanFile(defaultPlanPath);
517
+ if (loaded) {
518
+ plan = loaded;
519
+ } else {
520
+ consola5.warn("Plan file invalid, regenerating...");
521
+ plan = generatePlan(root, args);
522
+ }
523
+ } else {
524
+ plan = generatePlan(root, args);
525
+ }
526
+ }
527
+ if (args.llm || args["llm-only"]) plan.policies.allowLlmFallback = true;
528
+ if (args["llm-all"]) plan.policies.llmFallbackForAll = true;
529
+ if (args["llm-only"]) plan.policies.llmOnly = true;
530
+ if (args["max-files"]) plan.policies.maxFilesAutoApply = parseInt(args["max-files"], 10);
531
+ if (args.force) plan.policies.maxFilesAutoApply = Number.MAX_SAFE_INTEGER;
532
+ if (args["dry-run"]) {
533
+ consola5.log("");
534
+ consola5.log(pc4.bold(" CMS Assets Agent \u2014 Dry Run"));
535
+ consola5.log(pc4.dim(" \u2500".repeat(25)));
536
+ consola5.log("");
537
+ consola5.log(` ${pc4.cyan("Install:")} ${plan.install.command}`);
538
+ consola5.log(` ${pc4.cyan("Env:")} ${plan.env.key}=${plan.env.placeholder}`);
539
+ consola5.log("");
540
+ for (const patch of plan.patches) {
541
+ const color = patch.confidence === "high" ? pc4.green : patch.confidence === "medium" ? pc4.yellow : pc4.red;
542
+ consola5.log(` ${color("\u25CF")} ${patch.filePath} ${pc4.dim(`[${patch.confidence}]`)}`);
543
+ consola5.log(` ${pc4.dim(patch.wrapTarget.originalCode)}`);
544
+ consola5.log(` ${pc4.green("\u2192 " + patch.wrapTarget.transformedCode)}`);
545
+ }
546
+ consola5.log("");
547
+ consola5.log(` ${pc4.dim("Run without --dry-run to apply these changes.")}`);
548
+ return;
549
+ }
550
+ let branchCreated = false;
551
+ let previousCommit = null;
552
+ if (gitOps.isGitRepo(root)) {
553
+ previousCommit = gitOps.getHeadCommit(root);
554
+ const branchName = `cmsassets-agent/integrate-${Date.now()}`;
555
+ branchCreated = gitOps.createBranch(root, branchName);
556
+ }
557
+ let installed = false;
558
+ if (args.install) {
559
+ consola5.info(`Installing: ${plan.install.command}`);
560
+ try {
561
+ const { execSync } = await import("child_process");
562
+ execSync(plan.install.command, { cwd: root, stdio: "inherit" });
563
+ installed = true;
564
+ } catch {
565
+ consola5.warn("Package install failed. You can install it manually later.");
566
+ }
567
+ }
568
+ consola5.info("Applying patches...");
569
+ const applyReport = await applyPlan(plan, {
570
+ dryRun: false,
571
+ includeTests: args["include-tests"],
572
+ maxFiles: args.force ? Number.MAX_SAFE_INTEGER : args["max-files"] ? parseInt(args["max-files"], 10) : void 0
573
+ });
574
+ applyReport.installed = installed;
575
+ if (gitOps.isGitRepo(root) && branchCreated) {
576
+ gitOps.stageAll(root);
577
+ const appliedFiles = applyReport.files.filter((f) => f.applied).length;
578
+ const commitHash = gitOps.commit(
579
+ root,
580
+ `chore(cmsassets-agent): integrate response transformer (${appliedFiles} files)`
581
+ );
582
+ if (commitHash) {
583
+ applyReport.gitBranch = gitOps.getCurrentBranch(root);
584
+ applyReport.gitCommit = commitHash;
585
+ }
586
+ }
587
+ let aiReviewReport;
588
+ if (args["ai-verify"]) {
589
+ const appliedPaths = applyReport.files.filter((f) => f.applied).map((f) => f.filePath);
590
+ if (appliedPaths.length > 0) {
591
+ consola5.log("");
592
+ consola5.log(pc4.bold(" CMS Assets Agent \u2014 AI Verification"));
593
+ consola5.log(pc4.dim(" \u2500".repeat(25)));
594
+ consola5.log("");
595
+ const { aiReviewAll } = await import("./aiReview-HZKRPSUV.js");
596
+ aiReviewReport = await aiReviewAll(root, appliedPaths, plan, {
597
+ model: args["ai-model"] ?? "gpt-4o",
598
+ maxIterations: parseInt(args["ai-max-iterations"] ?? "3", 10)
599
+ });
600
+ consola5.log("");
601
+ consola5.log(` ${pc4.green("Passed:")} ${aiReviewReport.filesPassed} files`);
602
+ if (aiReviewReport.filesFixed > 0) {
603
+ consola5.log(` ${pc4.blue("Fixed:")} ${aiReviewReport.filesFixed} files`);
604
+ }
605
+ if (aiReviewReport.filesFailed > 0) {
606
+ consola5.log(` ${pc4.red("Failed:")} ${aiReviewReport.filesFailed} files`);
607
+ }
608
+ consola5.log(` ${pc4.dim(`Tokens used: ${aiReviewReport.tokensUsed}`)}`);
609
+ consola5.log("");
610
+ if (aiReviewReport.filesFixed > 0 && gitOps.isGitRepo(root) && branchCreated) {
611
+ gitOps.stageAll(root);
612
+ gitOps.commit(root, "chore(cmsassets-agent): AI-verified fixes for missed transforms");
613
+ }
614
+ }
615
+ }
616
+ let scriptAdded = false;
617
+ if (!args["no-script"]) {
618
+ const scriptResult = injectNpmScript(root);
619
+ scriptAdded = scriptResult.added;
620
+ }
621
+ const report = createReport({ apply: applyReport, plan, scan: plan.scan, aiReview: aiReviewReport });
622
+ saveReport(root, report);
623
+ const applied = applyReport.files.filter((f) => f.applied);
624
+ const skipped = applyReport.files.filter((f) => !f.applied);
625
+ consola5.log("");
626
+ consola5.log(pc4.bold(" CMS Assets Agent \u2014 Apply Complete"));
627
+ consola5.log(pc4.dim(" \u2500".repeat(25)));
628
+ consola5.log("");
629
+ consola5.log(` ${pc4.green("Applied:")} ${applied.length} files`);
630
+ consola5.log(` ${pc4.yellow("Skipped:")} ${skipped.length} files`);
631
+ consola5.log(` ${pc4.cyan("Duration:")} ${applyReport.totalDurationMs}ms`);
632
+ if (applyReport.gitBranch) {
633
+ consola5.log(` ${pc4.cyan("Branch:")} ${applyReport.gitBranch}`);
634
+ }
635
+ if (applyReport.gitCommit) {
636
+ consola5.log(` ${pc4.cyan("Commit:")} ${applyReport.gitCommit}`);
637
+ }
638
+ consola5.log("");
639
+ for (const file of applied) {
640
+ consola5.log(` ${pc4.green("\u2713")} ${file.filePath} ${pc4.dim(`(${file.method}, ${file.durationMs}ms)`)}`);
641
+ }
642
+ for (const file of skipped) {
643
+ consola5.log(` ${pc4.yellow("\u25CB")} ${file.filePath} ${pc4.dim(`\u2014 ${file.reason}`)}`);
644
+ }
645
+ consola5.log("");
646
+ if (scriptAdded) {
647
+ consola5.log(` ${pc4.green("Script:")} Added "cmsassets:transform" to package.json`);
648
+ }
649
+ if (!installed) {
650
+ consola5.log(` ${pc4.dim("Don't forget to install:")} ${pc4.cyan(plan.install.command)}`);
651
+ }
652
+ if (!applyReport.envUpdated) {
653
+ consola5.log(` ${pc4.dim("Set your env var:")} ${pc4.cyan(`${plan.env.key}=${plan.env.placeholder}`)}`);
654
+ }
655
+ if (scriptAdded) {
656
+ consola5.log(` ${pc4.dim("Re-run later with:")} ${pc4.cyan("npm run cmsassets:transform")}`);
657
+ }
658
+ consola5.log("");
659
+ if (args["create-pr"] && gitOps.isGitRepo(root)) {
660
+ const token = args["github-token"] ?? process.env.GITHUB_TOKEN;
661
+ if (!token) {
662
+ consola5.warn("No GitHub token provided. Use --github-token or set GITHUB_TOKEN.");
663
+ return;
664
+ }
665
+ consola5.info("Pushing branch and creating PR...");
666
+ const pushed = gitOps.push(root);
667
+ if (pushed) {
668
+ try {
669
+ const { execSync } = await import("child_process");
670
+ const prTitle = `chore: integrate CMS Assets response transformer`;
671
+ const prBody = `Automated integration by \`@synchronized-studio/cmsassets-agent\`.
672
+
673
+ **CMS:** ${plan.scan.cms.type}
674
+ **Framework:** ${plan.scan.framework.name}
675
+ **Files patched:** ${applied.length}`;
676
+ execSync(
677
+ `gh pr create --title "${prTitle}" --body "${prBody}"`,
678
+ { cwd: root, stdio: "inherit", env: { ...process.env, GITHUB_TOKEN: token } }
679
+ );
680
+ } catch {
681
+ consola5.warn("PR creation failed. Push was successful, create PR manually.");
682
+ }
683
+ }
684
+ }
685
+ }
686
+ });
687
+ function generatePlan(root, args) {
688
+ const scanResult = scan(root);
689
+ if (args.cms) scanResult.cms.type = args.cms;
690
+ if (args.repository) scanResult.cms.params.repository = args.repository;
691
+ if (args.spaceId) scanResult.cms.params.spaceId = args.spaceId;
692
+ const plan = createPlan(scanResult);
693
+ if (args.slug) plan.env.placeholder = `https://${args.slug}.cmsassets.com`;
694
+ return plan;
695
+ }
696
+
697
+ // src/commands/verify.ts
698
+ import { resolve as resolve3 } from "path";
699
+ import { defineCommand as defineCommand5 } from "citty";
700
+ import consola6 from "consola";
701
+ import pc5 from "picocolors";
702
+ var verifyCommand = defineCommand5({
703
+ meta: {
704
+ name: "verify",
705
+ description: "Verify the project builds/lints/tests correctly after patching."
706
+ },
707
+ args: {
708
+ dir: {
709
+ type: "string",
710
+ description: "Project root directory",
711
+ default: "."
712
+ },
713
+ profile: {
714
+ type: "string",
715
+ description: "Verify profile: quick or full",
716
+ default: "quick"
717
+ }
718
+ },
719
+ async run({ args }) {
720
+ const root = resolve3(args.dir);
721
+ const scanResult = scan(root);
722
+ consola6.log("");
723
+ consola6.log(pc5.bold(` CMS Assets Agent \u2014 Verify (${args.profile})`));
724
+ consola6.log(pc5.dim(" \u2500".repeat(25)));
725
+ consola6.log("");
726
+ const report = verify(root, {
727
+ profile: args.profile,
728
+ framework: scanResult.framework.name
729
+ });
730
+ consola6.log("");
731
+ const pass = pc5.green("PASS");
732
+ const fail = pc5.red("FAIL");
733
+ const skip = pc5.dim("SKIP");
734
+ consola6.log(` Lint: ${report.lintPassed === null ? skip : report.lintPassed ? pass : fail}`);
735
+ consola6.log(` Build: ${report.buildPassed === null ? skip : report.buildPassed ? pass : fail}`);
736
+ consola6.log(` Tests: ${report.testsPassed === null ? skip : report.testsPassed ? pass : fail}`);
737
+ consola6.log(` ${pc5.dim(`Duration: ${report.durationMs}ms`)}`);
738
+ if (report.warnings.length > 0) {
739
+ consola6.log("");
740
+ consola6.log(pc5.yellow(" Warnings:"));
741
+ for (const w of report.warnings) {
742
+ consola6.log(` ${pc5.dim("\u2022")} ${w.substring(0, 120)}`);
743
+ }
744
+ }
745
+ consola6.log("");
746
+ }
747
+ });
748
+
749
+ // src/commands/doctor.ts
750
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
751
+ import { resolve as resolve4, join as join3 } from "path";
752
+ import { defineCommand as defineCommand6 } from "citty";
753
+ import consola7 from "consola";
754
+ import pc6 from "picocolors";
755
+ var doctorCommand = defineCommand6({
756
+ meta: {
757
+ name: "doctor",
758
+ description: "Diagnose project setup and readiness for transformer integration."
759
+ },
760
+ args: {
761
+ dir: {
762
+ type: "string",
763
+ description: "Project root directory",
764
+ default: "."
765
+ }
766
+ },
767
+ async run({ args }) {
768
+ const root = resolve4(args.dir);
769
+ const checks = [];
770
+ checks.push({
771
+ label: "package.json",
772
+ ok: existsSync3(join3(root, "package.json")),
773
+ detail: existsSync3(join3(root, "package.json")) ? "Found" : "Missing \u2014 this may not be a JS/TS project"
774
+ });
775
+ const isGit = gitOps.isGitRepo(root);
776
+ checks.push({
777
+ label: "Git repository",
778
+ ok: isGit,
779
+ detail: isGit ? "Initialized" : "Not a git repo (rollback via git will not work)"
780
+ });
781
+ if (isGit) {
782
+ const clean = gitOps.isClean(root);
783
+ checks.push({
784
+ label: "Git worktree clean",
785
+ ok: clean,
786
+ detail: clean ? "Clean" : "Dirty \u2014 commit or stash before running apply"
787
+ });
788
+ }
789
+ const scanResult = scan(root);
790
+ checks.push({
791
+ label: "Framework detected",
792
+ ok: scanResult.framework.name !== "unknown",
793
+ detail: scanResult.framework.name !== "unknown" ? `${scanResult.framework.name} ${scanResult.framework.version}` : "Could not detect framework \u2014 agent will use generic strategies"
794
+ });
795
+ checks.push({
796
+ label: "CMS detected",
797
+ ok: scanResult.cms.type !== "unknown",
798
+ detail: scanResult.cms.type !== "unknown" ? `${scanResult.cms.type} (${Object.entries(scanResult.cms.params).map(([k, v]) => `${k}: ${v}`).join(", ")})` : "Could not detect CMS \u2014 use --cms flag to specify"
799
+ });
800
+ checks.push({
801
+ label: "Injection points found",
802
+ ok: scanResult.injectionPoints.length > 0,
803
+ detail: scanResult.injectionPoints.length > 0 ? `${scanResult.injectionPoints.length} candidates` : "None found \u2014 transformer may need manual integration"
804
+ });
805
+ const highConfidence = scanResult.injectionPoints.filter((p) => p.confidence === "high");
806
+ checks.push({
807
+ label: "High-confidence points",
808
+ ok: highConfidence.length > 0,
809
+ detail: highConfidence.length > 0 ? `${highConfidence.length} auto-eligible for patching` : "No high-confidence points \u2014 patches will need manual confirmation"
810
+ });
811
+ let pkgJson = {};
812
+ try {
813
+ pkgJson = JSON.parse(readFileSync3(join3(root, "package.json"), "utf-8"));
814
+ } catch {
815
+ }
816
+ const allDeps = { ...pkgJson.dependencies ?? {}, ...pkgJson.devDependencies ?? {} };
817
+ const hasTransformer = "@synchronized-studio/response-transformer" in allDeps;
818
+ checks.push({
819
+ label: "Transformer package",
820
+ ok: true,
821
+ detail: hasTransformer ? `Already installed (${allDeps["@synchronized-studio/response-transformer"]})` : "Not installed \u2014 will need: " + (scanResult.packageManager === "pnpm" ? "pnpm add" : scanResult.packageManager === "yarn" ? "yarn add" : "npm install") + " @synchronized-studio/response-transformer"
822
+ });
823
+ const envFiles = [".env", ".env.local", ".env.development"];
824
+ let hasEnvVar = false;
825
+ for (const f of envFiles) {
826
+ const p = join3(root, f);
827
+ if (existsSync3(p)) {
828
+ const content = readFileSync3(p, "utf-8");
829
+ if (content.includes("CMS_ASSETS_URL")) {
830
+ hasEnvVar = true;
831
+ break;
832
+ }
833
+ }
834
+ }
835
+ checks.push({
836
+ label: "CMS_ASSETS_URL env var",
837
+ ok: true,
838
+ detail: hasEnvVar ? "Set in .env file" : "Not set \u2014 you will need to add CMS_ASSETS_URL=https://YOUR-SLUG.cmsassets.com"
839
+ });
840
+ consola7.log("");
841
+ consola7.log(pc6.bold(" CMS Assets Agent \u2014 Doctor"));
842
+ consola7.log(pc6.dim(" \u2500".repeat(25)));
843
+ consola7.log("");
844
+ for (const check of checks) {
845
+ const icon = check.ok ? pc6.green("\u2713") : pc6.yellow("!");
846
+ consola7.log(` ${icon} ${pc6.bold(check.label)}`);
847
+ consola7.log(` ${pc6.dim(check.detail)}`);
848
+ }
849
+ consola7.log("");
850
+ const issues = checks.filter((c) => !c.ok);
851
+ if (issues.length === 0) {
852
+ consola7.log(pc6.green(" All checks passed. Ready to run: cmsassets-agent plan"));
853
+ } else {
854
+ consola7.log(pc6.yellow(` ${issues.length} issue(s) found. Fix them before running apply.`));
855
+ }
856
+ consola7.log("");
857
+ }
858
+ });
859
+
860
+ // src/commands/rollback.ts
861
+ import { resolve as resolve5 } from "path";
862
+ import { defineCommand as defineCommand7 } from "citty";
863
+ import consola8 from "consola";
864
+ import pc7 from "picocolors";
865
+ var rollbackCommand = defineCommand7({
866
+ meta: {
867
+ name: "rollback",
868
+ description: "Rollback the last agent apply by reverting the git commit."
869
+ },
870
+ args: {
871
+ dir: {
872
+ type: "string",
873
+ description: "Project root directory",
874
+ default: "."
875
+ },
876
+ commit: {
877
+ type: "string",
878
+ description: "Specific commit hash to rollback to (otherwise finds last agent commit)"
879
+ }
880
+ },
881
+ async run({ args }) {
882
+ const root = resolve5(args.dir);
883
+ if (!gitOps.isGitRepo(root)) {
884
+ consola8.error("Not a git repository. Cannot rollback.");
885
+ process.exit(1);
886
+ }
887
+ consola8.log("");
888
+ consola8.log(pc7.bold(" CMS Assets Agent \u2014 Rollback"));
889
+ consola8.log(pc7.dim(" \u2500".repeat(25)));
890
+ consola8.log("");
891
+ let targetCommit = null;
892
+ if (args.commit) {
893
+ targetCommit = args.commit;
894
+ } else {
895
+ const agentCommit = gitOps.getLastCommitByAgent(root);
896
+ if (!agentCommit) {
897
+ consola8.error("No agent commit found in git history.");
898
+ process.exit(1);
899
+ }
900
+ targetCommit = gitOps.getCommitBefore(root, agentCommit);
901
+ if (!targetCommit) {
902
+ consola8.error("Cannot find parent commit for rollback.");
903
+ process.exit(1);
904
+ }
905
+ }
906
+ consola8.info(`Rolling back to commit: ${targetCommit}`);
907
+ const success = gitOps.rollbackToCommit(root, targetCommit);
908
+ if (success) {
909
+ consola8.success(`Rollback complete. HEAD is now at ${targetCommit}`);
910
+ } else {
911
+ consola8.error("Rollback failed. Check your git state manually.");
912
+ process.exit(1);
913
+ }
914
+ consola8.log("");
915
+ }
916
+ });
917
+
918
+ // src/cli.ts
919
+ var main = defineCommand8({
920
+ meta: {
921
+ name: "cmsassets-agent",
922
+ version: "0.1.0",
923
+ description: "Auto-integrate @synchronized-studio/response-transformer into any JS/TS project."
924
+ },
925
+ subCommands: {
926
+ init: initCommand,
927
+ scan: scanCommand,
928
+ plan: planCommand,
929
+ apply: applyCommand,
930
+ verify: verifyCommand,
931
+ doctor: doctorCommand,
932
+ rollback: rollbackCommand
933
+ }
934
+ });
935
+ var cwd = process.cwd();
936
+ var cliDir = typeof __dirname !== "undefined" ? __dirname : pathDirname(fileURLToPath(import.meta.url));
937
+ function findProjectRootEnv(startDir) {
938
+ let dir = resolve6(startDir);
939
+ for (let i = 0; i < 6; i++) {
940
+ const envPath = resolve6(dir, ".env");
941
+ if (existsSync4(envPath)) return envPath;
942
+ const parent = pathDirname(dir);
943
+ if (parent === dir) break;
944
+ dir = parent;
945
+ }
946
+ return null;
947
+ }
948
+ var envPaths = [];
949
+ var explicitEnv = process.env.DOTENV_CONFIG_PATH || process.env.CMSASSETS_ENV_FILE;
950
+ if (explicitEnv) envPaths.push(resolve6(cwd, explicitEnv));
951
+ envPaths.push(resolve6(cwd, ".env"));
952
+ var projectRootEnv = findProjectRootEnv(cliDir);
953
+ if (projectRootEnv && !envPaths.includes(projectRootEnv)) envPaths.push(projectRootEnv);
954
+ for (const p of envPaths) {
955
+ if (existsSync4(p)) {
956
+ dotenvConfig({ path: p });
957
+ if (process.env.OPENAI_API_KEY) break;
958
+ }
959
+ }
960
+ runMain(main);