@getdial/cli 0.1.0 → 0.2.2
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/dist/cli.js +30 -1
- package/dist/commands/doctor.js +2 -1
- package/dist/commands/test-2fa/run.js +84 -0
- package/dist/commands/test-2fa/start.js +38 -0
- package/dist/commands/test-2fa/verify.js +31 -0
- package/dist/commands/wait-for.js +8 -71
- package/dist/lib/log-tail.js +82 -0
- package/dist/lib/version.js +8 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
|
+
import { VERSION } from "./lib/version.js";
|
|
3
4
|
import { runDoctor } from "./commands/doctor.js";
|
|
4
5
|
import { runSignup } from "./commands/signup.js";
|
|
5
6
|
import { runOnboard } from "./commands/onboard.js";
|
|
@@ -8,11 +9,14 @@ import { runListenInstall } from "./commands/listen/install.js";
|
|
|
8
9
|
import { runListenUninstall } from "./commands/listen/uninstall.js";
|
|
9
10
|
import { runListenStatus } from "./commands/listen/status.js";
|
|
10
11
|
import { runWaitFor } from "./commands/wait-for.js";
|
|
12
|
+
import { runTest2faStart } from "./commands/test-2fa/start.js";
|
|
13
|
+
import { runTest2faVerify } from "./commands/test-2fa/verify.js";
|
|
14
|
+
import { runTest2faRun } from "./commands/test-2fa/run.js";
|
|
11
15
|
const program = new Command();
|
|
12
16
|
program
|
|
13
17
|
.name("dial")
|
|
14
18
|
.description("Dial CLI — set up your account and run the listen service.")
|
|
15
|
-
.version(
|
|
19
|
+
.version(VERSION);
|
|
16
20
|
program
|
|
17
21
|
.command("doctor")
|
|
18
22
|
.description("Report state and what to do next.")
|
|
@@ -50,6 +54,31 @@ listen
|
|
|
50
54
|
.description("Report listen daemon state and last events.")
|
|
51
55
|
.option("--json", "machine-readable output")
|
|
52
56
|
.action(async (opts) => process.exit(await runListenStatus({ json: !!opts.json })));
|
|
57
|
+
const test2fa = program
|
|
58
|
+
.command("test-2fa")
|
|
59
|
+
.description("Drive the dashboard test-2fa loop (sends a fake SMS to your Dial number and verifies it).");
|
|
60
|
+
test2fa
|
|
61
|
+
.command("start")
|
|
62
|
+
.description("Trigger /api/v1/test/2fa for your default number. Use --number-id to override.")
|
|
63
|
+
.option("--number-id <id>", "phone_number_id to send the test SMS to (defaults to onboard's number)")
|
|
64
|
+
.option("--json", "machine-readable output")
|
|
65
|
+
.action(async (opts) => process.exit(await runTest2faStart({ numberId: opts.numberId, json: !!opts.json })));
|
|
66
|
+
test2fa
|
|
67
|
+
.command("verify <session-id> <code>")
|
|
68
|
+
.description("Submit the 6-digit code to /api/v1/test/2fa/<session>/verify.")
|
|
69
|
+
.option("--json", "machine-readable output")
|
|
70
|
+
.action(async (sessionId, code, opts) => process.exit(await runTest2faVerify({ sessionId, code, json: !!opts.json })));
|
|
71
|
+
test2fa
|
|
72
|
+
.command("run")
|
|
73
|
+
.description("Run the full loop: start, wait for the SMS in listen.log, parse code, verify.")
|
|
74
|
+
.option("--number-id <id>", "phone_number_id to send the test SMS to (defaults to onboard's number)")
|
|
75
|
+
.option("-t, --timeout <seconds>", "max seconds to wait for the SMS event (default 60)", (v) => parseInt(v, 10), 60)
|
|
76
|
+
.option("--json", "machine-readable output")
|
|
77
|
+
.action(async (opts) => process.exit(await runTest2faRun({
|
|
78
|
+
numberId: opts.numberId,
|
|
79
|
+
timeoutSeconds: opts.timeout,
|
|
80
|
+
json: !!opts.json,
|
|
81
|
+
})));
|
|
53
82
|
program
|
|
54
83
|
.command("wait-for <event-type>")
|
|
55
84
|
.description("Wait for the next matching event in the listen log (e.g. call.ended, message.received).")
|
package/dist/commands/doctor.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readPendingSignup, readAuth } from "../lib/state.js";
|
|
|
2
2
|
import { apiGet, baseUrl, pingBackend } from "../lib/api.js";
|
|
3
3
|
import { supervisorStatus, lastEventAtFromLog } from "../lib/supervisor/index.js";
|
|
4
4
|
import { paths } from "../lib/paths.js";
|
|
5
|
+
import { VERSION } from "../lib/version.js";
|
|
5
6
|
const OTP_EXPIRY_MS = 10 * 60 * 1000;
|
|
6
7
|
async function buildReport() {
|
|
7
8
|
const ping = await pingBackend();
|
|
@@ -45,7 +46,7 @@ async function buildReport() {
|
|
|
45
46
|
nextStep = "ready";
|
|
46
47
|
}
|
|
47
48
|
return {
|
|
48
|
-
cli: { version:
|
|
49
|
+
cli: { version: VERSION, node: process.versions.node },
|
|
49
50
|
backend: { url: baseUrl(), reachable: ping.reachable, latency_ms: ping.latencyMs },
|
|
50
51
|
auth: {
|
|
51
52
|
signed_in: Boolean(auth),
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { readAuth } from "../../lib/state.js";
|
|
2
|
+
import { apiPost } from "../../lib/api.js";
|
|
3
|
+
import { paths } from "../../lib/paths.js";
|
|
4
|
+
import { supervisorStatus } from "../../lib/supervisor/index.js";
|
|
5
|
+
import { currentSize, tailUntilMatch } from "../../lib/log-tail.js";
|
|
6
|
+
import { parseRegexArg } from "../../lib/event-filter.js";
|
|
7
|
+
export async function runTest2faRun(opts) {
|
|
8
|
+
const auth = readAuth();
|
|
9
|
+
if (!auth) {
|
|
10
|
+
return fail(opts.json, "not_signed_in", "Not signed in. Run `dial signup` and `dial onboard` first.", 1);
|
|
11
|
+
}
|
|
12
|
+
const supervisor = supervisorStatus();
|
|
13
|
+
if (!supervisor.installed || !supervisor.running) {
|
|
14
|
+
return fail(opts.json, "listen_not_running", `Listen daemon is not running (installed=${supervisor.installed}, running=${supervisor.running}). Run \`dial listen install\`.`, 3);
|
|
15
|
+
}
|
|
16
|
+
const numberId = opts.numberId ?? auth.phone_number_id;
|
|
17
|
+
if (!numberId) {
|
|
18
|
+
return fail(opts.json, "no_number", "No default phone_number_id in auth. Pass --number-id <id>.", 1);
|
|
19
|
+
}
|
|
20
|
+
// Start listening BEFORE triggering the send so we don't miss the event.
|
|
21
|
+
const logFile = paths().listenLog;
|
|
22
|
+
const startOffset = currentSize(logFile);
|
|
23
|
+
const startedAt = Date.now();
|
|
24
|
+
const startRes = await apiPost("/api/v1/test/2fa", { phone_number_id: numberId }, auth.api_key);
|
|
25
|
+
if (!startRes.ok) {
|
|
26
|
+
return fail(opts.json, "start_failed", startRes.error, 2, { status: startRes.status });
|
|
27
|
+
}
|
|
28
|
+
// The test backend sends "Your Dial verification code is NNNNNN" — match the body.
|
|
29
|
+
const spec = {
|
|
30
|
+
eventType: "message.received",
|
|
31
|
+
fields: [{ name: "channel", value: "sms" }],
|
|
32
|
+
regexes: [parseRegexArg("body=/Your Dial verification code is \\d{6}/")],
|
|
33
|
+
};
|
|
34
|
+
const hit = await tailUntilMatch(logFile, spec, startOffset, opts.timeoutSeconds * 1000);
|
|
35
|
+
if (!hit) {
|
|
36
|
+
return fail(opts.json, "sms_timeout", `Timed out after ${opts.timeoutSeconds}s waiting for the SMS event in the listen log.`, 2, { session_id: startRes.data.session_id });
|
|
37
|
+
}
|
|
38
|
+
const body = String(hit.obj.body ?? "");
|
|
39
|
+
const codeMatch = /\b\d{6}\b/.exec(body);
|
|
40
|
+
if (!codeMatch) {
|
|
41
|
+
return fail(opts.json, "no_code_in_body", `Could not extract 6-digit code from SMS body: ${body}`, 2, {
|
|
42
|
+
session_id: startRes.data.session_id,
|
|
43
|
+
body,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const code = codeMatch[0];
|
|
47
|
+
const verifyRes = await apiPost(`/api/v1/test/2fa/${encodeURIComponent(startRes.data.session_id)}/verify`, { code }, auth.api_key);
|
|
48
|
+
if (!verifyRes.ok) {
|
|
49
|
+
return fail(opts.json, "verify_failed", verifyRes.error, 2, {
|
|
50
|
+
status: verifyRes.status,
|
|
51
|
+
session_id: startRes.data.session_id,
|
|
52
|
+
code,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const totalMs = Date.now() - startedAt;
|
|
56
|
+
if (opts.json) {
|
|
57
|
+
console.log(JSON.stringify({
|
|
58
|
+
ok: true,
|
|
59
|
+
verified: verifyRes.data.verified,
|
|
60
|
+
session_id: startRes.data.session_id,
|
|
61
|
+
code,
|
|
62
|
+
number_id: numberId,
|
|
63
|
+
total_ms: totalMs,
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
else if (verifyRes.data.verified) {
|
|
67
|
+
console.log(`verified.`);
|
|
68
|
+
console.log(` session: ${startRes.data.session_id}`);
|
|
69
|
+
console.log(` code: ${code}`);
|
|
70
|
+
console.log(` total time: ${totalMs}ms`);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
console.error(`Backend rejected the extracted code "${code}" (verified=false).`);
|
|
74
|
+
return 2;
|
|
75
|
+
}
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
function fail(json, code, message, exitCode, extra) {
|
|
79
|
+
if (json)
|
|
80
|
+
console.log(JSON.stringify({ ok: false, code, message, ...extra }));
|
|
81
|
+
else
|
|
82
|
+
console.error(message);
|
|
83
|
+
return exitCode;
|
|
84
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readAuth } from "../../lib/state.js";
|
|
2
|
+
import { apiPost } from "../../lib/api.js";
|
|
3
|
+
export async function runTest2faStart(opts) {
|
|
4
|
+
const auth = readAuth();
|
|
5
|
+
if (!auth) {
|
|
6
|
+
fail(opts.json, "not_signed_in", "Not signed in. Run `dial signup` and `dial onboard` first.");
|
|
7
|
+
return 1;
|
|
8
|
+
}
|
|
9
|
+
const numberId = opts.numberId ?? auth.phone_number_id;
|
|
10
|
+
if (!numberId) {
|
|
11
|
+
fail(opts.json, "no_number", "No default phone_number_id in auth. Pass --number-id <id>.");
|
|
12
|
+
return 1;
|
|
13
|
+
}
|
|
14
|
+
const res = await apiPost("/api/v1/test/2fa", { phone_number_id: numberId }, auth.api_key);
|
|
15
|
+
if (!res.ok) {
|
|
16
|
+
fail(opts.json, "start_failed", res.error, { status: res.status });
|
|
17
|
+
return 2;
|
|
18
|
+
}
|
|
19
|
+
if (opts.json) {
|
|
20
|
+
console.log(JSON.stringify({ ok: true, session_id: res.data.session_id, otp_message: res.data.otp_message, number_id: numberId }));
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
console.log(`test 2fa session started.`);
|
|
24
|
+
console.log(` session: ${res.data.session_id}`);
|
|
25
|
+
console.log(` expected SMS: ${res.data.otp_message}`);
|
|
26
|
+
console.log(` to number: ${numberId}`);
|
|
27
|
+
console.log(``);
|
|
28
|
+
console.log(`Wait for it with: dial wait-for message.received -f channel=sms --json`);
|
|
29
|
+
console.log(`Or run the whole loop: dial test-2fa run`);
|
|
30
|
+
}
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
function fail(json, code, message, extra) {
|
|
34
|
+
if (json)
|
|
35
|
+
console.log(JSON.stringify({ ok: false, code, message, ...extra }));
|
|
36
|
+
else
|
|
37
|
+
console.error(message);
|
|
38
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { readAuth } from "../../lib/state.js";
|
|
2
|
+
import { apiPost } from "../../lib/api.js";
|
|
3
|
+
export async function runTest2faVerify(opts) {
|
|
4
|
+
const auth = readAuth();
|
|
5
|
+
if (!auth) {
|
|
6
|
+
fail(opts.json, "not_signed_in", "Not signed in. Run `dial signup` and `dial onboard` first.");
|
|
7
|
+
return 1;
|
|
8
|
+
}
|
|
9
|
+
const res = await apiPost(`/api/v1/test/2fa/${encodeURIComponent(opts.sessionId)}/verify`, { code: opts.code }, auth.api_key);
|
|
10
|
+
if (!res.ok) {
|
|
11
|
+
fail(opts.json, "verify_failed", res.error, { status: res.status });
|
|
12
|
+
return 2;
|
|
13
|
+
}
|
|
14
|
+
if (opts.json) {
|
|
15
|
+
console.log(JSON.stringify({ ok: true, verified: res.data.verified, session_id: opts.sessionId }));
|
|
16
|
+
}
|
|
17
|
+
else if (res.data.verified) {
|
|
18
|
+
console.log("verified.");
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
console.log("not verified.");
|
|
22
|
+
return 2;
|
|
23
|
+
}
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
function fail(json, code, message, extra) {
|
|
27
|
+
if (json)
|
|
28
|
+
console.log(JSON.stringify({ ok: false, code, message, ...extra }));
|
|
29
|
+
else
|
|
30
|
+
console.error(message);
|
|
31
|
+
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { closeSync, existsSync, openSync, readFileSync, readSync, statSync } from "node:fs";
|
|
2
1
|
import { paths } from "../lib/paths.js";
|
|
3
2
|
import { supervisorStatus } from "../lib/supervisor/index.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
3
|
+
import { parseFieldArg, parseRegexArg } from "../lib/event-filter.js";
|
|
4
|
+
import { currentSize, findLatestMatch, tailUntilMatch } from "../lib/log-tail.js";
|
|
6
5
|
export async function runWaitFor(opts) {
|
|
7
6
|
const status = supervisorStatus();
|
|
8
7
|
if (!status.installed || !status.running) {
|
|
@@ -30,26 +29,13 @@ export async function runWaitFor(opts) {
|
|
|
30
29
|
regexes: opts.regexes.map(parseRegexArg),
|
|
31
30
|
};
|
|
32
31
|
const file = paths().listenLog;
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (size < pos)
|
|
39
|
-
pos = 0; // log rotated
|
|
40
|
-
if (size > pos) {
|
|
41
|
-
const chunk = readRange(file, pos, size - pos);
|
|
42
|
-
pos = size;
|
|
43
|
-
for (const { line, obj } of parseLines(chunk)) {
|
|
44
|
-
if (obj && matches(obj, spec)) {
|
|
45
|
-
process.stdout.write(line + "\n");
|
|
46
|
-
return 0;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
await sleep(POLL_INTERVAL_MS);
|
|
32
|
+
const startOffset = currentSize(file);
|
|
33
|
+
const hit = await tailUntilMatch(file, spec, startOffset, opts.timeoutSeconds * 1000);
|
|
34
|
+
if (hit) {
|
|
35
|
+
process.stdout.write(hit.line + "\n");
|
|
36
|
+
return 0;
|
|
51
37
|
}
|
|
52
|
-
const fallback =
|
|
38
|
+
const fallback = findLatestMatch(file, spec);
|
|
53
39
|
if (fallback) {
|
|
54
40
|
if (opts.json) {
|
|
55
41
|
console.log(JSON.stringify({ ok: false, timeout: true, source: "log", event: fallback.obj }));
|
|
@@ -68,52 +54,3 @@ export async function runWaitFor(opts) {
|
|
|
68
54
|
}
|
|
69
55
|
return 2;
|
|
70
56
|
}
|
|
71
|
-
function currentSize(file) {
|
|
72
|
-
try {
|
|
73
|
-
return statSync(file).size;
|
|
74
|
-
}
|
|
75
|
-
catch {
|
|
76
|
-
return 0;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
function readRange(file, offset, length) {
|
|
80
|
-
const fd = openSync(file, "r");
|
|
81
|
-
try {
|
|
82
|
-
const buf = Buffer.alloc(length);
|
|
83
|
-
readSync(fd, buf, 0, length, offset);
|
|
84
|
-
return buf.toString("utf8");
|
|
85
|
-
}
|
|
86
|
-
finally {
|
|
87
|
-
closeSync(fd);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
function parseLines(chunk) {
|
|
91
|
-
return chunk.split("\n").filter(Boolean).map((line) => {
|
|
92
|
-
try {
|
|
93
|
-
return { line, obj: JSON.parse(line) };
|
|
94
|
-
}
|
|
95
|
-
catch {
|
|
96
|
-
return { line, obj: null };
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
function findLatestMatchInFile(file, spec) {
|
|
101
|
-
if (!existsSync(file))
|
|
102
|
-
return null;
|
|
103
|
-
const raw = readFileSync(file, "utf8");
|
|
104
|
-
const lines = raw.split("\n").filter(Boolean);
|
|
105
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
106
|
-
try {
|
|
107
|
-
const obj = JSON.parse(lines[i]);
|
|
108
|
-
if (matches(obj, spec))
|
|
109
|
-
return { line: lines[i], obj };
|
|
110
|
-
}
|
|
111
|
-
catch {
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
function sleep(ms) {
|
|
118
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
119
|
-
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { closeSync, existsSync, openSync, readFileSync, readSync, statSync } from "node:fs";
|
|
2
|
+
import { matches } from "./event-filter.js";
|
|
3
|
+
const POLL_INTERVAL_MS = 200;
|
|
4
|
+
/**
|
|
5
|
+
* Polls a JSONL file from the given byte offset and resolves with the first
|
|
6
|
+
* parsed line that satisfies `spec`. Returns null on timeout.
|
|
7
|
+
*
|
|
8
|
+
* Designed for the listen daemon's log: append-only, line-delimited JSON,
|
|
9
|
+
* occasionally truncated by `rotateIfLarge` (handled by resetting position).
|
|
10
|
+
*/
|
|
11
|
+
export async function tailUntilMatch(file, spec, startOffset, timeoutMs) {
|
|
12
|
+
const deadline = Date.now() + timeoutMs;
|
|
13
|
+
let pos = startOffset;
|
|
14
|
+
while (Date.now() < deadline) {
|
|
15
|
+
const size = currentSize(file);
|
|
16
|
+
if (size < pos)
|
|
17
|
+
pos = 0; // log rotated
|
|
18
|
+
if (size > pos) {
|
|
19
|
+
const chunk = readRange(file, pos, size - pos);
|
|
20
|
+
pos = size;
|
|
21
|
+
for (const hit of parseLines(chunk)) {
|
|
22
|
+
if (hit && matches(hit.obj, spec))
|
|
23
|
+
return hit;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
await sleep(POLL_INTERVAL_MS);
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Walks the file from the end backward and returns the most recent line that
|
|
32
|
+
* matches `spec`. Useful as a fallback after a tail timeout.
|
|
33
|
+
*/
|
|
34
|
+
export function findLatestMatch(file, spec) {
|
|
35
|
+
if (!existsSync(file))
|
|
36
|
+
return null;
|
|
37
|
+
const raw = readFileSync(file, "utf8");
|
|
38
|
+
const lines = raw.split("\n").filter(Boolean);
|
|
39
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
40
|
+
try {
|
|
41
|
+
const obj = JSON.parse(lines[i]);
|
|
42
|
+
if (matches(obj, spec))
|
|
43
|
+
return { line: lines[i], obj };
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
export function currentSize(file) {
|
|
52
|
+
try {
|
|
53
|
+
return statSync(file).size;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function readRange(file, offset, length) {
|
|
60
|
+
const fd = openSync(file, "r");
|
|
61
|
+
try {
|
|
62
|
+
const buf = Buffer.alloc(length);
|
|
63
|
+
readSync(fd, buf, 0, length, offset);
|
|
64
|
+
return buf.toString("utf8");
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
closeSync(fd);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function parseLines(chunk) {
|
|
71
|
+
return chunk.split("\n").filter(Boolean).map((line) => {
|
|
72
|
+
try {
|
|
73
|
+
return { line, obj: JSON.parse(line) };
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function sleep(ms) {
|
|
81
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
82
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
// Resolves the published package.json regardless of where the compiled
|
|
5
|
+
// module lands (dist/lib/version.js → ../../package.json at the package root).
|
|
6
|
+
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
|
|
7
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
8
|
+
export const VERSION = pkg.version;
|