@joshski/dust 0.1.71 → 0.1.73

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/dust.js CHANGED
@@ -275,7 +275,7 @@ async function loadSettings(cwd, fileSystem) {
275
275
  }
276
276
 
277
277
  // lib/version.ts
278
- var DUST_VERSION = "0.1.71";
278
+ var DUST_VERSION = "0.1.73";
279
279
 
280
280
  // lib/session.ts
281
281
  var DUST_UNATTENDED = "DUST_UNATTENDED";
@@ -0,0 +1,49 @@
1
+ /**
2
+ * In-memory FileSystem emulator for testing and cache-backed use cases.
3
+ *
4
+ * Provides a complete FileSystem + GlobScanner implementation backed by
5
+ * an in-memory file tree, with write tracking for test assertions.
6
+ */
7
+ import type { FileSystem, GlobScanner } from './types';
8
+ export type FileSystemTree = {
9
+ [name: string]: string | FileSystemTree;
10
+ };
11
+ /**
12
+ * Extended file system with write tracking for assertions.
13
+ * Also implements GlobScanner by scanning over known files.
14
+ */
15
+ export interface FileSystemEmulator extends FileSystem, GlobScanner {
16
+ createdDirs: string[];
17
+ writtenFiles: Map<string, string>;
18
+ /** Internal files map - exposed for tests that need to modify file system state */
19
+ files: Map<string, string>;
20
+ /** File permissions set via chmod - maps path to mode */
21
+ permissions: Map<string, number>;
22
+ }
23
+ /**
24
+ * Creates a file system emulator with optional file contents and write tracking.
25
+ * Implements both FileSystem and GlobScanner interfaces - the scan() method
26
+ * iterates over the files the emulator knows about.
27
+ *
28
+ * @param tree - Nested object representing file system hierarchy (paths get '/' prefix)
29
+ * @param flatFiles - Optional record of path→content entries added as-is (no prefix)
30
+ * @returns FileSystemEmulator with tracking for created directories and written files
31
+ *
32
+ * @example
33
+ * // Nested tree (paths become /project/.dust/...)
34
+ * createFileSystemEmulator({
35
+ * project: {
36
+ * '.dust': {
37
+ * principles: { 'my-principle.md': '# My Principle' },
38
+ * ideas: {} // empty directory
39
+ * }
40
+ * }
41
+ * })
42
+ *
43
+ * // Flat files (paths used as-is)
44
+ * createFileSystemEmulator({}, {
45
+ * '.dust/config/audits/security.md': '# Security Audit\n...',
46
+ * '.dust/tasks/audit-security.md': '# Run security audit\n...',
47
+ * })
48
+ */
49
+ export declare function createFileSystemEmulator(tree?: FileSystemTree, flatFiles?: Record<string, string>): FileSystemEmulator;
@@ -0,0 +1,146 @@
1
+ // lib/filesystem/emulator.ts
2
+ function flattenFileSystemTree(tree, basePath = "") {
3
+ const files = new Map;
4
+ const paths = new Set;
5
+ for (const [name, value] of Object.entries(tree)) {
6
+ const fullPath = basePath ? `${basePath}/${name}` : `/${name}`;
7
+ if (typeof value === "string") {
8
+ files.set(fullPath, value);
9
+ paths.add(fullPath);
10
+ } else {
11
+ paths.add(fullPath);
12
+ const nested = flattenFileSystemTree(value, fullPath);
13
+ for (const [path, content] of nested.files) {
14
+ files.set(path, content);
15
+ }
16
+ for (const path of nested.paths) {
17
+ paths.add(path);
18
+ }
19
+ }
20
+ }
21
+ for (const path of [...files.keys(), ...paths]) {
22
+ let dir = path;
23
+ while (dir.includes("/")) {
24
+ dir = dir.substring(0, dir.lastIndexOf("/"));
25
+ if (dir)
26
+ paths.add(dir);
27
+ }
28
+ }
29
+ return { files, paths };
30
+ }
31
+ function createFileSystemEmulator(tree = {}, flatFiles) {
32
+ const { files, paths } = flattenFileSystemTree(tree);
33
+ if (flatFiles) {
34
+ for (const [filePath, content] of Object.entries(flatFiles)) {
35
+ files.set(filePath, content);
36
+ paths.add(filePath);
37
+ for (let dir = filePath.substring(0, filePath.lastIndexOf("/"));dir; dir = dir.substring(0, dir.lastIndexOf("/"))) {
38
+ paths.add(dir);
39
+ }
40
+ }
41
+ }
42
+ const createdDirs = [];
43
+ const writtenFiles = new Map;
44
+ const permissions = new Map;
45
+ const creationTimes = new Map;
46
+ let nextCreationTime = 1000;
47
+ for (const path of files.keys()) {
48
+ creationTimes.set(path, nextCreationTime++);
49
+ }
50
+ return {
51
+ exists: (path) => paths.has(path),
52
+ isDirectory: (path) => paths.has(path) && !files.has(path),
53
+ readFile: async (path) => {
54
+ if (!files.has(path)) {
55
+ const error = new Error(`ENOENT: no such file or directory, open '${path}'`);
56
+ error.code = "ENOENT";
57
+ throw error;
58
+ }
59
+ return files.get(path);
60
+ },
61
+ writeFile: async (path, content, options) => {
62
+ if (options?.flag === "wx" && paths.has(path)) {
63
+ const error = new Error(`EEXIST: file already exists, open '${path}'`);
64
+ error.code = "EEXIST";
65
+ throw error;
66
+ }
67
+ writtenFiles.set(path, content);
68
+ paths.add(path);
69
+ files.set(path, content);
70
+ if (!creationTimes.has(path)) {
71
+ creationTimes.set(path, nextCreationTime++);
72
+ }
73
+ },
74
+ mkdir: async (path) => {
75
+ createdDirs.push(path);
76
+ },
77
+ readdir: async (path) => {
78
+ const prefix = `${path}/`;
79
+ const entries = new Set;
80
+ for (const f of files.keys()) {
81
+ if (f.startsWith(prefix)) {
82
+ const relativePath = f.slice(prefix.length);
83
+ if (!relativePath.includes("/")) {
84
+ entries.add(relativePath);
85
+ } else {
86
+ entries.add(relativePath.split("/")[0]);
87
+ }
88
+ }
89
+ }
90
+ return Array.from(entries);
91
+ },
92
+ chmod: async (path, mode) => {
93
+ permissions.set(path, mode);
94
+ },
95
+ getFileCreationTime: (path) => {
96
+ return creationTimes.get(path) ?? 0;
97
+ },
98
+ scan: async function* (dir) {
99
+ if (!paths.has(dir)) {
100
+ const error = new Error(`ENOENT: no such file or directory, scandir '${dir}'`);
101
+ error.code = "ENOENT";
102
+ throw error;
103
+ }
104
+ const prefix = `${dir}/`;
105
+ for (const file of files.keys()) {
106
+ if (file.startsWith(prefix)) {
107
+ yield file.slice(prefix.length);
108
+ }
109
+ }
110
+ },
111
+ rename: async (oldPath, newPath) => {
112
+ const entriesToMove = [];
113
+ const pathsToUpdate = [];
114
+ for (const [path, content] of files.entries()) {
115
+ if (path === oldPath || path.startsWith(`${oldPath}/`)) {
116
+ const relativePath = path.slice(oldPath.length);
117
+ const newFilePath = `${newPath}${relativePath}`;
118
+ entriesToMove.push([path, newFilePath]);
119
+ files.set(newFilePath, content);
120
+ paths.add(newFilePath);
121
+ }
122
+ }
123
+ for (const path of paths) {
124
+ if (path === oldPath || path.startsWith(`${oldPath}/`)) {
125
+ pathsToUpdate.push(path);
126
+ const relativePath = path.slice(oldPath.length);
127
+ paths.add(`${newPath}${relativePath}`);
128
+ }
129
+ }
130
+ for (const [oldFilePath] of entriesToMove) {
131
+ files.delete(oldFilePath);
132
+ paths.delete(oldFilePath);
133
+ }
134
+ for (const oldPathEntry of pathsToUpdate) {
135
+ paths.delete(oldPathEntry);
136
+ }
137
+ },
138
+ createdDirs,
139
+ writtenFiles,
140
+ files,
141
+ permissions
142
+ };
143
+ }
144
+ export {
145
+ createFileSystemEmulator
146
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joshski/dust",
3
- "version": "0.1.71",
3
+ "version": "0.1.73",
4
4
  "description": "Flow state for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -29,6 +29,10 @@
29
29
  "./filesystem": {
30
30
  "types": "./dist/filesystem/types.d.ts"
31
31
  },
32
+ "./filesystem/emulator": {
33
+ "import": "./dist/filesystem-emulator.js",
34
+ "types": "./dist/filesystem/emulator.d.ts"
35
+ },
32
36
  "./istanbul/minimal-reporter": "./lib/istanbul/minimal-reporter.cjs",
33
37
  "./biome": {
34
38
  "import": "./dist/biome.js",