json-term 1.0.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/LICENSE +21 -0
- package/README.md +176 -0
- package/dist/cli.d.mts +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +718 -0
- package/dist/cli.js.map +1 -0
- package/dist/cli.mjs +695 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.d.mts +111 -0
- package/dist/index.d.ts +111 -0
- package/dist/index.js +638 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +592 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +69 -0
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import pc4 from "picocolors";
|
|
7
|
+
|
|
8
|
+
// src/formatter.ts
|
|
9
|
+
import pc from "picocolors";
|
|
10
|
+
function getValueType(value) {
|
|
11
|
+
if (value === null || value === void 0) return "null";
|
|
12
|
+
if (Array.isArray(value)) return "array";
|
|
13
|
+
const t = typeof value;
|
|
14
|
+
if (t === "object") return "object";
|
|
15
|
+
if (t === "string") return "string";
|
|
16
|
+
if (t === "number") return "number";
|
|
17
|
+
if (t === "boolean") return "boolean";
|
|
18
|
+
return "string";
|
|
19
|
+
}
|
|
20
|
+
function formatValue(value, truncateLen = 80) {
|
|
21
|
+
const type = getValueType(value);
|
|
22
|
+
switch (type) {
|
|
23
|
+
case "string": {
|
|
24
|
+
const str = JSON.stringify(value);
|
|
25
|
+
const truncated = str.length > truncateLen ? str.slice(0, truncateLen - 3) + '..."' : str;
|
|
26
|
+
return pc.green(truncated);
|
|
27
|
+
}
|
|
28
|
+
case "number":
|
|
29
|
+
return pc.yellow(String(value));
|
|
30
|
+
case "boolean":
|
|
31
|
+
return pc.magenta(String(value));
|
|
32
|
+
case "null":
|
|
33
|
+
return pc.dim("null");
|
|
34
|
+
case "array":
|
|
35
|
+
return pc.dim(`[${value.length} items]`);
|
|
36
|
+
case "object":
|
|
37
|
+
return pc.dim(`{${Object.keys(value).length} keys}`);
|
|
38
|
+
default:
|
|
39
|
+
return String(value);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function formatKey(key) {
|
|
43
|
+
return pc.bold(pc.cyan(key));
|
|
44
|
+
}
|
|
45
|
+
function buildTree(data, key, path = "$", depth = 0, maxInitialDepth = 2, parentId) {
|
|
46
|
+
const type = getValueType(data);
|
|
47
|
+
const id = `${path}-${key || "root"}-${depth}`;
|
|
48
|
+
const collapsed = depth >= maxInitialDepth;
|
|
49
|
+
const node = {
|
|
50
|
+
id,
|
|
51
|
+
key,
|
|
52
|
+
value: data,
|
|
53
|
+
type,
|
|
54
|
+
depth,
|
|
55
|
+
path,
|
|
56
|
+
collapsed,
|
|
57
|
+
parentId
|
|
58
|
+
};
|
|
59
|
+
if (type === "object" && data !== null) {
|
|
60
|
+
const keys = Object.keys(data);
|
|
61
|
+
node.itemCount = keys.length;
|
|
62
|
+
node.children = keys.map(
|
|
63
|
+
(k) => buildTree(
|
|
64
|
+
data[k],
|
|
65
|
+
k,
|
|
66
|
+
path === "$" ? k : `${path}.${k}`,
|
|
67
|
+
depth + 1,
|
|
68
|
+
maxInitialDepth,
|
|
69
|
+
id
|
|
70
|
+
)
|
|
71
|
+
);
|
|
72
|
+
} else if (type === "array") {
|
|
73
|
+
node.itemCount = data.length;
|
|
74
|
+
node.children = data.map(
|
|
75
|
+
(item, idx) => buildTree(
|
|
76
|
+
item,
|
|
77
|
+
`[${idx}]`,
|
|
78
|
+
`${path}[${idx}]`,
|
|
79
|
+
depth + 1,
|
|
80
|
+
maxInitialDepth,
|
|
81
|
+
id
|
|
82
|
+
)
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return node;
|
|
86
|
+
}
|
|
87
|
+
function prettyPrintTree(node, indent = "", isLast = true) {
|
|
88
|
+
const isRoot = node.depth === 0;
|
|
89
|
+
const marker = node.type === "object" || node.type === "array" ? node.collapsed ? pc.cyan("\u25B6 ") : pc.cyan("\u25BC ") : pc.dim("\u2022 ");
|
|
90
|
+
let line = "";
|
|
91
|
+
if (!isRoot) {
|
|
92
|
+
const branch = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
93
|
+
line += pc.dim(indent + branch) + marker;
|
|
94
|
+
if (node.key) {
|
|
95
|
+
line += formatKey(node.key) + pc.dim(": ");
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
line += marker + (node.key ? formatKey(node.key) + pc.dim(": ") : pc.bold(pc.white("root: ")));
|
|
99
|
+
}
|
|
100
|
+
if (node.type === "object") {
|
|
101
|
+
line += pc.dim(`{ ${node.itemCount} keys }`);
|
|
102
|
+
} else if (node.type === "array") {
|
|
103
|
+
line += pc.dim(`[ ${node.itemCount} items ]`);
|
|
104
|
+
} else {
|
|
105
|
+
line += formatValue(node.value);
|
|
106
|
+
}
|
|
107
|
+
const lines = [line];
|
|
108
|
+
if (!node.collapsed && node.children && node.children.length > 0) {
|
|
109
|
+
const nextIndent = isRoot ? "" : indent + (isLast ? " " : "\u2502 ");
|
|
110
|
+
node.children.forEach((child, index) => {
|
|
111
|
+
const childIsLast = index === node.children.length - 1;
|
|
112
|
+
lines.push(prettyPrintTree(child, nextIndent, childIsLast));
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return lines.join("\n");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/differ.ts
|
|
119
|
+
import pc2 from "picocolors";
|
|
120
|
+
function diffJSON(a, b, key, path = "$", depth = 0, maxInitialDepth = 3) {
|
|
121
|
+
const typeA = getValueType(a);
|
|
122
|
+
const typeB = getValueType(b);
|
|
123
|
+
const id = `${path}-${key || "diff"}-${depth}`;
|
|
124
|
+
const collapsed = depth >= maxInitialDepth;
|
|
125
|
+
if (a === void 0 && b !== void 0) {
|
|
126
|
+
const node = {
|
|
127
|
+
id,
|
|
128
|
+
key,
|
|
129
|
+
path,
|
|
130
|
+
depth,
|
|
131
|
+
status: "added",
|
|
132
|
+
newValue: b,
|
|
133
|
+
type: typeB,
|
|
134
|
+
collapsed: false
|
|
135
|
+
};
|
|
136
|
+
if (typeB === "object" && b !== null) {
|
|
137
|
+
node.itemCount = Object.keys(b).length;
|
|
138
|
+
node.children = Object.keys(b).map(
|
|
139
|
+
(k) => diffJSON(void 0, b[k], k, path === "$" ? k : `${path}.${k}`, depth + 1, maxInitialDepth)
|
|
140
|
+
);
|
|
141
|
+
} else if (typeB === "array") {
|
|
142
|
+
node.itemCount = b.length;
|
|
143
|
+
node.children = b.map(
|
|
144
|
+
(item, idx) => diffJSON(void 0, item, `[${idx}]`, `${path}[${idx}]`, depth + 1, maxInitialDepth)
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
return node;
|
|
148
|
+
}
|
|
149
|
+
if (a !== void 0 && b === void 0) {
|
|
150
|
+
const node = {
|
|
151
|
+
id,
|
|
152
|
+
key,
|
|
153
|
+
path,
|
|
154
|
+
depth,
|
|
155
|
+
status: "removed",
|
|
156
|
+
oldValue: a,
|
|
157
|
+
type: typeA,
|
|
158
|
+
collapsed: false
|
|
159
|
+
};
|
|
160
|
+
if (typeA === "object" && a !== null) {
|
|
161
|
+
node.itemCount = Object.keys(a).length;
|
|
162
|
+
node.children = Object.keys(a).map(
|
|
163
|
+
(k) => diffJSON(a[k], void 0, k, path === "$" ? k : `${path}.${k}`, depth + 1, maxInitialDepth)
|
|
164
|
+
);
|
|
165
|
+
} else if (typeA === "array") {
|
|
166
|
+
node.itemCount = a.length;
|
|
167
|
+
node.children = a.map(
|
|
168
|
+
(item, idx) => diffJSON(item, void 0, `[${idx}]`, `${path}[${idx}]`, depth + 1, maxInitialDepth)
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
return node;
|
|
172
|
+
}
|
|
173
|
+
if (typeA !== typeB) {
|
|
174
|
+
return {
|
|
175
|
+
id,
|
|
176
|
+
key,
|
|
177
|
+
path,
|
|
178
|
+
depth,
|
|
179
|
+
status: "modified",
|
|
180
|
+
oldValue: a,
|
|
181
|
+
newValue: b,
|
|
182
|
+
type: typeB,
|
|
183
|
+
collapsed: false
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (typeA === "object" && a !== null && b !== null) {
|
|
187
|
+
const allKeys = Array.from(/* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]));
|
|
188
|
+
const children = [];
|
|
189
|
+
let hasModifications = false;
|
|
190
|
+
for (const k of allKeys) {
|
|
191
|
+
const child = diffJSON(
|
|
192
|
+
a[k],
|
|
193
|
+
b[k],
|
|
194
|
+
k,
|
|
195
|
+
path === "$" ? k : `${path}.${k}`,
|
|
196
|
+
depth + 1,
|
|
197
|
+
maxInitialDepth
|
|
198
|
+
);
|
|
199
|
+
if (child.status !== "unchanged") hasModifications = true;
|
|
200
|
+
children.push(child);
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
id,
|
|
204
|
+
key,
|
|
205
|
+
path,
|
|
206
|
+
depth,
|
|
207
|
+
status: hasModifications ? "modified" : "unchanged",
|
|
208
|
+
oldValue: a,
|
|
209
|
+
newValue: b,
|
|
210
|
+
type: "object",
|
|
211
|
+
itemCount: allKeys.length,
|
|
212
|
+
collapsed,
|
|
213
|
+
children
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
if (typeA === "array") {
|
|
217
|
+
const maxLen = Math.max(a.length, b.length);
|
|
218
|
+
const children = [];
|
|
219
|
+
let hasModifications = false;
|
|
220
|
+
for (let i = 0; i < maxLen; i++) {
|
|
221
|
+
const child = diffJSON(
|
|
222
|
+
a[i],
|
|
223
|
+
b[i],
|
|
224
|
+
`[${i}]`,
|
|
225
|
+
`${path}[${i}]`,
|
|
226
|
+
depth + 1,
|
|
227
|
+
maxInitialDepth
|
|
228
|
+
);
|
|
229
|
+
if (child.status !== "unchanged") hasModifications = true;
|
|
230
|
+
children.push(child);
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
id,
|
|
234
|
+
key,
|
|
235
|
+
path,
|
|
236
|
+
depth,
|
|
237
|
+
status: hasModifications ? "modified" : "unchanged",
|
|
238
|
+
oldValue: a,
|
|
239
|
+
newValue: b,
|
|
240
|
+
type: "array",
|
|
241
|
+
itemCount: maxLen,
|
|
242
|
+
collapsed,
|
|
243
|
+
children
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
if (a !== b) {
|
|
247
|
+
return {
|
|
248
|
+
id,
|
|
249
|
+
key,
|
|
250
|
+
path,
|
|
251
|
+
depth,
|
|
252
|
+
status: "modified",
|
|
253
|
+
oldValue: a,
|
|
254
|
+
newValue: b,
|
|
255
|
+
type: typeB,
|
|
256
|
+
collapsed: false
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
id,
|
|
261
|
+
key,
|
|
262
|
+
path,
|
|
263
|
+
depth,
|
|
264
|
+
status: "unchanged",
|
|
265
|
+
oldValue: a,
|
|
266
|
+
newValue: b,
|
|
267
|
+
type: typeA,
|
|
268
|
+
collapsed
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function calculateDiffStats(node) {
|
|
272
|
+
const stats = { added: 0, removed: 0, modified: 0, unchanged: 0 };
|
|
273
|
+
function traverse(n) {
|
|
274
|
+
if (n.children && n.children.length > 0) {
|
|
275
|
+
n.children.forEach(traverse);
|
|
276
|
+
} else {
|
|
277
|
+
stats[n.status]++;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
traverse(node);
|
|
281
|
+
return stats;
|
|
282
|
+
}
|
|
283
|
+
function prettyPrintDiff(node, options = {}, indent = "", isLast = true) {
|
|
284
|
+
const showUnchanged = options.showUnchanged ?? true;
|
|
285
|
+
if (!showUnchanged && node.status === "unchanged" && (!node.children || node.children.length === 0)) {
|
|
286
|
+
return "";
|
|
287
|
+
}
|
|
288
|
+
const isRoot = node.depth === 0;
|
|
289
|
+
let prefix = " ";
|
|
290
|
+
let statusColor = pc2.dim;
|
|
291
|
+
if (node.status === "added") {
|
|
292
|
+
prefix = pc2.green("+ ");
|
|
293
|
+
statusColor = pc2.green;
|
|
294
|
+
} else if (node.status === "removed") {
|
|
295
|
+
prefix = pc2.red("- ");
|
|
296
|
+
statusColor = pc2.red;
|
|
297
|
+
} else if (node.status === "modified") {
|
|
298
|
+
prefix = pc2.yellow("~ ");
|
|
299
|
+
statusColor = pc2.yellow;
|
|
300
|
+
}
|
|
301
|
+
let line = prefix;
|
|
302
|
+
if (!isRoot) {
|
|
303
|
+
const branch = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
304
|
+
line += pc2.dim(indent + branch);
|
|
305
|
+
if (node.key) {
|
|
306
|
+
line += formatKey(node.key) + pc2.dim(": ");
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
line += node.key ? formatKey(node.key) + pc2.dim(": ") : pc2.bold("diff: ");
|
|
310
|
+
}
|
|
311
|
+
if (node.status === "added") {
|
|
312
|
+
line += statusColor(formatValue(node.newValue));
|
|
313
|
+
} else if (node.status === "removed") {
|
|
314
|
+
line += statusColor(formatValue(node.oldValue));
|
|
315
|
+
} else if (node.status === "modified") {
|
|
316
|
+
if (node.type === "object" || node.type === "array") {
|
|
317
|
+
line += statusColor(`{ ${node.itemCount} items modified }`);
|
|
318
|
+
} else {
|
|
319
|
+
line += `${pc2.red(formatValue(node.oldValue))} ${pc2.dim("->")} ${pc2.green(formatValue(node.newValue))}`;
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
line += pc2.dim(formatValue(node.newValue));
|
|
323
|
+
}
|
|
324
|
+
const lines = [line];
|
|
325
|
+
if (!node.collapsed && node.children && node.children.length > 0) {
|
|
326
|
+
const nextIndent = isRoot ? "" : indent + (isLast ? " " : "\u2502 ");
|
|
327
|
+
node.children.forEach((child, index) => {
|
|
328
|
+
const childIsLast = index === node.children.length - 1;
|
|
329
|
+
const childStr = prettyPrintDiff(child, options, nextIndent, childIsLast);
|
|
330
|
+
if (childStr) lines.push(childStr);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
return lines.join("\n");
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// src/interactive.ts
|
|
337
|
+
import readline from "readline";
|
|
338
|
+
import pc3 from "picocolors";
|
|
339
|
+
|
|
340
|
+
// src/utils/clipboard.ts
|
|
341
|
+
import { exec } from "child_process";
|
|
342
|
+
import os from "os";
|
|
343
|
+
async function copyToClipboard(text) {
|
|
344
|
+
const platform = os.platform();
|
|
345
|
+
return new Promise((resolve) => {
|
|
346
|
+
let proc;
|
|
347
|
+
if (platform === "darwin") {
|
|
348
|
+
proc = exec("pbcopy", (err) => resolve(!err));
|
|
349
|
+
} else if (platform === "win32") {
|
|
350
|
+
proc = exec("clip", (err) => resolve(!err));
|
|
351
|
+
} else {
|
|
352
|
+
proc = exec("wl-copy 2>/dev/null || xclip -selection clipboard 2>/dev/null || xsel --clipboard --input", (err) => {
|
|
353
|
+
resolve(!err);
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
if (proc.stdin) {
|
|
357
|
+
proc.stdin.write(text);
|
|
358
|
+
proc.stdin.end();
|
|
359
|
+
} else {
|
|
360
|
+
resolve(false);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// src/interactive.ts
|
|
366
|
+
var InteractiveViewer = class {
|
|
367
|
+
root;
|
|
368
|
+
flatList = [];
|
|
369
|
+
selectedIndex = 0;
|
|
370
|
+
searchQuery = "";
|
|
371
|
+
isSearching = false;
|
|
372
|
+
searchInput = "";
|
|
373
|
+
notification = "";
|
|
374
|
+
notificationTimer = null;
|
|
375
|
+
isDiff;
|
|
376
|
+
constructor(root, isDiff = false) {
|
|
377
|
+
this.root = root;
|
|
378
|
+
this.isDiff = isDiff;
|
|
379
|
+
this.rebuildFlatList();
|
|
380
|
+
}
|
|
381
|
+
isDiffNode(node) {
|
|
382
|
+
return this.isDiff && "status" in node;
|
|
383
|
+
}
|
|
384
|
+
rebuildFlatList() {
|
|
385
|
+
const list = [];
|
|
386
|
+
const traverse = (node, currentDepth) => {
|
|
387
|
+
if (this.searchQuery) {
|
|
388
|
+
const query = this.searchQuery.toLowerCase();
|
|
389
|
+
const keyMatch = node.key && node.key.toLowerCase().includes(query);
|
|
390
|
+
const pathMatch = node.path && node.path.toLowerCase().includes(query);
|
|
391
|
+
const valMatch = "value" in node ? JSON.stringify(node.value).toLowerCase().includes(query) : "newValue" in node ? JSON.stringify(node.newValue).toLowerCase().includes(query) : false;
|
|
392
|
+
if (!keyMatch && !pathMatch && !valMatch && (!node.children || node.children.length === 0)) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
list.push({ node, depth: currentDepth, isDiff: this.isDiff });
|
|
397
|
+
if (!node.collapsed && node.children && node.children.length > 0) {
|
|
398
|
+
for (const child of node.children) {
|
|
399
|
+
traverse(child, currentDepth + 1);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
traverse(this.root, 0);
|
|
404
|
+
this.flatList = list;
|
|
405
|
+
if (this.selectedIndex >= this.flatList.length) {
|
|
406
|
+
this.selectedIndex = Math.max(0, this.flatList.length - 1);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
setNotification(msg) {
|
|
410
|
+
this.notification = msg;
|
|
411
|
+
if (this.notificationTimer) clearTimeout(this.notificationTimer);
|
|
412
|
+
this.notificationTimer = setTimeout(() => {
|
|
413
|
+
this.notification = "";
|
|
414
|
+
this.render();
|
|
415
|
+
}, 2500);
|
|
416
|
+
}
|
|
417
|
+
render() {
|
|
418
|
+
const rows = process.stdout.rows || 24;
|
|
419
|
+
const maxVisibleRows = Math.max(5, rows - 5);
|
|
420
|
+
let startIdx = 0;
|
|
421
|
+
if (this.selectedIndex >= maxVisibleRows) {
|
|
422
|
+
startIdx = this.selectedIndex - maxVisibleRows + 1;
|
|
423
|
+
}
|
|
424
|
+
const endIdx = Math.min(this.flatList.length, startIdx + maxVisibleRows);
|
|
425
|
+
process.stdout.write("\x1B[2J\x1B[3J\x1B[H\x1B[?25l");
|
|
426
|
+
const title = this.isDiff ? " JSON Structural Diff Explorer " : " Interactive JSON Tree Explorer ";
|
|
427
|
+
console.log(pc3.bgCyan(pc3.black(pc3.bold(` ${title} `)) + pc3.dim(` [Total items: ${this.flatList.length}]`)));
|
|
428
|
+
for (let i = startIdx; i < endIdx; i++) {
|
|
429
|
+
const { node, depth } = this.flatList[i];
|
|
430
|
+
const isSelected = i === this.selectedIndex;
|
|
431
|
+
const indent = " ".repeat(depth);
|
|
432
|
+
let marker = "\u2022 ";
|
|
433
|
+
if (node.children && node.children.length > 0) {
|
|
434
|
+
marker = node.collapsed ? "\u25B6 " : "\u25BC ";
|
|
435
|
+
}
|
|
436
|
+
let lineContent = "";
|
|
437
|
+
if (this.isDiffNode(node)) {
|
|
438
|
+
let diffPrefix = " ";
|
|
439
|
+
if (node.status === "added") diffPrefix = pc3.green("+ ");
|
|
440
|
+
else if (node.status === "removed") diffPrefix = pc3.red("- ");
|
|
441
|
+
else if (node.status === "modified") diffPrefix = pc3.yellow("~ ");
|
|
442
|
+
lineContent = diffPrefix + indent + marker;
|
|
443
|
+
if (node.key) lineContent += formatKey(node.key) + pc3.dim(": ");
|
|
444
|
+
if (node.status === "added") {
|
|
445
|
+
lineContent += pc3.green(formatValue(node.newValue, 50));
|
|
446
|
+
} else if (node.status === "removed") {
|
|
447
|
+
lineContent += pc3.red(formatValue(node.oldValue, 50));
|
|
448
|
+
} else if (node.status === "modified") {
|
|
449
|
+
if (node.type === "object" || node.type === "array") {
|
|
450
|
+
lineContent += pc3.yellow(`{ ${node.itemCount} modified }`);
|
|
451
|
+
} else {
|
|
452
|
+
lineContent += `${pc3.red(formatValue(node.oldValue, 25))} -> ${pc3.green(formatValue(node.newValue, 25))}`;
|
|
453
|
+
}
|
|
454
|
+
} else {
|
|
455
|
+
lineContent += pc3.dim(formatValue(node.newValue, 50));
|
|
456
|
+
}
|
|
457
|
+
} else {
|
|
458
|
+
const treeNode = node;
|
|
459
|
+
lineContent = indent + marker;
|
|
460
|
+
if (treeNode.key) lineContent += formatKey(treeNode.key) + pc3.dim(": ");
|
|
461
|
+
if (treeNode.type === "object") {
|
|
462
|
+
lineContent += pc3.dim(`{ ${treeNode.itemCount} keys }`);
|
|
463
|
+
} else if (treeNode.type === "array") {
|
|
464
|
+
lineContent += pc3.dim(`[ ${treeNode.itemCount} items ]`);
|
|
465
|
+
} else {
|
|
466
|
+
lineContent += formatValue(treeNode.value, 60);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (isSelected) {
|
|
470
|
+
console.log(pc3.bgBlue(pc3.white(pc3.bold(`> ${lineContent}`))));
|
|
471
|
+
} else {
|
|
472
|
+
console.log(` ${lineContent}`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
const selectedItem = this.flatList[this.selectedIndex];
|
|
476
|
+
const currentPath = selectedItem ? selectedItem.node.path : "$";
|
|
477
|
+
console.log("\n" + pc3.dim("\u2500".repeat(Math.min(process.stdout.columns || 80, 80))));
|
|
478
|
+
if (this.notification) {
|
|
479
|
+
console.log(pc3.green(pc3.bold(` ${this.notification}`)));
|
|
480
|
+
} else if (this.isSearching) {
|
|
481
|
+
console.log(pc3.yellow(pc3.bold(` Search: /${this.searchInput}\u2588`)));
|
|
482
|
+
} else {
|
|
483
|
+
console.log(pc3.bold(pc3.cyan(` Path: ${currentPath}`)));
|
|
484
|
+
console.log(
|
|
485
|
+
pc3.dim(" [\u2191/\u2193] Move [Enter/Space] Toggle [e] Expand All [c] Collapse All [p] Copy Path [y] Copy Val [/] Search [q] Exit")
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
expandAll(node) {
|
|
490
|
+
node.collapsed = false;
|
|
491
|
+
if (node.children) node.children.forEach((c) => this.expandAll(c));
|
|
492
|
+
}
|
|
493
|
+
collapseAll(node) {
|
|
494
|
+
if (node.depth > 0) node.collapsed = true;
|
|
495
|
+
if (node.children) node.children.forEach((c) => this.collapseAll(c));
|
|
496
|
+
}
|
|
497
|
+
async start() {
|
|
498
|
+
if (!process.stdin.isTTY) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
readline.emitKeypressEvents(process.stdin);
|
|
502
|
+
process.stdin.setRawMode(true);
|
|
503
|
+
process.stdin.resume();
|
|
504
|
+
this.render();
|
|
505
|
+
return new Promise((resolve) => {
|
|
506
|
+
const onKeypress = async (str, key) => {
|
|
507
|
+
if (this.isSearching) {
|
|
508
|
+
if (key.name === "return") {
|
|
509
|
+
this.searchQuery = this.searchInput;
|
|
510
|
+
this.isSearching = false;
|
|
511
|
+
this.rebuildFlatList();
|
|
512
|
+
this.render();
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (key.name === "escape") {
|
|
516
|
+
this.isSearching = false;
|
|
517
|
+
this.searchInput = "";
|
|
518
|
+
this.render();
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
if (key.name === "backspace") {
|
|
522
|
+
this.searchInput = this.searchInput.slice(0, -1);
|
|
523
|
+
this.render();
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
if (str && str.length === 1) {
|
|
527
|
+
this.searchInput += str;
|
|
528
|
+
this.render();
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
if (key.name === "up" || key.name === "k") {
|
|
533
|
+
if (this.selectedIndex > 0) {
|
|
534
|
+
this.selectedIndex--;
|
|
535
|
+
this.render();
|
|
536
|
+
}
|
|
537
|
+
} else if (key.name === "down" || key.name === "j") {
|
|
538
|
+
if (this.selectedIndex < this.flatList.length - 1) {
|
|
539
|
+
this.selectedIndex++;
|
|
540
|
+
this.render();
|
|
541
|
+
}
|
|
542
|
+
} else if (key.name === "return" || key.name === "space" || key.name === "right" || key.name === "left") {
|
|
543
|
+
const item = this.flatList[this.selectedIndex];
|
|
544
|
+
if (item && item.node.children && item.node.children.length > 0) {
|
|
545
|
+
item.node.collapsed = !item.node.collapsed;
|
|
546
|
+
this.rebuildFlatList();
|
|
547
|
+
this.render();
|
|
548
|
+
}
|
|
549
|
+
} else if (str === "e" || str === "E") {
|
|
550
|
+
this.expandAll(this.root);
|
|
551
|
+
this.rebuildFlatList();
|
|
552
|
+
this.render();
|
|
553
|
+
} else if (str === "c" || str === "C") {
|
|
554
|
+
this.collapseAll(this.root);
|
|
555
|
+
this.rebuildFlatList();
|
|
556
|
+
this.render();
|
|
557
|
+
} else if (str === "p" || str === "P") {
|
|
558
|
+
const item = this.flatList[this.selectedIndex];
|
|
559
|
+
if (item) {
|
|
560
|
+
await copyToClipboard(item.node.path);
|
|
561
|
+
this.setNotification(`\u2714 Copied JSONPath: ${item.node.path}`);
|
|
562
|
+
}
|
|
563
|
+
} else if (str === "y" || str === "Y") {
|
|
564
|
+
const item = this.flatList[this.selectedIndex];
|
|
565
|
+
if (item) {
|
|
566
|
+
const val = "value" in item.node ? item.node.value : item.node.newValue;
|
|
567
|
+
const textToCopy = typeof val === "object" ? JSON.stringify(val, null, 2) : String(val);
|
|
568
|
+
await copyToClipboard(textToCopy);
|
|
569
|
+
this.setNotification(`\u2714 Copied value to clipboard!`);
|
|
570
|
+
}
|
|
571
|
+
} else if (str === "/") {
|
|
572
|
+
this.isSearching = true;
|
|
573
|
+
this.searchInput = "";
|
|
574
|
+
this.render();
|
|
575
|
+
} else if (key.ctrl && key.name === "c" || key.name === "escape" || str === "q" || str === "Q") {
|
|
576
|
+
process.stdin.removeListener("keypress", onKeypress);
|
|
577
|
+
process.stdin.setRawMode(false);
|
|
578
|
+
process.stdin.pause();
|
|
579
|
+
process.stdout.write("\x1B[?25h\n");
|
|
580
|
+
resolve();
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
process.stdin.on("keypress", onKeypress);
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
// src/cli.ts
|
|
589
|
+
async function readStdin() {
|
|
590
|
+
return new Promise((resolve) => {
|
|
591
|
+
let data = "";
|
|
592
|
+
if (process.stdin.isTTY) {
|
|
593
|
+
resolve("");
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
process.stdin.setEncoding("utf-8");
|
|
597
|
+
process.stdin.on("data", (chunk) => {
|
|
598
|
+
data += chunk;
|
|
599
|
+
});
|
|
600
|
+
process.stdin.on("end", () => {
|
|
601
|
+
resolve(data.trim());
|
|
602
|
+
});
|
|
603
|
+
setTimeout(() => {
|
|
604
|
+
if (!data) resolve("");
|
|
605
|
+
}, 100);
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
function parseJSON(raw, label = "Input") {
|
|
609
|
+
try {
|
|
610
|
+
return JSON.parse(raw);
|
|
611
|
+
} catch (err) {
|
|
612
|
+
throw new Error(`Invalid JSON in ${label}: ${err.message}`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
async function main() {
|
|
616
|
+
const program = new Command();
|
|
617
|
+
program.name("json-term").description("High-fidelity interactive terminal JSON viewer, collapsible tree explorer, type syntax highlighter, and structural diff tool.").version("1.0.0", "-v, --version", "Output the current version").argument("[file]", "JSON file to view or compare").option("-p, --print", "Print formatted non-interactive tree to stdout", false).option("-d, --depth <number>", "Initial tree expansion depth", (val) => parseInt(val, 10), 2).option("-s, --search <query>", "Filter tree by key or value query").action(async (fileArg, options) => {
|
|
618
|
+
try {
|
|
619
|
+
let raw = "";
|
|
620
|
+
if (fileArg) {
|
|
621
|
+
if (!fs.existsSync(fileArg)) {
|
|
622
|
+
console.error(pc4.red(`Error: File not found: ${fileArg}`));
|
|
623
|
+
process.exit(1);
|
|
624
|
+
}
|
|
625
|
+
raw = await fs.promises.readFile(fileArg, "utf-8");
|
|
626
|
+
} else {
|
|
627
|
+
raw = await readStdin();
|
|
628
|
+
}
|
|
629
|
+
if (!raw) {
|
|
630
|
+
program.help();
|
|
631
|
+
process.exit(1);
|
|
632
|
+
}
|
|
633
|
+
const data = parseJSON(raw, fileArg || "stdin");
|
|
634
|
+
const tree = buildTree(data, fileArg || "root", "$", 0, options.depth);
|
|
635
|
+
if (options.print || !process.stdin.isTTY) {
|
|
636
|
+
console.log(prettyPrintTree(tree));
|
|
637
|
+
} else {
|
|
638
|
+
const viewer = new InteractiveViewer(tree, false);
|
|
639
|
+
await viewer.start();
|
|
640
|
+
}
|
|
641
|
+
} catch (err) {
|
|
642
|
+
console.error(pc4.red(`Error: ${err.message || err}`));
|
|
643
|
+
process.exit(1);
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
program.command("diff <fileA> [fileB]").description("Structural JSON diff between two files or stdin").option("-p, --print", "Print formatted non-interactive diff to stdout", false).option("-d, --depth <number>", "Initial expansion depth", (val) => parseInt(val, 10), 3).option("--no-unchanged", "Hide unchanged nodes from the diff view", false).action(async (fileA, fileB, diffOptions) => {
|
|
647
|
+
try {
|
|
648
|
+
if (!fs.existsSync(fileA)) {
|
|
649
|
+
console.error(pc4.red(`Error: File not found: ${fileA}`));
|
|
650
|
+
process.exit(1);
|
|
651
|
+
}
|
|
652
|
+
const rawA = await fs.promises.readFile(fileA, "utf-8");
|
|
653
|
+
const dataA = parseJSON(rawA, fileA);
|
|
654
|
+
let dataB;
|
|
655
|
+
if (fileB) {
|
|
656
|
+
if (!fs.existsSync(fileB)) {
|
|
657
|
+
console.error(pc4.red(`Error: File not found: ${fileB}`));
|
|
658
|
+
process.exit(1);
|
|
659
|
+
}
|
|
660
|
+
const rawB = await fs.promises.readFile(fileB, "utf-8");
|
|
661
|
+
dataB = parseJSON(rawB, fileB);
|
|
662
|
+
} else {
|
|
663
|
+
const stdinData = await readStdin();
|
|
664
|
+
if (!stdinData) {
|
|
665
|
+
console.error(pc4.red("Error: Please specify second file or pipe JSON via stdin."));
|
|
666
|
+
process.exit(1);
|
|
667
|
+
}
|
|
668
|
+
dataB = parseJSON(stdinData, "stdin");
|
|
669
|
+
}
|
|
670
|
+
const diffTree = diffJSON(dataA, dataB, `${fileA} \u2194 ${fileB || "stdin"}`, "$", 0, diffOptions.depth);
|
|
671
|
+
const stats = calculateDiffStats(diffTree);
|
|
672
|
+
if (diffOptions.print || !process.stdin.isTTY) {
|
|
673
|
+
console.log(
|
|
674
|
+
pc4.bold(
|
|
675
|
+
`Diff Stats: ${pc4.green(`+${stats.added}`)} ${pc4.red(`-${stats.removed}`)} ${pc4.yellow(`~${stats.modified}`)} ${pc4.dim(`=${stats.unchanged}`)}
|
|
676
|
+
`
|
|
677
|
+
)
|
|
678
|
+
);
|
|
679
|
+
console.log(prettyPrintDiff(diffTree, { showUnchanged: diffOptions.unchanged }));
|
|
680
|
+
} else {
|
|
681
|
+
const viewer = new InteractiveViewer(diffTree, true);
|
|
682
|
+
await viewer.start();
|
|
683
|
+
}
|
|
684
|
+
} catch (err) {
|
|
685
|
+
console.error(pc4.red(`Error: ${err.message || err}`));
|
|
686
|
+
process.exit(1);
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
await program.parseAsync(process.argv);
|
|
690
|
+
}
|
|
691
|
+
main().catch((err) => {
|
|
692
|
+
console.error(pc4.red(`Error: ${err.message || err}`));
|
|
693
|
+
process.exit(1);
|
|
694
|
+
});
|
|
695
|
+
//# sourceMappingURL=cli.mjs.map
|