stream-doctor 0.0.4 → 0.0.6
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 +2 -2
- package/dist/stream_doctor.d.ts +34 -11
- package/dist/stream_doctor.js +113 -94
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ your pipeline, watch what comes out the other end and assert on the metrics.
|
|
|
5
5
|
An early proof of concept, currently a single RTMP to HLS scenario with a
|
|
6
6
|
single metric, the audio/video drift.
|
|
7
7
|
|
|
8
|
-
This is the TypeScript client for the
|
|
8
|
+
This is the TypeScript client for the stream-doctor daemon. It brings the
|
|
9
9
|
daemon binary along, as an optional dependency on `@stream-doctor/<platform>`.
|
|
10
10
|
Setup, usage and the roadmap are described in the
|
|
11
|
-
[
|
|
11
|
+
[stream-doctor repository](https://github.com/membraneframework/stream-doctor).
|
package/dist/stream_doctor.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ChildProcess } from "node:child_process";
|
|
2
|
-
|
|
2
|
+
/** Lifecycle state of a streamer or viewer. */
|
|
3
3
|
export type Status = "streaming" | "receiving" | "waiting_for_playlist" | "ended" | "failed" | "stopped";
|
|
4
|
+
/** State of the publisher, as reported by the daemon. */
|
|
4
5
|
export interface StreamerStatus {
|
|
5
6
|
input: string;
|
|
6
7
|
rtmp_url: string;
|
|
@@ -8,15 +9,18 @@ export interface StreamerStatus {
|
|
|
8
9
|
error: string | null;
|
|
9
10
|
live: boolean;
|
|
10
11
|
}
|
|
12
|
+
/** Audio/video drift measured by a viewer, in milliseconds. */
|
|
11
13
|
export interface AvDrift {
|
|
12
14
|
drift_ms: number | null;
|
|
13
15
|
frame_duration_ms: number | null;
|
|
14
16
|
latest_samples: number[];
|
|
15
17
|
}
|
|
18
|
+
/** Metrics collected by a viewer. */
|
|
16
19
|
export interface Metrics {
|
|
17
20
|
av_drift?: AvDrift;
|
|
18
21
|
error?: string;
|
|
19
22
|
}
|
|
23
|
+
/** State of a viewer, as reported by the daemon. */
|
|
20
24
|
export interface ViewerStatus {
|
|
21
25
|
id: string;
|
|
22
26
|
hls_url: string;
|
|
@@ -24,47 +28,66 @@ export interface ViewerStatus {
|
|
|
24
28
|
error: string | null;
|
|
25
29
|
metrics: Metrics;
|
|
26
30
|
}
|
|
27
|
-
|
|
31
|
+
/** State of the whole daemon. */
|
|
32
|
+
export interface DaemonStatus {
|
|
28
33
|
streamer: StreamerStatus | null;
|
|
29
34
|
viewers: ViewerStatus[];
|
|
30
35
|
}
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
/** Spawns a daemon from `binary` (the bundled one by default) on `port` (a free one by default), or connects to a running one when `daemonUrl` is set. */
|
|
37
|
+
export declare function session({ daemonUrl, binary, port, }?: {
|
|
38
|
+
daemonUrl?: string;
|
|
33
39
|
binary?: string;
|
|
40
|
+
port?: number;
|
|
34
41
|
}): Promise<Session>;
|
|
42
|
+
/** Path to the daemon binary bundled for this platform, or null if there is none. */
|
|
43
|
+
export declare function bundledBinary(): string | null;
|
|
44
|
+
/** A connection to the daemon, grouping one publisher and any number of viewers. */
|
|
35
45
|
declare class Session {
|
|
36
|
-
|
|
46
|
+
daemonUrl: string;
|
|
37
47
|
child: ChildProcess | null;
|
|
38
48
|
logFile: string | null;
|
|
39
|
-
constructor(
|
|
49
|
+
constructor(daemonUrl: string, child: ChildProcess | null, logFile?: string | null);
|
|
50
|
+
/** Starts publishing `file` to `rtmpUrl`. */
|
|
40
51
|
publish(rtmpUrl: string, { file }?: {
|
|
41
52
|
file?: string;
|
|
42
53
|
}): Streamer;
|
|
54
|
+
/** Starts a viewer collecting metrics from the HLS playlist at `hlsUrl`. */
|
|
43
55
|
watch(hlsUrl: string): Promise<Viewer>;
|
|
44
|
-
|
|
56
|
+
/** Current state of the daemon. */
|
|
57
|
+
status(): Promise<DaemonStatus>;
|
|
58
|
+
/** Stops the daemon if this session spawned it. */
|
|
45
59
|
close(): Promise<void>;
|
|
46
60
|
}
|
|
61
|
+
/** The publisher of a session. */
|
|
47
62
|
declare class Streamer {
|
|
48
|
-
|
|
63
|
+
daemonUrl: string;
|
|
49
64
|
ready: Promise<StreamerStatus>;
|
|
50
|
-
constructor(
|
|
65
|
+
constructor(daemonUrl: string, ready: Promise<StreamerStatus>);
|
|
66
|
+
/** Current state of the publisher. */
|
|
51
67
|
status(): Promise<StreamerStatus>;
|
|
68
|
+
/** Resolves once the stream is live, rejects if it ends or fails first. */
|
|
52
69
|
waitUntilLive({ timeoutMs, intervalMs, }?: {
|
|
53
70
|
timeoutMs?: number;
|
|
54
71
|
intervalMs?: number;
|
|
55
72
|
}): Promise<StreamerStatus>;
|
|
73
|
+
/** Stops publishing. */
|
|
56
74
|
stop(): Promise<StreamerStatus>;
|
|
57
75
|
}
|
|
76
|
+
/** A viewer of a session. */
|
|
58
77
|
declare class Viewer {
|
|
59
|
-
|
|
78
|
+
daemonUrl: string;
|
|
60
79
|
id: string;
|
|
61
|
-
constructor(
|
|
80
|
+
constructor(daemonUrl: string, id: string);
|
|
81
|
+
/** Current state of the viewer. */
|
|
62
82
|
status(): Promise<ViewerStatus>;
|
|
83
|
+
/** Metrics collected so far. */
|
|
63
84
|
metrics(): Promise<Metrics>;
|
|
85
|
+
/** Resolves once the viewer ends, fails or is stopped, calling `onUpdate` with each polled state. */
|
|
64
86
|
waitUntilDone({ intervalMs, onUpdate, }?: {
|
|
65
87
|
intervalMs?: number;
|
|
66
88
|
onUpdate?: (viewer: ViewerStatus) => void;
|
|
67
89
|
}): Promise<ViewerStatus>;
|
|
90
|
+
/** Stops the viewer and returns its final metrics. */
|
|
68
91
|
stop(): Promise<Metrics>;
|
|
69
92
|
}
|
|
70
93
|
export type { Session, Streamer, Viewer };
|
package/dist/stream_doctor.js
CHANGED
|
@@ -1,19 +1,39 @@
|
|
|
1
|
-
// Client for the stream_doctor server. `session()` spawns the binary (the one
|
|
2
|
-
// bundled for this platform, or `binary`) when no server is listening;
|
|
3
|
-
// `session.close()` stops it. The daemon's output goes to `session.logFile`.
|
|
4
1
|
import { spawn } from "node:child_process";
|
|
5
2
|
import fs from "node:fs";
|
|
6
3
|
import { createRequire } from "node:module";
|
|
4
|
+
import net, {} from "node:net";
|
|
7
5
|
import os from "node:os";
|
|
8
6
|
import path from "node:path";
|
|
9
|
-
const
|
|
7
|
+
const LOG_FILE = path.join(os.tmpdir(), "stream_doctor.log");
|
|
10
8
|
const PLATFORM_PACKAGES = {
|
|
11
9
|
"darwin-arm64": "@stream-doctor/darwin-arm64",
|
|
12
10
|
"linux-arm64": "@stream-doctor/linux-arm64",
|
|
13
11
|
"linux-x64": "@stream-doctor/linux-x64",
|
|
14
12
|
};
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
const TERMINAL_STATUSES = ["ended", "failed", "stopped"];
|
|
14
|
+
/** Spawns a daemon from `binary` (the bundled one by default) on `port` (a free one by default), or connects to a running one when `daemonUrl` is set. */
|
|
15
|
+
export async function session({ daemonUrl, binary, port, } = {}) {
|
|
16
|
+
if (daemonUrl) {
|
|
17
|
+
if (binary !== undefined || port !== undefined) {
|
|
18
|
+
throw new Error("`daemonUrl` connects to a running daemon, it cannot be combined with `binary` or `port`");
|
|
19
|
+
}
|
|
20
|
+
await api("GET", "/status", null, daemonUrl);
|
|
21
|
+
return new Session(daemonUrl, null);
|
|
22
|
+
}
|
|
23
|
+
let installDir;
|
|
24
|
+
if (!binary) {
|
|
25
|
+
binary = bundledBinary() ?? undefined;
|
|
26
|
+
if (!binary) {
|
|
27
|
+
throw new Error(`no stream_doctor binary bundled for ${process.platform}-${process.arch}, pass one with \`binary\` or a running daemon with \`daemonUrl\``);
|
|
28
|
+
}
|
|
29
|
+
installDir = path.join(path.dirname(binary), "..", ".burrito");
|
|
30
|
+
}
|
|
31
|
+
port ??= await freePort();
|
|
32
|
+
const url = `http://localhost:${port}`;
|
|
33
|
+
const child = await spawnDaemon(binary, url, port, installDir);
|
|
34
|
+
return new Session(url, child, LOG_FILE);
|
|
35
|
+
}
|
|
36
|
+
/** Path to the daemon binary bundled for this platform, or null if there is none. */
|
|
17
37
|
export function bundledBinary() {
|
|
18
38
|
const pkg = PLATFORM_PACKAGES[`${process.platform}-${process.arch}`];
|
|
19
39
|
if (!pkg)
|
|
@@ -25,95 +45,31 @@ export function bundledBinary() {
|
|
|
25
45
|
return null;
|
|
26
46
|
}
|
|
27
47
|
}
|
|
28
|
-
|
|
29
|
-
let res;
|
|
30
|
-
try {
|
|
31
|
-
res = await fetch(server + path, {
|
|
32
|
-
method,
|
|
33
|
-
headers: body ? { "content-type": "application/json" } : undefined,
|
|
34
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
catch (e) {
|
|
38
|
-
throw new Error(`${method} ${path}: cannot reach ${server} (is the server running?): ${e.message}`);
|
|
39
|
-
}
|
|
40
|
-
const text = await res.text();
|
|
41
|
-
if (!res.ok)
|
|
42
|
-
throw new Error(`${method} ${path}: HTTP ${res.status}: ${text}`);
|
|
43
|
-
return JSON.parse(text);
|
|
44
|
-
}
|
|
45
|
-
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
46
|
-
const TERMINAL_STATUSES = ["ended", "failed", "stopped"];
|
|
47
|
-
export async function session({ server = DEFAULT_SERVER, binary, } = {}) {
|
|
48
|
-
try {
|
|
49
|
-
await api("GET", "/status", null, server);
|
|
50
|
-
return new Session(server, null);
|
|
51
|
-
}
|
|
52
|
-
catch (e) {
|
|
53
|
-
let installDir;
|
|
54
|
-
const logFile = path.join(os.tmpdir(), "stream_doctor.log");
|
|
55
|
-
if (!binary) {
|
|
56
|
-
binary = bundledBinary() ?? undefined;
|
|
57
|
-
if (!binary) {
|
|
58
|
-
throw new Error(`${e.message}; no stream_doctor binary bundled for ${process.platform}-${process.arch}, pass one with \`binary\``);
|
|
59
|
-
}
|
|
60
|
-
// Burrito unpacks the payload once per release name + ERTS + app version,
|
|
61
|
-
// none of which change between npm releases, so its default cache under
|
|
62
|
-
// ~/.local/share would keep serving the previous package's daemon.
|
|
63
|
-
installDir = path.join(path.dirname(binary), "..", ".burrito");
|
|
64
|
-
}
|
|
65
|
-
const child = await spawnServer(binary, server, logFile, installDir);
|
|
66
|
-
return new Session(server, child, logFile);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
async function spawnServer(binary, server, logFile, installDir) {
|
|
70
|
-
if (!fs.existsSync(binary)) {
|
|
71
|
-
throw new Error(`${binary} not found, build it with: MIX_ENV=prod mix release`);
|
|
72
|
-
}
|
|
73
|
-
const env = {
|
|
74
|
-
...process.env,
|
|
75
|
-
// the daemon quits once its stdin (the pipe below) breaks, i.e. when this
|
|
76
|
-
// process dies, however it dies
|
|
77
|
-
STREAM_DOCTOR_EXIT_ON_STDIN_EOF: "1",
|
|
78
|
-
...(installDir ? { STREAM_DOCTOR_INSTALL_DIR: installDir } : {}),
|
|
79
|
-
};
|
|
80
|
-
const log = fs.openSync(logFile, "a");
|
|
81
|
-
// own process group, so that killing the burrito launcher takes the BEAM with it
|
|
82
|
-
const child = spawn(binary, [], { stdio: ["pipe", log, log], detached: true, env });
|
|
83
|
-
child.on("exit", () => fs.closeSync(log));
|
|
84
|
-
for (const deadline = Date.now() + 120_000; Date.now() < deadline;) {
|
|
85
|
-
if (child.exitCode !== null) {
|
|
86
|
-
throw new Error(`server exited with ${child.exitCode}, see ${logFile}`);
|
|
87
|
-
}
|
|
88
|
-
await sleep(1000);
|
|
89
|
-
try {
|
|
90
|
-
await api("GET", "/status", null, server);
|
|
91
|
-
return child;
|
|
92
|
-
}
|
|
93
|
-
catch { }
|
|
94
|
-
}
|
|
95
|
-
throw new Error(`server didn't come up in 2 minutes, see ${logFile}`);
|
|
96
|
-
}
|
|
48
|
+
/** A connection to the daemon, grouping one publisher and any number of viewers. */
|
|
97
49
|
class Session {
|
|
98
|
-
|
|
50
|
+
daemonUrl;
|
|
99
51
|
child;
|
|
100
52
|
logFile;
|
|
101
|
-
constructor(
|
|
102
|
-
this.
|
|
53
|
+
constructor(daemonUrl, child, logFile = null) {
|
|
54
|
+
this.daemonUrl = daemonUrl;
|
|
103
55
|
this.child = child;
|
|
104
56
|
this.logFile = logFile;
|
|
105
57
|
}
|
|
58
|
+
/** Starts publishing `file` to `rtmpUrl`. */
|
|
106
59
|
publish(rtmpUrl, { file = "test.mp4" } = {}) {
|
|
107
|
-
const ready = api("POST", "/streamer", { input: file, rtmp_url: rtmpUrl }, this.
|
|
108
|
-
return new Streamer(this.
|
|
60
|
+
const ready = api("POST", "/streamer", { input: file, rtmp_url: rtmpUrl }, this.daemonUrl);
|
|
61
|
+
return new Streamer(this.daemonUrl, ready);
|
|
109
62
|
}
|
|
63
|
+
/** Starts a viewer collecting metrics from the HLS playlist at `hlsUrl`. */
|
|
110
64
|
async watch(hlsUrl) {
|
|
111
|
-
const { id } = await api("POST", "/viewers", { hls_url: hlsUrl }, this.
|
|
112
|
-
return new Viewer(this.
|
|
65
|
+
const { id } = await api("POST", "/viewers", { hls_url: hlsUrl }, this.daemonUrl);
|
|
66
|
+
return new Viewer(this.daemonUrl, id);
|
|
113
67
|
}
|
|
68
|
+
/** Current state of the daemon. */
|
|
114
69
|
status() {
|
|
115
|
-
return api("GET", "/status", null, this.
|
|
70
|
+
return api("GET", "/status", null, this.daemonUrl);
|
|
116
71
|
}
|
|
72
|
+
/** Stops the daemon if this session spawned it. */
|
|
117
73
|
close() {
|
|
118
74
|
const child = this.child;
|
|
119
75
|
if (!child || child.exitCode !== null || child.pid === undefined)
|
|
@@ -126,22 +82,25 @@ class Session {
|
|
|
126
82
|
});
|
|
127
83
|
}
|
|
128
84
|
}
|
|
85
|
+
/** The publisher of a session. */
|
|
129
86
|
class Streamer {
|
|
130
|
-
|
|
87
|
+
daemonUrl;
|
|
131
88
|
ready;
|
|
132
|
-
constructor(
|
|
133
|
-
this.
|
|
89
|
+
constructor(daemonUrl, ready) {
|
|
90
|
+
this.daemonUrl = daemonUrl;
|
|
134
91
|
this.ready = ready;
|
|
135
92
|
ready.catch(() => { });
|
|
136
93
|
}
|
|
94
|
+
/** Current state of the publisher. */
|
|
137
95
|
status() {
|
|
138
|
-
return this.ready.then(() => api("GET", "/streamer", null, this.
|
|
96
|
+
return this.ready.then(() => api("GET", "/streamer", null, this.daemonUrl));
|
|
139
97
|
}
|
|
98
|
+
/** Resolves once the stream is live, rejects if it ends or fails first. */
|
|
140
99
|
async waitUntilLive({ timeoutMs = 60_000, intervalMs = 250, } = {}) {
|
|
141
100
|
await this.ready;
|
|
142
101
|
const deadline = Date.now() + timeoutMs;
|
|
143
102
|
for (;;) {
|
|
144
|
-
const streamer = await api("GET", "/streamer", null, this.
|
|
103
|
+
const streamer = await api("GET", "/streamer", null, this.daemonUrl);
|
|
145
104
|
if (streamer.live)
|
|
146
105
|
return streamer;
|
|
147
106
|
if (TERMINAL_STATUSES.includes(streamer.status)) {
|
|
@@ -152,25 +111,30 @@ class Streamer {
|
|
|
152
111
|
await sleep(intervalMs);
|
|
153
112
|
}
|
|
154
113
|
}
|
|
114
|
+
/** Stops publishing. */
|
|
155
115
|
async stop() {
|
|
156
116
|
await this.ready;
|
|
157
|
-
return api("DELETE", "/streamer", null, this.
|
|
117
|
+
return api("DELETE", "/streamer", null, this.daemonUrl);
|
|
158
118
|
}
|
|
159
119
|
}
|
|
120
|
+
/** A viewer of a session. */
|
|
160
121
|
class Viewer {
|
|
161
|
-
|
|
122
|
+
daemonUrl;
|
|
162
123
|
id;
|
|
163
|
-
constructor(
|
|
164
|
-
this.
|
|
124
|
+
constructor(daemonUrl, id) {
|
|
125
|
+
this.daemonUrl = daemonUrl;
|
|
165
126
|
this.id = id;
|
|
166
127
|
}
|
|
128
|
+
/** Current state of the viewer. */
|
|
167
129
|
status() {
|
|
168
|
-
return api("GET", `/viewers/${this.id}`, null, this.
|
|
130
|
+
return api("GET", `/viewers/${this.id}`, null, this.daemonUrl);
|
|
169
131
|
}
|
|
132
|
+
/** Metrics collected so far. */
|
|
170
133
|
async metrics() {
|
|
171
134
|
const { metrics } = await this.status();
|
|
172
135
|
return metrics;
|
|
173
136
|
}
|
|
137
|
+
/** Resolves once the viewer ends, fails or is stopped, calling `onUpdate` with each polled state. */
|
|
174
138
|
async waitUntilDone({ intervalMs = 1000, onUpdate, } = {}) {
|
|
175
139
|
for (;;) {
|
|
176
140
|
const viewer = await this.status();
|
|
@@ -180,8 +144,63 @@ class Viewer {
|
|
|
180
144
|
await sleep(intervalMs);
|
|
181
145
|
}
|
|
182
146
|
}
|
|
147
|
+
/** Stops the viewer and returns its final metrics. */
|
|
183
148
|
async stop() {
|
|
184
|
-
const { metrics } = await api("DELETE", `/viewers/${this.id}`, null, this.
|
|
149
|
+
const { metrics } = await api("DELETE", `/viewers/${this.id}`, null, this.daemonUrl);
|
|
185
150
|
return metrics;
|
|
186
151
|
}
|
|
187
152
|
}
|
|
153
|
+
async function spawnDaemon(binary, daemonUrl, port, installDir) {
|
|
154
|
+
if (!fs.existsSync(binary)) {
|
|
155
|
+
throw new Error(`${binary} not found, build it with: MIX_ENV=prod mix release`);
|
|
156
|
+
}
|
|
157
|
+
const env = {
|
|
158
|
+
...process.env,
|
|
159
|
+
STREAM_DOCTOR_EXIT_ON_STDIN_EOF: "1",
|
|
160
|
+
PORT: String(port),
|
|
161
|
+
...(installDir ? { STREAM_DOCTOR_INSTALL_DIR: installDir } : {}),
|
|
162
|
+
};
|
|
163
|
+
const log = fs.openSync(LOG_FILE, "a");
|
|
164
|
+
const child = spawn(binary, [], { stdio: ["pipe", log, log], detached: true, env });
|
|
165
|
+
child.on("exit", () => fs.closeSync(log));
|
|
166
|
+
for (const deadline = Date.now() + 120_000; Date.now() < deadline;) {
|
|
167
|
+
if (child.exitCode !== null) {
|
|
168
|
+
throw new Error(`daemon exited with ${child.exitCode}, see ${LOG_FILE}`);
|
|
169
|
+
}
|
|
170
|
+
await sleep(1000);
|
|
171
|
+
try {
|
|
172
|
+
await api("GET", "/status", null, daemonUrl);
|
|
173
|
+
return child;
|
|
174
|
+
}
|
|
175
|
+
catch { }
|
|
176
|
+
}
|
|
177
|
+
throw new Error(`daemon didn't come up in 2 minutes, see ${LOG_FILE}`);
|
|
178
|
+
}
|
|
179
|
+
async function api(method, path, body, daemonUrl) {
|
|
180
|
+
let res;
|
|
181
|
+
try {
|
|
182
|
+
res = await fetch(daemonUrl + path, {
|
|
183
|
+
method,
|
|
184
|
+
headers: body ? { "content-type": "application/json" } : undefined,
|
|
185
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
catch (e) {
|
|
189
|
+
throw new Error(`${method} ${path}: cannot reach ${daemonUrl} (is the daemon running?): ${e.message}`);
|
|
190
|
+
}
|
|
191
|
+
const text = await res.text();
|
|
192
|
+
if (!res.ok)
|
|
193
|
+
throw new Error(`${method} ${path}: HTTP ${res.status}: ${text}`);
|
|
194
|
+
return JSON.parse(text);
|
|
195
|
+
}
|
|
196
|
+
function freePort() {
|
|
197
|
+
return new Promise((resolve, reject) => {
|
|
198
|
+
const probe = net.createServer();
|
|
199
|
+
probe.once("error", reject);
|
|
200
|
+
probe.listen(0, () => {
|
|
201
|
+
const { port } = probe.address();
|
|
202
|
+
probe.close(() => resolve(port));
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stream-doctor",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "Automated end-to-end testing for video infrastructure (early proof of concept)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/membraneframework-
|
|
8
|
+
"url": "git+https://github.com/membraneframework/stream-doctor.git"
|
|
9
9
|
},
|
|
10
10
|
"type": "module",
|
|
11
11
|
"exports": "./dist/stream_doctor.js",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"typescript-eslint": "^8.70.0"
|
|
34
34
|
},
|
|
35
35
|
"optionalDependencies": {
|
|
36
|
-
"@stream-doctor/darwin-arm64": "0.0.
|
|
37
|
-
"@stream-doctor/linux-arm64": "0.0.
|
|
38
|
-
"@stream-doctor/linux-x64": "0.0.
|
|
36
|
+
"@stream-doctor/darwin-arm64": "0.0.6",
|
|
37
|
+
"@stream-doctor/linux-arm64": "0.0.6",
|
|
38
|
+
"@stream-doctor/linux-x64": "0.0.6"
|
|
39
39
|
}
|
|
40
40
|
}
|