universal-logs 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/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # universal-logs
2
+
3
+ `universal-logs` is a small npm package with two parts:
4
+
5
+ - A CLI that reads logs from HTTP endpoints or React Native Metro
6
+ - Runtime helpers that expose HTTP log snapshots from Node or Electron apps
7
+
8
+ ## CLI
9
+
10
+ Run against a named target from config:
11
+
12
+ ```bash
13
+ npx universal-logs electron --limit 80
14
+ ```
15
+
16
+ Run against an explicit endpoint:
17
+
18
+ ```bash
19
+ npx universal-logs --endpoint http://127.0.0.1:6973/logs --limit 80
20
+ ```
21
+
22
+ Read live React Native logs from Metro:
23
+
24
+ ```bash
25
+ npx universal-logs metro --app com.fieldy --limit 80 --timeout 5000
26
+ ```
27
+
28
+ List configured targets:
29
+
30
+ ```bash
31
+ npx universal-logs list
32
+ ```
33
+
34
+ ## Config
35
+
36
+ Create `universal-logs.config.json` in your repo:
37
+
38
+ ```json
39
+ {
40
+ "targets": {
41
+ "electron": {
42
+ "endpoint": "http://127.0.0.1:6973/logs"
43
+ },
44
+ "api": {
45
+ "endpoint": "http://127.0.0.1:6971/logs"
46
+ },
47
+ "native": {
48
+ "provider": "metro",
49
+ "metroUrl": "http://127.0.0.1:8081",
50
+ "app": "com.fieldy",
51
+ "timeoutMs": 5000
52
+ }
53
+ }
54
+ }
55
+ ```
56
+
57
+ The CLI also reads `package.json` from a top-level `universalLogs` field.
58
+
59
+ Configured Metro targets work like any other target:
60
+
61
+ ```bash
62
+ npx universal-logs native --limit 80
63
+ ```
64
+
65
+ Metro is a live stream, not a snapshot. The CLI listens for new console events until it reaches `--limit` or `--timeout`.
66
+
67
+ ## Runtime
68
+
69
+ Expose a log buffer from a Node process:
70
+
71
+ ```ts
72
+ import {
73
+ createConsoleMirror,
74
+ createLogBuffer,
75
+ startHttpLogEndpoint,
76
+ } from "universal-logs/runtime"
77
+
78
+ const buffer = createLogBuffer({ limit: 500 })
79
+ createConsoleMirror({ buffer, source: "api" })
80
+
81
+ await startHttpLogEndpoint({
82
+ buffer,
83
+ port: 6971,
84
+ service: "api",
85
+ })
86
+ ```
87
+
88
+ Attach renderer logs in Electron:
89
+
90
+ ```ts
91
+ import {
92
+ attachElectronRendererConsoleMirror,
93
+ createLogBuffer,
94
+ } from "universal-logs/runtime"
95
+
96
+ const buffer = createLogBuffer()
97
+ app.on("browser-window-created", (_event, window) => {
98
+ attachElectronRendererConsoleMirror({
99
+ buffer,
100
+ webContents: window.webContents,
101
+ })
102
+ })
103
+ ```
104
+
105
+ Metro support does not need a runtime helper. It attaches to Metro's inspector endpoints at `/json/list` and `/json`, then listens for `Runtime.consoleAPICalled` over CDP.
106
+
107
+ ## Response shape
108
+
109
+ `GET /logs?limit=100` returns:
110
+
111
+ ```json
112
+ {
113
+ "service": "electron",
114
+ "limit": 100,
115
+ "total": 42,
116
+ "entries": [
117
+ {
118
+ "timestamp": "2026-04-03T10:00:00.000Z",
119
+ "level": "info",
120
+ "source": "desktop:renderer",
121
+ "message": "hello"
122
+ }
123
+ ]
124
+ }
125
+ ```
@@ -0,0 +1,198 @@
1
+ // src/runtime/console-mirror.ts
2
+ import util from "util";
3
+ var consoleMethodLevels = {
4
+ debug: "debug",
5
+ error: "error",
6
+ info: "info",
7
+ log: "info",
8
+ trace: "trace",
9
+ warn: "warn"
10
+ };
11
+ function createConsoleMirror(options) {
12
+ const { buffer, consoleObject = console, source = "console" } = options;
13
+ const originals = /* @__PURE__ */ new Map();
14
+ const consoleTarget = consoleObject;
15
+ for (const method of Object.keys(
16
+ consoleMethodLevels
17
+ )) {
18
+ originals.set(method, consoleTarget[method].bind(consoleObject));
19
+ const level = consoleMethodLevels[method];
20
+ consoleTarget[method] = (...args) => {
21
+ buffer.append({
22
+ level,
23
+ message: util.formatWithOptions(
24
+ {
25
+ colors: false,
26
+ depth: 5,
27
+ maxArrayLength: 50
28
+ },
29
+ ...args
30
+ ),
31
+ source,
32
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
33
+ });
34
+ const original = originals.get(method);
35
+ if (original) {
36
+ original(...args);
37
+ }
38
+ };
39
+ }
40
+ return {
41
+ restore() {
42
+ for (const method of Object.keys(
43
+ consoleMethodLevels
44
+ )) {
45
+ const original = originals.get(method);
46
+ if (original) {
47
+ consoleTarget[method] = original;
48
+ }
49
+ }
50
+ }
51
+ };
52
+ }
53
+
54
+ // src/runtime/electron.ts
55
+ function attachElectronRendererConsoleMirror(options) {
56
+ const { buffer, source = "electron:renderer", webContents } = options;
57
+ webContents.on(
58
+ "console-message",
59
+ (_event, level, message, line, sourceId) => {
60
+ const location = sourceId.length > 0 ? ` (${sourceId}${line > 0 ? `:${line}` : ""})` : "";
61
+ buffer.append({
62
+ level: normalizeElectronLogLevel(level),
63
+ message: `${message}${location}`,
64
+ source,
65
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
66
+ });
67
+ }
68
+ );
69
+ }
70
+ function normalizeElectronLogLevel(level) {
71
+ if (level >= 3) {
72
+ return "error";
73
+ }
74
+ if (level === 2) {
75
+ return "warn";
76
+ }
77
+ if (level === 0) {
78
+ return "debug";
79
+ }
80
+ return "info";
81
+ }
82
+
83
+ // src/runtime/http-endpoint.ts
84
+ import http from "http";
85
+ var DEFAULT_LIMIT = 200;
86
+ var MAX_LIMIT = 500;
87
+ async function startHttpLogEndpoint(options) {
88
+ const {
89
+ buffer,
90
+ healthPath = "/health",
91
+ host = "127.0.0.1",
92
+ logsPath = "/logs",
93
+ port,
94
+ service
95
+ } = options;
96
+ const server = http.createServer((req, res) => {
97
+ const requestUrl = new URL(req.url ?? "/", `http://${host}:${port}`);
98
+ if (req.method === "GET" && requestUrl.pathname === healthPath) {
99
+ writeJson(res, 200, { service, status: "ok" });
100
+ return;
101
+ }
102
+ if (req.method === "GET" && requestUrl.pathname === logsPath) {
103
+ const limit = normalizeLimit(requestUrl.searchParams.get("limit"));
104
+ writeJson(res, 200, buffer.getSnapshot(service, limit));
105
+ return;
106
+ }
107
+ writeJson(res, 404, { error: "Not Found" });
108
+ });
109
+ await new Promise((resolve, reject) => {
110
+ server.once("error", reject);
111
+ server.listen(port, host, () => {
112
+ server.off("error", reject);
113
+ resolve();
114
+ });
115
+ });
116
+ const address = server.address();
117
+ const addressHost = address?.address ?? host;
118
+ const addressPort = address?.port ?? port;
119
+ const url = `http://${addressHost}:${addressPort}${logsPath}`;
120
+ return {
121
+ async close() {
122
+ await new Promise((resolve, reject) => {
123
+ server.close((error) => {
124
+ if (error) {
125
+ reject(error);
126
+ return;
127
+ }
128
+ resolve();
129
+ });
130
+ });
131
+ },
132
+ server,
133
+ url
134
+ };
135
+ }
136
+ function normalizeLimit(limitParam) {
137
+ const raw = Number(limitParam ?? DEFAULT_LIMIT);
138
+ if (!Number.isFinite(raw) || raw <= 0) {
139
+ return DEFAULT_LIMIT;
140
+ }
141
+ return Math.min(MAX_LIMIT, Math.floor(raw));
142
+ }
143
+ function writeJson(response, statusCode, payload) {
144
+ response.writeHead(statusCode, {
145
+ "Cache-Control": "no-store",
146
+ "Content-Type": "application/json"
147
+ });
148
+ response.end(JSON.stringify(payload));
149
+ }
150
+
151
+ // src/runtime/log-buffer.ts
152
+ var DEFAULT_BUFFER_LIMIT = 1e3;
153
+ var DEFAULT_SNAPSHOT_LIMIT = 200;
154
+ function createLogBuffer(options = {}) {
155
+ const bufferLimit = normalizePositiveInt(options.limit, DEFAULT_BUFFER_LIMIT);
156
+ const entries = [];
157
+ return {
158
+ append(entry) {
159
+ entries.push(entry);
160
+ if (entries.length > bufferLimit) {
161
+ entries.splice(0, entries.length - bufferLimit);
162
+ }
163
+ },
164
+ clear() {
165
+ entries.length = 0;
166
+ },
167
+ getEntries(limit = DEFAULT_SNAPSHOT_LIMIT) {
168
+ const snapshotLimit = normalizePositiveInt(limit, DEFAULT_SNAPSHOT_LIMIT);
169
+ return entries.slice(Math.max(0, entries.length - snapshotLimit));
170
+ },
171
+ getSnapshot(service, limit = DEFAULT_SNAPSHOT_LIMIT) {
172
+ const snapshotLimit = normalizePositiveInt(limit, DEFAULT_SNAPSHOT_LIMIT);
173
+ return {
174
+ entries: this.getEntries(snapshotLimit),
175
+ limit: snapshotLimit,
176
+ service,
177
+ total: entries.length
178
+ };
179
+ },
180
+ size() {
181
+ return entries.length;
182
+ }
183
+ };
184
+ }
185
+ function normalizePositiveInt(value, fallback) {
186
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
187
+ return fallback;
188
+ }
189
+ return Math.floor(value);
190
+ }
191
+
192
+ export {
193
+ createConsoleMirror,
194
+ attachElectronRendererConsoleMirror,
195
+ startHttpLogEndpoint,
196
+ createLogBuffer
197
+ };
198
+ //# sourceMappingURL=chunk-IUSSBPEQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/runtime/console-mirror.ts","../src/runtime/electron.ts","../src/runtime/http-endpoint.ts","../src/runtime/log-buffer.ts"],"sourcesContent":["import util from \"node:util\"\nimport type { LogBuffer, UniversalLogLevel } from \"./types\"\n\nconst consoleMethodLevels = {\n debug: \"debug\",\n error: \"error\",\n info: \"info\",\n log: \"info\",\n trace: \"trace\",\n warn: \"warn\",\n} as const\n\ntype ConsoleMethodName = keyof typeof consoleMethodLevels\n\nexport function createConsoleMirror(options: {\n buffer: LogBuffer\n consoleObject?: Console\n source?: string\n}): { restore: () => void } {\n const { buffer, consoleObject = console, source = \"console\" } = options\n const originals = new Map<ConsoleMethodName, (...args: unknown[]) => void>()\n const consoleTarget = consoleObject as Console &\n Record<ConsoleMethodName, (...args: unknown[]) => void>\n\n for (const method of Object.keys(\n consoleMethodLevels,\n ) as ConsoleMethodName[]) {\n originals.set(method, consoleTarget[method].bind(consoleObject))\n const level = consoleMethodLevels[method] satisfies UniversalLogLevel\n\n consoleTarget[method] = (...args: unknown[]): void => {\n buffer.append({\n level,\n message: util.formatWithOptions(\n {\n colors: false,\n depth: 5,\n maxArrayLength: 50,\n },\n ...args,\n ),\n source,\n timestamp: new Date().toISOString(),\n })\n\n const original = originals.get(method)\n if (original) {\n original(...args)\n }\n }\n }\n\n return {\n restore(): void {\n for (const method of Object.keys(\n consoleMethodLevels,\n ) as ConsoleMethodName[]) {\n const original = originals.get(method)\n if (original) {\n consoleTarget[method] = original\n }\n }\n },\n }\n}\n","import type { LogBuffer, UniversalLogLevel } from \"./types\"\n\ntype ElectronConsoleMessageSource = {\n on: (\n event: \"console-message\",\n listener: (\n event: unknown,\n level: number,\n message: string,\n line: number,\n sourceId: string,\n ) => void,\n ) => void\n}\n\nexport function attachElectronRendererConsoleMirror(options: {\n buffer: LogBuffer\n source?: string\n webContents: ElectronConsoleMessageSource\n}): void {\n const { buffer, source = \"electron:renderer\", webContents } = options\n\n webContents.on(\n \"console-message\",\n (_event, level, message, line, sourceId) => {\n const location =\n sourceId.length > 0 ? ` (${sourceId}${line > 0 ? `:${line}` : \"\"})` : \"\"\n\n buffer.append({\n level: normalizeElectronLogLevel(level),\n message: `${message}${location}`,\n source,\n timestamp: new Date().toISOString(),\n })\n },\n )\n}\n\nfunction normalizeElectronLogLevel(level: number): UniversalLogLevel {\n if (level >= 3) {\n return \"error\"\n }\n\n if (level === 2) {\n return \"warn\"\n }\n\n if (level === 0) {\n return \"debug\"\n }\n\n return \"info\"\n}\n","import http from \"node:http\"\nimport type { AddressInfo } from \"node:net\"\nimport type { LogBuffer, UniversalLogSnapshot } from \"./types\"\n\nconst DEFAULT_LIMIT = 200\nconst MAX_LIMIT = 500\n\nexport async function startHttpLogEndpoint(options: {\n buffer: LogBuffer\n healthPath?: string\n host?: string\n logsPath?: string\n port: number\n service: string\n}): Promise<{\n close: () => Promise<void>\n server: http.Server\n url: string\n}> {\n const {\n buffer,\n healthPath = \"/health\",\n host = \"127.0.0.1\",\n logsPath = \"/logs\",\n port,\n service,\n } = options\n\n const server = http.createServer((req, res) => {\n const requestUrl = new URL(req.url ?? \"/\", `http://${host}:${port}`)\n\n if (req.method === \"GET\" && requestUrl.pathname === healthPath) {\n writeJson(res, 200, { service, status: \"ok\" })\n return\n }\n\n if (req.method === \"GET\" && requestUrl.pathname === logsPath) {\n const limit = normalizeLimit(requestUrl.searchParams.get(\"limit\"))\n writeJson(res, 200, buffer.getSnapshot(service, limit))\n return\n }\n\n writeJson(res, 404, { error: \"Not Found\" })\n })\n\n await new Promise<void>((resolve, reject) => {\n server.once(\"error\", reject)\n server.listen(port, host, () => {\n server.off(\"error\", reject)\n resolve()\n })\n })\n\n const address = server.address() as AddressInfo | null\n const addressHost = address?.address ?? host\n const addressPort = address?.port ?? port\n const url = `http://${addressHost}:${addressPort}${logsPath}`\n\n return {\n async close(): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n server.close((error) => {\n if (error) {\n reject(error)\n return\n }\n\n resolve()\n })\n })\n },\n server,\n url,\n }\n}\n\nfunction normalizeLimit(limitParam: string | null): number {\n const raw = Number(limitParam ?? DEFAULT_LIMIT)\n if (!Number.isFinite(raw) || raw <= 0) {\n return DEFAULT_LIMIT\n }\n\n return Math.min(MAX_LIMIT, Math.floor(raw))\n}\n\nfunction writeJson(\n response: http.ServerResponse,\n statusCode: number,\n payload:\n | UniversalLogSnapshot\n | { error: string }\n | { service: string; status: string },\n): void {\n response.writeHead(statusCode, {\n \"Cache-Control\": \"no-store\",\n \"Content-Type\": \"application/json\",\n })\n response.end(JSON.stringify(payload))\n}\n","import type {\n LogBuffer,\n UniversalLogEntry,\n UniversalLogSnapshot,\n} from \"./types\"\n\nconst DEFAULT_BUFFER_LIMIT = 1000\nconst DEFAULT_SNAPSHOT_LIMIT = 200\n\nexport function createLogBuffer(options: { limit?: number } = {}): LogBuffer {\n const bufferLimit = normalizePositiveInt(options.limit, DEFAULT_BUFFER_LIMIT)\n const entries: UniversalLogEntry[] = []\n\n return {\n append(entry): void {\n entries.push(entry)\n\n if (entries.length > bufferLimit) {\n entries.splice(0, entries.length - bufferLimit)\n }\n },\n clear(): void {\n entries.length = 0\n },\n getEntries(limit = DEFAULT_SNAPSHOT_LIMIT): UniversalLogEntry[] {\n const snapshotLimit = normalizePositiveInt(limit, DEFAULT_SNAPSHOT_LIMIT)\n return entries.slice(Math.max(0, entries.length - snapshotLimit))\n },\n getSnapshot(\n service: string,\n limit = DEFAULT_SNAPSHOT_LIMIT,\n ): UniversalLogSnapshot {\n const snapshotLimit = normalizePositiveInt(limit, DEFAULT_SNAPSHOT_LIMIT)\n\n return {\n entries: this.getEntries(snapshotLimit),\n limit: snapshotLimit,\n service,\n total: entries.length,\n }\n },\n size(): number {\n return entries.length\n },\n }\n}\n\nfunction normalizePositiveInt(\n value: number | undefined,\n fallback: number,\n): number {\n if (typeof value !== \"number\" || !Number.isFinite(value) || value <= 0) {\n return fallback\n }\n\n return Math.floor(value)\n}\n"],"mappings":"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","names":[]}
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,670 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/lib/config.ts
4
+ import fs from "fs";
5
+ import path from "path";
6
+ var CONFIG_FILE_NAME = "universal-logs.config.json";
7
+ function findConfigPath(cwd) {
8
+ let currentDir = path.resolve(cwd);
9
+ while (true) {
10
+ const configPath = path.join(currentDir, CONFIG_FILE_NAME);
11
+ if (fs.existsSync(configPath)) {
12
+ return configPath;
13
+ }
14
+ const packageJsonPath = path.join(currentDir, "package.json");
15
+ if (fs.existsSync(packageJsonPath)) {
16
+ const packageJson = JSON.parse(
17
+ fs.readFileSync(packageJsonPath, "utf8")
18
+ );
19
+ if (packageJson.universalLogs) {
20
+ return packageJsonPath;
21
+ }
22
+ }
23
+ const parentDir = path.dirname(currentDir);
24
+ if (parentDir === currentDir) {
25
+ return null;
26
+ }
27
+ currentDir = parentDir;
28
+ }
29
+ }
30
+ function loadConfig(options = {}) {
31
+ const { configPath, cwd = process.cwd() } = options;
32
+ const resolvedPath = configPath ? path.resolve(cwd, configPath) : findConfigPath(cwd);
33
+ if (!resolvedPath) {
34
+ return null;
35
+ }
36
+ if (path.basename(resolvedPath) === "package.json") {
37
+ const packageJson = JSON.parse(fs.readFileSync(resolvedPath, "utf8"));
38
+ if (!packageJson.universalLogs) {
39
+ throw new Error(`No universalLogs field found in ${resolvedPath}`);
40
+ }
41
+ return validateConfig(packageJson.universalLogs, resolvedPath);
42
+ }
43
+ const parsed = JSON.parse(
44
+ fs.readFileSync(resolvedPath, "utf8")
45
+ );
46
+ return validateConfig(parsed, resolvedPath);
47
+ }
48
+ function validateConfig(config, sourcePath) {
49
+ if (!config || typeof config !== "object" || !config.targets) {
50
+ throw new Error(`Invalid config in ${sourcePath}: missing targets object`);
51
+ }
52
+ const targets = Object.fromEntries(
53
+ Object.entries(config.targets).map(([name, target]) => [
54
+ name,
55
+ validateTarget(name, target, sourcePath)
56
+ ])
57
+ );
58
+ return {
59
+ targets
60
+ };
61
+ }
62
+ function validateTarget(name, target, sourcePath) {
63
+ if (!target || typeof target !== "object") {
64
+ throw new Error(`Invalid target "${name}" in ${sourcePath}`);
65
+ }
66
+ if (target.provider === "metro") {
67
+ if (target.app !== void 0 && typeof target.app !== "string") {
68
+ throw new Error(`Invalid target "${name}" in ${sourcePath}: invalid app`);
69
+ }
70
+ if (target.metroUrl !== void 0 && typeof target.metroUrl !== "string") {
71
+ throw new Error(
72
+ `Invalid target "${name}" in ${sourcePath}: invalid metroUrl`
73
+ );
74
+ }
75
+ if (target.timeoutMs !== void 0 && (!Number.isFinite(target.timeoutMs) || target.timeoutMs <= 0)) {
76
+ throw new Error(
77
+ `Invalid target "${name}" in ${sourcePath}: invalid timeoutMs`
78
+ );
79
+ }
80
+ return target;
81
+ }
82
+ if (target.provider !== void 0 && target.provider !== "endpoint") {
83
+ throw new Error(
84
+ `Invalid target "${name}" in ${sourcePath}: unsupported provider`
85
+ );
86
+ }
87
+ if (typeof target.endpoint !== "string") {
88
+ throw new Error(
89
+ `Invalid target "${name}" in ${sourcePath}: missing endpoint`
90
+ );
91
+ }
92
+ if (target.headers !== void 0 && !isStringRecord(target.headers)) {
93
+ throw new Error(
94
+ `Invalid target "${name}" in ${sourcePath}: invalid headers`
95
+ );
96
+ }
97
+ return target;
98
+ }
99
+ function isStringRecord(value) {
100
+ if (!value || typeof value !== "object") {
101
+ return false;
102
+ }
103
+ return Object.values(value).every((entry) => typeof entry === "string");
104
+ }
105
+
106
+ // src/lib/fetch-logs.ts
107
+ async function fetchLogs(options) {
108
+ const { endpoint, headers, limit } = options;
109
+ const url = new URL(endpoint);
110
+ url.searchParams.set("limit", String(limit));
111
+ let response;
112
+ try {
113
+ response = await fetch(url, {
114
+ headers
115
+ });
116
+ } catch (error) {
117
+ throw new Error(
118
+ `Failed to fetch logs from ${url.origin}${url.pathname}: ${formatFetchError(error)}`
119
+ );
120
+ }
121
+ if (!response.ok) {
122
+ throw new Error(
123
+ `Endpoint returned ${response.status} ${response.statusText}`
124
+ );
125
+ }
126
+ return await response.json();
127
+ }
128
+ function formatFetchError(error) {
129
+ if (!(error instanceof Error)) {
130
+ return String(error);
131
+ }
132
+ const cause = error.cause;
133
+ if (cause instanceof Error && cause.message) {
134
+ return cause.message;
135
+ }
136
+ if (cause && typeof cause === "object" && "code" in cause && typeof cause.code === "string") {
137
+ return cause.code;
138
+ }
139
+ return error.message;
140
+ }
141
+
142
+ // src/lib/fetch-metro-logs.ts
143
+ import WebSocket from "ws";
144
+ var DEFAULT_METRO_URL = "http://127.0.0.1:8081";
145
+ var DEFAULT_METRO_TIMEOUT_MS = 5e3;
146
+ async function fetchMetroLogs(options) {
147
+ const metroOrigin = getMetroServerOrigin(options.metroUrl);
148
+ const apps = await fetchMetroInspectorApps(metroOrigin);
149
+ if (apps.length === 0) {
150
+ const status = await fetchMetroStatus(metroOrigin);
151
+ if (status?.includes("packager-status:running")) {
152
+ throw new Error(
153
+ `Metro is running at ${metroOrigin}, but no React Native inspector apps are connected. Open the app on a simulator/device and make sure it is using this Metro server.`
154
+ );
155
+ }
156
+ throw new Error(
157
+ `No supported React Native inspector apps found at ${metroOrigin}`
158
+ );
159
+ }
160
+ const app = selectMetroInspectorApp(apps, options.app);
161
+ const entries = await captureMetroConsoleLogs({
162
+ app,
163
+ limit: options.limit,
164
+ timeoutMs: options.timeoutMs ?? DEFAULT_METRO_TIMEOUT_MS
165
+ });
166
+ return {
167
+ entries,
168
+ limit: options.limit,
169
+ service: options.service ?? app.appId ?? "metro",
170
+ total: entries.length
171
+ };
172
+ }
173
+ function getMetroServerOrigin(rawMetroUrl) {
174
+ const input = rawMetroUrl?.trim() || DEFAULT_METRO_URL;
175
+ const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(input);
176
+ const url = hasScheme ? new URL(input) : new URL(`http://${input}`);
177
+ if (!url.port) {
178
+ url.port = "8081";
179
+ }
180
+ return url.origin;
181
+ }
182
+ function formatMetroTarget(target) {
183
+ const parts = ["metro", getMetroServerOrigin(target.metroUrl)];
184
+ if (target.app) {
185
+ parts.push(`app=${target.app}`);
186
+ }
187
+ if (target.timeoutMs) {
188
+ parts.push(`timeout=${target.timeoutMs}ms`);
189
+ }
190
+ return parts.join(" ");
191
+ }
192
+ async function fetchMetroInspectorApps(metroOrigin) {
193
+ const endpoints = ["/json/list", "/json"];
194
+ let lastError = null;
195
+ for (const endpoint of endpoints) {
196
+ try {
197
+ const response = await fetch(`${metroOrigin}${endpoint}`);
198
+ if (!response.ok) {
199
+ continue;
200
+ }
201
+ const data = await response.json();
202
+ if (!Array.isArray(data)) {
203
+ continue;
204
+ }
205
+ const apps = [...data].reverse().filter(isSupportedMetroApp);
206
+ if (apps.length > 0) {
207
+ return apps;
208
+ }
209
+ return [...data].reverse().filter(hasDebuggerUrl);
210
+ } catch (error) {
211
+ lastError = normalizeError(error);
212
+ }
213
+ }
214
+ if (lastError) {
215
+ throw new Error(
216
+ `Failed to query Metro inspector at ${metroOrigin}: ${lastError.message}`
217
+ );
218
+ }
219
+ return [];
220
+ }
221
+ async function fetchMetroStatus(metroOrigin) {
222
+ try {
223
+ const response = await fetch(`${metroOrigin}/status`);
224
+ if (!response.ok) {
225
+ return null;
226
+ }
227
+ return await response.text();
228
+ } catch {
229
+ return null;
230
+ }
231
+ }
232
+ function isSupportedMetroApp(app) {
233
+ if (!hasDebuggerUrl(app)) {
234
+ return false;
235
+ }
236
+ return app.title === "React Native Experimental (Improved Chrome Reloads)" || app.reactNative?.capabilities?.nativePageReloads === true;
237
+ }
238
+ function hasDebuggerUrl(app) {
239
+ return Boolean(
240
+ app && typeof app === "object" && "id" in app && "title" in app && "webSocketDebuggerUrl" in app && typeof app.id === "string" && typeof app.title === "string" && typeof app.webSocketDebuggerUrl === "string"
241
+ );
242
+ }
243
+ function selectMetroInspectorApp(apps, selector) {
244
+ if (!selector) {
245
+ if (apps.length === 1) {
246
+ return apps[0];
247
+ }
248
+ throw new Error(
249
+ `Multiple Metro apps found. Pass --app. Available: ${apps.map(formatMetroApp).join(", ")}`
250
+ );
251
+ }
252
+ const normalizedSelector = selector.trim().toLowerCase();
253
+ const matches = apps.filter((app) => {
254
+ return [app.id, app.appId, app.deviceName].filter((value) => Boolean(value)).some((value) => value.toLowerCase() === normalizedSelector);
255
+ });
256
+ if (matches.length === 1) {
257
+ return matches[0];
258
+ }
259
+ if (matches.length > 1) {
260
+ throw new Error(
261
+ `Multiple Metro apps match "${selector}". Available: ${apps.map(formatMetroApp).join(", ")}`
262
+ );
263
+ }
264
+ throw new Error(
265
+ `No Metro app matched "${selector}". Available: ${apps.map(formatMetroApp).join(", ")}`
266
+ );
267
+ }
268
+ async function captureMetroConsoleLogs(options) {
269
+ const { app, limit, timeoutMs } = options;
270
+ return new Promise((resolve, reject) => {
271
+ const entries = [];
272
+ let settled = false;
273
+ let opened = false;
274
+ const socket = new WebSocket(app.webSocketDebuggerUrl);
275
+ const finish = (error) => {
276
+ if (settled) {
277
+ return;
278
+ }
279
+ settled = true;
280
+ clearTimeout(timer);
281
+ socket.removeAllListeners();
282
+ if (socket.readyState === WebSocket.OPEN) {
283
+ socket.close();
284
+ }
285
+ if (error) {
286
+ reject(error);
287
+ return;
288
+ }
289
+ resolve(entries);
290
+ };
291
+ const timer = setTimeout(() => {
292
+ finish();
293
+ }, timeoutMs);
294
+ socket.once("open", () => {
295
+ opened = true;
296
+ socket.send(JSON.stringify({ id: 1, method: "Runtime.enable" }));
297
+ });
298
+ socket.on("message", (data) => {
299
+ const payload = parseSocketMessage(data);
300
+ if (!isRuntimeConsoleApiCalledEvent(payload)) {
301
+ return;
302
+ }
303
+ entries.push(formatMetroLogEntry(app, payload));
304
+ if (entries.length >= limit) {
305
+ finish();
306
+ }
307
+ });
308
+ socket.once("close", () => {
309
+ finish();
310
+ });
311
+ socket.once("error", (error) => {
312
+ const normalizedError = normalizeError(error);
313
+ if (!opened) {
314
+ finish(normalizedError);
315
+ return;
316
+ }
317
+ finish(normalizedError);
318
+ });
319
+ });
320
+ }
321
+ function parseSocketMessage(data) {
322
+ const text = typeof data === "string" ? data : data.toString();
323
+ try {
324
+ return JSON.parse(text);
325
+ } catch {
326
+ return text;
327
+ }
328
+ }
329
+ function isRuntimeConsoleApiCalledEvent(value) {
330
+ return Boolean(
331
+ value && typeof value === "object" && "method" in value && value.method === "Runtime.consoleAPICalled"
332
+ );
333
+ }
334
+ function formatMetroLogEntry(app, event) {
335
+ return {
336
+ level: normalizeLogLevel(event.params?.type),
337
+ message: formatConsoleArguments(event.params?.args ?? []),
338
+ source: formatMetroSource(app),
339
+ timestamp: normalizeTimestamp(event.params?.timestamp)
340
+ };
341
+ }
342
+ function normalizeLogLevel(rawType) {
343
+ switch (rawType) {
344
+ case "trace":
345
+ return "trace";
346
+ case "debug":
347
+ return "debug";
348
+ case "warning":
349
+ return "warn";
350
+ case "error":
351
+ case "assert":
352
+ return "error";
353
+ default:
354
+ return "info";
355
+ }
356
+ }
357
+ function formatConsoleArguments(args) {
358
+ const values = args.map(formatConsoleArgument).filter(Boolean);
359
+ return values.join(" ").trim();
360
+ }
361
+ function formatConsoleArgument(arg) {
362
+ if (arg.type === "undefined") {
363
+ return "undefined";
364
+ }
365
+ if (arg.subtype === "null") {
366
+ return "null";
367
+ }
368
+ if (arg.description !== void 0) {
369
+ return arg.description;
370
+ }
371
+ if (arg.value !== void 0) {
372
+ if (typeof arg.value === "string") {
373
+ return arg.value;
374
+ }
375
+ if (typeof arg.value === "number" || typeof arg.value === "boolean" || typeof arg.value === "bigint") {
376
+ return String(arg.value);
377
+ }
378
+ return JSON.stringify(arg.value);
379
+ }
380
+ if (arg.unserializableValue !== void 0) {
381
+ return arg.unserializableValue;
382
+ }
383
+ return `[${arg.type}${arg.subtype ? ` ${arg.subtype}` : ""}]`;
384
+ }
385
+ function formatMetroSource(app) {
386
+ return `metro:${app.deviceName ?? app.appId ?? app.id}`;
387
+ }
388
+ function normalizeTimestamp(rawTimestamp) {
389
+ if (!rawTimestamp || Number.isNaN(rawTimestamp)) {
390
+ return (/* @__PURE__ */ new Date()).toISOString();
391
+ }
392
+ const milliseconds = rawTimestamp < 1e12 ? rawTimestamp * 1e3 : rawTimestamp;
393
+ return new Date(milliseconds).toISOString();
394
+ }
395
+ function formatMetroApp(app) {
396
+ return [app.id, app.appId, app.deviceName].filter(Boolean).join("/");
397
+ }
398
+ function normalizeError(error) {
399
+ if (error instanceof Error) {
400
+ return error;
401
+ }
402
+ return new Error(String(error));
403
+ }
404
+
405
+ // src/lib/format.ts
406
+ function formatSnapshot(snapshot) {
407
+ const lines = [
408
+ `service: ${snapshot.service}`,
409
+ `entries: ${snapshot.entries.length}/${snapshot.total}`
410
+ ];
411
+ for (const entry of snapshot.entries) {
412
+ const context = entry.context && Object.keys(entry.context).length > 0 ? ` ${JSON.stringify(entry.context)}` : "";
413
+ lines.push(
414
+ `${entry.timestamp} [${entry.level.toUpperCase()}] [${entry.source}] ${entry.message}${context}`
415
+ );
416
+ }
417
+ return lines.join("\n");
418
+ }
419
+
420
+ // src/lib/parse-args.ts
421
+ var DEFAULT_LIMIT = 120;
422
+ function parseCliArgs(args) {
423
+ const parsed = {
424
+ headers: {},
425
+ json: false,
426
+ limit: DEFAULT_LIMIT
427
+ };
428
+ for (let index = 0; index < args.length; index += 1) {
429
+ const arg = args[index];
430
+ if (!arg) {
431
+ continue;
432
+ }
433
+ if (!arg.startsWith("--") && !parsed.target) {
434
+ parsed.target = arg;
435
+ continue;
436
+ }
437
+ if (arg === "--json") {
438
+ parsed.json = true;
439
+ continue;
440
+ }
441
+ if (arg === "--endpoint") {
442
+ parsed.endpoint = readValue(args, ++index, "--endpoint");
443
+ continue;
444
+ }
445
+ if (arg === "--provider") {
446
+ parsed.provider = normalizeProvider(
447
+ readValue(args, ++index, "--provider")
448
+ );
449
+ continue;
450
+ }
451
+ if (arg === "--metro-url") {
452
+ parsed.metroUrl = readValue(args, ++index, "--metro-url");
453
+ continue;
454
+ }
455
+ if (arg === "--app") {
456
+ parsed.app = readValue(args, ++index, "--app");
457
+ continue;
458
+ }
459
+ if (arg === "--config") {
460
+ parsed.configPath = readValue(args, ++index, "--config");
461
+ continue;
462
+ }
463
+ if (arg === "--limit") {
464
+ parsed.limit = normalizeLimit(readValue(args, ++index, "--limit"));
465
+ continue;
466
+ }
467
+ if (arg === "--header") {
468
+ const rawHeader = readValue(args, ++index, "--header");
469
+ const separatorIndex = rawHeader.indexOf(":");
470
+ if (separatorIndex <= 0) {
471
+ throw new Error(`Invalid header "${rawHeader}". Use name:value.`);
472
+ }
473
+ const name = rawHeader.slice(0, separatorIndex).trim();
474
+ const value = rawHeader.slice(separatorIndex + 1).trim();
475
+ parsed.headers[name] = value;
476
+ continue;
477
+ }
478
+ if (arg === "--timeout") {
479
+ parsed.timeoutMs = normalizePositiveNumber(
480
+ readValue(args, ++index, "--timeout"),
481
+ "--timeout"
482
+ );
483
+ continue;
484
+ }
485
+ throw new Error(`Unknown argument: ${arg}`);
486
+ }
487
+ return parsed;
488
+ }
489
+ function normalizeLimit(rawLimit) {
490
+ const limit = Number(rawLimit);
491
+ if (!Number.isFinite(limit) || limit <= 0) {
492
+ throw new Error(`Invalid limit: ${rawLimit}`);
493
+ }
494
+ return Math.floor(limit);
495
+ }
496
+ function normalizePositiveNumber(rawValue, optionName) {
497
+ const value = Number(rawValue);
498
+ if (!Number.isFinite(value) || value <= 0) {
499
+ throw new Error(`Invalid value for ${optionName}: ${rawValue}`);
500
+ }
501
+ return Math.floor(value);
502
+ }
503
+ function normalizeProvider(rawProvider) {
504
+ if (rawProvider === "endpoint" || rawProvider === "metro") {
505
+ return rawProvider;
506
+ }
507
+ throw new Error(`Invalid provider: ${rawProvider}`);
508
+ }
509
+ function readValue(args, index, optionName) {
510
+ const value = args[index];
511
+ if (!value) {
512
+ throw new Error(`Missing value for ${optionName}`);
513
+ }
514
+ return value;
515
+ }
516
+
517
+ // src/lib/run-cli.ts
518
+ async function runCli(args) {
519
+ if (args.includes("--help") || args.includes("-h")) {
520
+ printHelp();
521
+ return 0;
522
+ }
523
+ const command = args[0];
524
+ if (command === "list") {
525
+ return runList(args.slice(1));
526
+ }
527
+ const parsed = parseCliArgs(args);
528
+ const config = loadConfig({ configPath: parsed.configPath });
529
+ const targetConfig = parsed.target && config ? config.targets[parsed.target] : void 0;
530
+ const directProvider = resolveDirectProvider(parsed);
531
+ if (parsed.target && !targetConfig && !directProvider) {
532
+ if (!config) {
533
+ throw new Error(
534
+ `Target "${parsed.target}" was provided, but no config was found. Create universal-logs.config.json, add package.json#universalLogs, or pass --config/--endpoint.`
535
+ );
536
+ }
537
+ throw new Error(
538
+ `Target "${parsed.target}" was not found in config. Run "universal-logs list" or check universal-logs.config.json.`
539
+ );
540
+ }
541
+ const request = resolveLogRequest({
542
+ directProvider,
543
+ parsed,
544
+ targetConfig
545
+ });
546
+ const snapshot = request.provider === "metro" ? await fetchMetroLogs({
547
+ app: request.app,
548
+ limit: parsed.limit,
549
+ metroUrl: request.metroUrl,
550
+ service: request.service,
551
+ timeoutMs: request.timeoutMs
552
+ }) : await fetchLogs({
553
+ endpoint: request.endpoint,
554
+ headers: request.headers,
555
+ limit: parsed.limit
556
+ });
557
+ if (parsed.json) {
558
+ process.stdout.write(`${JSON.stringify(snapshot, null, 2)}
559
+ `);
560
+ } else {
561
+ process.stdout.write(`${formatSnapshot(snapshot)}
562
+ `);
563
+ }
564
+ return 0;
565
+ }
566
+ function runList(args) {
567
+ const parsed = parseCliArgs(args);
568
+ const config = loadConfig({ configPath: parsed.configPath });
569
+ if (!config) {
570
+ throw new Error("No config found. Create universal-logs.config.json first.");
571
+ }
572
+ for (const [name, target] of Object.entries(config.targets)) {
573
+ const details = target.provider === "metro" ? formatMetroTarget(target) : formatEndpointTarget(target);
574
+ process.stdout.write(`${name} ${details}
575
+ `);
576
+ }
577
+ return 0;
578
+ }
579
+ function resolveLogRequest(options) {
580
+ const { directProvider, parsed, targetConfig } = options;
581
+ if (targetConfig) {
582
+ if (targetConfig.provider === "metro") {
583
+ return {
584
+ app: parsed.app ?? targetConfig.app,
585
+ metroUrl: parsed.metroUrl ?? targetConfig.metroUrl,
586
+ provider: "metro",
587
+ service: parsed.target,
588
+ timeoutMs: parsed.timeoutMs ?? targetConfig.timeoutMs ?? DEFAULT_METRO_TIMEOUT_MS
589
+ };
590
+ }
591
+ return {
592
+ endpoint: parsed.endpoint ?? targetConfig.endpoint,
593
+ headers: {
594
+ ...targetConfig.headers,
595
+ ...parsed.headers
596
+ },
597
+ provider: "endpoint"
598
+ };
599
+ }
600
+ const provider = directProvider ?? resolveDirectProvider(parsed);
601
+ if (provider === "metro") {
602
+ return {
603
+ app: parsed.app,
604
+ metroUrl: parsed.metroUrl,
605
+ provider: "metro",
606
+ service: parsed.target === "metro" ? "metro" : parsed.target,
607
+ timeoutMs: parsed.timeoutMs ?? DEFAULT_METRO_TIMEOUT_MS
608
+ };
609
+ }
610
+ if (provider === "endpoint" && parsed.endpoint) {
611
+ return {
612
+ endpoint: parsed.endpoint,
613
+ headers: parsed.headers,
614
+ provider: "endpoint"
615
+ };
616
+ }
617
+ throw new Error(
618
+ "No log source resolved. Pass --endpoint, use --provider metro, or configure a target in universal-logs.config.json."
619
+ );
620
+ }
621
+ function resolveDirectProvider(parsed) {
622
+ if (parsed.provider) {
623
+ return parsed.provider;
624
+ }
625
+ if (parsed.endpoint) {
626
+ return "endpoint";
627
+ }
628
+ if (parsed.metroUrl || parsed.app || parsed.target === "metro") {
629
+ return "metro";
630
+ }
631
+ return null;
632
+ }
633
+ function formatEndpointTarget(target) {
634
+ return ["endpoint", target.endpoint].join(" ");
635
+ }
636
+ function printHelp() {
637
+ process.stdout.write(
638
+ `${[
639
+ "Usage:",
640
+ " universal-logs <target> [--limit 120] [--json]",
641
+ " universal-logs --endpoint <url> [--limit 120] [--json]",
642
+ " universal-logs metro [--app <selector>] [--metro-url <url>] [--timeout 5000] [--limit 120] [--json]",
643
+ " universal-logs list",
644
+ "",
645
+ "Options:",
646
+ " --app <selector> Select a Metro app by id, appId, or device name",
647
+ " --config <path> Use a specific config file",
648
+ " --endpoint <url> Override the configured endpoint",
649
+ " --header name:value Add a request header",
650
+ " --limit <n> Number of log entries to request",
651
+ " --metro-url <url> Override the Metro server origin",
652
+ " --provider <name> Force endpoint or metro mode",
653
+ " --timeout <ms> Metro capture window before exiting",
654
+ " --json Print raw JSON"
655
+ ].join("\n")}
656
+ `
657
+ );
658
+ }
659
+
660
+ // src/cli.ts
661
+ try {
662
+ const exitCode = await runCli(process.argv.slice(2));
663
+ process.exit(exitCode);
664
+ } catch (error) {
665
+ const message = error instanceof Error ? error.message : String(error);
666
+ process.stderr.write(`${message}
667
+ `);
668
+ process.exit(1);
669
+ }
670
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/config.ts","../src/lib/fetch-logs.ts","../src/lib/fetch-metro-logs.ts","../src/lib/format.ts","../src/lib/parse-args.ts","../src/lib/run-cli.ts","../src/cli.ts"],"sourcesContent":["import fs from \"node:fs\"\nimport path from \"node:path\"\n\nexport type EndpointTargetConfig = {\n endpoint: string\n headers?: Record<string, string>\n provider?: \"endpoint\"\n}\n\nexport type MetroTargetConfig = {\n app?: string\n metroUrl?: string\n provider: \"metro\"\n timeoutMs?: number\n}\n\nexport type UniversalLogsTargetConfig = EndpointTargetConfig | MetroTargetConfig\n\nexport type UniversalLogsConfig = {\n targets: Record<string, UniversalLogsTargetConfig>\n}\n\nconst CONFIG_FILE_NAME = \"universal-logs.config.json\"\n\nexport function findConfigPath(cwd: string): string | null {\n let currentDir = path.resolve(cwd)\n\n while (true) {\n const configPath = path.join(currentDir, CONFIG_FILE_NAME)\n if (fs.existsSync(configPath)) {\n return configPath\n }\n\n const packageJsonPath = path.join(currentDir, \"package.json\")\n if (fs.existsSync(packageJsonPath)) {\n const packageJson = JSON.parse(\n fs.readFileSync(packageJsonPath, \"utf8\"),\n ) as {\n universalLogs?: UniversalLogsConfig\n }\n\n if (packageJson.universalLogs) {\n return packageJsonPath\n }\n }\n\n const parentDir = path.dirname(currentDir)\n if (parentDir === currentDir) {\n return null\n }\n\n currentDir = parentDir\n }\n}\n\nexport function loadConfig(\n options: { configPath?: string; cwd?: string } = {},\n): UniversalLogsConfig | null {\n const { configPath, cwd = process.cwd() } = options\n const resolvedPath = configPath\n ? path.resolve(cwd, configPath)\n : findConfigPath(cwd)\n\n if (!resolvedPath) {\n return null\n }\n\n if (path.basename(resolvedPath) === \"package.json\") {\n const packageJson = JSON.parse(fs.readFileSync(resolvedPath, \"utf8\")) as {\n universalLogs?: UniversalLogsConfig\n }\n\n if (!packageJson.universalLogs) {\n throw new Error(`No universalLogs field found in ${resolvedPath}`)\n }\n\n return validateConfig(packageJson.universalLogs, resolvedPath)\n }\n\n const parsed = JSON.parse(\n fs.readFileSync(resolvedPath, \"utf8\"),\n ) as UniversalLogsConfig\n return validateConfig(parsed, resolvedPath)\n}\n\nfunction validateConfig(\n config: UniversalLogsConfig,\n sourcePath: string,\n): UniversalLogsConfig {\n if (!config || typeof config !== \"object\" || !config.targets) {\n throw new Error(`Invalid config in ${sourcePath}: missing targets object`)\n }\n\n const targets = Object.fromEntries(\n Object.entries(config.targets).map(([name, target]) => [\n name,\n validateTarget(name, target, sourcePath),\n ]),\n )\n\n return {\n targets,\n }\n}\n\nfunction validateTarget(\n name: string,\n target: UniversalLogsTargetConfig,\n sourcePath: string,\n): UniversalLogsTargetConfig {\n if (!target || typeof target !== \"object\") {\n throw new Error(`Invalid target \"${name}\" in ${sourcePath}`)\n }\n\n if (target.provider === \"metro\") {\n if (target.app !== undefined && typeof target.app !== \"string\") {\n throw new Error(`Invalid target \"${name}\" in ${sourcePath}: invalid app`)\n }\n\n if (target.metroUrl !== undefined && typeof target.metroUrl !== \"string\") {\n throw new Error(\n `Invalid target \"${name}\" in ${sourcePath}: invalid metroUrl`,\n )\n }\n\n if (\n target.timeoutMs !== undefined &&\n (!Number.isFinite(target.timeoutMs) || target.timeoutMs <= 0)\n ) {\n throw new Error(\n `Invalid target \"${name}\" in ${sourcePath}: invalid timeoutMs`,\n )\n }\n\n return target\n }\n\n if (target.provider !== undefined && target.provider !== \"endpoint\") {\n throw new Error(\n `Invalid target \"${name}\" in ${sourcePath}: unsupported provider`,\n )\n }\n\n if (typeof target.endpoint !== \"string\") {\n throw new Error(\n `Invalid target \"${name}\" in ${sourcePath}: missing endpoint`,\n )\n }\n\n if (target.headers !== undefined && !isStringRecord(target.headers)) {\n throw new Error(\n `Invalid target \"${name}\" in ${sourcePath}: invalid headers`,\n )\n }\n\n return target\n}\n\nfunction isStringRecord(value: unknown): value is Record<string, string> {\n if (!value || typeof value !== \"object\") {\n return false\n }\n\n return Object.values(value).every((entry) => typeof entry === \"string\")\n}\n","import type { UniversalLogSnapshot } from \"../runtime\"\n\nexport async function fetchLogs(options: {\n endpoint: string\n headers?: HeadersInit\n limit: number\n}): Promise<UniversalLogSnapshot> {\n const { endpoint, headers, limit } = options\n const url = new URL(endpoint)\n url.searchParams.set(\"limit\", String(limit))\n\n let response: Response\n\n try {\n response = await fetch(url, {\n headers,\n })\n } catch (error) {\n throw new Error(\n `Failed to fetch logs from ${url.origin}${url.pathname}: ${formatFetchError(error)}`,\n )\n }\n\n if (!response.ok) {\n throw new Error(\n `Endpoint returned ${response.status} ${response.statusText}`,\n )\n }\n\n return (await response.json()) as UniversalLogSnapshot\n}\n\nfunction formatFetchError(error: unknown): string {\n if (!(error instanceof Error)) {\n return String(error)\n }\n\n const cause = error.cause\n if (cause instanceof Error && cause.message) {\n return cause.message\n }\n\n if (\n cause &&\n typeof cause === \"object\" &&\n \"code\" in cause &&\n typeof cause.code === \"string\"\n ) {\n return cause.code\n }\n\n return error.message\n}\n","import WebSocket from \"ws\"\nimport type {\n UniversalLogEntry,\n UniversalLogLevel,\n UniversalLogSnapshot,\n} from \"../runtime\"\n\ntype MetroInspectorApp = {\n appId?: string\n deviceName?: string\n id: string\n reactNative?: {\n capabilities?: {\n nativePageReloads?: boolean\n }\n }\n title: string\n webSocketDebuggerUrl: string\n}\n\ntype MetroRemoteObject = {\n description?: string\n subtype?: string\n type: string\n unserializableValue?: string\n value?: unknown\n}\n\ntype RuntimeConsoleApiCalledEvent = {\n method: \"Runtime.consoleAPICalled\"\n params?: {\n args?: MetroRemoteObject[]\n timestamp?: number\n type?: string\n }\n}\n\nexport const DEFAULT_METRO_URL = \"http://127.0.0.1:8081\"\nexport const DEFAULT_METRO_TIMEOUT_MS = 5000\n\nexport async function fetchMetroLogs(options: {\n app?: string\n limit: number\n metroUrl?: string\n service?: string\n timeoutMs?: number\n}): Promise<UniversalLogSnapshot> {\n const metroOrigin = getMetroServerOrigin(options.metroUrl)\n const apps = await fetchMetroInspectorApps(metroOrigin)\n\n if (apps.length === 0) {\n const status = await fetchMetroStatus(metroOrigin)\n if (status?.includes(\"packager-status:running\")) {\n throw new Error(\n `Metro is running at ${metroOrigin}, but no React Native inspector apps are connected. Open the app on a simulator/device and make sure it is using this Metro server.`,\n )\n }\n\n throw new Error(\n `No supported React Native inspector apps found at ${metroOrigin}`,\n )\n }\n\n const app = selectMetroInspectorApp(apps, options.app)\n const entries = await captureMetroConsoleLogs({\n app,\n limit: options.limit,\n timeoutMs: options.timeoutMs ?? DEFAULT_METRO_TIMEOUT_MS,\n })\n\n return {\n entries,\n limit: options.limit,\n service: options.service ?? app.appId ?? \"metro\",\n total: entries.length,\n }\n}\n\nexport function getMetroServerOrigin(rawMetroUrl?: string): string {\n const input = rawMetroUrl?.trim() || DEFAULT_METRO_URL\n const hasScheme = /^[a-z][a-z0-9+.-]*:\\/\\//i.test(input)\n const url = hasScheme ? new URL(input) : new URL(`http://${input}`)\n\n if (!url.port) {\n url.port = \"8081\"\n }\n\n return url.origin\n}\n\nexport function formatMetroTarget(target: {\n app?: string\n metroUrl?: string\n timeoutMs?: number\n}): string {\n const parts = [\"metro\", getMetroServerOrigin(target.metroUrl)]\n\n if (target.app) {\n parts.push(`app=${target.app}`)\n }\n\n if (target.timeoutMs) {\n parts.push(`timeout=${target.timeoutMs}ms`)\n }\n\n return parts.join(\"\\t\")\n}\n\nasync function fetchMetroInspectorApps(\n metroOrigin: string,\n): Promise<MetroInspectorApp[]> {\n const endpoints = [\"/json/list\", \"/json\"]\n let lastError: Error | null = null\n\n for (const endpoint of endpoints) {\n try {\n const response = await fetch(`${metroOrigin}${endpoint}`)\n if (!response.ok) {\n continue\n }\n\n const data = (await response.json()) as unknown\n if (!Array.isArray(data)) {\n continue\n }\n\n const apps = [...data].reverse().filter(isSupportedMetroApp)\n if (apps.length > 0) {\n return apps\n }\n\n return [...data].reverse().filter(hasDebuggerUrl)\n } catch (error) {\n lastError = normalizeError(error)\n }\n }\n\n if (lastError) {\n throw new Error(\n `Failed to query Metro inspector at ${metroOrigin}: ${lastError.message}`,\n )\n }\n\n return []\n}\n\nasync function fetchMetroStatus(metroOrigin: string): Promise<string | null> {\n try {\n const response = await fetch(`${metroOrigin}/status`)\n if (!response.ok) {\n return null\n }\n\n return await response.text()\n } catch {\n return null\n }\n}\n\nfunction isSupportedMetroApp(app: unknown): app is MetroInspectorApp {\n if (!hasDebuggerUrl(app)) {\n return false\n }\n\n return (\n app.title === \"React Native Experimental (Improved Chrome Reloads)\" ||\n app.reactNative?.capabilities?.nativePageReloads === true\n )\n}\n\nfunction hasDebuggerUrl(app: unknown): app is MetroInspectorApp {\n return Boolean(\n app &&\n typeof app === \"object\" &&\n \"id\" in app &&\n \"title\" in app &&\n \"webSocketDebuggerUrl\" in app &&\n typeof app.id === \"string\" &&\n typeof app.title === \"string\" &&\n typeof app.webSocketDebuggerUrl === \"string\",\n )\n}\n\nfunction selectMetroInspectorApp(\n apps: MetroInspectorApp[],\n selector?: string,\n): MetroInspectorApp {\n if (!selector) {\n if (apps.length === 1) {\n return apps[0]\n }\n\n throw new Error(\n `Multiple Metro apps found. Pass --app. Available: ${apps.map(formatMetroApp).join(\", \")}`,\n )\n }\n\n const normalizedSelector = selector.trim().toLowerCase()\n const matches = apps.filter((app) => {\n return [app.id, app.appId, app.deviceName]\n .filter((value): value is string => Boolean(value))\n .some((value) => value.toLowerCase() === normalizedSelector)\n })\n\n if (matches.length === 1) {\n return matches[0]\n }\n\n if (matches.length > 1) {\n throw new Error(\n `Multiple Metro apps match \"${selector}\". Available: ${apps.map(formatMetroApp).join(\", \")}`,\n )\n }\n\n throw new Error(\n `No Metro app matched \"${selector}\". Available: ${apps.map(formatMetroApp).join(\", \")}`,\n )\n}\n\nasync function captureMetroConsoleLogs(options: {\n app: MetroInspectorApp\n limit: number\n timeoutMs: number\n}): Promise<UniversalLogEntry[]> {\n const { app, limit, timeoutMs } = options\n\n return new Promise((resolve, reject) => {\n const entries: UniversalLogEntry[] = []\n let settled = false\n let opened = false\n const socket = new WebSocket(app.webSocketDebuggerUrl)\n\n const finish = (error?: Error) => {\n if (settled) {\n return\n }\n\n settled = true\n clearTimeout(timer)\n socket.removeAllListeners()\n\n if (socket.readyState === WebSocket.OPEN) {\n socket.close()\n }\n\n if (error) {\n reject(error)\n return\n }\n\n resolve(entries)\n }\n\n const timer = setTimeout(() => {\n finish()\n }, timeoutMs)\n\n socket.once(\"open\", () => {\n opened = true\n socket.send(JSON.stringify({ id: 1, method: \"Runtime.enable\" }))\n })\n\n socket.on(\"message\", (data: WebSocket.RawData) => {\n const payload = parseSocketMessage(data)\n if (!isRuntimeConsoleApiCalledEvent(payload)) {\n return\n }\n\n entries.push(formatMetroLogEntry(app, payload))\n if (entries.length >= limit) {\n finish()\n }\n })\n\n socket.once(\"close\", () => {\n finish()\n })\n\n socket.once(\"error\", (error: Error) => {\n const normalizedError = normalizeError(error)\n if (!opened) {\n finish(normalizedError)\n return\n }\n\n finish(normalizedError)\n })\n })\n}\n\nfunction parseSocketMessage(data: WebSocket.RawData): unknown {\n const text = typeof data === \"string\" ? data : data.toString()\n\n try {\n return JSON.parse(text) as unknown\n } catch {\n return text\n }\n}\n\nfunction isRuntimeConsoleApiCalledEvent(\n value: unknown,\n): value is RuntimeConsoleApiCalledEvent {\n return Boolean(\n value &&\n typeof value === \"object\" &&\n \"method\" in value &&\n value.method === \"Runtime.consoleAPICalled\",\n )\n}\n\nfunction formatMetroLogEntry(\n app: MetroInspectorApp,\n event: RuntimeConsoleApiCalledEvent,\n): UniversalLogEntry {\n return {\n level: normalizeLogLevel(event.params?.type),\n message: formatConsoleArguments(event.params?.args ?? []),\n source: formatMetroSource(app),\n timestamp: normalizeTimestamp(event.params?.timestamp),\n }\n}\n\nfunction normalizeLogLevel(rawType?: string): UniversalLogLevel {\n switch (rawType) {\n case \"trace\":\n return \"trace\"\n case \"debug\":\n return \"debug\"\n case \"warning\":\n return \"warn\"\n case \"error\":\n case \"assert\":\n return \"error\"\n default:\n return \"info\"\n }\n}\n\nfunction formatConsoleArguments(args: MetroRemoteObject[]): string {\n const values = args.map(formatConsoleArgument).filter(Boolean)\n return values.join(\" \").trim()\n}\n\nfunction formatConsoleArgument(arg: MetroRemoteObject): string {\n if (arg.type === \"undefined\") {\n return \"undefined\"\n }\n\n if (arg.subtype === \"null\") {\n return \"null\"\n }\n\n if (arg.description !== undefined) {\n return arg.description\n }\n\n if (arg.value !== undefined) {\n if (typeof arg.value === \"string\") {\n return arg.value\n }\n\n if (\n typeof arg.value === \"number\" ||\n typeof arg.value === \"boolean\" ||\n typeof arg.value === \"bigint\"\n ) {\n return String(arg.value)\n }\n\n return JSON.stringify(arg.value)\n }\n\n if (arg.unserializableValue !== undefined) {\n return arg.unserializableValue\n }\n\n return `[${arg.type}${arg.subtype ? ` ${arg.subtype}` : \"\"}]`\n}\n\nfunction formatMetroSource(app: MetroInspectorApp): string {\n return `metro:${app.deviceName ?? app.appId ?? app.id}`\n}\n\nfunction normalizeTimestamp(rawTimestamp?: number): string {\n if (!rawTimestamp || Number.isNaN(rawTimestamp)) {\n return new Date().toISOString()\n }\n\n const milliseconds =\n rawTimestamp < 1_000_000_000_000 ? rawTimestamp * 1000 : rawTimestamp\n\n return new Date(milliseconds).toISOString()\n}\n\nfunction formatMetroApp(app: MetroInspectorApp): string {\n return [app.id, app.appId, app.deviceName].filter(Boolean).join(\"/\")\n}\n\nfunction normalizeError(error: unknown): Error {\n if (error instanceof Error) {\n return error\n }\n\n return new Error(String(error))\n}\n","import type { UniversalLogSnapshot } from \"../runtime\"\n\nexport function formatSnapshot(snapshot: UniversalLogSnapshot): string {\n const lines = [\n `service: ${snapshot.service}`,\n `entries: ${snapshot.entries.length}/${snapshot.total}`,\n ]\n\n for (const entry of snapshot.entries) {\n const context =\n entry.context && Object.keys(entry.context).length > 0\n ? ` ${JSON.stringify(entry.context)}`\n : \"\"\n\n lines.push(\n `${entry.timestamp} [${entry.level.toUpperCase()}] [${entry.source}] ${entry.message}${context}`,\n )\n }\n\n return lines.join(\"\\n\")\n}\n","export type ParsedCliArgs = {\n app?: string\n configPath?: string\n endpoint?: string\n headers: Record<string, string>\n json: boolean\n limit: number\n metroUrl?: string\n provider?: \"endpoint\" | \"metro\"\n target?: string\n timeoutMs?: number\n}\n\nconst DEFAULT_LIMIT = 120\n\nexport function parseCliArgs(args: string[]): ParsedCliArgs {\n const parsed: ParsedCliArgs = {\n headers: {},\n json: false,\n limit: DEFAULT_LIMIT,\n }\n\n for (let index = 0; index < args.length; index += 1) {\n const arg = args[index]\n\n if (!arg) {\n continue\n }\n\n if (!arg.startsWith(\"--\") && !parsed.target) {\n parsed.target = arg\n continue\n }\n\n if (arg === \"--json\") {\n parsed.json = true\n continue\n }\n\n if (arg === \"--endpoint\") {\n parsed.endpoint = readValue(args, ++index, \"--endpoint\")\n continue\n }\n\n if (arg === \"--provider\") {\n parsed.provider = normalizeProvider(\n readValue(args, ++index, \"--provider\"),\n )\n continue\n }\n\n if (arg === \"--metro-url\") {\n parsed.metroUrl = readValue(args, ++index, \"--metro-url\")\n continue\n }\n\n if (arg === \"--app\") {\n parsed.app = readValue(args, ++index, \"--app\")\n continue\n }\n\n if (arg === \"--config\") {\n parsed.configPath = readValue(args, ++index, \"--config\")\n continue\n }\n\n if (arg === \"--limit\") {\n parsed.limit = normalizeLimit(readValue(args, ++index, \"--limit\"))\n continue\n }\n\n if (arg === \"--header\") {\n const rawHeader = readValue(args, ++index, \"--header\")\n const separatorIndex = rawHeader.indexOf(\":\")\n if (separatorIndex <= 0) {\n throw new Error(`Invalid header \"${rawHeader}\". Use name:value.`)\n }\n\n const name = rawHeader.slice(0, separatorIndex).trim()\n const value = rawHeader.slice(separatorIndex + 1).trim()\n parsed.headers[name] = value\n continue\n }\n\n if (arg === \"--timeout\") {\n parsed.timeoutMs = normalizePositiveNumber(\n readValue(args, ++index, \"--timeout\"),\n \"--timeout\",\n )\n continue\n }\n\n throw new Error(`Unknown argument: ${arg}`)\n }\n\n return parsed\n}\n\nfunction normalizeLimit(rawLimit: string): number {\n const limit = Number(rawLimit)\n if (!Number.isFinite(limit) || limit <= 0) {\n throw new Error(`Invalid limit: ${rawLimit}`)\n }\n\n return Math.floor(limit)\n}\n\nfunction normalizePositiveNumber(rawValue: string, optionName: string): number {\n const value = Number(rawValue)\n if (!Number.isFinite(value) || value <= 0) {\n throw new Error(`Invalid value for ${optionName}: ${rawValue}`)\n }\n\n return Math.floor(value)\n}\n\nfunction normalizeProvider(rawProvider: string): \"endpoint\" | \"metro\" {\n if (rawProvider === \"endpoint\" || rawProvider === \"metro\") {\n return rawProvider\n }\n\n throw new Error(`Invalid provider: ${rawProvider}`)\n}\n\nfunction readValue(args: string[], index: number, optionName: string): string {\n const value = args[index]\n if (!value) {\n throw new Error(`Missing value for ${optionName}`)\n }\n\n return value\n}\n","import type { EndpointTargetConfig, UniversalLogsTargetConfig } from \"./config\"\nimport { loadConfig } from \"./config\"\nimport { fetchLogs } from \"./fetch-logs\"\nimport {\n DEFAULT_METRO_TIMEOUT_MS,\n fetchMetroLogs,\n formatMetroTarget,\n} from \"./fetch-metro-logs\"\nimport { formatSnapshot } from \"./format\"\nimport { parseCliArgs } from \"./parse-args\"\n\nexport async function runCli(args: string[]): Promise<number> {\n if (args.includes(\"--help\") || args.includes(\"-h\")) {\n printHelp()\n return 0\n }\n\n const command = args[0]\n if (command === \"list\") {\n return runList(args.slice(1))\n }\n\n const parsed = parseCliArgs(args)\n const config = loadConfig({ configPath: parsed.configPath })\n const targetConfig =\n parsed.target && config ? config.targets[parsed.target] : undefined\n const directProvider = resolveDirectProvider(parsed)\n\n if (parsed.target && !targetConfig && !directProvider) {\n if (!config) {\n throw new Error(\n `Target \"${parsed.target}\" was provided, but no config was found. Create universal-logs.config.json, add package.json#universalLogs, or pass --config/--endpoint.`,\n )\n }\n\n throw new Error(\n `Target \"${parsed.target}\" was not found in config. Run \"universal-logs list\" or check universal-logs.config.json.`,\n )\n }\n\n const request = resolveLogRequest({\n directProvider,\n parsed,\n targetConfig,\n })\n\n const snapshot =\n request.provider === \"metro\"\n ? await fetchMetroLogs({\n app: request.app,\n limit: parsed.limit,\n metroUrl: request.metroUrl,\n service: request.service,\n timeoutMs: request.timeoutMs,\n })\n : await fetchLogs({\n endpoint: request.endpoint,\n headers: request.headers,\n limit: parsed.limit,\n })\n\n if (parsed.json) {\n process.stdout.write(`${JSON.stringify(snapshot, null, 2)}\\n`)\n } else {\n process.stdout.write(`${formatSnapshot(snapshot)}\\n`)\n }\n\n return 0\n}\n\nfunction runList(args: string[]): number {\n const parsed = parseCliArgs(args)\n const config = loadConfig({ configPath: parsed.configPath })\n\n if (!config) {\n throw new Error(\"No config found. Create universal-logs.config.json first.\")\n }\n\n for (const [name, target] of Object.entries(config.targets)) {\n const details =\n target.provider === \"metro\"\n ? formatMetroTarget(target)\n : formatEndpointTarget(target)\n process.stdout.write(`${name}\\t${details}\\n`)\n }\n\n return 0\n}\n\ntype ResolvedLogRequest =\n | {\n app?: string\n metroUrl?: string\n provider: \"metro\"\n service?: string\n timeoutMs: number\n }\n | {\n endpoint: string\n headers: Record<string, string>\n provider: \"endpoint\"\n }\n\nfunction resolveLogRequest(options: {\n directProvider?: \"endpoint\" | \"metro\" | null\n parsed: ReturnType<typeof parseCliArgs>\n targetConfig?: UniversalLogsTargetConfig\n}): ResolvedLogRequest {\n const { directProvider, parsed, targetConfig } = options\n\n if (targetConfig) {\n if (targetConfig.provider === \"metro\") {\n return {\n app: parsed.app ?? targetConfig.app,\n metroUrl: parsed.metroUrl ?? targetConfig.metroUrl,\n provider: \"metro\",\n service: parsed.target,\n timeoutMs:\n parsed.timeoutMs ??\n targetConfig.timeoutMs ??\n DEFAULT_METRO_TIMEOUT_MS,\n }\n }\n\n return {\n endpoint: parsed.endpoint ?? targetConfig.endpoint,\n headers: {\n ...targetConfig.headers,\n ...parsed.headers,\n },\n provider: \"endpoint\",\n }\n }\n\n const provider = directProvider ?? resolveDirectProvider(parsed)\n if (provider === \"metro\") {\n return {\n app: parsed.app,\n metroUrl: parsed.metroUrl,\n provider: \"metro\",\n service: parsed.target === \"metro\" ? \"metro\" : parsed.target,\n timeoutMs: parsed.timeoutMs ?? DEFAULT_METRO_TIMEOUT_MS,\n }\n }\n\n if (provider === \"endpoint\" && parsed.endpoint) {\n return {\n endpoint: parsed.endpoint,\n headers: parsed.headers,\n provider: \"endpoint\",\n }\n }\n\n throw new Error(\n \"No log source resolved. Pass --endpoint, use --provider metro, or configure a target in universal-logs.config.json.\",\n )\n}\n\nfunction resolveDirectProvider(\n parsed: ReturnType<typeof parseCliArgs>,\n): \"endpoint\" | \"metro\" | null {\n if (parsed.provider) {\n return parsed.provider\n }\n\n if (parsed.endpoint) {\n return \"endpoint\"\n }\n\n if (parsed.metroUrl || parsed.app || parsed.target === \"metro\") {\n return \"metro\"\n }\n\n return null\n}\n\nfunction formatEndpointTarget(target: EndpointTargetConfig): string {\n return [\"endpoint\", target.endpoint].join(\"\\t\")\n}\n\nfunction printHelp(): void {\n process.stdout.write(\n `${[\n \"Usage:\",\n \" universal-logs <target> [--limit 120] [--json]\",\n \" universal-logs --endpoint <url> [--limit 120] [--json]\",\n \" universal-logs metro [--app <selector>] [--metro-url <url>] [--timeout 5000] [--limit 120] [--json]\",\n \" universal-logs list\",\n \"\",\n \"Options:\",\n \" --app <selector> Select a Metro app by id, appId, or device name\",\n \" --config <path> Use a specific config file\",\n \" --endpoint <url> Override the configured endpoint\",\n \" --header name:value Add a request header\",\n \" --limit <n> Number of log entries to request\",\n \" --metro-url <url> Override the Metro server origin\",\n \" --provider <name> Force endpoint or metro mode\",\n \" --timeout <ms> Metro capture window before exiting\",\n \" --json Print raw JSON\",\n ].join(\"\\n\")}\\n`,\n )\n}\n","#!/usr/bin/env node\nimport { runCli } from \"./lib/run-cli\"\n\ntry {\n const exitCode = await runCli(process.argv.slice(2))\n process.exit(exitCode)\n} catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n process.stderr.write(`${message}\\n`)\n process.exit(1)\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,2 @@
1
+ export { LogBuffer, UniversalLogEntry, UniversalLogLevel, UniversalLogSnapshot, attachElectronRendererConsoleMirror, createConsoleMirror, createLogBuffer, startHttpLogEndpoint } from './runtime/index.js';
2
+ import 'node:http';
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ import {
2
+ attachElectronRendererConsoleMirror,
3
+ createConsoleMirror,
4
+ createLogBuffer,
5
+ startHttpLogEndpoint
6
+ } from "./chunk-IUSSBPEQ.js";
7
+ export {
8
+ attachElectronRendererConsoleMirror,
9
+ createConsoleMirror,
10
+ createLogBuffer,
11
+ startHttpLogEndpoint
12
+ };
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,59 @@
1
+ import http from 'node:http';
2
+
3
+ type UniversalLogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal";
4
+ type UniversalLogEntry = {
5
+ context?: Record<string, unknown>;
6
+ level: UniversalLogLevel;
7
+ message: string;
8
+ source: string;
9
+ timestamp: string;
10
+ };
11
+ type UniversalLogSnapshot = {
12
+ entries: UniversalLogEntry[];
13
+ limit: number;
14
+ service: string;
15
+ total: number;
16
+ };
17
+ type LogBuffer = {
18
+ append: (entry: UniversalLogEntry) => void;
19
+ clear: () => void;
20
+ getEntries: (limit?: number) => UniversalLogEntry[];
21
+ getSnapshot: (service: string, limit?: number) => UniversalLogSnapshot;
22
+ size: () => number;
23
+ };
24
+
25
+ declare function createConsoleMirror(options: {
26
+ buffer: LogBuffer;
27
+ consoleObject?: Console;
28
+ source?: string;
29
+ }): {
30
+ restore: () => void;
31
+ };
32
+
33
+ type ElectronConsoleMessageSource = {
34
+ on: (event: "console-message", listener: (event: unknown, level: number, message: string, line: number, sourceId: string) => void) => void;
35
+ };
36
+ declare function attachElectronRendererConsoleMirror(options: {
37
+ buffer: LogBuffer;
38
+ source?: string;
39
+ webContents: ElectronConsoleMessageSource;
40
+ }): void;
41
+
42
+ declare function startHttpLogEndpoint(options: {
43
+ buffer: LogBuffer;
44
+ healthPath?: string;
45
+ host?: string;
46
+ logsPath?: string;
47
+ port: number;
48
+ service: string;
49
+ }): Promise<{
50
+ close: () => Promise<void>;
51
+ server: http.Server;
52
+ url: string;
53
+ }>;
54
+
55
+ declare function createLogBuffer(options?: {
56
+ limit?: number;
57
+ }): LogBuffer;
58
+
59
+ export { type LogBuffer, type UniversalLogEntry, type UniversalLogLevel, type UniversalLogSnapshot, attachElectronRendererConsoleMirror, createConsoleMirror, createLogBuffer, startHttpLogEndpoint };
@@ -0,0 +1,13 @@
1
+ import {
2
+ attachElectronRendererConsoleMirror,
3
+ createConsoleMirror,
4
+ createLogBuffer,
5
+ startHttpLogEndpoint
6
+ } from "../chunk-IUSSBPEQ.js";
7
+ export {
8
+ attachElectronRendererConsoleMirror,
9
+ createConsoleMirror,
10
+ createLogBuffer,
11
+ startHttpLogEndpoint
12
+ };
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "universal-logs",
3
+ "version": "0.1.0",
4
+ "description": "CLI and runtime helpers for exposing local log endpoints and reading them from coding agents.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "universal-logs": "./dist/cli.js"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ },
15
+ "./runtime": {
16
+ "types": "./dist/runtime/index.d.ts",
17
+ "import": "./dist/runtime/index.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "README.md"
23
+ ],
24
+ "engines": {
25
+ "node": ">=18"
26
+ },
27
+ "scripts": {
28
+ "build": "tsup",
29
+ "check": "biome check .",
30
+ "format": "biome format --write .",
31
+ "test": "vitest run",
32
+ "typecheck": "tsc --noEmit",
33
+ "verify": "bun run typecheck && bun run test && bun run check"
34
+ },
35
+ "dependencies": {
36
+ "ws": "^8.18.3"
37
+ },
38
+ "devDependencies": {
39
+ "@biomejs/biome": "^2.3.8",
40
+ "@types/node": "^24.10.2",
41
+ "@types/ws": "^8.18.1",
42
+ "tsup": "^8.5.0",
43
+ "typescript": "^5.9.3",
44
+ "vitest": "^4.0.10"
45
+ }
46
+ }