gangtise-openapi-cli 0.42.0 → 0.43.1
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 +43 -36
- package/dist/src/cli.js +29 -11
- package/dist/src/commands/ai.js +7 -6
- package/dist/src/commands/fundamental.js +3 -3
- package/dist/src/commands/indicator.js +3 -3
- package/dist/src/commands/insight.js +5 -4
- package/dist/src/commands/quote.js +46 -12
- package/dist/src/commands/raw.js +2 -2
- package/dist/src/commands/shared.js +60 -8
- package/dist/src/commands/vault.js +2 -2
- package/dist/src/core/args.js +6 -5
- package/dist/src/core/auth.js +3 -2
- package/dist/src/core/client.js +114 -11
- package/dist/src/core/commandBodies.js +2 -1
- package/dist/src/core/download.js +9 -5
- package/dist/src/core/endpoints.js +103 -1
- package/dist/src/core/opinionDetail.js +16 -6
- package/dist/src/core/output.js +37 -8
- package/dist/src/core/perSecurity.js +29 -11
- package/dist/src/core/quoteSharding.js +19 -15
- package/dist/src/core/rowSink.js +21 -0
- package/dist/src/core/titleCache.js +28 -2
- package/dist/src/core/transport.js +3 -1
- package/dist/src/core/updateCheck.js +51 -0
- package/dist/src/version.js +19 -1
- package/gangtise-openapi/SKILL.md +50 -49
- package/gangtise-openapi/references/commands/ai.md +34 -15
- package/gangtise-openapi/references/commands/alternative.md +2 -2
- package/gangtise-openapi/references/commands/bond.md +1 -1
- package/gangtise-openapi/references/commands/fundamental.md +4 -4
- package/gangtise-openapi/references/commands/indicator.md +31 -29
- package/gangtise-openapi/references/commands/insight.md +40 -32
- package/gangtise-openapi/references/commands/quote.md +19 -19
- package/gangtise-openapi/references/commands/reference-and-lookup.md +18 -15
- package/gangtise-openapi/references/commands/vault.md +10 -7
- package/gangtise-openapi/references/errors.md +22 -23
- package/gangtise-openapi/references/examples.md +36 -34
- package/gangtise-openapi/references/fields.md +8 -6
- package/gangtise-openapi/references/lookup-ids.md +10 -8
- package/gangtise-openapi/references/response-schema.md +7 -7
- package/package.json +1 -1
package/dist/src/core/output.js
CHANGED
|
@@ -1,9 +1,32 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { rmSync, statSync } from "node:fs";
|
|
2
3
|
import fs from "node:fs/promises";
|
|
3
4
|
import { ConfigError } from "./errors.js";
|
|
4
5
|
const OUTPUT_FORMATS = ["table", "json", "jsonl", "csv", "markdown"];
|
|
5
6
|
export function stagingPath(target, suffix = "part") {
|
|
6
|
-
|
|
7
|
+
const staging = `${target}.${randomBytes(4).toString("hex")}.${suffix}`;
|
|
8
|
+
handedOut.add(staging);
|
|
9
|
+
return staging;
|
|
10
|
+
}
|
|
11
|
+
const handedOut = new Set();
|
|
12
|
+
const claimed = new Set();
|
|
13
|
+
export function trackClaim(name) {
|
|
14
|
+
claimed.add(name);
|
|
15
|
+
}
|
|
16
|
+
export function removeStagingFiles() {
|
|
17
|
+
for (const staging of handedOut) {
|
|
18
|
+
try {
|
|
19
|
+
rmSync(staging, { force: true });
|
|
20
|
+
}
|
|
21
|
+
catch { }
|
|
22
|
+
}
|
|
23
|
+
for (const name of claimed) {
|
|
24
|
+
try {
|
|
25
|
+
if (statSync(name).size === 0)
|
|
26
|
+
rmSync(name, { force: true });
|
|
27
|
+
}
|
|
28
|
+
catch { }
|
|
29
|
+
}
|
|
7
30
|
}
|
|
8
31
|
export function digestBuffer(content) {
|
|
9
32
|
const buf = typeof content === "string" ? Buffer.from(content, "utf8") : content;
|
|
@@ -29,9 +52,13 @@ export function parseOutputFormat(value) {
|
|
|
29
52
|
throw new ConfigError(`Unsupported format: ${format}`);
|
|
30
53
|
}
|
|
31
54
|
function sanitizeCell(value) {
|
|
55
|
+
if (!/[\u0000-\u001f\u007f-\u009f]/.test(value))
|
|
56
|
+
return value;
|
|
32
57
|
return value.replace(/[\r\n]+/g, " ").replace(/[\u0000-\u001f\u007f\u0080-\u009f]/g, "");
|
|
33
58
|
}
|
|
34
59
|
function displayWidth(value) {
|
|
60
|
+
if (/^[\x20-\x7e]*$/.test(value))
|
|
61
|
+
return value.length;
|
|
35
62
|
let width = 0;
|
|
36
63
|
for (const ch of value) {
|
|
37
64
|
const cp = ch.codePointAt(0);
|
|
@@ -97,11 +124,13 @@ function renderTable(rows) {
|
|
|
97
124
|
const columns = Array.from(new Set(rows.flatMap((row) => Object.keys(row))));
|
|
98
125
|
const headerCells = columns.map((column) => clampCell(sanitizeCell(column)));
|
|
99
126
|
const matrix = rows.map((row) => columns.map((column) => clampCell(sanitizeCell(formatScalar(row[column])))));
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
const
|
|
127
|
+
const cellWidths = matrix.map((cells) => cells.map(displayWidth));
|
|
128
|
+
const headerWidths = headerCells.map(displayWidth);
|
|
129
|
+
const widths = columns.map((_, c) => cellWidths.reduce((max, cells) => Math.max(max, cells[c]), headerWidths[c]));
|
|
130
|
+
const renderLine = (values, valueWidths) => values.map((value, index) => value + " ".repeat(Math.max(0, widths[index] - valueWidths[index]))).join(" ");
|
|
131
|
+
const header = renderLine(headerCells, headerWidths);
|
|
132
|
+
const divider = renderLine(widths.map((width) => "-".repeat(width)), widths);
|
|
133
|
+
const body = matrix.map((cells, r) => renderLine(cells, cellWidths[r]));
|
|
105
134
|
return [header, divider, ...body].join("\n");
|
|
106
135
|
}
|
|
107
136
|
function renderMarkdown(rows) {
|
|
@@ -242,7 +271,7 @@ export function writeLine(stream, line) {
|
|
|
242
271
|
stream.once("error", onError);
|
|
243
272
|
});
|
|
244
273
|
}
|
|
245
|
-
export async function saveOutputIfNeeded(content, outputPath) {
|
|
274
|
+
export async function saveOutputIfNeeded(content, outputPath, withDigest = true) {
|
|
246
275
|
if (!outputPath) {
|
|
247
276
|
return;
|
|
248
277
|
}
|
|
@@ -256,7 +285,7 @@ export async function saveOutputIfNeeded(content, outputPath) {
|
|
|
256
285
|
else {
|
|
257
286
|
await fs.writeFile(partPath, content);
|
|
258
287
|
}
|
|
259
|
-
const digest = digestBuffer(content);
|
|
288
|
+
const digest = withDigest ? digestBuffer(content) : undefined;
|
|
260
289
|
await fs.rename(partPath, outputPath);
|
|
261
290
|
return digest;
|
|
262
291
|
}
|
|
@@ -17,7 +17,10 @@ export function estimateTradingDays(startDate, endDate) {
|
|
|
17
17
|
}
|
|
18
18
|
return days;
|
|
19
19
|
}
|
|
20
|
-
export async function callPerSecurity(client, endpointKey, securities, makeBody, cap, label) {
|
|
20
|
+
export async function callPerSecurity(client, endpointKey, securities, makeBody, cap, label, groupSize = 1) {
|
|
21
|
+
const groups = [];
|
|
22
|
+
for (let i = 0; i < securities.length; i += groupSize)
|
|
23
|
+
groups.push(securities.slice(i, i + groupSize));
|
|
21
24
|
const sink = client.claimRowSink?.();
|
|
22
25
|
let fieldList;
|
|
23
26
|
let headerSecurity;
|
|
@@ -29,14 +32,14 @@ export async function callPerSecurity(client, endpointKey, securities, makeBody,
|
|
|
29
32
|
const mergePart = async (part, i) => {
|
|
30
33
|
const rec = part;
|
|
31
34
|
if (!(rec && typeof rec === "object" && Array.isArray(rec.list))) {
|
|
32
|
-
throw markStructural(new ApiError(`${label}: ${
|
|
35
|
+
throw markStructural(new ApiError(`${label}: ${groups[i].join(", ")} returned no list payload — the response layout may have changed`, undefined, undefined, rec));
|
|
33
36
|
}
|
|
34
37
|
if (rec.partial === true)
|
|
35
38
|
partial = true;
|
|
36
39
|
if (rec.list.length === 0) {
|
|
37
40
|
if ((typeof rec.total === "number" && rec.total > 0) || rec.partial === true) {
|
|
38
41
|
const claim = typeof rec.total === "number" && rec.total > 0 ? `reported total=${rec.total}` : "carried a partial marker";
|
|
39
|
-
throw markStructural(new ApiError(`${label}: ${
|
|
42
|
+
throw markStructural(new ApiError(`${label}: ${groups[i].join(", ")} ${claim} but delivered no rows — the response is inconsistent`, undefined, undefined, rec));
|
|
40
43
|
}
|
|
41
44
|
if (!emptyFields && Array.isArray(rec.fieldList))
|
|
42
45
|
emptyFields = rec.fieldList;
|
|
@@ -44,26 +47,27 @@ export async function callPerSecurity(client, endpointKey, securities, makeBody,
|
|
|
44
47
|
}
|
|
45
48
|
const fields = Array.isArray(rec.fieldList) && rec.fieldList.length > 0 ? rec.fieldList : undefined;
|
|
46
49
|
if (rec.list.some(Array.isArray) && !columnarSchemaValid(fields, rec.list)) {
|
|
47
|
-
throw markStructural(new ApiError(`${label}: ${
|
|
50
|
+
throw markStructural(new ApiError(`${label}: ${groups[i].join(", ")} returned columnar rows without a usable fieldList (missing, duplicated or mis-sized) — they cannot be read by position`, undefined, undefined, rec));
|
|
48
51
|
}
|
|
49
52
|
if (fields && !fieldList) {
|
|
50
53
|
fieldList = fields;
|
|
51
|
-
headerSecurity =
|
|
54
|
+
headerSecurity = groups[i].join(", ");
|
|
52
55
|
sink?.setFieldList(fieldList);
|
|
53
56
|
}
|
|
54
57
|
if ((fields ?? fieldList) && !sameColumns(fieldList, fields)) {
|
|
55
|
-
throw markStructural(new ApiError(`${label}: ${
|
|
58
|
+
throw markStructural(new ApiError(`${label}: ${groups[i].join(", ")} answered with columns ${JSON.stringify(fields)} while ${headerSecurity} answered ${JSON.stringify(fieldList)} — the parts cannot be merged`, undefined, undefined, rec));
|
|
56
59
|
}
|
|
57
60
|
if (rec.list.length >= cap)
|
|
58
|
-
truncated.push(
|
|
59
|
-
|
|
61
|
+
truncated.push(...groups[i]);
|
|
62
|
+
const rows = inInputOrder(rec.list, fields ?? fieldList, groups[i], label);
|
|
63
|
+
count += rows.length;
|
|
60
64
|
if (sink)
|
|
61
|
-
await sink.push(
|
|
65
|
+
await sink.push(rows);
|
|
62
66
|
else
|
|
63
|
-
for (const row of
|
|
67
|
+
for (const row of rows)
|
|
64
68
|
merged.push(row);
|
|
65
69
|
};
|
|
66
|
-
await runInOrder(
|
|
70
|
+
await runInOrder(groups, PAGE_CONCURRENCY, (codes) => client.call(endpointKey, makeBody(codes)), mergePart);
|
|
67
71
|
const out = { total: count, list: merged };
|
|
68
72
|
if (sink)
|
|
69
73
|
attachRowSink(out, sink);
|
|
@@ -80,6 +84,20 @@ export async function callPerSecurity(client, endpointKey, securities, makeBody,
|
|
|
80
84
|
}
|
|
81
85
|
return out;
|
|
82
86
|
}
|
|
87
|
+
function inInputOrder(rows, fields, group, label) {
|
|
88
|
+
if (group.length < 2)
|
|
89
|
+
return rows;
|
|
90
|
+
const column = fields?.indexOf("securityCode") ?? -1;
|
|
91
|
+
if (rows.some(Array.isArray) && column < 0) {
|
|
92
|
+
throw markStructural(new ApiError(`${label}: ${group.join(", ")} answered without a securityCode column — rows of several securities cannot be told apart`));
|
|
93
|
+
}
|
|
94
|
+
const rank = new Map(group.map((code, i) => [code.toUpperCase(), i]));
|
|
95
|
+
const codeOf = (row) => Array.isArray(row) ? row[column] : row?.securityCode;
|
|
96
|
+
return rows
|
|
97
|
+
.map((row, i) => ({ row, i, at: rank.get(String(codeOf(row)).toUpperCase()) ?? group.length }))
|
|
98
|
+
.sort((a, b) => a.at - b.at || a.i - b.i)
|
|
99
|
+
.map(({ row }) => row);
|
|
100
|
+
}
|
|
83
101
|
function sameColumns(a, b) {
|
|
84
102
|
if (!a || !b)
|
|
85
103
|
return false;
|
|
@@ -3,7 +3,8 @@ import { columnarSchemaValid } from "./normalize.js";
|
|
|
3
3
|
import { attachRowSink } from "./rowSink.js";
|
|
4
4
|
import { isVerbose, PAGE_CONCURRENCY, runInOrder } from "./transport.js";
|
|
5
5
|
const DAY_MS = 86_400_000;
|
|
6
|
-
const
|
|
6
|
+
export const QUOTE_MAX_LIMIT = 10_000;
|
|
7
|
+
const ALL_MARKET_LIMIT = QUOTE_MAX_LIMIT;
|
|
7
8
|
function parseDate(value) {
|
|
8
9
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
9
10
|
return null;
|
|
@@ -42,18 +43,21 @@ function isWeekendUtc(d) {
|
|
|
42
43
|
}
|
|
43
44
|
function buildShards(start, end, shardDays) {
|
|
44
45
|
const shards = [];
|
|
45
|
-
let
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
46
|
+
let group = [];
|
|
47
|
+
const flush = () => {
|
|
48
|
+
if (group.length === 0)
|
|
49
|
+
return;
|
|
50
|
+
shards.push({ startDate: formatDate(new Date(group[0])), endDate: formatDate(new Date(group[group.length - 1])) });
|
|
51
|
+
group = [];
|
|
52
|
+
};
|
|
53
|
+
for (let day = start.getTime(); day <= end.getTime(); day += DAY_MS) {
|
|
54
|
+
if (isWeekendUtc(new Date(day)))
|
|
55
|
+
continue;
|
|
56
|
+
group.push(day);
|
|
57
|
+
if (group.length === shardDays)
|
|
58
|
+
flush();
|
|
59
|
+
}
|
|
60
|
+
flush();
|
|
57
61
|
return shards;
|
|
58
62
|
}
|
|
59
63
|
export async function callKlineWithSharding(client, endpointKey, body, config) {
|
|
@@ -65,7 +69,7 @@ export async function callKlineWithSharding(client, endpointKey, body, config) {
|
|
|
65
69
|
const perShardLimit = allMarketBody.limit ?? ALL_MARKET_LIMIT;
|
|
66
70
|
const truncationHint = perShardLimit < ALL_MARKET_LIMIT
|
|
67
71
|
? "raise or omit --limit to fetch the full market"
|
|
68
|
-
: `a single ${config.shardDays}-
|
|
72
|
+
: `a single ${config.shardDays}-weekday window exceeds the ${ALL_MARKET_LIMIT}-row API cap`;
|
|
69
73
|
const isTruncated = (rec) => Array.isArray(rec.list) && rec.list.length >= perShardLimit;
|
|
70
74
|
const callSingle = async () => {
|
|
71
75
|
const single = await client.call(endpointKey, allMarketBody);
|
|
@@ -92,7 +96,7 @@ export async function callKlineWithSharding(client, endpointKey, body, config) {
|
|
|
92
96
|
if (shards.length === 0)
|
|
93
97
|
return { list: [] };
|
|
94
98
|
if (isVerbose()) {
|
|
95
|
-
process.stderr.write(`[gangtise] sharding ${endpointKey} into ${shards.length} requests (${config.shardDays}
|
|
99
|
+
process.stderr.write(`[gangtise] sharding ${endpointKey} into ${shards.length} requests (${config.shardDays} weekday(s) each)\n`);
|
|
96
100
|
}
|
|
97
101
|
const failedShards = [];
|
|
98
102
|
let firstError = null;
|
package/dist/src/core/rowSink.js
CHANGED
|
@@ -22,6 +22,7 @@ export class ExportSink {
|
|
|
22
22
|
columns = [];
|
|
23
23
|
columnSet = new Set();
|
|
24
24
|
objectRows = 0;
|
|
25
|
+
earliestDates = new Map();
|
|
25
26
|
partPath;
|
|
26
27
|
rowsPath;
|
|
27
28
|
constructor(outputPath, format = "jsonl", cache) {
|
|
@@ -45,7 +46,27 @@ export class ExportSink {
|
|
|
45
46
|
setFieldList(fields) {
|
|
46
47
|
this.fields = fields;
|
|
47
48
|
}
|
|
49
|
+
watchEarliest(field) {
|
|
50
|
+
if (!this.earliestDates.has(field))
|
|
51
|
+
this.earliestDates.set(field, undefined);
|
|
52
|
+
}
|
|
53
|
+
earliest(field) {
|
|
54
|
+
return this.earliestDates.get(field);
|
|
55
|
+
}
|
|
48
56
|
async push(rows) {
|
|
57
|
+
for (const [field, current] of this.earliestDates) {
|
|
58
|
+
const index = this.fields ? this.fields.indexOf(field) : -1;
|
|
59
|
+
let earliest = current;
|
|
60
|
+
for (const row of rows) {
|
|
61
|
+
const value = Array.isArray(row) ? (index >= 0 ? row[index] : undefined) : row && typeof row === "object" ? row[field] : undefined;
|
|
62
|
+
if (typeof value !== "string")
|
|
63
|
+
continue;
|
|
64
|
+
const day = value.slice(0, 10);
|
|
65
|
+
if (earliest === undefined || day < earliest)
|
|
66
|
+
earliest = day;
|
|
67
|
+
}
|
|
68
|
+
this.earliestDates.set(field, earliest);
|
|
69
|
+
}
|
|
49
70
|
if (this.stream) {
|
|
50
71
|
await this.write(rows);
|
|
51
72
|
return;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { stagingPath } from "./output.js";
|
|
4
5
|
export const DEFAULT_TITLE_CACHE_PATH = path.join(os.homedir(), ".config", "gangtise", "title-cache.json");
|
|
5
6
|
export const TITLE_LOOKUP_SIZE = 200;
|
|
6
7
|
const TITLE_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
@@ -43,6 +44,29 @@ async function loadInto(filePath) {
|
|
|
43
44
|
export async function readTitleCache(filePath = DEFAULT_TITLE_CACHE_PATH) {
|
|
44
45
|
return loadInto(filePath);
|
|
45
46
|
}
|
|
47
|
+
async function mergeFromDisk(filePath, data) {
|
|
48
|
+
let disk;
|
|
49
|
+
try {
|
|
50
|
+
disk = JSON.parse(await fs.readFile(filePath, "utf8"));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!disk || typeof disk !== "object" || Array.isArray(disk))
|
|
56
|
+
return;
|
|
57
|
+
for (const [key, entry] of Object.entries(disk)) {
|
|
58
|
+
if (!entry?.titles || typeof entry.titles !== "object" || typeof entry.ts !== "number")
|
|
59
|
+
continue;
|
|
60
|
+
const mine = data[key];
|
|
61
|
+
if (!mine) {
|
|
62
|
+
data[key] = { titles: entry.titles, ts: entry.ts };
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const titles = { ...entry.titles, ...mine.titles };
|
|
66
|
+
data[key] = { titles: capTitles(titles, Object.keys(mine.titles), MAX_TITLES_PER_ENDPOINT), ts: Math.max(entry.ts, mine.ts) };
|
|
67
|
+
}
|
|
68
|
+
pruneCache(data, "", []);
|
|
69
|
+
}
|
|
46
70
|
async function flush(filePath) {
|
|
47
71
|
for (;;) {
|
|
48
72
|
if (!dirty || !memoryCache) {
|
|
@@ -50,9 +74,11 @@ async function flush(filePath) {
|
|
|
50
74
|
return;
|
|
51
75
|
}
|
|
52
76
|
dirty = false;
|
|
53
|
-
const
|
|
77
|
+
const data = memoryCache;
|
|
78
|
+
await mergeFromDisk(filePath, data);
|
|
79
|
+
const snapshot = JSON.stringify(data);
|
|
54
80
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
55
|
-
const tmp =
|
|
81
|
+
const tmp = stagingPath(filePath, "tmp");
|
|
56
82
|
await fs.writeFile(tmp, snapshot, { encoding: "utf8", mode: 0o600 });
|
|
57
83
|
try {
|
|
58
84
|
await fs.rename(tmp, filePath);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import { gunzipSync } from "node:zlib";
|
|
2
|
-
import { Agent } from "undici";
|
|
3
3
|
import { ApiError } from "./errors.js";
|
|
4
4
|
export function decodeResponseBody(buf, contentEncoding) {
|
|
5
5
|
const enc = (Array.isArray(contentEncoding) ? contentEncoding[0] : contentEncoding)?.toLowerCase().trim();
|
|
@@ -34,8 +34,10 @@ function retryAfterFromError(error) {
|
|
|
34
34
|
return undefined;
|
|
35
35
|
}
|
|
36
36
|
let cachedDispatcher = null;
|
|
37
|
+
const requireLazily = createRequire(import.meta.url);
|
|
37
38
|
export function getDispatcher() {
|
|
38
39
|
if (!cachedDispatcher) {
|
|
40
|
+
const { Agent } = requireLazily("undici");
|
|
39
41
|
cachedDispatcher = new Agent({
|
|
40
42
|
keepAliveTimeout: 60_000,
|
|
41
43
|
keepAliveMaxTimeout: 600_000,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { isVersionNewer } from "./args.js";
|
|
5
|
+
import { stagingPath } from "./output.js";
|
|
6
|
+
export const UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1000;
|
|
7
|
+
export const DEFAULT_UPDATE_CHECK_PATH = path.join(os.homedir(), ".config", "gangtise", "update-check.json");
|
|
8
|
+
function saveLatest(cachePath, latest, now) {
|
|
9
|
+
const staging = stagingPath(cachePath, "tmp");
|
|
10
|
+
try {
|
|
11
|
+
mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
12
|
+
writeFileSync(staging, JSON.stringify({ checkedAt: now, latest }));
|
|
13
|
+
renameSync(staging, cachePath);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
try {
|
|
17
|
+
rmSync(staging, { force: true });
|
|
18
|
+
}
|
|
19
|
+
catch { }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function latestVersion(cachePath = DEFAULT_UPDATE_CHECK_PATH, timeoutMs = 2000, now = Date.now()) {
|
|
23
|
+
const cacheable = path.isAbsolute(cachePath);
|
|
24
|
+
if (cacheable) {
|
|
25
|
+
try {
|
|
26
|
+
const cached = JSON.parse(readFileSync(cachePath, "utf8"));
|
|
27
|
+
const age = typeof cached.checkedAt === "number" ? now - cached.checkedAt : -1;
|
|
28
|
+
if (typeof cached.latest === "string" && age >= 0 && age < UPDATE_CHECK_TTL_MS)
|
|
29
|
+
return cached.latest;
|
|
30
|
+
}
|
|
31
|
+
catch { }
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const response = await fetch("https://registry.npmjs.org/gangtise-openapi-cli/latest", { signal: AbortSignal.timeout(timeoutMs) });
|
|
35
|
+
const latest = (await response.json()).version;
|
|
36
|
+
if (typeof latest !== "string")
|
|
37
|
+
return undefined;
|
|
38
|
+
if (cacheable)
|
|
39
|
+
saveLatest(cachePath, latest, now);
|
|
40
|
+
return latest;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export async function checkForUpdate(current) {
|
|
47
|
+
const latest = await latestVersion();
|
|
48
|
+
if (latest && isVersionNewer(latest, current)) {
|
|
49
|
+
process.stderr.write(`Update available: ${current} → ${latest}\nRun: npm update -g gangtise-openapi-cli\n`);
|
|
50
|
+
}
|
|
51
|
+
}
|
package/dist/src/version.js
CHANGED
|
@@ -1 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
function packageVersion() {
|
|
5
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
for (;;) {
|
|
7
|
+
const file = path.join(dir, "package.json");
|
|
8
|
+
if (fs.existsSync(file)) {
|
|
9
|
+
const pkg = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
10
|
+
if (pkg.name === "gangtise-openapi-cli" && pkg.version)
|
|
11
|
+
return pkg.version;
|
|
12
|
+
}
|
|
13
|
+
const parent = path.dirname(dir);
|
|
14
|
+
if (parent === dir)
|
|
15
|
+
throw new Error("gangtise-openapi-cli: its package.json was not found above " + fileURLToPath(import.meta.url));
|
|
16
|
+
dir = parent;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export const CLI_VERSION = packageVersion();
|