@forwardimpact/libutil 0.1.85 → 0.1.86

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.86",
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,17 +19,25 @@ 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
43
  * `clearTimeout(handle)`.
@@ -35,8 +46,12 @@ import { Finder } from "./finder.js";
35
46
  * exitCode}>` (async, buffered), `runSync(cmd, args, opts) -> {stdout,
36
47
  * stderr, exitCode}` (synchronous, buffered — for the rare caller that
37
48
  * 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.
49
+ * token`), and `spawn(cmd, args, opts) -> {stdout, stderr, stdin, exitCode,
50
+ * signal, kill, pid}` where `stdout`/`stderr` are AsyncIterables,
51
+ * `exitCode`/`signal` are Promises (the terminating signal name or `null`),
52
+ * `stdin` is the child's writable (only when `opts.stdio` pipes stdin, else
53
+ * `null`), `kill(signal)` signals the child, and `pid` is its id (`undefined`
54
+ * on spawn failure).
40
55
  * @property {Object} finder
41
56
  * A constructed `Finder` (project path resolution + symlink management).
42
57
  */
@@ -81,6 +96,9 @@ export function createDefaultProc({ source = process, env = source.env } = {}) {
81
96
  stderr: { write: (s) => source.stderr.write(s) },
82
97
  exit: (code) => source.exit(code),
83
98
  kill: (pid, signal) => source.kill(pid, signal),
99
+ pid: source.pid,
100
+ platform: source.platform,
101
+ on: (event, handler) => source.on(event, handler),
84
102
  };
85
103
  Object.defineProperty(proc, "exitCode", {
86
104
  enumerable: true,
@@ -172,13 +190,45 @@ export function createDefaultSubprocess() {
172
190
 
173
191
  const spawn = (cmd, args = [], opts = {}) => {
174
192
  const child = nodeSpawn(cmd, args, opts);
193
+ let resolveSignal;
194
+ const signal = new Promise((r) => {
195
+ resolveSignal = r;
196
+ });
197
+ let resolveExit;
198
+ const exitCode = new Promise((r) => {
199
+ resolveExit = r;
200
+ });
201
+ child.on("close", (code, sig) => {
202
+ resolveSignal(sig ?? null);
203
+ resolveExit(code ?? 0);
204
+ });
205
+ // A spawn failure (e.g. ENOENT for a missing binary) emits an `error`
206
+ // event. With no listener Node rethrows it as an uncaughtException and
207
+ // crashes the whole process — even for callers that synchronously guard
208
+ // `pid === undefined`, because the event fires on a later tick. Mirror the
209
+ // run()/runSync() contract instead: resolve a 127 exit code and a null
210
+ // signal so the surface never rejects and never crashes. `resolveExit` is
211
+ // idempotent, so a `close` arriving after `error` is a no-op.
212
+ child.on("error", () => {
213
+ resolveSignal(null);
214
+ resolveExit(127);
215
+ });
175
216
  return {
176
217
  stdout: child.stdout ?? emptyAsyncIterable(),
177
218
  stderr: child.stderr ?? emptyAsyncIterable(),
178
- exitCode: new Promise((resolve) => {
179
- child.on("close", (code) => resolve(code ?? 0));
180
- }),
219
+ // The child's writable stdin — present only when `opts.stdio` makes
220
+ // stdin a pipe (e.g. `["pipe", ...]`); `null` otherwise. A supervising
221
+ // caller writes its piped output into it.
222
+ stdin: child.stdin ?? null,
223
+ exitCode,
224
+ // Resolves with the terminating signal name (or `null` on a clean exit),
225
+ // alongside `exitCode`. Supervisors that distinguish a SIGTERM teardown
226
+ // from a crash read it; clean-exit callers can ignore it.
227
+ signal,
181
228
  kill: (signal) => child.kill(signal),
229
+ // The child's pid — `undefined` if the spawn failed. Detached callers
230
+ // read it to derive the process-group id for group teardown.
231
+ pid: child.pid,
182
232
  };
183
233
  };
184
234
 
@@ -215,7 +265,14 @@ function emptyAsyncIterable() {
215
265
  * @returns {Readonly<Runtime>}
216
266
  */
217
267
  export function createDefaultRuntime({ env = process.env } = {}) {
218
- const fs = nodeFs;
268
+ // The async fs surface is `node:fs/promises` augmented with the two stream
269
+ // factories (which only exist on `node:fs`), so streaming consumers never
270
+ // import `node:fs` directly.
271
+ const fs = {
272
+ ...nodeFs,
273
+ createReadStream: nodeCreateReadStream,
274
+ createWriteStream: nodeCreateWriteStream,
275
+ };
219
276
  const fsSync = nodeFsSync;
220
277
  const proc = createDefaultProc({ source: process, env });
221
278
  const clock = createDefaultClock();