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/index.js ADDED
@@ -0,0 +1,432 @@
1
+ // src/index.ts
2
+ import { loadConfig, SchemaAnalyzer, TransformEngine } from "@schemashift/core";
3
+ import { createIoTsToZodHandler } from "@schemashift/io-ts-zod";
4
+ import { createJoiToZodHandler } from "@schemashift/joi-zod";
5
+ import { LicenseManager, LicenseTier, TIER_FEATURES } from "@schemashift/license";
6
+ import { createYupToZodHandler } from "@schemashift/yup-zod";
7
+ import { createZodV3ToV4Handler } from "@schemashift/zod-v3-v4";
8
+
9
+ // src/backup.ts
10
+ import {
11
+ copyFileSync,
12
+ existsSync,
13
+ mkdirSync,
14
+ readdirSync,
15
+ readFileSync,
16
+ rmSync,
17
+ writeFileSync
18
+ } from "fs";
19
+ import { dirname, join, relative } from "path";
20
+ var BackupManager = class {
21
+ backupDir;
22
+ constructor(backupDir = ".schemashift-backup") {
23
+ this.backupDir = backupDir;
24
+ }
25
+ createBackup(files, from, to) {
26
+ const id = `backup-${Date.now()}`;
27
+ const backupPath = join(this.backupDir, id);
28
+ if (!existsSync(backupPath)) {
29
+ mkdirSync(backupPath, { recursive: true });
30
+ }
31
+ const manifest = {
32
+ id,
33
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
34
+ from,
35
+ to,
36
+ files: []
37
+ };
38
+ for (const file of files) {
39
+ const relativePath = relative(process.cwd(), file);
40
+ const backupFile = join(backupPath, relativePath);
41
+ const backupFileDir = dirname(backupFile);
42
+ if (!existsSync(backupFileDir)) {
43
+ mkdirSync(backupFileDir, { recursive: true });
44
+ }
45
+ copyFileSync(file, backupFile);
46
+ manifest.files.push({
47
+ original: file,
48
+ backup: backupFile,
49
+ transformed: false
50
+ });
51
+ }
52
+ writeFileSync(join(backupPath, "manifest.json"), JSON.stringify(manifest, null, 2));
53
+ return manifest;
54
+ }
55
+ listBackups() {
56
+ if (!existsSync(this.backupDir)) return [];
57
+ const backups = [];
58
+ const dirs = readdirSync(this.backupDir, { withFileTypes: true }).filter(
59
+ (d) => d.isDirectory() && d.name.startsWith("backup-")
60
+ );
61
+ for (const dir of dirs) {
62
+ const manifestPath = join(this.backupDir, dir.name, "manifest.json");
63
+ if (existsSync(manifestPath)) {
64
+ backups.push(JSON.parse(readFileSync(manifestPath, "utf-8")));
65
+ }
66
+ }
67
+ return backups.sort(
68
+ (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
69
+ );
70
+ }
71
+ restore(backupId) {
72
+ const backupPath = join(this.backupDir, backupId);
73
+ const manifestPath = join(backupPath, "manifest.json");
74
+ if (!existsSync(manifestPath)) {
75
+ throw new Error(`Backup ${backupId} not found`);
76
+ }
77
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
78
+ for (const file of manifest.files) {
79
+ if (existsSync(file.backup)) {
80
+ copyFileSync(file.backup, file.original);
81
+ }
82
+ }
83
+ }
84
+ deleteBackup(backupId) {
85
+ const backupPath = join(this.backupDir, backupId);
86
+ if (existsSync(backupPath)) {
87
+ rmSync(backupPath, { recursive: true });
88
+ }
89
+ }
90
+ cleanOldBackups(keepCount = 5) {
91
+ const backups = this.listBackups();
92
+ const toDelete = backups.slice(keepCount);
93
+ for (const backup of toDelete) {
94
+ this.deleteBackup(backup.id);
95
+ }
96
+ }
97
+ };
98
+
99
+ // src/git.ts
100
+ import { execSync } from "child_process";
101
+ import { existsSync as existsSync2 } from "fs";
102
+ var GitIntegration = class {
103
+ isGitRepo;
104
+ constructor() {
105
+ this.isGitRepo = existsSync2(".git");
106
+ }
107
+ isAvailable() {
108
+ if (!this.isGitRepo) return false;
109
+ try {
110
+ execSync("git --version", { stdio: "ignore" });
111
+ return true;
112
+ } catch {
113
+ return false;
114
+ }
115
+ }
116
+ getCurrentBranch() {
117
+ return execSync("git rev-parse --abbrev-ref HEAD", {
118
+ encoding: "utf-8"
119
+ }).trim();
120
+ }
121
+ hasUncommittedChanges() {
122
+ const status = execSync("git status --porcelain", { encoding: "utf-8" });
123
+ return status.length > 0;
124
+ }
125
+ createBranch(name) {
126
+ execSync(`git checkout -b ${name}`, { stdio: "inherit" });
127
+ }
128
+ stageFiles(files) {
129
+ for (const file of files) {
130
+ execSync(`git add "${file}"`, { stdio: "ignore" });
131
+ }
132
+ }
133
+ commit(message) {
134
+ execSync(`git commit -m "${message}"`, { stdio: "inherit" });
135
+ }
136
+ generateBranchName(prefix, from, to) {
137
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
138
+ return `${prefix}${from}-to-${to}-${timestamp}`;
139
+ }
140
+ };
141
+
142
+ // src/report.ts
143
+ import { writeFileSync as writeFileSync2 } from "fs";
144
+ var ReportGenerator = class {
145
+ generateReport(results, from, to, startTime, customRules = []) {
146
+ const endTime = Date.now();
147
+ return {
148
+ summary: {
149
+ totalFiles: results.length,
150
+ successfulFiles: results.filter((r) => r.success).length,
151
+ failedFiles: results.filter((r) => !r.success).length,
152
+ totalWarnings: results.reduce((acc, r) => acc + r.warnings.length, 0),
153
+ from,
154
+ to,
155
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
156
+ duration: endTime - startTime
157
+ },
158
+ files: results.map((r) => ({
159
+ path: r.filePath,
160
+ success: r.success,
161
+ errors: r.errors.map((e) => e.message),
162
+ warnings: r.warnings,
163
+ diff: r.transformedCode ? {
164
+ before: r.originalCode,
165
+ after: r.transformedCode
166
+ } : void 0
167
+ })),
168
+ customRulesApplied: customRules
169
+ };
170
+ }
171
+ writeJson(report, outputPath) {
172
+ writeFileSync2(outputPath, JSON.stringify(report, null, 2));
173
+ }
174
+ writeHtml(report, outputPath) {
175
+ const html = this.generateHtml(report);
176
+ writeFileSync2(outputPath, html);
177
+ }
178
+ generateHtml(report) {
179
+ const successRate = Math.round(
180
+ report.summary.successfulFiles / report.summary.totalFiles * 100
181
+ );
182
+ return `<!DOCTYPE html>
183
+ <html lang="en">
184
+ <head>
185
+ <meta charset="UTF-8">
186
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
187
+ <title>SchemaShift Migration Report</title>
188
+ <style>
189
+ :root {
190
+ --success: #10b981;
191
+ --error: #ef4444;
192
+ --warning: #f59e0b;
193
+ --bg: #0f172a;
194
+ --surface: #1e293b;
195
+ --text: #e2e8f0;
196
+ --muted: #94a3b8;
197
+ }
198
+ * { box-sizing: border-box; margin: 0; padding: 0; }
199
+ body {
200
+ font-family: system-ui, -apple-system, sans-serif;
201
+ background: var(--bg);
202
+ color: var(--text);
203
+ line-height: 1.6;
204
+ padding: 2rem;
205
+ }
206
+ .container { max-width: 1200px; margin: 0 auto; }
207
+ h1 { font-size: 2rem; margin-bottom: 0.5rem; }
208
+ .subtitle { color: var(--muted); margin-bottom: 2rem; }
209
+ .summary {
210
+ display: grid;
211
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
212
+ gap: 1rem;
213
+ margin-bottom: 2rem;
214
+ }
215
+ .card {
216
+ background: var(--surface);
217
+ border-radius: 0.5rem;
218
+ padding: 1.5rem;
219
+ }
220
+ .card-label { color: var(--muted); font-size: 0.875rem; }
221
+ .card-value { font-size: 2rem; font-weight: bold; }
222
+ .card-value.success { color: var(--success); }
223
+ .card-value.error { color: var(--error); }
224
+ .card-value.warning { color: var(--warning); }
225
+ .progress-bar {
226
+ height: 8px;
227
+ background: var(--surface);
228
+ border-radius: 4px;
229
+ margin: 1rem 0;
230
+ overflow: hidden;
231
+ }
232
+ .progress-fill {
233
+ height: 100%;
234
+ background: var(--success);
235
+ transition: width 0.3s;
236
+ }
237
+ .file-list { list-style: none; }
238
+ .file-item {
239
+ background: var(--surface);
240
+ border-radius: 0.5rem;
241
+ padding: 1rem;
242
+ margin-bottom: 0.5rem;
243
+ }
244
+ .file-header {
245
+ display: flex;
246
+ justify-content: space-between;
247
+ align-items: center;
248
+ }
249
+ .file-path { font-family: monospace; }
250
+ .badge {
251
+ padding: 0.25rem 0.5rem;
252
+ border-radius: 0.25rem;
253
+ font-size: 0.75rem;
254
+ font-weight: bold;
255
+ }
256
+ .badge.success { background: var(--success); color: white; }
257
+ .badge.error { background: var(--error); color: white; }
258
+ .warnings {
259
+ margin-top: 0.5rem;
260
+ padding-left: 1rem;
261
+ border-left: 2px solid var(--warning);
262
+ color: var(--warning);
263
+ font-size: 0.875rem;
264
+ }
265
+ .diff {
266
+ margin-top: 1rem;
267
+ background: #0d1117;
268
+ border-radius: 0.25rem;
269
+ padding: 1rem;
270
+ font-family: monospace;
271
+ font-size: 0.75rem;
272
+ overflow-x: auto;
273
+ }
274
+ details summary { cursor: pointer; color: var(--muted); }
275
+ </style>
276
+ </head>
277
+ <body>
278
+ <div class="container">
279
+ <h1>SchemaShift Migration Report</h1>
280
+ <p class="subtitle">${report.summary.from} \u2192 ${report.summary.to} | ${new Date(report.summary.timestamp).toLocaleString()}</p>
281
+
282
+ <div class="summary">
283
+ <div class="card">
284
+ <div class="card-label">Total Files</div>
285
+ <div class="card-value">${report.summary.totalFiles}</div>
286
+ </div>
287
+ <div class="card">
288
+ <div class="card-label">Successful</div>
289
+ <div class="card-value success">${report.summary.successfulFiles}</div>
290
+ </div>
291
+ <div class="card">
292
+ <div class="card-label">Failed</div>
293
+ <div class="card-value error">${report.summary.failedFiles}</div>
294
+ </div>
295
+ <div class="card">
296
+ <div class="card-label">Warnings</div>
297
+ <div class="card-value warning">${report.summary.totalWarnings}</div>
298
+ </div>
299
+ </div>
300
+
301
+ <div class="card">
302
+ <div class="card-label">Success Rate</div>
303
+ <div class="progress-bar">
304
+ <div class="progress-fill" style="width: ${successRate}%"></div>
305
+ </div>
306
+ <div class="card-value">${successRate}%</div>
307
+ </div>
308
+
309
+ <h2 style="margin: 2rem 0 1rem;">Files</h2>
310
+ <ul class="file-list">
311
+ ${report.files.map(
312
+ (file) => `
313
+ <li class="file-item">
314
+ <div class="file-header">
315
+ <span class="file-path">${file.path}</span>
316
+ <span class="badge ${file.success ? "success" : "error"}">${file.success ? "Success" : "Failed"}</span>
317
+ </div>
318
+ ${file.errors.length > 0 ? `
319
+ <div class="warnings" style="border-color: var(--error); color: var(--error);">
320
+ ${file.errors.map((e) => `<div>${e}</div>`).join("")}
321
+ </div>
322
+ ` : ""}
323
+ ${file.warnings.length > 0 ? `
324
+ <div class="warnings">
325
+ ${file.warnings.map((w) => `<div>${w}</div>`).join("")}
326
+ </div>
327
+ ` : ""}
328
+ ${file.diff ? `
329
+ <details>
330
+ <summary>View diff</summary>
331
+ <div class="diff">
332
+ <pre>${this.escapeHtml(file.diff.after)}</pre>
333
+ </div>
334
+ </details>
335
+ ` : ""}
336
+ </li>
337
+ `
338
+ ).join("")}
339
+ </ul>
340
+
341
+ <footer style="margin-top: 2rem; text-align: center; color: var(--muted);">
342
+ Generated by SchemaShift | Duration: ${report.summary.duration}ms
343
+ </footer>
344
+ </div>
345
+ </body>
346
+ </html>`;
347
+ }
348
+ escapeHtml(text) {
349
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
350
+ }
351
+ };
352
+
353
+ // src/watch.ts
354
+ import { watch } from "fs";
355
+ import { relative as relative2 } from "path";
356
+ import { glob } from "glob";
357
+ import pc from "picocolors";
358
+ var WatchMode = class {
359
+ watchers = [];
360
+ debounceTimers = /* @__PURE__ */ new Map();
361
+ async start(options) {
362
+ const files = await glob(options.patterns, {
363
+ ignore: options.exclude
364
+ });
365
+ console.log(pc.cyan(`
366
+ Watching ${files.length} files for changes...
367
+ `));
368
+ console.log(pc.dim("Press Ctrl+C to stop\n"));
369
+ const directories = new Set(files.map((f) => f.split("/").slice(0, -1).join("/")));
370
+ for (const dir of directories) {
371
+ const watcher = watch(dir || ".", { recursive: true }, async (_event, filename) => {
372
+ if (!filename) return;
373
+ const fullPath = dir ? `${dir}/${filename}` : filename;
374
+ if (!this.matchesPatterns(fullPath, options.patterns, options.exclude)) {
375
+ return;
376
+ }
377
+ const existingTimer = this.debounceTimers.get(fullPath);
378
+ if (existingTimer) {
379
+ clearTimeout(existingTimer);
380
+ }
381
+ this.debounceTimers.set(
382
+ fullPath,
383
+ setTimeout(async () => {
384
+ console.log(pc.yellow(`
385
+ Changed: ${relative2(process.cwd(), fullPath)}`));
386
+ try {
387
+ await options.onTransform(fullPath);
388
+ console.log(pc.green(`Transformed successfully
389
+ `));
390
+ } catch (error) {
391
+ console.error(pc.red(`Transform failed: ${error}
392
+ `));
393
+ }
394
+ this.debounceTimers.delete(fullPath);
395
+ }, 100)
396
+ );
397
+ });
398
+ this.watchers.push(watcher);
399
+ }
400
+ }
401
+ stop() {
402
+ for (const watcher of this.watchers) {
403
+ watcher.close();
404
+ }
405
+ this.watchers = [];
406
+ for (const timer of this.debounceTimers.values()) {
407
+ clearTimeout(timer);
408
+ }
409
+ this.debounceTimers.clear();
410
+ }
411
+ matchesPatterns(file, _include, _exclude) {
412
+ const isIncluded = file.endsWith(".ts") || file.endsWith(".tsx");
413
+ const isExcluded = file.includes("node_modules") || file.includes("dist");
414
+ return isIncluded && !isExcluded;
415
+ }
416
+ };
417
+ export {
418
+ BackupManager,
419
+ GitIntegration,
420
+ LicenseManager,
421
+ LicenseTier,
422
+ ReportGenerator,
423
+ SchemaAnalyzer,
424
+ TIER_FEATURES,
425
+ TransformEngine,
426
+ WatchMode,
427
+ createIoTsToZodHandler,
428
+ createJoiToZodHandler,
429
+ createYupToZodHandler,
430
+ createZodV3ToV4Handler,
431
+ loadConfig
432
+ };
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "schemashift-cli",
3
+ "version": "0.3.0",
4
+ "description": "TypeScript schema migration CLI - migrate between Zod, Yup, Joi, and more",
5
+ "type": "module",
6
+ "bin": {
7
+ "schemashift": "./dist/cli.js"
8
+ },
9
+ "main": "./dist/index.cjs",
10
+ "module": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "import": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ },
18
+ "require": {
19
+ "types": "./dist/index.d.cts",
20
+ "default": "./dist/index.cjs"
21
+ }
22
+ }
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "scripts": {
28
+ "build": "tsup",
29
+ "test": "vitest run",
30
+ "typecheck": "tsc --noEmit"
31
+ },
32
+ "keywords": [
33
+ "typescript",
34
+ "schema",
35
+ "migration",
36
+ "zod",
37
+ "yup",
38
+ "joi",
39
+ "codemod"
40
+ ],
41
+ "homepage": "https://schemashift.qwady.app",
42
+ "bugs": {
43
+ "email": "support@qwady.com"
44
+ },
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "https://github.com/jdmay2/schemashift"
48
+ },
49
+ "author": "Joseph May",
50
+ "license": "MIT",
51
+ "dependencies": {
52
+ "@schemashift/core": "0.3.0",
53
+ "@schemashift/io-ts-zod": "0.3.0",
54
+ "@schemashift/joi-zod": "0.3.0",
55
+ "@schemashift/license": "0.3.0",
56
+ "@schemashift/yup-zod": "0.3.0",
57
+ "@schemashift/zod-v3-v4": "0.3.0",
58
+ "commander": "14.0.2",
59
+ "cosmiconfig": "9.0.0",
60
+ "glob": "13.0.0",
61
+ "listr2": "10.1.0",
62
+ "picocolors": "1.1.1"
63
+ },
64
+ "publishConfig": {
65
+ "access": "public"
66
+ }
67
+ }