speechflow 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/etc/stx.conf +54 -58
- package/package.json +25 -106
- package/{etc → speechflow-cli/etc}/eslint.mjs +1 -2
- package/speechflow-cli/etc/stx.conf +77 -0
- package/speechflow-cli/package.json +116 -0
- package/{src → speechflow-cli/src}/speechflow-node-a2a-gender.ts +148 -64
- package/speechflow-cli/src/speechflow-node-a2a-meter.ts +217 -0
- package/{src → speechflow-cli/src}/speechflow-node-a2a-mute.ts +39 -11
- package/speechflow-cli/src/speechflow-node-a2a-vad.ts +384 -0
- package/{src → speechflow-cli/src}/speechflow-node-a2a-wav.ts +27 -11
- package/speechflow-cli/src/speechflow-node-a2t-deepgram.ts +313 -0
- package/{src → speechflow-cli/src}/speechflow-node-t2a-elevenlabs.ts +59 -12
- package/{src → speechflow-cli/src}/speechflow-node-t2a-kokoro.ts +11 -4
- package/{src → speechflow-cli/src}/speechflow-node-t2t-deepl.ts +9 -4
- package/{src → speechflow-cli/src}/speechflow-node-t2t-format.ts +2 -2
- package/{src → speechflow-cli/src}/speechflow-node-t2t-ollama.ts +1 -1
- package/{src → speechflow-cli/src}/speechflow-node-t2t-openai.ts +1 -1
- package/{src → speechflow-cli/src}/speechflow-node-t2t-sentence.ts +37 -20
- package/speechflow-cli/src/speechflow-node-t2t-subtitle.ts +276 -0
- package/{src → speechflow-cli/src}/speechflow-node-t2t-transformers.ts +4 -3
- package/{src → speechflow-cli/src}/speechflow-node-x2x-filter.ts +9 -5
- package/{src → speechflow-cli/src}/speechflow-node-x2x-trace.ts +16 -8
- package/{src → speechflow-cli/src}/speechflow-node-xio-device.ts +12 -8
- package/{src → speechflow-cli/src}/speechflow-node-xio-file.ts +9 -3
- package/{src → speechflow-cli/src}/speechflow-node-xio-mqtt.ts +5 -2
- package/{src → speechflow-cli/src}/speechflow-node-xio-websocket.ts +12 -12
- package/{src → speechflow-cli/src}/speechflow-node.ts +7 -0
- package/{src → speechflow-cli/src}/speechflow-utils.ts +78 -44
- package/{src → speechflow-cli/src}/speechflow.ts +188 -53
- package/speechflow-ui-db/etc/eslint.mjs +106 -0
- package/speechflow-ui-db/etc/htmllint.json +55 -0
- package/speechflow-ui-db/etc/stx.conf +79 -0
- package/speechflow-ui-db/etc/stylelint.js +46 -0
- package/speechflow-ui-db/etc/stylelint.yaml +33 -0
- package/speechflow-ui-db/etc/tsc-client.json +30 -0
- package/speechflow-ui-db/etc/tsc.node.json +9 -0
- package/speechflow-ui-db/etc/vite-client.mts +63 -0
- package/speechflow-ui-db/package.d/htmllint-cli+0.0.7.patch +20 -0
- package/speechflow-ui-db/package.json +75 -0
- package/speechflow-ui-db/src/app-icon.ai +1989 -4
- package/speechflow-ui-db/src/app-icon.svg +26 -0
- package/speechflow-ui-db/src/app.styl +64 -0
- package/speechflow-ui-db/src/app.vue +221 -0
- package/speechflow-ui-db/src/index.html +23 -0
- package/speechflow-ui-db/src/index.ts +26 -0
- package/{dst/speechflow.d.ts → speechflow-ui-db/src/lib.d.ts} +5 -3
- package/speechflow-ui-db/src/tsconfig.json +3 -0
- package/speechflow-ui-st/etc/eslint.mjs +106 -0
- package/speechflow-ui-st/etc/htmllint.json +55 -0
- package/speechflow-ui-st/etc/stx.conf +79 -0
- package/speechflow-ui-st/etc/stylelint.js +46 -0
- package/speechflow-ui-st/etc/stylelint.yaml +33 -0
- package/speechflow-ui-st/etc/tsc-client.json +30 -0
- package/speechflow-ui-st/etc/tsc.node.json +9 -0
- package/speechflow-ui-st/etc/vite-client.mts +63 -0
- package/speechflow-ui-st/package.d/htmllint-cli+0.0.7.patch +20 -0
- package/speechflow-ui-st/package.json +79 -0
- package/speechflow-ui-st/src/app-icon.ai +1989 -4
- package/speechflow-ui-st/src/app-icon.svg +26 -0
- package/speechflow-ui-st/src/app.styl +64 -0
- package/speechflow-ui-st/src/app.vue +142 -0
- package/speechflow-ui-st/src/index.html +23 -0
- package/speechflow-ui-st/src/index.ts +26 -0
- package/speechflow-ui-st/src/lib.d.ts +9 -0
- package/speechflow-ui-st/src/tsconfig.json +3 -0
- package/dst/speechflow-node-a2a-ffmpeg.d.ts +0 -13
- package/dst/speechflow-node-a2a-ffmpeg.js +0 -153
- package/dst/speechflow-node-a2a-ffmpeg.js.map +0 -1
- package/dst/speechflow-node-a2a-gender.d.ts +0 -18
- package/dst/speechflow-node-a2a-gender.js +0 -271
- package/dst/speechflow-node-a2a-gender.js.map +0 -1
- package/dst/speechflow-node-a2a-meter.d.ts +0 -12
- package/dst/speechflow-node-a2a-meter.js +0 -155
- package/dst/speechflow-node-a2a-meter.js.map +0 -1
- package/dst/speechflow-node-a2a-mute.d.ts +0 -16
- package/dst/speechflow-node-a2a-mute.js +0 -91
- package/dst/speechflow-node-a2a-mute.js.map +0 -1
- package/dst/speechflow-node-a2a-vad.d.ts +0 -16
- package/dst/speechflow-node-a2a-vad.js +0 -285
- package/dst/speechflow-node-a2a-vad.js.map +0 -1
- package/dst/speechflow-node-a2a-wav.d.ts +0 -11
- package/dst/speechflow-node-a2a-wav.js +0 -195
- package/dst/speechflow-node-a2a-wav.js.map +0 -1
- package/dst/speechflow-node-a2t-deepgram.d.ts +0 -15
- package/dst/speechflow-node-a2t-deepgram.js +0 -255
- package/dst/speechflow-node-a2t-deepgram.js.map +0 -1
- package/dst/speechflow-node-t2a-elevenlabs.d.ts +0 -16
- package/dst/speechflow-node-t2a-elevenlabs.js +0 -195
- package/dst/speechflow-node-t2a-elevenlabs.js.map +0 -1
- package/dst/speechflow-node-t2a-kokoro.d.ts +0 -13
- package/dst/speechflow-node-t2a-kokoro.js +0 -149
- package/dst/speechflow-node-t2a-kokoro.js.map +0 -1
- package/dst/speechflow-node-t2t-deepl.d.ts +0 -15
- package/dst/speechflow-node-t2t-deepl.js +0 -142
- package/dst/speechflow-node-t2t-deepl.js.map +0 -1
- package/dst/speechflow-node-t2t-format.d.ts +0 -11
- package/dst/speechflow-node-t2t-format.js +0 -82
- package/dst/speechflow-node-t2t-format.js.map +0 -1
- package/dst/speechflow-node-t2t-ollama.d.ts +0 -13
- package/dst/speechflow-node-t2t-ollama.js +0 -247
- package/dst/speechflow-node-t2t-ollama.js.map +0 -1
- package/dst/speechflow-node-t2t-openai.d.ts +0 -13
- package/dst/speechflow-node-t2t-openai.js +0 -227
- package/dst/speechflow-node-t2t-openai.js.map +0 -1
- package/dst/speechflow-node-t2t-sentence.d.ts +0 -17
- package/dst/speechflow-node-t2t-sentence.js +0 -234
- package/dst/speechflow-node-t2t-sentence.js.map +0 -1
- package/dst/speechflow-node-t2t-subtitle.d.ts +0 -13
- package/dst/speechflow-node-t2t-subtitle.js +0 -278
- package/dst/speechflow-node-t2t-subtitle.js.map +0 -1
- package/dst/speechflow-node-t2t-transformers.d.ts +0 -14
- package/dst/speechflow-node-t2t-transformers.js +0 -265
- package/dst/speechflow-node-t2t-transformers.js.map +0 -1
- package/dst/speechflow-node-x2x-filter.d.ts +0 -11
- package/dst/speechflow-node-x2x-filter.js +0 -117
- package/dst/speechflow-node-x2x-filter.js.map +0 -1
- package/dst/speechflow-node-x2x-trace.d.ts +0 -11
- package/dst/speechflow-node-x2x-trace.js +0 -111
- package/dst/speechflow-node-x2x-trace.js.map +0 -1
- package/dst/speechflow-node-xio-device.d.ts +0 -13
- package/dst/speechflow-node-xio-device.js +0 -226
- package/dst/speechflow-node-xio-device.js.map +0 -1
- package/dst/speechflow-node-xio-file.d.ts +0 -11
- package/dst/speechflow-node-xio-file.js +0 -210
- package/dst/speechflow-node-xio-file.js.map +0 -1
- package/dst/speechflow-node-xio-mqtt.d.ts +0 -13
- package/dst/speechflow-node-xio-mqtt.js +0 -185
- package/dst/speechflow-node-xio-mqtt.js.map +0 -1
- package/dst/speechflow-node-xio-websocket.d.ts +0 -13
- package/dst/speechflow-node-xio-websocket.js +0 -278
- package/dst/speechflow-node-xio-websocket.js.map +0 -1
- package/dst/speechflow-node.d.ts +0 -65
- package/dst/speechflow-node.js +0 -180
- package/dst/speechflow-node.js.map +0 -1
- package/dst/speechflow-utils.d.ts +0 -69
- package/dst/speechflow-utils.js +0 -486
- package/dst/speechflow-utils.js.map +0 -1
- package/dst/speechflow.js +0 -768
- package/dst/speechflow.js.map +0 -1
- package/src/speechflow-node-a2a-meter.ts +0 -130
- package/src/speechflow-node-a2a-vad.ts +0 -285
- package/src/speechflow-node-a2t-deepgram.ts +0 -234
- package/src/speechflow-node-t2t-subtitle.ts +0 -149
- /package/{etc → speechflow-cli/etc}/biome.jsonc +0 -0
- /package/{etc → speechflow-cli/etc}/oxlint.jsonc +0 -0
- /package/{etc → speechflow-cli/etc}/speechflow.bat +0 -0
- /package/{etc → speechflow-cli/etc}/speechflow.sh +0 -0
- /package/{etc → speechflow-cli/etc}/speechflow.yaml +0 -0
- /package/{etc → speechflow-cli/etc}/tsconfig.json +0 -0
- /package/{package.d → speechflow-cli/package.d}/@ericedouard+vad-node-realtime+0.2.0.patch +0 -0
- /package/{src → speechflow-cli/src}/lib.d.ts +0 -0
- /package/{src → speechflow-cli/src}/speechflow-logo.ai +0 -0
- /package/{src → speechflow-cli/src}/speechflow-logo.svg +0 -0
- /package/{src → speechflow-cli/src}/speechflow-node-a2a-ffmpeg.ts +0 -0
- /package/{tsconfig.json → speechflow-cli/tsconfig.json} +0 -0
|
@@ -11,6 +11,7 @@ import Stream from "node:stream"
|
|
|
11
11
|
import { EventEmitter } from "node:events"
|
|
12
12
|
import http from "node:http"
|
|
13
13
|
import * as HAPI from "@hapi/hapi"
|
|
14
|
+
import Inert from "@hapi/inert"
|
|
14
15
|
import WebSocket from "ws"
|
|
15
16
|
import HAPIWebSocket from "hapi-plugin-websocket"
|
|
16
17
|
import HAPIHeader from "hapi-plugin-header"
|
|
@@ -32,7 +33,7 @@ import chalk from "chalk"
|
|
|
32
33
|
|
|
33
34
|
/* internal dependencies */
|
|
34
35
|
import SpeechFlowNode from "./speechflow-node"
|
|
35
|
-
import pkg from "
|
|
36
|
+
import pkg from "../../package.json"
|
|
36
37
|
|
|
37
38
|
/* central CLI context */
|
|
38
39
|
let cli: CLIio | null = null
|
|
@@ -67,6 +68,7 @@ type wsPeerInfo = {
|
|
|
67
68
|
"[-a|--address <ip-address>] " +
|
|
68
69
|
"[-p|--port <tcp-port>] " +
|
|
69
70
|
"[-C|--cache <directory>] " +
|
|
71
|
+
"[-d|--dashboard <type>:<id>:<name>[,...]] " +
|
|
70
72
|
"[-e|--expression <expression>] " +
|
|
71
73
|
"[-f|--file <file>] " +
|
|
72
74
|
"[-c|--config <id>@<yaml-config-file>] " +
|
|
@@ -125,6 +127,15 @@ type wsPeerInfo = {
|
|
|
125
127
|
default: path.join(dataDir, "cache"),
|
|
126
128
|
describe: "directory for cached files (primarily AI model files)"
|
|
127
129
|
})
|
|
130
|
+
.option("d", {
|
|
131
|
+
alias: "dashboard",
|
|
132
|
+
type: "string",
|
|
133
|
+
array: false,
|
|
134
|
+
coerce,
|
|
135
|
+
nargs: 1,
|
|
136
|
+
default: "",
|
|
137
|
+
describe: "list of dashboard block types and names"
|
|
138
|
+
})
|
|
128
139
|
.option("e", {
|
|
129
140
|
alias: "expression",
|
|
130
141
|
type: "string",
|
|
@@ -176,6 +187,21 @@ type wsPeerInfo = {
|
|
|
176
187
|
logPrefix: pkg.name
|
|
177
188
|
})
|
|
178
189
|
|
|
190
|
+
/* catch uncaught exceptions */
|
|
191
|
+
process.on("uncaughtException", (err) => {
|
|
192
|
+
cli!.log("error", `uncaught exception: ${err.message}`)
|
|
193
|
+
process.exit(1)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
/* catch unhandled promise rejections */
|
|
197
|
+
process.on("unhandledRejection", (reason) => {
|
|
198
|
+
if (reason instanceof Error)
|
|
199
|
+
cli!.log("error", `unhandled rejection: ${reason.message}`)
|
|
200
|
+
else
|
|
201
|
+
cli!.log("error", `unhandled rejection: ${reason}`)
|
|
202
|
+
process.exit(1)
|
|
203
|
+
})
|
|
204
|
+
|
|
179
205
|
/* provide startup information */
|
|
180
206
|
cli.log("info", `starting SpeechFlow ${pkg["x-stdver"]} (${pkg["x-release"]})`)
|
|
181
207
|
|
|
@@ -185,21 +211,6 @@ type wsPeerInfo = {
|
|
|
185
211
|
for (const key of Object.keys(result.parsed))
|
|
186
212
|
cli.log("info", `loaded environment variable "${key}" from ".env" files`)
|
|
187
213
|
|
|
188
|
-
/* handle uncaught exceptions */
|
|
189
|
-
process.on("uncaughtException", async (err: Error) => {
|
|
190
|
-
cli!.log("warning", `process crashed with a fatal error: ${err} ${err.stack}`)
|
|
191
|
-
process.exit(1)
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
/* handle unhandled promise rejections */
|
|
195
|
-
process.on("unhandledRejection", async (reason, promise) => {
|
|
196
|
-
if (reason instanceof Error)
|
|
197
|
-
cli!.log("error", `promise rejection not handled: ${reason.message}: ${reason.stack}`)
|
|
198
|
-
else
|
|
199
|
-
cli!.log("error", `promise rejection not handled: ${reason}`)
|
|
200
|
-
process.exit(1)
|
|
201
|
-
})
|
|
202
|
-
|
|
203
214
|
/* sanity check usage */
|
|
204
215
|
let n = 0
|
|
205
216
|
if (typeof args.e === "string" && args.e !== "") n++
|
|
@@ -220,7 +231,16 @@ type wsPeerInfo = {
|
|
|
220
231
|
throw new Error("invalid configuration file specification (expected \"<id>@<yaml-config-file>\")")
|
|
221
232
|
const [ , id, file ] = m
|
|
222
233
|
const yaml = await cli.input(file, { encoding: "utf8" })
|
|
223
|
-
|
|
234
|
+
let obj: any
|
|
235
|
+
try {
|
|
236
|
+
obj = jsYAML.load(yaml)
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
if (err instanceof Error)
|
|
240
|
+
throw new Error(`failed to parse YAML configuration: ${err.message}`)
|
|
241
|
+
else
|
|
242
|
+
throw new Error(`failed to parse YAML configuration: ${err}`)
|
|
243
|
+
}
|
|
224
244
|
if (obj[id] === undefined)
|
|
225
245
|
throw new Error(`no such id "${id}" found in configuration file`)
|
|
226
246
|
config = obj[id] as string
|
|
@@ -308,7 +328,14 @@ type wsPeerInfo = {
|
|
|
308
328
|
for (const name of Object.keys(nodes)) {
|
|
309
329
|
cli!.log("info", `gathering status of node <${name}>`)
|
|
310
330
|
const node = new nodes[name](name, cfg, {}, [])
|
|
311
|
-
const status = await
|
|
331
|
+
const status = await Promise.race<{ [ key: string ]: string | number }>([
|
|
332
|
+
node.status(),
|
|
333
|
+
new Promise<never>((resolve, reject) => setTimeout(() =>
|
|
334
|
+
reject(new Error("timeout")), 10 * 1000))
|
|
335
|
+
]).catch((err: Error) => {
|
|
336
|
+
cli!.log("warning", `[${node.id}]: failed to gather status of node <${node.id}>: ${err.message}`)
|
|
337
|
+
return {} as { [ key: string ]: string | number }
|
|
338
|
+
})
|
|
312
339
|
if (Object.keys(status).length > 0) {
|
|
313
340
|
let first = true
|
|
314
341
|
for (const key of Object.keys(status)) {
|
|
@@ -336,12 +363,9 @@ type wsPeerInfo = {
|
|
|
336
363
|
ast = flowlink.compile(config)
|
|
337
364
|
}
|
|
338
365
|
catch (err) {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
cli!.log("error", `failed to parse SpeechFlow configuration: ${err.message}"`)
|
|
343
|
-
else
|
|
344
|
-
cli!.log("error", "failed to parse SpeechFlow configuration: internal error")
|
|
366
|
+
const errorMsg = err instanceof Error && err.name === "FlowLinkError"
|
|
367
|
+
? err.toString() : (err instanceof Error ? err.message : "internal error")
|
|
368
|
+
cli!.log("error", `failed to parse SpeechFlow configuration: ${errorMsg}`)
|
|
345
369
|
process.exit(1)
|
|
346
370
|
}
|
|
347
371
|
try {
|
|
@@ -358,10 +382,11 @@ type wsPeerInfo = {
|
|
|
358
382
|
throw new Error(`unknown node <${id}>`)
|
|
359
383
|
let node: SpeechFlowNode
|
|
360
384
|
try {
|
|
361
|
-
|
|
362
|
-
nodeNums.
|
|
385
|
+
const NodeClass = nodes[id]
|
|
386
|
+
let num = nodeNums.get(NodeClass) ?? 0
|
|
387
|
+
nodeNums.set(NodeClass, ++num)
|
|
363
388
|
const name = num === 1 ? id : `${id}:${num}`
|
|
364
|
-
node = new
|
|
389
|
+
node = new NodeClass(name, cfg, opts, args)
|
|
365
390
|
}
|
|
366
391
|
catch (err) {
|
|
367
392
|
/* fatal error */
|
|
@@ -384,12 +409,9 @@ type wsPeerInfo = {
|
|
|
384
409
|
})
|
|
385
410
|
}
|
|
386
411
|
catch (err) {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
cli!.log("error", `failed to materialize SpeechFlow configuration: ${err.message}`)
|
|
391
|
-
else
|
|
392
|
-
cli!.log("error", "failed to materialize SpeechFlow configuration: internal error")
|
|
412
|
+
const errorMsg = err instanceof Error && err.name === "FlowLinkError"
|
|
413
|
+
? err.toString() : (err instanceof Error ? err.message : "internal error")
|
|
414
|
+
cli!.log("error", `failed to materialize SpeechFlow configuration: ${errorMsg}`)
|
|
393
415
|
process.exit(1)
|
|
394
416
|
}
|
|
395
417
|
|
|
@@ -438,9 +460,13 @@ type wsPeerInfo = {
|
|
|
438
460
|
/* open node */
|
|
439
461
|
cli!.log("info", `open node <${node.id}>`)
|
|
440
462
|
node.setTimeZero(timeZero)
|
|
441
|
-
await
|
|
442
|
-
|
|
443
|
-
|
|
463
|
+
await Promise.race<void>([
|
|
464
|
+
node.open(),
|
|
465
|
+
new Promise<never>((resolve, reject) => setTimeout(() =>
|
|
466
|
+
reject(new Error("timeout")), 10 * 1000))
|
|
467
|
+
]).catch((err: Error) => {
|
|
468
|
+
cli!.log("error", `[${node.id}]: failed to open node <${node.id}>: ${err.message}`)
|
|
469
|
+
throw new Error(`failed to open node <${node.id}>: ${err.message}`)
|
|
444
470
|
})
|
|
445
471
|
}
|
|
446
472
|
|
|
@@ -462,9 +488,10 @@ type wsPeerInfo = {
|
|
|
462
488
|
}
|
|
463
489
|
}
|
|
464
490
|
|
|
465
|
-
/* graph processing: PASS
|
|
491
|
+
/* graph processing: PASS 5: track stream finishing */
|
|
466
492
|
const activeNodes = new Set<SpeechFlowNode>()
|
|
467
493
|
const finishEvents = new EventEmitter()
|
|
494
|
+
finishEvents.setMaxListeners(graphNodes.size + 10)
|
|
468
495
|
for (const node of graphNodes) {
|
|
469
496
|
if (node.stream === null)
|
|
470
497
|
throw new Error(`stream of node <${node.id}> still not initialized`)
|
|
@@ -512,9 +539,12 @@ type wsPeerInfo = {
|
|
|
512
539
|
throw new Error(`external request failed: no such node <${name}>`)
|
|
513
540
|
}
|
|
514
541
|
else {
|
|
515
|
-
await
|
|
516
|
-
|
|
517
|
-
|
|
542
|
+
await Promise.race<void>([
|
|
543
|
+
foundNode.receiveRequest(args),
|
|
544
|
+
new Promise<never>((resolve, reject) => setTimeout(() =>
|
|
545
|
+
reject(new Error("timeout")), 10 * 1000))
|
|
546
|
+
]).catch((err: Error) => {
|
|
547
|
+
cli!.log("warning", `external request to node <${name}> failed: ${err.message}`)
|
|
518
548
|
})
|
|
519
549
|
}
|
|
520
550
|
}
|
|
@@ -525,6 +555,7 @@ type wsPeerInfo = {
|
|
|
525
555
|
address: args.a,
|
|
526
556
|
port: args.p
|
|
527
557
|
})
|
|
558
|
+
await hapi.register({ plugin: Inert })
|
|
528
559
|
await hapi.register({ plugin: HAPIHeader, options: { Server: `${pkg.name}/${pkg.version}` } })
|
|
529
560
|
await hapi.register({ plugin: HAPIWebSocket })
|
|
530
561
|
hapi.events.on("response", (request: HAPI.Request) => {
|
|
@@ -558,6 +589,29 @@ type wsPeerInfo = {
|
|
|
558
589
|
cli!.log("error", `HAPI: log: ${err}`)
|
|
559
590
|
}
|
|
560
591
|
})
|
|
592
|
+
hapi.route({
|
|
593
|
+
method: "GET",
|
|
594
|
+
path: "/{param*}",
|
|
595
|
+
handler: {
|
|
596
|
+
directory: {
|
|
597
|
+
path: path.join(__dirname, "../../speechflow-ui-db/dst"),
|
|
598
|
+
redirectToSlash: true,
|
|
599
|
+
index: true
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
})
|
|
603
|
+
hapi.route({
|
|
604
|
+
method: "GET",
|
|
605
|
+
path: "/api/dashboard",
|
|
606
|
+
handler: (request: HAPI.Request, h: HAPI.ResponseToolkit) => {
|
|
607
|
+
const config = []
|
|
608
|
+
for (const block of args.d.split(",")) {
|
|
609
|
+
const [ type, id, name ] = block.split(":")
|
|
610
|
+
config.push({ type, id, name })
|
|
611
|
+
}
|
|
612
|
+
return h.response(config).code(200)
|
|
613
|
+
}
|
|
614
|
+
})
|
|
561
615
|
hapi.route({
|
|
562
616
|
method: "GET",
|
|
563
617
|
path: "/api/{req}/{node}/{params*}",
|
|
@@ -565,10 +619,13 @@ type wsPeerInfo = {
|
|
|
565
619
|
},
|
|
566
620
|
handler: (request: HAPI.Request, h: HAPI.ResponseToolkit) => {
|
|
567
621
|
const peer = request.info.remoteAddress
|
|
622
|
+
const params = request.params.params as string ?? ""
|
|
623
|
+
if (params.length > 1000)
|
|
624
|
+
return h.response({ response: "ERROR", data: "parameters too long" }).code(400)
|
|
568
625
|
const req = {
|
|
569
626
|
request: request.params.req,
|
|
570
627
|
node: request.params.node,
|
|
571
|
-
args:
|
|
628
|
+
args: params.split("/").filter((seg) => seg !== "")
|
|
572
629
|
}
|
|
573
630
|
cli!.log("info", `HAPI: peer ${peer}: GET: ${JSON.stringify(req)}`)
|
|
574
631
|
return consumeExternalRequest(req).then(() => {
|
|
@@ -583,9 +640,10 @@ type wsPeerInfo = {
|
|
|
583
640
|
path: "/api",
|
|
584
641
|
options: {
|
|
585
642
|
payload: {
|
|
586
|
-
output:
|
|
587
|
-
parse:
|
|
588
|
-
allow:
|
|
643
|
+
output: "data",
|
|
644
|
+
parse: true,
|
|
645
|
+
allow: "application/json",
|
|
646
|
+
maxBytes: 1 * 1024 * 1024
|
|
589
647
|
},
|
|
590
648
|
plugins: {
|
|
591
649
|
websocket: {
|
|
@@ -601,8 +659,10 @@ type wsPeerInfo = {
|
|
|
601
659
|
},
|
|
602
660
|
disconnect: (args: any) => {
|
|
603
661
|
const ctx: wsPeerCtx = args.ctx
|
|
662
|
+
const ws: WebSocket = args.ws
|
|
604
663
|
const peer = ctx.peer
|
|
605
664
|
wsPeers.delete(peer)
|
|
665
|
+
ws.removeAllListeners()
|
|
606
666
|
cli!.log("info", `HAPI: WebSocket: disconnect: peer ${peer}`)
|
|
607
667
|
}
|
|
608
668
|
}
|
|
@@ -623,14 +683,35 @@ type wsPeerInfo = {
|
|
|
623
683
|
}
|
|
624
684
|
})
|
|
625
685
|
await hapi.start()
|
|
626
|
-
cli!.log("info", `HAPI: started REST/WebSocket network service: http://${args.
|
|
686
|
+
cli!.log("info", `HAPI: started REST/WebSocket network service: http://${args.a}:${args.p}`)
|
|
627
687
|
|
|
628
688
|
/* hook for sendResponse method of nodes */
|
|
629
689
|
for (const node of graphNodes) {
|
|
630
690
|
node.on("send-response", (args: any[]) => {
|
|
631
691
|
const data = JSON.stringify({ response: "NOTIFY", node: node.id, args })
|
|
632
692
|
for (const [ peer, info ] of wsPeers.entries()) {
|
|
633
|
-
cli!.log("
|
|
693
|
+
cli!.log("debug", `HAPI: remote peer ${peer}: sending ${data}`)
|
|
694
|
+
if (info.ws.readyState === WebSocket.OPEN)
|
|
695
|
+
info.ws.send(data)
|
|
696
|
+
}
|
|
697
|
+
})
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/* hook for dashboardInfo method of nodes */
|
|
701
|
+
for (const node of graphNodes) {
|
|
702
|
+
node.on("dashboard-info", (info: {
|
|
703
|
+
type: string,
|
|
704
|
+
id: string,
|
|
705
|
+
kind: "final" | "intermediate",
|
|
706
|
+
value: string | number
|
|
707
|
+
}) => {
|
|
708
|
+
const data = JSON.stringify({
|
|
709
|
+
response: "DASHBOARD",
|
|
710
|
+
node: "",
|
|
711
|
+
args: [ info.type, info.id, info.kind, info.value ]
|
|
712
|
+
})
|
|
713
|
+
for (const [ peer, info ] of wsPeers.entries()) {
|
|
714
|
+
cli!.log("debug", `HAPI: dashboard peer ${peer}: send ${data}`)
|
|
634
715
|
info.ws.send(data)
|
|
635
716
|
}
|
|
636
717
|
})
|
|
@@ -653,9 +734,39 @@ type wsPeerInfo = {
|
|
|
653
734
|
cli!.log("warning", `**** received signal ${signal} -- shutting down service ****`)
|
|
654
735
|
|
|
655
736
|
/* shutdown HAPI service */
|
|
656
|
-
cli!.log("info", `HAPI: stopping REST/WebSocket network service: http://${args.
|
|
737
|
+
cli!.log("info", `HAPI: stopping REST/WebSocket network service: http://${args.a}:${args.p}`)
|
|
657
738
|
await hapi.stop({ timeout: 2000 })
|
|
658
739
|
|
|
740
|
+
/* clear WebSocket connections */
|
|
741
|
+
if (wsPeers.size > 0) {
|
|
742
|
+
cli!.log("info", "HAPI: closing WebSocket connections")
|
|
743
|
+
const closePromises: Promise<void>[] = []
|
|
744
|
+
for (const [ peer, info ] of wsPeers.entries()) {
|
|
745
|
+
closePromises.push(new Promise<void>((resolve, reject) => {
|
|
746
|
+
if (info.ws.readyState !== WebSocket.OPEN)
|
|
747
|
+
resolve()
|
|
748
|
+
else {
|
|
749
|
+
const timeout = setTimeout(() => {
|
|
750
|
+
reject(new Error(`timeout for peer ${peer}`))
|
|
751
|
+
}, 2 * 1000)
|
|
752
|
+
info.ws.once("close", () => {
|
|
753
|
+
clearTimeout(timeout)
|
|
754
|
+
resolve()
|
|
755
|
+
})
|
|
756
|
+
info.ws.close()
|
|
757
|
+
}
|
|
758
|
+
}))
|
|
759
|
+
}
|
|
760
|
+
await Promise.race([
|
|
761
|
+
Promise.all(closePromises),
|
|
762
|
+
new Promise((resolve, reject) =>
|
|
763
|
+
setTimeout(() => reject(new Error("timeout for all peers")), 5 * 1000))
|
|
764
|
+
]).catch((err) => {
|
|
765
|
+
cli!.log("warning", `HAPI: WebSockets failed to close: ${err}`)
|
|
766
|
+
})
|
|
767
|
+
wsPeers.clear()
|
|
768
|
+
}
|
|
769
|
+
|
|
659
770
|
/* graph processing: PASS 1: disconnect node streams */
|
|
660
771
|
for (const node of graphNodes) {
|
|
661
772
|
if (node.stream === null) {
|
|
@@ -685,8 +796,12 @@ type wsPeerInfo = {
|
|
|
685
796
|
/* graph processing: PASS 2: close nodes */
|
|
686
797
|
for (const node of graphNodes) {
|
|
687
798
|
cli!.log("info", `close node <${node.id}>`)
|
|
688
|
-
await
|
|
689
|
-
|
|
799
|
+
await Promise.race<void>([
|
|
800
|
+
node.close(),
|
|
801
|
+
new Promise<never>((resolve, reject) => setTimeout(() =>
|
|
802
|
+
reject(new Error("timeout")), 10 * 1000))
|
|
803
|
+
]).catch((err: Error) => {
|
|
804
|
+
cli!.log("warning", `node <${node.id}> failed to close: ${err.message}`)
|
|
690
805
|
})
|
|
691
806
|
}
|
|
692
807
|
|
|
@@ -705,6 +820,12 @@ type wsPeerInfo = {
|
|
|
705
820
|
graphNodes.delete(node)
|
|
706
821
|
}
|
|
707
822
|
|
|
823
|
+
/* clear event emitters */
|
|
824
|
+
finishEvents.removeAllListeners()
|
|
825
|
+
|
|
826
|
+
/* clear active nodes */
|
|
827
|
+
activeNodes.clear()
|
|
828
|
+
|
|
708
829
|
/* terminate process */
|
|
709
830
|
if (signal === "finished") {
|
|
710
831
|
cli!.log("info", "terminate process (exit code 0)")
|
|
@@ -715,24 +836,38 @@ type wsPeerInfo = {
|
|
|
715
836
|
process.exit(1)
|
|
716
837
|
}
|
|
717
838
|
}
|
|
839
|
+
|
|
840
|
+
/* hook into regular finish */
|
|
718
841
|
finishEvents.on("finished", () => { shutdown("finished") })
|
|
842
|
+
|
|
843
|
+
/* hook into process signals */
|
|
719
844
|
process.on("SIGINT", () => { shutdown("SIGINT") })
|
|
720
845
|
process.on("SIGUSR1", () => { shutdown("SIGUSR1") })
|
|
721
846
|
process.on("SIGUSR2", () => { shutdown("SIGUSR2") })
|
|
722
847
|
process.on("SIGTERM", () => { shutdown("SIGTERM") })
|
|
848
|
+
|
|
849
|
+
/* re-hook into uncaught exception handler */
|
|
850
|
+
process.removeAllListeners("uncaughtException")
|
|
723
851
|
process.on("uncaughtException", (err) => {
|
|
724
|
-
cli!.log("error", `uncaught exception: ${err}`)
|
|
852
|
+
cli!.log("error", `uncaught exception: ${err.message}`)
|
|
725
853
|
shutdown("exception")
|
|
726
854
|
})
|
|
855
|
+
|
|
856
|
+
/* re-hook into unhandled promise rejection handler */
|
|
857
|
+
process.removeAllListeners("unhandledRejection")
|
|
727
858
|
process.on("unhandledRejection", (reason) => {
|
|
728
|
-
|
|
859
|
+
if (reason instanceof Error)
|
|
860
|
+
cli!.log("error", `unhandled rejection: ${reason.message}`)
|
|
861
|
+
else
|
|
862
|
+
cli!.log("error", `unhandled rejection: ${reason}`)
|
|
729
863
|
shutdown("exception")
|
|
730
864
|
})
|
|
731
865
|
})().catch((err: Error) => {
|
|
866
|
+
/* top-level exception handling */
|
|
732
867
|
if (cli !== null)
|
|
733
|
-
cli.log("error", err.message)
|
|
868
|
+
cli.log("error", `${err.message}:\n${err.stack}`)
|
|
734
869
|
else
|
|
735
|
-
process.stderr.write(`${pkg.name}: ${chalk.red("ERROR")}: ${err.message}
|
|
870
|
+
process.stderr.write(`${pkg.name}: ${chalk.red("ERROR")}: ${err.message}\n${err.stack}\n`)
|
|
736
871
|
process.exit(1)
|
|
737
872
|
})
|
|
738
873
|
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** SpeechFlow - Speech Processing Flow Graph
|
|
3
|
+
** Copyright (c) 2024-2025 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import pluginJs from "@eslint/js"
|
|
8
|
+
import pluginStd from "neostandard"
|
|
9
|
+
import pluginN from "eslint-plugin-n"
|
|
10
|
+
import pluginImport from "eslint-plugin-import"
|
|
11
|
+
import pluginPromise from "eslint-plugin-promise"
|
|
12
|
+
import pluginVue from "eslint-plugin-vue"
|
|
13
|
+
import pluginTS from "typescript-eslint"
|
|
14
|
+
import globals from "globals"
|
|
15
|
+
import parserTS from "@typescript-eslint/parser"
|
|
16
|
+
import parserVue from "vue-eslint-parser"
|
|
17
|
+
|
|
18
|
+
export default [
|
|
19
|
+
pluginJs.configs.recommended,
|
|
20
|
+
...pluginTS.configs.strict,
|
|
21
|
+
...pluginTS.configs.stylistic,
|
|
22
|
+
...pluginStd({
|
|
23
|
+
ignores: pluginStd.resolveIgnoresFromGitignore()
|
|
24
|
+
}),
|
|
25
|
+
...pluginVue.configs["flat/recommended"],
|
|
26
|
+
{
|
|
27
|
+
plugins: {
|
|
28
|
+
"n": pluginN,
|
|
29
|
+
"import": pluginImport,
|
|
30
|
+
"promise": pluginPromise
|
|
31
|
+
},
|
|
32
|
+
files: [ "**/*.{vue,ts}" ],
|
|
33
|
+
ignores: [ "dst/" ],
|
|
34
|
+
languageOptions: {
|
|
35
|
+
ecmaVersion: 2022,
|
|
36
|
+
sourceType: "module",
|
|
37
|
+
parser: parserVue,
|
|
38
|
+
parserOptions: {
|
|
39
|
+
parser: parserTS,
|
|
40
|
+
extraFileExtensions: [ ".vue" ],
|
|
41
|
+
ecmaFeatures: {
|
|
42
|
+
jsx: false
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
globals: {
|
|
46
|
+
...globals.browser,
|
|
47
|
+
...globals.node,
|
|
48
|
+
...globals.commonjs
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
rules: {
|
|
52
|
+
"curly": "off",
|
|
53
|
+
"require-atomic-updates": "off",
|
|
54
|
+
"dot-notation": "off",
|
|
55
|
+
"no-labels": "off",
|
|
56
|
+
"no-useless-constructor": "off",
|
|
57
|
+
|
|
58
|
+
"@stylistic/indent": [ "error", 4, { SwitchCase: 1 } ],
|
|
59
|
+
"@stylistic/linebreak-style": [ "error", "unix" ],
|
|
60
|
+
"@stylistic/semi": [ "error", "never" ],
|
|
61
|
+
"@stylistic/operator-linebreak": [ "error", "after", { overrides: { "&&": "before", "||": "before", ":": "after" } } ],
|
|
62
|
+
"@stylistic/brace-style": [ "error", "stroustrup", { allowSingleLine: true } ],
|
|
63
|
+
"@stylistic/quotes": [ "error", "double" ],
|
|
64
|
+
|
|
65
|
+
"@stylistic/no-multi-spaces": "off",
|
|
66
|
+
"@stylistic/no-multi-spaces": "off",
|
|
67
|
+
"@stylistic/no-multiple-empty-lines": "off",
|
|
68
|
+
"@stylistic/key-spacing": "off",
|
|
69
|
+
"@stylistic/object-property-newline": "off",
|
|
70
|
+
"@stylistic/space-in-parens": "off",
|
|
71
|
+
"@stylistic/array-bracket-spacing": "off",
|
|
72
|
+
"@stylistic/lines-between-class-members": "off",
|
|
73
|
+
"@stylistic/multiline-ternary": "off",
|
|
74
|
+
"@stylistic/quote-props": "off",
|
|
75
|
+
|
|
76
|
+
"vue/html-indent": "off",
|
|
77
|
+
"vue/v-bind-style": [ "error", "longform" ],
|
|
78
|
+
"vue/max-attributes-per-line": "off",
|
|
79
|
+
"vue/html-self-closing": "off",
|
|
80
|
+
"vue/no-multi-spaces": "off",
|
|
81
|
+
"vue/html-closing-bracket-newline": "off",
|
|
82
|
+
"vue/html-closing-bracket-spacing": "off",
|
|
83
|
+
"vue/singleline-html-element-content-newline": "off",
|
|
84
|
+
"vue/no-v-html": "off",
|
|
85
|
+
"vue/v-on-style": "off",
|
|
86
|
+
"vue/component-tags-order": "off",
|
|
87
|
+
"vue/first-attribute-linebreak": "off",
|
|
88
|
+
"vue/attributes-order": "off",
|
|
89
|
+
"vue/component-definition-name-casing": "off",
|
|
90
|
+
"vue/block-order": "off",
|
|
91
|
+
|
|
92
|
+
"@typescript-eslint/no-empty-function": "off",
|
|
93
|
+
"@typescript-eslint/no-explicit-any": "off",
|
|
94
|
+
"@typescript-eslint/no-unused-vars": "off",
|
|
95
|
+
"@typescript-eslint/ban-ts-comment": "off",
|
|
96
|
+
"@typescript-eslint/no-this-alias": "off",
|
|
97
|
+
"@typescript-eslint/no-non-null-assertion": "off",
|
|
98
|
+
"@typescript-eslint/consistent-type-definitions": "off",
|
|
99
|
+
"@typescript-eslint/array-type": "off",
|
|
100
|
+
"@typescript-eslint/no-extraneous-class": "off",
|
|
101
|
+
"@typescript-eslint/consistent-indexed-object-style": "off",
|
|
102
|
+
"@typescript-eslint/adjacent-overload-signatures": "off"
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"plugins": [],
|
|
3
|
+
"maxerr": false,
|
|
4
|
+
"raw-ignore-regex": false,
|
|
5
|
+
"attr-bans": [ "align", "background", "bgcolor", "border", "frameborder", "longdesc", "marginwidth", "marginheight", "scrolling", "style", "width" ],
|
|
6
|
+
"indent-delta": false,
|
|
7
|
+
"indent-style": "nonmixed",
|
|
8
|
+
"indent-width": 4,
|
|
9
|
+
"indent-width-cont": false,
|
|
10
|
+
"spec-char-escape": true,
|
|
11
|
+
"text-ignore-regex": false,
|
|
12
|
+
"tag-bans": [ "b", "i" ],
|
|
13
|
+
"tag-close": true,
|
|
14
|
+
"tag-name-lowercase": true,
|
|
15
|
+
"tag-name-match": true,
|
|
16
|
+
"tag-self-close": false,
|
|
17
|
+
"doctype-first": false,
|
|
18
|
+
"doctype-html5": false,
|
|
19
|
+
"attr-name-style": "dash",
|
|
20
|
+
"attr-name-ignore-regex": false,
|
|
21
|
+
"attr-no-dup": true,
|
|
22
|
+
"attr-no-unsafe-char": true,
|
|
23
|
+
"attr-order": false,
|
|
24
|
+
"attr-quote-style": "double",
|
|
25
|
+
"attr-req-value": true,
|
|
26
|
+
"attr-new-line": false,
|
|
27
|
+
"attr-validate": true,
|
|
28
|
+
"id-no-dup": true,
|
|
29
|
+
"id-class-no-ad": true,
|
|
30
|
+
"id-class-style": "underscore",
|
|
31
|
+
"class-no-dup": true,
|
|
32
|
+
"class-style": false,
|
|
33
|
+
"id-class-ignore-regex": false,
|
|
34
|
+
"img-req-alt": true,
|
|
35
|
+
"img-req-src": true,
|
|
36
|
+
"html-valid-content-model": true,
|
|
37
|
+
"head-valid-content-model": true,
|
|
38
|
+
"href-style": false,
|
|
39
|
+
"label-req-for": true,
|
|
40
|
+
"line-end-style": "lf",
|
|
41
|
+
"line-no-trailing-whitespace": false,
|
|
42
|
+
"line-max-len": false,
|
|
43
|
+
"line-max-len-ignore-regex": false,
|
|
44
|
+
"head-req-title": true,
|
|
45
|
+
"title-no-dup": true,
|
|
46
|
+
"title-max-len": 60,
|
|
47
|
+
"html-req-lang": false,
|
|
48
|
+
"lang-style": "case",
|
|
49
|
+
"fig-req-figcaption": false,
|
|
50
|
+
"focusable-tabindex-style": false,
|
|
51
|
+
"input-radio-req-name": true,
|
|
52
|
+
"input-req-label": false,
|
|
53
|
+
"table-req-caption": false,
|
|
54
|
+
"table-req-header": false
|
|
55
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
##
|
|
2
|
+
## SpeechFlow - Speech Processing Flow Graph
|
|
3
|
+
## Copyright (c) 2024-2025 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
## Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
|
+
##
|
|
6
|
+
|
|
7
|
+
# (internal): create patch set for NPM dependencies
|
|
8
|
+
patch-make
|
|
9
|
+
npm shrinkwrap && \
|
|
10
|
+
patch-package --patch-dir package.d \
|
|
11
|
+
"@typescript-eslint/typescript-estree" \
|
|
12
|
+
htmllint-cli && \
|
|
13
|
+
shx rm -f npm-shrinkwrap.json
|
|
14
|
+
|
|
15
|
+
# (internal): apply patch set for NPM dependencies
|
|
16
|
+
patch-apply
|
|
17
|
+
patch-package --patch-dir package.d
|
|
18
|
+
|
|
19
|
+
# static code analysis (continuous file watching)
|
|
20
|
+
lint-watch
|
|
21
|
+
nodemon --exec "npm start lint" --watch src --ext html,styl,js,ts,vue
|
|
22
|
+
|
|
23
|
+
# static code analysis
|
|
24
|
+
lint : lint-vue-tsc lint-eslint lint-stylelint lint-htmllint
|
|
25
|
+
|
|
26
|
+
# static code analysis (Vue language)
|
|
27
|
+
lint-vue-tsc
|
|
28
|
+
vue-tsc --project etc/tsc-client.json --noEmit
|
|
29
|
+
|
|
30
|
+
# static code analysis (TypeScript language)
|
|
31
|
+
lint-eslint
|
|
32
|
+
eslint --config etc/eslint.mjs src/**/*.vue src/**/*.ts
|
|
33
|
+
|
|
34
|
+
# static code analysis (CSS/Stylus languages)
|
|
35
|
+
lint-stylelint
|
|
36
|
+
stylelint --config etc/stylelint.yaml src/**/*.styl src/**/*.vue
|
|
37
|
+
|
|
38
|
+
# static code analysis (HTML language)
|
|
39
|
+
lint-htmllint
|
|
40
|
+
htmllint --rc etc/htmllint.json src/**/*.html
|
|
41
|
+
|
|
42
|
+
# build components for production
|
|
43
|
+
build
|
|
44
|
+
npm start build-client
|
|
45
|
+
|
|
46
|
+
# build components for development
|
|
47
|
+
build-dev
|
|
48
|
+
npm start build-client-dev
|
|
49
|
+
|
|
50
|
+
# build client components for production
|
|
51
|
+
build-client
|
|
52
|
+
vite --config etc/vite-client.mts build --mode production
|
|
53
|
+
|
|
54
|
+
# build client components for development
|
|
55
|
+
build-client-dev
|
|
56
|
+
vite --config etc/vite-client.mts build --mode development
|
|
57
|
+
|
|
58
|
+
# build client components for development (continuous file watching)
|
|
59
|
+
build-client-dev-watch
|
|
60
|
+
cross-env NODE_OPTIONS="--max-old-space-size=4096" \
|
|
61
|
+
vite --config etc/vite-client.mts build --mode development --watch
|
|
62
|
+
|
|
63
|
+
# run server
|
|
64
|
+
server
|
|
65
|
+
serve -d --listen 12345 --single dst
|
|
66
|
+
|
|
67
|
+
# remove all development-only NPM dependencies
|
|
68
|
+
prune
|
|
69
|
+
npm prune --omit=dev
|
|
70
|
+
|
|
71
|
+
# remove all generated artifacts (reverse of "npm start build")
|
|
72
|
+
clean
|
|
73
|
+
shx rm -rf dst-stage1 dst-stage2
|
|
74
|
+
|
|
75
|
+
# remove all generated artifacts (reverse of "npm install" and "npm start build")
|
|
76
|
+
clean:dist : clean
|
|
77
|
+
shx rm -f package-lock.json && \
|
|
78
|
+
shx rm -rf node_modules
|
|
79
|
+
|