ogthing 0.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 extoci
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # ogthing – inspect link metadata from your own machine
2
+
3
+ ogthing is golbat energy for your localhost: a metadata inspector that runs on
4
+ your device so the urls you actually care about — `localhost:3000`, your dev
5
+ preview, your coworker's half-finished branch deployed on port 8080 — can be
6
+ inspected directly. no tunnel, no deploy, no public exposure.
7
+
8
+ ## usage
9
+
10
+ on your machine:
11
+
12
+ ```sh
13
+ bunx ogthing
14
+ ```
15
+
16
+ your browser opens the inspector. paste any url, including local ones:
17
+
18
+ ```text
19
+ http://localhost:3000 # works. it is running next to you.
20
+ ```
21
+
22
+ the exact url is printed in the terminal. if a compatible Portless proxy is
23
+ already running, ogthing reuses it and normally opens:
24
+
25
+ ```text
26
+ https://ogthing.localhost
27
+ ```
28
+
29
+ otherwise it starts its own unprivileged HTTP proxy, usually at:
30
+
31
+ ```text
32
+ http://ogthing.localhost:1355
33
+ ```
34
+
35
+ no sudo prompt is needed just to get a nice name for localhost.
36
+
37
+ if you want the clean HTTPS url, start Portless yourself before running ogthing:
38
+
39
+ ```sh
40
+ portless proxy start --https --tld localhost
41
+ bunx ogthing
42
+ ```
43
+
44
+ ## what it does
45
+
46
+ * fetches the page **from your machine**, then parses title, description,
47
+ Open Graph, Twitter/X card, favicon, canonical, robots and viewport tags
48
+ * previews how the link renders on Telegram, Discord, Slack, X, Facebook,
49
+ LinkedIn, and WhatsApp
50
+ * scores metadata quality out of 100 with per-check pass/fail dots
51
+ * picks the user agent the way real crawlers do: facebookbot, twitterbot,
52
+ slackbot, discordbot, telegrambot, WhatsApp, LinkedInBot, or a plain browser
53
+ (some sites only serve their social tags to known bots)
54
+ * validates image dimensions against recommended sizes (1200×630 for OG)
55
+ * checks for robots.txt and sitemap.xml
56
+ * copies the metadata as JSON or as ready-to-paste HTML meta tags
57
+ * keeps your last few inspected urls around
58
+
59
+ keyboard shortcuts: keys **1-7** switch tabs while results are showing.
60
+
61
+ because the fetch happens on your device rather than on some hosted inspector,
62
+ sites behind your firewall, your docker network, and plain old localhost are all
63
+ fair game. that is the whole point.
64
+
65
+ ## requirements
66
+
67
+ * Bun `1.3.14` or newer
68
+
69
+ Portless ships with ogthing. you do not need to install it separately.
70
+
71
+ ## environment variables
72
+
73
+ the useful ones:
74
+
75
+ * `OGTHING_PORT` – local app/API port (default: `4646`)
76
+ * `OGTHING_NO_OPEN=1` – start without opening a browser
77
+ * `OGTHING_FETCH_TIMEOUT_MS` – fetch timeout in ms (default: `15000`)
78
+
79
+ Portless knobs, if you need them:
80
+
81
+ * `OGTHING_PROXY_PORT` – preferred proxy port (default: `443`)
82
+ * `OGTHING_PROXY_TLS=0` – use HTTP for the preferred proxy
83
+ * `OGTHING_FALLBACK_PROXY_PORT` – unprivileged fallback port (default: `1355`)
84
+ * `OGTHING_FALLBACK_PROXY_TLS=1` – use HTTPS on the fallback port
85
+ * `OGTHING_PORTLESS_STATE_DIR` – state for the proxy ogthing starts (default: `~/.ogthing/portless`)
86
+ * `OGTHING_NO_PORTLESS=1` – serve directly on `127.0.0.1:<port>` instead
87
+ * `OGTHING_NO_PORTLESS_AUTOSTART=1` – fail instead of starting a proxy automatically
88
+
89
+ example:
90
+
91
+ ```sh
92
+ OGTHING_NO_OPEN=1 OGTHING_PORT=5000 bunx ogthing
93
+ ```
94
+
95
+ ## trust and privacy
96
+
97
+ ogthing does not send your urls anywhere except to the sites themselves. every
98
+ request goes straight from your machine to the target host.
99
+
100
+ note that the inspector will happily fetch internal addresses — that feature is
101
+ not a bug. do not expose your ogthing port or its Portless route to networks you
102
+ do not trust; it has no authentication and it makes requests on behalf of whoever
103
+ asks.
104
+
105
+ ## troubleshooting
106
+
107
+ * **the browser opened the wrong url** – use the exact url printed by ogthing.
108
+ a fresh setup normally uses `http://ogthing.localhost:1355`, not HTTPS on port 443.
109
+ * **a site shows no og tags** – try a crawler user agent in the selector; some
110
+ sites gate social metadata on bot user agents.
111
+ * **Safari cannot resolve ogthing.localhost** – Chrome, Firefox, and Edge resolve
112
+ `.localhost` natively. for Safari, run `portless hosts sync` once.
113
+
114
+ ## development
115
+
116
+ install dependencies and run the named local app:
117
+
118
+ ```sh
119
+ bun install
120
+ bun run dev:named
121
+ ```
122
+
123
+ run the checks:
124
+
125
+ ```sh
126
+ bun run typecheck
127
+ bun run lint
128
+ bun run format:check
129
+ bun test
130
+ bun run build
131
+ bun run pack:check
132
+ ```
133
+
134
+ for a direct smoke test without Portless or a browser:
135
+
136
+ ```sh
137
+ OGTHING_NO_PORTLESS=1 OGTHING_NO_OPEN=1 bun bin/ogthing.mjs
138
+ ```
139
+
140
+ the parser reads the html itself because asking permission from cheerio for
141
+ seven tags felt excessive.
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { spawn } from "node:child_process";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const packageRoot = fileURLToPath(new URL("..", import.meta.url));
7
+ const viteModule = fileURLToPath(await import.meta.resolve("vite"));
8
+ const viteBin = viteModule.replace(/dist[\\/]node[\\/]index\.js$/, "bin/vite.js");
9
+ const port = process.env.PORT || "4646";
10
+
11
+ const child = spawn(process.execPath, [viteBin, "--host", "127.0.0.1", "--port", port], {
12
+ cwd: packageRoot,
13
+ stdio: "inherit",
14
+ env: { ...process.env },
15
+ });
16
+
17
+ const forwardSignal = (signal) => child.kill(signal);
18
+ process.on("SIGINT", () => forwardSignal("SIGINT"));
19
+ process.on("SIGTERM", () => forwardSignal("SIGTERM"));
20
+
21
+ child.on("exit", (code, signal) => {
22
+ if (signal) process.kill(process.pid, signal);
23
+ process.exit(code ?? 0);
24
+ });
@@ -0,0 +1,387 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { spawn } from "node:child_process";
4
+ import { readFile } from "node:fs/promises";
5
+ import http from "node:http";
6
+ import https from "node:https";
7
+ import { createServer } from "node:net";
8
+ import { homedir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+
12
+ const packageRoot = fileURLToPath(new URL("..", import.meta.url));
13
+ const packageMetadata = JSON.parse(
14
+ await readFile(new URL("../package.json", import.meta.url), "utf8"),
15
+ );
16
+ const packageVersion = packageMetadata.version || "unknown";
17
+ const args = process.argv.slice(2);
18
+ const portlessModule = fileURLToPath(await import.meta.resolve("portless"));
19
+ const portlessBin = portlessModule.replace(/dist[\\/]index\.js$/, "dist/cli.js");
20
+ const appEntry = process.env.OGTHING_TEST_APP_ENTRY || "bin/dev-named.mjs";
21
+ const appPort = Number(process.env.OGTHING_PORT || "4646");
22
+ const fallbackProxyPort = Number(process.env.OGTHING_FALLBACK_PROXY_PORT || "1355");
23
+ const ownedPortlessStateDir =
24
+ process.env.OGTHING_PORTLESS_STATE_DIR || join(homedir(), ".ogthing", "portless");
25
+
26
+ const usage = () => {
27
+ console.log(
28
+ `ogthing - inspect link metadata from your own machine\n\nUsage:\n bunx ogthing Inspect any url, localhost included, at ogthing.localhost\n bunx ogthing --version Print the installed ogthing version\n\nEnvironment:\n OGTHING_PORT Local app port (default: 4646)\n OGTHING_PROXY_PORT Portless proxy port (default: 443)\n OGTHING_PROXY_TLS Set to 0 for an HTTP Portless proxy\n OGTHING_FALLBACK_PROXY_PORT Unprivileged fallback port (default: 1355)\n OGTHING_NO_OPEN=1 Start without opening a browser\n OGTHING_NO_PORTLESS=1 Serve directly on 127.0.0.1 without Portless\n OGTHING_NO_PORTLESS_AUTOSTART Fail instead of starting a proxy automatically`,
29
+ );
30
+ };
31
+
32
+ if (args.includes("--help") || args.includes("-h")) {
33
+ usage();
34
+ process.exit(0);
35
+ }
36
+
37
+ if (args.includes("--version") || args.includes("-v")) {
38
+ console.log(`ogthing ${packageVersion}`);
39
+ process.exit(0);
40
+ }
41
+
42
+ console.log(`ogthing ${packageVersion}`);
43
+
44
+ const parsePort = (value, name) => {
45
+ const port = Number(value);
46
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
47
+ throw new Error(`${name} must be a port between 1 and 65535`);
48
+ }
49
+ return port;
50
+ };
51
+
52
+ try {
53
+ parsePort(appPort, "OGTHING_PORT");
54
+ } catch (error) {
55
+ console.error(`ogthing: ${error instanceof Error ? error.message : String(error)}`);
56
+ process.exit(1);
57
+ }
58
+
59
+ const wait = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
60
+
61
+ const requestStatus = (url, { requirePortless = false, requireSuccess = true } = {}) =>
62
+ new Promise((resolve) => {
63
+ const parsed = new URL(url);
64
+ const isHttps = parsed.protocol === "https:";
65
+ let settled = false;
66
+ const finish = (value) => {
67
+ if (settled) return;
68
+ settled = true;
69
+ resolve(value);
70
+ };
71
+ const request = (isHttps ? https : http).request(
72
+ {
73
+ hostname: "127.0.0.1",
74
+ port: parsed.port || (isHttps ? 443 : 80),
75
+ path: `${parsed.pathname}${parsed.search}`,
76
+ method: "GET",
77
+ headers: { host: parsed.host },
78
+ rejectUnauthorized: false,
79
+ servername: parsed.hostname,
80
+ timeout: 750,
81
+ },
82
+ (response) => {
83
+ const portless = response.headers["x-portless"] === "1";
84
+ response.resume();
85
+ finish({
86
+ ok:
87
+ (!requireSuccess || (response.statusCode >= 200 && response.statusCode < 300)) &&
88
+ (!requirePortless || portless),
89
+ portless,
90
+ });
91
+ },
92
+ );
93
+ request.once("error", () => finish({ ok: false, portless: false }));
94
+ request.once("timeout", () => {
95
+ finish({ ok: false, portless: false });
96
+ request.destroy();
97
+ });
98
+ request.end();
99
+ });
100
+
101
+ const waitForUrl = async (url, options, attempts = 100, shouldStop = () => false) => {
102
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
103
+ if (shouldStop()) return false;
104
+ if ((await requestStatus(url, options)).ok) return true;
105
+ await wait(100);
106
+ }
107
+ return false;
108
+ };
109
+
110
+ const proxyUrlFor = (port, tls) => {
111
+ const protocol = tls ? "https" : "http";
112
+ const defaultProxyPort = tls ? 443 : 80;
113
+ return `${protocol}://ogthing.localhost${port === defaultProxyPort ? "" : `:${port}`}/`;
114
+ };
115
+
116
+ const stopProcess = (child) => {
117
+ if (!child || child.exitCode !== null || child.signalCode !== null) return;
118
+ child.kill("SIGTERM");
119
+ setTimeout(() => {
120
+ if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
121
+ }, 2_000).unref();
122
+ };
123
+
124
+ const portlessProxyArgs = (port, tls) => [
125
+ "proxy",
126
+ "start",
127
+ "--foreground",
128
+ "--port",
129
+ String(port),
130
+ tls ? "--https" : "--no-tls",
131
+ ];
132
+
133
+ const portlessProxyEnv = (port, tls, stateDir) => ({
134
+ ...process.env,
135
+ PORTLESS_HTTPS: tls ? "1" : "0",
136
+ PORTLESS_LAN: "0",
137
+ PORTLESS_PORT: String(port),
138
+ ...(stateDir ? { PORTLESS_STATE_DIR: stateDir } : {}),
139
+ PORTLESS_SYNC_HOSTS: "0",
140
+ PORTLESS_TLD: "localhost",
141
+ });
142
+
143
+ const hasCompatiblePortlessConfig = (port, tls) =>
144
+ new Promise((resolve) => {
145
+ const child = spawn(process.execPath, [portlessBin, ...portlessProxyArgs(port, tls)], {
146
+ cwd: packageRoot,
147
+ env: portlessProxyEnv(port, tls, process.env.PORTLESS_STATE_DIR),
148
+ stdio: "ignore",
149
+ });
150
+ let settled = false;
151
+ const finish = (compatible) => {
152
+ if (settled) return;
153
+ settled = true;
154
+ clearTimeout(timeout);
155
+ resolve(compatible);
156
+ };
157
+ const timeout = setTimeout(() => {
158
+ stopProcess(child);
159
+ finish(false);
160
+ }, 3_000);
161
+ child.once("error", () => finish(false));
162
+ child.once("exit", (code) => finish(code === 0));
163
+ });
164
+
165
+ const findAvailablePort = () =>
166
+ new Promise((resolve, reject) => {
167
+ const server = createServer();
168
+ server.once("error", reject);
169
+ server.listen(0, "127.0.0.1", () => {
170
+ const address = server.address();
171
+ if (!address || typeof address === "string") {
172
+ reject(new Error("could not determine an available Portless port"));
173
+ return;
174
+ }
175
+ server.close((error) => (error ? reject(error) : resolve(address.port)));
176
+ });
177
+ });
178
+
179
+ const startPortlessProxy = (port, tls, stateDir) => {
180
+ const child = spawn(process.execPath, [portlessBin, ...portlessProxyArgs(port, tls)], {
181
+ cwd: packageRoot,
182
+ env: portlessProxyEnv(port, tls, stateDir),
183
+ stdio: ["ignore", "ignore", "pipe"],
184
+ });
185
+ let exited = false;
186
+ let stderr = "";
187
+ child.stderr?.setEncoding("utf8");
188
+ child.stderr?.on("data", (chunk) => {
189
+ stderr = `${stderr}${chunk}`.slice(-3000);
190
+ });
191
+ child.once("exit", () => {
192
+ exited = true;
193
+ });
194
+ return {
195
+ child,
196
+ get exited() {
197
+ return exited;
198
+ },
199
+ get stderr() {
200
+ return stderr;
201
+ },
202
+ };
203
+ };
204
+
205
+ const tryStartOwnedProxy = async (port, tls) => {
206
+ const url = proxyUrlFor(port, tls);
207
+ const started = startPortlessProxy(port, tls, ownedPortlessStateDir);
208
+ const ready = await waitForUrl(
209
+ url,
210
+ { requirePortless: true, requireSuccess: false },
211
+ 150,
212
+ () => started.exited,
213
+ );
214
+ if (!ready) {
215
+ stopProcess(started.child);
216
+ return null;
217
+ }
218
+ return { port, tls, url, owned: started.child, stateDir: ownedPortlessStateDir };
219
+ };
220
+
221
+ const ensurePortlessProxy = async (requestedPort, requestedTls) => {
222
+ const requestedUrl = proxyUrlFor(requestedPort, requestedTls);
223
+ const requestedProxyRunning = await waitForUrl(
224
+ requestedUrl,
225
+ { requirePortless: true, requireSuccess: false },
226
+ 3,
227
+ );
228
+ if (requestedProxyRunning && (await hasCompatiblePortlessConfig(requestedPort, requestedTls))) {
229
+ return {
230
+ port: requestedPort,
231
+ tls: requestedTls,
232
+ url: requestedUrl,
233
+ owned: null,
234
+ stateDir: process.env.PORTLESS_STATE_DIR,
235
+ };
236
+ }
237
+
238
+ if (process.env.OGTHING_NO_PORTLESS_AUTOSTART === "1") {
239
+ throw new Error(
240
+ requestedProxyRunning
241
+ ? `Portless at ${requestedUrl.replace(/\/$/, "")} is not configured for ogthing routing`
242
+ : `Portless is not running at ${requestedUrl.replace(/\/$/, "")}`,
243
+ );
244
+ }
245
+
246
+ let port = requestedPort;
247
+ let tls = requestedTls;
248
+ const needsPrivilege = port < 1024 && (process.getuid?.() ?? 1000) !== 0;
249
+ if (needsPrivilege || requestedProxyRunning) {
250
+ port = parsePort(fallbackProxyPort, "OGTHING_FALLBACK_PROXY_PORT");
251
+ tls = process.env.OGTHING_FALLBACK_PROXY_TLS === "1";
252
+ }
253
+
254
+ let url = proxyUrlFor(port, tls);
255
+ const fallbackRunning = await waitForUrl(
256
+ url,
257
+ { requirePortless: true, requireSuccess: false },
258
+ 3,
259
+ );
260
+ if (fallbackRunning && (await hasCompatiblePortlessConfig(port, tls))) {
261
+ return {
262
+ port,
263
+ tls,
264
+ url,
265
+ owned: null,
266
+ stateDir: process.env.PORTLESS_STATE_DIR,
267
+ };
268
+ }
269
+ if (!fallbackRunning) {
270
+ const started = await tryStartOwnedProxy(port, tls);
271
+ if (started) return started;
272
+ }
273
+
274
+ // The fallback port is taken by something incompatible, or it refused us.
275
+ // Grab a random free port so ogthing still gets a named URL.
276
+ port = await findAvailablePort();
277
+ const started = await tryStartOwnedProxy(port, tls);
278
+ if (started) {
279
+ console.log(`ogthing: using fallback proxy port ${port}`);
280
+ return started;
281
+ }
282
+ throw new Error(`Portless could not start at ${url.replace(/\/$/, "")}`);
283
+ };
284
+
285
+ const openBrowser = (url) => {
286
+ if (process.env.OGTHING_NO_OPEN === "1") return;
287
+ const command =
288
+ process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
289
+ const commandArgs = process.platform === "win32" ? ["/c", "start", "", url] : [url];
290
+ const browser = spawn(command, commandArgs, { detached: true, stdio: "ignore" });
291
+ browser.once("error", () => {});
292
+ browser.unref();
293
+ };
294
+
295
+ let localUrl = "";
296
+ try {
297
+ const requestedProxyPort = parsePort(
298
+ process.env.OGTHING_PROXY_PORT || "443",
299
+ "OGTHING_PROXY_PORT",
300
+ );
301
+ const requestedProxyTls = process.env.OGTHING_PROXY_TLS !== "0";
302
+ const bypassPortless = process.env.OGTHING_NO_PORTLESS === "1";
303
+ const proxy = bypassPortless
304
+ ? {
305
+ port: requestedProxyPort,
306
+ tls: requestedProxyTls,
307
+ url: `http://127.0.0.1:${appPort}/`,
308
+ owned: null,
309
+ }
310
+ : await ensurePortlessProxy(requestedProxyPort, requestedProxyTls);
311
+ const { port: proxyPort, tls: proxyTls, url: proxyUrl, owned: ownedProxy } = proxy;
312
+ localUrl = bypassPortless ? proxy.url : proxyUrl;
313
+ const appUrl = `http://127.0.0.1:${appPort}/`;
314
+
315
+ const env = {
316
+ ...process.env,
317
+ OGTHING_MODE: "server",
318
+ PORTLESS_HTTPS: proxyTls ? "1" : "0",
319
+ PORTLESS_PORT: String(proxyPort),
320
+ PORTLESS_TLD: "localhost",
321
+ PORTLESS_LAN: "0",
322
+ PORTLESS_SYNC_HOSTS: "0",
323
+ PORT: String(appPort),
324
+ ...(proxy.stateDir ? { PORTLESS_STATE_DIR: proxy.stateDir } : {}),
325
+ };
326
+
327
+ const child = spawn(
328
+ process.execPath,
329
+ bypassPortless
330
+ ? [appEntry]
331
+ : [
332
+ portlessBin,
333
+ "ogthing",
334
+ "--force",
335
+ "--app-port",
336
+ String(appPort),
337
+ process.execPath,
338
+ appEntry,
339
+ ],
340
+ { cwd: packageRoot, env, stdio: ["ignore", "ignore", "pipe"] },
341
+ );
342
+
343
+ let childExited = false;
344
+ let stderr = "";
345
+ child.stderr?.setEncoding("utf8");
346
+ child.stderr?.on("data", (chunk) => {
347
+ stderr = `${stderr}${chunk}`.slice(-3000);
348
+ });
349
+
350
+ const forwardSignal = (signal) => {
351
+ if (childExited) return;
352
+ child.kill(signal);
353
+ stopProcess(ownedProxy);
354
+ setTimeout(() => {
355
+ if (!childExited) child.kill("SIGKILL");
356
+ }, 2000).unref();
357
+ };
358
+
359
+ process.once("SIGINT", () => forwardSignal("SIGINT"));
360
+ process.once("SIGTERM", () => forwardSignal("SIGTERM"));
361
+
362
+ child.once("error", (error) => {
363
+ stderr = `${stderr}\n${error.message}`.slice(-3000);
364
+ });
365
+
366
+ child.once("exit", (code, signal) => {
367
+ childExited = true;
368
+ stopProcess(ownedProxy);
369
+ if (code !== 0 && code !== 130 && code !== 143 && signal === null) {
370
+ const detail = stderr.trim().split("\n").filter(Boolean).slice(-1)[0];
371
+ console.error(`ogthing: ${detail || `the app exited with code ${code}`}`);
372
+ }
373
+ process.exit(signal ? 128 : (code ?? 0));
374
+ });
375
+
376
+ if (await waitForUrl(appUrl, {}, 150, () => childExited)) {
377
+ if (ownedProxy) console.log(`ogthing: started Portless at ${proxyUrl}`);
378
+ console.log(`ogthing running at ${localUrl}`);
379
+ openBrowser(localUrl);
380
+ } else if (!childExited) {
381
+ console.error("ogthing: Portless could not route to the local app");
382
+ forwardSignal("SIGTERM");
383
+ }
384
+ } catch (error) {
385
+ console.error(`ogthing: ${error instanceof Error ? error.message : String(error)}`);
386
+ process.exit(1);
387
+ }
package/index.html ADDED
@@ -0,0 +1,17 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="color-scheme" content="dark light" />
7
+ <link
8
+ rel="icon"
9
+ href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='.9em' font-size='90'%3E%F0%9F%A6%87%3C/text%3E%3C/svg%3E"
10
+ />
11
+ <title>ogthing</title>
12
+ </head>
13
+ <body>
14
+ <div id="root"></div>
15
+ <script type="module" src="/src/main.tsx"></script>
16
+ </body>
17
+ </html>
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "ogthing",
3
+ "version": "0.0.0-alpha.1",
4
+ "description": "Inspect Open Graph and social metadata from your own machine, localhost included.",
5
+ "keywords": [
6
+ "cli",
7
+ "opengraph",
8
+ "open-graph",
9
+ "metadata",
10
+ "seo",
11
+ "link-preview",
12
+ "twitter-card",
13
+ "bun"
14
+ ],
15
+ "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/extoci/ogthing.git"
19
+ },
20
+ "bin": {
21
+ "ogthing": "bin/ogthing.mjs"
22
+ },
23
+ "files": [
24
+ "bin",
25
+ "server",
26
+ "src",
27
+ "index.html",
28
+ "tsconfig.json",
29
+ "vite.config.ts",
30
+ "portless.json",
31
+ "README.md",
32
+ "LICENSE"
33
+ ],
34
+ "type": "module",
35
+ "scripts": {
36
+ "dev": "vite",
37
+ "dev:named": "bun bin/dev-named.mjs",
38
+ "build": "vite build",
39
+ "start": "vite preview",
40
+ "test": "bun run build && bun test",
41
+ "typecheck": "tsc --noEmit",
42
+ "lint": "oxlint bin server src tests vite.config.ts",
43
+ "format": "oxfmt --write bin server src tests vite.config.ts",
44
+ "format:check": "oxfmt --check bin server src tests vite.config.ts",
45
+ "pack:check": "bun pm pack --dry-run",
46
+ "prepublishOnly": "bun run typecheck && bun run lint && bun run format:check && bun test && bun run build"
47
+ },
48
+ "dependencies": {
49
+ "@vitejs/plugin-react": "6.0.2",
50
+ "portless": "0.12.0",
51
+ "react": "19.2.6",
52
+ "react-dom": "19.2.6",
53
+ "vite": "8.0.13"
54
+ },
55
+ "devDependencies": {
56
+ "@types/node": "22.19.19",
57
+ "@types/react": "19.2.14",
58
+ "@types/react-dom": "19.2.3",
59
+ "bun-types": "1.3.14",
60
+ "oxfmt": "0.61.0",
61
+ "oxlint": "1.76.0",
62
+ "typescript": "7.0.2"
63
+ },
64
+ "engines": {
65
+ "bun": ">=1.3.14"
66
+ },
67
+ "publishConfig": {
68
+ "access": "public",
69
+ "tag": "alpha"
70
+ },
71
+ "packageManager": "bun@1.3.14"
72
+ }
package/portless.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "ogthing",
3
+ "script": "dev:named"
4
+ }