@agent-workspace/utils 0.7.0 → 0.8.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/frontmatter.d.ts.map +1 -1
- package/dist/frontmatter.js +3 -2
- package/dist/frontmatter.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/safe-io.d.ts +47 -0
- package/dist/safe-io.d.ts.map +1 -0
- package/dist/safe-io.js +227 -0
- package/dist/safe-io.js.map +1 -0
- package/dist/safe-io.test.d.ts +2 -0
- package/dist/safe-io.test.d.ts.map +1 -0
- package/dist/safe-io.test.js +196 -0
- package/dist/safe-io.test.js.map +1 -0
- package/dist/validation.d.ts +2 -0
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +2 -0
- package/dist/validation.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"frontmatter.d.ts","sourceRoot":"","sources":["../src/frontmatter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"frontmatter.d.ts","sourceRoot":"","sources":["../src/frontmatter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAG5E;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CAAC,CAAC,SAAS,eAAe,EAChE,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAQ3B;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,SAAS,eAAe,EAAE,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,MAAM,CAEhG;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,CAAC,SAAS,eAAe,EAChE,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC,GACrB,OAAO,CAAC,IAAI,CAAC,CAGf;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,GAAG,SAAS,CAEpF"}
|
package/dist/frontmatter.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { readFile
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
2
|
import matter from "gray-matter";
|
|
3
|
+
import { atomicWriteFile } from "./safe-io.js";
|
|
3
4
|
/**
|
|
4
5
|
* Parse an AWP workspace file (Markdown with YAML frontmatter).
|
|
5
6
|
*
|
|
@@ -32,7 +33,7 @@ export function serializeWorkspaceFile(file) {
|
|
|
32
33
|
*/
|
|
33
34
|
export async function writeWorkspaceFile(file) {
|
|
34
35
|
const content = serializeWorkspaceFile(file);
|
|
35
|
-
await
|
|
36
|
+
await atomicWriteFile(file.filePath, content);
|
|
36
37
|
}
|
|
37
38
|
/**
|
|
38
39
|
* Extract the type from frontmatter data.
|
package/dist/frontmatter.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"frontmatter.js","sourceRoot":"","sources":["../src/frontmatter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,
|
|
1
|
+
{"version":3,"file":"frontmatter.js","sourceRoot":"","sources":["../src/frontmatter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,MAAM,MAAM,aAAa,CAAC;AAEjC,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,QAAgB;IAEhB,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAC9C,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IACtC,OAAO;QACL,WAAW,EAAE,IAAS;QACtB,IAAI,EAAE,OAAO;QACb,QAAQ;KACT,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CAA4B,IAAsB;IACtF,OAAO,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;AACvD,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,IAAsB;IAEtB,MAAM,OAAO,GAAG,sBAAsB,CAAC,IAAI,CAAC,CAAC;IAC7C,MAAM,eAAe,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;AAChD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAA6B;IAC9D,OAAO,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;AAC/D,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -9,5 +9,6 @@ export { computeConfidence, computeDecayedScore, updateDimension, computeWeighte
|
|
|
9
9
|
export { parseWorkspaceFile, serializeWorkspaceFile, writeWorkspaceFile, getFrontmatterType, } from "./frontmatter.js";
|
|
10
10
|
export { findWorkspaceRoot, loadManifest, fileExists, getAgentDid, safeReadFile, getWorkspaceRoot, } from "./workspace.js";
|
|
11
11
|
export { type TaskNode, type DependencyGraph, type GraphAnalysis, buildGraph, topologicalSort, detectCycles, findCriticalPath, getBlockedTasks, analyzeGraph, getTaskSlug, getProjectSlug, } from "./graph.js";
|
|
12
|
+
export { atomicWriteFile, withFileLock, safeWriteJson, loadJsonFile, type AtomicWriteOptions, type FileLockOptions, } from "./safe-io.js";
|
|
12
13
|
export { type RecruitmentCandidate, type RecruitmentResult, findCandidatesForRole, autoRecruitSwarm, isSwarmFullyStaffed, getSwarmStaffingSummary, } from "./swarm.js";
|
|
13
14
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,gBAAgB,GACjB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,eAAe,EACf,oBAAoB,GACrB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EACL,kBAAkB,EAClB,sBAAsB,EACtB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,UAAU,EACV,WAAW,EACX,YAAY,EACZ,gBAAgB,GACjB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,UAAU,EACV,eAAe,EACf,YAAY,EACZ,gBAAgB,EAChB,eAAe,EACf,YAAY,EACZ,WAAW,EACX,cAAc,GACf,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EACtB,qBAAqB,EACrB,gBAAgB,EAChB,mBAAmB,EACnB,uBAAuB,GACxB,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,gBAAgB,GACjB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,eAAe,EACf,oBAAoB,GACrB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EACL,kBAAkB,EAClB,sBAAsB,EACtB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,UAAU,EACV,WAAW,EACX,YAAY,EACZ,gBAAgB,GACjB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,UAAU,EACV,eAAe,EACf,YAAY,EACZ,gBAAgB,EAChB,eAAe,EACf,YAAY,EACZ,WAAW,EACX,cAAc,GACf,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,eAAe,EACf,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,KAAK,kBAAkB,EACvB,KAAK,eAAe,GACrB,MAAM,cAAc,CAAC;AAGtB,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EACtB,qBAAqB,EACrB,gBAAgB,EAChB,mBAAmB,EACnB,uBAAuB,GACxB,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,8 @@ export { parseWorkspaceFile, serializeWorkspaceFile, writeWorkspaceFile, getFron
|
|
|
14
14
|
export { findWorkspaceRoot, loadManifest, fileExists, getAgentDid, safeReadFile, getWorkspaceRoot, } from "./workspace.js";
|
|
15
15
|
// Graph utilities (dependency analysis)
|
|
16
16
|
export { buildGraph, topologicalSort, detectCycles, findCriticalPath, getBlockedTasks, analyzeGraph, getTaskSlug, getProjectSlug, } from "./graph.js";
|
|
17
|
+
// Safe I/O utilities
|
|
18
|
+
export { atomicWriteFile, withFileLock, safeWriteJson, loadJsonFile, } from "./safe-io.js";
|
|
17
19
|
// Swarm utilities (recruitment)
|
|
18
20
|
export { findCandidatesForRole, autoRecruitSwarm, isSwarmFullyStaffed, getSwarmStaffingSummary, } from "./swarm.js";
|
|
19
21
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,uBAAuB;AACvB,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,gBAAgB,GACjB,MAAM,iBAAiB,CAAC;AAEzB,uBAAuB;AACvB,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,eAAe,EACf,oBAAoB,GACrB,MAAM,iBAAiB,CAAC;AAEzB,wBAAwB;AACxB,OAAO,EACL,kBAAkB,EAClB,sBAAsB,EACtB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAE1B,sBAAsB;AACtB,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,UAAU,EACV,WAAW,EACX,YAAY,EACZ,gBAAgB,GACjB,MAAM,gBAAgB,CAAC;AAExB,wCAAwC;AACxC,OAAO,EAIL,UAAU,EACV,eAAe,EACf,YAAY,EACZ,gBAAgB,EAChB,eAAe,EACf,YAAY,EACZ,WAAW,EACX,cAAc,GACf,MAAM,YAAY,CAAC;AAEpB,gCAAgC;AAChC,OAAO,EAGL,qBAAqB,EACrB,gBAAgB,EAChB,mBAAmB,EACnB,uBAAuB,GACxB,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,uBAAuB;AACvB,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,gBAAgB,GACjB,MAAM,iBAAiB,CAAC;AAEzB,uBAAuB;AACvB,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,eAAe,EACf,oBAAoB,GACrB,MAAM,iBAAiB,CAAC;AAEzB,wBAAwB;AACxB,OAAO,EACL,kBAAkB,EAClB,sBAAsB,EACtB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAE1B,sBAAsB;AACtB,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,UAAU,EACV,WAAW,EACX,YAAY,EACZ,gBAAgB,GACjB,MAAM,gBAAgB,CAAC;AAExB,wCAAwC;AACxC,OAAO,EAIL,UAAU,EACV,eAAe,EACf,YAAY,EACZ,gBAAgB,EAChB,eAAe,EACf,YAAY,EACZ,WAAW,EACX,cAAc,GACf,MAAM,YAAY,CAAC;AAEpB,qBAAqB;AACrB,OAAO,EACL,eAAe,EACf,YAAY,EACZ,aAAa,EACb,YAAY,GAGb,MAAM,cAAc,CAAC;AAEtB,gCAAgC;AAChC,OAAO,EAGL,qBAAqB,EACrB,gBAAgB,EAChB,mBAAmB,EACnB,uBAAuB,GACxB,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe file I/O utilities inspired by clawdbot.
|
|
3
|
+
*
|
|
4
|
+
* Provides atomic writes (temp file + rename), file locking with stale
|
|
5
|
+
* detection, backup rotation, and safe JSON helpers.
|
|
6
|
+
*/
|
|
7
|
+
export interface AtomicWriteOptions {
|
|
8
|
+
/** Enable backup rotation on overwrite (default: true) */
|
|
9
|
+
backup?: boolean;
|
|
10
|
+
/** Maximum number of backup copies to keep (default: 5) */
|
|
11
|
+
maxBackups?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface FileLockOptions {
|
|
14
|
+
/** Lock acquisition timeout in milliseconds (default: 10_000) */
|
|
15
|
+
timeout?: number;
|
|
16
|
+
/** Age in milliseconds after which a lock is considered stale (default: 1_800_000 = 30min) */
|
|
17
|
+
staleThreshold?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Write a file atomically using temp file + rename.
|
|
21
|
+
*
|
|
22
|
+
* - Writes content to a uniquely-named temp file
|
|
23
|
+
* - Backs up the existing file with rotation (if it exists)
|
|
24
|
+
* - Atomically renames the temp file to the target
|
|
25
|
+
* - Falls back to copyFile on cross-device or Windows errors
|
|
26
|
+
* - Cleans up the temp file on failure
|
|
27
|
+
*/
|
|
28
|
+
export declare function atomicWriteFile(filePath: string, content: string, options?: AtomicWriteOptions): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Execute a function while holding an exclusive file lock.
|
|
31
|
+
*
|
|
32
|
+
* - Uses `fs.open(lockPath, "wx")` for exclusive creation
|
|
33
|
+
* - Supports nested lock counting (same process, same file)
|
|
34
|
+
* - Detects stale locks via timestamp age and PID liveness
|
|
35
|
+
* - Retries with exponential backoff up to timeout
|
|
36
|
+
* - Always releases lock in finally block
|
|
37
|
+
*/
|
|
38
|
+
export declare function withFileLock<T>(filePath: string, fn: () => Promise<T>, options?: FileLockOptions): Promise<T>;
|
|
39
|
+
/**
|
|
40
|
+
* Write a JSON file atomically with directory creation.
|
|
41
|
+
*/
|
|
42
|
+
export declare function safeWriteJson(filePath: string, data: unknown, options?: AtomicWriteOptions): Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* Load and parse a JSON file, returning undefined if missing or corrupt.
|
|
45
|
+
*/
|
|
46
|
+
export declare function loadJsonFile<T = unknown>(filePath: string): Promise<T | undefined>;
|
|
47
|
+
//# sourceMappingURL=safe-io.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safe-io.d.ts","sourceRoot":"","sources":["../src/safe-io.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAWH,MAAM,WAAW,kBAAkB;IACjC,0DAA0D;IAC1D,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,2DAA2D;IAC3D,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,iEAAiE;IACjE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8FAA8F;IAC9F,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAwED;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,kBAAkB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAqCf;AAMD;;;;;;;;GAQG;AACH,wBAAsB,YAAY,CAAC,CAAC,EAClC,QAAQ,EAAE,MAAM,EAChB,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,OAAO,CAAC,EAAE,eAAe,GACxB,OAAO,CAAC,CAAC,CAAC,CA2FZ;AAMD;;GAEG;AACH,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,OAAO,EACb,OAAO,CAAC,EAAE,kBAAkB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAGf;AAMD;;GAEG;AACH,wBAAsB,YAAY,CAAC,CAAC,GAAG,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAWxF"}
|
package/dist/safe-io.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe file I/O utilities inspired by clawdbot.
|
|
3
|
+
*
|
|
4
|
+
* Provides atomic writes (temp file + rename), file locking with stale
|
|
5
|
+
* detection, backup rotation, and safe JSON helpers.
|
|
6
|
+
*/
|
|
7
|
+
import { mkdir, writeFile, readFile, rename, copyFile, unlink, open, rm } from "node:fs/promises";
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { dirname, basename, join, resolve } from "node:path";
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
11
|
+
const HELD_LOCKS = new Map();
|
|
12
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
// Helpers
|
|
14
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
15
|
+
/**
|
|
16
|
+
* Check if a process is still alive via signal 0.
|
|
17
|
+
*/
|
|
18
|
+
function isProcessAlive(pid) {
|
|
19
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
20
|
+
return false;
|
|
21
|
+
try {
|
|
22
|
+
process.kill(pid, 0);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Read and parse lock file payload.
|
|
31
|
+
*/
|
|
32
|
+
async function readLockPayload(lockPath) {
|
|
33
|
+
try {
|
|
34
|
+
const raw = await readFile(lockPath, "utf-8");
|
|
35
|
+
const parsed = JSON.parse(raw);
|
|
36
|
+
if (typeof parsed.pid !== "number" || typeof parsed.createdAt !== "string")
|
|
37
|
+
return null;
|
|
38
|
+
return { pid: parsed.pid, createdAt: parsed.createdAt };
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Rotate backup files: .bak → .bak.1, .bak.1 → .bak.2, etc.
|
|
46
|
+
* Deletes the oldest backup if at max capacity.
|
|
47
|
+
*/
|
|
48
|
+
async function rotateBackups(filePath, maxBackups) {
|
|
49
|
+
if (maxBackups <= 1)
|
|
50
|
+
return;
|
|
51
|
+
const backupBase = `${filePath}.bak`;
|
|
52
|
+
const maxIndex = maxBackups - 1;
|
|
53
|
+
// Delete oldest
|
|
54
|
+
await unlink(`${backupBase}.${maxIndex}`).catch(() => { });
|
|
55
|
+
// Shift all down
|
|
56
|
+
for (let i = maxIndex - 1; i >= 1; i--) {
|
|
57
|
+
await rename(`${backupBase}.${i}`, `${backupBase}.${i + 1}`).catch(() => { });
|
|
58
|
+
}
|
|
59
|
+
// Move current .bak to .bak.1
|
|
60
|
+
await rename(backupBase, `${backupBase}.1`).catch(() => { });
|
|
61
|
+
}
|
|
62
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
63
|
+
// atomicWriteFile
|
|
64
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
65
|
+
/**
|
|
66
|
+
* Write a file atomically using temp file + rename.
|
|
67
|
+
*
|
|
68
|
+
* - Writes content to a uniquely-named temp file
|
|
69
|
+
* - Backs up the existing file with rotation (if it exists)
|
|
70
|
+
* - Atomically renames the temp file to the target
|
|
71
|
+
* - Falls back to copyFile on cross-device or Windows errors
|
|
72
|
+
* - Cleans up the temp file on failure
|
|
73
|
+
*/
|
|
74
|
+
export async function atomicWriteFile(filePath, content, options) {
|
|
75
|
+
const doBackup = options?.backup !== false;
|
|
76
|
+
const maxBackups = options?.maxBackups ?? 5;
|
|
77
|
+
const dir = dirname(filePath);
|
|
78
|
+
const base = basename(filePath);
|
|
79
|
+
const tmp = join(dir, `${base}.${process.pid}.${randomUUID()}.tmp`);
|
|
80
|
+
// Ensure directory exists
|
|
81
|
+
await mkdir(dir, { recursive: true });
|
|
82
|
+
// Write to temp file
|
|
83
|
+
await writeFile(tmp, content, { encoding: "utf-8" });
|
|
84
|
+
try {
|
|
85
|
+
// Backup existing file
|
|
86
|
+
if (doBackup && existsSync(filePath)) {
|
|
87
|
+
await rotateBackups(filePath, maxBackups);
|
|
88
|
+
await copyFile(filePath, `${filePath}.bak`).catch(() => { });
|
|
89
|
+
}
|
|
90
|
+
// Atomic rename
|
|
91
|
+
await rename(tmp, filePath);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
const code = err.code;
|
|
95
|
+
// Cross-device rename or Windows permission error: fall back to copy
|
|
96
|
+
if (code === "EXDEV" || code === "EPERM" || code === "EEXIST") {
|
|
97
|
+
await copyFile(tmp, filePath);
|
|
98
|
+
await unlink(tmp).catch(() => { });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
// Clean up temp file on unexpected error
|
|
102
|
+
await unlink(tmp).catch(() => { });
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
107
|
+
// withFileLock
|
|
108
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
109
|
+
/**
|
|
110
|
+
* Execute a function while holding an exclusive file lock.
|
|
111
|
+
*
|
|
112
|
+
* - Uses `fs.open(lockPath, "wx")` for exclusive creation
|
|
113
|
+
* - Supports nested lock counting (same process, same file)
|
|
114
|
+
* - Detects stale locks via timestamp age and PID liveness
|
|
115
|
+
* - Retries with exponential backoff up to timeout
|
|
116
|
+
* - Always releases lock in finally block
|
|
117
|
+
*/
|
|
118
|
+
export async function withFileLock(filePath, fn, options) {
|
|
119
|
+
const timeout = options?.timeout ?? 10_000;
|
|
120
|
+
const staleThreshold = options?.staleThreshold ?? 30 * 60 * 1000;
|
|
121
|
+
const normalizedPath = resolve(filePath);
|
|
122
|
+
const lockPath = `${normalizedPath}.lock`;
|
|
123
|
+
// Check for nested lock (same process already holds this lock)
|
|
124
|
+
const held = HELD_LOCKS.get(normalizedPath);
|
|
125
|
+
if (held) {
|
|
126
|
+
held.count++;
|
|
127
|
+
try {
|
|
128
|
+
return await fn();
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
held.count--;
|
|
132
|
+
if (held.count <= 0) {
|
|
133
|
+
HELD_LOCKS.delete(normalizedPath);
|
|
134
|
+
await rm(held.lockPath, { force: true }).catch(() => { });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Acquire new lock
|
|
139
|
+
const startedAt = Date.now();
|
|
140
|
+
let attempt = 0;
|
|
141
|
+
while (Date.now() - startedAt < timeout) {
|
|
142
|
+
attempt++;
|
|
143
|
+
try {
|
|
144
|
+
// Ensure directory exists
|
|
145
|
+
await mkdir(dirname(lockPath), { recursive: true });
|
|
146
|
+
// Exclusive creation — fails with EEXIST if lock already held
|
|
147
|
+
const handle = await open(lockPath, "wx");
|
|
148
|
+
const payload = JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }, null, 2);
|
|
149
|
+
await handle.writeFile(payload, "utf-8");
|
|
150
|
+
await handle.close();
|
|
151
|
+
// Register in-process lock
|
|
152
|
+
HELD_LOCKS.set(normalizedPath, { count: 1, lockPath });
|
|
153
|
+
try {
|
|
154
|
+
return await fn();
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
const current = HELD_LOCKS.get(normalizedPath);
|
|
158
|
+
if (current) {
|
|
159
|
+
current.count--;
|
|
160
|
+
if (current.count <= 0) {
|
|
161
|
+
HELD_LOCKS.delete(normalizedPath);
|
|
162
|
+
await rm(current.lockPath, { force: true }).catch(() => { });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
const code = err.code;
|
|
169
|
+
if (code !== "EEXIST")
|
|
170
|
+
throw err;
|
|
171
|
+
// Lock file exists — check if stale
|
|
172
|
+
const lockPayload = await readLockPayload(lockPath);
|
|
173
|
+
if (lockPayload) {
|
|
174
|
+
const createdAt = Date.parse(lockPayload.createdAt);
|
|
175
|
+
const isStale = !Number.isFinite(createdAt) || Date.now() - createdAt > staleThreshold;
|
|
176
|
+
const isAlive = isProcessAlive(lockPayload.pid);
|
|
177
|
+
if (isStale || !isAlive) {
|
|
178
|
+
// Remove stale lock and retry immediately
|
|
179
|
+
await rm(lockPath, { force: true }).catch(() => { });
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
// Can't read lock file — remove and retry
|
|
185
|
+
await rm(lockPath, { force: true }).catch(() => { });
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
// Lock is held by a live process — wait with backoff
|
|
189
|
+
const delay = Math.min(1000, 50 * attempt);
|
|
190
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Timeout
|
|
194
|
+
const lockPayload = await readLockPayload(lockPath);
|
|
195
|
+
const owner = lockPayload?.pid ? `pid=${lockPayload.pid}` : "unknown";
|
|
196
|
+
throw new Error(`File lock timeout (${timeout}ms): ${owner} holds ${lockPath}`);
|
|
197
|
+
}
|
|
198
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
199
|
+
// safeWriteJson
|
|
200
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
201
|
+
/**
|
|
202
|
+
* Write a JSON file atomically with directory creation.
|
|
203
|
+
*/
|
|
204
|
+
export async function safeWriteJson(filePath, data, options) {
|
|
205
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
206
|
+
await atomicWriteFile(filePath, JSON.stringify(data, null, 2) + "\n", options);
|
|
207
|
+
}
|
|
208
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
209
|
+
// loadJsonFile
|
|
210
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
211
|
+
/**
|
|
212
|
+
* Load and parse a JSON file, returning undefined if missing or corrupt.
|
|
213
|
+
*/
|
|
214
|
+
export async function loadJsonFile(filePath) {
|
|
215
|
+
try {
|
|
216
|
+
const raw = await readFile(filePath, "utf-8");
|
|
217
|
+
return JSON.parse(raw);
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
const code = err.code;
|
|
221
|
+
if (code === "ENOENT" || err instanceof SyntaxError) {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
throw err;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
//# sourceMappingURL=safe-io.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safe-io.js","sourceRoot":"","sources":["../src/safe-io.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safe-io.test.d.ts","sourceRoot":"","sources":["../src/safe-io.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtemp, readFile, writeFile, readdir, rm, mkdir } from "node:fs/promises";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { atomicWriteFile, withFileLock, safeWriteJson, loadJsonFile } from "./safe-io.js";
|
|
7
|
+
let testDir;
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
testDir = await mkdtemp(join(tmpdir(), "awp-safe-io-test-"));
|
|
10
|
+
});
|
|
11
|
+
afterEach(async () => {
|
|
12
|
+
await rm(testDir, { recursive: true, force: true });
|
|
13
|
+
});
|
|
14
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
15
|
+
// atomicWriteFile
|
|
16
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
17
|
+
describe("atomicWriteFile", () => {
|
|
18
|
+
it("writes file content correctly", async () => {
|
|
19
|
+
const filePath = join(testDir, "test.md");
|
|
20
|
+
await atomicWriteFile(filePath, "hello world");
|
|
21
|
+
const content = await readFile(filePath, "utf-8");
|
|
22
|
+
expect(content).toBe("hello world");
|
|
23
|
+
});
|
|
24
|
+
it("creates parent directories", async () => {
|
|
25
|
+
const filePath = join(testDir, "a", "b", "c", "test.md");
|
|
26
|
+
await atomicWriteFile(filePath, "nested");
|
|
27
|
+
const content = await readFile(filePath, "utf-8");
|
|
28
|
+
expect(content).toBe("nested");
|
|
29
|
+
});
|
|
30
|
+
it("creates .bak on overwrite", async () => {
|
|
31
|
+
const filePath = join(testDir, "test.md");
|
|
32
|
+
await writeFile(filePath, "original", "utf-8");
|
|
33
|
+
await atomicWriteFile(filePath, "updated");
|
|
34
|
+
expect(await readFile(filePath, "utf-8")).toBe("updated");
|
|
35
|
+
expect(await readFile(`${filePath}.bak`, "utf-8")).toBe("original");
|
|
36
|
+
});
|
|
37
|
+
it("rotates backups on repeated overwrites", async () => {
|
|
38
|
+
const filePath = join(testDir, "test.md");
|
|
39
|
+
await writeFile(filePath, "v1", "utf-8");
|
|
40
|
+
await atomicWriteFile(filePath, "v2");
|
|
41
|
+
await atomicWriteFile(filePath, "v3");
|
|
42
|
+
await atomicWriteFile(filePath, "v4");
|
|
43
|
+
expect(await readFile(filePath, "utf-8")).toBe("v4");
|
|
44
|
+
expect(await readFile(`${filePath}.bak`, "utf-8")).toBe("v3");
|
|
45
|
+
expect(await readFile(`${filePath}.bak.1`, "utf-8")).toBe("v2");
|
|
46
|
+
expect(await readFile(`${filePath}.bak.2`, "utf-8")).toBe("v1");
|
|
47
|
+
});
|
|
48
|
+
it("does not create backup when backup=false", async () => {
|
|
49
|
+
const filePath = join(testDir, "test.md");
|
|
50
|
+
await writeFile(filePath, "original", "utf-8");
|
|
51
|
+
await atomicWriteFile(filePath, "updated", { backup: false });
|
|
52
|
+
expect(await readFile(filePath, "utf-8")).toBe("updated");
|
|
53
|
+
expect(existsSync(`${filePath}.bak`)).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
it("does not leave temp files on success", async () => {
|
|
56
|
+
const filePath = join(testDir, "test.md");
|
|
57
|
+
await atomicWriteFile(filePath, "content");
|
|
58
|
+
const files = await readdir(testDir);
|
|
59
|
+
expect(files.filter((f) => f.includes(".tmp"))).toHaveLength(0);
|
|
60
|
+
});
|
|
61
|
+
it("respects maxBackups option", async () => {
|
|
62
|
+
const filePath = join(testDir, "test.md");
|
|
63
|
+
await writeFile(filePath, "v1", "utf-8");
|
|
64
|
+
for (let i = 2; i <= 6; i++) {
|
|
65
|
+
await atomicWriteFile(filePath, `v${i}`, { maxBackups: 2 });
|
|
66
|
+
}
|
|
67
|
+
// Should only keep 2 backups: .bak and .bak.1
|
|
68
|
+
expect(existsSync(`${filePath}.bak`)).toBe(true);
|
|
69
|
+
expect(existsSync(`${filePath}.bak.1`)).toBe(true);
|
|
70
|
+
// .bak.2 should not exist (maxBackups=2 means slots: .bak, .bak.1)
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
74
|
+
// withFileLock
|
|
75
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
76
|
+
describe("withFileLock", () => {
|
|
77
|
+
it("acquires lock and runs callback", async () => {
|
|
78
|
+
const filePath = join(testDir, "data.md");
|
|
79
|
+
const result = await withFileLock(filePath, async () => {
|
|
80
|
+
return 42;
|
|
81
|
+
});
|
|
82
|
+
expect(result).toBe(42);
|
|
83
|
+
});
|
|
84
|
+
it("cleans up lock file after callback", async () => {
|
|
85
|
+
const filePath = join(testDir, "data.md");
|
|
86
|
+
await withFileLock(filePath, async () => { });
|
|
87
|
+
const files = await readdir(testDir);
|
|
88
|
+
expect(files.filter((f) => f.includes(".lock"))).toHaveLength(0);
|
|
89
|
+
});
|
|
90
|
+
it("cleans up lock file after callback throws", async () => {
|
|
91
|
+
const filePath = join(testDir, "data.md");
|
|
92
|
+
await expect(withFileLock(filePath, async () => {
|
|
93
|
+
throw new Error("test error");
|
|
94
|
+
})).rejects.toThrow("test error");
|
|
95
|
+
const files = await readdir(testDir);
|
|
96
|
+
expect(files.filter((f) => f.includes(".lock"))).toHaveLength(0);
|
|
97
|
+
});
|
|
98
|
+
it("serializes concurrent access", async () => {
|
|
99
|
+
const filePath = join(testDir, "counter.txt");
|
|
100
|
+
await writeFile(filePath, "0", "utf-8");
|
|
101
|
+
// Run 5 concurrent increments
|
|
102
|
+
const results = await Promise.all(Array.from({ length: 5 }, () => withFileLock(filePath, async () => {
|
|
103
|
+
const val = parseInt(await readFile(filePath, "utf-8"), 10);
|
|
104
|
+
await writeFile(filePath, String(val + 1), "utf-8");
|
|
105
|
+
return val + 1;
|
|
106
|
+
})));
|
|
107
|
+
// All increments should have serialized correctly
|
|
108
|
+
const finalValue = await readFile(filePath, "utf-8");
|
|
109
|
+
expect(parseInt(finalValue, 10)).toBe(5);
|
|
110
|
+
// Results should contain all values 1-5
|
|
111
|
+
expect(results.sort()).toEqual([1, 2, 3, 4, 5]);
|
|
112
|
+
});
|
|
113
|
+
it("supports nested lock counting", async () => {
|
|
114
|
+
const filePath = join(testDir, "data.md");
|
|
115
|
+
const result = await withFileLock(filePath, async () => {
|
|
116
|
+
// Nested lock on same file should not deadlock
|
|
117
|
+
return withFileLock(filePath, async () => {
|
|
118
|
+
return "nested ok";
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
expect(result).toBe("nested ok");
|
|
122
|
+
});
|
|
123
|
+
it("times out if lock cannot be acquired", async () => {
|
|
124
|
+
const filePath = join(testDir, "data.md");
|
|
125
|
+
const lockPath = `${filePath}.lock`;
|
|
126
|
+
// Create a lock file from a "live" process (our own PID)
|
|
127
|
+
await mkdir(testDir, { recursive: true });
|
|
128
|
+
await writeFile(lockPath, JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }), "utf-8");
|
|
129
|
+
// Should timeout since lock is held by a "live" process
|
|
130
|
+
await expect(withFileLock(filePath, async () => { }, { timeout: 200 })).rejects.toThrow(/timeout/i);
|
|
131
|
+
// Clean up the lock file manually
|
|
132
|
+
await rm(lockPath, { force: true });
|
|
133
|
+
});
|
|
134
|
+
it("detects stale locks from dead process", async () => {
|
|
135
|
+
const filePath = join(testDir, "data.md");
|
|
136
|
+
const lockPath = `${filePath}.lock`;
|
|
137
|
+
// Create a lock from a definitely-dead PID
|
|
138
|
+
await mkdir(testDir, { recursive: true });
|
|
139
|
+
await writeFile(lockPath, JSON.stringify({ pid: 999999999, createdAt: new Date().toISOString() }), "utf-8");
|
|
140
|
+
// Should succeed because the PID is dead
|
|
141
|
+
const result = await withFileLock(filePath, async () => "recovered");
|
|
142
|
+
expect(result).toBe("recovered");
|
|
143
|
+
});
|
|
144
|
+
it("detects stale locks by age", async () => {
|
|
145
|
+
const filePath = join(testDir, "data.md");
|
|
146
|
+
const lockPath = `${filePath}.lock`;
|
|
147
|
+
// Create a lock from our own PID but with an old timestamp
|
|
148
|
+
await mkdir(testDir, { recursive: true });
|
|
149
|
+
await writeFile(lockPath, JSON.stringify({
|
|
150
|
+
pid: process.pid,
|
|
151
|
+
createdAt: new Date(Date.now() - 2_000_000).toISOString(),
|
|
152
|
+
}), "utf-8");
|
|
153
|
+
// Should succeed because the lock is stale (older than 30min default)
|
|
154
|
+
const result = await withFileLock(filePath, async () => "recovered");
|
|
155
|
+
expect(result).toBe("recovered");
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
159
|
+
// safeWriteJson
|
|
160
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
161
|
+
describe("safeWriteJson", () => {
|
|
162
|
+
it("writes valid JSON with formatting", async () => {
|
|
163
|
+
const filePath = join(testDir, "data.json");
|
|
164
|
+
await safeWriteJson(filePath, { key: "value", num: 42 });
|
|
165
|
+
const raw = await readFile(filePath, "utf-8");
|
|
166
|
+
expect(raw).toBe('{\n "key": "value",\n "num": 42\n}\n');
|
|
167
|
+
});
|
|
168
|
+
it("creates directories recursively", async () => {
|
|
169
|
+
const filePath = join(testDir, "a", "b", "data.json");
|
|
170
|
+
await safeWriteJson(filePath, { ok: true });
|
|
171
|
+
const data = JSON.parse(await readFile(filePath, "utf-8"));
|
|
172
|
+
expect(data).toEqual({ ok: true });
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
176
|
+
// loadJsonFile
|
|
177
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
178
|
+
describe("loadJsonFile", () => {
|
|
179
|
+
it("loads valid JSON", async () => {
|
|
180
|
+
const filePath = join(testDir, "data.json");
|
|
181
|
+
await writeFile(filePath, '{"key": "value"}', "utf-8");
|
|
182
|
+
const data = await loadJsonFile(filePath);
|
|
183
|
+
expect(data).toEqual({ key: "value" });
|
|
184
|
+
});
|
|
185
|
+
it("returns undefined for missing file", async () => {
|
|
186
|
+
const data = await loadJsonFile(join(testDir, "nonexistent.json"));
|
|
187
|
+
expect(data).toBeUndefined();
|
|
188
|
+
});
|
|
189
|
+
it("returns undefined for corrupt JSON", async () => {
|
|
190
|
+
const filePath = join(testDir, "corrupt.json");
|
|
191
|
+
await writeFile(filePath, "not valid json {{{", "utf-8");
|
|
192
|
+
const data = await loadJsonFile(filePath);
|
|
193
|
+
expect(data).toBeUndefined();
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
//# sourceMappingURL=safe-io.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safe-io.test.js","sourceRoot":"","sources":["../src/safe-io.test.ts"],"names":[],"mappings":"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"}
|
package/dist/validation.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export declare function validatePath(root: string, targetPath: string): string;
|
|
|
28
28
|
*
|
|
29
29
|
* @param dateStr - The date string to validate
|
|
30
30
|
* @returns true if valid, false otherwise
|
|
31
|
+
* @internal Utility function for future validation use
|
|
31
32
|
*/
|
|
32
33
|
export declare function isValidDate(dateStr: string): boolean;
|
|
33
34
|
/**
|
|
@@ -35,6 +36,7 @@ export declare function isValidDate(dateStr: string): boolean;
|
|
|
35
36
|
*
|
|
36
37
|
* @param timestamp - The timestamp string to validate
|
|
37
38
|
* @returns true if valid, false otherwise
|
|
39
|
+
* @internal Utility function for future validation use
|
|
38
40
|
*/
|
|
39
41
|
export declare function isValidTimestamp(timestamp: string): boolean;
|
|
40
42
|
//# sourceMappingURL=validation.d.ts.map
|
package/dist/validation.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAQA;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAElD;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAWjD;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAUrE;AAED
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAQA;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAElD;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAWjD;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAUrE;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAMpD;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAG3D"}
|
package/dist/validation.js
CHANGED
|
@@ -52,6 +52,7 @@ export function validatePath(root, targetPath) {
|
|
|
52
52
|
*
|
|
53
53
|
* @param dateStr - The date string to validate
|
|
54
54
|
* @returns true if valid, false otherwise
|
|
55
|
+
* @internal Utility function for future validation use
|
|
55
56
|
*/
|
|
56
57
|
export function isValidDate(dateStr) {
|
|
57
58
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
|
|
@@ -65,6 +66,7 @@ export function isValidDate(dateStr) {
|
|
|
65
66
|
*
|
|
66
67
|
* @param timestamp - The timestamp string to validate
|
|
67
68
|
* @returns true if valid, false otherwise
|
|
69
|
+
* @internal Utility function for future validation use
|
|
68
70
|
*/
|
|
69
71
|
export function isValidTimestamp(timestamp) {
|
|
70
72
|
const date = new Date(timestamp);
|
package/dist/validation.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.js","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAE1D,8FAA8F;AAC9F,MAAM,YAAY,GAAG,sBAAsB,CAAC;AAE5C,0BAA0B;AAC1B,MAAM,eAAe,GAAG,GAAG,CAAC;AAE5B;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,OAAO,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,IAAI,eAAe,CAAC;AACnE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC1C,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CACb,kBAAkB,IAAI,2EAA2E,CAClG,CAAC;IACJ,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,GAAG,eAAe,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CAAC,sBAAsB,eAAe,aAAa,CAAC,CAAC;IACtE,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,UAAkB;IAC3D,MAAM,UAAU,GAAG,OAAO,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;IAC7C,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;IAEvC,8BAA8B;IAC9B,IAAI,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5C,MAAM,IAAI,KAAK,CAAC,4BAA4B,UAAU,EAAE,CAAC,CAAC;IAC5D,CAAC;IAED,OAAO,UAAU,CAAC;AACpB,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"validation.js","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAE1D,8FAA8F;AAC9F,MAAM,YAAY,GAAG,sBAAsB,CAAC;AAE5C,0BAA0B;AAC1B,MAAM,eAAe,GAAG,GAAG,CAAC;AAE5B;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,OAAO,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,IAAI,eAAe,CAAC;AACnE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC1C,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CACb,kBAAkB,IAAI,2EAA2E,CAClG,CAAC;IACJ,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,GAAG,eAAe,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CAAC,sBAAsB,eAAe,aAAa,CAAC,CAAC;IACtE,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,UAAkB;IAC3D,MAAM,UAAU,GAAG,OAAO,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;IAC7C,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;IAEvC,8BAA8B;IAC9B,IAAI,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5C,MAAM,IAAI,KAAK,CAAC,4BAA4B,UAAU,EAAE,CAAC,CAAC;IAC5D,CAAC;IAED,OAAO,UAAU,CAAC;AACpB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,OAAe;IACzC,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QACzC,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,OAAO,CAAC,CAAC;IAC/B,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;AAChC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,SAAiB;IAChD,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC;IACjC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;AAChC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-workspace/utils",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "Shared utilities for AWP packages — validation, reputation calculations, and frontmatter parsing",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"prepublishOnly": "npm run build"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@agent-workspace/core": "0.
|
|
44
|
+
"@agent-workspace/core": "0.8.2",
|
|
45
45
|
"gray-matter": "^4.0.3"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|