schemashift-cli 0.3.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,810 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
5
+ import { dirname as dirname2, join as join2, resolve } from "path";
6
+ import { fileURLToPath } from "url";
7
+ import {
8
+ loadConfig,
9
+ SchemaAnalyzer,
10
+ TransformEngine
11
+ } from "@schemashift/core";
12
+ import { createIoTsToZodHandler } from "@schemashift/io-ts-zod";
13
+ import { createJoiToZodHandler } from "@schemashift/joi-zod";
14
+ import { canUseMigration, LicenseManager, LicenseTier, TIER_FEATURES } from "@schemashift/license";
15
+ import { createYupToZodHandler } from "@schemashift/yup-zod";
16
+ import { createZodV3ToV4Handler } from "@schemashift/zod-v3-v4";
17
+ import { Command } from "commander";
18
+ import { glob as glob2 } from "glob";
19
+ import { Listr } from "listr2";
20
+ import pc2 from "picocolors";
21
+
22
+ // src/backup.ts
23
+ import {
24
+ copyFileSync,
25
+ existsSync,
26
+ mkdirSync,
27
+ readdirSync,
28
+ readFileSync,
29
+ rmSync,
30
+ writeFileSync
31
+ } from "fs";
32
+ import { dirname, join, relative } from "path";
33
+ var BackupManager = class {
34
+ backupDir;
35
+ constructor(backupDir = ".schemashift-backup") {
36
+ this.backupDir = backupDir;
37
+ }
38
+ createBackup(files, from, to) {
39
+ const id = `backup-${Date.now()}`;
40
+ const backupPath = join(this.backupDir, id);
41
+ if (!existsSync(backupPath)) {
42
+ mkdirSync(backupPath, { recursive: true });
43
+ }
44
+ const manifest = {
45
+ id,
46
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
47
+ from,
48
+ to,
49
+ files: []
50
+ };
51
+ for (const file of files) {
52
+ const relativePath = relative(process.cwd(), file);
53
+ const backupFile = join(backupPath, relativePath);
54
+ const backupFileDir = dirname(backupFile);
55
+ if (!existsSync(backupFileDir)) {
56
+ mkdirSync(backupFileDir, { recursive: true });
57
+ }
58
+ copyFileSync(file, backupFile);
59
+ manifest.files.push({
60
+ original: file,
61
+ backup: backupFile,
62
+ transformed: false
63
+ });
64
+ }
65
+ writeFileSync(join(backupPath, "manifest.json"), JSON.stringify(manifest, null, 2));
66
+ return manifest;
67
+ }
68
+ listBackups() {
69
+ if (!existsSync(this.backupDir)) return [];
70
+ const backups = [];
71
+ const dirs = readdirSync(this.backupDir, { withFileTypes: true }).filter(
72
+ (d) => d.isDirectory() && d.name.startsWith("backup-")
73
+ );
74
+ for (const dir of dirs) {
75
+ const manifestPath = join(this.backupDir, dir.name, "manifest.json");
76
+ if (existsSync(manifestPath)) {
77
+ backups.push(JSON.parse(readFileSync(manifestPath, "utf-8")));
78
+ }
79
+ }
80
+ return backups.sort(
81
+ (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
82
+ );
83
+ }
84
+ restore(backupId) {
85
+ const backupPath = join(this.backupDir, backupId);
86
+ const manifestPath = join(backupPath, "manifest.json");
87
+ if (!existsSync(manifestPath)) {
88
+ throw new Error(`Backup ${backupId} not found`);
89
+ }
90
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
91
+ for (const file of manifest.files) {
92
+ if (existsSync(file.backup)) {
93
+ copyFileSync(file.backup, file.original);
94
+ }
95
+ }
96
+ }
97
+ deleteBackup(backupId) {
98
+ const backupPath = join(this.backupDir, backupId);
99
+ if (existsSync(backupPath)) {
100
+ rmSync(backupPath, { recursive: true });
101
+ }
102
+ }
103
+ cleanOldBackups(keepCount = 5) {
104
+ const backups = this.listBackups();
105
+ const toDelete = backups.slice(keepCount);
106
+ for (const backup of toDelete) {
107
+ this.deleteBackup(backup.id);
108
+ }
109
+ }
110
+ };
111
+
112
+ // src/git.ts
113
+ import { execSync } from "child_process";
114
+ import { existsSync as existsSync2 } from "fs";
115
+ var GitIntegration = class {
116
+ isGitRepo;
117
+ constructor() {
118
+ this.isGitRepo = existsSync2(".git");
119
+ }
120
+ isAvailable() {
121
+ if (!this.isGitRepo) return false;
122
+ try {
123
+ execSync("git --version", { stdio: "ignore" });
124
+ return true;
125
+ } catch {
126
+ return false;
127
+ }
128
+ }
129
+ getCurrentBranch() {
130
+ return execSync("git rev-parse --abbrev-ref HEAD", {
131
+ encoding: "utf-8"
132
+ }).trim();
133
+ }
134
+ hasUncommittedChanges() {
135
+ const status = execSync("git status --porcelain", { encoding: "utf-8" });
136
+ return status.length > 0;
137
+ }
138
+ createBranch(name) {
139
+ execSync(`git checkout -b ${name}`, { stdio: "inherit" });
140
+ }
141
+ stageFiles(files) {
142
+ for (const file of files) {
143
+ execSync(`git add "${file}"`, { stdio: "ignore" });
144
+ }
145
+ }
146
+ commit(message) {
147
+ execSync(`git commit -m "${message}"`, { stdio: "inherit" });
148
+ }
149
+ generateBranchName(prefix, from, to) {
150
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
151
+ return `${prefix}${from}-to-${to}-${timestamp}`;
152
+ }
153
+ };
154
+
155
+ // src/report.ts
156
+ import { writeFileSync as writeFileSync2 } from "fs";
157
+ var ReportGenerator = class {
158
+ generateReport(results, from, to, startTime, customRules = []) {
159
+ const endTime = Date.now();
160
+ return {
161
+ summary: {
162
+ totalFiles: results.length,
163
+ successfulFiles: results.filter((r) => r.success).length,
164
+ failedFiles: results.filter((r) => !r.success).length,
165
+ totalWarnings: results.reduce((acc, r) => acc + r.warnings.length, 0),
166
+ from,
167
+ to,
168
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
169
+ duration: endTime - startTime
170
+ },
171
+ files: results.map((r) => ({
172
+ path: r.filePath,
173
+ success: r.success,
174
+ errors: r.errors.map((e) => e.message),
175
+ warnings: r.warnings,
176
+ diff: r.transformedCode ? {
177
+ before: r.originalCode,
178
+ after: r.transformedCode
179
+ } : void 0
180
+ })),
181
+ customRulesApplied: customRules
182
+ };
183
+ }
184
+ writeJson(report, outputPath) {
185
+ writeFileSync2(outputPath, JSON.stringify(report, null, 2));
186
+ }
187
+ writeHtml(report, outputPath) {
188
+ const html = this.generateHtml(report);
189
+ writeFileSync2(outputPath, html);
190
+ }
191
+ generateHtml(report) {
192
+ const successRate = Math.round(
193
+ report.summary.successfulFiles / report.summary.totalFiles * 100
194
+ );
195
+ return `<!DOCTYPE html>
196
+ <html lang="en">
197
+ <head>
198
+ <meta charset="UTF-8">
199
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
200
+ <title>SchemaShift Migration Report</title>
201
+ <style>
202
+ :root {
203
+ --success: #10b981;
204
+ --error: #ef4444;
205
+ --warning: #f59e0b;
206
+ --bg: #0f172a;
207
+ --surface: #1e293b;
208
+ --text: #e2e8f0;
209
+ --muted: #94a3b8;
210
+ }
211
+ * { box-sizing: border-box; margin: 0; padding: 0; }
212
+ body {
213
+ font-family: system-ui, -apple-system, sans-serif;
214
+ background: var(--bg);
215
+ color: var(--text);
216
+ line-height: 1.6;
217
+ padding: 2rem;
218
+ }
219
+ .container { max-width: 1200px; margin: 0 auto; }
220
+ h1 { font-size: 2rem; margin-bottom: 0.5rem; }
221
+ .subtitle { color: var(--muted); margin-bottom: 2rem; }
222
+ .summary {
223
+ display: grid;
224
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
225
+ gap: 1rem;
226
+ margin-bottom: 2rem;
227
+ }
228
+ .card {
229
+ background: var(--surface);
230
+ border-radius: 0.5rem;
231
+ padding: 1.5rem;
232
+ }
233
+ .card-label { color: var(--muted); font-size: 0.875rem; }
234
+ .card-value { font-size: 2rem; font-weight: bold; }
235
+ .card-value.success { color: var(--success); }
236
+ .card-value.error { color: var(--error); }
237
+ .card-value.warning { color: var(--warning); }
238
+ .progress-bar {
239
+ height: 8px;
240
+ background: var(--surface);
241
+ border-radius: 4px;
242
+ margin: 1rem 0;
243
+ overflow: hidden;
244
+ }
245
+ .progress-fill {
246
+ height: 100%;
247
+ background: var(--success);
248
+ transition: width 0.3s;
249
+ }
250
+ .file-list { list-style: none; }
251
+ .file-item {
252
+ background: var(--surface);
253
+ border-radius: 0.5rem;
254
+ padding: 1rem;
255
+ margin-bottom: 0.5rem;
256
+ }
257
+ .file-header {
258
+ display: flex;
259
+ justify-content: space-between;
260
+ align-items: center;
261
+ }
262
+ .file-path { font-family: monospace; }
263
+ .badge {
264
+ padding: 0.25rem 0.5rem;
265
+ border-radius: 0.25rem;
266
+ font-size: 0.75rem;
267
+ font-weight: bold;
268
+ }
269
+ .badge.success { background: var(--success); color: white; }
270
+ .badge.error { background: var(--error); color: white; }
271
+ .warnings {
272
+ margin-top: 0.5rem;
273
+ padding-left: 1rem;
274
+ border-left: 2px solid var(--warning);
275
+ color: var(--warning);
276
+ font-size: 0.875rem;
277
+ }
278
+ .diff {
279
+ margin-top: 1rem;
280
+ background: #0d1117;
281
+ border-radius: 0.25rem;
282
+ padding: 1rem;
283
+ font-family: monospace;
284
+ font-size: 0.75rem;
285
+ overflow-x: auto;
286
+ }
287
+ details summary { cursor: pointer; color: var(--muted); }
288
+ </style>
289
+ </head>
290
+ <body>
291
+ <div class="container">
292
+ <h1>SchemaShift Migration Report</h1>
293
+ <p class="subtitle">${report.summary.from} \u2192 ${report.summary.to} | ${new Date(report.summary.timestamp).toLocaleString()}</p>
294
+
295
+ <div class="summary">
296
+ <div class="card">
297
+ <div class="card-label">Total Files</div>
298
+ <div class="card-value">${report.summary.totalFiles}</div>
299
+ </div>
300
+ <div class="card">
301
+ <div class="card-label">Successful</div>
302
+ <div class="card-value success">${report.summary.successfulFiles}</div>
303
+ </div>
304
+ <div class="card">
305
+ <div class="card-label">Failed</div>
306
+ <div class="card-value error">${report.summary.failedFiles}</div>
307
+ </div>
308
+ <div class="card">
309
+ <div class="card-label">Warnings</div>
310
+ <div class="card-value warning">${report.summary.totalWarnings}</div>
311
+ </div>
312
+ </div>
313
+
314
+ <div class="card">
315
+ <div class="card-label">Success Rate</div>
316
+ <div class="progress-bar">
317
+ <div class="progress-fill" style="width: ${successRate}%"></div>
318
+ </div>
319
+ <div class="card-value">${successRate}%</div>
320
+ </div>
321
+
322
+ <h2 style="margin: 2rem 0 1rem;">Files</h2>
323
+ <ul class="file-list">
324
+ ${report.files.map(
325
+ (file) => `
326
+ <li class="file-item">
327
+ <div class="file-header">
328
+ <span class="file-path">${file.path}</span>
329
+ <span class="badge ${file.success ? "success" : "error"}">${file.success ? "Success" : "Failed"}</span>
330
+ </div>
331
+ ${file.errors.length > 0 ? `
332
+ <div class="warnings" style="border-color: var(--error); color: var(--error);">
333
+ ${file.errors.map((e) => `<div>${e}</div>`).join("")}
334
+ </div>
335
+ ` : ""}
336
+ ${file.warnings.length > 0 ? `
337
+ <div class="warnings">
338
+ ${file.warnings.map((w) => `<div>${w}</div>`).join("")}
339
+ </div>
340
+ ` : ""}
341
+ ${file.diff ? `
342
+ <details>
343
+ <summary>View diff</summary>
344
+ <div class="diff">
345
+ <pre>${this.escapeHtml(file.diff.after)}</pre>
346
+ </div>
347
+ </details>
348
+ ` : ""}
349
+ </li>
350
+ `
351
+ ).join("")}
352
+ </ul>
353
+
354
+ <footer style="margin-top: 2rem; text-align: center; color: var(--muted);">
355
+ Generated by SchemaShift | Duration: ${report.summary.duration}ms
356
+ </footer>
357
+ </div>
358
+ </body>
359
+ </html>`;
360
+ }
361
+ escapeHtml(text) {
362
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
363
+ }
364
+ };
365
+
366
+ // src/watch.ts
367
+ import { watch } from "fs";
368
+ import { relative as relative2 } from "path";
369
+ import { glob } from "glob";
370
+ import pc from "picocolors";
371
+ var WatchMode = class {
372
+ watchers = [];
373
+ debounceTimers = /* @__PURE__ */ new Map();
374
+ async start(options) {
375
+ const files = await glob(options.patterns, {
376
+ ignore: options.exclude
377
+ });
378
+ console.log(pc.cyan(`
379
+ Watching ${files.length} files for changes...
380
+ `));
381
+ console.log(pc.dim("Press Ctrl+C to stop\n"));
382
+ const directories = new Set(files.map((f) => f.split("/").slice(0, -1).join("/")));
383
+ for (const dir of directories) {
384
+ const watcher = watch(dir || ".", { recursive: true }, async (_event, filename) => {
385
+ if (!filename) return;
386
+ const fullPath = dir ? `${dir}/${filename}` : filename;
387
+ if (!this.matchesPatterns(fullPath, options.patterns, options.exclude)) {
388
+ return;
389
+ }
390
+ const existingTimer = this.debounceTimers.get(fullPath);
391
+ if (existingTimer) {
392
+ clearTimeout(existingTimer);
393
+ }
394
+ this.debounceTimers.set(
395
+ fullPath,
396
+ setTimeout(async () => {
397
+ console.log(pc.yellow(`
398
+ Changed: ${relative2(process.cwd(), fullPath)}`));
399
+ try {
400
+ await options.onTransform(fullPath);
401
+ console.log(pc.green(`Transformed successfully
402
+ `));
403
+ } catch (error) {
404
+ console.error(pc.red(`Transform failed: ${error}
405
+ `));
406
+ }
407
+ this.debounceTimers.delete(fullPath);
408
+ }, 100)
409
+ );
410
+ });
411
+ this.watchers.push(watcher);
412
+ }
413
+ }
414
+ stop() {
415
+ for (const watcher of this.watchers) {
416
+ watcher.close();
417
+ }
418
+ this.watchers = [];
419
+ for (const timer of this.debounceTimers.values()) {
420
+ clearTimeout(timer);
421
+ }
422
+ this.debounceTimers.clear();
423
+ }
424
+ matchesPatterns(file, _include, _exclude) {
425
+ const isIncluded = file.endsWith(".ts") || file.endsWith(".tsx");
426
+ const isExcluded = file.includes("node_modules") || file.includes("dist");
427
+ return isIncluded && !isExcluded;
428
+ }
429
+ };
430
+
431
+ // src/cli.ts
432
+ var __dirname2 = dirname2(fileURLToPath(import.meta.url));
433
+ var pkg = JSON.parse(readFileSync2(join2(__dirname2, "..", "package.json"), "utf-8"));
434
+ var program = new Command();
435
+ var licenseManager = new LicenseManager();
436
+ var engine = new TransformEngine();
437
+ engine.registerHandler("yup", "zod", createYupToZodHandler());
438
+ engine.registerHandler("joi", "zod", createJoiToZodHandler());
439
+ engine.registerHandler("io-ts", "zod", createIoTsToZodHandler());
440
+ engine.registerHandler("zod-v3", "v4", createZodV3ToV4Handler());
441
+ var POLAR_URL = "https://schemashift.qwady.app";
442
+ program.name("schemashift").version(pkg.version).description("TypeScript schema migration CLI");
443
+ program.command("init").description("Create .schemashiftrc.json config file").option("-f, --force", "Overwrite existing config").action((options) => {
444
+ const configPath = ".schemashiftrc.json";
445
+ if (existsSync3(configPath) && !options.force) {
446
+ console.error(pc2.red("Config file already exists. Use --force to overwrite."));
447
+ process.exit(1);
448
+ }
449
+ const defaultConfig = {
450
+ include: ["src/**/*.ts", "src/**/*.tsx"],
451
+ exclude: ["**/node_modules/**", "**/dist/**", "**/*.d.ts", "**/*.test.ts"],
452
+ git: {
453
+ enabled: false,
454
+ createBranch: true,
455
+ branchPrefix: "schemashift/",
456
+ autoCommit: false,
457
+ commitMessage: "chore: migrate schemas with SchemaShift"
458
+ },
459
+ backup: {
460
+ enabled: true,
461
+ dir: ".schemashift-backup"
462
+ },
463
+ customRules: [],
464
+ ci: false
465
+ };
466
+ writeFileSync3(configPath, JSON.stringify(defaultConfig, null, 2));
467
+ console.log(pc2.green("Created .schemashiftrc.json"));
468
+ console.log(pc2.dim("Edit the file to customize your migration settings."));
469
+ });
470
+ program.command("analyze <path>").description("Analyze schemas in your project").option("--json", "Output as JSON").option("-v, --verbose", "Show detailed analysis").action(async (targetPath, options) => {
471
+ const analyzer = new SchemaAnalyzer();
472
+ const files = await glob2(join2(targetPath, "**/*.{ts,tsx}"), {
473
+ ignore: ["**/node_modules/**", "**/dist/**"]
474
+ });
475
+ for (const file of files) {
476
+ analyzer.addSourceFiles([file]);
477
+ }
478
+ const result = analyzer.analyze();
479
+ if (options.json) {
480
+ console.log(JSON.stringify(result, null, 2));
481
+ return;
482
+ }
483
+ console.log(pc2.bold("\nSchema Analysis\n"));
484
+ console.log(`Total files: ${pc2.cyan(result.totalFiles.toString())}`);
485
+ console.log(`Files with schemas: ${pc2.cyan(result.filesWithSchemas.toString())}`);
486
+ console.log(`Total schemas: ${pc2.cyan(result.schemas.length.toString())}`);
487
+ if (result.schemas.length > 0) {
488
+ console.log(pc2.bold("\nSchemas by library:\n"));
489
+ const byLibrary = /* @__PURE__ */ new Map();
490
+ for (const schema of result.schemas) {
491
+ byLibrary.set(schema.library, (byLibrary.get(schema.library) || 0) + 1);
492
+ }
493
+ for (const [lib, count] of byLibrary) {
494
+ console.log(` ${lib}: ${count}`);
495
+ }
496
+ if (options.verbose) {
497
+ console.log(pc2.bold("\nDetailed schemas:\n"));
498
+ for (const schema of result.schemas.slice(0, 20)) {
499
+ console.log(` ${pc2.cyan(schema.name)} (${schema.library})`);
500
+ console.log(` ${pc2.dim(schema.filePath)}:${schema.lineNumber}`);
501
+ }
502
+ if (result.schemas.length > 20) {
503
+ console.log(pc2.dim(` ... and ${result.schemas.length - 20} more`));
504
+ }
505
+ }
506
+ }
507
+ });
508
+ program.command("migrate <path>").description("Migrate schemas from one library to another").requiredOption("-f, --from <library>", "Source library (yup, joi, io-ts, zod-v3)").requiredOption("-t, --to <library>", "Target library (zod, v4, valibot)").option("-d, --dry-run", "Preview changes without writing files").option("-v, --verbose", "Show detailed transformation info").option("-c, --config <path>", "Path to config file").option("--report <format>", "Generate report (json, html)").option("--report-output <path>", "Report output path").option("--git-branch", "Create git branch for changes").option("--git-commit", "Auto-commit changes").option("--no-backup", "Skip backup creation").option("--ci", "CI mode (non-interactive, exit code on failure)").action(async (targetPath, options) => {
509
+ const startTime = Date.now();
510
+ let config;
511
+ try {
512
+ config = await loadConfig(options.config);
513
+ } catch {
514
+ config = {
515
+ include: ["**/*.ts", "**/*.tsx"],
516
+ exclude: ["**/node_modules/**", "**/dist/**"],
517
+ backup: { enabled: true, dir: ".schemashift-backup" },
518
+ git: { enabled: false }
519
+ };
520
+ }
521
+ const validation = await licenseManager.validate();
522
+ if (!validation.valid) {
523
+ console.error(pc2.red(`License error: ${validation.error}`));
524
+ process.exit(1);
525
+ }
526
+ const tier = validation.license?.tier || LicenseTier.FREE;
527
+ const features = validation.license?.features || TIER_FEATURES[LicenseTier.FREE];
528
+ const migrationPath = `${options.from}->${options.to}`;
529
+ if (!canUseMigration(tier, options.from, options.to)) {
530
+ console.error(pc2.red(`Migration ${migrationPath} requires a higher tier.`));
531
+ console.log(`Your tier: ${pc2.yellow(tier)}`);
532
+ console.log(`Upgrade at: ${pc2.cyan(POLAR_URL)}`);
533
+ process.exit(1);
534
+ }
535
+ if (!engine.hasHandler(options.from, options.to)) {
536
+ console.error(pc2.red(`No handler for ${migrationPath}`));
537
+ console.log(
538
+ "Supported migrations:",
539
+ engine.getSupportedPaths().map((p) => `${p.from}->${p.to}`).join(", ")
540
+ );
541
+ process.exit(1);
542
+ }
543
+ const files = await glob2(join2(targetPath, "**/*.{ts,tsx}"), {
544
+ ignore: config.exclude
545
+ });
546
+ if (files.length === 0) {
547
+ console.log(pc2.yellow("No files found matching patterns."));
548
+ process.exit(0);
549
+ }
550
+ if (files.length > features.maxFiles) {
551
+ console.error(
552
+ pc2.red(`File limit exceeded. Found ${files.length}, max ${features.maxFiles}.`)
553
+ );
554
+ console.log(`Upgrade at: ${pc2.cyan(POLAR_URL)}`);
555
+ process.exit(1);
556
+ }
557
+ console.log(pc2.bold(`
558
+ Migrating ${options.from} to ${options.to}
559
+ `));
560
+ console.log(`Files: ${files.length}`);
561
+ console.log(`Tier: ${tier}`);
562
+ if (options.dryRun) {
563
+ console.log(pc2.yellow("DRY RUN - no files will be modified\n"));
564
+ }
565
+ const git = new GitIntegration();
566
+ if (options.gitBranch && git.isAvailable()) {
567
+ if (git.hasUncommittedChanges()) {
568
+ console.error(pc2.red("Uncommitted changes detected. Commit or stash first."));
569
+ process.exit(1);
570
+ }
571
+ const branchName = git.generateBranchName(
572
+ config.git?.branchPrefix || "schemashift/",
573
+ options.from,
574
+ options.to
575
+ );
576
+ git.createBranch(branchName);
577
+ console.log(pc2.dim(`Created branch: ${branchName}`));
578
+ }
579
+ const backup = new BackupManager(config.backup?.dir);
580
+ let backupManifest;
581
+ if (options.backup !== false && config.backup?.enabled !== false && !options.dryRun) {
582
+ backupManifest = backup.createBackup(files, options.from, options.to);
583
+ console.log(pc2.dim(`Backup created: ${backupManifest.id}`));
584
+ }
585
+ const results = [];
586
+ const analyzer = new SchemaAnalyzer();
587
+ const tasks = new Listr(
588
+ files.map((file) => ({
589
+ title: file.replace(`${process.cwd()}/`, ""),
590
+ task: async () => {
591
+ analyzer.addSourceFiles([file]);
592
+ const sourceFile = analyzer.getProject().getSourceFileOrThrow(resolve(file));
593
+ const result = engine.transform(sourceFile, options.from, options.to, {
594
+ from: options.from,
595
+ to: options.to,
596
+ dryRun: options.dryRun,
597
+ preserveComments: true
598
+ });
599
+ results.push(result);
600
+ if (result.success && result.transformedCode && !options.dryRun) {
601
+ writeFileSync3(file, result.transformedCode);
602
+ }
603
+ if (!result.success) {
604
+ throw new Error(result.errors[0]?.message || "Transform failed");
605
+ }
606
+ }
607
+ })),
608
+ { concurrent: false, exitOnError: false }
609
+ );
610
+ try {
611
+ await tasks.run();
612
+ } catch {
613
+ }
614
+ if (options.gitCommit && git.isAvailable() && !options.dryRun) {
615
+ const successFiles = results.filter((r) => r.success).map((r) => r.filePath);
616
+ if (successFiles.length > 0) {
617
+ git.stageFiles(successFiles);
618
+ git.commit(config.git?.commitMessage || `chore: migrate ${options.from} to ${options.to}`);
619
+ console.log(pc2.dim("Changes committed"));
620
+ }
621
+ }
622
+ if (options.report) {
623
+ if (options.report === "html" && !features.htmlReports) {
624
+ console.error(pc2.red("HTML reports require Individual tier or higher."));
625
+ console.log(`Upgrade at: ${pc2.cyan(POLAR_URL)}`);
626
+ } else {
627
+ const reporter = new ReportGenerator();
628
+ const report = reporter.generateReport(
629
+ results,
630
+ options.from,
631
+ options.to,
632
+ startTime,
633
+ config.customRules?.map((r) => r.name) || []
634
+ );
635
+ const outputPath = options.reportOutput || `schemashift-report.${options.report}`;
636
+ if (options.report === "html") {
637
+ reporter.writeHtml(report, outputPath);
638
+ } else {
639
+ reporter.writeJson(report, outputPath);
640
+ }
641
+ console.log(pc2.green(`Report saved: ${outputPath}`));
642
+ }
643
+ }
644
+ const successful = results.filter((r) => r.success).length;
645
+ const failed = results.filter((r) => !r.success).length;
646
+ const warnings = results.reduce((acc, r) => acc + r.warnings.length, 0);
647
+ console.log(pc2.bold("\nSummary\n"));
648
+ console.log(`Successful: ${pc2.green(successful.toString())}`);
649
+ console.log(`Failed: ${pc2.red(failed.toString())}`);
650
+ console.log(`Warnings: ${pc2.yellow(warnings.toString())}`);
651
+ console.log(`Duration: ${Date.now() - startTime}ms`);
652
+ if (options.verbose && warnings > 0) {
653
+ console.log(pc2.yellow("\nWarnings:\n"));
654
+ for (const result of results) {
655
+ for (const warning of result.warnings.slice(0, 5)) {
656
+ console.log(` ${warning}`);
657
+ }
658
+ }
659
+ }
660
+ if (options.ci && failed > 0) {
661
+ process.exit(1);
662
+ }
663
+ });
664
+ program.command("watch <path>").description("Watch files and migrate on change").requiredOption("-f, --from <library>", "Source library").requiredOption("-t, --to <library>", "Target library").option("-c, --config <path>", "Path to config file").action(async (targetPath, options) => {
665
+ const validation = await licenseManager.validate();
666
+ if (!validation.license?.features.watchMode) {
667
+ console.error(pc2.red("Watch mode requires Pro tier or higher."));
668
+ console.log(`Upgrade at: ${pc2.cyan(POLAR_URL)}`);
669
+ process.exit(1);
670
+ }
671
+ const config = await loadConfig(options.config);
672
+ const watchMode = new WatchMode();
673
+ const analyzer = new SchemaAnalyzer();
674
+ await watchMode.start({
675
+ patterns: config.include.map((p) => join2(targetPath, p)),
676
+ exclude: config.exclude,
677
+ from: options.from,
678
+ to: options.to,
679
+ onTransform: async (file) => {
680
+ analyzer.addSourceFiles([file]);
681
+ const sourceFile = analyzer.getProject().getSourceFileOrThrow(resolve(file));
682
+ const result = engine.transform(sourceFile, options.from, options.to, {
683
+ from: options.from,
684
+ to: options.to,
685
+ dryRun: false,
686
+ preserveComments: true
687
+ });
688
+ if (result.success && result.transformedCode) {
689
+ writeFileSync3(file, result.transformedCode);
690
+ } else {
691
+ throw new Error(result.errors[0]?.message || "Transform failed");
692
+ }
693
+ }
694
+ });
695
+ process.on("SIGINT", () => {
696
+ watchMode.stop();
697
+ console.log(pc2.dim("\nStopped watching."));
698
+ process.exit(0);
699
+ });
700
+ });
701
+ program.command("rollback [backupId]").description("Restore files from a backup").option("-l, --list", "List available backups").option("--clean", "Remove old backups (keeps last 5)").action((backupId, options) => {
702
+ const backup = new BackupManager();
703
+ if (options.list) {
704
+ const backups = backup.listBackups();
705
+ if (backups.length === 0) {
706
+ console.log(pc2.yellow("No backups found."));
707
+ return;
708
+ }
709
+ console.log(pc2.bold("\nAvailable backups:\n"));
710
+ for (const b of backups) {
711
+ console.log(` ${pc2.cyan(b.id)}`);
712
+ console.log(` ${b.from} -> ${b.to} | ${b.files.length} files`);
713
+ console.log(` ${pc2.dim(new Date(b.timestamp).toLocaleString())}
714
+ `);
715
+ }
716
+ return;
717
+ }
718
+ if (options.clean) {
719
+ backup.cleanOldBackups(5);
720
+ console.log(pc2.green("Old backups cleaned."));
721
+ return;
722
+ }
723
+ if (!backupId) {
724
+ const backups = backup.listBackups();
725
+ if (backups.length === 0) {
726
+ console.error(pc2.red("No backups available."));
727
+ process.exit(1);
728
+ }
729
+ backupId = backups[0]?.id;
730
+ }
731
+ if (!backupId) {
732
+ console.error(pc2.red("No backup ID provided."));
733
+ process.exit(1);
734
+ }
735
+ try {
736
+ backup.restore(backupId);
737
+ console.log(pc2.green(`Restored from ${backupId}`));
738
+ } catch (error) {
739
+ console.error(pc2.red(error instanceof Error ? error.message : "Restore failed"));
740
+ process.exit(1);
741
+ }
742
+ });
743
+ program.command("license").description("Show license information").option("-a, --activate <key>", "Activate a license key").option("-d, --deactivate", "Deactivate current license").option("-s, --status", "Show license status").action(async (options) => {
744
+ if (options.activate) {
745
+ console.log(pc2.dim("Activating license..."));
746
+ const result = await licenseManager.activate(options.activate);
747
+ if (result.valid && result.license) {
748
+ console.log(pc2.green("\nLicense activated successfully!\n"));
749
+ console.log(`Tier: ${pc2.cyan(result.license.tier)}`);
750
+ console.log(`Migrations: ${result.license.features.migrations.join(", ")}`);
751
+ console.log(
752
+ `Max files: ${result.license.features.maxFiles === Infinity ? "Unlimited" : result.license.features.maxFiles}`
753
+ );
754
+ } else {
755
+ console.error(pc2.red(`
756
+ Activation failed: ${result.error}`));
757
+ process.exit(1);
758
+ }
759
+ return;
760
+ }
761
+ if (options.deactivate) {
762
+ const result = await licenseManager.deactivate();
763
+ if (result.success) {
764
+ console.log(pc2.green("License deactivated. You can now activate on another device."));
765
+ } else {
766
+ console.error(pc2.red(`Deactivation failed: ${result.error}`));
767
+ process.exit(1);
768
+ }
769
+ return;
770
+ }
771
+ const status = await licenseManager.getStatus();
772
+ console.log(pc2.bold("\nLicense Status\n"));
773
+ if (!status.activated) {
774
+ console.log(`Tier: ${pc2.yellow("FREE")}`);
775
+ console.log("Max files: 5");
776
+ console.log("Migrations: yup->zod only");
777
+ console.log("\nActivate a license: schemashift license --activate <key>");
778
+ console.log(`Purchase: ${pc2.cyan(POLAR_URL)}`);
779
+ return;
780
+ }
781
+ const features = TIER_FEATURES[status.tier];
782
+ console.log(`Tier: ${pc2.cyan(status.tier.toUpperCase())}`);
783
+ console.log(`Activated: ${pc2.green("Yes")}`);
784
+ if (status.email) console.log(`Email: ${status.email}`);
785
+ if (status.expiresAt) console.log(`Expires: ${status.expiresAt.toLocaleDateString()}`);
786
+ if (status.lastValidated)
787
+ console.log(`Last validated: ${status.lastValidated.toLocaleString()}`);
788
+ console.log("\nFeatures:");
789
+ console.log(` Max files: ${features.maxFiles === Infinity ? "Unlimited" : features.maxFiles}`);
790
+ console.log(` Migrations: ${features.migrations.join(", ")}`);
791
+ console.log(` CI/CD: ${features.ciSupport ? pc2.green("Yes") : pc2.red("No")}`);
792
+ console.log(` HTML reports: ${features.htmlReports ? pc2.green("Yes") : pc2.red("No")}`);
793
+ console.log(` Watch mode: ${features.watchMode ? pc2.green("Yes") : pc2.red("No")}`);
794
+ });
795
+ program.command("pricing").description("Show pricing information").action(() => {
796
+ console.log(pc2.bold("\nSchemaShift Pricing\n"));
797
+ console.log(`${pc2.green("FREE")} - $0`);
798
+ console.log(" 5 files per migration, Yup -> Zod only\n");
799
+ console.log(`${pc2.blue("INDIVIDUAL")} - $49 one-time`);
800
+ console.log(" Unlimited files, + Joi -> Zod, Zod v3 -> v4");
801
+ console.log(" HTML reports, 1 device\n");
802
+ console.log(`${pc2.magenta("PRO")} - $149 one-time`);
803
+ console.log(" All migrations including io-ts and Valibot");
804
+ console.log(" CI/CD support, watch mode, 4 devices\n");
805
+ console.log(`${pc2.yellow("TEAM")} - $29/month`);
806
+ console.log(" Everything in Pro, unlimited devices");
807
+ console.log(" Single key for whole organization\n");
808
+ console.log(`Purchase: ${pc2.cyan(POLAR_URL)}`);
809
+ });
810
+ program.parse(process.argv);