@xnetjs/cli 0.0.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/LICENSE +21 -0
- package/README.md +49 -0
- package/dist/chunk-IUOECMVU.js +314 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +890 -0
- package/dist/index.d.ts +120 -0
- package/dist/index.js +8 -0
- package/package.json +50 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
diffSchemas,
|
|
4
|
+
generateLensCode,
|
|
5
|
+
generateLensSnippet
|
|
6
|
+
} from "./chunk-IUOECMVU.js";
|
|
7
|
+
|
|
8
|
+
// src/cli.ts
|
|
9
|
+
import { program } from "commander";
|
|
10
|
+
|
|
11
|
+
// src/commands/doctor.ts
|
|
12
|
+
import { writeFileSync, readFileSync, existsSync, readdirSync, statSync } from "fs";
|
|
13
|
+
import { resolve, join } from "path";
|
|
14
|
+
import {
|
|
15
|
+
verifyIntegrity,
|
|
16
|
+
quickIntegrityCheck,
|
|
17
|
+
attemptRepair,
|
|
18
|
+
findOrphans,
|
|
19
|
+
findRoots,
|
|
20
|
+
findHeads,
|
|
21
|
+
getChainDepth
|
|
22
|
+
} from "@xnetjs/sync";
|
|
23
|
+
async function getChalk() {
|
|
24
|
+
try {
|
|
25
|
+
const chalk = await import("chalk");
|
|
26
|
+
return chalk.default;
|
|
27
|
+
} catch {
|
|
28
|
+
const identity = (s) => s;
|
|
29
|
+
return {
|
|
30
|
+
green: identity,
|
|
31
|
+
yellow: identity,
|
|
32
|
+
red: identity,
|
|
33
|
+
blue: identity,
|
|
34
|
+
gray: identity,
|
|
35
|
+
cyan: identity,
|
|
36
|
+
bold: identity,
|
|
37
|
+
dim: identity
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function findDataDir(providedDir) {
|
|
42
|
+
if (providedDir) {
|
|
43
|
+
const resolved = resolve(process.cwd(), providedDir);
|
|
44
|
+
if (existsSync(resolved)) {
|
|
45
|
+
return resolved;
|
|
46
|
+
}
|
|
47
|
+
throw new Error(`Data directory not found: ${resolved}`);
|
|
48
|
+
}
|
|
49
|
+
const commonPaths = [".xnet/data", "data", ".data", resolve(process.env.HOME ?? "", ".xnet/data")];
|
|
50
|
+
for (const path of commonPaths) {
|
|
51
|
+
const resolved = resolve(process.cwd(), path);
|
|
52
|
+
if (existsSync(resolved)) {
|
|
53
|
+
return resolved;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
throw new Error("Could not find data directory. Use --data-dir to specify location.");
|
|
57
|
+
}
|
|
58
|
+
function loadChangesFromDir(dataDir) {
|
|
59
|
+
const changes = [];
|
|
60
|
+
const patterns = ["changes.json", "changes/*.json", "*.changes.json"];
|
|
61
|
+
for (const pattern of patterns) {
|
|
62
|
+
const changesFile = join(dataDir, pattern.split("/")[0]);
|
|
63
|
+
if (existsSync(changesFile) && statSync(changesFile).isFile()) {
|
|
64
|
+
try {
|
|
65
|
+
const content = readFileSync(changesFile, "utf-8");
|
|
66
|
+
const data = JSON.parse(content);
|
|
67
|
+
if (Array.isArray(data)) {
|
|
68
|
+
changes.push(...data);
|
|
69
|
+
} else if (data.changes && Array.isArray(data.changes)) {
|
|
70
|
+
changes.push(...data.changes);
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const changesDir = join(dataDir, "changes");
|
|
77
|
+
if (existsSync(changesDir) && statSync(changesDir).isDirectory()) {
|
|
78
|
+
const files = readdirSync(changesDir).filter((f) => f.endsWith(".json"));
|
|
79
|
+
for (const file of files) {
|
|
80
|
+
try {
|
|
81
|
+
const content = readFileSync(join(changesDir, file), "utf-8");
|
|
82
|
+
const change = JSON.parse(content);
|
|
83
|
+
changes.push(change);
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return changes;
|
|
89
|
+
}
|
|
90
|
+
function formatDuration(ms) {
|
|
91
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
92
|
+
return `${(ms / 1e3).toFixed(2)}s`;
|
|
93
|
+
}
|
|
94
|
+
async function doctorCommand(options) {
|
|
95
|
+
const chalk = await getChalk();
|
|
96
|
+
console.log(chalk.bold("\nxNet Health Check\n"));
|
|
97
|
+
console.log(chalk.dim("\u2500".repeat(50)));
|
|
98
|
+
try {
|
|
99
|
+
let dataDir;
|
|
100
|
+
let changes = [];
|
|
101
|
+
try {
|
|
102
|
+
dataDir = findDataDir(options.dataDir);
|
|
103
|
+
console.log(chalk.gray(`Data directory: ${dataDir}`));
|
|
104
|
+
changes = loadChangesFromDir(dataDir);
|
|
105
|
+
console.log(chalk.gray(`Found ${changes.length} changes`));
|
|
106
|
+
} catch {
|
|
107
|
+
console.log(chalk.yellow("\nNote: No data directory found."));
|
|
108
|
+
console.log(chalk.gray("Use --data-dir to specify a data directory."));
|
|
109
|
+
console.log(chalk.gray("Running with demo data...\n"));
|
|
110
|
+
changes = generateDemoChanges();
|
|
111
|
+
}
|
|
112
|
+
console.log();
|
|
113
|
+
console.log(chalk.bold("Checking data integrity..."));
|
|
114
|
+
const report = options.quick ? await quickIntegrityCheck(changes) : await verifyIntegrity(changes);
|
|
115
|
+
if (options.json) {
|
|
116
|
+
console.log(JSON.stringify(report, null, 2));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
printIntegrityResults(report, chalk, options.verbose);
|
|
120
|
+
console.log();
|
|
121
|
+
console.log(chalk.bold("Analyzing change chains..."));
|
|
122
|
+
const orphans = findOrphans(changes);
|
|
123
|
+
const roots = findRoots(changes);
|
|
124
|
+
const heads = findHeads(changes);
|
|
125
|
+
const depth = getChainDepth(changes);
|
|
126
|
+
console.log(` ${chalk.green("\u2713")} Roots: ${roots.length}`);
|
|
127
|
+
console.log(` ${chalk.green("\u2713")} Heads: ${heads.length}`);
|
|
128
|
+
console.log(` ${chalk.green("\u2713")} Depth: ${depth}`);
|
|
129
|
+
if (orphans.length > 0) {
|
|
130
|
+
console.log(` ${chalk.yellow("\u26A0")} Orphans: ${orphans.length}`);
|
|
131
|
+
} else {
|
|
132
|
+
console.log(` ${chalk.green("\u2713")} Orphans: 0`);
|
|
133
|
+
}
|
|
134
|
+
console.log();
|
|
135
|
+
console.log(chalk.bold("Checking schema compatibility..."));
|
|
136
|
+
console.log(chalk.gray(" (Schema analysis requires @xnetjs/data integration)"));
|
|
137
|
+
console.log();
|
|
138
|
+
console.log(chalk.bold("Checking sync state..."));
|
|
139
|
+
console.log(chalk.gray(" (Sync analysis requires active SyncProvider)"));
|
|
140
|
+
console.log();
|
|
141
|
+
console.log(chalk.dim("\u2500".repeat(50)));
|
|
142
|
+
const hasErrors = report.summary.errors > 0;
|
|
143
|
+
const hasWarnings = report.summary.warnings > 0 || orphans.length > 0;
|
|
144
|
+
if (hasErrors) {
|
|
145
|
+
console.log(chalk.red(chalk.bold("Status: UNHEALTHY")));
|
|
146
|
+
console.log(chalk.red(" Data integrity issues detected. Run `xnet repair` to fix."));
|
|
147
|
+
} else if (hasWarnings) {
|
|
148
|
+
console.log(chalk.yellow(chalk.bold("Status: HEALTHY with warnings")));
|
|
149
|
+
console.log(chalk.gray(" Some issues detected but data is intact."));
|
|
150
|
+
} else {
|
|
151
|
+
console.log(chalk.green(chalk.bold("Status: HEALTHY")));
|
|
152
|
+
console.log(chalk.green(" All checks passed."));
|
|
153
|
+
}
|
|
154
|
+
console.log();
|
|
155
|
+
} catch (error) {
|
|
156
|
+
console.error(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function printIntegrityResults(report, chalk, verbose) {
|
|
161
|
+
const { checked, valid, issues, summary, durationMs } = report;
|
|
162
|
+
const percentage = checked > 0 ? Math.round(valid / checked * 100) : 100;
|
|
163
|
+
if (issues.length === 0) {
|
|
164
|
+
console.log(` ${chalk.green("\u2713")} ${checked} changes verified (${percentage}% valid)`);
|
|
165
|
+
console.log(` ${chalk.green("\u2713")} Hash chains intact`);
|
|
166
|
+
console.log(` ${chalk.green("\u2713")} No issues detected`);
|
|
167
|
+
console.log(chalk.gray(` Completed in ${formatDuration(durationMs)}`));
|
|
168
|
+
} else {
|
|
169
|
+
console.log(
|
|
170
|
+
` ${chalk.yellow("!")} ${checked} changes checked (${valid} valid, ${checked - valid} issues)`
|
|
171
|
+
);
|
|
172
|
+
if (summary.errors > 0) {
|
|
173
|
+
console.log(` ${chalk.red("\u2717")} ${summary.errors} errors`);
|
|
174
|
+
}
|
|
175
|
+
if (summary.warnings > 0) {
|
|
176
|
+
console.log(` ${chalk.yellow("\u26A0")} ${summary.warnings} warnings`);
|
|
177
|
+
}
|
|
178
|
+
console.log(chalk.gray(` Completed in ${formatDuration(durationMs)}`));
|
|
179
|
+
if (verbose) {
|
|
180
|
+
console.log();
|
|
181
|
+
console.log(chalk.bold("Issues by type:"));
|
|
182
|
+
for (const [type, count] of Object.entries(summary.byType)) {
|
|
183
|
+
if (count > 0) {
|
|
184
|
+
console.log(` - ${type}: ${count}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
console.log();
|
|
188
|
+
console.log(chalk.bold("Details:"));
|
|
189
|
+
for (const issue of issues.slice(0, 10)) {
|
|
190
|
+
const icon = issue.severity === "error" ? chalk.red("\u2717") : chalk.yellow("\u26A0");
|
|
191
|
+
console.log(` ${icon} [${issue.type}] ${issue.details}`);
|
|
192
|
+
if (issue.repairAction) {
|
|
193
|
+
console.log(chalk.gray(` \u2192 ${issue.repairAction.description}`));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (issues.length > 10) {
|
|
197
|
+
console.log(chalk.gray(` ... and ${issues.length - 10} more`));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (report.repairable) {
|
|
201
|
+
console.log();
|
|
202
|
+
console.log(chalk.cyan(` Run \`xnet repair\` to fix these issues.`));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function generateDemoChanges() {
|
|
207
|
+
const now = Date.now();
|
|
208
|
+
const demoAuthor = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK";
|
|
209
|
+
const demoHash1 = "cid:blake3:0000000000000000000000000000000000000000000000000000000000000001";
|
|
210
|
+
const demoHash2 = "cid:blake3:0000000000000000000000000000000000000000000000000000000000000002";
|
|
211
|
+
return [
|
|
212
|
+
{
|
|
213
|
+
id: "change-1",
|
|
214
|
+
protocolVersion: 1,
|
|
215
|
+
type: "create-node",
|
|
216
|
+
payload: { schemaIRI: "xnet://xnet.fyi/Task@1.0.0" },
|
|
217
|
+
hash: demoHash1,
|
|
218
|
+
parentHash: null,
|
|
219
|
+
authorDID: demoAuthor,
|
|
220
|
+
signature: new Uint8Array([1, 2, 3, 4]),
|
|
221
|
+
wallTime: now - 1e4,
|
|
222
|
+
lamport: { time: 1, author: demoAuthor }
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
id: "change-2",
|
|
226
|
+
protocolVersion: 1,
|
|
227
|
+
type: "update-node",
|
|
228
|
+
payload: { nodeId: "node-1", properties: { title: "Demo Task" } },
|
|
229
|
+
hash: demoHash2,
|
|
230
|
+
parentHash: demoHash1,
|
|
231
|
+
authorDID: demoAuthor,
|
|
232
|
+
signature: new Uint8Array([5, 6, 7, 8]),
|
|
233
|
+
wallTime: now - 5e3,
|
|
234
|
+
lamport: { time: 2, author: demoAuthor }
|
|
235
|
+
}
|
|
236
|
+
];
|
|
237
|
+
}
|
|
238
|
+
async function repairCommand(options) {
|
|
239
|
+
const chalk = await getChalk();
|
|
240
|
+
console.log(chalk.bold("\nxNet Data Repair\n"));
|
|
241
|
+
try {
|
|
242
|
+
let dataDir;
|
|
243
|
+
let changes = [];
|
|
244
|
+
try {
|
|
245
|
+
dataDir = findDataDir(options.dataDir);
|
|
246
|
+
changes = loadChangesFromDir(dataDir);
|
|
247
|
+
} catch {
|
|
248
|
+
console.log(chalk.yellow("Note: No data directory found."));
|
|
249
|
+
console.log(chalk.gray("Using demo data for illustration.\n"));
|
|
250
|
+
dataDir = "";
|
|
251
|
+
changes = generateDemoChanges();
|
|
252
|
+
}
|
|
253
|
+
console.log(chalk.gray("Running integrity check..."));
|
|
254
|
+
const report = await verifyIntegrity(changes);
|
|
255
|
+
if (report.issues.length === 0) {
|
|
256
|
+
console.log(chalk.green("\n\u2713 No issues found. Nothing to repair."));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
console.log(`
|
|
260
|
+
Found ${report.issues.length} issues:`);
|
|
261
|
+
console.log(` - ${report.summary.errors} errors`);
|
|
262
|
+
console.log(` - ${report.summary.warnings} warnings`);
|
|
263
|
+
console.log();
|
|
264
|
+
if (options.dryRun) {
|
|
265
|
+
console.log(chalk.yellow("DRY RUN - No changes will be made\n"));
|
|
266
|
+
}
|
|
267
|
+
const { remainingIssues, repairCount } = await attemptRepair(changes, report.issues);
|
|
268
|
+
if (options.json) {
|
|
269
|
+
console.log(
|
|
270
|
+
JSON.stringify(
|
|
271
|
+
{
|
|
272
|
+
original: changes.length,
|
|
273
|
+
repaired: repairCount,
|
|
274
|
+
remaining: remainingIssues.length,
|
|
275
|
+
remainingIssues
|
|
276
|
+
},
|
|
277
|
+
null,
|
|
278
|
+
2
|
|
279
|
+
)
|
|
280
|
+
);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
console.log(chalk.bold("Repair results:"));
|
|
284
|
+
console.log(` ${chalk.green("\u2713")} Repaired: ${repairCount} issues`);
|
|
285
|
+
console.log(` ${chalk.yellow("!")} Remaining: ${remainingIssues.length} issues`);
|
|
286
|
+
if (remainingIssues.length > 0) {
|
|
287
|
+
console.log();
|
|
288
|
+
console.log(chalk.yellow("Issues that require manual intervention:"));
|
|
289
|
+
for (const issue of remainingIssues.slice(0, 5)) {
|
|
290
|
+
console.log(` - [${issue.type}] ${issue.details}`);
|
|
291
|
+
}
|
|
292
|
+
if (remainingIssues.length > 5) {
|
|
293
|
+
console.log(chalk.gray(` ... and ${remainingIssues.length - 5} more`));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (!options.dryRun && dataDir && repairCount > 0) {
|
|
297
|
+
console.log();
|
|
298
|
+
console.log(chalk.gray("Note: Writing repaired changes requires NodeStore integration."));
|
|
299
|
+
console.log(chalk.gray("Changes are prepared but not written to disk."));
|
|
300
|
+
}
|
|
301
|
+
console.log();
|
|
302
|
+
} catch (error) {
|
|
303
|
+
console.error(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
async function exportCommand(options) {
|
|
308
|
+
const chalk = await getChalk();
|
|
309
|
+
console.log(chalk.bold("\nxNet Data Export\n"));
|
|
310
|
+
try {
|
|
311
|
+
let dataDir;
|
|
312
|
+
let changes = [];
|
|
313
|
+
try {
|
|
314
|
+
dataDir = findDataDir(options.dataDir);
|
|
315
|
+
changes = loadChangesFromDir(dataDir);
|
|
316
|
+
} catch {
|
|
317
|
+
console.log(chalk.yellow("Note: No data directory found."));
|
|
318
|
+
console.log(chalk.gray("Using demo data for illustration.\n"));
|
|
319
|
+
dataDir = "";
|
|
320
|
+
changes = generateDemoChanges();
|
|
321
|
+
}
|
|
322
|
+
const outputPath = resolve(process.cwd(), options.output);
|
|
323
|
+
console.log(chalk.gray(`Source: ${dataDir || "demo data"}`));
|
|
324
|
+
console.log(chalk.gray(`Output: ${outputPath}`));
|
|
325
|
+
console.log();
|
|
326
|
+
const exportData = {
|
|
327
|
+
version: 1,
|
|
328
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
329
|
+
source: dataDir || "demo",
|
|
330
|
+
stats: {
|
|
331
|
+
changes: changes.length,
|
|
332
|
+
roots: findRoots(changes).length,
|
|
333
|
+
heads: findHeads(changes).length,
|
|
334
|
+
depth: getChainDepth(changes)
|
|
335
|
+
},
|
|
336
|
+
changes: changes.map((c) => ({
|
|
337
|
+
...c,
|
|
338
|
+
// Convert Uint8Array to base64 for JSON serialization
|
|
339
|
+
signature: Buffer.from(c.signature).toString("base64")
|
|
340
|
+
}))
|
|
341
|
+
};
|
|
342
|
+
const content = options.pretty ? JSON.stringify(exportData, null, 2) : JSON.stringify(exportData);
|
|
343
|
+
if (options.format === "jsonl") {
|
|
344
|
+
const lines = exportData.changes.map((c) => JSON.stringify(c)).join("\n");
|
|
345
|
+
writeFileSync(outputPath, lines, "utf-8");
|
|
346
|
+
} else {
|
|
347
|
+
writeFileSync(outputPath, content, "utf-8");
|
|
348
|
+
}
|
|
349
|
+
console.log(chalk.green(`\u2713 Exported ${changes.length} changes`));
|
|
350
|
+
console.log(chalk.gray(` File size: ${Math.round(content.length / 1024)}KB`));
|
|
351
|
+
console.log();
|
|
352
|
+
} catch (error) {
|
|
353
|
+
console.error(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
354
|
+
process.exit(1);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
async function importCommand(options) {
|
|
358
|
+
const chalk = await getChalk();
|
|
359
|
+
console.log(chalk.bold("\nxNet Data Import\n"));
|
|
360
|
+
try {
|
|
361
|
+
const inputPath = resolve(process.cwd(), options.input);
|
|
362
|
+
if (!existsSync(inputPath)) {
|
|
363
|
+
console.error(chalk.red(`Error: Input file not found: ${inputPath}`));
|
|
364
|
+
process.exit(1);
|
|
365
|
+
}
|
|
366
|
+
console.log(chalk.gray(`Input: ${inputPath}`));
|
|
367
|
+
if (options.dryRun) {
|
|
368
|
+
console.log(chalk.yellow("\nDRY RUN - No changes will be applied\n"));
|
|
369
|
+
}
|
|
370
|
+
const content = readFileSync(inputPath, "utf-8");
|
|
371
|
+
let importData;
|
|
372
|
+
try {
|
|
373
|
+
const parsed = JSON.parse(content);
|
|
374
|
+
if (Array.isArray(parsed)) {
|
|
375
|
+
importData = { changes: parsed };
|
|
376
|
+
} else {
|
|
377
|
+
importData = parsed;
|
|
378
|
+
}
|
|
379
|
+
} catch {
|
|
380
|
+
const lines = content.split("\n").filter((l) => l.trim());
|
|
381
|
+
importData = { changes: lines.map((l) => JSON.parse(l)) };
|
|
382
|
+
}
|
|
383
|
+
console.log(chalk.gray(`Found ${importData.changes.length} changes to import`));
|
|
384
|
+
console.log();
|
|
385
|
+
const changes = importData.changes.map((c) => ({
|
|
386
|
+
...c,
|
|
387
|
+
signature: typeof c.signature === "string" ? new Uint8Array(Buffer.from(c.signature, "base64")) : c.signature
|
|
388
|
+
}));
|
|
389
|
+
console.log(chalk.bold("Verifying import data..."));
|
|
390
|
+
const report = await quickIntegrityCheck(changes);
|
|
391
|
+
if (report.issues.length > 0) {
|
|
392
|
+
console.log(chalk.yellow(` \u26A0 ${report.issues.length} integrity issues found`));
|
|
393
|
+
if (report.summary.errors > 0) {
|
|
394
|
+
console.log(chalk.red(` ${report.summary.errors} errors may prevent import`));
|
|
395
|
+
}
|
|
396
|
+
} else {
|
|
397
|
+
console.log(chalk.green(" \u2713 All changes verified"));
|
|
398
|
+
}
|
|
399
|
+
if (options.applyMigrations) {
|
|
400
|
+
console.log();
|
|
401
|
+
console.log(chalk.bold("Checking for required migrations..."));
|
|
402
|
+
console.log(chalk.gray(" (Migration detection requires schema registry)"));
|
|
403
|
+
}
|
|
404
|
+
console.log();
|
|
405
|
+
console.log(chalk.bold("Import summary:"));
|
|
406
|
+
console.log(` Changes: ${changes.length}`);
|
|
407
|
+
console.log(` Roots: ${findRoots(changes).length}`);
|
|
408
|
+
console.log(` Heads: ${findHeads(changes).length}`);
|
|
409
|
+
console.log(` Chain depth: ${getChainDepth(changes)}`);
|
|
410
|
+
if (!options.dryRun) {
|
|
411
|
+
console.log();
|
|
412
|
+
console.log(chalk.gray("Note: Writing imported changes requires NodeStore integration."));
|
|
413
|
+
console.log(chalk.gray("Changes are validated but not written to storage."));
|
|
414
|
+
}
|
|
415
|
+
console.log();
|
|
416
|
+
console.log(chalk.green("\u2713 Import validation complete"));
|
|
417
|
+
console.log();
|
|
418
|
+
} catch (error) {
|
|
419
|
+
console.error(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
function registerDoctorCommand(program2) {
|
|
424
|
+
program2.command("doctor").description("Diagnose data integrity and sync issues").option("--data-dir <path>", "Path to data directory").option("-q, --quick", "Quick check (skip signature verification)").option("--json", "Output as JSON").option("-v, --verbose", "Show detailed issue information").action(async (opts) => {
|
|
425
|
+
await doctorCommand(opts);
|
|
426
|
+
});
|
|
427
|
+
program2.command("repair").description("Attempt automatic repair of data issues").option("--data-dir <path>", "Path to data directory").option("--dry-run", "Preview repairs without applying").option("--json", "Output as JSON").action(async (opts) => {
|
|
428
|
+
await repairCommand(opts);
|
|
429
|
+
});
|
|
430
|
+
program2.command("export").description("Export all data to JSON format").requiredOption("-o, --output <path>", "Output file path").option("--data-dir <path>", "Path to data directory").option("--format <type>", "Output format (json or jsonl)", "json").option("--pretty", "Pretty-print JSON output").action(async (opts) => {
|
|
431
|
+
await exportCommand(opts);
|
|
432
|
+
});
|
|
433
|
+
program2.command("import").description("Import data from JSON format").requiredOption("-i, --input <path>", "Input file path").option("--data-dir <path>", "Path to data directory").option("--dry-run", "Validate import without applying").option("--apply-migrations", "Apply schema migrations during import").action(async (opts) => {
|
|
434
|
+
await importCommand(opts);
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// src/commands/migrate.ts
|
|
439
|
+
import { writeFileSync as writeFileSync2, readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
440
|
+
import { resolve as resolve2 } from "path";
|
|
441
|
+
async function getChalk2() {
|
|
442
|
+
try {
|
|
443
|
+
const chalk = await import("chalk");
|
|
444
|
+
return chalk.default;
|
|
445
|
+
} catch {
|
|
446
|
+
const identity = (s) => s;
|
|
447
|
+
return {
|
|
448
|
+
green: identity,
|
|
449
|
+
yellow: identity,
|
|
450
|
+
red: identity,
|
|
451
|
+
blue: identity,
|
|
452
|
+
gray: identity,
|
|
453
|
+
bold: identity,
|
|
454
|
+
dim: identity
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
function parseSchemaIRI(iri) {
|
|
459
|
+
const match = iri.match(/([^/@]+)@(\d+\.\d+\.\d+)$/);
|
|
460
|
+
if (!match) {
|
|
461
|
+
throw new Error(
|
|
462
|
+
`Invalid schema IRI: ${iri}. Expected format: Name@version or xnet://namespace/Name@version`
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
return { name: match[1], version: match[2] };
|
|
466
|
+
}
|
|
467
|
+
function loadSchemasFromFile(filePath) {
|
|
468
|
+
if (!existsSync2(filePath)) {
|
|
469
|
+
throw new Error(`Schema file not found: ${filePath}`);
|
|
470
|
+
}
|
|
471
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
472
|
+
const data = JSON.parse(content);
|
|
473
|
+
const schemas = /* @__PURE__ */ new Map();
|
|
474
|
+
const schemaList = Array.isArray(data) ? data : data.schemas ?? [data];
|
|
475
|
+
for (const schema of schemaList) {
|
|
476
|
+
if (schema["@id"]) {
|
|
477
|
+
schemas.set(schema["@id"], schema);
|
|
478
|
+
const name = schema.name ?? schema["@id"].split("/").pop()?.split("@")[0];
|
|
479
|
+
const version = schema.version ?? "1.0.0";
|
|
480
|
+
schemas.set(`${name}@${version}`, schema);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return schemas;
|
|
484
|
+
}
|
|
485
|
+
function findSchema(schemaIRI, schemas) {
|
|
486
|
+
if (schemas) {
|
|
487
|
+
const schema = schemas.get(schemaIRI);
|
|
488
|
+
if (schema) return schema;
|
|
489
|
+
}
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
async function analyzeCommand(options) {
|
|
493
|
+
const chalk = await getChalk2();
|
|
494
|
+
try {
|
|
495
|
+
const fromParsed = parseSchemaIRI(options.from);
|
|
496
|
+
const toParsed = parseSchemaIRI(options.to);
|
|
497
|
+
let schemas = null;
|
|
498
|
+
if (options.schemaFile) {
|
|
499
|
+
schemas = loadSchemasFromFile(options.schemaFile);
|
|
500
|
+
}
|
|
501
|
+
const fromSchema = findSchema(options.from, schemas);
|
|
502
|
+
const toSchema = findSchema(options.to, schemas);
|
|
503
|
+
if (!fromSchema || !toSchema) {
|
|
504
|
+
console.log(chalk.yellow("\nNote: Schema file not provided. Showing example output.\n"));
|
|
505
|
+
console.log(chalk.gray("Use --schema-file to load actual schemas from a JSON file.\n"));
|
|
506
|
+
const mockFromSchema = {
|
|
507
|
+
"@id": `xnet://xnet.fyi/${fromParsed.name}@${fromParsed.version}`,
|
|
508
|
+
"@type": "xnet://xnet.fyi/Schema",
|
|
509
|
+
name: fromParsed.name,
|
|
510
|
+
namespace: "xnet://xnet.fyi/",
|
|
511
|
+
version: fromParsed.version,
|
|
512
|
+
properties: [
|
|
513
|
+
{ "@id": `#complete`, name: "complete", type: "checkbox", required: false },
|
|
514
|
+
{ "@id": `#title`, name: "title", type: "text", required: true }
|
|
515
|
+
]
|
|
516
|
+
};
|
|
517
|
+
const mockToSchema = {
|
|
518
|
+
"@id": `xnet://xnet.fyi/${toParsed.name}@${toParsed.version}`,
|
|
519
|
+
"@type": "xnet://xnet.fyi/Schema",
|
|
520
|
+
name: toParsed.name,
|
|
521
|
+
namespace: "xnet://xnet.fyi/",
|
|
522
|
+
version: toParsed.version,
|
|
523
|
+
properties: [
|
|
524
|
+
{
|
|
525
|
+
"@id": `#status`,
|
|
526
|
+
name: "status",
|
|
527
|
+
type: "select",
|
|
528
|
+
required: false
|
|
529
|
+
},
|
|
530
|
+
{ "@id": `#title`, name: "title", type: "text", required: true },
|
|
531
|
+
{ "@id": `#priority`, name: "priority", type: "text", required: true }
|
|
532
|
+
]
|
|
533
|
+
};
|
|
534
|
+
printAnalysisResult(diffSchemas(mockFromSchema, mockToSchema), options.json, chalk);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
const diff = diffSchemas(fromSchema, toSchema);
|
|
538
|
+
printAnalysisResult(diff, options.json, chalk);
|
|
539
|
+
} catch (error) {
|
|
540
|
+
console.error(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
541
|
+
process.exit(1);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
function printAnalysisResult(diff, json, chalk) {
|
|
545
|
+
if (json) {
|
|
546
|
+
console.log(JSON.stringify(diff, null, 2));
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
console.log(chalk.bold(`
|
|
550
|
+
Schema changes: ${diff.fromVersion} \u2192 ${diff.toVersion}
|
|
551
|
+
`));
|
|
552
|
+
if (diff.changes.length === 0) {
|
|
553
|
+
console.log(chalk.green("No changes detected."));
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
const breaking = diff.changes.filter((c) => c.risk === "breaking");
|
|
557
|
+
const caution = diff.changes.filter((c) => c.risk === "caution");
|
|
558
|
+
const safe = diff.changes.filter((c) => c.risk === "safe");
|
|
559
|
+
if (breaking.length > 0) {
|
|
560
|
+
console.log(chalk.red(chalk.bold("BREAKING CHANGES:")));
|
|
561
|
+
for (const change of breaking) {
|
|
562
|
+
console.log(chalk.red(` - ${change.type.toUpperCase()}: ${change.description}`));
|
|
563
|
+
}
|
|
564
|
+
console.log();
|
|
565
|
+
}
|
|
566
|
+
if (caution.length > 0) {
|
|
567
|
+
console.log(chalk.yellow(chalk.bold("CAUTION:")));
|
|
568
|
+
for (const change of caution) {
|
|
569
|
+
console.log(chalk.yellow(` - ${change.type.toUpperCase()}: ${change.description}`));
|
|
570
|
+
}
|
|
571
|
+
console.log();
|
|
572
|
+
}
|
|
573
|
+
if (safe.length > 0) {
|
|
574
|
+
console.log(chalk.green(chalk.bold("SAFE:")));
|
|
575
|
+
for (const change of safe) {
|
|
576
|
+
console.log(chalk.green(` - ${change.type.toUpperCase()}: ${change.description}`));
|
|
577
|
+
}
|
|
578
|
+
console.log();
|
|
579
|
+
}
|
|
580
|
+
console.log(chalk.bold("Summary:"));
|
|
581
|
+
console.log(
|
|
582
|
+
` ${chalk.red(`${diff.summary.breaking} breaking`)} | ${chalk.yellow(`${diff.summary.caution} caution`)} | ${chalk.green(`${diff.summary.safe} safe`)}`
|
|
583
|
+
);
|
|
584
|
+
console.log();
|
|
585
|
+
if (diff.changes.some((c) => c.suggestedLens)) {
|
|
586
|
+
console.log(chalk.bold("Suggested lens:"));
|
|
587
|
+
console.log(chalk.dim("\u2500".repeat(40)));
|
|
588
|
+
console.log(generateLensSnippet(diff));
|
|
589
|
+
console.log(chalk.dim("\u2500".repeat(40)));
|
|
590
|
+
console.log();
|
|
591
|
+
}
|
|
592
|
+
if (diff.autoMigratable) {
|
|
593
|
+
console.log(chalk.green("\u2713 Automatic migration possible"));
|
|
594
|
+
} else {
|
|
595
|
+
console.log(chalk.yellow("\u26A0 Manual intervention required for some changes"));
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
async function generateCommand(options) {
|
|
599
|
+
const chalk = await getChalk2();
|
|
600
|
+
try {
|
|
601
|
+
const fromParsed = parseSchemaIRI(options.from);
|
|
602
|
+
const toParsed = parseSchemaIRI(options.to);
|
|
603
|
+
let schemas = null;
|
|
604
|
+
if (options.schemaFile) {
|
|
605
|
+
schemas = loadSchemasFromFile(options.schemaFile);
|
|
606
|
+
}
|
|
607
|
+
const fromSchema = findSchema(options.from, schemas);
|
|
608
|
+
const toSchema = findSchema(options.to, schemas);
|
|
609
|
+
const from = fromSchema ?? createMockSchema(fromParsed.name, fromParsed.version, "from");
|
|
610
|
+
const to = toSchema ?? createMockSchema(toParsed.name, toParsed.version, "to");
|
|
611
|
+
if (!fromSchema || !toSchema) {
|
|
612
|
+
console.log(chalk.yellow("\nNote: Using mock schemas for demonstration.\n"));
|
|
613
|
+
}
|
|
614
|
+
const diff = diffSchemas(from, to);
|
|
615
|
+
const sourceIRI = from["@id"];
|
|
616
|
+
const targetIRI = to["@id"];
|
|
617
|
+
const generated = generateLensCode({
|
|
618
|
+
diff,
|
|
619
|
+
sourceIRI,
|
|
620
|
+
targetIRI,
|
|
621
|
+
includeComments: true,
|
|
622
|
+
includeTodos: true
|
|
623
|
+
});
|
|
624
|
+
if (options.output) {
|
|
625
|
+
const outputPath = resolve2(process.cwd(), options.output);
|
|
626
|
+
if (existsSync2(outputPath) && !options.force) {
|
|
627
|
+
console.error(chalk.red(`Error: File already exists: ${outputPath}`));
|
|
628
|
+
console.error(chalk.gray("Use --force to overwrite."));
|
|
629
|
+
process.exit(1);
|
|
630
|
+
}
|
|
631
|
+
writeFileSync2(outputPath, generated.code, "utf-8");
|
|
632
|
+
console.log(chalk.green(`
|
|
633
|
+
\u2713 Generated lens written to: ${outputPath}`));
|
|
634
|
+
} else {
|
|
635
|
+
console.log(generated.code);
|
|
636
|
+
}
|
|
637
|
+
if (!generated.isComplete) {
|
|
638
|
+
console.log(chalk.yellow("\n\u26A0 Manual completion required:"));
|
|
639
|
+
for (const item of generated.manualItems) {
|
|
640
|
+
console.log(chalk.yellow(` - ${item}`));
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
} catch (error) {
|
|
644
|
+
console.error(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
645
|
+
process.exit(1);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
function createMockSchema(name, version, type) {
|
|
649
|
+
if (type === "from") {
|
|
650
|
+
return {
|
|
651
|
+
"@id": `xnet://xnet.fyi/${name}@${version}`,
|
|
652
|
+
"@type": "xnet://xnet.fyi/Schema",
|
|
653
|
+
name,
|
|
654
|
+
namespace: "xnet://xnet.fyi/",
|
|
655
|
+
version,
|
|
656
|
+
properties: [
|
|
657
|
+
{ "@id": `#complete`, name: "complete", type: "checkbox", required: false },
|
|
658
|
+
{ "@id": `#title`, name: "title", type: "text", required: true }
|
|
659
|
+
]
|
|
660
|
+
};
|
|
661
|
+
} else {
|
|
662
|
+
return {
|
|
663
|
+
"@id": `xnet://xnet.fyi/${name}@${version}`,
|
|
664
|
+
"@type": "xnet://xnet.fyi/Schema",
|
|
665
|
+
name,
|
|
666
|
+
namespace: "xnet://xnet.fyi/",
|
|
667
|
+
version,
|
|
668
|
+
properties: [
|
|
669
|
+
{
|
|
670
|
+
"@id": `#status`,
|
|
671
|
+
name: "status",
|
|
672
|
+
type: "select",
|
|
673
|
+
required: false
|
|
674
|
+
},
|
|
675
|
+
{ "@id": `#title`, name: "title", type: "text", required: true },
|
|
676
|
+
{ "@id": `#priority`, name: "priority", type: "text", required: true }
|
|
677
|
+
]
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
async function runCommand(options) {
|
|
682
|
+
const chalk = await getChalk2();
|
|
683
|
+
if (!options.dryRun && !options.apply) {
|
|
684
|
+
console.error(chalk.red("Error: Must specify either --dry-run or --apply"));
|
|
685
|
+
process.exit(1);
|
|
686
|
+
}
|
|
687
|
+
if (options.dryRun && options.apply) {
|
|
688
|
+
console.error(chalk.red("Error: Cannot specify both --dry-run and --apply"));
|
|
689
|
+
process.exit(1);
|
|
690
|
+
}
|
|
691
|
+
try {
|
|
692
|
+
console.log(chalk.blue(`
|
|
693
|
+
Migration: ${options.from} \u2192 ${options.to}
|
|
694
|
+
`));
|
|
695
|
+
if (options.dryRun) {
|
|
696
|
+
console.log(chalk.yellow("DRY RUN - No changes will be applied\n"));
|
|
697
|
+
console.log(chalk.gray("Would migrate nodes:"));
|
|
698
|
+
console.log(chalk.gray(" - Scan data directory for nodes with schema " + options.from));
|
|
699
|
+
console.log(chalk.gray(" - Apply migration lens to each node"));
|
|
700
|
+
console.log(chalk.gray(" - Update schema version to " + options.to));
|
|
701
|
+
console.log();
|
|
702
|
+
console.log(chalk.dim("Note: Full migration requires --data-dir and --lens-file options"));
|
|
703
|
+
console.log(chalk.dim(" or a configured NodeStore with registered lenses."));
|
|
704
|
+
} else {
|
|
705
|
+
console.log(chalk.yellow("APPLY MODE - Changes will be written\n"));
|
|
706
|
+
console.log(chalk.gray("Would apply migration:"));
|
|
707
|
+
console.log(chalk.gray(" - Load nodes with schema " + options.from));
|
|
708
|
+
console.log(chalk.gray(" - Transform using migration lens"));
|
|
709
|
+
console.log(chalk.gray(" - Write updated nodes with schema " + options.to));
|
|
710
|
+
console.log();
|
|
711
|
+
console.log(chalk.dim("Note: Full migration requires --data-dir and --lens-file options"));
|
|
712
|
+
console.log(chalk.dim(" or a configured NodeStore with registered lenses."));
|
|
713
|
+
}
|
|
714
|
+
console.log();
|
|
715
|
+
console.log(chalk.green("\u2713 Migration command parsed successfully"));
|
|
716
|
+
console.log(chalk.gray(" Full implementation requires NodeStore integration."));
|
|
717
|
+
} catch (error) {
|
|
718
|
+
console.error(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
719
|
+
process.exit(1);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
function registerMigrateCommand(program2) {
|
|
723
|
+
const migrate = program2.command("migrate").description("Schema migration tools");
|
|
724
|
+
migrate.command("analyze").description("Analyze changes between two schema versions").requiredOption("--from <schema>", "Source schema (e.g., Task@1.0.0)").requiredOption("--to <schema>", "Target schema (e.g., Task@2.0.0)").option("--json", "Output as JSON").option("--schema-file <path>", "Load schemas from JSON file").action(async (opts) => {
|
|
725
|
+
await analyzeCommand(opts);
|
|
726
|
+
});
|
|
727
|
+
migrate.command("generate").description("Generate migration lens code").requiredOption("--from <schema>", "Source schema (e.g., Task@1.0.0)").requiredOption("--to <schema>", "Target schema (e.g., Task@2.0.0)").option("-o, --output <path>", "Output file path").option("--schema-file <path>", "Load schemas from JSON file").option("-f, --force", "Overwrite existing files").action(async (opts) => {
|
|
728
|
+
await generateCommand(opts);
|
|
729
|
+
});
|
|
730
|
+
migrate.command("run").description("Execute a migration").requiredOption("--from <schema>", "Source schema (e.g., Task@1.0.0)").requiredOption("--to <schema>", "Target schema (e.g., Task@2.0.0)").option("--dry-run", "Preview changes without applying").option("--apply", "Apply the migration").option("--lens-file <path>", "Path to lens file").option("--data-dir <path>", "Path to data directory").action(async (opts) => {
|
|
731
|
+
await runCommand(opts);
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// src/commands/schema.ts
|
|
736
|
+
import { writeFileSync as writeFileSync3, readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
|
|
737
|
+
function extractSchemas() {
|
|
738
|
+
const schemaExportPath = process.env.XNET_SCHEMA_EXPORT || "./schemas-export.json";
|
|
739
|
+
if (existsSync3(schemaExportPath)) {
|
|
740
|
+
const content = readFileSync3(schemaExportPath, "utf-8");
|
|
741
|
+
const data = JSON.parse(content);
|
|
742
|
+
return data.schemas || data;
|
|
743
|
+
}
|
|
744
|
+
console.warn("No schemas found. Set XNET_SCHEMA_EXPORT or create schemas-export.json");
|
|
745
|
+
return [];
|
|
746
|
+
}
|
|
747
|
+
function formatChange(change) {
|
|
748
|
+
const icon = change.risk === "breaking" ? "!" : change.risk === "caution" ? "*" : "+";
|
|
749
|
+
return ` ${icon} ${change.description}`;
|
|
750
|
+
}
|
|
751
|
+
function registerSchemaCommand(program2) {
|
|
752
|
+
const schema = program2.command("schema").description("Schema extraction and diffing utilities");
|
|
753
|
+
schema.command("extract").description("Extract schemas from the codebase to a JSON file").option("-o, --output <file>", "Output file path", "schemas.json").option("--pretty", "Pretty-print the JSON output", false).action(async (options) => {
|
|
754
|
+
console.log("Extracting schemas...");
|
|
755
|
+
const schemas = extractSchemas();
|
|
756
|
+
const output = {
|
|
757
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
758
|
+
schemas
|
|
759
|
+
};
|
|
760
|
+
const json = options.pretty ? JSON.stringify(output, null, 2) : JSON.stringify(output);
|
|
761
|
+
writeFileSync3(options.output, json, "utf-8");
|
|
762
|
+
console.log(`Extracted ${schemas.length} schemas to ${options.output}`);
|
|
763
|
+
});
|
|
764
|
+
schema.command("diff").description("Compare two schema files and report changes").argument("<old-file>", "Path to the old schemas JSON file").argument("<new-file>", "Path to the new schemas JSON file").option("-o, --output <file>", "Output diff to JSON file").option("--fail-on-breaking", "Exit with code 1 if breaking changes found", false).option("--quiet", "Only output JSON, no console messages", false).action(
|
|
765
|
+
async (oldFile, newFile, options) => {
|
|
766
|
+
if (!existsSync3(oldFile)) {
|
|
767
|
+
console.error(`Error: Old file not found: ${oldFile}`);
|
|
768
|
+
process.exit(1);
|
|
769
|
+
}
|
|
770
|
+
if (!existsSync3(newFile)) {
|
|
771
|
+
console.error(`Error: New file not found: ${newFile}`);
|
|
772
|
+
process.exit(1);
|
|
773
|
+
}
|
|
774
|
+
const oldData = JSON.parse(readFileSync3(oldFile, "utf-8"));
|
|
775
|
+
const newData = JSON.parse(readFileSync3(newFile, "utf-8"));
|
|
776
|
+
const oldSchemas = new Map(oldData.schemas.map((s) => [s["@id"], s]));
|
|
777
|
+
const newSchemas = new Map(newData.schemas.map((s) => [s["@id"], s]));
|
|
778
|
+
const diffs = [];
|
|
779
|
+
let totalChanges = 0;
|
|
780
|
+
let breakingChanges = 0;
|
|
781
|
+
let cautionChanges = 0;
|
|
782
|
+
let safeChanges = 0;
|
|
783
|
+
for (const [iri, oldSchema] of oldSchemas) {
|
|
784
|
+
const newSchema = newSchemas.get(iri);
|
|
785
|
+
if (newSchema) {
|
|
786
|
+
const result = diffSchemas(oldSchema, newSchema);
|
|
787
|
+
if (result.changes.length > 0) {
|
|
788
|
+
diffs.push({ schemaName: oldSchema.name, result });
|
|
789
|
+
totalChanges += result.changes.length;
|
|
790
|
+
breakingChanges += result.summary.breaking;
|
|
791
|
+
cautionChanges += result.summary.caution;
|
|
792
|
+
safeChanges += result.summary.safe;
|
|
793
|
+
}
|
|
794
|
+
} else {
|
|
795
|
+
diffs.push({
|
|
796
|
+
schemaName: oldSchema.name,
|
|
797
|
+
result: {
|
|
798
|
+
fromVersion: oldSchema.version,
|
|
799
|
+
toVersion: "removed",
|
|
800
|
+
changes: [
|
|
801
|
+
{
|
|
802
|
+
type: "remove",
|
|
803
|
+
property: oldSchema["@id"],
|
|
804
|
+
risk: "breaking",
|
|
805
|
+
description: `Schema "${oldSchema.name}" was removed`
|
|
806
|
+
}
|
|
807
|
+
],
|
|
808
|
+
overallRisk: "breaking",
|
|
809
|
+
autoMigratable: false,
|
|
810
|
+
summary: { safe: 0, caution: 0, breaking: 1 }
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
totalChanges += 1;
|
|
814
|
+
breakingChanges += 1;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
for (const [iri, newSchema] of newSchemas) {
|
|
818
|
+
if (!oldSchemas.has(iri)) {
|
|
819
|
+
diffs.push({
|
|
820
|
+
schemaName: newSchema.name,
|
|
821
|
+
result: {
|
|
822
|
+
fromVersion: "new",
|
|
823
|
+
toVersion: newSchema.version,
|
|
824
|
+
changes: [
|
|
825
|
+
{
|
|
826
|
+
type: "add",
|
|
827
|
+
property: newSchema["@id"],
|
|
828
|
+
risk: "safe",
|
|
829
|
+
description: `New schema "${newSchema.name}" added`
|
|
830
|
+
}
|
|
831
|
+
],
|
|
832
|
+
overallRisk: "safe",
|
|
833
|
+
autoMigratable: true,
|
|
834
|
+
summary: { safe: 1, caution: 0, breaking: 0 }
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
totalChanges += 1;
|
|
838
|
+
safeChanges += 1;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
const output = {
|
|
842
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
843
|
+
diffs,
|
|
844
|
+
summary: {
|
|
845
|
+
schemasChanged: diffs.length,
|
|
846
|
+
totalChanges,
|
|
847
|
+
breakingChanges,
|
|
848
|
+
cautionChanges,
|
|
849
|
+
safeChanges
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
if (options.output) {
|
|
853
|
+
writeFileSync3(options.output, JSON.stringify(output, null, 2), "utf-8");
|
|
854
|
+
}
|
|
855
|
+
if (!options.quiet) {
|
|
856
|
+
if (diffs.length === 0) {
|
|
857
|
+
console.log("No schema changes detected.");
|
|
858
|
+
} else {
|
|
859
|
+
console.log(`
|
|
860
|
+
Schema changes detected: ${diffs.length} schema(s) changed
|
|
861
|
+
`);
|
|
862
|
+
for (const { schemaName, result } of diffs) {
|
|
863
|
+
const riskIcon = result.overallRisk === "breaking" ? "!" : result.overallRisk === "caution" ? "*" : "+";
|
|
864
|
+
console.log(
|
|
865
|
+
`${riskIcon} ${schemaName} (${result.fromVersion} -> ${result.toVersion})`
|
|
866
|
+
);
|
|
867
|
+
for (const change of result.changes) {
|
|
868
|
+
console.log(formatChange(change));
|
|
869
|
+
}
|
|
870
|
+
console.log();
|
|
871
|
+
}
|
|
872
|
+
console.log("Summary:");
|
|
873
|
+
console.log(` Breaking: ${breakingChanges}`);
|
|
874
|
+
console.log(` Caution: ${cautionChanges}`);
|
|
875
|
+
console.log(` Safe: ${safeChanges}`);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
if (options.failOnBreaking && breakingChanges > 0) {
|
|
879
|
+
process.exit(1);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// src/cli.ts
|
|
886
|
+
program.name("xnet").description("xNet CLI - Schema migrations, diagnostics, and development tools").version("0.0.1");
|
|
887
|
+
registerMigrateCommand(program);
|
|
888
|
+
registerSchemaCommand(program);
|
|
889
|
+
registerDoctorCommand(program);
|
|
890
|
+
program.parse();
|