omkit 0.0.1 → 0.0.2
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/dist/actions/prompt.js +5 -1
- package/dist/actions/tail-log.js +4 -1
- package/dist/core/action.d.ts +13 -0
- package/dist/core/action.js +22 -0
- package/dist/core/folder-cache.d.ts +36 -0
- package/dist/core/folder-cache.js +90 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/package.json +1 -1
package/dist/actions/prompt.js
CHANGED
|
@@ -74,7 +74,11 @@ export const prompt = action("Prompt")
|
|
|
74
74
|
via = "input";
|
|
75
75
|
}
|
|
76
76
|
else {
|
|
77
|
-
logs.append({
|
|
77
|
+
logs.append({
|
|
78
|
+
source: "Prompt",
|
|
79
|
+
level: "info",
|
|
80
|
+
message: `invalid answer "${raw}", using default`,
|
|
81
|
+
});
|
|
78
82
|
via = "default";
|
|
79
83
|
}
|
|
80
84
|
}
|
package/dist/actions/tail-log.js
CHANGED
|
@@ -20,7 +20,7 @@ export const tailLog = action("Tail Log").run(async ({ logs, signal }, target, o
|
|
|
20
20
|
const file = resolve(target);
|
|
21
21
|
const dir = dirname(file);
|
|
22
22
|
const name = basename(file);
|
|
23
|
-
const { source = name, level = "info", fromStart = false, encoding = "utf8", debounceMs = 50 } = opts;
|
|
23
|
+
const { source = name, level = "info", fromStart = false, encoding = "utf8", debounceMs = 50, } = opts;
|
|
24
24
|
let pos = 0; // bytes consumed so far
|
|
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.
|
|
@@ -99,6 +99,9 @@ export const tailLog = action("Tail Log").run(async ({ logs, signal }, target, o
|
|
|
99
99
|
catch {
|
|
100
100
|
pos = 0;
|
|
101
101
|
}
|
|
102
|
+
// Announce the target up front so the combined log shows which file these
|
|
103
|
+
// lines come from — the resolved absolute path, plus whether we're replaying.
|
|
104
|
+
logs.append({ source, level, message: `tailing ${file}${fromStart ? " (from start)" : ""}` });
|
|
102
105
|
if (fromStart)
|
|
103
106
|
pump();
|
|
104
107
|
const watcher = fsWatch(dir, (_event, filename) => {
|
package/dist/core/action.d.ts
CHANGED
|
@@ -62,6 +62,19 @@ export interface ActionInstance<Result = unknown, Events extends object = NoEven
|
|
|
62
62
|
* subscriptions; `once` is the awaitable, one-value form.
|
|
63
63
|
*/
|
|
64
64
|
once<K extends keyof InstanceEvents<Events, Result>>(key: K): Promise<InstanceEvents<Events, Result>[K]>;
|
|
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.
|
|
69
|
+
* Every path must be **absolute** (throws otherwise). A cache hit resolves
|
|
70
|
+
* immediately as success (result `undefined`) and logs a `cached, skipping`
|
|
71
|
+
* line; a failure of the inner action records nothing, so it re-runs next time.
|
|
72
|
+
*
|
|
73
|
+
* 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))`.
|
|
76
|
+
*/
|
|
77
|
+
withCache(...folders: string[]): AnyActionInstance;
|
|
65
78
|
}
|
|
66
79
|
/**
|
|
67
80
|
* Any constructed instance, whatever its result, events, or handle. The launch
|
package/dist/core/action.js
CHANGED
|
@@ -1,5 +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
4
|
/** A promise paired with its settlers, for the set-once handle. */
|
|
4
5
|
const deferred = () => {
|
|
5
6
|
let resolve;
|
|
@@ -103,6 +104,27 @@ const build = (name, exec) => {
|
|
|
103
104
|
once(key) {
|
|
104
105
|
return awaitEvent(emitter, name, key);
|
|
105
106
|
},
|
|
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
|
|
109
|
+
// runs+records. The wrapper keeps this action's `name`, so log lines and
|
|
110
|
+
// the `launch <name>` line are unchanged. It forwards its injected
|
|
111
|
+
// logs/signal straight into the inner action's `start`.
|
|
112
|
+
withCache(...folders) {
|
|
113
|
+
// Validate + canonicalize eagerly here (at the call site), not lazily
|
|
114
|
+
// inside the run — a relative path fails fast where it's written, and
|
|
115
|
+
// the resolved paths give a stable cache key regardless of spelling.
|
|
116
|
+
const dirs = resolveFolders(folders);
|
|
117
|
+
return action(name).run(async (ctx) => {
|
|
118
|
+
const fp = await fingerprint(dirs);
|
|
119
|
+
if ((await read(dirs)) === fp) {
|
|
120
|
+
ctx.logs.append({ source: name, level: "info", message: "cached, skipping" });
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
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
|
|
125
|
+
return result;
|
|
126
|
+
})();
|
|
127
|
+
},
|
|
106
128
|
};
|
|
107
129
|
return instance;
|
|
108
130
|
};
|
|
@@ -0,0 +1,36 @@
|
|
|
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.
|
|
6
|
+
*
|
|
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.
|
|
9
|
+
*/
|
|
10
|
+
/** Where cache entries are written — `node_modules/.cache/omkit/` under the cwd. */
|
|
11
|
+
export declare const cacheDir: string;
|
|
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.
|
|
18
|
+
*/
|
|
19
|
+
export declare const resolveFolders: (folders: string[]) => string[];
|
|
20
|
+
/**
|
|
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.
|
|
25
|
+
*/
|
|
26
|
+
export declare const fingerprint: (folders: string[]) => Promise<string>;
|
|
27
|
+
/**
|
|
28
|
+
* Reads the fingerprint stored for these folders, or `undefined` when there's
|
|
29
|
+
* no cache (absent, unreadable, or malformed — all treated as a miss).
|
|
30
|
+
*/
|
|
31
|
+
export declare const read: (folders: string[]) => Promise<string | undefined>;
|
|
32
|
+
/**
|
|
33
|
+
* Records the fingerprint for these folders after a successful run. Writes
|
|
34
|
+
* atomically (temp file + rename) so a concurrent read never sees a half-file.
|
|
35
|
+
*/
|
|
36
|
+
export declare const write: (folders: string[], fp: string) => Promise<void>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
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.
|
|
9
|
+
*
|
|
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.
|
|
12
|
+
*/
|
|
13
|
+
/** Where cache entries are written — `node_modules/.cache/omkit/` under the cwd. */
|
|
14
|
+
export const cacheDir = join(process.cwd(), "node_modules", ".cache", "omkit");
|
|
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.
|
|
21
|
+
*/
|
|
22
|
+
export const resolveFolders = (folders) => folders.map((folder) => {
|
|
23
|
+
if (!isAbsolute(folder)) {
|
|
24
|
+
throw new Error(`withCache: folder path must be absolute, got "${folder}"`);
|
|
25
|
+
}
|
|
26
|
+
return resolve(folder);
|
|
27
|
+
});
|
|
28
|
+
/**
|
|
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.
|
|
33
|
+
*/
|
|
34
|
+
export const fingerprint = async (folders) => {
|
|
35
|
+
const dirs = resolveFolders(folders);
|
|
36
|
+
const entries = [];
|
|
37
|
+
for (const folder of dirs) {
|
|
38
|
+
let rels;
|
|
39
|
+
try {
|
|
40
|
+
rels = await readdir(folder, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
continue; // folder missing/unreadable — contributes no entries
|
|
44
|
+
}
|
|
45
|
+
for (const rel of rels) {
|
|
46
|
+
const abs = join(folder, rel);
|
|
47
|
+
try {
|
|
48
|
+
const stats = await stat(abs);
|
|
49
|
+
if (stats.isFile())
|
|
50
|
+
entries.push(`${folder}\0${rel}\0${stats.mtimeMs}\0${stats.size}`);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// entry vanished between readdir and stat — skip
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
entries.sort();
|
|
58
|
+
return createHash("sha1").update(entries.join("\n")).digest("hex");
|
|
59
|
+
};
|
|
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");
|
|
63
|
+
return join(cacheDir, `${key}.json`);
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Reads the fingerprint stored for these folders, or `undefined` when there's
|
|
67
|
+
* no cache (absent, unreadable, or malformed — all treated as a miss).
|
|
68
|
+
*/
|
|
69
|
+
export const read = async (folders) => {
|
|
70
|
+
try {
|
|
71
|
+
const raw = await readFile(entryPath(folders), "utf8");
|
|
72
|
+
const entry = JSON.parse(raw);
|
|
73
|
+
return typeof entry.fingerprint === "string" ? entry.fingerprint : undefined;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Records the fingerprint for these folders after a successful run. Writes
|
|
81
|
+
* atomically (temp file + rename) so a concurrent read never sees a half-file.
|
|
82
|
+
*/
|
|
83
|
+
export const write = async (folders, fp) => {
|
|
84
|
+
await mkdir(cacheDir, { recursive: true });
|
|
85
|
+
const target = entryPath(folders);
|
|
86
|
+
const entry = { folders, fingerprint: fp, ts: Date.now() };
|
|
87
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
88
|
+
await writeFile(tmp, JSON.stringify(entry, null, 2));
|
|
89
|
+
await rename(tmp, target);
|
|
90
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -23,3 +23,4 @@ export type { Logger, LogEntry, LogInput, SubscribeOptions, } from "./core/log-c
|
|
|
23
23
|
export { log } from "./core/log-collector/global.ts";
|
|
24
24
|
export { snapshot, captureSnapshot } from "./core/output.ts";
|
|
25
25
|
export type { SnapshotRef } from "./core/output.ts";
|
|
26
|
+
export { fingerprint, cacheDir } from "./core/folder-cache.ts";
|
package/dist/index.js
CHANGED
|
@@ -23,3 +23,7 @@ export { LogsCollector } from "./core/log-collector/LogsCollector.js";
|
|
|
23
23
|
export { log } from "./core/log-collector/global.js";
|
|
24
24
|
// ── Snapshots ──────────────────────────────────────────────────────────────
|
|
25
25
|
export { snapshot, captureSnapshot } from "./core/output.js";
|
|
26
|
+
// ── Folder cache ─────────────────────────────────────────────────────────────
|
|
27
|
+
// Backs the `.withCache(...folders)` method on instances; exported for scripts
|
|
28
|
+
// that want to inspect or manually invalidate the cache.
|
|
29
|
+
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
|
+
"version": "0.0.2",
|
|
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",
|