@hey-research-lab/check 0.0.0-stage → 0.1.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/CHANGELOG.md +49 -0
- package/LICENSE +21 -0
- package/README.md +408 -2
- package/assets/fonts/Geist-Bold.ttf +0 -0
- package/assets/fonts/Geist-Regular.ttf +0 -0
- package/assets/fonts/Geist-SemiBold.ttf +0 -0
- package/assets/fonts/OFL.txt +93 -0
- package/dist/cli.js +1629 -0
- package/package.json +72 -4
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1629 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/card/render.ts
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
|
+
import { createRequire } from "module";
|
|
6
|
+
import { dirname, join } from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
|
|
9
|
+
// src/card/svg.ts
|
|
10
|
+
var CARD_WIDTH = 1200;
|
|
11
|
+
var CARD_HEIGHT = 675;
|
|
12
|
+
var CARD_FONT_FAMILY = "Geist";
|
|
13
|
+
var COLOR = {
|
|
14
|
+
background: "#07080b",
|
|
15
|
+
frame: "#1b1f26",
|
|
16
|
+
rule: "#1f242c",
|
|
17
|
+
ink: "#f1f3f6",
|
|
18
|
+
secondary: "#a2a9b4",
|
|
19
|
+
lime: "#c2fb03",
|
|
20
|
+
amber: "#f2b84b"
|
|
21
|
+
};
|
|
22
|
+
function escapeXml(value) {
|
|
23
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
24
|
+
}
|
|
25
|
+
var NOT_DRAWN = /[\u0000-\u001F\u007F-\u009F\u00AD\u061C\u200B-\u200F\u202A-\u202E\u2060-\u2069\uFE00-\uFE0F\uFEFF]/g;
|
|
26
|
+
var DRAWN = /[ -~\u00A0-\u024F\u0259\u02B0-\u02FF\u0370-\u03FF\u0400-\u04FF\u1E00-\u1EFF\u2010-\u2027\u2030-\u205E\u20A0-\u20C0\u2100-\u214F\u2190-\u2199\u2212]/u;
|
|
27
|
+
function cardText(value) {
|
|
28
|
+
let out = "";
|
|
29
|
+
for (const ch of value.toWellFormed().replace(NOT_DRAWN, "")) if (DRAWN.test(ch)) out += ch;
|
|
30
|
+
return out.replace(/\s+/g, " ").trim();
|
|
31
|
+
}
|
|
32
|
+
function wrap(text2, maxWidth, size, weight, maxLines, measure, letterSpacing = 0) {
|
|
33
|
+
const fits = (s) => measure(s, size, weight, letterSpacing) <= maxWidth;
|
|
34
|
+
const words = text2.split(" ").filter(Boolean);
|
|
35
|
+
const lines = [];
|
|
36
|
+
let current = "";
|
|
37
|
+
let i = 0;
|
|
38
|
+
for (; i < words.length; i += 1) {
|
|
39
|
+
const word = words[i];
|
|
40
|
+
const next = current ? `${current} ${word}` : word;
|
|
41
|
+
if (fits(next)) {
|
|
42
|
+
current = next;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (current) {
|
|
46
|
+
lines.push(current);
|
|
47
|
+
current = "";
|
|
48
|
+
if (lines.length === maxLines) break;
|
|
49
|
+
}
|
|
50
|
+
if (fits(word)) current = word;
|
|
51
|
+
else {
|
|
52
|
+
let part = "";
|
|
53
|
+
for (const ch of word) {
|
|
54
|
+
if (fits(part + ch)) part += ch;
|
|
55
|
+
else {
|
|
56
|
+
lines.push(part);
|
|
57
|
+
if (lines.length === maxLines) break;
|
|
58
|
+
part = ch;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (lines.length === maxLines) break;
|
|
62
|
+
current = part;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (current && lines.length < maxLines) {
|
|
66
|
+
lines.push(current);
|
|
67
|
+
current = "";
|
|
68
|
+
}
|
|
69
|
+
const cut = i < words.length || current !== "";
|
|
70
|
+
if (cut && lines.length > 0) {
|
|
71
|
+
let last = lines[lines.length - 1];
|
|
72
|
+
while (last.length > 1 && !fits(`${last}\u2026`)) last = [...last].slice(0, -1).join("").trimEnd();
|
|
73
|
+
lines[lines.length - 1] = `${last.replace(/[\s,;:.·—-]+$/u, "")}\u2026`;
|
|
74
|
+
}
|
|
75
|
+
return lines;
|
|
76
|
+
}
|
|
77
|
+
function sizeToFit(text2, maxWidth, sizes, weight, measure, ls) {
|
|
78
|
+
for (const size of sizes) if (measure(text2, size, weight, ls(size)) <= maxWidth) return size;
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
function textEl(content, o) {
|
|
82
|
+
const anchor = o.anchor === "end" ? ' text-anchor="end"' : "";
|
|
83
|
+
const ls = o.letterSpacing ? ` letter-spacing="${o.letterSpacing}"` : "";
|
|
84
|
+
return `<text x="${o.x}" y="${o.y}" font-family="${CARD_FONT_FAMILY}" font-size="${o.size}" font-weight="${o.weight}" fill="${o.fill}"${anchor}${ls} xml:space="preserve">${escapeXml(content)}</text>`;
|
|
85
|
+
}
|
|
86
|
+
var PAD = 80;
|
|
87
|
+
var RIGHT = CARD_WIDTH - PAD;
|
|
88
|
+
var CONTENT = RIGHT - PAD;
|
|
89
|
+
function buildCardSvg(model, measure) {
|
|
90
|
+
const parts = [];
|
|
91
|
+
const titleLs = (s) => -Math.round(s * 0.03);
|
|
92
|
+
parts.push(
|
|
93
|
+
`<rect width="${CARD_WIDTH}" height="${CARD_HEIGHT}" fill="${COLOR.background}"/>`,
|
|
94
|
+
`<rect x="24" y="24" width="${CARD_WIDTH - 48}" height="${CARD_HEIGHT - 48}" rx="28" fill="none" stroke="${COLOR.frame}" stroke-width="2"/>`,
|
|
95
|
+
`<rect x="${PAD}" y="77" width="14" height="14" rx="3" fill="${COLOR.lime}"/>`,
|
|
96
|
+
textEl("HEY RESEARCH LAB \xB7 DEV CHECK", {
|
|
97
|
+
x: PAD + 28,
|
|
98
|
+
y: 91,
|
|
99
|
+
size: 22,
|
|
100
|
+
weight: 600,
|
|
101
|
+
fill: COLOR.secondary,
|
|
102
|
+
letterSpacing: 2.4
|
|
103
|
+
}),
|
|
104
|
+
textEl("Robinhood Chain", {
|
|
105
|
+
x: RIGHT,
|
|
106
|
+
y: 91,
|
|
107
|
+
size: 22,
|
|
108
|
+
weight: 400,
|
|
109
|
+
fill: COLOR.secondary,
|
|
110
|
+
anchor: "end"
|
|
111
|
+
})
|
|
112
|
+
);
|
|
113
|
+
const title = cardText(model.title) || "Untitled";
|
|
114
|
+
let y;
|
|
115
|
+
const one = sizeToFit(title, CONTENT, [92, 84, 76, 68], 700, measure, titleLs);
|
|
116
|
+
if (one !== null) {
|
|
117
|
+
y = 196;
|
|
118
|
+
parts.push(
|
|
119
|
+
textEl(title, {
|
|
120
|
+
x: PAD,
|
|
121
|
+
y,
|
|
122
|
+
size: one,
|
|
123
|
+
weight: 700,
|
|
124
|
+
fill: COLOR.ink,
|
|
125
|
+
letterSpacing: titleLs(one)
|
|
126
|
+
})
|
|
127
|
+
);
|
|
128
|
+
} else {
|
|
129
|
+
const lines = wrap(title, CONTENT, 64, 700, 2, measure, titleLs(64));
|
|
130
|
+
y = 176;
|
|
131
|
+
lines.forEach(
|
|
132
|
+
(line, i) => parts.push(
|
|
133
|
+
textEl(line, {
|
|
134
|
+
x: PAD,
|
|
135
|
+
y: y + i * 70,
|
|
136
|
+
size: 64,
|
|
137
|
+
weight: 700,
|
|
138
|
+
fill: COLOR.ink,
|
|
139
|
+
letterSpacing: titleLs(64)
|
|
140
|
+
})
|
|
141
|
+
)
|
|
142
|
+
);
|
|
143
|
+
y += (lines.length - 1) * 70;
|
|
144
|
+
}
|
|
145
|
+
const twoLineTitle = one === null;
|
|
146
|
+
const ticker = model.ticker ? cardText(model.ticker) : "";
|
|
147
|
+
const hasRow = ticker !== "" || model.pill !== null;
|
|
148
|
+
if (hasRow) {
|
|
149
|
+
y += 70;
|
|
150
|
+
let x = PAD;
|
|
151
|
+
if (ticker) {
|
|
152
|
+
const t = wrap(ticker, 360, 36, 600, 1, measure)[0] ?? "";
|
|
153
|
+
parts.push(textEl(t, { x, y, size: 36, weight: 600, fill: COLOR.secondary }));
|
|
154
|
+
x += measure(t, 36, 600) + 28;
|
|
155
|
+
}
|
|
156
|
+
if (model.pill) {
|
|
157
|
+
const pillLabel = cardText(model.pill.label);
|
|
158
|
+
const pillText = 26;
|
|
159
|
+
const pillW = measure(pillLabel, pillText, 600) + 30 + 26 + 26;
|
|
160
|
+
const pillH = 50;
|
|
161
|
+
const pillY = y - 37;
|
|
162
|
+
const lime = model.pill.tone === "lime";
|
|
163
|
+
const pillFill = lime ? "rgba(194,251,3,0.10)" : "rgba(162,169,180,0.08)";
|
|
164
|
+
const pillStroke = lime ? COLOR.lime : "#3a404a";
|
|
165
|
+
const pillInk = lime ? COLOR.lime : COLOR.secondary;
|
|
166
|
+
parts.push(
|
|
167
|
+
`<rect x="${x}" y="${pillY}" width="${Math.round(pillW)}" height="${pillH}" rx="${pillH / 2}" fill="${pillFill}" stroke="${pillStroke}" stroke-width="2"/>`,
|
|
168
|
+
`<circle cx="${x + 28}" cy="${pillY + pillH / 2}" r="7" fill="${pillInk}"/>`,
|
|
169
|
+
textEl(pillLabel, { x: x + 48, y: pillY + 34, size: pillText, weight: 600, fill: pillInk })
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
y += hasRow ? twoLineTitle ? 88 : 96 : 98;
|
|
174
|
+
const headline = cardText(model.headline);
|
|
175
|
+
const hSize = sizeToFit(headline, CONTENT, [62, 56, 50, 44], 700, measure, (s) => -Math.round(s * 0.02)) ?? 44;
|
|
176
|
+
const hLines = wrap(headline, CONTENT, hSize, 700, 1, measure, -Math.round(hSize * 0.02));
|
|
177
|
+
parts.push(
|
|
178
|
+
textEl(hLines[0] ?? "", {
|
|
179
|
+
x: PAD,
|
|
180
|
+
y,
|
|
181
|
+
size: hSize,
|
|
182
|
+
weight: 700,
|
|
183
|
+
fill: COLOR.ink,
|
|
184
|
+
letterSpacing: -Math.round(hSize * 0.02)
|
|
185
|
+
})
|
|
186
|
+
);
|
|
187
|
+
const ruleY = 562;
|
|
188
|
+
parts.push(
|
|
189
|
+
`<line x1="${PAD}" y1="${ruleY}" x2="${RIGHT}" y2="${ruleY}" stroke="${COLOR.rule}" stroke-width="2"/>`
|
|
190
|
+
);
|
|
191
|
+
const checked = cardText(model.checkedOn);
|
|
192
|
+
const checkedW = measure(checked, 22, 400);
|
|
193
|
+
const urlMax = CONTENT - checkedW - 32;
|
|
194
|
+
const url = cardText(model.footerUrl);
|
|
195
|
+
const urlSize = sizeToFit(url, urlMax, [26, 24, 22, 20], 600, measure, () => 0) ?? 20;
|
|
196
|
+
parts.push(
|
|
197
|
+
textEl(wrap(url, urlMax, urlSize, 600, 1, measure)[0] ?? "", {
|
|
198
|
+
x: PAD,
|
|
199
|
+
y: 606,
|
|
200
|
+
size: urlSize,
|
|
201
|
+
weight: 600,
|
|
202
|
+
fill: COLOR.ink
|
|
203
|
+
}),
|
|
204
|
+
textEl(checked, {
|
|
205
|
+
x: RIGHT,
|
|
206
|
+
y: 606,
|
|
207
|
+
size: 22,
|
|
208
|
+
weight: 400,
|
|
209
|
+
fill: COLOR.secondary,
|
|
210
|
+
anchor: "end"
|
|
211
|
+
}),
|
|
212
|
+
textEl("Builder evidence, not a safety verdict or investment advice.", {
|
|
213
|
+
x: PAD,
|
|
214
|
+
y: 640,
|
|
215
|
+
size: 22,
|
|
216
|
+
weight: 400,
|
|
217
|
+
fill: COLOR.secondary
|
|
218
|
+
})
|
|
219
|
+
);
|
|
220
|
+
const facts = model.facts.map(cardText).filter(Boolean);
|
|
221
|
+
const factsY = ruleY - 34;
|
|
222
|
+
const bottom = facts.length > 0 ? factsY - 44 : ruleY - 22;
|
|
223
|
+
if (facts.length > 0) {
|
|
224
|
+
const joined = facts.join(" \xB7 ");
|
|
225
|
+
const fSize = sizeToFit(joined, CONTENT, [28, 26, 24], 400, measure, () => 0) ?? 22;
|
|
226
|
+
const line = wrap(joined, CONTENT, fSize, 400, 1, measure)[0] ?? "";
|
|
227
|
+
parts.push(textEl(line, { x: PAD, y: factsY, size: fSize, weight: 400, fill: COLOR.ink }));
|
|
228
|
+
}
|
|
229
|
+
if (model.notice) {
|
|
230
|
+
const top = y + 36;
|
|
231
|
+
const inner = CONTENT - 56;
|
|
232
|
+
const lineH = 36;
|
|
233
|
+
const foot = cardText(model.notice.foot);
|
|
234
|
+
const footH = foot ? 40 : 0;
|
|
235
|
+
const room = Math.max(1, Math.floor((bottom - top - 54 - footH) / lineH));
|
|
236
|
+
const body = wrap(cardText(model.notice.body), inner, 26, 400, Math.min(3, room), measure);
|
|
237
|
+
const boxH = 54 + body.length * lineH + footH + (foot ? 14 : 6);
|
|
238
|
+
parts.push(
|
|
239
|
+
`<rect x="${PAD}" y="${top}" width="${CONTENT}" height="${boxH}" rx="18" fill="rgba(242,184,75,0.08)" stroke="${COLOR.amber}" stroke-width="2"/>`,
|
|
240
|
+
textEl(cardText(model.notice.heading).toUpperCase(), {
|
|
241
|
+
x: PAD + 28,
|
|
242
|
+
y: top + 40,
|
|
243
|
+
size: 20,
|
|
244
|
+
weight: 700,
|
|
245
|
+
fill: COLOR.amber,
|
|
246
|
+
letterSpacing: 2
|
|
247
|
+
})
|
|
248
|
+
);
|
|
249
|
+
body.forEach(
|
|
250
|
+
(line, i) => parts.push(
|
|
251
|
+
textEl(line, {
|
|
252
|
+
x: PAD + 28,
|
|
253
|
+
y: top + 80 + i * lineH,
|
|
254
|
+
size: 26,
|
|
255
|
+
weight: 400,
|
|
256
|
+
fill: COLOR.ink
|
|
257
|
+
})
|
|
258
|
+
)
|
|
259
|
+
);
|
|
260
|
+
if (foot) {
|
|
261
|
+
parts.push(
|
|
262
|
+
textEl(wrap(foot, inner, 22, 400, 1, measure)[0] ?? "", {
|
|
263
|
+
x: PAD + 28,
|
|
264
|
+
y: top + 80 + (body.length - 1) * lineH + 40,
|
|
265
|
+
size: 22,
|
|
266
|
+
weight: 400,
|
|
267
|
+
fill: COLOR.secondary
|
|
268
|
+
})
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
} else if (model.detail) {
|
|
272
|
+
const detail = cardText(model.detail);
|
|
273
|
+
const first = y + 56;
|
|
274
|
+
const lineH = 42;
|
|
275
|
+
const room = Math.max(0, Math.floor((bottom - first) / lineH) + 1);
|
|
276
|
+
const lines = wrap(detail, CONTENT, 31, 400, Math.min(2, room), measure);
|
|
277
|
+
lines.forEach(
|
|
278
|
+
(line, i) => parts.push(
|
|
279
|
+
textEl(line, {
|
|
280
|
+
x: PAD,
|
|
281
|
+
y: first + i * lineH,
|
|
282
|
+
size: 31,
|
|
283
|
+
weight: 400,
|
|
284
|
+
fill: COLOR.secondary
|
|
285
|
+
})
|
|
286
|
+
)
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${CARD_WIDTH}" height="${CARD_HEIGHT}" viewBox="0 0 ${CARD_WIDTH} ${CARD_HEIGHT}">${parts.join("")}</svg>`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/card/render.ts
|
|
293
|
+
var FONT_FILES = ["Geist-Regular.ttf", "Geist-SemiBold.ttf", "Geist-Bold.ttf"];
|
|
294
|
+
function defaultFontDir() {
|
|
295
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
296
|
+
for (let i = 0; i < 4; i += 1) {
|
|
297
|
+
const candidate = join(dir, "assets", "fonts");
|
|
298
|
+
if (existsSync(join(candidate, FONT_FILES[0]))) return candidate;
|
|
299
|
+
dir = dirname(dir);
|
|
300
|
+
}
|
|
301
|
+
throw new Error("The card font (assets/fonts/Geist-*.ttf) is missing from this install.");
|
|
302
|
+
}
|
|
303
|
+
var resvgModule;
|
|
304
|
+
function loadResvg(load = createRequire(import.meta.url)) {
|
|
305
|
+
if (resvgModule) return resvgModule;
|
|
306
|
+
try {
|
|
307
|
+
resvgModule = load("@resvg/resvg-js");
|
|
308
|
+
} catch (error) {
|
|
309
|
+
const reason = error instanceof Error ? error.message.split("\n")[0] ?? "" : String(error);
|
|
310
|
+
throw new Error(
|
|
311
|
+
`the card renderer (@resvg/resvg-js) could not load on ${process.platform}-${process.arch}; run with --no-card for the text answer (${reason})`,
|
|
312
|
+
{ cause: error }
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
return resvgModule;
|
|
316
|
+
}
|
|
317
|
+
function loadFonts(fontDir = defaultFontDir()) {
|
|
318
|
+
const files = FONT_FILES.map((f) => join(fontDir, f));
|
|
319
|
+
for (const file of files) {
|
|
320
|
+
if (!existsSync(file)) throw new Error(`The card font ${file} is missing from this install.`);
|
|
321
|
+
}
|
|
322
|
+
return { files };
|
|
323
|
+
}
|
|
324
|
+
function options(fonts) {
|
|
325
|
+
return {
|
|
326
|
+
font: {
|
|
327
|
+
fontFiles: fonts.files,
|
|
328
|
+
loadSystemFonts: false,
|
|
329
|
+
defaultFontFamily: CARD_FONT_FAMILY
|
|
330
|
+
},
|
|
331
|
+
fitTo: { mode: "original" },
|
|
332
|
+
shapeRendering: 2,
|
|
333
|
+
textRendering: 1,
|
|
334
|
+
imageRendering: 0
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
var escapeForMeasure = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
338
|
+
function createMeasure(fonts) {
|
|
339
|
+
const cache = /* @__PURE__ */ new Map();
|
|
340
|
+
return (text2, size, weight, letterSpacing = 0) => {
|
|
341
|
+
const key = `${size}|${weight}|${letterSpacing}|${text2}`;
|
|
342
|
+
const hit = cache.get(key);
|
|
343
|
+
if (hit !== void 0) return hit;
|
|
344
|
+
const svg = (t) => `<svg xmlns="http://www.w3.org/2000/svg" width="8000" height="400"><text x="0" y="300" font-family="${CARD_FONT_FAMILY}" font-size="${size}" font-weight="${weight}" letter-spacing="${letterSpacing}" xml:space="preserve">${escapeForMeasure(t)}</text></svg>`;
|
|
345
|
+
const { Resvg } = loadResvg();
|
|
346
|
+
const width = (t) => new Resvg(svg(t), options(fonts)).getBBox()?.width ?? 0;
|
|
347
|
+
const bars = width("||");
|
|
348
|
+
const value = text2 === "" ? 0 : Math.max(0, width(`|${text2}|`) - bars);
|
|
349
|
+
cache.set(key, value);
|
|
350
|
+
return value;
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
function renderPng(svg, fonts) {
|
|
354
|
+
const { Resvg } = loadResvg();
|
|
355
|
+
return new Resvg(svg, options(fonts)).render().asPng();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/run.ts
|
|
359
|
+
import { isAbsolute, relative } from "path";
|
|
360
|
+
import { parseArgs } from "util";
|
|
361
|
+
|
|
362
|
+
// src/chain.ts
|
|
363
|
+
var CHAIN_NAME = "Robinhood Chain";
|
|
364
|
+
var CHAIN_ID = 4663;
|
|
365
|
+
var CAIP2 = "eip155:4663";
|
|
366
|
+
|
|
367
|
+
// src/exit-codes.ts
|
|
368
|
+
var EXIT = {
|
|
369
|
+
/** HEY answered: a published project, with its builder evidence. */
|
|
370
|
+
OK: 0,
|
|
371
|
+
/** The answer printed, but the card could not be written (bad path, symlink, no permission). */
|
|
372
|
+
CARD_FAILED: 1,
|
|
373
|
+
/** Usage error: a bad flag, input that is not an address/ticker/name/handle, `unsupported_chain`. */
|
|
374
|
+
USAGE: 2,
|
|
375
|
+
/** HEY refused the request (an `invalid_request` other than `ambiguous`, a 4xx other than 404/429). */
|
|
376
|
+
REFUSED: 3,
|
|
377
|
+
/** HEY holds no published project for the input (`not_found`). */
|
|
378
|
+
NOT_FOUND: 4,
|
|
379
|
+
/** Rate limited (429): retry after the printed delay. */
|
|
380
|
+
RATE_LIMITED: 5,
|
|
381
|
+
/** Network failure, timeout, redirect, an unreadable or oversized answer, or a HEY 5xx. */
|
|
382
|
+
NETWORK: 6,
|
|
383
|
+
/** More than one published project matches: HEY lists them and does not guess; pick one. */
|
|
384
|
+
AMBIGUOUS: 7
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
// src/errors.ts
|
|
388
|
+
var CheckError = class extends Error {
|
|
389
|
+
constructor(code2, message, exitCode2, details = {}) {
|
|
390
|
+
super(message);
|
|
391
|
+
this.code = code2;
|
|
392
|
+
this.exitCode = exitCode2;
|
|
393
|
+
this.details = details;
|
|
394
|
+
this.name = "CheckError";
|
|
395
|
+
}
|
|
396
|
+
code;
|
|
397
|
+
exitCode;
|
|
398
|
+
details;
|
|
399
|
+
};
|
|
400
|
+
var usageError = (code2, message) => new CheckError(code2, message, EXIT.USAGE);
|
|
401
|
+
var networkError = (code2, message, details = {}) => new CheckError(code2, message, EXIT.NETWORK, { retryable: true, ...details });
|
|
402
|
+
|
|
403
|
+
// src/evm.ts
|
|
404
|
+
var ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
405
|
+
var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
406
|
+
var DEAD_ADDRESS = "0x000000000000000000000000000000000000dead";
|
|
407
|
+
|
|
408
|
+
// src/version.ts
|
|
409
|
+
var CHECK_VERSION = true ? "0.1.0" : "0.0.0-dev";
|
|
410
|
+
|
|
411
|
+
// src/http.ts
|
|
412
|
+
var DEFAULT_BASE_URL = "https://heyresearch.xyz";
|
|
413
|
+
var MAX_RESPONSE_BYTES = 1024 * 1024;
|
|
414
|
+
var TIMEOUT_MS = 15e3;
|
|
415
|
+
var MAX_REQUESTS = 2;
|
|
416
|
+
var USER_AGENT = `hey-check/${CHECK_VERSION} (+https://github.com/hey-research-lab/hey-check)`;
|
|
417
|
+
var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
418
|
+
function forbiddenKeyPath(value, path = "$", depth = 0) {
|
|
419
|
+
if (depth > 64) return path;
|
|
420
|
+
if (Array.isArray(value)) {
|
|
421
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
422
|
+
const hit = forbiddenKeyPath(value[i], `${path}[${i}]`, depth + 1);
|
|
423
|
+
if (hit) return hit;
|
|
424
|
+
}
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
if (value && typeof value === "object") {
|
|
428
|
+
for (const key of Object.keys(value)) {
|
|
429
|
+
if (FORBIDDEN_KEYS.has(key)) return `${path}.${key}`;
|
|
430
|
+
const hit = forbiddenKeyPath(
|
|
431
|
+
value[key],
|
|
432
|
+
`${path}.${key}`,
|
|
433
|
+
depth + 1
|
|
434
|
+
);
|
|
435
|
+
if (hit) return hit;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
async function readCapped(response, max) {
|
|
441
|
+
const declared = Number(response.headers.get("content-length"));
|
|
442
|
+
if (Number.isFinite(declared) && declared > max) throw tooLarge(max);
|
|
443
|
+
if (!response.body) return "";
|
|
444
|
+
const reader = response.body.getReader();
|
|
445
|
+
const chunks = [];
|
|
446
|
+
let total = 0;
|
|
447
|
+
for (; ; ) {
|
|
448
|
+
const { done, value } = await reader.read();
|
|
449
|
+
if (done) break;
|
|
450
|
+
total += value.byteLength;
|
|
451
|
+
if (total > max) {
|
|
452
|
+
await reader.cancel().catch(() => void 0);
|
|
453
|
+
throw tooLarge(max);
|
|
454
|
+
}
|
|
455
|
+
chunks.push(value);
|
|
456
|
+
}
|
|
457
|
+
return new TextDecoder("utf-8", { fatal: false }).decode(Buffer.concat(chunks));
|
|
458
|
+
}
|
|
459
|
+
var tooLarge = (max) => new CheckError(
|
|
460
|
+
"response_too_large",
|
|
461
|
+
`HEY's answer was larger than ${max} bytes; hey-check does not read it.`,
|
|
462
|
+
EXIT.NETWORK,
|
|
463
|
+
{ retryable: false }
|
|
464
|
+
);
|
|
465
|
+
function parseBaseUrl(raw) {
|
|
466
|
+
let url;
|
|
467
|
+
try {
|
|
468
|
+
url = new URL(raw);
|
|
469
|
+
} catch {
|
|
470
|
+
throw new CheckError("invalid_base_url", "The base URL is not a URL.", EXIT.USAGE);
|
|
471
|
+
}
|
|
472
|
+
const local = url.protocol === "http:" && ["localhost", "127.0.0.1"].includes(url.hostname);
|
|
473
|
+
const bare = url.pathname === "/" && !url.search && !url.hash && !raw.includes("#");
|
|
474
|
+
if (url.protocol !== "https:" && !local || url.username || url.password || !bare) {
|
|
475
|
+
throw new CheckError(
|
|
476
|
+
"invalid_base_url",
|
|
477
|
+
"The base URL must be an https origin (or http on localhost) with no path or query.",
|
|
478
|
+
EXIT.USAGE
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
return url.origin;
|
|
482
|
+
}
|
|
483
|
+
var HeyHttp = class {
|
|
484
|
+
constructor(fetchImpl, baseUrl = DEFAULT_BASE_URL) {
|
|
485
|
+
this.fetchImpl = fetchImpl;
|
|
486
|
+
this.baseUrl = baseUrl;
|
|
487
|
+
}
|
|
488
|
+
fetchImpl;
|
|
489
|
+
baseUrl;
|
|
490
|
+
requests = [];
|
|
491
|
+
async getJson(path, params) {
|
|
492
|
+
if (this.requests.length >= MAX_REQUESTS) {
|
|
493
|
+
throw new CheckError(
|
|
494
|
+
"request_budget",
|
|
495
|
+
"hey-check makes at most two requests a run.",
|
|
496
|
+
EXIT.NETWORK
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
const url = `${this.baseUrl}${path}?${new URLSearchParams(params).toString()}`;
|
|
500
|
+
const record = { url, status: null, requestId: null };
|
|
501
|
+
this.requests.push(record);
|
|
502
|
+
let response;
|
|
503
|
+
try {
|
|
504
|
+
response = await this.fetchImpl(url, {
|
|
505
|
+
method: "GET",
|
|
506
|
+
redirect: "manual",
|
|
507
|
+
credentials: "omit",
|
|
508
|
+
headers: {
|
|
509
|
+
accept: "application/json",
|
|
510
|
+
"user-agent": USER_AGENT,
|
|
511
|
+
"x-hey-integration": `hey-check/${CHECK_VERSION}`
|
|
512
|
+
},
|
|
513
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
514
|
+
});
|
|
515
|
+
} catch (error) {
|
|
516
|
+
const name = error instanceof Error ? error.name : "";
|
|
517
|
+
if (name === "TimeoutError" || name === "AbortError") {
|
|
518
|
+
throw networkError("timeout", `HEY did not answer within ${TIMEOUT_MS / 1e3} seconds.`);
|
|
519
|
+
}
|
|
520
|
+
throw networkError("network", "Could not reach heyresearch.xyz. Check your connection.");
|
|
521
|
+
}
|
|
522
|
+
record.status = response.status;
|
|
523
|
+
record.requestId = response.headers.get("x-request-id");
|
|
524
|
+
const requestId = record.requestId ?? void 0;
|
|
525
|
+
const details = requestId ? { requestId } : {};
|
|
526
|
+
if (response.status === 0 || response.status >= 300 && response.status < 400) {
|
|
527
|
+
throw networkError("redirect", "HEY answered with a redirect; hey-check follows none.", {
|
|
528
|
+
...details,
|
|
529
|
+
retryable: false
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
if (response.status === 429) {
|
|
533
|
+
const header = Number(response.headers.get("retry-after"));
|
|
534
|
+
const retryAfterSeconds = Number.isFinite(header) && header > 0 ? Math.ceil(header) : 60;
|
|
535
|
+
throw new CheckError(
|
|
536
|
+
"rate_limited",
|
|
537
|
+
`HEY is rate limiting requests from here. Wait ${retryAfterSeconds} seconds and try again.`,
|
|
538
|
+
EXIT.RATE_LIMITED,
|
|
539
|
+
{ ...details, retryable: true, retryAfterSeconds }
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
if (response.status >= 500) {
|
|
543
|
+
throw networkError(
|
|
544
|
+
"server_error",
|
|
545
|
+
`HEY answered ${response.status}. Try again shortly.`,
|
|
546
|
+
details
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
const text2 = await readCapped(response, MAX_RESPONSE_BYTES);
|
|
550
|
+
let body;
|
|
551
|
+
try {
|
|
552
|
+
body = JSON.parse(text2);
|
|
553
|
+
} catch {
|
|
554
|
+
throw networkError("invalid_response", `HEY's answer (${response.status}) was not JSON.`, {
|
|
555
|
+
...details,
|
|
556
|
+
retryable: false
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
const bad = forbiddenKeyPath(body);
|
|
560
|
+
if (bad) {
|
|
561
|
+
throw networkError("invalid_response", `HEY's answer carries a forbidden key at ${bad}.`, {
|
|
562
|
+
...details,
|
|
563
|
+
retryable: false
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
return { status: response.status, body, requestId: record.requestId };
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
// src/input.ts
|
|
571
|
+
var MAX_INPUT_LENGTH = 200;
|
|
572
|
+
var HANDLE = /^[A-Za-z0-9_]{1,15}$/;
|
|
573
|
+
var TICKER = /^\$[A-Za-z0-9][A-Za-z0-9._-]{0,19}$/;
|
|
574
|
+
var NAME = /^[\p{L}\p{N}][\p{L}\p{N} ._'&+-]{0,79}$/u;
|
|
575
|
+
var X_RESERVED = /* @__PURE__ */ new Set([
|
|
576
|
+
"home",
|
|
577
|
+
"i",
|
|
578
|
+
"intent",
|
|
579
|
+
"share",
|
|
580
|
+
"search",
|
|
581
|
+
"explore",
|
|
582
|
+
"hashtag",
|
|
583
|
+
"settings",
|
|
584
|
+
"messages",
|
|
585
|
+
"notifications",
|
|
586
|
+
"login",
|
|
587
|
+
"signup",
|
|
588
|
+
"tos",
|
|
589
|
+
"privacy"
|
|
590
|
+
]);
|
|
591
|
+
var X_HOSTS = /* @__PURE__ */ new Set([
|
|
592
|
+
"x.com",
|
|
593
|
+
"www.x.com",
|
|
594
|
+
"mobile.x.com",
|
|
595
|
+
"twitter.com",
|
|
596
|
+
"www.twitter.com",
|
|
597
|
+
"mobile.twitter.com"
|
|
598
|
+
]);
|
|
599
|
+
var HELP = "hey-check takes a Robinhood Chain contract address (0x + 40 hex), a $TICKER, a project name or slug, an @handle, or an x.com profile URL.";
|
|
600
|
+
var EMPTY_HINT = "A $TICKER must be quoted with single quotes in a shell ('$CHIT'): unquoted or in double quotes, the shell replaces it with nothing.";
|
|
601
|
+
var WRAPPED_IN_QUOTES = /^(['"\u2018\u2019\u201C\u201D]).+(['"\u2018\u2019\u201C\u201D])$/u;
|
|
602
|
+
function handleFromXUrl(raw) {
|
|
603
|
+
const withScheme = /^https?:\/\//i.test(raw) ? raw : `https://${raw}`;
|
|
604
|
+
let url;
|
|
605
|
+
try {
|
|
606
|
+
url = new URL(withScheme);
|
|
607
|
+
} catch {
|
|
608
|
+
throw usageError("invalid_input", `That is not a URL hey-check can read. ${HELP}`);
|
|
609
|
+
}
|
|
610
|
+
if (!X_HOSTS.has(url.hostname.toLowerCase()) || url.username || url.password || url.port) {
|
|
611
|
+
throw usageError(
|
|
612
|
+
"invalid_input",
|
|
613
|
+
`hey-check reads x.com and twitter.com profile URLs only. ${HELP}`
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
const first = url.pathname.split("/").filter(Boolean)[0] ?? "";
|
|
617
|
+
if (!HANDLE.test(first) || X_RESERVED.has(first.toLowerCase())) {
|
|
618
|
+
throw usageError("invalid_input", "That x.com URL does not name a profile (x.com/<handle>).");
|
|
619
|
+
}
|
|
620
|
+
return first;
|
|
621
|
+
}
|
|
622
|
+
function parseInput(value) {
|
|
623
|
+
if (value === void 0) throw usageError("missing_argument", `Nothing to check. ${HELP}`);
|
|
624
|
+
if (/[\u0000-\u001F\u007F-\u009F\u200B-\u200F\u202A-\u202E\u2066-\u2069]/.test(value)) {
|
|
625
|
+
throw usageError("invalid_input", "The input carries control or invisible characters.");
|
|
626
|
+
}
|
|
627
|
+
const raw = value.trim();
|
|
628
|
+
if (raw === "") throw usageError("missing_argument", `Nothing to check. ${EMPTY_HINT}`);
|
|
629
|
+
if (raw.length > MAX_INPUT_LENGTH) {
|
|
630
|
+
throw usageError("invalid_input", `The input is longer than ${MAX_INPUT_LENGTH} characters.`);
|
|
631
|
+
}
|
|
632
|
+
if (WRAPPED_IN_QUOTES.test(raw)) {
|
|
633
|
+
throw usageError(
|
|
634
|
+
"invalid_input",
|
|
635
|
+
`The input arrived with its quotes (${raw}). Your shell passed them on as part of it \u2014 cmd.exe does \u2014 so leave them out there: hey-check ${raw.slice(1, -1)}`
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
const caip = /^eip155:(\d+):(.+)$/i.exec(raw);
|
|
639
|
+
if (caip) {
|
|
640
|
+
if (Number(caip[1]) !== CHAIN_ID || caip[1] !== String(CHAIN_ID)) {
|
|
641
|
+
throw usageError(
|
|
642
|
+
"unsupported_chain",
|
|
643
|
+
`HEY supports Robinhood Chain (4663) only; chain ${caip[1]} is not supported.`
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
return parseAddress(raw, caip[2] ?? "");
|
|
647
|
+
}
|
|
648
|
+
if (/^0x/i.test(raw)) return parseAddress(raw, raw);
|
|
649
|
+
if (/^(https?:\/\/)?([a-z0-9-]+\.)*(x|twitter)\.com(\/|$)/i.test(raw)) {
|
|
650
|
+
const handle = handleFromXUrl(raw);
|
|
651
|
+
return { kind: "x_profile", raw, q: `@${handle}` };
|
|
652
|
+
}
|
|
653
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(raw) || raw.includes("/")) {
|
|
654
|
+
throw usageError("invalid_input", `hey-check does not read that kind of URL. ${HELP}`);
|
|
655
|
+
}
|
|
656
|
+
if (raw.startsWith("@")) {
|
|
657
|
+
const handle = raw.slice(1);
|
|
658
|
+
if (!HANDLE.test(handle)) {
|
|
659
|
+
throw usageError("invalid_input", "An @handle is 1\u201315 letters, digits or underscores.");
|
|
660
|
+
}
|
|
661
|
+
return { kind: "handle", raw, q: `@${handle}` };
|
|
662
|
+
}
|
|
663
|
+
if (raw.startsWith("$")) {
|
|
664
|
+
if (!TICKER.test(raw)) {
|
|
665
|
+
throw usageError("invalid_input", "A $TICKER is $ followed by 1\u201320 letters or digits.");
|
|
666
|
+
}
|
|
667
|
+
return { kind: "ticker", raw, q: raw };
|
|
668
|
+
}
|
|
669
|
+
if (!NAME.test(raw)) throw usageError("invalid_input", `That does not look like a name. ${HELP}`);
|
|
670
|
+
return { kind: "name", raw, q: raw };
|
|
671
|
+
}
|
|
672
|
+
function parseAddress(raw, candidate) {
|
|
673
|
+
const normalised = /^0X/.test(candidate) ? `0x${candidate.slice(2)}` : candidate;
|
|
674
|
+
if (!ADDRESS_RE.test(normalised)) {
|
|
675
|
+
throw usageError(
|
|
676
|
+
"invalid_address",
|
|
677
|
+
"A contract address is 0x followed by exactly 40 hexadecimal characters."
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
const address2 = normalised.toLowerCase();
|
|
681
|
+
if (address2 === ZERO_ADDRESS || address2 === DEAD_ADDRESS) {
|
|
682
|
+
throw usageError(
|
|
683
|
+
"not_a_contract_identity",
|
|
684
|
+
"The zero and burn addresses are not a contract identity."
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
return { kind: "address", raw, address: address2 };
|
|
688
|
+
}
|
|
689
|
+
function queryFor(input) {
|
|
690
|
+
return input.kind === "address" ? { key: "address", value: input.address } : { key: "q", value: input.q };
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// src/text.ts
|
|
694
|
+
var CONTROL = /[\u0000-\u0008\u000B-\u001F\u007F-\u009F]/g;
|
|
695
|
+
var INVISIBLE = /[\u00AD\u061C\u180E\u200B-\u200F\u202A-\u202E\u2060-\u2064\u2066-\u2069\u3164\uFE00-\uFE0F\uFEFF\uFFA0\u{E0000}-\u{E007F}\u{E0100}-\u{E01EF}]/gu;
|
|
696
|
+
var WHITESPACE = /\s+/g;
|
|
697
|
+
function cleanText(value, max = 280) {
|
|
698
|
+
const folded = value.toWellFormed().replace(/[\t\n\r]/g, " ").replace(CONTROL, "").replace(INVISIBLE, "").replace(/\uFFFD/g, "").replace(WHITESPACE, " ").trim();
|
|
699
|
+
const chars = [...folded];
|
|
700
|
+
return chars.length > max ? `${chars.slice(0, max - 1).join("")}\u2026` : folded;
|
|
701
|
+
}
|
|
702
|
+
function cleanUrl(value) {
|
|
703
|
+
if (typeof value !== "string" || value.length > 600) return null;
|
|
704
|
+
const cleaned = cleanText(value, 600);
|
|
705
|
+
if (!/^https:\/\/\S+$/.test(cleaned)) return null;
|
|
706
|
+
try {
|
|
707
|
+
const url = new URL(cleaned);
|
|
708
|
+
if (url.protocol !== "https:" || url.username || url.password) return null;
|
|
709
|
+
return cleaned;
|
|
710
|
+
} catch {
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
function firstSentence(text2) {
|
|
715
|
+
const match = /^(.+?[.!?])(?=\s|$)/u.exec(text2);
|
|
716
|
+
return (match?.[1] ?? text2).trim();
|
|
717
|
+
}
|
|
718
|
+
var plainStyle = { strong: (s) => s, dim: (s) => s };
|
|
719
|
+
var ansiStyle = {
|
|
720
|
+
strong: (s) => `\x1B[1m${s}\x1B[22m`,
|
|
721
|
+
dim: (s) => `\x1B[2m${s}\x1B[22m`
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
// src/hey.ts
|
|
725
|
+
var RESEARCH_PATH = "/api/agent/research_project";
|
|
726
|
+
var SCAN_PATH = "/api/v1/scan";
|
|
727
|
+
var SLUG = /^[a-z0-9][a-z0-9-]{0,119}$/;
|
|
728
|
+
var obj = (v) => v !== null && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
729
|
+
var arr = (v) => Array.isArray(v) ? v : [];
|
|
730
|
+
var rawStr = (v) => typeof v === "string" ? v : null;
|
|
731
|
+
var text = (v, max = 280) => {
|
|
732
|
+
const s = rawStr(v) ?? rawStr(obj(v)?.text);
|
|
733
|
+
if (s === null) return null;
|
|
734
|
+
const cleaned = cleanText(s, max);
|
|
735
|
+
return cleaned === "" ? null : cleaned;
|
|
736
|
+
};
|
|
737
|
+
var code = (v) => typeof v === "string" && /^[A-Z][A-Z0-9_]{0,40}$/.test(v) ? v : null;
|
|
738
|
+
var lowerCode = (v) => typeof v === "string" && /^[a-z][a-z0-9_]{0,60}$/.test(v) ? v : null;
|
|
739
|
+
var instant = (v) => typeof v === "string" && /^\d{4}-\d{2}-\d{2}(T[\d:.]+(Z|[+-]\d{2}:?\d{2}))?$/.test(v) && !Number.isNaN(Date.parse(v)) ? v : null;
|
|
740
|
+
var address = (v) => typeof v === "string" && ADDRESS_RE.test(v) ? v.toLowerCase() : null;
|
|
741
|
+
var slug = (v) => typeof v === "string" && SLUG.test(v) ? v : null;
|
|
742
|
+
var count = (v) => typeof v === "number" && Number.isInteger(v) && v >= 0 ? v : null;
|
|
743
|
+
var evidenceId = (v) => typeof v === "string" && /^[a-z0-9]{2,20}:[A-Za-z0-9:._-]{1,200}$/.test(v) ? v : null;
|
|
744
|
+
function disclaimerOf(body) {
|
|
745
|
+
return text(obj(body.boundaries)?.disclaimer, 600);
|
|
746
|
+
}
|
|
747
|
+
function parseAnswered(body, input) {
|
|
748
|
+
const data = obj(body.data);
|
|
749
|
+
const identity = obj(data?.identity);
|
|
750
|
+
const subject = obj(obj(body.subject)?.project);
|
|
751
|
+
const projectSlug = slug(identity?.slug) ?? slug(subject?.slug);
|
|
752
|
+
if (!data || !identity || !projectSlug) {
|
|
753
|
+
throw networkError("invalid_response", "HEY's answer did not carry the project it names.", {
|
|
754
|
+
retryable: false
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
const page = cleanUrl(obj(body.links)?.page) ?? cleanUrl(identity.url) ?? `https://heyresearch.xyz/project/${projectSlug}`;
|
|
758
|
+
const state = obj(data.builderState);
|
|
759
|
+
const change = obj(data.latestMeaningfulChange);
|
|
760
|
+
const evidence = obj(arr(change?.evidence)[0]);
|
|
761
|
+
const token = obj(obj(data.contractIdentity)?.token);
|
|
762
|
+
const tokenAddress = address(token?.address);
|
|
763
|
+
const seen = /* @__PURE__ */ new Set();
|
|
764
|
+
const unknowns = [];
|
|
765
|
+
for (const entry of arr(body.unknowns)) {
|
|
766
|
+
const u = obj(entry);
|
|
767
|
+
const category = code(u?.category);
|
|
768
|
+
const statement = text(u?.statement);
|
|
769
|
+
if (!u || !category || !statement) continue;
|
|
770
|
+
const key = `${category}|${statement}`;
|
|
771
|
+
if (seen.has(key)) continue;
|
|
772
|
+
seen.add(key);
|
|
773
|
+
unknowns.push({
|
|
774
|
+
category,
|
|
775
|
+
dimension: rawStr(u.dimension) ? cleanText(String(u.dimension), 60) : null,
|
|
776
|
+
text: statement,
|
|
777
|
+
reason: lowerCode(u.reason)
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
const asked = input.kind === "address" ? input.address : null;
|
|
781
|
+
const answerText = text(body.answer, 1200);
|
|
782
|
+
return {
|
|
783
|
+
outcome: "answered",
|
|
784
|
+
answer: answerText ? firstSentence(answerText) : null,
|
|
785
|
+
project: {
|
|
786
|
+
slug: projectSlug,
|
|
787
|
+
name: text(identity.name, 120) ?? text(subject?.name, 120) ?? projectSlug,
|
|
788
|
+
symbol: text(identity.symbol, 24),
|
|
789
|
+
url: cleanUrl(identity.url) ?? page,
|
|
790
|
+
researchLevel: code(identity.researchLevel),
|
|
791
|
+
catalogStatus: code(identity.catalogStatus)
|
|
792
|
+
},
|
|
793
|
+
builder: {
|
|
794
|
+
activityStatus: code(state?.activityStatus) ?? "UNKNOWN",
|
|
795
|
+
activityMeasured: typeof state?.activityMeasured === "boolean" ? state.activityMeasured : null,
|
|
796
|
+
lastMeaningfulShipAt: instant(state?.lastMeaningfulShipAt),
|
|
797
|
+
meaningfulEvents30d: count(state?.meaningfulEvents30d)
|
|
798
|
+
},
|
|
799
|
+
latestShip: change ? {
|
|
800
|
+
id: evidenceId(change.id),
|
|
801
|
+
title: text(change.summary, 200),
|
|
802
|
+
occurredAt: instant(change.occurredAt),
|
|
803
|
+
precision: code(change.precision),
|
|
804
|
+
sourceUrl: cleanUrl(evidence?.sourceUrl) ?? cleanUrl(obj(change.summary)?.sourceUrl),
|
|
805
|
+
receiptUrl: cleanUrl(evidence?.receiptUrl)
|
|
806
|
+
} : null,
|
|
807
|
+
contract: tokenAddress ? { address: tokenAddress, verification: code(token?.verification) } : null,
|
|
808
|
+
askedAddress: asked && asked !== tokenAddress ? asked : null,
|
|
809
|
+
unknowns,
|
|
810
|
+
citation: text(obj(body.citation)?.text, 400),
|
|
811
|
+
page,
|
|
812
|
+
asOf: instant(body.asOf),
|
|
813
|
+
disclaimer: disclaimerOf(body)
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
function parseAmbiguous(body, error) {
|
|
817
|
+
const candidates = [];
|
|
818
|
+
for (const entry of arr(error.candidates)) {
|
|
819
|
+
const c = obj(entry);
|
|
820
|
+
const s = slug(c?.slug);
|
|
821
|
+
if (!c || !s) continue;
|
|
822
|
+
const token = obj(c.token);
|
|
823
|
+
candidates.push({
|
|
824
|
+
slug: s,
|
|
825
|
+
name: text(c.name, 120),
|
|
826
|
+
symbol: text(c.symbol, 24),
|
|
827
|
+
url: cleanUrl(c.url),
|
|
828
|
+
activityStatus: code(c.activityStatus),
|
|
829
|
+
catalogStatus: code(c.catalogStatus),
|
|
830
|
+
tokenAddress: token && token.chainId === CHAIN_ID ? address(token.address) : null
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
const total = obj(error.candidatesTotal);
|
|
834
|
+
const totalCount = count(total?.count);
|
|
835
|
+
return {
|
|
836
|
+
outcome: "ambiguous",
|
|
837
|
+
message: text(error.message, 1200) ?? "More than one published project matches. HEY does not guess: pick one.",
|
|
838
|
+
candidates,
|
|
839
|
+
candidatesTotal: totalCount === null ? null : { count: totalCount, atLeast: total?.atLeast === true },
|
|
840
|
+
asOf: instant(body.asOf),
|
|
841
|
+
disclaimer: disclaimerOf(body)
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
function parseNamesakes(scan) {
|
|
845
|
+
const block = obj(obj(scan)?.publishedNamesakes);
|
|
846
|
+
if (!block) return null;
|
|
847
|
+
const items = [];
|
|
848
|
+
for (const entry of arr(block.items)) {
|
|
849
|
+
const n = obj(entry);
|
|
850
|
+
const name = text(n?.name, 120);
|
|
851
|
+
if (!n || !name) continue;
|
|
852
|
+
items.push({
|
|
853
|
+
name,
|
|
854
|
+
slug: slug(n.slug),
|
|
855
|
+
symbol: text(n.symbol, 24),
|
|
856
|
+
contractAddress: address(n.contractAddress),
|
|
857
|
+
shares: lowerCode(n.shares),
|
|
858
|
+
sharesAddressEnding: typeof n.sharesAddressEnding === "boolean" ? n.sharesAddressEnding : null,
|
|
859
|
+
url: cleanUrl(n.url)
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
const total = count(block.total) ?? items.length;
|
|
863
|
+
if (total === 0 && items.length === 0) return null;
|
|
864
|
+
return { total, items, line: text(block.line, 600) };
|
|
865
|
+
}
|
|
866
|
+
function bodyOf(answer) {
|
|
867
|
+
const body = obj(answer.body);
|
|
868
|
+
if (!body || typeof body.status !== "string") {
|
|
869
|
+
throw networkError("invalid_response", "HEY's answer was not an agent answer.", {
|
|
870
|
+
retryable: false
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
return body;
|
|
874
|
+
}
|
|
875
|
+
async function check(input, http) {
|
|
876
|
+
const first = queryFor(input);
|
|
877
|
+
let body = bodyOf(await http.getJson(RESEARCH_PATH, { [first.key]: first.value }));
|
|
878
|
+
if (body.status === "invalid_request" && obj(body.error)?.code === "ambiguous") {
|
|
879
|
+
const typed = input.kind === "name" ? input.q.toLowerCase() : null;
|
|
880
|
+
const exact = typed && SLUG.test(typed) ? arr(obj(body.error)?.candidates).some((c) => obj(c)?.slug === typed) : false;
|
|
881
|
+
if (typed && exact) {
|
|
882
|
+
body = bodyOf(await http.getJson(RESEARCH_PATH, { project: typed }));
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
const error = obj(body.error);
|
|
886
|
+
switch (body.status) {
|
|
887
|
+
case "ok":
|
|
888
|
+
return parseAnswered(body, input);
|
|
889
|
+
case "invalid_request":
|
|
890
|
+
if (error?.code === "ambiguous") return parseAmbiguous(body, error);
|
|
891
|
+
throw new CheckError(
|
|
892
|
+
lowerCode(error?.code) ?? "invalid_request",
|
|
893
|
+
text(error?.message, 600) ?? "HEY refused the request.",
|
|
894
|
+
EXIT.REFUSED
|
|
895
|
+
);
|
|
896
|
+
case "not_found": {
|
|
897
|
+
const message = text(error?.message, 1200) ?? text(body.answer, 1200) ?? "HEY holds no published project for that.";
|
|
898
|
+
if (input.kind !== "address") {
|
|
899
|
+
return {
|
|
900
|
+
outcome: "not_found",
|
|
901
|
+
message,
|
|
902
|
+
address: null,
|
|
903
|
+
scanUrl: null,
|
|
904
|
+
namesakes: null,
|
|
905
|
+
namesakesRead: false,
|
|
906
|
+
asOf: instant(body.asOf),
|
|
907
|
+
disclaimer: disclaimerOf(body)
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
let scanBody = null;
|
|
911
|
+
try {
|
|
912
|
+
const scan = await http.getJson(SCAN_PATH, {
|
|
913
|
+
chain: String(CHAIN_ID),
|
|
914
|
+
token: input.address
|
|
915
|
+
});
|
|
916
|
+
scanBody = scan.status < 400 ? obj(scan.body) : null;
|
|
917
|
+
} catch {
|
|
918
|
+
scanBody = null;
|
|
919
|
+
}
|
|
920
|
+
return {
|
|
921
|
+
outcome: "not_found",
|
|
922
|
+
message,
|
|
923
|
+
address: input.address,
|
|
924
|
+
scanUrl: cleanUrl(scanBody?.scan_url) ?? `https://heyresearch.xyz/scan/${input.address}`,
|
|
925
|
+
namesakes: parseNamesakes(scanBody),
|
|
926
|
+
namesakesRead: scanBody !== null,
|
|
927
|
+
asOf: instant(body.asOf),
|
|
928
|
+
disclaimer: disclaimerOf(body)
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
case "unavailable":
|
|
932
|
+
throw networkError(
|
|
933
|
+
"unavailable",
|
|
934
|
+
text(error?.message, 600) ?? "HEY could not answer right now. Try again shortly."
|
|
935
|
+
);
|
|
936
|
+
case "moved":
|
|
937
|
+
throw new CheckError(
|
|
938
|
+
"moved",
|
|
939
|
+
text(body.answer, 600) ?? "HEY says this project moved; check its new name or slug.",
|
|
940
|
+
EXIT.REFUSED
|
|
941
|
+
);
|
|
942
|
+
default:
|
|
943
|
+
throw networkError("invalid_response", `HEY answered an unknown status.`, {
|
|
944
|
+
retryable: false
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// src/words.ts
|
|
950
|
+
var STATUS_LABEL = {
|
|
951
|
+
SHIPPING: "Shipping",
|
|
952
|
+
ACTIVE: "Active",
|
|
953
|
+
RESUMED: "Resumed",
|
|
954
|
+
QUIET: "Quiet",
|
|
955
|
+
DORMANT: "Dormant",
|
|
956
|
+
UNKNOWN: "Unknown"
|
|
957
|
+
};
|
|
958
|
+
var statusLabel = (code2) => STATUS_LABEL[code2] ?? "Unknown";
|
|
959
|
+
var isBuilding = (code2) => code2 === "SHIPPING" || code2 === "ACTIVE" || code2 === "RESUMED";
|
|
960
|
+
var BOUNDARY = "Builder evidence, not a safety verdict or investment advice.";
|
|
961
|
+
var shortAddress = (address2) => `${address2.slice(0, 6)}\u2026${address2.slice(-4)}`;
|
|
962
|
+
var HOUR = 36e5;
|
|
963
|
+
var DAY = 24 * HOUR;
|
|
964
|
+
function ago(at, now) {
|
|
965
|
+
const ms = now.getTime() - Date.parse(at);
|
|
966
|
+
if (!Number.isFinite(ms) || ms < HOUR) return ms < 0 ? "just now" : "under an hour ago";
|
|
967
|
+
if (ms < DAY) {
|
|
968
|
+
const h = Math.floor(ms / HOUR);
|
|
969
|
+
return `${h} hour${h === 1 ? "" : "s"} ago`;
|
|
970
|
+
}
|
|
971
|
+
const d = Math.floor(ms / DAY);
|
|
972
|
+
return `${d} day${d === 1 ? "" : "s"} ago`;
|
|
973
|
+
}
|
|
974
|
+
var isoDay = (at) => new Date(Date.parse(at)).toISOString().slice(0, 10);
|
|
975
|
+
var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
976
|
+
function humanDay(at) {
|
|
977
|
+
return `${at.getUTCDate()} ${MONTHS[at.getUTCMonth()]} ${at.getUTCFullYear()}`;
|
|
978
|
+
}
|
|
979
|
+
function lastShipLine(builder, now) {
|
|
980
|
+
if (builder.lastMeaningfulShipAt) return `Last shipped ${ago(builder.lastMeaningfulShipAt, now)}`;
|
|
981
|
+
if (builder.meaningfulEvents30d === 0) return "No meaningful ship recorded in 30 days";
|
|
982
|
+
return "Last ship not known to HEY";
|
|
983
|
+
}
|
|
984
|
+
function eventsLine(n) {
|
|
985
|
+
if (n === null) return "Meaningful events in 30 days: not measured";
|
|
986
|
+
return `${n} meaningful event${n === 1 ? "" : "s"} in 30 days`;
|
|
987
|
+
}
|
|
988
|
+
function verificationWords(v) {
|
|
989
|
+
switch (v) {
|
|
990
|
+
case "VERIFIED":
|
|
991
|
+
return "token verified";
|
|
992
|
+
case "UNVERIFIED":
|
|
993
|
+
return "token not verified";
|
|
994
|
+
case "MISMATCH":
|
|
995
|
+
return "token mismatch: the project's own site names another contract";
|
|
996
|
+
default:
|
|
997
|
+
return "token verification unknown";
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
function sharesPhrase(shares) {
|
|
1001
|
+
switch (shares) {
|
|
1002
|
+
case "name_and_ticker":
|
|
1003
|
+
return "carries the same name and ticker";
|
|
1004
|
+
case "name":
|
|
1005
|
+
return "carries the same name";
|
|
1006
|
+
case "ticker":
|
|
1007
|
+
return "carries the same ticker";
|
|
1008
|
+
default:
|
|
1009
|
+
return "carries its name or ticker";
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
function namesakeSentence(n) {
|
|
1013
|
+
const where = n.contractAddress ? `on a different contract, ${shortAddress(n.contractAddress)}` : "on a different contract";
|
|
1014
|
+
const ending = n.sharesAddressEnding ? ", and its address ends in the same four characters" : "";
|
|
1015
|
+
return `${n.name} is published on HEY ${where}; this token ${sharesPhrase(n.shares)}${ending}.`;
|
|
1016
|
+
}
|
|
1017
|
+
var CONTRACT_IS_IDENTITY = "On chain a token is its contract, never its name or ticker.";
|
|
1018
|
+
|
|
1019
|
+
// src/card/model.ts
|
|
1020
|
+
var host = (url) => url.replace(/^https:\/\//, "").replace(/\/$/, "");
|
|
1021
|
+
function cardModel(result, input, now) {
|
|
1022
|
+
const checkedOn = `Checked ${humanDay(now)} UTC`;
|
|
1023
|
+
switch (result.outcome) {
|
|
1024
|
+
case "answered": {
|
|
1025
|
+
const { project, builder, contract } = result;
|
|
1026
|
+
const facts = [eventsLine(builder.meaningfulEvents30d)];
|
|
1027
|
+
if (contract) {
|
|
1028
|
+
facts.push(
|
|
1029
|
+
`${shortAddress(contract.address)}${contract.verification === "VERIFIED" ? ` \xB7 ${verificationWords("VERIFIED")}` : ""}`
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
return {
|
|
1033
|
+
// A name the card's font cannot draw at all (emoji, CJK …) falls back to the slug.
|
|
1034
|
+
title: cardText(project.name) ? project.name : project.slug,
|
|
1035
|
+
ticker: project.symbol ? `$${project.symbol.replace(/^\$/, "")}` : null,
|
|
1036
|
+
pill: {
|
|
1037
|
+
label: statusLabel(builder.activityStatus),
|
|
1038
|
+
tone: isBuilding(builder.activityStatus) ? "lime" : "grey"
|
|
1039
|
+
},
|
|
1040
|
+
headline: lastShipLine(builder, now),
|
|
1041
|
+
detail: result.latestShip?.title ?? null,
|
|
1042
|
+
facts,
|
|
1043
|
+
notice: null,
|
|
1044
|
+
footerUrl: host(result.page),
|
|
1045
|
+
checkedOn
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
case "not_found": {
|
|
1049
|
+
if (result.address) {
|
|
1050
|
+
const n = result.namesakes?.items[0];
|
|
1051
|
+
const others = (result.namesakes?.total ?? 0) - 1;
|
|
1052
|
+
return {
|
|
1053
|
+
title: shortAddress(result.address),
|
|
1054
|
+
ticker: null,
|
|
1055
|
+
pill: null,
|
|
1056
|
+
headline: "Not a published project on HEY",
|
|
1057
|
+
detail: n ? null : "HEY holds no published project for this contract. That is a statement about HEY's index, not about the token.",
|
|
1058
|
+
facts: [],
|
|
1059
|
+
notice: n ? {
|
|
1060
|
+
heading: "Same name on another contract",
|
|
1061
|
+
body: `${namesakeSentence(n)}${others > 0 ? ` ${others} more published project${others === 1 ? "" : "s"} share it.` : ""}`,
|
|
1062
|
+
foot: CONTRACT_IS_IDENTITY
|
|
1063
|
+
} : null,
|
|
1064
|
+
footerUrl: host(result.scanUrl ?? `https://heyresearch.xyz/scan/${result.address}`),
|
|
1065
|
+
checkedOn
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
return {
|
|
1069
|
+
title: input.raw,
|
|
1070
|
+
ticker: null,
|
|
1071
|
+
pill: null,
|
|
1072
|
+
headline: "Not a published project on HEY",
|
|
1073
|
+
detail: "HEY holds no published project matching that name or ticker. That is a statement about HEY's index, not about the project.",
|
|
1074
|
+
facts: [],
|
|
1075
|
+
notice: null,
|
|
1076
|
+
footerUrl: "heyresearch.xyz",
|
|
1077
|
+
checkedOn
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
case "ambiguous":
|
|
1081
|
+
return null;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// src/card/write.ts
|
|
1086
|
+
import { closeSync, constants, lstatSync, openSync, statSync, writeSync } from "fs";
|
|
1087
|
+
import { dirname as dirname2, resolve } from "path";
|
|
1088
|
+
var cardError = (message) => new CheckError("card_not_written", message, EXIT.CARD_FAILED);
|
|
1089
|
+
function defaultCardName(result, input) {
|
|
1090
|
+
let stem;
|
|
1091
|
+
if (result.outcome === "answered") stem = result.project.slug;
|
|
1092
|
+
else if (input.kind === "address")
|
|
1093
|
+
stem = `${input.address.slice(0, 6)}-${input.address.slice(-4)}`;
|
|
1094
|
+
else {
|
|
1095
|
+
stem = input.q.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
1096
|
+
}
|
|
1097
|
+
return `hey-check-${stem || "result"}.png`;
|
|
1098
|
+
}
|
|
1099
|
+
function writeCard(path, png, cwd) {
|
|
1100
|
+
if (!path.toLowerCase().endsWith(".png")) throw cardError("The card path must end in .png.");
|
|
1101
|
+
const target = resolve(cwd, path);
|
|
1102
|
+
let parent;
|
|
1103
|
+
try {
|
|
1104
|
+
parent = statSync(dirname2(target));
|
|
1105
|
+
} catch {
|
|
1106
|
+
throw cardError(`The directory ${dirname2(target)} does not exist.`);
|
|
1107
|
+
}
|
|
1108
|
+
if (!parent.isDirectory()) throw cardError(`${dirname2(target)} is not a directory.`);
|
|
1109
|
+
let existing;
|
|
1110
|
+
try {
|
|
1111
|
+
existing = lstatSync(target);
|
|
1112
|
+
} catch {
|
|
1113
|
+
existing = null;
|
|
1114
|
+
}
|
|
1115
|
+
if (existing?.isSymbolicLink()) {
|
|
1116
|
+
throw cardError(`${target} is a symbolic link; hey-check does not write through links.`);
|
|
1117
|
+
}
|
|
1118
|
+
if (existing && !existing.isFile()) throw cardError(`${target} exists and is not a file.`);
|
|
1119
|
+
const noFollow = typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0;
|
|
1120
|
+
let fd;
|
|
1121
|
+
try {
|
|
1122
|
+
fd = openSync(
|
|
1123
|
+
target,
|
|
1124
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | noFollow,
|
|
1125
|
+
420
|
|
1126
|
+
);
|
|
1127
|
+
} catch (error) {
|
|
1128
|
+
const code2 = error.code;
|
|
1129
|
+
if (code2 === "ELOOP")
|
|
1130
|
+
throw cardError(`${target} is a symbolic link; hey-check does not write through links.`);
|
|
1131
|
+
throw cardError(`Could not write ${target}${code2 ? ` (${code2})` : ""}.`);
|
|
1132
|
+
}
|
|
1133
|
+
try {
|
|
1134
|
+
writeSync(fd, png);
|
|
1135
|
+
} finally {
|
|
1136
|
+
closeSync(fd);
|
|
1137
|
+
}
|
|
1138
|
+
return target;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// src/json.ts
|
|
1142
|
+
var JSON_SCHEMA = "hey.check/v1";
|
|
1143
|
+
function emptyDocument(now, input, requests) {
|
|
1144
|
+
const sent = input ? queryFor(input) : null;
|
|
1145
|
+
return {
|
|
1146
|
+
schema: JSON_SCHEMA,
|
|
1147
|
+
ok: false,
|
|
1148
|
+
outcome: null,
|
|
1149
|
+
chain: { name: CHAIN_NAME, chainId: CHAIN_ID, caip2: CAIP2 },
|
|
1150
|
+
input: input && sent ? { raw: input.raw, kind: input.kind, sent: { [sent.key]: sent.value } } : null,
|
|
1151
|
+
answer: null,
|
|
1152
|
+
project: null,
|
|
1153
|
+
builder: null,
|
|
1154
|
+
latestShip: null,
|
|
1155
|
+
contract: null,
|
|
1156
|
+
unknowns: null,
|
|
1157
|
+
candidates: null,
|
|
1158
|
+
candidatesTotal: null,
|
|
1159
|
+
notFound: null,
|
|
1160
|
+
citation: null,
|
|
1161
|
+
page: null,
|
|
1162
|
+
card: null,
|
|
1163
|
+
tweet: null,
|
|
1164
|
+
boundary: BOUNDARY,
|
|
1165
|
+
disclaimer: null,
|
|
1166
|
+
error: null,
|
|
1167
|
+
source: { requests, asOf: null, fetchedAt: now.toISOString() }
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
function resultDocument(result, base, extra) {
|
|
1171
|
+
const doc = {
|
|
1172
|
+
...base,
|
|
1173
|
+
ok: true,
|
|
1174
|
+
outcome: result.outcome,
|
|
1175
|
+
card: extra.cardPath ? { path: extra.cardPath } : null,
|
|
1176
|
+
tweet: extra.tweet,
|
|
1177
|
+
disclaimer: result.disclaimer,
|
|
1178
|
+
source: { ...base.source, asOf: result.asOf }
|
|
1179
|
+
};
|
|
1180
|
+
switch (result.outcome) {
|
|
1181
|
+
case "answered":
|
|
1182
|
+
return {
|
|
1183
|
+
...doc,
|
|
1184
|
+
answer: result.answer,
|
|
1185
|
+
project: { ...result.project },
|
|
1186
|
+
builder: { ...result.builder, activityLabel: statusLabel(result.builder.activityStatus) },
|
|
1187
|
+
latestShip: result.latestShip ? { ...result.latestShip } : null,
|
|
1188
|
+
contract: result.contract ? { ...result.contract } : null,
|
|
1189
|
+
unknowns: result.unknowns.map((u) => ({ ...u })),
|
|
1190
|
+
citation: result.citation,
|
|
1191
|
+
page: result.page
|
|
1192
|
+
};
|
|
1193
|
+
case "ambiguous":
|
|
1194
|
+
return {
|
|
1195
|
+
...doc,
|
|
1196
|
+
answer: result.message,
|
|
1197
|
+
candidates: result.candidates.map((c) => ({ ...c })),
|
|
1198
|
+
candidatesTotal: result.candidatesTotal
|
|
1199
|
+
};
|
|
1200
|
+
case "not_found":
|
|
1201
|
+
return {
|
|
1202
|
+
...doc,
|
|
1203
|
+
answer: result.message,
|
|
1204
|
+
notFound: {
|
|
1205
|
+
message: result.message,
|
|
1206
|
+
address: result.address,
|
|
1207
|
+
scanUrl: result.scanUrl,
|
|
1208
|
+
namesakesRead: result.namesakesRead,
|
|
1209
|
+
namesakes: result.namesakes ? {
|
|
1210
|
+
total: result.namesakes.total,
|
|
1211
|
+
line: result.namesakes.line,
|
|
1212
|
+
items: result.namesakes.items.map((n) => ({ ...n }))
|
|
1213
|
+
} : null
|
|
1214
|
+
},
|
|
1215
|
+
page: result.scanUrl
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
function errorDocument(error, base) {
|
|
1220
|
+
return {
|
|
1221
|
+
...base,
|
|
1222
|
+
error: {
|
|
1223
|
+
code: error.code,
|
|
1224
|
+
message: error.message,
|
|
1225
|
+
retryable: error.details.retryable ?? false,
|
|
1226
|
+
...error.details.requestId ? { requestId: error.details.requestId } : {},
|
|
1227
|
+
...error.details.retryAfterSeconds !== void 0 ? { retryAfterSeconds: error.details.retryAfterSeconds } : {}
|
|
1228
|
+
}
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// src/terminal.ts
|
|
1233
|
+
var LABEL_WIDTH = 14;
|
|
1234
|
+
var row = (s, label, value) => ` ${s.dim(label.padEnd(LABEL_WIDTH))}${value}`;
|
|
1235
|
+
var more = (value) => ` ${" ".repeat(LABEL_WIDTH)}${value}`;
|
|
1236
|
+
function answered(r, ctx) {
|
|
1237
|
+
const s = ctx.style;
|
|
1238
|
+
const lines = [];
|
|
1239
|
+
if (r.answer) lines.push(s.strong(r.answer), "");
|
|
1240
|
+
const ticker = r.project.symbol ? ` $${r.project.symbol.replace(/^\$/, "")}` : "";
|
|
1241
|
+
lines.push(` ${s.strong(r.project.name)}${ticker}`);
|
|
1242
|
+
lines.push(row(s, "Status", s.strong(statusLabel(r.builder.activityStatus))));
|
|
1243
|
+
const at = r.builder.lastMeaningfulShipAt;
|
|
1244
|
+
if (at) {
|
|
1245
|
+
const title = r.latestShip?.title ? ` \u2014 ${r.latestShip.title}` : "";
|
|
1246
|
+
lines.push(row(s, "Last ship", `${ago(at, ctx.now)} (${isoDay(at)})${title}`));
|
|
1247
|
+
if (r.latestShip?.sourceUrl) lines.push(more(s.dim(r.latestShip.sourceUrl)));
|
|
1248
|
+
} else {
|
|
1249
|
+
lines.push(row(s, "Last ship", lastShipLine(r.builder, ctx.now)));
|
|
1250
|
+
}
|
|
1251
|
+
const n = r.builder.meaningfulEvents30d;
|
|
1252
|
+
lines.push(
|
|
1253
|
+
row(
|
|
1254
|
+
s,
|
|
1255
|
+
"Last 30 days",
|
|
1256
|
+
n === null ? "not measured" : `${n} meaningful event${n === 1 ? "" : "s"}`
|
|
1257
|
+
)
|
|
1258
|
+
);
|
|
1259
|
+
if (r.contract) {
|
|
1260
|
+
const v = r.contract.verification;
|
|
1261
|
+
lines.push(
|
|
1262
|
+
row(s, "Contract", `${r.contract.address} \xB7 ${verificationWords(v)}${v ? ` (${v})` : ""}`)
|
|
1263
|
+
);
|
|
1264
|
+
} else {
|
|
1265
|
+
lines.push(row(s, "Contract", "no token on HEY\u2019s record for this project"));
|
|
1266
|
+
}
|
|
1267
|
+
if (r.askedAddress) lines.push(row(s, "You asked", r.askedAddress));
|
|
1268
|
+
const shown = r.unknowns.slice(0, 2);
|
|
1269
|
+
shown.forEach((u, i) => {
|
|
1270
|
+
const value = `${u.category} \u2014 ${u.text}`;
|
|
1271
|
+
lines.push(i === 0 ? row(s, "Unknowns", value) : more(value));
|
|
1272
|
+
});
|
|
1273
|
+
if (r.unknowns.length > shown.length) {
|
|
1274
|
+
lines.push(more(s.dim(`+${r.unknowns.length - shown.length} more (--json lists them all)`)));
|
|
1275
|
+
}
|
|
1276
|
+
lines.push(row(s, "Page", r.page));
|
|
1277
|
+
if (ctx.cardPath) lines.push(row(s, "Card", ctx.cardPath));
|
|
1278
|
+
lines.push("");
|
|
1279
|
+
if (r.citation) lines.push(s.dim(r.citation));
|
|
1280
|
+
lines.push(s.dim(BOUNDARY));
|
|
1281
|
+
return lines;
|
|
1282
|
+
}
|
|
1283
|
+
function ambiguous(r, ctx) {
|
|
1284
|
+
const s = ctx.style;
|
|
1285
|
+
const lines = [s.strong("More than one published project matches. HEY does not guess."), ""];
|
|
1286
|
+
const width = Math.min(44, Math.max(12, ...r.candidates.map((c) => c.slug.length)));
|
|
1287
|
+
for (const c of r.candidates) {
|
|
1288
|
+
const name = c.name ?? c.slug;
|
|
1289
|
+
const sym = c.symbol ? ` ($${c.symbol.replace(/^\$/, "")})` : "";
|
|
1290
|
+
const status = c.activityStatus ? ` \xB7 ${statusLabel(c.activityStatus)}` : "";
|
|
1291
|
+
const token = c.tokenAddress ? ` \xB7 ${shortAddress(c.tokenAddress)}` : "";
|
|
1292
|
+
lines.push(` ${s.strong(c.slug.padEnd(width))} ${name}${sym}${status}${token}`);
|
|
1293
|
+
}
|
|
1294
|
+
const total = r.candidatesTotal;
|
|
1295
|
+
if (total && total.count > r.candidates.length) {
|
|
1296
|
+
lines.push(
|
|
1297
|
+
"",
|
|
1298
|
+
s.dim(
|
|
1299
|
+
` Showing ${r.candidates.length} of ${total.atLeast ? "at least " : ""}${total.count} matches.`
|
|
1300
|
+
)
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
const example = r.candidates[0]?.slug ?? "<slug>";
|
|
1304
|
+
lines.push(
|
|
1305
|
+
"",
|
|
1306
|
+
`Pick one: hey-check ${example} (or pass the token's 0x contract address)`,
|
|
1307
|
+
"",
|
|
1308
|
+
s.dim(BOUNDARY)
|
|
1309
|
+
);
|
|
1310
|
+
return lines;
|
|
1311
|
+
}
|
|
1312
|
+
function notFound(r, ctx) {
|
|
1313
|
+
const s = ctx.style;
|
|
1314
|
+
const subject = r.address ?? ctx.input.raw;
|
|
1315
|
+
const lines = [s.strong(`${subject} is not a published project on HEY.`), "", ` ${r.message}`];
|
|
1316
|
+
if (r.namesakes && r.namesakes.items.length > 0) {
|
|
1317
|
+
lines.push("", ` ${s.strong("Same name or ticker on another contract")}`);
|
|
1318
|
+
if (!/different contract/i.test(r.message)) {
|
|
1319
|
+
lines.push(` ${r.namesakes.line ?? namesakeSentence(r.namesakes.items[0])}`);
|
|
1320
|
+
}
|
|
1321
|
+
for (const n of r.namesakes.items) {
|
|
1322
|
+
const sym = n.symbol ? ` ($${n.symbol.replace(/^\$/, "")})` : "";
|
|
1323
|
+
const addr = n.contractAddress ? ` \xB7 ${n.contractAddress}` : "";
|
|
1324
|
+
lines.push(` ${n.name}${sym}${addr}${n.url ? ` \xB7 ${n.url}` : ""}`);
|
|
1325
|
+
}
|
|
1326
|
+
if (r.namesakes.total > r.namesakes.items.length) {
|
|
1327
|
+
lines.push(` ${s.dim(`+${r.namesakes.total - r.namesakes.items.length} more`)}`);
|
|
1328
|
+
}
|
|
1329
|
+
} else if (r.address && !r.namesakesRead) {
|
|
1330
|
+
lines.push("", s.dim(" HEY\u2019s list of published namesakes could not be read this time."));
|
|
1331
|
+
}
|
|
1332
|
+
lines.push("");
|
|
1333
|
+
if (r.scanUrl) lines.push(row(s, "Scan", r.scanUrl));
|
|
1334
|
+
if (ctx.cardPath) lines.push(row(s, "Card", ctx.cardPath));
|
|
1335
|
+
if (r.scanUrl || ctx.cardPath) lines.push("");
|
|
1336
|
+
if (r.address && !r.message.includes(CONTRACT_IS_IDENTITY))
|
|
1337
|
+
lines.push(s.dim(CONTRACT_IS_IDENTITY));
|
|
1338
|
+
lines.push(s.dim(BOUNDARY));
|
|
1339
|
+
return lines;
|
|
1340
|
+
}
|
|
1341
|
+
function renderTerminal(result, ctx) {
|
|
1342
|
+
switch (result.outcome) {
|
|
1343
|
+
case "answered":
|
|
1344
|
+
return answered(result, ctx);
|
|
1345
|
+
case "ambiguous":
|
|
1346
|
+
return ambiguous(result, ctx);
|
|
1347
|
+
case "not_found":
|
|
1348
|
+
return notFound(result, ctx);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
function quietLine(result, now) {
|
|
1352
|
+
switch (result.outcome) {
|
|
1353
|
+
case "answered":
|
|
1354
|
+
return `${result.project.slug} ${statusLabel(result.builder.activityStatus)} ${lastShipLine(result.builder, now)}`;
|
|
1355
|
+
case "ambiguous":
|
|
1356
|
+
return result.candidates.map((c) => c.slug).join("\n");
|
|
1357
|
+
case "not_found":
|
|
1358
|
+
return "not_published";
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// src/tweet.ts
|
|
1363
|
+
var TWEET_LIMIT = 280;
|
|
1364
|
+
var TWEET_URL_WEIGHT = 23;
|
|
1365
|
+
var URL_RE = /https?:\/\/\S+/g;
|
|
1366
|
+
function tweetLength(text2) {
|
|
1367
|
+
let total = 0;
|
|
1368
|
+
const rest = text2.replace(URL_RE, () => {
|
|
1369
|
+
total += TWEET_URL_WEIGHT;
|
|
1370
|
+
return "";
|
|
1371
|
+
});
|
|
1372
|
+
for (const ch of rest) {
|
|
1373
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
1374
|
+
const light = cp <= 4351 || cp >= 8192 && cp <= 8205 || cp >= 8208 && cp <= 8223 || cp >= 8242 && cp <= 8247;
|
|
1375
|
+
total += light ? 1 : 2;
|
|
1376
|
+
}
|
|
1377
|
+
return total;
|
|
1378
|
+
}
|
|
1379
|
+
function fit(build, name) {
|
|
1380
|
+
let n = [...name];
|
|
1381
|
+
let out = build(name);
|
|
1382
|
+
while (tweetLength(out) > TWEET_LIMIT && n.length > 4) {
|
|
1383
|
+
n = n.slice(0, -1);
|
|
1384
|
+
out = build(`${n.join("").trimEnd()}\u2026`);
|
|
1385
|
+
}
|
|
1386
|
+
return out;
|
|
1387
|
+
}
|
|
1388
|
+
function tweetText(result, input, now) {
|
|
1389
|
+
switch (result.outcome) {
|
|
1390
|
+
case "answered": {
|
|
1391
|
+
const { project, builder } = result;
|
|
1392
|
+
const sym = project.symbol ? ` ($${project.symbol.replace(/^\$/, "")})` : "";
|
|
1393
|
+
const label = statusLabel(builder.activityStatus);
|
|
1394
|
+
const facts = `${lastShipLine(builder, now)}; ${eventsLine(builder.meaningfulEvents30d)}.`;
|
|
1395
|
+
return fit(
|
|
1396
|
+
(name) => `Dev check on ${name}${sym}, Robinhood Chain: ${label}. ${facts}
|
|
1397
|
+
|
|
1398
|
+
Builder evidence from HEY Research Lab, not a safety verdict: ${result.page}`,
|
|
1399
|
+
project.name
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
case "not_found": {
|
|
1403
|
+
if (result.address) {
|
|
1404
|
+
const n = result.namesakes?.items[0];
|
|
1405
|
+
const namesake = n ? ` ${n.name} is published on a different contract${n.contractAddress ? `, ${shortAddress(n.contractAddress)}` : ""}. On chain a token is its contract, never its name or ticker.` : "";
|
|
1406
|
+
const url = result.scanUrl ?? `https://heyresearch.xyz/scan/${result.address}`;
|
|
1407
|
+
return fit(
|
|
1408
|
+
(name) => `Dev check on ${shortAddress(result.address)}, Robinhood Chain: not a published project on HEY Research Lab.${name}
|
|
1409
|
+
|
|
1410
|
+
${url}`,
|
|
1411
|
+
namesake
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1414
|
+
return fit(
|
|
1415
|
+
(name) => `Dev check on "${name}", Robinhood Chain: no published project on HEY Research Lab matches it. That is a statement about HEY's index, not a verdict.
|
|
1416
|
+
|
|
1417
|
+
https://heyresearch.xyz`,
|
|
1418
|
+
input.raw
|
|
1419
|
+
);
|
|
1420
|
+
}
|
|
1421
|
+
case "ambiguous":
|
|
1422
|
+
return null;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// src/run.ts
|
|
1427
|
+
var OPTIONS = {
|
|
1428
|
+
json: { type: "boolean" },
|
|
1429
|
+
quiet: { type: "boolean" },
|
|
1430
|
+
tweet: { type: "boolean" },
|
|
1431
|
+
"no-card": { type: "boolean" },
|
|
1432
|
+
out: { type: "string", short: "o" },
|
|
1433
|
+
"no-color": { type: "boolean" },
|
|
1434
|
+
help: { type: "boolean", short: "h" },
|
|
1435
|
+
version: { type: "boolean", short: "v" }
|
|
1436
|
+
};
|
|
1437
|
+
var NETWORK_FLAGS = /^--(chain|chain-id|chainid|network|net|rpc|rpc-url)(=|$)/i;
|
|
1438
|
+
var HELP2 = `hey-check \u2014 a dev check for a Robinhood Chain token or project
|
|
1439
|
+
|
|
1440
|
+
Is anyone building it, and what did they ship? Answered from HEY Research Lab's public record
|
|
1441
|
+
(heyresearch.xyz), printed here and drawn as a 1200\xD7675 PNG card.
|
|
1442
|
+
|
|
1443
|
+
Usage
|
|
1444
|
+
hey-check <input> [options]
|
|
1445
|
+
|
|
1446
|
+
Input
|
|
1447
|
+
0x\u2026 a token or contract address (0x + 40 hex)
|
|
1448
|
+
$TICKER a ticker, e.g. $CHIT (in a shell, quote it: '$CHIT')
|
|
1449
|
+
name or slug a project name, e.g. loom
|
|
1450
|
+
@handle an X handle
|
|
1451
|
+
https://x.com/<handle> an X profile URL
|
|
1452
|
+
|
|
1453
|
+
Options
|
|
1454
|
+
--json print one JSON document (schema hey.check/v1) on stdout
|
|
1455
|
+
--no-card do not draw the PNG card
|
|
1456
|
+
-o, --out <file.png> where to write the card (default ./hey-check-<slug>.png)
|
|
1457
|
+
--tweet print a ready-to-post text (under 280 characters) instead of the report
|
|
1458
|
+
--quiet print one line
|
|
1459
|
+
--no-color no ANSI styling (also NO_COLOR)
|
|
1460
|
+
-h, --help this help
|
|
1461
|
+
-v, --version the version
|
|
1462
|
+
|
|
1463
|
+
Exit codes
|
|
1464
|
+
0 answered \xB7 1 card not written \xB7 2 bad input \xB7 3 HEY refused \xB7 4 not published on HEY
|
|
1465
|
+
5 rate limited \xB7 6 network or HEY error \xB7 7 more than one match (pick one)
|
|
1466
|
+
|
|
1467
|
+
Builder evidence, not a safety verdict or investment advice. Robinhood Chain (4663) only.
|
|
1468
|
+
HEY Research Lab is independent and not affiliated with Robinhood.`;
|
|
1469
|
+
async function run(argv, io) {
|
|
1470
|
+
const wantsJson = argv.includes("--json");
|
|
1471
|
+
let input = null;
|
|
1472
|
+
let http = null;
|
|
1473
|
+
const fail = (error) => {
|
|
1474
|
+
if (wantsJson) {
|
|
1475
|
+
const doc = errorDocument(error, emptyDocument(io.now(), input, http?.requests ?? []));
|
|
1476
|
+
io.stdout(`${JSON.stringify(doc, null, 2)}
|
|
1477
|
+
`);
|
|
1478
|
+
} else {
|
|
1479
|
+
io.stderr(`hey-check: ${error.message} (${error.code})
|
|
1480
|
+
`);
|
|
1481
|
+
if (error.details.requestId) io.stderr(`hey-check: request id ${error.details.requestId}
|
|
1482
|
+
`);
|
|
1483
|
+
if (error.exitCode === EXIT.USAGE) io.stderr("hey-check: see `hey-check --help`\n");
|
|
1484
|
+
}
|
|
1485
|
+
return error.exitCode;
|
|
1486
|
+
};
|
|
1487
|
+
try {
|
|
1488
|
+
if (argv.some((a) => NETWORK_FLAGS.test(a))) {
|
|
1489
|
+
throw usageError(
|
|
1490
|
+
"unsupported_chain",
|
|
1491
|
+
"HEY supports Robinhood Chain (4663) only, and it is implicit: hey-check takes no chain or network option."
|
|
1492
|
+
);
|
|
1493
|
+
}
|
|
1494
|
+
let parsed;
|
|
1495
|
+
try {
|
|
1496
|
+
parsed = parseArgs({
|
|
1497
|
+
args: [...argv],
|
|
1498
|
+
options: OPTIONS,
|
|
1499
|
+
allowPositionals: true,
|
|
1500
|
+
strict: true
|
|
1501
|
+
});
|
|
1502
|
+
} catch (error) {
|
|
1503
|
+
throw usageError("invalid_option", error instanceof Error ? error.message : String(error));
|
|
1504
|
+
}
|
|
1505
|
+
const { values, positionals } = parsed;
|
|
1506
|
+
if (values.version) {
|
|
1507
|
+
io.stdout(`${CHECK_VERSION}
|
|
1508
|
+
`);
|
|
1509
|
+
return EXIT.OK;
|
|
1510
|
+
}
|
|
1511
|
+
if (values.help) {
|
|
1512
|
+
io.stdout(`${HELP2}
|
|
1513
|
+
`);
|
|
1514
|
+
return EXIT.OK;
|
|
1515
|
+
}
|
|
1516
|
+
if (positionals.length === 0) {
|
|
1517
|
+
if (wantsJson)
|
|
1518
|
+
throw usageError("missing_argument", "Nothing to check; see `hey-check --help`.");
|
|
1519
|
+
io.stderr(`${HELP2}
|
|
1520
|
+
`);
|
|
1521
|
+
return EXIT.USAGE;
|
|
1522
|
+
}
|
|
1523
|
+
if (positionals.length > 1) {
|
|
1524
|
+
positionals.splice(0, positionals.length, positionals.join(" "));
|
|
1525
|
+
}
|
|
1526
|
+
if (values.out !== void 0 && values["no-card"]) {
|
|
1527
|
+
throw usageError("invalid_option", "--out and --no-card cannot be used together.");
|
|
1528
|
+
}
|
|
1529
|
+
if (values.out !== void 0 && !values.out.toLowerCase().endsWith(".png")) {
|
|
1530
|
+
throw usageError("invalid_option", "--out takes a path ending in .png.");
|
|
1531
|
+
}
|
|
1532
|
+
input = parseInput(positionals[0]);
|
|
1533
|
+
const baseUrl = io.env.HEY_BASE_URL ? parseBaseUrl(io.env.HEY_BASE_URL) : DEFAULT_BASE_URL;
|
|
1534
|
+
http = new HeyHttp(io.fetchImpl, baseUrl);
|
|
1535
|
+
const now = io.now();
|
|
1536
|
+
const result = await check(input, http);
|
|
1537
|
+
let cardPath = null;
|
|
1538
|
+
let cardFailure = null;
|
|
1539
|
+
const model = values["no-card"] ? null : cardModel(result, input, now);
|
|
1540
|
+
if (model) {
|
|
1541
|
+
try {
|
|
1542
|
+
const renderer2 = io.cardRenderer();
|
|
1543
|
+
const png = renderer2.render(buildCardSvg(model, renderer2.measure));
|
|
1544
|
+
const written = writeCard(values.out ?? defaultCardName(result, input), png, io.cwd);
|
|
1545
|
+
const rel = relative(io.cwd, written);
|
|
1546
|
+
cardPath = rel && !rel.startsWith("..") && !isAbsolute(rel) ? `./${rel}` : written;
|
|
1547
|
+
} catch (error) {
|
|
1548
|
+
cardFailure = error instanceof CheckError ? error : new CheckError(
|
|
1549
|
+
"card_not_written",
|
|
1550
|
+
`The card could not be drawn: ${error instanceof Error ? error.message : String(error)}`,
|
|
1551
|
+
EXIT.CARD_FAILED
|
|
1552
|
+
);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
const tweet = values.tweet ? tweetText(result, input, now) : null;
|
|
1556
|
+
const exitCode2 = result.outcome === "ambiguous" ? EXIT.AMBIGUOUS : result.outcome === "not_found" ? EXIT.NOT_FOUND : cardFailure ? EXIT.CARD_FAILED : EXIT.OK;
|
|
1557
|
+
if (values.json) {
|
|
1558
|
+
let doc = resultDocument(result, emptyDocument(now, input, http.requests), {
|
|
1559
|
+
cardPath,
|
|
1560
|
+
tweet
|
|
1561
|
+
});
|
|
1562
|
+
if (cardFailure) doc = { ...errorDocument(cardFailure, doc), ok: true };
|
|
1563
|
+
io.stdout(`${JSON.stringify(doc, null, 2)}
|
|
1564
|
+
`);
|
|
1565
|
+
return exitCode2;
|
|
1566
|
+
}
|
|
1567
|
+
if (cardFailure) io.stderr(`hey-check: ${cardFailure.message} (${cardFailure.code})
|
|
1568
|
+
`);
|
|
1569
|
+
if (values.tweet) {
|
|
1570
|
+
if (tweet) io.stdout(`${tweet}
|
|
1571
|
+
`);
|
|
1572
|
+
else io.stderr("hey-check: nothing to post until one project is picked.\n");
|
|
1573
|
+
if (cardPath) io.stderr(`hey-check: card written to ${cardPath}
|
|
1574
|
+
`);
|
|
1575
|
+
if (result.outcome === "ambiguous") {
|
|
1576
|
+
io.stderr(
|
|
1577
|
+
`${renderTerminal(result, { input, now, style: plainStyle, cardPath }).join("\n")}
|
|
1578
|
+
`
|
|
1579
|
+
);
|
|
1580
|
+
}
|
|
1581
|
+
return exitCode2;
|
|
1582
|
+
}
|
|
1583
|
+
if (values.quiet) {
|
|
1584
|
+
io.stdout(`${quietLine(result, now)}
|
|
1585
|
+
`);
|
|
1586
|
+
return exitCode2;
|
|
1587
|
+
}
|
|
1588
|
+
const colour = io.isTTY && !values["no-color"] && !(io.env.NO_COLOR !== void 0 && io.env.NO_COLOR !== "");
|
|
1589
|
+
const lines = renderTerminal(result, {
|
|
1590
|
+
input,
|
|
1591
|
+
now,
|
|
1592
|
+
style: colour ? ansiStyle : plainStyle,
|
|
1593
|
+
cardPath
|
|
1594
|
+
});
|
|
1595
|
+
io.stdout(`${lines.join("\n")}
|
|
1596
|
+
`);
|
|
1597
|
+
return exitCode2;
|
|
1598
|
+
} catch (error) {
|
|
1599
|
+
if (error instanceof CheckError) return fail(error);
|
|
1600
|
+
return fail(
|
|
1601
|
+
new CheckError(
|
|
1602
|
+
"internal_error",
|
|
1603
|
+
error instanceof Error ? error.message : String(error),
|
|
1604
|
+
EXIT.NETWORK,
|
|
1605
|
+
{ retryable: false }
|
|
1606
|
+
)
|
|
1607
|
+
);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
// src/cli.ts
|
|
1612
|
+
var renderer = null;
|
|
1613
|
+
var exitCode = await run(process.argv.slice(2), {
|
|
1614
|
+
fetchImpl: (input, init) => fetch(input, init),
|
|
1615
|
+
stdout: (text2) => process.stdout.write(text2),
|
|
1616
|
+
stderr: (text2) => process.stderr.write(text2),
|
|
1617
|
+
env: process.env,
|
|
1618
|
+
isTTY: process.stdout.isTTY === true,
|
|
1619
|
+
now: () => /* @__PURE__ */ new Date(),
|
|
1620
|
+
cwd: process.cwd(),
|
|
1621
|
+
cardRenderer: () => {
|
|
1622
|
+
if (!renderer) {
|
|
1623
|
+
const fonts = loadFonts();
|
|
1624
|
+
renderer = { measure: createMeasure(fonts), render: (svg) => renderPng(svg, fonts) };
|
|
1625
|
+
}
|
|
1626
|
+
return renderer;
|
|
1627
|
+
}
|
|
1628
|
+
});
|
|
1629
|
+
process.exitCode = exitCode;
|