omkit 0.0.3 → 0.0.5

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.
@@ -8,6 +8,12 @@ export interface CommandOptions {
8
8
  cwd?: string;
9
9
  /** Label the child's output is logged under. Defaults to the action's name. */
10
10
  label?: string;
11
+ /**
12
+ * Environment for the child. Pass the full set you want (e.g.
13
+ * `{ ...process.env, NODE_ENV: "development" }`) — it's handed to the child
14
+ * as-is, not merged. Omit to inherit the parent's `process.env`.
15
+ */
16
+ env?: NodeJS.ProcessEnv;
11
17
  }
12
18
  /**
13
19
  * Runs a fixed command as an action **instance** — built in a single call, the
@@ -22,7 +22,7 @@ export function command(name, cmdOrFile, argsOrOptions, maybeOptions) {
22
22
  // against process.cwd() — exactly the "absolute from where node runs" rule.
23
23
  const cwd = path.resolve(options.cwd ?? ".");
24
24
  // proc sources the LogsCollector from async context and streams output into it.
25
- return proc(options.label ?? name, { cwd })(verbatim(cmdOrFile));
25
+ return proc(options.label ?? name, { cwd, ...(options.env ? { env: options.env } : {}) })(verbatim(cmdOrFile));
26
26
  })(); // construct the instance here, so callers get one in a single call
27
27
  }
28
28
  /**
@@ -36,7 +36,8 @@ const spawnDirect = (logs, signal, name, file, args, options) => {
36
36
  const cwd = path.resolve(options.cwd ?? ".");
37
37
  const source = options.label ?? name;
38
38
  // windowsHide keeps a stray console window from flashing for the child.
39
- const child = spawn(file, args, { cwd, windowsHide: true });
39
+ // env omitted -> spawn inherits the parent's process.env (Node's default).
40
+ const child = spawn(file, args, { cwd, windowsHide: true, env: options.env });
40
41
  // Mirror proc's launch line: a filterable milestone on the timeline, with the
41
42
  // structured args snapshotted and linked (see process.ts for the rationale).
42
43
  const snap = captureSnapshot(`proc-${name}-start`, { cmd: file, args, cwd });
@@ -25,7 +25,13 @@ export const tailLog = action("Tail Log").run(async ({ logs, signal }, target, o
25
25
  let pending = ""; // partial trailing line, buffered until its newline
26
26
  // Append every complete line in `text`, keeping any remainder for next time.
27
27
  const feed = (text) => {
28
- pending += text;
28
+ // Strip NUL bytes first. Log rotators (log4js/streamroller) can pre-allocate
29
+ // or leave a rotated file padded with \0, and a NUL run contains no newline —
30
+ // so without this the whole padded region buffers and flushes as one huge
31
+ // bogus line (the tell-tale "line of blanks/spaces"). NUL is never valid in
32
+ // a text log, so dropping it is safe; a NUL-only stretch collapses to "" and
33
+ // is skipped by the empty-line guard below.
34
+ pending += text.replace(/\0/g, "");
29
35
  let newline;
30
36
  while ((newline = pending.indexOf("\n")) !== -1) {
31
37
  const line = pending.slice(0, newline).replace(/\r$/, "");
@@ -63,18 +63,22 @@ export interface ActionInstance<Result = unknown, Events extends object = NoEven
63
63
  */
64
64
  once<K extends keyof InstanceEvents<Events, Result>>(key: K): Promise<InstanceEvents<Events, Result>[K]>;
65
65
  /**
66
- * Gate this action on a folder fingerprint: returns a new instance that skips
67
- * running when the mtime+size of every file under `folders` matches the last
68
- * successful run, and otherwise runs this action and records the fingerprint.
66
+ * Gate this action on its **inputs** — the files/folders it *reads* (its
67
+ * sources), **not** what it produces. `paths` (each a file or a folder) are
68
+ * fingerprinted by mtime+size; the returned instance skips running when those
69
+ * inputs are unchanged since the last successful run, and otherwise runs this
70
+ * action and records the fingerprint. Pass inputs, not build outputs: caching
71
+ * on an output would skip the very run that produces it.
72
+ *
69
73
  * Every path must be **absolute** (throws otherwise). A cache hit resolves
70
74
  * immediately as success (result `undefined`) and logs a `cached, skipping`
71
75
  * line; a failure of the inner action records nothing, so it re-runs next time.
72
76
  *
73
77
  * Because `nod`/`run` launch an instance the moment they receive it, chain
74
- * `.withCache` *before* handing the instance in:
75
- * `nod(command("build", "…").withCache(distAbs))`.
78
+ * `.withCache` *before* handing the instance in — passing the source dir:
79
+ * `nod(command("build", "npm run build").withCache(srcAbs))`.
76
80
  */
