gangtise-openapi-cli 0.20.0 → 0.22.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/README.md +35 -24
- package/dist/src/cli.js +67 -38
- package/dist/src/core/args.js +15 -3
- package/dist/src/core/asyncContent.js +4 -5
- package/dist/src/core/auth.js +19 -2
- package/dist/src/core/client.js +201 -38
- package/dist/src/core/download.js +64 -7
- package/dist/src/core/endpoints.js +6 -87
- package/dist/src/core/indicatorMatrix.js +11 -6
- package/dist/src/core/output.js +88 -33
- package/dist/src/core/printer.js +11 -7
- package/dist/src/core/quoteSharding.js +16 -9
- package/dist/src/core/titleCache.js +5 -3
- package/dist/src/core/transport.js +2 -0
- package/dist/src/version.js +1 -1
- package/package.json +4 -3
package/dist/src/core/output.js
CHANGED
|
@@ -8,6 +8,26 @@ export function parseOutputFormat(value) {
|
|
|
8
8
|
}
|
|
9
9
|
throw new ConfigError(`Unsupported format: ${format}`);
|
|
10
10
|
}
|
|
11
|
+
/** Cell text for terminal-facing formats (table/markdown): newlines collapsed for
|
|
12
|
+
* alignment, remaining C0/DEL control chars stripped so server data can't inject
|
|
13
|
+
* terminal escape sequences (ESC[31m etc.) into the user's terminal. */
|
|
14
|
+
function sanitizeCell(value) {
|
|
15
|
+
return value.replace(/[\r\n]+/g, " ").replace(/[\u0000-\u001f\u007f]/g, "");
|
|
16
|
+
}
|
|
17
|
+
/** Terminal display width: CJK/fullwidth chars occupy 2 columns — padEnd counts
|
|
18
|
+
* UTF-16 code units and misaligns every table containing Chinese text. */
|
|
19
|
+
function displayWidth(value) {
|
|
20
|
+
let width = 0;
|
|
21
|
+
for (const ch of value) {
|
|
22
|
+
const cp = ch.codePointAt(0);
|
|
23
|
+
const wide = (cp >= 0x1100 && cp <= 0x115f) || (cp >= 0x2e80 && cp <= 0xa4cf)
|
|
24
|
+
|| (cp >= 0xac00 && cp <= 0xd7a3) || (cp >= 0xf900 && cp <= 0xfaff)
|
|
25
|
+
|| (cp >= 0xfe30 && cp <= 0xfe4f) || (cp >= 0xff00 && cp <= 0xff60)
|
|
26
|
+
|| (cp >= 0xffe0 && cp <= 0xffe6) || (cp >= 0x20000 && cp <= 0x3fffd);
|
|
27
|
+
width += wide ? 2 : 1;
|
|
28
|
+
}
|
|
29
|
+
return width;
|
|
30
|
+
}
|
|
11
31
|
function formatScalar(value) {
|
|
12
32
|
if (value === null || value === undefined) {
|
|
13
33
|
return "";
|
|
@@ -17,21 +37,24 @@ function formatScalar(value) {
|
|
|
17
37
|
}
|
|
18
38
|
return String(value);
|
|
19
39
|
}
|
|
40
|
+
/** Rows for a list: object rows as-is with stray null/scalar rows dropped (one bad
|
|
41
|
+
* row must not degrade the whole table to index/value pairs — and the streaming CSV
|
|
42
|
+
* path already skips them, so both paths agree). A list with NO object rows at all
|
|
43
|
+
* (e.g. plain string codes) still renders as index/value pairs. */
|
|
44
|
+
function rowsFromList(list) {
|
|
45
|
+
const objectRows = list.filter((item) => Boolean(item && typeof item === "object" && !Array.isArray(item)));
|
|
46
|
+
if (objectRows.length > 0)
|
|
47
|
+
return objectRows;
|
|
48
|
+
return list.map((item, index) => ({ index, value: item }));
|
|
49
|
+
}
|
|
20
50
|
function toRows(value) {
|
|
21
51
|
if (Array.isArray(value)) {
|
|
22
|
-
|
|
23
|
-
return value;
|
|
24
|
-
}
|
|
25
|
-
return value.map((item, index) => ({ index, value: item }));
|
|
52
|
+
return rowsFromList(value);
|
|
26
53
|
}
|
|
27
54
|
if (value && typeof value === "object") {
|
|
28
55
|
const record = value;
|
|
29
56
|
if (Array.isArray(record.list)) {
|
|
30
|
-
|
|
31
|
-
if (list.every((item) => item && typeof item === "object" && !Array.isArray(item))) {
|
|
32
|
-
return list;
|
|
33
|
-
}
|
|
34
|
-
return list.map((item, index) => ({ index, value: item }));
|
|
57
|
+
return rowsFromList(record.list);
|
|
35
58
|
}
|
|
36
59
|
return [record];
|
|
37
60
|
}
|
|
@@ -42,14 +65,19 @@ function renderTable(rows) {
|
|
|
42
65
|
return "(empty)";
|
|
43
66
|
}
|
|
44
67
|
const columns = Array.from(new Set(rows.flatMap((row) => Object.keys(row))));
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
68
|
+
// Format every cell once (formatScalar may JSON.stringify objects), sanitizing
|
|
69
|
+
// control chars so multi-line fields don't break alignment. Reuse the matrix for
|
|
70
|
+
// both width and rendering — and compute widths with reduce, NOT Math.max(...arr):
|
|
71
|
+
// spreading a per-row array overflows the call stack on large results (table is
|
|
72
|
+
// the default format, e.g. `quote day-kline --security all`). Widths and padding
|
|
73
|
+
// use displayWidth so CJK cells stay aligned.
|
|
74
|
+
const headerCells = columns.map(sanitizeCell);
|
|
75
|
+
const matrix = rows.map((row) => columns.map((column) => sanitizeCell(formatScalar(row[column]))));
|
|
76
|
+
const widths = columns.map((_, c) => matrix.reduce((max, cells) => Math.max(max, displayWidth(cells[c])), displayWidth(headerCells[c])));
|
|
77
|
+
const renderLine = (values) => values.map((value, index) => value + " ".repeat(Math.max(0, widths[index] - displayWidth(value)))).join(" ");
|
|
78
|
+
const header = renderLine(headerCells);
|
|
51
79
|
const divider = renderLine(widths.map((width) => "-".repeat(width)));
|
|
52
|
-
const body =
|
|
80
|
+
const body = matrix.map((cells) => renderLine(cells));
|
|
53
81
|
return [header, divider, ...body].join("\n");
|
|
54
82
|
}
|
|
55
83
|
function renderMarkdown(rows) {
|
|
@@ -57,9 +85,11 @@ function renderMarkdown(rows) {
|
|
|
57
85
|
return "(empty)";
|
|
58
86
|
}
|
|
59
87
|
const columns = Array.from(new Set(rows.flatMap((row) => Object.keys(row))));
|
|
60
|
-
|
|
88
|
+
// Column names come from server data (e.g. EDE indicator display names) — escape
|
|
89
|
+
// them like cell values or a name containing | / , breaks the whole table.
|
|
90
|
+
const header = `| ${columns.map((column) => sanitizeCell(column).replaceAll("|", "\\|")).join(" | ")} |`;
|
|
61
91
|
const divider = `| ${columns.map(() => "---").join(" | ")} |`;
|
|
62
|
-
const body = rows.map((row) => `| ${columns.map((column) => formatScalar(row[column]).replaceAll("|", "\\|")).join(" | ")} |`);
|
|
92
|
+
const body = rows.map((row) => `| ${columns.map((column) => sanitizeCell(formatScalar(row[column])).replaceAll("|", "\\|")).join(" | ")} |`);
|
|
63
93
|
return [header, divider, ...body].join("\n");
|
|
64
94
|
}
|
|
65
95
|
function renderCsv(rows) {
|
|
@@ -67,7 +97,7 @@ function renderCsv(rows) {
|
|
|
67
97
|
return "";
|
|
68
98
|
}
|
|
69
99
|
const columns = Array.from(new Set(rows.flatMap((row) => Object.keys(row))));
|
|
70
|
-
const header = columns.join(",");
|
|
100
|
+
const header = columns.map(csvEscape).join(",");
|
|
71
101
|
const body = rows.map((row) => columns.map((column) => csvEscape(formatScalar(row[column]))).join(","));
|
|
72
102
|
return [header, ...body].join("\n");
|
|
73
103
|
}
|
|
@@ -96,16 +126,30 @@ export function renderOutput(value, format) {
|
|
|
96
126
|
export async function streamOutputToFile(value, format, outputPath) {
|
|
97
127
|
if (format !== "jsonl" && format !== "csv")
|
|
98
128
|
return false;
|
|
99
|
-
const list =
|
|
129
|
+
const list = pickList(value);
|
|
100
130
|
if (!list)
|
|
101
131
|
return false;
|
|
102
132
|
// Below this row count the join() approach is cheaper than per-row writes.
|
|
103
133
|
if (list.length < 1000)
|
|
104
134
|
return false;
|
|
135
|
+
// csv can only stream object rows; an all-scalar list has no columns — fall back
|
|
136
|
+
// to renderOutput's index/value shaping instead of writing a BOM-only file.
|
|
137
|
+
let csvRows = [];
|
|
138
|
+
let csvColumns = [];
|
|
139
|
+
if (format === "csv") {
|
|
140
|
+
csvRows = list.filter((row) => Boolean(row && typeof row === "object" && !Array.isArray(row)));
|
|
141
|
+
if (csvRows.length === 0)
|
|
142
|
+
return false;
|
|
143
|
+
csvColumns = Array.from(new Set(csvRows.flatMap((row) => Object.keys(row))));
|
|
144
|
+
}
|
|
105
145
|
const { dirname } = await import("node:path");
|
|
106
146
|
const { createWriteStream } = await import("node:fs");
|
|
107
147
|
await fs.mkdir(dirname(outputPath), { recursive: true });
|
|
108
148
|
const stream = createWriteStream(outputPath, { encoding: "utf8" });
|
|
149
|
+
// A stream 'error' with no listener (EACCES on open, ENOSPC mid-write) crashes the
|
|
150
|
+
// process before any write callback fires. Swallow the event here — the failure
|
|
151
|
+
// still surfaces through the write/end callbacks below.
|
|
152
|
+
stream.on("error", () => { });
|
|
109
153
|
try {
|
|
110
154
|
if (format === "jsonl") {
|
|
111
155
|
for (const item of list) {
|
|
@@ -113,23 +157,28 @@ export async function streamOutputToFile(value, format, outputPath) {
|
|
|
113
157
|
}
|
|
114
158
|
}
|
|
115
159
|
else {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const cells = columns.map((column) => csvEscape(formatScalar(row[column])));
|
|
160
|
+
// BOM so Excel double-click decodes Chinese as UTF-8 instead of ANSI/GBK.
|
|
161
|
+
await writeLine(stream, "\ufeff" + csvColumns.map(csvEscape).join(","));
|
|
162
|
+
for (const row of csvRows) {
|
|
163
|
+
const cells = csvColumns.map((column) => csvEscape(formatScalar(row[column])));
|
|
121
164
|
await writeLine(stream, cells.join(","));
|
|
122
165
|
}
|
|
123
166
|
}
|
|
124
|
-
}
|
|
125
|
-
finally {
|
|
126
167
|
await new Promise((resolve, reject) => {
|
|
127
168
|
stream.end((err) => err ? reject(err) : resolve());
|
|
128
169
|
});
|
|
129
170
|
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
// Mirror the download path: never leave a truncated file that looks complete.
|
|
173
|
+
stream.destroy();
|
|
174
|
+
await fs.unlink(outputPath).catch(() => { });
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
130
177
|
return true;
|
|
131
178
|
}
|
|
132
|
-
|
|
179
|
+
/** Extract a row array from a value: the array itself, or its `.list` property,
|
|
180
|
+
* else null. Shared by streaming, printer's title-cache, and list detection. */
|
|
181
|
+
export function pickList(value) {
|
|
133
182
|
if (Array.isArray(value))
|
|
134
183
|
return value;
|
|
135
184
|
if (value && typeof value === "object") {
|
|
@@ -146,17 +195,23 @@ function csvEscape(value) {
|
|
|
146
195
|
// so values like "-3.5" stay numeric for Excel/pandas.
|
|
147
196
|
if (/^[=@\t\r]/.test(out) || (/^[+\-]/.test(out) && !Number.isFinite(Number(out))))
|
|
148
197
|
out = "'" + out;
|
|
149
|
-
if (/[",\n]/.test(out))
|
|
198
|
+
if (/[",\n\r]/.test(out))
|
|
150
199
|
return `"${out.replaceAll("\"", "\"\"")}"`;
|
|
151
200
|
return out;
|
|
152
201
|
}
|
|
153
202
|
function writeLine(stream, line) {
|
|
154
203
|
return new Promise((resolve, reject) => {
|
|
155
204
|
const ok = stream.write(line + "\n", (err) => err ? reject(err) : undefined);
|
|
156
|
-
if (ok)
|
|
205
|
+
if (ok) {
|
|
157
206
|
resolve();
|
|
158
|
-
|
|
159
|
-
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
// Waiting only for 'drain' would hang forever if the stream errors instead;
|
|
210
|
+
// race the two and detach the loser so listeners don't pile up per write.
|
|
211
|
+
const onDrain = () => { stream.off("error", onError); resolve(); };
|
|
212
|
+
const onError = (err) => { stream.off("drain", onDrain); reject(err); };
|
|
213
|
+
stream.once("drain", onDrain);
|
|
214
|
+
stream.once("error", onError);
|
|
160
215
|
});
|
|
161
216
|
}
|
|
162
217
|
export async function saveOutputIfNeeded(content, outputPath) {
|
|
@@ -164,7 +219,7 @@ export async function saveOutputIfNeeded(content, outputPath) {
|
|
|
164
219
|
return;
|
|
165
220
|
}
|
|
166
221
|
const { dirname } = await import("node:path");
|
|
167
|
-
await
|
|
222
|
+
await fs.mkdir(dirname(outputPath), { recursive: true });
|
|
168
223
|
if (typeof content === "string") {
|
|
169
224
|
await fs.writeFile(outputPath, content, "utf8");
|
|
170
225
|
return;
|
package/dist/src/core/printer.js
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
import { normalizeRows } from "./normalize.js";
|
|
2
|
-
import { renderOutput, saveOutputIfNeeded, streamOutputToFile } from "./output.js";
|
|
2
|
+
import { pickList, renderOutput, saveOutputIfNeeded, streamOutputToFile } from "./output.js";
|
|
3
3
|
import { extractTitles, writeTitleCache } from "./titleCache.js";
|
|
4
4
|
export async function printData(data, format, output, cache) {
|
|
5
5
|
const normalized = normalizeRows(data);
|
|
6
|
-
const items =
|
|
7
|
-
? normalized
|
|
8
|
-
: (normalized && typeof normalized === "object" && Array.isArray(normalized.list))
|
|
9
|
-
? normalized.list
|
|
10
|
-
: null;
|
|
6
|
+
const items = pickList(normalized);
|
|
11
7
|
if (cache && items) {
|
|
12
8
|
const titles = extractTitles(items, cache);
|
|
13
9
|
if (Object.keys(titles).length > 0) {
|
|
@@ -20,6 +16,12 @@ export async function printData(data, format, output, cache) {
|
|
|
20
16
|
const listLen = Array.isArray(meta.list) ? meta.list.length : 0;
|
|
21
17
|
process.stderr.write(`Total: ${meta.total}, showing: ${listLen}\n`);
|
|
22
18
|
}
|
|
19
|
+
// Partial results (failed pages/shards) exit with code 3: the table/csv/jsonl
|
|
20
|
+
// renderers only emit the rows, so without a distinct exit code a script or AI
|
|
21
|
+
// consumer cannot tell a partial export from a complete one.
|
|
22
|
+
if (meta.partial === true) {
|
|
23
|
+
process.exitCode = 3;
|
|
24
|
+
}
|
|
23
25
|
}
|
|
24
26
|
if (output) {
|
|
25
27
|
if (await streamOutputToFile(normalized, format, output)) {
|
|
@@ -27,7 +29,9 @@ export async function printData(data, format, output, cache) {
|
|
|
27
29
|
return;
|
|
28
30
|
}
|
|
29
31
|
const content = renderOutput(normalized, format);
|
|
30
|
-
|
|
32
|
+
// CSV files get a BOM so Excel double-click decodes Chinese as UTF-8 (stdout
|
|
33
|
+
// stays BOM-free for pipes).
|
|
34
|
+
await saveOutputIfNeeded(format === "csv" ? `\ufeff${content}` : content, output);
|
|
31
35
|
process.stdout.write(`${output}\n`);
|
|
32
36
|
return;
|
|
33
37
|
}
|
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
import { runWithConcurrency } from "./transport.js";
|
|
1
|
+
import { isVerbose, PAGE_CONCURRENCY, runWithConcurrency } from "./transport.js";
|
|
2
2
|
const DAY_MS = 86_400_000;
|
|
3
3
|
/** API-side row cap (per docs). Used to lift the default 6000-row cap on
|
|
4
4
|
* `--security all` queries so a 2-day A-share shard (~11K rows) isn't
|
|
5
5
|
* silently truncated. Single-security queries are untouched. */
|
|
6
6
|
const ALL_MARKET_LIMIT = 10_000;
|
|
7
|
-
/** Shard fan-out concurrency. Shares the GANGTISE_PAGE_CONCURRENCY knob with
|
|
8
|
-
* pagination (see transport/client) so one env var tunes all request fan-out. */
|
|
9
|
-
const SHARD_CONCURRENCY = Number(process.env.GANGTISE_PAGE_CONCURRENCY ?? 5) || 5;
|
|
10
7
|
function parseDate(value) {
|
|
11
8
|
// Accept yyyy-MM-dd; reject anything else so we can fall back to a single request.
|
|
12
9
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
@@ -46,12 +43,17 @@ function buildShards(start, end, shardDays) {
|
|
|
46
43
|
* single-security queries this is a no-op.
|
|
47
44
|
*/
|
|
48
45
|
export async function callKlineWithSharding(client, endpointKey, body, config) {
|
|
49
|
-
if (!isAllMarket(body)
|
|
46
|
+
if (!isAllMarket(body)) {
|
|
50
47
|
return client.call(endpointKey, body);
|
|
51
48
|
}
|
|
52
49
|
// `--security all` returns thousands of rows per day; lift the default 6000-row
|
|
53
|
-
// cap to the API max so single-shard requests aren't silently truncated.
|
|
50
|
+
// cap to the API max so single-shard requests aren't silently truncated. This
|
|
51
|
+
// must apply even when a date is missing (no sharding possible then, but the
|
|
52
|
+
// single request still needs the lifted cap).
|
|
54
53
|
const allMarketBody = { ...body, limit: body.limit ?? ALL_MARKET_LIMIT };
|
|
54
|
+
if (!body.startDate || !body.endDate) {
|
|
55
|
+
return client.call(endpointKey, allMarketBody);
|
|
56
|
+
}
|
|
55
57
|
const start = parseDate(body.startDate);
|
|
56
58
|
const end = parseDate(body.endDate);
|
|
57
59
|
if (!start || !end || end < start) {
|
|
@@ -62,7 +64,9 @@ export async function callKlineWithSharding(client, endpointKey, body, config) {
|
|
|
62
64
|
return client.call(endpointKey, allMarketBody);
|
|
63
65
|
}
|
|
64
66
|
const shards = buildShards(start, end, config.shardDays);
|
|
65
|
-
|
|
67
|
+
// isVerbose() (not a direct env read) so the global --verbose flag reaches
|
|
68
|
+
// shard logging too — cli.ts enables it via setVerbose in a preAction hook.
|
|
69
|
+
if (isVerbose()) {
|
|
66
70
|
process.stderr.write(`[gangtise] sharding ${endpointKey} into ${shards.length} requests (${config.shardDays} day(s) each)\n`);
|
|
67
71
|
}
|
|
68
72
|
// Per-shard fault tolerance: a failing shard is recorded and skipped (returns a
|
|
@@ -71,7 +75,7 @@ export async function callKlineWithSharding(client, endpointKey, body, config) {
|
|
|
71
75
|
// every shard on the first rejection.
|
|
72
76
|
const failedShards = [];
|
|
73
77
|
let firstError = null;
|
|
74
|
-
const results = await runWithConcurrency(shards, config.concurrency ??
|
|
78
|
+
const results = await runWithConcurrency(shards, config.concurrency ?? PAGE_CONCURRENCY, async (shard) => {
|
|
75
79
|
try {
|
|
76
80
|
return await client.call(endpointKey, { ...allMarketBody, startDate: shard.startDate, endDate: shard.endDate });
|
|
77
81
|
}
|
|
@@ -93,8 +97,11 @@ export async function callKlineWithSharding(client, endpointKey, body, config) {
|
|
|
93
97
|
header = rec;
|
|
94
98
|
if (!fieldList && Array.isArray(rec.fieldList))
|
|
95
99
|
fieldList = rec.fieldList;
|
|
100
|
+
// Append one-by-one rather than push(...list): a future higher row cap could
|
|
101
|
+
// make a single shard's list large enough to overflow the stack via spread.
|
|
96
102
|
if (Array.isArray(rec.list))
|
|
97
|
-
|
|
103
|
+
for (const item of rec.list)
|
|
104
|
+
merged.push(item);
|
|
98
105
|
}
|
|
99
106
|
// Every shard failed → surface the error loudly (non-zero exit) rather than
|
|
100
107
|
// masking a total outage as an empty success.
|
|
@@ -75,9 +75,11 @@ function capTitles(merged, freshKeys, cap) {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
if (n < cap) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
// Newest-inserted keys sit at the END of the merged object — fill in reverse so
|
|
79
|
+
// eviction drops the oldest entries, not the batch the user just listed yesterday.
|
|
80
|
+
const rest = Object.keys(merged);
|
|
81
|
+
for (let i = rest.length - 1; i >= 0 && n < cap; i--) {
|
|
82
|
+
const k = rest[i];
|
|
81
83
|
if (k in out)
|
|
82
84
|
continue;
|
|
83
85
|
out[k] = merged[k];
|
|
@@ -29,6 +29,8 @@ export async function runWithConcurrency(items, concurrency, fn) {
|
|
|
29
29
|
await Promise.all(Array.from({ length: limit }, () => worker()));
|
|
30
30
|
return results;
|
|
31
31
|
}
|
|
32
|
+
/** Fan-out width for pagination and kline shards — one env knob tunes both. */
|
|
33
|
+
export const PAGE_CONCURRENCY = Number(process.env.GANGTISE_PAGE_CONCURRENCY ?? 5) || 5;
|
|
32
34
|
const RETRYABLE_HTTP_STATUS = new Set([429, 500, 502, 503, 504]);
|
|
33
35
|
const RETRYABLE_NETWORK_CODES = new Set(["ECONNRESET", "ETIMEDOUT", "ENOTFOUND", "EAI_AGAIN", "UND_ERR_SOCKET", "UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT"]);
|
|
34
36
|
const RETRYABLE_API_CODES = new Set(["999999"]);
|
package/dist/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated — DO NOT EDIT
|
|
2
|
-
export const CLI_VERSION = "0.
|
|
2
|
+
export const CLI_VERSION = "0.22.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gangtise-openapi-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "CLI for Gangtise OpenAPI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"build": "tsc -p tsconfig.json",
|
|
27
27
|
"dev": "tsx src/cli.ts",
|
|
28
28
|
"test": "vitest run",
|
|
29
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
29
30
|
"prepare": "node scripts/prepare.cjs && npm run build"
|
|
30
31
|
},
|
|
31
32
|
"engines": {
|
|
@@ -36,9 +37,9 @@
|
|
|
36
37
|
"undici": "^7.16.0"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
|
-
"@types/node": "^
|
|
40
|
+
"@types/node": "^20.19.43",
|
|
40
41
|
"tsx": "^4.20.5",
|
|
41
42
|
"typescript": "^5.9.2",
|
|
42
|
-
"vitest": "^3.2.
|
|
43
|
+
"vitest": "^3.2.6"
|
|
43
44
|
}
|
|
44
45
|
}
|