@forwardimpact/libutil 0.1.85 → 0.1.87

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forwardimpact/libutil",
3
- "version": "0.1.85",
3
+ "version": "0.1.87",
4
4
  "description": "Cross-cutting utilities: retry, hashing, token counting, and project discovery.",
5
5
  "keywords": [
6
6
  "util",
package/src/calendar.js CHANGED
@@ -28,6 +28,16 @@ export function isoDate(input) {
28
28
  return toDate(input).toISOString().slice(0, 10);
29
29
  }
30
30
 
31
+ /**
32
+ * Format an input as a full ISO 8601 timestamp (`YYYY-MM-DDTHH:mm:ss.sssZ`,
33
+ * UTC). Pass `runtime.clock.now()` for "now"; never reads the wall clock.
34
+ * @param {Date|number|string} input
35
+ * @returns {string}
36
+ */
37
+ export function isoTimestamp(input) {
38
+ return toDate(input).toISOString();
39
+ }
40
+
31
41
  /**
32
42
  * Compute the ISO 8601 year-week for an input. `year` is the ISO week-year
33
43
  * (not necessarily the calendar year for edge weeks).
package/src/index.js CHANGED
@@ -165,6 +165,7 @@ export {
165
165
  } from "./runtime.js";
166
166
  export {
167
167
  isoDate,
168
+ isoTimestamp,
168
169
  isoWeek,
169
170
  isoWeekString,
170
171
  yearMonth,
package/src/runtime.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { spawn as nodeSpawn, execFile, spawnSync } from "node:child_process";
2
- import nodeFsSync from "node:fs";
2
+ import nodeFsSync, {
3
+ createReadStream as nodeCreateReadStream,
4
+ createWriteStream as nodeCreateWriteStream,
5
+ } from "node:fs";
3
6
  import nodeFs from "node:fs/promises";
4
7
  import { Finder } from "./finder.js";
5
8
 
@@ -16,27 +19,40 @@ import { Finder } from "./finder.js";
16
19
  * @property {Object} fs
17
20
  * Async filesystem surface (the `node:fs/promises` shape): `readFile`,
18
21
  * `writeFile`, `readdir`, `stat`, `mkdir`, `access`, `copyFile`, `cp`, `rm`,
19
- * `lstat`, `unlink`, `symlink`, `utimes`, `chmod`. A module destructures
20
- * `fs` xor `fsSync`, never both (design Decision 7).
22
+ * `lstat`, `unlink`, `symlink`, `utimes`, `chmod`, plus the two stream
23
+ * factories `createReadStream` / `createWriteStream` (the `node:fs` shape
24
+ * the promises API has no stream factories, so they live on the async
25
+ * surface as the canonical streaming seam). A module destructures `fs` xor
26
+ * `fsSync`, never both (design Decision 7).
21
27
  * @property {Object} fsSync
22
28
  * Sync filesystem surface (the `node:fs` shape): `existsSync`,
23
29
  * `readFileSync`, `writeFileSync`, `mkdirSync`, `readdirSync`, `statSync`,
24
- * `openSync`, `closeSync`, `unlinkSync`.
30
+ * `openSync`, `readSync`, `closeSync`, `unlinkSync`.
25
31
  * @property {Object} proc
26
32
  * Process surface: `cwd()`, `env`, `argv`, `stdin`, `stdout.write`,
27
33
  * `stderr.write`, `exit(code)`, `kill(pid, signal)` (a negative `pid`
28
- * signals the process group, e.g. for daemon teardown), and an `exitCode`
29
- * accessor.
34
+ * signals the process group, e.g. for daemon teardown), `pid` (this
35
+ * process's id — used to exclude self from process-group descendant scans),
36
+ * `platform` (the `process.platform` string — `"darwin"`/`"win32"`/`"linux"`
37
+ * — for per-platform path resolution), `on(event, handler)` (subscribe to
38
+ * process events such as `"SIGTERM"`/`"SIGINT"` so daemons register signal
39
+ * handlers through the collaborator instead of the global), and an
40
+ * `exitCode` accessor.
30
41
  * @property {Object} clock
31
42
  * Time surface: `now()`, `sleep(ms)`, `setTimeout(fn, ms)`,
32
- * `clearTimeout(handle)`.
43
+ * `clearTimeout(handle)`, `setInterval(fn, ms)`, `clearInterval(handle)`.
44
+ * The interval handle mirrors the host timer (so callers may `.unref()` it).
33
45
  * @property {Object} subprocess
34
46
  * Subprocess surface: `run(cmd, args, opts) -> Promise<{stdout, stderr,
35
47
  * exitCode}>` (async, buffered), `runSync(cmd, args, opts) -> {stdout,
36
48
  * stderr, exitCode}` (synchronous, buffered — for the rare caller that
37
49
  * cannot go async, e.g. a sync config accessor shelling to `gh auth
38
- * token`), and `spawn(cmd, args, opts) -> {stdout, stderr, exitCode, kill}`
39
- * where `stdout`/`stderr` are AsyncIterables and `exitCode` a Promise.
50
+ * token`), and `spawn(cmd, args, opts) -> {stdout, stderr, stdin, exitCode,
51
+ * signal, kill, pid}` where `stdout`/`stderr` are AsyncIterables,
52
+ * `exitCode`/`signal` are Promises (the terminating signal name or `null`),
53
+ * `stdin` is the child's writable (only when `opts.stdio` pipes stdin, else
54
+ * `null`), `kill(signal)` signals the child, and `pid` is its id (`undefined`
55
+ * on spawn failure).
40
56
  * @property {Object} finder
41
57
  * A constructed `Finder` (project path resolution + symlink management).
42
58
  */
@@ -81,6 +97,9 @@ export function createDefaultProc({ source = process, env = source.env } = {}) {
81
97
  stderr: { write: (s) => source.stderr.write(s) },
82
98
  exit: (code) => source.exit(code),
83
99
  kill: (pid, signal) => source.kill(pid, signal),
100
+ pid: source.pid,
101
+ platform: source.platform,
102
+ on: (event, handler) => source.on(event, handler),
84
103
  };
85
104
  Object.defineProperty(proc, "exitCode", {
86
105
  enumerable: true,
@@ -116,7 +135,7 @@ function lineIterator(stream) {
116
135
 
117
136
  /**
118
137
  * Build the clock surface backed by real timers.
119
- * @returns {{now: () => number, sleep: (ms: number) => Promise<void>, setTimeout: Function, clearTimeout: Function}}
138
+ * @returns {{now: () => number, sleep: (ms: number) => Promise<void>, setTimeout: Function, clearTimeout: Function, setInterval: Function, clearInterval: Function}}
120
139
  */
121
140
  export function createDefaultClock() {
122
141
  return {
@@ -124,6 +143,8 @@ export function createDefaultClock() {
124
143
  sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
125
144
  setTimeout: (fn, ms) => setTimeout(fn, ms),
126
145
  clearTimeout: (handle) => clearTimeout(handle),
146
+ setInterval: (fn, ms) => setInterval(fn, ms),
147
+ clearInterval: (handle) => clearInterval(handle),
127
148
  };
128
149
  }
129
150
 
@@ -172,13 +193,45 @@ export function createDefaultSubprocess() {
172
193
 
173
194
  const spawn = (cmd, args = [], opts = {}) => {
174
195
  const child = nodeSpawn(cmd, args, opts);
196
+ let resolveSignal;
197
+ const signal = new Promise((r) => {
198
+ resolveSignal = r;
199
+ });
200
+ let resolveExit;
201
+ const exitCode = new Promise((r) => {
202
+ resolveExit = r;
203
+ });
204
+ child.on("close", (code, sig) => {
205
+ resolveSignal(sig ?? null);
206
+ resolveExit(code ?? 0);
207
+ });
208
+ // A spawn failure (e.g. ENOENT for a missing binary) emits an `error`
209
+ // event. With no listener Node rethrows it as an uncaughtException and
210
+ // crashes the whole process — even for callers that synchronously guard
211
+ // `pid === undefined`, because the event fires on a later tick. Mirror the
212
+ // run()/runSync() contract instead: resolve a 127 exit code and a null
213
+ // signal so the surface never rejects and never crashes. `resolveExit` is
214
+ // idempotent, so a `close` arriving after `error` is a no-op.
215
+ child.on("error", () => {
216
+ resolveSignal(null);
217
+ resolveExit(127);
218
+ });
175
219
  return {
176
220
  stdout: child.stdout ?? emptyAsyncIterable(),
177
221
  stderr: child.stderr ?? emptyAsyncIterable(),
178
- exitCode: new Promise((resolve) => {
179
- child.on("close", (code) => resolve(code ?? 0));
180
- }),
222
+ // The child's writable stdin — present only when `opts.stdio` makes
223
+ // stdin a pipe (e.g. `["pipe", ...]`); `null` otherwise. A supervising
224
+ // caller writes its piped output into it.
225
+ stdin: child.stdin ?? null,
226
+ exitCode,
227
+ // Resolves with the terminating signal name (or `null` on a clean exit),
228
+ // alongside `exitCode`. Supervisors that distinguish a SIGTERM teardown
229
+ // from a crash read it; clean-exit callers can ignore it.
230
+ signal,
181
231
  kill: (signal) => child.kill(signal),
232
+ // The child's pid — `undefined` if the spawn failed. Detached callers
233
+ // read it to derive the process-group id for group teardown.
234
+ pid: child.pid,
182
235
  };
183
236
  };
184
237
 
@@ -215,7 +268,14 @@ function emptyAsyncIterable() {
215
268
  * @returns {Readonly<Runtime>}
216
269
  */
217
270
  export function createDefaultRuntime({ env = process.env } = {}) {
218
- const fs = nodeFs;
271
+ // The async fs surface is `node:fs/promises` augmented with the two stream
272
+ // factories (which only exist on `node:fs`), so streaming consumers never
273
+ // import `node:fs` directly.
274
+ const fs = {
275
+ ...nodeFs,
276
+ createReadStream: nodeCreateReadStream,
277
+ createWriteStream: nodeCreateWriteStream,
278
+ };
219
279
  const fsSync = nodeFsSync;
220
280
  const proc = createDefaultProc({ source: process, env });
221
281
  const clock = createDefaultClock();