77
- withCache(...folders: string[]): AnyActionInstance;
81
+ withCache(...paths: string[]): AnyActionInstance;
78
82
  }
79
83
  /**
80
84
  * Any constructed instance, whatever its result, events, or handle. The launch
@@ -1,6 +1,6 @@
1
1
  import { events } from "./events.js";
2
2
  import { createProc } from "./process.js";
3
- import { fingerprint, read, write, resolveFolders } from "./folder-cache.js";
3
+ import { fingerprint, read, write, resolvePaths } from "./folder-cache.js";
4
4
  /** A promise paired with its settlers, for the set-once handle. */
5
5
  const deferred = () => {
6
6
  let resolve;
@@ -105,23 +105,23 @@ const build = (name, exec) => {
105
105
  return awaitEvent(emitter, name, key);
106
106
  },
107
107
  // Wrap this instance in a fresh one (reusing the engine for its own
108
- // emitter/ref/on/once) that fingerprints `folders` and either skips or
108
+ // emitter/ref/on/once) that fingerprints `paths` and either skips or
109
109
  // runs+records. The wrapper keeps this action's `name`, so log lines and
110
110
  // the `launch <name>` line are unchanged. It forwards its injected
111
111
  // logs/signal straight into the inner action's `start`.
112
- withCache(...folders) {
112
+ withCache(...paths) {
113
113
  // Validate + canonicalize eagerly here (at the call site), not lazily
114
114
  // inside the run — a relative path fails fast where it's written, and
115
115
  // the resolved paths give a stable cache key regardless of spelling.
116
- const dirs = resolveFolders(folders);
116
+ const targets = resolvePaths(paths);
117
117
  return action(name).run(async (ctx) => {
118
- const fp = await fingerprint(dirs);
119
- if ((await read(dirs)) === fp) {
118
+ const fp = await fingerprint(targets);
119
+ if ((await read(targets)) === fp) {
120
120
  ctx.logs.append({ source: name, level: "info", message: "cached, skipping" });
121
121
  return undefined;
122
122
  }
123
123
  const result = await instance.start({ logs: ctx.logs, signal: ctx.signal });
124
- await write(dirs, fp); // only on success — a throw above skips this
124
+ await write(targets, fp); // only on success — a throw above skips this
125
125
  return result;
126
126
  })();
127
127
  },
@@ -1,36 +1,39 @@
1
1
  /**
2
- * A folder-fingerprint cache: hash the mtime+size of every file under one or
3
- * more folders, so an expensive action can be skipped when nothing it depends
4
- * on has changed since its last successful run. Backs the `.withCache(...)`
5
- * method on action instances.
2
+ * An input-fingerprint cache: hash the mtime+size of one or more **input**
3
+ * targets (the files/folders an action reads — *not* what it produces), so an
4
+ * expensive action can be skipped when its inputs are unchanged since its last
5
+ * successful run. Backs the `.withCache(...)` method on action instances.
6
6
  *
7
7
  * Entries live under `node_modules/.cache/omkit/` (conventional, gitignored) —
8
- * one JSON file per set of folders, named by a hash of their absolute paths.
8
+ * one JSON file per set of inputs, named by a hash of their absolute paths.
9
9
  */
10
10
  /** Where cache entries are written — `node_modules/.cache/omkit/` under the cwd. */
11
11
  export declare const cacheDir: string;
12
12
  /**
13
- * Require every path to be **absolute** (a relative folder is a developer error
14
- * — throws, naming it) and return them canonicalized with `resolve`, so
15
- * different spellings of the same folder (`.`/`..`/trailing slash) collapse to
16
- * one stable fingerprint prefix and cache key. `resolve` on an already-absolute
17
- * path only normalizes — it never falls back to the cwd.
13
+ * Require every path to be **absolute** (a relative path is a developer error —
14
+ * throws, naming it) and return them canonicalized with `resolve`, so different
15
+ * spellings of the same target (`.`/`..`/trailing slash) collapse to one stable
16
+ * fingerprint prefix and cache key. `resolve` on an already-absolute path only
17
+ * normalizes — it never falls back to the cwd. Each target may be a file or a
18
+ * folder (see {@link fingerprint}).
18
19
  */
19
- export declare const resolveFolders: (folders: string[]) => string[];
20
+ export declare const resolvePaths: (paths: string[]) => string[];
20
21
  /**
21
- * Fingerprints one or more folders by their files' mtime+size. Every path must
22
- * be absolute (throws otherwise). A missing folder contributes nothing (so an
23
- * output folder that hasn't been built yet is naturally a miss, not an error).
24
- * The entry list is sorted for determinism before hashing.
22
+ * Fingerprints one or more **input** targets by mtime+size. Each target may be
23
+ * a **file** (fingerprinted directly) or a **folder** (walked recursively, its
24
+ * files fingerprinted). Every path must be absolute (throws otherwise). A
25
+ * missing target contributes nothing — so an input that doesn't exist yet is
26
+ * naturally a miss, not an error. The entry list is sorted for determinism
27
+ * before hashing.
25
28
  */
26
- export declare const fingerprint: (folders: string[]) => Promise<string>;
29
+ export declare const fingerprint: (paths: string[]) => Promise<string>;
27
30
  /**
28
- * Reads the fingerprint stored for these folders, or `undefined` when there's
31
+ * Reads the fingerprint stored for these targets, or `undefined` when there's
29
32
  * no cache (absent, unreadable, or malformed — all treated as a miss).
30
33
  */
31
- export declare const read: (folders: string[]) => Promise<string | undefined>;
34
+ export declare const read: (paths: string[]) => Promise<string | undefined>;
32
35
  /**
33
- * Records the fingerprint for these folders after a successful run. Writes
36
+ * Records the fingerprint for these targets after a successful run. Writes
34
37
  * atomically (temp file + rename) so a concurrent read never sees a half-file.
35
38
  */
36
- export declare const write: (folders: string[], fp: string) => Promise<void>;
39
+ export declare const write: (paths: string[], fp: string) => Promise<void>;
@@ -2,52 +2,71 @@ import { createHash } from "node:crypto";
2
2
  import { mkdir, readdir, readFile, rename, stat, writeFile } from "node:fs/promises";
3
3
  import { isAbsolute, join, resolve } from "node:path";
4
4
  /**
5
- * A folder-fingerprint cache: hash the mtime+size of every file under one or
6
- * more folders, so an expensive action can be skipped when nothing it depends
7
- * on has changed since its last successful run. Backs the `.withCache(...)`
8
- * method on action instances.
5
+ * An input-fingerprint cache: hash the mtime+size of one or more **input**
6
+ * targets (the files/folders an action reads — *not* what it produces), so an
7
+ * expensive action can be skipped when its inputs are unchanged since its last
8
+ * successful run. Backs the `.withCache(...)` method on action instances.
9
9
  *
10
10
  * Entries live under `node_modules/.cache/omkit/` (conventional, gitignored) —
11
- * one JSON file per set of folders, named by a hash of their absolute paths.
11
+ * one JSON file per set of inputs, named by a hash of their absolute paths.
12
12
  */
13
13
  /** Where cache entries are written — `node_modules/.cache/omkit/` under the cwd. */
14
14
  export const cacheDir = join(process.cwd(), "node_modules", ".cache", "omkit");
15
15
  /**
16
- * Require every path to be **absolute** (a relative folder is a developer error
17
- * — throws, naming it) and return them canonicalized with `resolve`, so
18
- * different spellings of the same folder (`.`/`..`/trailing slash) collapse to
19
- * one stable fingerprint prefix and cache key. `resolve` on an already-absolute
20
- * path only normalizes — it never falls back to the cwd.
16
+ * Require every path to be **absolute** (a relative path is a developer error —
17
+ * throws, naming it) and return them canonicalized with `resolve`, so different
18
+ * spellings of the same target (`.`/`..`/trailing slash) collapse to one stable
19
+ * fingerprint prefix and cache key. `resolve` on an already-absolute path only
20
+ * normalizes — it never falls back to the cwd. Each target may be a file or a
21
+ * folder (see {@link fingerprint}).
21
22
  */
22
- export const resolveFolders = (folders) => folders.map((folder) => {
23
- if (!isAbsolute(folder)) {
24
- throw new Error(`withCache: folder path must be absolute, got "${folder}"`);
23
+ export const resolvePaths = (paths) => paths.map((p) => {
24
+ if (!isAbsolute(p)) {
25
+ throw new Error(`withCache: path must be absolute, got "${p}"`);
25
26
  }
26
- return resolve(folder);
27
+ return resolve(p);
27
28
  });
28
29
  /**
29
- * Fingerprints one or more folders by their files' mtime+size. Every path must
30
- * be absolute (throws otherwise). A missing folder contributes nothing (so an
31
- * output folder that hasn't been built yet is naturally a miss, not an error).
32
- * The entry list is sorted for determinism before hashing.
30
+ * Fingerprints one or more **input** targets by mtime+size. Each target may be
31
+ * a **file** (fingerprinted directly) or a **folder** (walked recursively, its
32
+ * files fingerprinted). Every path must be absolute (throws otherwise). A
33
+ * missing target contributes nothing — so an input that doesn't exist yet is
34
+ * naturally a miss, not an error. The entry list is sorted for determinism
35
+ * before hashing.
33
36
  */
34
- export const fingerprint = async (folders) => {
35
- const dirs = resolveFolders(folders);
37
+ export const fingerprint = async (paths) => {
38
+ const targets = resolvePaths(paths);
36
39
  const entries = [];
37
- for (const folder of dirs) {
40
+ const record = (target, rel, mtimeMs, size) => {
41
+ entries.push(`${target}\0${rel}\0${mtimeMs}\0${size}`);
42
+ };
43
+ for (const target of targets) {
44
+ let root;
45
+ try {
46
+ root = await stat(target);
47
+ }
48
+ catch {
49
+ continue; // missing/unreadable — contributes no entries
50
+ }
51
+ // A file target fingerprints itself (empty rel — there's no walk).
52
+ if (root.isFile()) {
53
+ record(target, "", root.mtimeMs, root.size);
54
+ continue;
55
+ }
56
+ // A folder target: walk it and fingerprint every file inside.
38
57
  let rels;
39
58
  try {
40
- rels = await readdir(folder, { recursive: true });
59
+ rels = await readdir(target, { recursive: true });
41
60
  }
42
61
  catch {
43
- continue; // folder missing/unreadable — contributes no entries
62
+ continue;
44
63
  }
45
64
  for (const rel of rels) {
46
- const abs = join(folder, rel);
65
+ const abs = join(target, rel);
47
66
  try {
48
67
  const stats = await stat(abs);
49
68
  if (stats.isFile())
50
- entries.push(`${folder}\0${rel}\0${stats.mtimeMs}\0${stats.size}`);
69
+ record(target, rel, stats.mtimeMs, stats.size);
51
70
  }
52
71
  catch {
53
72
  // entry vanished between readdir and stat — skip
@@ -57,18 +76,18 @@ export const fingerprint = async (folders) => {
57
76
  entries.sort();
58
77
  return createHash("sha1").update(entries.join("\n")).digest("hex");
59
78
  };
60
- /** The on-disk filename for a set of folders — a hash of their absolute paths. */
61
- const entryPath = (folders) => {
62
- const key = createHash("sha1").update(resolveFolders(folders).sort().join("\0")).digest("hex");
79
+ /** The on-disk filename for a set of targets — a hash of their absolute paths. */
80
+ const entryPath = (paths) => {
81
+ const key = createHash("sha1").update(resolvePaths(paths).sort().join("\0")).digest("hex");
63
82
  return join(cacheDir, `${key}.json`);
64
83
  };
65
84
  /**
66
- * Reads the fingerprint stored for these folders, or `undefined` when there's
85
+ * Reads the fingerprint stored for these targets, or `undefined` when there's
67
86
  * no cache (absent, unreadable, or malformed — all treated as a miss).
68
87
  */
69
- export const read = async (folders) => {
88
+ export const read = async (paths) => {
70
89
  try {
71
- const raw = await readFile(entryPath(folders), "utf8");
90
+ const raw = await readFile(entryPath(paths), "utf8");
72
91
  const entry = JSON.parse(raw);
73
92
  return typeof entry.fingerprint === "string" ? entry.fingerprint : undefined;
74
93
  }
@@ -77,13 +96,13 @@ export const read = async (folders) => {
77
96
  }
78
97
  };
79
98
  /**
80
- * Records the fingerprint for these folders after a successful run. Writes
99
+ * Records the fingerprint for these targets after a successful run. Writes
81
100
  * atomically (temp file + rename) so a concurrent read never sees a half-file.
82
101
  */
83
- export const write = async (folders, fp) => {
102
+ export const write = async (paths, fp) => {
84
103
  await mkdir(cacheDir, { recursive: true });
85
- const target = entryPath(folders);
86
- const entry = { folders, fingerprint: fp, ts: Date.now() };
104
+ const target = entryPath(paths);
105
+ const entry = { folders: paths, fingerprint: fp, ts: Date.now() };
87
106
  const tmp = `${target}.${process.pid}.tmp`;
88
107
  await writeFile(tmp, JSON.stringify(entry, null, 2));
89
108
  await rename(tmp, target);
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ export { log } from "./core/log-collector/global.js";
24
24
  // ── Snapshots ──────────────────────────────────────────────────────────────
25
25
  export { snapshot, captureSnapshot } from "./core/output.js";
26
26
  // ── Folder cache ─────────────────────────────────────────────────────────────
27
- // Backs the `.withCache(...folders)` method on instances; exported for scripts
28
- // that want to inspect or manually invalidate the cache.
27
+ // Backs the `.withCache(...inputs)` method on instances (fingerprints an
28
+ // action's input files/folders, not its outputs); exported for scripts that
29
+ // want to inspect or manually invalidate the cache.
29
30
  export { fingerprint, cacheDir } from "./core/folder-cache.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omkit",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "A tiny runtime for developer workflows: typed actions that pass runtime capabilities and emit typed events, under one run and one shared log. The operational/runtime layer to tskb's knowledge layer.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",