gangtise-openapi-cli 0.47.0 → 0.49.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.
@@ -0,0 +1,301 @@
1
+ import { beijingDateString } from "./args.js";
2
+ import { normalizeRows } from "./normalize.js";
3
+ import { PAGE_CONCURRENCY, runWithConcurrency } from "./transport.js";
4
+ const PRICE_TOLERANCE = 0.0005;
5
+ const agrees = (close, price) => price > 0 && Math.abs(close - price) / price <= PRICE_TOLERANCE;
6
+ const QUOTABLE = /^(\d{6}\.(SH|SZ|BJ)|\d{5}\.HK|[A-Z][A-Z0-9.]*\.(O|N|A))$/;
7
+ const QUOTE_BATCH = 500;
8
+ const CLOSES_WINDOW_DAYS = 10;
9
+ const CLOSES_BATCH = 1000;
10
+ const squash = (name) => name.replace(/\s+/g, "");
11
+ const base = (name) => squash(name).replace(/-[A-Z]+$/, "");
12
+ const ticker = (code) => code.slice(0, code.lastIndexOf("."));
13
+ const exchange = (code) => code.slice(code.lastIndexOf(".") + 1);
14
+ const market = (code) => {
15
+ const suffix = exchange(code);
16
+ return ["SH", "SZ", "BJ"].includes(suffix) ? "CN" : ["O", "N", "A"].includes(suffix) ? "US" : suffix;
17
+ };
18
+ function toCandidates(rows) {
19
+ return rows.flatMap((row) => {
20
+ const r = row;
21
+ return typeof r.gtsCode === "string" && typeof r.gtsName === "string" && r.category !== "index"
22
+ ? [{ code: r.gtsCode, name: r.gtsName, category: String(r.category), prefix: false }]
23
+ : [];
24
+ });
25
+ }
26
+ async function candidatesFor(name, lookup) {
27
+ const key = squash(name);
28
+ const found = toCandidates(await lookup.search(key));
29
+ const exact = found.filter((c) => base(c.name) === base(key) || ticker(c.code) === key);
30
+ if (/^[A-Z][A-Z0-9]{0,6}$/.test(key) && !exact.some((c) => market(c.code) === "US")) {
31
+ const bySuffix = (await Promise.all(["O", "N"].map((suffix) => lookup.search(`${key}.${suffix}`))))
32
+ .flatMap((rows) => toCandidates(rows))
33
+ .filter((c) => ticker(c.code) === key);
34
+ exact.push(...bySuffix);
35
+ }
36
+ const prefix = base(key).length >= 2 ? found.filter((c) => !exact.includes(c) && base(c.name).startsWith(base(key))) : [];
37
+ return [...exact, ...prefix.map((c) => ({ ...c, prefix: true }))];
38
+ }
39
+ function narrow(candidates, etf) {
40
+ let left = candidates;
41
+ if (new Set(left.map((c) => market(c.code))).size > 1) {
42
+ if (!etf.market)
43
+ return null;
44
+ left = left.filter((c) => market(c.code) === etf.market);
45
+ }
46
+ if (left.length > 1) {
47
+ const same = left.filter((c) => exchange(c.code) === etf.exchange);
48
+ if (same.length)
49
+ left = same;
50
+ }
51
+ return left.length === 1 ? left[0].code : null;
52
+ }
53
+ function byPrice(row, candidates, prices, etf, unchecked) {
54
+ const close = typeof row.close === "number" ? row.close : undefined;
55
+ if (close === undefined)
56
+ return undefined;
57
+ const open = candidates.filter((c) => !etf.used.has(c.code));
58
+ for (const tier of [open.filter((c) => !c.prefix), open.filter((c) => c.prefix)]) {
59
+ if (tier.some((c) => !prices.get(c.code)?.some((p) => p > 0) && unchecked(c)))
60
+ return undefined;
61
+ const matched = tier.filter((c) => prices.get(c.code)?.some((p) => agrees(close, p)));
62
+ if (matched.length)
63
+ return narrow(matched, etf);
64
+ }
65
+ return undefined;
66
+ }
67
+ function byName(candidates, etf) {
68
+ if (!etf.market)
69
+ return null;
70
+ const pool = candidates.filter((c) => !c.prefix && !etf.used.has(c.code) && market(c.code) === etf.market);
71
+ return pool.length ? narrow(pool, etf) : null;
72
+ }
73
+ async function splitting(codes, fetch) {
74
+ try {
75
+ return await fetch(codes);
76
+ }
77
+ catch (error) {
78
+ if (codes.length === 1 || error?.code !== "120001")
79
+ return new Map();
80
+ const half = Math.ceil(codes.length / 2);
81
+ const first = await splitting(codes.slice(0, half), fetch);
82
+ return new Map([...first, ...await splitting(codes.slice(half), fetch)]);
83
+ }
84
+ }
85
+ const inBatches = (codes, size) => Array.from({ length: Math.ceil(codes.length / size) }, (_, i) => codes.slice(i * size, (i + 1) * size));
86
+ function listDates(rows, candidates, history) {
87
+ const votes = new Map();
88
+ for (const row of rows) {
89
+ if (typeof row.close !== "number")
90
+ continue;
91
+ const exact = (candidates.get(row.componentName) ?? []).filter((c) => !c.prefix);
92
+ if (exact.length !== 1)
93
+ continue;
94
+ const tally = votes.get(market(exact[0].code)) ?? new Map();
95
+ for (const [date, close] of history.get(exact[0].code) ?? []) {
96
+ if (agrees(row.close, close))
97
+ tally.set(date, (tally.get(date) ?? 0) + 1);
98
+ }
99
+ votes.set(market(exact[0].code), tally);
100
+ }
101
+ const dates = new Map();
102
+ for (const [where, tally] of votes) {
103
+ const ranked = [...tally].sort((a, b) => b[1] - a[1]);
104
+ if (ranked.length && ranked[0][1] >= 2 && ranked[1]?.[1] !== ranked[0][1])
105
+ dates.set(where, ranked[0][0]);
106
+ }
107
+ return dates;
108
+ }
109
+ function closesOn(dates, history) {
110
+ const out = new Map();
111
+ for (const [code, series] of history) {
112
+ const date = dates.get(market(code));
113
+ if (!date)
114
+ continue;
115
+ const last = series.filter(([d]) => d <= date).at(-1);
116
+ if (last)
117
+ out.set(code, [last[1]]);
118
+ }
119
+ return out;
120
+ }
121
+ const carriesCode = (row) => typeof row.componentCode === "string" && row.componentCode !== "";
122
+ const needsCode = (row) => !carriesCode(row) && typeof row.componentName === "string";
123
+ export async function fillComponentCodes(rows, lookup) {
124
+ const pending = rows.filter(needsCode);
125
+ const names = [...new Set(pending.map((r) => r.componentName))];
126
+ const candidates = new Map();
127
+ await runWithConcurrency(names, PAGE_CONCURRENCY, async (name) => {
128
+ candidates.set(name, await candidatesFor(name, lookup).catch(() => []));
129
+ });
130
+ const quotable = [...new Set([...candidates.values()].flat().filter((c) => ["stock", "dr"].includes(c.category) && QUOTABLE.test(c.code)).map((c) => c.code))];
131
+ const prices = new Map((await runWithConcurrency(inBatches(quotable, QUOTE_BATCH), PAGE_CONCURRENCY, (batch) => splitting(batch, (codes) => lookup.quotes(codes)))).flatMap((quoted) => [...quoted]));
132
+ const isQuotable = (c) => ["stock", "dr"].includes(c.category) && QUOTABLE.test(c.code);
133
+ const codes = new Map();
134
+ const etfs = new Map();
135
+ const place = (row, code, etf) => {
136
+ codes.set(row, code);
137
+ if (code)
138
+ etf.used.add(code);
139
+ };
140
+ for (const fundCode of new Set(pending.map((r) => r.fundCode))) {
141
+ const own = pending.filter((r) => r.fundCode === fundCode);
142
+ const etf = { exchange: exchange(String(fundCode)), used: new Set() };
143
+ etfs.set(fundCode, { own, etf });
144
+ for (const row of own) {
145
+ const code = byPrice(row, candidates.get(row.componentName) ?? [], prices, etf, isQuotable);
146
+ if (code !== undefined)
147
+ place(row, code, etf);
148
+ }
149
+ }
150
+ const open = (own) => own.filter((r) => !codes.has(r) && typeof r.close === "number" && (candidates.get(r.componentName) ?? []).some(isQuotable));
151
+ const lagging = [...etfs.values()].filter(({ own }) => open(own).length);
152
+ if (lagging.length && lookup.closes) {
153
+ const wanted = [...new Set(lagging.flatMap(({ own }) => own.flatMap((r) => (candidates.get(r.componentName) ?? []).filter(isQuotable).map((c) => c.code))))];
154
+ const end = beijingDateString(new Date());
155
+ const start = beijingDateString(new Date(Date.now() - CLOSES_WINDOW_DAYS * 86_400_000));
156
+ const history = new Map((await runWithConcurrency(inBatches(wanted, CLOSES_BATCH), PAGE_CONCURRENCY, (batch) => splitting(batch, (codes) => lookup.closes(codes, start, end)))).flatMap((part) => [...part]));
157
+ for (const { own, etf } of lagging) {
158
+ const dates = listDates(own, candidates, history);
159
+ const onDate = closesOn(dates, history);
160
+ for (const row of open(own)) {
161
+ const unchecked = (c) => isQuotable(c) && (history.get(c.code)?.some(([, close]) => agrees(row.close, close)) ?? true);
162
+ const code = byPrice(row, candidates.get(row.componentName) ?? [], onDate, etf, unchecked);
163
+ if (code !== undefined)
164
+ place(row, code, etf);
165
+ }
166
+ }
167
+ }
168
+ for (const { own, etf } of etfs.values()) {
169
+ const markets = new Set([...etf.used].map(market));
170
+ for (const r of own) {
171
+ const held = new Set((candidates.get(r.componentName) ?? []).filter((c) => !c.prefix).map((c) => market(c.code)));
172
+ if (held.size === 1)
173
+ markets.add([...held][0]);
174
+ }
175
+ if (markets.size === 1)
176
+ etf.market = [...markets][0];
177
+ for (const row of own)
178
+ if (!codes.has(row))
179
+ place(row, byName(candidates.get(row.componentName) ?? [], etf), etf);
180
+ }
181
+ const unresolved = [];
182
+ const filled = rows.map((row) => {
183
+ if (!codes.has(row))
184
+ return row;
185
+ const code = codes.get(row) ?? null;
186
+ if (!code)
187
+ unresolved.push({ fundCode: row.fundCode, serialNumber: row.serialNumber, componentName: row.componentName });
188
+ const out = {};
189
+ for (const [key, value] of Object.entries(row)) {
190
+ if (key === "componentCode")
191
+ continue;
192
+ out[key] = value;
193
+ if (key === "serialNumber")
194
+ out.componentCode = code;
195
+ }
196
+ if (!("componentCode" in out))
197
+ out.componentCode = code;
198
+ return out;
199
+ });
200
+ return { rows: filled, unresolved };
201
+ }
202
+ export async function addComponentCodes(data, lookup) {
203
+ const list = data?.list;
204
+ if (!Array.isArray(list))
205
+ return data;
206
+ const rows = list;
207
+ const pending = rows.filter(needsCode).length;
208
+ if (!pending)
209
+ return data;
210
+ const filled = await fillComponentCodes(rows, lookup);
211
+ const left = filled.unresolved.map((u) => `${String(u.fundCode)} ${String(u.componentName)}`);
212
+ const shown = left.length > 20 ? `${left.slice(0, 20).join(", ")}, …` : left.join(", ");
213
+ process.stderr.write(`[gangtise] note: the API returns component names only; componentCode is the CLI's match for each name (looked up in the securities list, and checked against the component's close where it can be quoted): ${pending - left.length} of ${pending} rows.${left.length ? ` Left null (cash lines, commodities, names without a confident match): ${shown}` : ""}\n`);
214
+ return { ...data, list: filled.rows };
215
+ }
216
+ const MARKET_SECTORS = ["1000000002", "1000002615", "1000002618"];
217
+ export async function loadSecurityTable(fetchSector) {
218
+ try {
219
+ const markets = await Promise.all(MARKET_SECTORS.map(async (sectorId) => {
220
+ const data = await fetchSector(sectorId);
221
+ const list = (Array.isArray(data?.list) ? data.list : []).flatMap((row) => {
222
+ const r = row;
223
+ return typeof r.gtsCode === "string" && typeof r.gtsName === "string" ? [[r.gtsCode, r.gtsName]] : [];
224
+ });
225
+ if (!list.length)
226
+ throw new Error(`sector ${sectorId} listed nothing`);
227
+ return list;
228
+ }));
229
+ return markets.flat();
230
+ }
231
+ catch {
232
+ return undefined;
233
+ }
234
+ }
235
+ export function tableSearch(list) {
236
+ const byBase = new Map();
237
+ const byTicker = new Map();
238
+ const byCode = new Map();
239
+ const add = (index, key, listing) => {
240
+ const listed = index.get(key);
241
+ if (listed)
242
+ listed.push(listing);
243
+ else
244
+ index.set(key, [listing]);
245
+ };
246
+ for (const listing of list) {
247
+ add(byBase, base(listing[1]), listing);
248
+ add(byTicker, ticker(listing[0]), listing);
249
+ byCode.set(listing[0], listing);
250
+ }
251
+ const bases = [...byBase.keys()];
252
+ return (keyword) => {
253
+ const key = squash(keyword);
254
+ const name = base(key);
255
+ const coded = byCode.get(key);
256
+ const exact = [...(byBase.get(name) ?? []), ...(byTicker.get(key) ?? []), ...(coded ? [coded] : [])];
257
+ if (!exact.length)
258
+ return undefined;
259
+ const prefix = name.length >= 2 ? bases.filter((b) => b !== name && b.startsWith(name)).flatMap((b) => byBase.get(b)) : [];
260
+ return [...new Set([...exact, ...prefix])].map(([gtsCode, gtsName]) => ({ gtsCode, gtsName, category: "stock" }));
261
+ };
262
+ }
263
+ export function clientLookup(client) {
264
+ let table;
265
+ const local = () => (table ??= loadSecurityTable((sectorId) => client.call("reference.sector-constituents", { sectorId })).then((list) => list && tableSearch(list)));
266
+ return {
267
+ async search(keyword) {
268
+ const hits = (await local())?.(keyword);
269
+ if (hits)
270
+ return hits;
271
+ const data = await client.call("reference.securities-search", { keyword, top: 10 });
272
+ return Array.isArray(data?.list) ? data.list : [];
273
+ },
274
+ async closes(codes, startDate, endDate) {
275
+ const data = normalizeRows(await client.call("quote.day-kline", { securityList: codes, startDate, endDate, fieldList: ["securityCode", "tradeDate", "close"], limit: 10_000 }));
276
+ const list = Array.isArray(data) ? data : (data?.list ?? []);
277
+ const out = new Map();
278
+ for (const k of list) {
279
+ if (typeof k.securityCode !== "string" || typeof k.tradeDate !== "string" || typeof k.close !== "number")
280
+ continue;
281
+ const series = out.get(k.securityCode) ?? [];
282
+ series.push([k.tradeDate, k.close]);
283
+ out.set(k.securityCode, series);
284
+ }
285
+ for (const series of out.values())
286
+ series.sort((a, b) => (a[0] < b[0] ? -1 : 1));
287
+ return out;
288
+ },
289
+ async quotes(codes) {
290
+ const data = normalizeRows(await client.call("quote.realtime", { securityList: codes }));
291
+ const list = Array.isArray(data) ? data : (data?.list ?? []);
292
+ const out = new Map();
293
+ for (const q of list) {
294
+ const prices = [q.preClose, q.latestPrice].filter((p) => typeof p === "number");
295
+ if (typeof q.securityCode === "string" && prices.length)
296
+ out.set(q.securityCode, prices);
297
+ }
298
+ return out;
299
+ },
300
+ };
301
+ }
@@ -0,0 +1,94 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { isVersionNewer } from "./args.js";
5
+ import { PACKAGE_NAME, REGISTRY } from "./updateCheck.js";
6
+ export const SKILL_HOMES = [[".claude", "skills"], [".codex", "skills"], [".openclaw", "workspace", "skills"], [".hermes", "skills"]];
7
+ const SKILL_DIR = "gangtise-openapi";
8
+ const runNpm = (args, inherit) => {
9
+ const options = { encoding: "utf8", stdio: inherit ? "inherit" : "pipe" };
10
+ const result = process.platform === "win32" ? spawnSync(["npm", ...args].join(" "), { ...options, shell: true }) : spawnSync("npm", args, options);
11
+ return { status: result.status, stdout: result.stdout ?? "" };
12
+ };
13
+ function isSymlink(file) {
14
+ try {
15
+ return fs.lstatSync(file).isSymbolicLink();
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ function samePath(a, b) {
22
+ try {
23
+ return fs.realpathSync(a) === fs.realpathSync(b);
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ }
29
+ export function installVersion(version, packageRoot, npm = runNpm) {
30
+ if (!/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(version))
31
+ throw new Error(`The registry answered an unexpected version (${JSON.stringify(version)}); nothing was installed.`);
32
+ const root = npm(["root", "-g"], false);
33
+ const globalRoot = root.status === 0 ? root.stdout.trim() : "";
34
+ if (!globalRoot)
35
+ throw new Error("Could not run npm, which gangtise update uses to install. Install Node.js with npm, or update gangtise the way it was installed.");
36
+ const installed = path.join(globalRoot, PACKAGE_NAME);
37
+ if (!samePath(installed, packageRoot)) {
38
+ throw new Error(`This gangtise runs from ${packageRoot}, which is not npm's global install (${globalRoot}). Update it the way it was installed — in a clone: git pull && npm install && npm run build.`);
39
+ }
40
+ if (isSymlink(installed)) {
41
+ throw new Error(`This gangtise is a checkout linked into npm's global install (${installed} → ${packageRoot}, npm link). Update the checkout instead: git pull && npm install && npm run build.`);
42
+ }
43
+ const spec = `${PACKAGE_NAME}@${version}`;
44
+ const install = npm(["install", "-g", spec, `--registry=${REGISTRY}`], true);
45
+ if (install.status !== 0) {
46
+ throw new Error(`npm install exited with ${install.status ?? "a signal"}. On a permission error (EACCES), run: sudo npm install -g ${spec} --registry=${REGISTRY} — then gangtise update again to refresh the Agent Skill copies.`);
47
+ }
48
+ const landed = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8")).version;
49
+ if (landed !== version)
50
+ throw new Error(`npm reported success, but ${packageRoot} still holds ${landed}.`);
51
+ }
52
+ function skillVersion(dir) {
53
+ try {
54
+ return /^version:\s*["']?([^"'\n]+)/m.exec(fs.readFileSync(path.join(dir, "SKILL.md"), "utf8"))?.[1];
55
+ }
56
+ catch {
57
+ return undefined;
58
+ }
59
+ }
60
+ export function refreshSkillCopies(packageRoot, home) {
61
+ const source = path.join(packageRoot, SKILL_DIR);
62
+ const version = skillVersion(source);
63
+ if (!version)
64
+ return [];
65
+ const replaced = [];
66
+ for (const parts of SKILL_HOMES) {
67
+ const target = path.join(home, ...parts, SKILL_DIR);
68
+ let stat;
69
+ try {
70
+ stat = fs.lstatSync(target);
71
+ }
72
+ catch {
73
+ continue;
74
+ }
75
+ if (!stat.isDirectory())
76
+ continue;
77
+ const current = skillVersion(target);
78
+ if (current !== undefined && !isVersionNewer(version, current))
79
+ continue;
80
+ const staging = `${target}.update-${process.pid}`;
81
+ fs.rmSync(staging, { recursive: true, force: true });
82
+ try {
83
+ fs.cpSync(source, staging, { recursive: true });
84
+ fs.rmSync(target, { recursive: true, force: true });
85
+ fs.renameSync(staging, target);
86
+ }
87
+ catch (error) {
88
+ fs.rmSync(staging, { recursive: true, force: true });
89
+ throw new Error(`Could not replace the Agent Skill copy at ${target} (${error.message}). Copy ${source} there again.`);
90
+ }
91
+ replaced.push(`Agent Skill updated: ${target} (${current ?? "no version"} → ${version})`);
92
+ }
93
+ return replaced;
94
+ }
@@ -1,36 +1,47 @@
1
+ import { spawn } from "node:child_process";
1
2
  import { mkdirSync, readFileSync } from "node:fs";
2
3
  import os from "node:os";
3
4
  import path from "node:path";
4
5
  import { isVersionNewer } from "./args.js";
5
6
  import { writeFileAtomicSync } from "./output.js";
6
7
  export const UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1000;
8
+ export const REFRESH_RETRY_MS = 60 * 60 * 1000;
7
9
  export const DEFAULT_UPDATE_CHECK_PATH = path.join(os.homedir(), ".config", "gangtise", "update-check.json");
8
- function saveLatest(cachePath, latest, now) {
10
+ export const REGISTRY = "https://registry.npmjs.org";
11
+ export const PACKAGE_NAME = "gangtise-openapi-cli";
12
+ function readFile(cachePath) {
13
+ try {
14
+ const kept = JSON.parse(readFileSync(cachePath, "utf8"));
15
+ if (kept && typeof kept === "object")
16
+ return kept;
17
+ }
18
+ catch { }
19
+ return {};
20
+ }
21
+ const answerIn = (kept) => typeof kept.checkedAt === "number" && typeof kept.latest === "string" ? kept : undefined;
22
+ const isFresh = (kept, now) => now - kept.checkedAt >= 0 && now - kept.checkedAt < UPDATE_CHECK_TTL_MS;
23
+ function keep(cachePath, kept) {
9
24
  try {
10
25
  mkdirSync(path.dirname(cachePath), { recursive: true });
11
- writeFileAtomicSync(cachePath, JSON.stringify({ checkedAt: now, latest }), { suffix: "tmp" });
26
+ writeFileAtomicSync(cachePath, JSON.stringify(kept), { suffix: "tmp" });
12
27
  }
13
28
  catch {
14
29
  }
15
30
  }
16
- export async function latestVersion(cachePath = DEFAULT_UPDATE_CHECK_PATH, timeoutMs = 2000, now = Date.now()) {
31
+ export async function latestVersion(cachePath = DEFAULT_UPDATE_CHECK_PATH, timeoutMs = 2000, now = Date.now(), fresh = false) {
17
32
  const cacheable = path.isAbsolute(cachePath);
18
- if (cacheable) {
19
- try {
20
- const cached = JSON.parse(readFileSync(cachePath, "utf8"));
21
- const age = typeof cached.checkedAt === "number" ? now - cached.checkedAt : -1;
22
- if (typeof cached.latest === "string" && age >= 0 && age < UPDATE_CHECK_TTL_MS)
23
- return cached.latest;
24
- }
25
- catch { }
33
+ if (cacheable && !fresh) {
34
+ const kept = answerIn(readFile(cachePath));
35
+ if (kept && isFresh(kept, now))
36
+ return kept.latest;
26
37
  }
27
38
  try {
28
- const response = await fetch("https://registry.npmjs.org/gangtise-openapi-cli/latest", { signal: AbortSignal.timeout(timeoutMs) });
39
+ const response = await fetch(`${REGISTRY}/${PACKAGE_NAME}/latest`, { signal: AbortSignal.timeout(timeoutMs) });
29
40
  const latest = (await response.json()).version;
30
41
  if (typeof latest !== "string")
31
42
  return undefined;
32
43
  if (cacheable)
33
- saveLatest(cachePath, latest, now);
44
+ keep(cachePath, { checkedAt: now, latest });
34
45
  return latest;
35
46
  }
36
47
  catch {
@@ -40,6 +51,31 @@ export async function latestVersion(cachePath = DEFAULT_UPDATE_CHECK_PATH, timeo
40
51
  export async function checkForUpdate(current) {
41
52
  const latest = await latestVersion();
42
53
  if (latest && isVersionNewer(latest, current)) {
43
- process.stderr.write(`Update available: ${current} → ${latest}\nRun: npm update -g gangtise-openapi-cli\n`);
54
+ process.stderr.write(`Update available: ${current} → ${latest}\nRun: gangtise update\n`);
55
+ }
56
+ }
57
+ function refreshInBackground(cachePath) {
58
+ try {
59
+ const script = `import(${JSON.stringify(import.meta.url)}).then((m) => m.latestVersion(${JSON.stringify(cachePath)}, 10000, Date.now(), true))`;
60
+ const child = spawn(process.execPath, ["-e", script], { detached: true, stdio: "ignore", windowsHide: true });
61
+ child.on("error", () => { });
62
+ child.unref();
63
+ }
64
+ catch { }
65
+ }
66
+ export function noticeUpdate(current, cachePath = DEFAULT_UPDATE_CHECK_PATH, now = Date.now(), refresh = refreshInBackground) {
67
+ if (!path.isAbsolute(cachePath))
68
+ return;
69
+ const file = readFile(cachePath);
70
+ const kept = answerIn(file);
71
+ const started = file.refreshStartedAt;
72
+ if ((!kept || !isFresh(kept, now)) && !(typeof started === "number" && now - started >= 0 && now - started < REFRESH_RETRY_MS)) {
73
+ file.refreshStartedAt = now;
74
+ keep(cachePath, file);
75
+ refresh(cachePath);
44
76
  }
77
+ if (!kept || kept.notifiedAt !== undefined || !isVersionNewer(kept.latest, current))
78
+ return;
79
+ process.stderr.write(`Update available: ${current} → ${kept.latest}. Run: gangtise update\n`);
80
+ keep(cachePath, { ...file, notifiedAt: now });
45
81
  }
@@ -1,14 +1,14 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- function packageVersion() {
4
+ function findPackage() {
5
5
  let dir = path.dirname(fileURLToPath(import.meta.url));
6
6
  for (;;) {
7
7
  const file = path.join(dir, "package.json");
8
8
  if (fs.existsSync(file)) {
9
9
  const pkg = JSON.parse(fs.readFileSync(file, "utf8"));
10
10
  if (pkg.name === "gangtise-openapi-cli" && pkg.version)
11
- return pkg.version;
11
+ return { root: dir, version: pkg.version };
12
12
  }
13
13
  const parent = path.dirname(dir);
14
14
  if (parent === dir)
@@ -16,4 +16,6 @@ function packageVersion() {
16
16
  dir = parent;
17
17
  }
18
18
  }
19
- export const CLI_VERSION = packageVersion();
19
+ const found = findPackage();
20
+ export const PACKAGE_ROOT = found.root;
21
+ export const CLI_VERSION = found.version;