@lowlighter/run 0.2.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,14 @@
1
+ # ⏯️ Run subprocesses
2
+
3
+ [![JSR](https://jsr.io/badges/@libs/run)](https://jsr.io/@libs/run) [![JSR Score](https://jsr.io/badges/@libs/run/score)](https://jsr.io/@libs/run)
4
+ [![NPM](https://img.shields.io/npm/v/@lowlighter%2Frun?logo=npm&labelColor=cb0000&color=183e4e)](https://www.npmjs.com/package/@lowlighter/run) [![Coverage](https://libs-coverage.lecoq.io/run/badge.svg)](https://libs-coverage.lecoq.io/run)
5
+
6
+ - [`🦕 Playground`](https://libs.lecoq.io/run)
7
+ - [`📚 Documentation`](https://jsr.io/@libs/run/doc)
8
+
9
+ ## 📜 Licenses
10
+
11
+ ```
12
+ Copyright (c) Lecoq Simon <@lowlighter>. (MIT License)
13
+ https://github.com/lowlighter/libs/blob/main/LICENSE
14
+ ```
package/command.mjs ADDED
@@ -0,0 +1,33 @@
1
+ var e=class e{constructor({level:t,format:n,output:o,tags:r,options:s}={}){if("granted"===globalThis.Deno?.permissions.querySync?.({name:"env",variable:"LOG_LEVEL"}).state){const n=globalThis.Deno?.env.get("LOG_LEVEL")??"";n in e.level&&(t??=e.level[n]),Number.isNaN(Number.parseInt(n))||(t??=Number.parseInt(n))}this.level=t??e.level.log,this.#e=o||null===o?o:console,this.#t=n??e.format.text,this.tags=r??{},this.options={date:!1,time:!1,delta:!0,...s,caller:!1!==s?.caller&&{file:!1,name:!1,line:!1,...s?.caller}}}level;#t;#e;tags;options;error(...t){return this.level>=e.level.error&&this.#e?.error(...this.#t(this,{level:e.level.error,content:t})),this}warn(...t){return this.level>=e.level.warn&&this.#e?.warn(...this.#t(this,{level:e.level.warn,content:t})),this}info(...t){return this.level>=e.level.info&&this.#e?.info(...this.#t(this,{level:e.level.info,content:t})),this}log(...t){return this.level>=e.level.log&&this.#e?.log(...this.#t(this,{level:e.level.log,content:t})),this}debug(...t){return this.level>=e.level.debug&&this.#e?.debug(...this.#t(this,{level:e.level.debug,content:t})),this}with(t={}){return new e({level:this.level,format:this.#t,output:this.#e,options:{...this.options},tags:{...this.tags,...t}})}#n(e=3){const t=Error,n=t.prepareStackTrace;t.prepareStackTrace=(e,t)=>t;const{stack:o}=new Error;t.prepareStackTrace=n;const r=o[e];return{file:r.getFileName(),name:r.getFunctionName(),line:r.getLineNumber(),column:r.getColumnNumber()}}static level=Object.freeze({disabled:NaN,error:0,warn:1,info:2,log:3,debug:4});static format={text(t,{level:n=0,content:o}){const r=["red","orange","cyan","white","gray"][n],s=[`%c ${Object.keys(e.level).find((t=>e.level[t]===n)).toLocaleUpperCase().padEnd(5)} │%c`],i=[`color: black; background-color: ${r}`,""];if(t.options.date||t.options.time||t.options.delta){const e=(new Date).toISOString(),n=[];if(t.options.delta){const e=performance.now()/1e3;let t=e.toPrecision(4);e<1&&(t=e.toPrecision(2)),n.push(`+${t}`)}t.options.date&&t.options.time?n.push(e):t.options.date?n.push(e.slice(0,e.indexOf("T"))):t.options.time&&n.push(e.slice(e.indexOf("T")+1,-1)),s.push(`%c ${n.join(" ¦ ").trim()} %c`),i.push(`color: black; background-color: ${r}`,"")}if(t.options.caller){const e=t.#n();if(e){const n=[];t.options.caller.file&&n.push(`${e.file.replace(t.options.caller.fileformat,"$<file>")}`),t.options.caller.name&&e.name&&n.push(e.name),t.options.caller.line&&n.push(e.line,e.column),s.push(`%c ${n.join(":").trim()} %c`),i.push("color: black; background-color: gray","")}}{const n=[];for(const[o,r]of Object.entries(t.tags))n.push(`${o}:${e.inspect(r)}`);s.push(`%c ${n.join(" ").trim()} %c`),i.push("background-color: black","")}return[s.join(""),...i,...o.map(e.inspect)]},json(t,{level:n=0,content:o}){const r={level:Object.keys(e.level).find((t=>e.level[t]===n)),timestamp:Date.now(),tags:t.tags,content:o},s={};if(t.options.date||t.options.time){const e=new Date(r.timestamp).toISOString();t.options.date&&(s.date=e.slice(0,e.indexOf("T"))),t.options.time&&(s.time=e.slice(e.indexOf("T")+1,-1))}if(t.options.delta&&(s.delta=performance.now()/1e3),t.options.caller){const e=t.#n();e&&(s.caller={},t.options.caller.file&&(s.caller.file=`${e.file.replace(t.options.caller.fileformat,"$<file>")}`),t.options.caller.name&&e.name&&(s.caller.name=e.name),t.options.caller.line&&(s.caller.line=[e.line,e.column]))}return[JSON.stringify({...r,...s})]}};static inspect(e){return globalThis.Deno?.inspect(e,{colors:!0,depth:1/0})??e}},t=class extends Error{constructor(e){super(e),this.name="AssertionError"}};TransformStream;function n(e){let t=0;for(const n of e)t+=n.length;const n=new Uint8Array(t);let o=0;for(const t of e)n.set(t,o),o+=t.length;return n}function o(e){const t=e.length,n=new Uint8Array(t);n[0]=0;let o=0,r=1;for(;r<t;)e[r]===e[o]?(o++,n[r]=o,r++):0===o?(n[r]=0,r++):o=n[o-1];return n}TransformStream,TransformStream,TransformStream,TransformStream;var r=class extends TransformStream{#o="";constructor(e={allowCR:!1}){super({transform:(t,n)=>{for(t=this.#o+t;;){const o=t.indexOf("\n"),r=e.allowCR?t.indexOf("\r"):-1;if(-1!==r&&r!==t.length-1&&(-1===o||o-1>r)){n.enqueue(t.slice(0,r)),t=t.slice(r+1);continue}if(-1===o)break;const s="\r"===t[o-1]?o-1:o;n.enqueue(t.slice(0,s)),t=t.slice(o+1)}this.#o=t},flush:t=>{if(""===this.#o)return;const n=e.allowCR&&this.#o.endsWith("\r")?this.#o.slice(0,-1):this.#o;t.enqueue(n)}})}};new TextDecoder;var s=new TextEncoder,i=new TextDecoder;function l(t,n,{log:o=new e,stdin:l=null,stdout:c="debug",stderr:u="error",env:d,cwd:p,raw:h,callback:f,buffering:m,sync:w,throw:g,winext:b="",os:v=Deno.build.os}={}){"windows"===v&&(t=`${t}${b}`),o=o.with({bin:t}),f&&"piped"!==a(l)&&(l="piped");const T=new Deno.Command(t,{args:n,stdin:w?"null":a(l),stdout:a(c),stderr:a(u),env:d,cwd:p,windowsRawArguments:h});return w?function(e,{bin:t,log:n,throw:o,stdout:r,stderr:s}){const l=Date.now(),c=e.outputSync(),{success:u,code:d}=c,p=Date.now()-l,h={get stdio(){return[[p,1,this.stdout],[p,2,this.stderr]]},stdin:"",stdout:"piped"===a(r)?i.decode(c.stdout):"",stderr:"piped"===a(s)?i.decode(c.stderr):""};for(const{channel:e,mode:t}of[{channel:"stdout",mode:r},{channel:"stderr",mode:s}])"piped"===a(t)&&h[e]&&n.with({t:p,channel:e})[t]?.(h[e]);if(!u&&o)throw new EvalError(`${t} exited with non-zero code ${d}:\n${h.stdout}\n${h.stderr}`);return{success:u,code:d,...h}}(T,{bin:t,log:o,throw:g,stdout:c,stderr:u}):async function(e,{bin:t,log:n,callback:o=(({close:e})=>e?.()),buffering:i=250,throw:l,...c}){const u=e.spawn(),d=Date.now(),p={stdio:[],get stdin(){return this.stdio.filter((([e,t])=>0===t)).map((([e,t,n])=>n)).join("\n")},get stdout(){return this.stdio.filter((([e,t])=>1===t)).map((([e,t,n])=>n)).join("\n")},get stderr(){return this.stdio.filter((([e,t])=>2===t)).map((([e,t,n])=>n)).join("\n")}},h={};let f="";const m=function(e,t){let n=null,o=null;const r=(...s)=>{r.clear(),o=()=>{r.clear(),e.call(r,...s)},n=setTimeout(o,t)};return r.clear=()=>{"number"==typeof n&&(clearTimeout(n),n=null,o=null)},r.flush=()=>{o?.()},Object.defineProperty(r,"pending",{get:()=>"number"==typeof n}),r}((async e=>{n.with({t:e}).debug("debounced"),f="",await o({stdio:p,i:p.stdin.length,...h})}),i);if("piped"===a(c.stdin)){const e=u.stdin.getWriter();Object.assign(h,{async write(t,o=!0){const r=Date.now()-d;c.stdin&&n.with({t:r,channel:"stdin"})[c.stdin]?.(t),p.stdio.push([r,0,t]),o&&!t.endsWith("\n")&&(t+="\n"),await e.write(s.encode(t)),f="stdin",e.releaseLock()},async close(){try{e.releaseLock(),await u.stdin.close(),n.with({t:Date.now()-d,closed:"stdin"}).debug()}catch{}},async wait(e=1e3){const t=Date.now()-d;n.with({t:t,waiting:e}).debug(),await function(e,t={}){const{signal:n,persistent:o=!0}=t;return n?.aborted?Promise.reject(n.reason):new Promise(((t,r)=>{const s=()=>{clearTimeout(i),r(n?.reason)},i=setTimeout((()=>{n?.removeEventListener("abort",s),t()}),e);if(n?.addEventListener("abort",s,{once:!0}),!1===o)try{Deno.unrefTimer(i)}catch(e){if(!(e instanceof ReferenceError))throw e;console.error("`persistent` option is only available in Deno")}}))}(e),m(t)}}),m(Date.now()-d)}await Promise.all(["stdout","stderr"].filter((e=>"piped"===a(c[e]))).map((async e=>{for await(const t of u[e].pipeThrough(new TextDecoderStream).pipeThrough(new r)){const o=Date.now()-d,r={stdout:1,stderr:2}[e];if(c[e]&&n.with({t:o,channel:e})[c[e]]?.(t),p.stdio.length&&f===e){const e=p.stdio.at(-1);e[1]===r&&(e[2]+=`\n${t}`)}else p.stdio.push([o,r,t]);f=e,m(o)}}))),m.flush();const{success:w,code:g}=await u.status;if(!w&&l)throw new EvalError(`${t} exited with non-zero code ${g}:\n${p.stdout}\n${p.stderr}`);return{success:w,code:g,...p}}(T,{bin:t,log:o,callback:f,buffering:m,throw:g,stdin:"piped"===a(l)?l:null,stdout:"piped"===a(c)?c:null,stderr:"piped"===a(u)?u:null})}function a(e){return["inherit","null"].includes(`${e}`)?`${e}`:"piped"}export{l as command};
2
+ /**
3
+ * Logger library
4
+ *
5
+ * It is intended to supersed {@link https://developer.mozilla.org/en-US/docs/Web/API/console | console} by providing:
6
+ * - Colored output
7
+ * - Log levels
8
+ * - Tags
9
+ * - Timestamps
10
+ * - Delta
11
+ * - Caller information
12
+ * - Log formatters
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * import { Logger } from "./mod.ts"
17
+ *
18
+ * // Configure logger
19
+ * const tags = { foo: true, bar: "string" }
20
+ * const options = { date: true, time: true, delta: true, caller: { file: true, fileformat: /.*\/(?<file>libs\/.*)$/, name: true, line: true } }
21
+ * const log = new Logger({ level: Logger.level.debug, options, tags })
22
+ *
23
+ * // Print logs
24
+ * log.error("🍱 bento")
25
+ * log.warn("🍜 ramen")
26
+ * log.info("🍣 sushi")
27
+ * log.log("🍥 narutomaki")
28
+ * log.debug("🍡 dango")
29
+ * ```
30
+ *
31
+ * @author Simon Lecoq (lowlighter)
32
+ * @license MIT
33
+ */
package/command.ts ADDED
@@ -0,0 +1,269 @@
1
+ // Imports
2
+ import type { Arg, Nullable, Promisable } from "@libs/typing"
3
+ import { Logger, type loglevel } from "@libs/logger"
4
+ import { TextLineStream } from "@std/streams"
5
+ import { debounce } from "@std/async/debounce"
6
+ import { delay } from "@std/async/delay"
7
+
8
+ /** Run options. */
9
+ export type options = {
10
+ /** Logger. */
11
+ log?: Logger
12
+ /** Environment variables. */
13
+ env?: Deno.CommandOptions["env"]
14
+ /** Current working directory. */
15
+ cwd?: Deno.CommandOptions["cwd"]
16
+ /** Raw arguments (Windows only). */
17
+ raw?: boolean
18
+ /** Handling of stdin. When using a loglevel, channel will be piped and logged to specified log level. */
19
+ stdin?: loglevel | "piped" | "inherit" | null
20
+ /** Handling of stdout. When using a loglevel, channel will be piped and logged to specified log level. */
21
+ stdout?: loglevel | "piped" | "inherit" | null
22
+ /** Handling of stderr. When using a loglevel, channel will be piped and logged to specified log level. */
23
+ stderr?: loglevel | "piped" | "inherit" | null
24
+ /**
25
+ * Stdin interaction callback.
26
+ * Each time data is received on either stdin or stdout, this will be called after input buffering.
27
+ * You can then read stdio content, write to stdin, close stdin or retry later (for polling).
28
+ * Passing this option will automatically set stdin to "piped" if it is "inherit" or "null".
29
+ */
30
+ callback?: callback
31
+ /**
32
+ * Stdio buffering.
33
+ * This is used to merge messages that are received relatively closely.
34
+ * Buffering is skipped when a different channel is used in-between.
35
+ */
36
+ buffering?: number
37
+ /**
38
+ * Execute process synchronously.
39
+ * Note that stdin is not usable in sync mode.
40
+ */
41
+ sync?: boolean
42
+ /** Process extension on Windows. */
43
+ winext?: string
44
+ /** Operating system. */
45
+ os?: typeof Deno.build.os
46
+ /** Throw an error if exit code is non-zero rather than returning a result. */
47
+ throw?: boolean
48
+ }
49
+
50
+ /** Run result. */
51
+ export type result = {
52
+ /** Whether the process exited with a zero-code. */
53
+ success: Deno.CommandStatus["success"]
54
+ /** Process exit code. */
55
+ code: Deno.CommandStatus["code"]
56
+ /**
57
+ * Process stdio content.
58
+ * First element is the delta timestamp since process start, second element is the channel (0:stdin, 1:stdout, 2:stderr), third element is the content.
59
+ */
60
+ stdio: Array<[number, 0 | 1 | 2, string]>
61
+ /** Process stdin content. */
62
+ stdin: string
63
+ /** Process stdout content. */
64
+ stdout: string
65
+ /** Process stderr content. */
66
+ stderr: string
67
+ }
68
+
69
+ /** Stdin interaction callback. */
70
+ export type callback = (options: { stdio: Pick<result, "stdin" | "stdout" | "stderr">; i: number; write: (content: string) => Promise<void>; close: () => Promise<void>; wait: (dt: number) => Promise<void> }) => Promisable<void>
71
+
72
+ /** Text encoder */
73
+ const encoder = new TextEncoder()
74
+
75
+ /** Text decoder */
76
+ const decoder = new TextDecoder()
77
+
78
+ export function command(bin: string, args: string[], options?: options & { sync?: false }): Promise<result>
79
+ export function command(bin: string, args: string[], options?: options & { sync: true }): result
80
+ /**
81
+ * Run a command.
82
+ *
83
+ * This is a wrapper around `Deno.command` that provides a better handling of stdio for interactive processes.
84
+ *
85
+ * `stdin`, `stdout` and `stderr` can be set to allowed `Deno.command` values (`"inherit"`, `"null"`, `"piped"`),
86
+ * or can be set to a supported log level of `@libs/logger` library. In the later case, the content will be piped and
87
+ * logged to the specified level.
88
+ *
89
+ * Like `Deno.command`, it is possible to pass `env`, `cwd`, and `raw` (alias for `windowsRawArguments`).
90
+ *
91
+ * You can pass the `sync` option to run the process synchronously. Note that stdin is not usable in sync mode and will
92
+ * always be empty.
93
+ *
94
+ * You can pass the `throw` option to throw an error if the process exits with a non-zero code rather than returning a result.
95
+ *
96
+ * You can pass the `winext` option to append an extension to the binary path on Windows (like `.cmd` or `.exe`) when it won't
97
+ * automatically be resolved.
98
+ *
99
+ * Resulted object contains the same properties as `Deno.CommandStatus` with additional `stdio` property that contains an array
100
+ * of ordered tuples with the delta timestamp since process start, the channel (0:stdin, 1:stdout, 2:stderr) and the content.
101
+ * This allows to easily track the order of messages and have a proper history rather than a merged output.
102
+ *
103
+ * Additionally, you can buffer output using the `buffering` option to merge messages that are received relatively closely.
104
+ *
105
+ * Finally, you can pass a `callback` option to interact with the process stdin and stdout.
106
+ * This callback receives an object with the current stdio content, the current command index (based on the content written to stdin),
107
+ * along with a few additional methods:
108
+ * - `write(content: string, newline?: boolean): Promise<void>` encodes and writes content to stdin. It automatically appends a newline
109
+ * by default but can be toggled off by setting `newline` to `false`.
110
+ * - `close(): Promise<void>` closes stdin. Note that you **need** to eventually call this method to prevent most processes from hanging as
111
+ * they're waiting for more input.
112
+ * - `wait(dt: number): Promise<void>` waits for a given amount of time before calling the callback again. This can be useful for polling,
113
+ * like checking if a specific line has been written to stdio or not.
114
+ *
115
+ * The callback is called at each new line from piped channels, except that it is debounced by the `buffering` option which makes it more
116
+ * likely to send input when the process is actually expecting it.
117
+ *
118
+ * @example
119
+ * ```ts
120
+ * import { command } from "./command.ts"
121
+ * const { stdout } = await command("deno", ["repl"], {
122
+ * env: { NO_COLOR: "true" },
123
+ * callback: ({ i, write, close }) => i === 0 ? write("console.log('hello')") : close(),
124
+ * })
125
+ * console.assert(stdout.includes("hello"))
126
+ * ```
127
+ *
128
+ * @example
129
+ * ```ts
130
+ * import { command } from "./command.ts"
131
+ * command("deno", ["eval", "Deno.exit(1)"], { throw: true, sync: true })
132
+ * ```
133
+ */
134
+ export function command(bin: string, args: string[], { log = new Logger(), stdin = null, stdout = "debug", stderr = "error", env, cwd, raw, callback, buffering, sync, throw: _throw, winext = "", os = Deno.build.os } = {} as options): Promisable<result> {
135
+ if (os === "windows") {
136
+ bin = `${bin}${winext}`
137
+ }
138
+ log = log.with({ bin })
139
+ if (callback && (handle(stdin) !== "piped")) {
140
+ stdin = "piped"
141
+ }
142
+ const command = new Deno.Command(bin, { args, stdin: !sync ? handle(stdin) : "null", stdout: handle(stdout), stderr: handle(stderr), env, cwd, windowsRawArguments: raw })
143
+ if (sync) {
144
+ return exec(command, { bin, log, throw: _throw, stdout, stderr })
145
+ }
146
+ return spawn(command, { bin, log, callback, buffering, throw: _throw, stdin: handle(stdin) === "piped" ? stdin as loglevel : null, stdout: handle(stdout) === "piped" ? stdout as loglevel : null, stderr: handle(stderr) === "piped" ? stderr as loglevel : null })
147
+ }
148
+
149
+ /** Returns the handle type for a given mode. */
150
+ function handle(mode: Nullable<string>) {
151
+ return ["inherit", "null"].includes(`${mode}`) ? `${mode}` as "inherit" | "null" : "piped"
152
+ }
153
+
154
+ /** Execute a command synchronously. */
155
+ function exec(command: Deno.Command, { bin, log, throw: _throw, stdout, stderr }: { bin: string; log: Logger; throw?: boolean; stdout: Nullable<string>; stderr: Nullable<string> }) {
156
+ const start = Date.now()
157
+ const output = command.outputSync()
158
+ const { success, code } = output // Do not access stdout or stderr before "piped" status check
159
+ const t = Date.now() - start
160
+ const stdio = {
161
+ get stdio() {
162
+ return [[t, 1, this.stdout], [t, 2, this.stderr]]
163
+ },
164
+ stdin: "",
165
+ stdout: handle(stdout) === "piped" ? decoder.decode(output.stdout) : "",
166
+ stderr: handle(stderr) === "piped" ? decoder.decode(output.stderr) : "",
167
+ } as Pick<result, "stdio" | "stdin" | "stdout" | "stderr">
168
+ for (const { channel, mode } of [{ channel: "stdout", mode: stdout }, { channel: "stderr", mode: stderr }] as const) {
169
+ if ((handle(mode) === "piped") && (stdio[channel])) {
170
+ log.with({ t, channel })[mode as loglevel]?.(stdio[channel])
171
+ }
172
+ }
173
+ if ((!success) && _throw) {
174
+ throw new EvalError(`${bin} exited with non-zero code ${code}:\n${stdio.stdout}\n${stdio.stderr}`)
175
+ }
176
+ return { success, code, ...stdio }
177
+ }
178
+
179
+ /** Spawn a command asynchronously. */
180
+ async function spawn(
181
+ command: Deno.Command,
182
+ { bin, log, callback = ({ close }) => close?.(), buffering = 250, throw: _throw, ...channels }: { bin: string; log: Logger; callback?: callback; buffering?: number; throw?: boolean; stdin: Nullable<loglevel>; stdout: Nullable<loglevel>; stderr: Nullable<loglevel> },
183
+ ) {
184
+ const process = command.spawn()
185
+ const start = Date.now()
186
+ const stdio = {
187
+ stdio: [],
188
+ get stdin() {
189
+ return this.stdio.filter(([_, i]) => i === 0).map(([_, __, content]) => content).join("\n")
190
+ },
191
+ get stdout() {
192
+ return this.stdio.filter(([_, i]) => i === 1).map(([_, __, content]) => content).join("\n")
193
+ },
194
+ get stderr() {
195
+ return this.stdio.filter(([_, i]) => i === 2).map(([_, __, content]) => content).join("\n")
196
+ },
197
+ } as Pick<result, "stdio" | "stdin" | "stdout" | "stderr">
198
+ const options = {} as Pick<Arg<callback>, "write" | "close" | "wait">
199
+ let last = ""
200
+ const debounced = debounce(async (t: number) => {
201
+ log.with({ t }).debug("debounced")
202
+ last = ""
203
+ await callback({ stdio, i: stdio.stdin.length, ...options })
204
+ }, buffering)
205
+ // Prepare stdin handlers if channel is piped
206
+ if (handle(channels.stdin) === "piped") {
207
+ const writer = process.stdin.getWriter()
208
+ Object.assign(options, {
209
+ async write(content: string, newline = true) {
210
+ const t = Date.now() - start
211
+ if (channels.stdin) {
212
+ log.with({ t, channel: "stdin" })[channels.stdin]?.(content)
213
+ }
214
+ stdio.stdio.push([t, 0, content])
215
+ if (newline && (!content.endsWith("\n"))) {
216
+ content += "\n"
217
+ }
218
+ await writer.write(encoder.encode(content))
219
+ last = "stdin"
220
+ writer.releaseLock()
221
+ },
222
+ async close() {
223
+ try {
224
+ writer.releaseLock()
225
+ await process.stdin.close()
226
+ log.with({ t: Date.now() - start, closed: "stdin" }).debug()
227
+ } catch {
228
+ // Ignore
229
+ }
230
+ },
231
+ async wait(dt = 1000) {
232
+ const t = Date.now() - start
233
+ log.with({ t, waiting: dt }).debug()
234
+ await delay(dt)
235
+ debounced(t)
236
+ },
237
+ })
238
+ debounced(Date.now() - start)
239
+ }
240
+ // Buffer output and debounce interaction callback
241
+ await Promise.all(
242
+ (["stdout", "stderr"] as const).filter((channel) => handle(channels[channel]) === "piped").map(async (channel) => {
243
+ for await (const line of process[channel].pipeThrough(new TextDecoderStream()).pipeThrough(new TextLineStream())) {
244
+ const t = Date.now() - start
245
+ const stdi = { stdout: 1, stderr: 2 }[channel] as 1 | 2
246
+ if (channels[channel]) {
247
+ log.with({ t, channel })[channels[channel]!]?.(line)
248
+ }
249
+ if ((stdio.stdio.length) && (last === channel)) {
250
+ const previous = stdio.stdio.at(-1)!
251
+ if (previous[1] === stdi) {
252
+ previous[2] += `\n${line}`
253
+ }
254
+ } else {
255
+ stdio.stdio.push([t, stdi, line])
256
+ }
257
+ last = channel
258
+ debounced(t)
259
+ }
260
+ }),
261
+ )
262
+ debounced.flush()
263
+ // Result
264
+ const { success, code } = await process.status
265
+ if ((!success) && _throw) {
266
+ throw new EvalError(`${bin} exited with non-zero code ${code}:\n${stdio.stdout}\n${stdio.stderr}`)
267
+ }
268
+ return { success, code, ...stdio }
269
+ }
@@ -0,0 +1,118 @@
1
+ import { Logger } from "@libs/logger"
2
+ import { command } from "./command.ts"
3
+ import { expect, test, type testing } from "@libs/testing"
4
+
5
+ test("deno")("command() can spawn subprocesses asynchronously", async () => {
6
+ let result = command("deno", ["--version"], { env: { NO_COLOR: "true" } }) as testing
7
+ expect(result).toBeInstanceOf(Promise)
8
+ result = await result
9
+ expect(result).toMatchObject({ success: true, code: 0, stdin: "", stderr: "" })
10
+ expect(result.stdio).toBeInstanceOf(Array)
11
+ expect(result.stdout).toMatch(/deno/)
12
+ }, { permissions: { run: ["deno"] } })
13
+
14
+ test("deno")("command() can spawn subprocesses synchronously", () => {
15
+ const result = command("deno", ["--version"], { env: { NO_COLOR: "true" }, sync: true })
16
+ expect(result).not.toBeInstanceOf(Promise)
17
+ expect(result).toMatchObject({ success: true, code: 0, stdin: "", stderr: "" })
18
+ expect(result.stdio).toBeInstanceOf(Array)
19
+ expect(result.stdout).toMatch(/deno/)
20
+ }, { permissions: { run: ["deno"] } })
21
+
22
+ test("deno")("command() handles callback<write()> and callback<close()> calls", async () => {
23
+ const result = await command("deno", ["repl"], { env: { NO_COLOR: "true" }, callback: ({ i, write, close }) => i === 0 ? write("console.log('hello')") : close() })
24
+ expect(result).toMatchObject({ success: true, code: 0, stdin: "console.log('hello')" })
25
+ expect(result.stdout).toMatch(/hello/)
26
+ }, { permissions: { run: ["deno"] } })
27
+
28
+ test("deno")("command() handles multiple callback<close()> calls", async () => {
29
+ const result = await command("deno", ["repl"], {
30
+ env: { NO_COLOR: "true" },
31
+ callback: async ({ close }) => {
32
+ await close()
33
+ await close()
34
+ },
35
+ })
36
+ expect(result).toMatchObject({ success: true, code: 0 })
37
+ }, { permissions: { run: ["deno"] } })
38
+
39
+ test("deno")("command() handles callback<wait()> calls", async () => {
40
+ let waited = false
41
+ const result = await command("deno", ["repl"], {
42
+ env: { NO_COLOR: "true" },
43
+ callback: ({ wait, close }) => waited ? close() : (waited = true, wait(250)),
44
+ })
45
+ expect(result).toMatchObject({ success: true, code: 0 })
46
+ }, { permissions: { run: ["deno"] } })
47
+
48
+ for (const sync of [false, true]) {
49
+ for (const mode of ["inherit", "piped", null, "debug", "log", "info", "warn", "error"] as const) {
50
+ test("deno")(`command() supports stdio set to "${mode}" in "${sync ? "sync" : "async"}" mode`, async () => {
51
+ const result = await command("deno", ["--version"], {
52
+ log: new Logger({ level: Logger.level.disabled }),
53
+ env: { NO_COLOR: "true" },
54
+ stdin: mode,
55
+ stdout: mode,
56
+ stderr: mode,
57
+ sync: sync as testing,
58
+ })
59
+ expect(result).toMatchObject({ success: true, code: 0 })
60
+ }, { permissions: { run: ["deno"] } })
61
+ }
62
+ }
63
+
64
+ test("deno")("command() handles both stdout and stderr channels", async () => {
65
+ const result = await command("deno", ["eval", "await Deno.stdout.write(new TextEncoder().encode(`foo\n`));await Deno.stderr.write(new TextEncoder().encode(`bar\n`))"], {
66
+ env: { NO_COLOR: "true" },
67
+ stdout: "piped",
68
+ stderr: "piped",
69
+ })
70
+ expect(result).toMatchObject({ success: true, code: 0, stdout: "foo", stderr: "bar" })
71
+ }, { permissions: { run: ["deno"] } })
72
+
73
+ test("deno")("command() supports buffering", async () => {
74
+ // Combined entries if buffering is greater than the time between writes
75
+ {
76
+ const result = await command("deno", ["eval", "console.log(`foo`);await new Promise(resolve => setTimeout(resolve, 100));console.log(`bar`)"], {
77
+ env: { NO_COLOR: "true" },
78
+ buffering: 150,
79
+ })
80
+ expect(result.stdio).toHaveLength(1)
81
+ expect(result.stdio[0][2]).toBe("foo\nbar")
82
+ expect(result.stdout).toBe("foo\nbar")
83
+ }
84
+ // Separated entries if buffering is less than the time between writes
85
+ {
86
+ const result = await command("deno", ["eval", "console.log(`foo`);await new Promise(resolve => setTimeout(resolve, 100));console.log(`bar`)"], {
87
+ env: { NO_COLOR: "true" },
88
+ buffering: 50,
89
+ })
90
+ expect(result.stdio).toHaveLength(2)
91
+ expect(result.stdio[0][2]).toBe("foo")
92
+ expect(result.stdio[1][2]).toBe("bar")
93
+ expect(result.stdout).toBe("foo\nbar")
94
+ }
95
+ // Separated entries even if buffering is active but channel was changed in-between
96
+ {
97
+ const result = await command("deno", [
98
+ "eval",
99
+ "await Deno.stdout.write(new TextEncoder().encode(`foo\n`));await Deno.stderr.write(new TextEncoder().encode(`baz\n`));await Deno.stdout.write(new TextEncoder().encode(`bar\n`))",
100
+ ], {
101
+ stdout: "piped",
102
+ stderr: "piped",
103
+ env: { NO_COLOR: "true" },
104
+ buffering: 200,
105
+ })
106
+ expect(result.stdio).toHaveLength(3)
107
+ expect(result.stdio[0][2]).toBe("foo")
108
+ expect(result.stdio[1][2]).toBe("baz")
109
+ expect(result.stdio[2][2]).toBe("bar")
110
+ expect(result.stdout).toBe("foo\nbar")
111
+ expect(result.stderr).toBe("baz")
112
+ }
113
+ }, { permissions: { run: ["deno"] } })
114
+
115
+ test("deno")("command() throws an error when `throw` option is enabled and exit code is non-zero", async () => {
116
+ expect(() => command("deno", ["eval", "Deno.exit(1)"], { env: { NO_COLOR: "true" }, throw: true, sync: true })).toThrow(EvalError)
117
+ await expect(command("deno", ["eval", "Deno.exit(1)"], { env: { NO_COLOR: "true" }, throw: true })).rejects.toThrow(EvalError)
118
+ }, { permissions: { run: ["deno"] } })
package/deno.jsonc ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "icon": "⏯️",
3
+ "name": "@libs/run",
4
+ "version": "0.2.0",
5
+ "description": "Utilities to run subprocess.",
6
+ "keywords": [
7
+ "subprocess",
8
+ "esm"
9
+ ],
10
+ "license": "MIT License",
11
+ "author": "lowlighter (Simon Lecoq)",
12
+ "funding": "https://github.com/sponsors/lowlighter",
13
+ "homepage": "https://github.com/lowlighter/libs",
14
+ "playground": "https://libs.lecoq.io/run",
15
+ "supported": [
16
+ "deno"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/lowlighter/libs.git"
21
+ },
22
+ "npm": true,
23
+ "exports": {
24
+ ".": "./mod.ts",
25
+ "./command": "./command.ts"
26
+ },
27
+ "imports": {
28
+ "@std/async/delay": "jsr:@std/async@0.224.1/delay",
29
+ "@std/async/debounce": "jsr:@std/async@0.224.1/debounce",
30
+ "@std/streams": "jsr:@std/streams@0.224.2",
31
+ "@libs/logger": "jsr:@libs/logger@1",
32
+ "@libs/testing": "jsr:@libs/testing@1",
33
+ "@libs/typing": "jsr:@libs/typing@2"
34
+ },
35
+ "test:permissions": {
36
+ "run": [
37
+ "deno",
38
+ "node",
39
+ "bun",
40
+ "npx"
41
+ ]
42
+ },
43
+ "tasks": {
44
+ "test": "deno test --allow-run=deno,node,bun,npx --no-prompt --coverage --clean --trace-leaks --doc",
45
+ "dev": "deno fmt && deno task test --filter='/^\\[deno\\]/' && deno coverage --exclude=.js --detailed && deno lint && deno publish --dry-run --quiet --allow-dirty",
46
+ "dev:future": "DENO_FUTURE=1 && deno task dev",
47
+ "coverage": "deno task test --filter='/^\\[deno\\]/' --quiet && deno coverage --exclude=.js",
48
+ "ci": "deno fmt --check && deno task test --filter='/^\\[node|bun \\]/' --quiet && deno coverage --exclude=.js && deno lint",
49
+ "ci:coverage": "deno task coverage --html && sleep 1 && mkdir -p ../coverage && rm -rf ../coverage/run && mv coverage/html ../coverage/run"
50
+ },
51
+ "lint": {
52
+ "rules": {
53
+ "include": [
54
+ "no-throw-literal",
55
+ "no-eval",
56
+ "eqeqeq",
57
+ "ban-untagged-todo"
58
+ ]
59
+ },
60
+ "exclude": [
61
+ "**/wasm_*",
62
+ "**/*.mjs"
63
+ ]
64
+ },
65
+ "fmt": {
66
+ "lineWidth": 280,
67
+ "semiColons": false,
68
+ "exclude": [
69
+ "coverage",
70
+ "**/coverage",
71
+ "**/node_modules",
72
+ "**/package.json",
73
+ "**/package-lock.json",
74
+ "**/wasm_*",
75
+ "**/*.mjs"
76
+ ]
77
+ }
78
+ }
package/deno.lock ADDED
@@ -0,0 +1,100 @@
1
+ {
2
+ "version": "3",
3
+ "packages": {
4
+ "specifiers": {
5
+ "jsr:@libs/logger@1": "jsr:@libs/logger@1.1.1",
6
+ "jsr:@libs/testing@1": "jsr:@libs/testing@1.0.7",
7
+ "jsr:@libs/typing@2": "jsr:@libs/typing@2.1.0",
8
+ "jsr:@std/assert@^0.224.0": "jsr:@std/assert@0.224.0",
9
+ "jsr:@std/assert@^0.225.3": "jsr:@std/assert@0.225.3",
10
+ "jsr:@std/async@0.224.1": "jsr:@std/async@0.224.1",
11
+ "jsr:@std/bytes@^0.224.0": "jsr:@std/bytes@0.224.0",
12
+ "jsr:@std/bytes@^1.0.0-rc.3": "jsr:@std/bytes@1.0.0-rc.3",
13
+ "jsr:@std/expect@0.224.0": "jsr:@std/expect@0.224.0",
14
+ "jsr:@std/fmt@^0.224.0": "jsr:@std/fmt@0.224.0",
15
+ "jsr:@std/internal@^0.224.0": "jsr:@std/internal@0.224.0",
16
+ "jsr:@std/io@^0.224.0": "jsr:@std/io@0.224.0",
17
+ "jsr:@std/path@0.225.1": "jsr:@std/path@0.225.1",
18
+ "jsr:@std/streams@0.224.2": "jsr:@std/streams@0.224.2",
19
+ "npm:@types/node": "npm:@types/node@18.16.19"
20
+ },
21
+ "jsr": {
22
+ "@libs/logger@1.1.1": {
23
+ "integrity": "7b74ca947062b955e8d63299f77a41fc4f2cf0a663a6ad836b703d601f622ccf"
24
+ },
25
+ "@libs/testing@1.0.7": {
26
+ "integrity": "b1a7caa9d2e00569f90b9753eb768ec6b16ff5c859f104b73a6ef4611968892e",
27
+ "dependencies": [
28
+ "jsr:@std/expect@0.224.0",
29
+ "jsr:@std/path@0.225.1"
30
+ ]
31
+ },
32
+ "@libs/typing@2.1.0": {
33
+ "integrity": "3c3b93e1da1e4bb45a22dde168fdfd1cc765edbdb371b46bf4ca7873ee79e15d"
34
+ },
35
+ "@std/assert@0.224.0": {
36
+ "integrity": "8643233ec7aec38a940a8264a6e3eed9bfa44e7a71cc6b3c8874213ff401967f"
37
+ },
38
+ "@std/assert@0.225.3": {
39
+ "integrity": "b3c2847aecf6955b50644cdb9cf072004ea3d1998dd7579fc0acb99dbb23bd4f"
40
+ },
41
+ "@std/async@0.224.1": {
42
+ "integrity": "2fda2c8151cc5811a6ca37fe825f1f71c95e02a374abb6ef868e0e19eca814a5"
43
+ },
44
+ "@std/bytes@0.224.0": {
45
+ "integrity": "a2250e1d0eb7d1c5a426f21267ab9bdeac2447fa87a3d0d1a467d3f7a6058e49"
46
+ },
47
+ "@std/bytes@1.0.0-rc.3": {
48
+ "integrity": "b3e93f73f1ccf167124f695596d2d026b0030930c38bd0ddec81d7f75ab41948"
49
+ },
50
+ "@std/expect@0.224.0": {
51
+ "integrity": "54bc071f7edcbd7bb4531f913e466e5ec3642f401dc3771fe5975f0693f25969",
52
+ "dependencies": [
53
+ "jsr:@std/assert@^0.224.0",
54
+ "jsr:@std/fmt@^0.224.0",
55
+ "jsr:@std/internal@^0.224.0"
56
+ ]
57
+ },
58
+ "@std/fmt@0.224.0": {
59
+ "integrity": "e20e9a2312a8b5393272c26191c0a68eda8d2c4b08b046bad1673148f1d69851"
60
+ },
61
+ "@std/internal@0.224.0": {
62
+ "integrity": "afc50644f9cdf4495eeb80523a8f6d27226b4b36c45c7c195dfccad4b8509291"
63
+ },
64
+ "@std/io@0.224.0": {
65
+ "integrity": "0aff885d21d829c050b8a08b1d71b54aed5841aecf227f8d77e99ec529a11e8e",
66
+ "dependencies": [
67
+ "jsr:@std/assert@^0.224.0",
68
+ "jsr:@std/bytes@^0.224.0"
69
+ ]
70
+ },
71
+ "@std/path@0.225.1": {
72
+ "integrity": "8c3220635a73730eb51fe43de9e10b79e2724a5bb8638b9355d35ae012fd9429"
73
+ },
74
+ "@std/streams@0.224.2": {
75
+ "integrity": "5d437af1423e4f616111b986ea783c15e0bc998b53e23e93b623de7ef0b14c2b",
76
+ "dependencies": [
77
+ "jsr:@std/assert@^0.225.3",
78
+ "jsr:@std/bytes@^1.0.0-rc.3",
79
+ "jsr:@std/io@^0.224.0"
80
+ ]
81
+ }
82
+ },
83
+ "npm": {
84
+ "@types/node@18.16.19": {
85
+ "integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==",
86
+ "dependencies": {}
87
+ }
88
+ }
89
+ },
90
+ "remote": {},
91
+ "workspace": {
92
+ "dependencies": [
93
+ "jsr:@libs/logger@1",
94
+ "jsr:@libs/testing@1",
95
+ "jsr:@libs/typing@2",
96
+ "jsr:@std/async@0.224.1",
97
+ "jsr:@std/streams@0.224.2"
98
+ ]
99
+ }
100
+ }
package/mod.mjs ADDED
@@ -0,0 +1,33 @@
1
+ var e=class e{constructor({level:t,format:n,output:o,tags:r,options:s}={}){if("granted"===globalThis.Deno?.permissions.querySync?.({name:"env",variable:"LOG_LEVEL"}).state){const n=globalThis.Deno?.env.get("LOG_LEVEL")??"";n in e.level&&(t??=e.level[n]),Number.isNaN(Number.parseInt(n))||(t??=Number.parseInt(n))}this.level=t??e.level.log,this.#e=o||null===o?o:console,this.#t=n??e.format.text,this.tags=r??{},this.options={date:!1,time:!1,delta:!0,...s,caller:!1!==s?.caller&&{file:!1,name:!1,line:!1,...s?.caller}}}level;#t;#e;tags;options;error(...t){return this.level>=e.level.error&&this.#e?.error(...this.#t(this,{level:e.level.error,content:t})),this}warn(...t){return this.level>=e.level.warn&&this.#e?.warn(...this.#t(this,{level:e.level.warn,content:t})),this}info(...t){return this.level>=e.level.info&&this.#e?.info(...this.#t(this,{level:e.level.info,content:t})),this}log(...t){return this.level>=e.level.log&&this.#e?.log(...this.#t(this,{level:e.level.log,content:t})),this}debug(...t){return this.level>=e.level.debug&&this.#e?.debug(...this.#t(this,{level:e.level.debug,content:t})),this}with(t={}){return new e({level:this.level,format:this.#t,output:this.#e,options:{...this.options},tags:{...this.tags,...t}})}#n(e=3){const t=Error,n=t.prepareStackTrace;t.prepareStackTrace=(e,t)=>t;const{stack:o}=new Error;t.prepareStackTrace=n;const r=o[e];return{file:r.getFileName(),name:r.getFunctionName(),line:r.getLineNumber(),column:r.getColumnNumber()}}static level=Object.freeze({disabled:NaN,error:0,warn:1,info:2,log:3,debug:4});static format={text(t,{level:n=0,content:o}){const r=["red","orange","cyan","white","gray"][n],s=[`%c ${Object.keys(e.level).find((t=>e.level[t]===n)).toLocaleUpperCase().padEnd(5)} │%c`],i=[`color: black; background-color: ${r}`,""];if(t.options.date||t.options.time||t.options.delta){const e=(new Date).toISOString(),n=[];if(t.options.delta){const e=performance.now()/1e3;let t=e.toPrecision(4);e<1&&(t=e.toPrecision(2)),n.push(`+${t}`)}t.options.date&&t.options.time?n.push(e):t.options.date?n.push(e.slice(0,e.indexOf("T"))):t.options.time&&n.push(e.slice(e.indexOf("T")+1,-1)),s.push(`%c ${n.join(" ¦ ").trim()} %c`),i.push(`color: black; background-color: ${r}`,"")}if(t.options.caller){const e=t.#n();if(e){const n=[];t.options.caller.file&&n.push(`${e.file.replace(t.options.caller.fileformat,"$<file>")}`),t.options.caller.name&&e.name&&n.push(e.name),t.options.caller.line&&n.push(e.line,e.column),s.push(`%c ${n.join(":").trim()} %c`),i.push("color: black; background-color: gray","")}}{const n=[];for(const[o,r]of Object.entries(t.tags))n.push(`${o}:${e.inspect(r)}`);s.push(`%c ${n.join(" ").trim()} %c`),i.push("background-color: black","")}return[s.join(""),...i,...o.map(e.inspect)]},json(t,{level:n=0,content:o}){const r={level:Object.keys(e.level).find((t=>e.level[t]===n)),timestamp:Date.now(),tags:t.tags,content:o},s={};if(t.options.date||t.options.time){const e=new Date(r.timestamp).toISOString();t.options.date&&(s.date=e.slice(0,e.indexOf("T"))),t.options.time&&(s.time=e.slice(e.indexOf("T")+1,-1))}if(t.options.delta&&(s.delta=performance.now()/1e3),t.options.caller){const e=t.#n();e&&(s.caller={},t.options.caller.file&&(s.caller.file=`${e.file.replace(t.options.caller.fileformat,"$<file>")}`),t.options.caller.name&&e.name&&(s.caller.name=e.name),t.options.caller.line&&(s.caller.line=[e.line,e.column]))}return[JSON.stringify({...r,...s})]}};static inspect(e){return globalThis.Deno?.inspect(e,{colors:!0,depth:1/0})??e}},t=class extends Error{constructor(e){super(e),this.name="AssertionError"}};TransformStream;function n(e){let t=0;for(const n of e)t+=n.length;const n=new Uint8Array(t);let o=0;for(const t of e)n.set(t,o),o+=t.length;return n}function o(e){const t=e.length,n=new Uint8Array(t);n[0]=0;let o=0,r=1;for(;r<t;)e[r]===e[o]?(o++,n[r]=o,r++):0===o?(n[r]=0,r++):o=n[o-1];return n}TransformStream,TransformStream,TransformStream,TransformStream;var r=class extends TransformStream{#o="";constructor(e={allowCR:!1}){super({transform:(t,n)=>{for(t=this.#o+t;;){const o=t.indexOf("\n"),r=e.allowCR?t.indexOf("\r"):-1;if(-1!==r&&r!==t.length-1&&(-1===o||o-1>r)){n.enqueue(t.slice(0,r)),t=t.slice(r+1);continue}if(-1===o)break;const s="\r"===t[o-1]?o-1:o;n.enqueue(t.slice(0,s)),t=t.slice(o+1)}this.#o=t},flush:t=>{if(""===this.#o)return;const n=e.allowCR&&this.#o.endsWith("\r")?this.#o.slice(0,-1):this.#o;t.enqueue(n)}})}};new TextDecoder;var s=new TextEncoder,i=new TextDecoder;function l(t,n,{log:o=new e,stdin:l=null,stdout:c="debug",stderr:u="error",env:d,cwd:p,raw:h,callback:f,buffering:m,sync:w,throw:g,winext:b="",os:v=Deno.build.os}={}){"windows"===v&&(t=`${t}${b}`),o=o.with({bin:t}),f&&"piped"!==a(l)&&(l="piped");const T=new Deno.Command(t,{args:n,stdin:w?"null":a(l),stdout:a(c),stderr:a(u),env:d,cwd:p,windowsRawArguments:h});return w?function(e,{bin:t,log:n,throw:o,stdout:r,stderr:s}){const l=Date.now(),c=e.outputSync(),{success:u,code:d}=c,p=Date.now()-l,h={get stdio(){return[[p,1,this.stdout],[p,2,this.stderr]]},stdin:"",stdout:"piped"===a(r)?i.decode(c.stdout):"",stderr:"piped"===a(s)?i.decode(c.stderr):""};for(const{channel:e,mode:t}of[{channel:"stdout",mode:r},{channel:"stderr",mode:s}])"piped"===a(t)&&h[e]&&n.with({t:p,channel:e})[t]?.(h[e]);if(!u&&o)throw new EvalError(`${t} exited with non-zero code ${d}:\n${h.stdout}\n${h.stderr}`);return{success:u,code:d,...h}}(T,{bin:t,log:o,throw:g,stdout:c,stderr:u}):async function(e,{bin:t,log:n,callback:o=(({close:e})=>e?.()),buffering:i=250,throw:l,...c}){const u=e.spawn(),d=Date.now(),p={stdio:[],get stdin(){return this.stdio.filter((([e,t])=>0===t)).map((([e,t,n])=>n)).join("\n")},get stdout(){return this.stdio.filter((([e,t])=>1===t)).map((([e,t,n])=>n)).join("\n")},get stderr(){return this.stdio.filter((([e,t])=>2===t)).map((([e,t,n])=>n)).join("\n")}},h={};let f="";const m=function(e,t){let n=null,o=null;const r=(...s)=>{r.clear(),o=()=>{r.clear(),e.call(r,...s)},n=setTimeout(o,t)};return r.clear=()=>{"number"==typeof n&&(clearTimeout(n),n=null,o=null)},r.flush=()=>{o?.()},Object.defineProperty(r,"pending",{get:()=>"number"==typeof n}),r}((async e=>{n.with({t:e}).debug("debounced"),f="",await o({stdio:p,i:p.stdin.length,...h})}),i);if("piped"===a(c.stdin)){const e=u.stdin.getWriter();Object.assign(h,{async write(t,o=!0){const r=Date.now()-d;c.stdin&&n.with({t:r,channel:"stdin"})[c.stdin]?.(t),p.stdio.push([r,0,t]),o&&!t.endsWith("\n")&&(t+="\n"),await e.write(s.encode(t)),f="stdin",e.releaseLock()},async close(){try{e.releaseLock(),await u.stdin.close(),n.with({t:Date.now()-d,closed:"stdin"}).debug()}catch{}},async wait(e=1e3){const t=Date.now()-d;n.with({t:t,waiting:e}).debug(),await function(e,t={}){const{signal:n,persistent:o=!0}=t;return n?.aborted?Promise.reject(n.reason):new Promise(((t,r)=>{const s=()=>{clearTimeout(i),r(n?.reason)},i=setTimeout((()=>{n?.removeEventListener("abort",s),t()}),e);if(n?.addEventListener("abort",s,{once:!0}),!1===o)try{Deno.unrefTimer(i)}catch(e){if(!(e instanceof ReferenceError))throw e;console.error("`persistent` option is only available in Deno")}}))}(e),m(t)}}),m(Date.now()-d)}await Promise.all(["stdout","stderr"].filter((e=>"piped"===a(c[e]))).map((async e=>{for await(const t of u[e].pipeThrough(new TextDecoderStream).pipeThrough(new r)){const o=Date.now()-d,r={stdout:1,stderr:2}[e];if(c[e]&&n.with({t:o,channel:e})[c[e]]?.(t),p.stdio.length&&f===e){const e=p.stdio.at(-1);e[1]===r&&(e[2]+=`\n${t}`)}else p.stdio.push([o,r,t]);f=e,m(o)}}))),m.flush();const{success:w,code:g}=await u.status;if(!w&&l)throw new EvalError(`${t} exited with non-zero code ${g}:\n${p.stdout}\n${p.stderr}`);return{success:w,code:g,...p}}(T,{bin:t,log:o,callback:f,buffering:m,throw:g,stdin:"piped"===a(l)?l:null,stdout:"piped"===a(c)?c:null,stderr:"piped"===a(u)?u:null})}function a(e){return["inherit","null"].includes(`${e}`)?`${e}`:"piped"}export{l as command};
2
+ /**
3
+ * Logger library
4
+ *
5
+ * It is intended to supersed {@link https://developer.mozilla.org/en-US/docs/Web/API/console | console} by providing:
6
+ * - Colored output
7
+ * - Log levels
8
+ * - Tags
9
+ * - Timestamps
10
+ * - Delta
11
+ * - Caller information
12
+ * - Log formatters
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * import { Logger } from "./mod.ts"
17
+ *
18
+ * // Configure logger
19
+ * const tags = { foo: true, bar: "string" }
20
+ * const options = { date: true, time: true, delta: true, caller: { file: true, fileformat: /.*\/(?<file>libs\/.*)$/, name: true, line: true } }
21
+ * const log = new Logger({ level: Logger.level.debug, options, tags })
22
+ *
23
+ * // Print logs
24
+ * log.error("🍱 bento")
25
+ * log.warn("🍜 ramen")
26
+ * log.info("🍣 sushi")
27
+ * log.log("🍥 narutomaki")
28
+ * log.debug("🍡 dango")
29
+ * ```
30
+ *
31
+ * @author Simon Lecoq (lowlighter)
32
+ * @license MIT
33
+ */
package/mod.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./command.ts"
package/mod_test.ts ADDED
@@ -0,0 +1 @@
1
+ import "./mod.ts"
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@lowlighter/run",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "scripts": {},
6
+ "description": "Utilities to run subprocess.",
7
+ "keywords": [
8
+ "subprocess",
9
+ "esm"
10
+ ],
11
+ "license": "MIT License",
12
+ "author": "lowlighter (Simon Lecoq)",
13
+ "homepage": "https://github.com/lowlighter/libs",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/lowlighter/libs.git"
17
+ },
18
+ "funding": "https://github.com/sponsors/lowlighter",
19
+ "exports": {
20
+ ".": "./mod.mjs",
21
+ "./command": "./command.mjs"
22
+ }
23
+ }