@jini-ai/platform 0.5.2 → 0.5.3
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/asset-cache.d.ts +12 -1
- package/dist/asset-cache.js +9 -5
- package/dist/child-environment.d.ts +18 -0
- package/dist/child-environment.js +26 -0
- package/dist/fs/boot-identity.d.ts +10 -0
- package/dist/fs/boot-identity.js +34 -0
- package/dist/fs/directory-home.d.ts +15 -0
- package/dist/fs/directory-home.js +17 -0
- package/dist/fs/directory-registry.d.ts +50 -0
- package/dist/fs/directory-registry.js +99 -0
- package/dist/fs/directory-running-lock.d.ts +43 -0
- package/dist/fs/directory-running-lock.js +66 -0
- package/dist/fs/directory-sync.d.ts +20 -0
- package/dist/fs/directory-sync.js +87 -0
- package/dist/fs/directory-user-config.d.ts +79 -0
- package/dist/fs/directory-user-config.js +183 -0
- package/dist/fs/directory-workspace.d.ts +132 -0
- package/dist/fs/directory-workspace.js +266 -0
- package/dist/fs/durable-json.d.ts +21 -0
- package/dist/fs/durable-json.js +59 -4
- package/dist/fs/file-lock.d.ts +3 -0
- package/dist/fs/file-lock.js +16 -7
- package/dist/fs/index.d.ts +6 -0
- package/dist/fs/index.js +6 -0
- package/dist/fs/user-data-directories.d.ts +28 -0
- package/dist/fs/user-data-directories.js +53 -0
- package/dist/fs/verified-directory-move.d.ts +37 -0
- package/dist/fs/verified-directory-move.js +138 -0
- package/dist/fs/verified-file-relocation.d.ts +27 -0
- package/dist/fs/verified-file-relocation.js +115 -0
- package/dist/fs/workspace-directory.d.ts +19 -0
- package/dist/fs/workspace-directory.js +23 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/secrets/credential-sets/index.d.ts +5 -0
- package/dist/secrets/credential-sets/index.js +5 -0
- package/dist/secrets/credential-sets/vault.aad.d.ts +4 -0
- package/dist/secrets/credential-sets/vault.aad.js +8 -0
- package/dist/secrets/credential-sets/vault.meta.d.ts +10 -0
- package/dist/secrets/credential-sets/vault.meta.js +52 -0
- package/dist/secrets/credential-sets/vault.repo.memory.d.ts +51 -0
- package/dist/secrets/credential-sets/vault.repo.memory.js +148 -0
- package/dist/secrets/credential-sets/vault.service.d.ts +130 -0
- package/dist/secrets/credential-sets/vault.service.js +310 -0
- package/dist/secrets/credential-sets/vault.types.d.ts +150 -0
- package/dist/secrets/credential-sets/vault.types.js +28 -0
- package/dist/web/html-document.d.ts +70 -0
- package/dist/web/html-document.js +268 -0
- package/dist/web/html-resource.d.ts +15 -0
- package/dist/web/html-resource.js +12 -0
- package/dist/web/html-to-markdown.d.ts +11 -0
- package/dist/web/html-to-markdown.js +245 -0
- package/dist/web/index.d.ts +5 -0
- package/dist/web/index.js +5 -0
- package/dist/web/web-page.d.ts +157 -0
- package/dist/web/web-page.js +310 -0
- package/package.json +14 -3
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { resolveDirectoryHome } from "./directory-home.js";
|
|
4
|
+
import { resolveUserDataDirectories } from "./user-data-directories.js";
|
|
5
|
+
import { detectDirectorySync } from "./directory-sync.js";
|
|
6
|
+
/** One atomic settings owner for multiple processes. Existing DB files always win over preferences:
|
|
7
|
+
* changing placement controls new databases, never silently moves or abandons existing ones.
|
|
8
|
+
* @complexity O(n) canonicalizations for n roots; DB lookup probes three fixed locations.
|
|
9
|
+
*/
|
|
10
|
+
export function createDirectoryUserConfig({ homeDir, defaultFolder, envName, policy, fileAt, canonicalPath, exists, isDirectory, removeFile }, { env = process.env, cwd = process.cwd(), report = console.warn, ...directoryOptions } = {}) {
|
|
11
|
+
const directories = resolveUserDataDirectories({ homeDir, appName: policy.appName }, { ...directoryOptions, env });
|
|
12
|
+
const paths = directoryOptions.platform === "win32" ? path.win32 : path;
|
|
13
|
+
const defaultHome = paths.join(directories.documents, defaultFolder, policy.defaultSubfolder ?? "");
|
|
14
|
+
const localHome = paths.join(homeDir, defaultFolder, policy.defaultSubfolder ?? "");
|
|
15
|
+
const legacyHomePaths = policy.defaultSubfolder ? [paths.join(directories.documents, defaultFolder), paths.join(homeDir, defaultFolder)] : [];
|
|
16
|
+
const filePath = paths.join(directories.appData, "config.json"), file = fileAt(filePath);
|
|
17
|
+
const expand = (value) => canonicalPath({ path: resolveDirectoryHome({ homeDir, defaultFolder, envName, env: {}, setting: value }, { cwd, ...(directoryOptions.platform ? { platform: directoryOptions.platform } : {}) }) });
|
|
18
|
+
const distinct = (values) => [...new Set(values.map(expand))];
|
|
19
|
+
const legacyHomes = distinct(legacyHomePaths);
|
|
20
|
+
function stored(locked = false) {
|
|
21
|
+
const result = file.read({});
|
|
22
|
+
if (result.state === "missing")
|
|
23
|
+
return {};
|
|
24
|
+
if (result.state === "ok") {
|
|
25
|
+
try {
|
|
26
|
+
return validateDirectoryUserConfig({ value: result.value });
|
|
27
|
+
}
|
|
28
|
+
catch { /* preserve invalid settings too */ }
|
|
29
|
+
}
|
|
30
|
+
// Bad settings must not stop boot, and read-only discovery/dry-run must not mutate settings.
|
|
31
|
+
// Preserve the original on reads; the next mutation quarantines it under the existing lock.
|
|
32
|
+
if (!locked) {
|
|
33
|
+
report(`User config is unreadable: ${filePath}. Using defaults; the original was left untouched.`);
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
if (!file.quarantine({ notice: { label: "user config", consequence: "Using default settings; existing databases keep their locations." } })) {
|
|
37
|
+
throw new Error(`User config is unreadable: ${filePath}. Nothing was overwritten.`);
|
|
38
|
+
}
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
const syncProvider = (sitesHome) => detectDirectorySync({ dir: sitesHome, homeDir, documents: directories.documents }, { ...directoryOptions, env, canonicalPath });
|
|
42
|
+
function read(_required, _options = {}) {
|
|
43
|
+
const saved = stored();
|
|
44
|
+
// Old default pointers follow the owner-selected nesting rule; arbitrary custom homes and
|
|
45
|
+
// explicit environment overrides keep their contracts. Reading never moves user folders.
|
|
46
|
+
const savedHome = saved.sitesHome === undefined ? defaultHome : expand(saved.sitesHome);
|
|
47
|
+
const nestedHome = legacyHomes.includes(savedHome) ? paths.join(savedHome, policy.defaultSubfolder) : savedHome;
|
|
48
|
+
const sitesHome = expand(env[envName] ?? nestedHome);
|
|
49
|
+
const sync = syncProvider(sitesHome);
|
|
50
|
+
return {
|
|
51
|
+
sitesHome,
|
|
52
|
+
siteRoots: distinct(env[policy.rootsEnvName] !== undefined ? env[policy.rootsEnvName].split(",").map((root) => root.trim()).filter(Boolean) : saved.siteRoots ?? policy.sourceRoots ?? []),
|
|
53
|
+
databases: saved.databases ?? (sync && sync !== "iCloud" ? "appData" : "inSite"),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function update({ patch }, { transform } = {}) {
|
|
57
|
+
// Compute incremental root changes inside the same lock as the write: a sibling process's
|
|
58
|
+
// added root must not disappear because this caller previously read an older array.
|
|
59
|
+
file.withLock({ critical: () => {
|
|
60
|
+
const previous = stored(true);
|
|
61
|
+
const checked = validateDirectoryUserConfig({ value: transform ? transform(read({}), previous) : patch });
|
|
62
|
+
// Prefer explicit settings, never coerce an existing regular file into a directory.
|
|
63
|
+
for (const directory of [checked.sitesHome, ...(checked.siteRoots ?? [])]) {
|
|
64
|
+
if (directory === undefined)
|
|
65
|
+
continue;
|
|
66
|
+
const expanded = expand(directory);
|
|
67
|
+
if (exists(expanded) && !isDirectory(expanded))
|
|
68
|
+
throw new Error(`Sites folder is not a directory: ${directory}`);
|
|
69
|
+
}
|
|
70
|
+
if (checked.sitesHome !== undefined && env[envName] !== undefined)
|
|
71
|
+
throw new Error(`${envName} overrides this setting. Unset it before changing the directory home.`);
|
|
72
|
+
if (checked.siteRoots !== undefined && env[policy.rootsEnvName] !== undefined)
|
|
73
|
+
throw new Error(`${policy.rootsEnvName} overrides this setting. Unset it before changing site roots.`);
|
|
74
|
+
const next = { ...previous, ...checked,
|
|
75
|
+
...(checked.sitesHome !== undefined ? { sitesHome: expand(checked.sitesHome) } : {}),
|
|
76
|
+
...(checked.siteRoots !== undefined ? { siteRoots: distinct(checked.siteRoots) } : {}),
|
|
77
|
+
};
|
|
78
|
+
// Empty incremental extras restore implicit defaults instead of persisting those defaults.
|
|
79
|
+
if (transform && checked.siteRoots?.length === 0)
|
|
80
|
+
delete next.siteRoots;
|
|
81
|
+
// With no other saved settings, removing the final extra also restores an absent config.
|
|
82
|
+
// The Node assembly supplies removal; non-Node adapters may retain the equivalent {}.
|
|
83
|
+
if (transform && Object.keys(next).length === 0 && removeFile)
|
|
84
|
+
removeFile({ filePath });
|
|
85
|
+
else
|
|
86
|
+
file.write({ value: next });
|
|
87
|
+
} });
|
|
88
|
+
return read({});
|
|
89
|
+
}
|
|
90
|
+
function updateRoots({ add = [], remove = [] }, _options = {}) {
|
|
91
|
+
return update({ patch: {} }, { transform: (current, saved) => {
|
|
92
|
+
const protectedRoots = new Set(distinct([current.sitesHome, ...(policy.sourceRoots ?? [])]));
|
|
93
|
+
const excluded = new Set(distinct(remove));
|
|
94
|
+
return { siteRoots: distinct([...(saved.siteRoots ?? []), ...add]).filter((root) => !excluded.has(root) && !protectedRoots.has(root)) };
|
|
95
|
+
} });
|
|
96
|
+
}
|
|
97
|
+
function databasePath({ dir, id, filename, placement }, _options = {}) {
|
|
98
|
+
if (path.basename(filename) !== filename || !filename.endsWith(".db"))
|
|
99
|
+
throw new Error("A database filename must be a .db basename.");
|
|
100
|
+
const inSite = path.join(dir, "data.nosync", filename), legacy = path.join(dir, filename);
|
|
101
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(id)) {
|
|
102
|
+
if (!placement && exists(legacy))
|
|
103
|
+
return legacy;
|
|
104
|
+
throw new Error("A database directory requires a safe, stable site identity.");
|
|
105
|
+
}
|
|
106
|
+
const appData = path.join(directories.appData, "databases", id, filename);
|
|
107
|
+
if (placement)
|
|
108
|
+
return placement === "inSite" ? inSite : appData;
|
|
109
|
+
const candidates = [legacy, inSite, appData];
|
|
110
|
+
// A move journal is written before copying; the old name stays authoritative until verification
|
|
111
|
+
// retires it. Recovery never guesses from two DB sizes/timestamps or asks users to delete data.
|
|
112
|
+
for (const source of candidates) {
|
|
113
|
+
const markerPath = source + ".home2-relocation.json", marker = fileAt(markerPath).read({});
|
|
114
|
+
if (marker.state === "missing")
|
|
115
|
+
continue;
|
|
116
|
+
const target = marker.state === "ok" ? marker.value?.to : undefined;
|
|
117
|
+
if (typeof target !== "string" || !candidates.includes(target))
|
|
118
|
+
throw new Error(`Interrupted database move has an invalid recovery marker: ${markerPath}. Restore the pre-move backup before starting.`);
|
|
119
|
+
const namedBackup = marker.state === "ok" ? marker.value?.backup : undefined;
|
|
120
|
+
// Old markers use the fixed home2 name; new moves identify their unique backup. Admit
|
|
121
|
+
// only a sibling of this source so malformed metadata cannot supply unrelated evidence.
|
|
122
|
+
if (namedBackup !== undefined && (typeof namedBackup !== "string" || path.dirname(namedBackup) !== path.dirname(source) || !namedBackup.startsWith(source + ".relocation-") || !namedBackup.endsWith("-backup")))
|
|
123
|
+
throw new Error(`Interrupted database move has an invalid recovery marker: ${markerPath}. Restore the pre-move backup before starting.`);
|
|
124
|
+
const backup = typeof namedBackup === "string" ? namedBackup : source + ".home2-backup";
|
|
125
|
+
const authoritative = exists(source) ? source : exists(backup) && exists(target) ? target : undefined;
|
|
126
|
+
if (!authoritative)
|
|
127
|
+
throw new Error(`Interrupted database move needs recovery: ${markerPath}. Restore the pre-move backup before starting.`);
|
|
128
|
+
report(`Recovered interrupted database move using ${authoritative}. Keep the backup files (${backup}); finish the move before changing database placement.`);
|
|
129
|
+
return authoritative;
|
|
130
|
+
}
|
|
131
|
+
const present = candidates.filter(exists);
|
|
132
|
+
if (present.length > 1)
|
|
133
|
+
throw new Error(`Multiple copies of ${filename} exist for this site. Choose the authoritative database before starting it.`);
|
|
134
|
+
if (present[0])
|
|
135
|
+
return present[0];
|
|
136
|
+
// A missing chat DB follows an existing content DB even after the user changes preferences.
|
|
137
|
+
if (filename !== "content.db") {
|
|
138
|
+
const content = databasePath({ dir, id, filename: "content.db" });
|
|
139
|
+
return path.join(path.dirname(content), filename);
|
|
140
|
+
}
|
|
141
|
+
return read({}).databases === "inSite" ? inSite : appData;
|
|
142
|
+
}
|
|
143
|
+
return { read, update, updateRoots, databasePath, filePath, ...directories,
|
|
144
|
+
// Discovery/catalog callers list real old homes. Pointer normalization and creation admission
|
|
145
|
+
// may inspect the supported spellings even before their directories have been created.
|
|
146
|
+
legacyHomes: (_required, { includeMissing = false } = {}) => distinct(legacyHomes).filter((root) => includeMissing || (exists(root) && isDirectory(root))),
|
|
147
|
+
defaultRoots: (_required, _options = {}) => distinct(policy.sourceRoots ?? []),
|
|
148
|
+
extraRoots: (_required, _options = {}) => distinct(env[policy.rootsEnvName] !== undefined ? read({}).siteRoots : stored().siteRoots ?? []),
|
|
149
|
+
roots: (_required, _options = {}) => {
|
|
150
|
+
const config = read({}), parent = paths.dirname(config.sitesHome);
|
|
151
|
+
// Discover only the selected home's existing former level. Scanning another old home
|
|
152
|
+
// crosses the local/synced boundary and admits broad or absent roots at every boot.
|
|
153
|
+
const formerRoots = legacyHomes.includes(parent) && exists(parent) && isDirectory(parent) ? [parent] : [];
|
|
154
|
+
return distinct([config.sitesHome, ...formerRoots, ...(policy.sourceRoots ?? []), ...config.siteRoots]);
|
|
155
|
+
},
|
|
156
|
+
runningPath: ({ dir }, _options = {}) => path.join(directories.appData, "running", createHash("sha256").update(canonicalPath({ path: dir })).digest("hex") + ".json"),
|
|
157
|
+
syncWarning: (_required, _options = {}) => {
|
|
158
|
+
const config = read({});
|
|
159
|
+
const sync = syncProvider(config.sitesHome);
|
|
160
|
+
if (!sync || sync === "iCloud")
|
|
161
|
+
return null;
|
|
162
|
+
return { provider: sync, message: config.databases === "appData" ? `${sync} syncs the sites folder. New databases use OS app-data for safety. Site folders are not self-contained: include their app-data databases when transferring them.` : `${sync} syncs the sites folder. It does not honor data.nosync; syncing live SQLite databases can corrupt them. Use app-data databases or a local sites folder.`, localHome };
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/** Validate only the three public settings; reject arrays, unknown fields and invalid enums.
|
|
167
|
+
* @complexity O(n) root strings. No I/O or mutation.
|
|
168
|
+
*/
|
|
169
|
+
export function validateDirectoryUserConfig({ value }, _options = {}) {
|
|
170
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
171
|
+
throw new Error("User config must be an object.");
|
|
172
|
+
const row = value;
|
|
173
|
+
if (Object.keys(row).some((key) => !["sitesHome", "siteRoots", "databases"].includes(key)))
|
|
174
|
+
throw new Error("Unknown user config setting.");
|
|
175
|
+
if (row.sitesHome !== undefined && (typeof row.sitesHome !== "string" || !row.sitesHome.trim()))
|
|
176
|
+
throw new Error("sitesHome must name a directory.");
|
|
177
|
+
if (row.siteRoots !== undefined && (!Array.isArray(row.siteRoots) || !row.siteRoots.every((root) => typeof root === "string" && root.trim())))
|
|
178
|
+
throw new Error("siteRoots must be an array of directory paths.");
|
|
179
|
+
if (row.databases !== undefined && row.databases !== "inSite" && row.databases !== "appData")
|
|
180
|
+
throw new Error('databases must be "inSite" or "appData".');
|
|
181
|
+
return row;
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=directory-user-config.js.map
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { type DurableJsonPorts, type DurableJsonChange } from "./durable-json.js";
|
|
2
|
+
import { type DirectoryUserConfigPolicy, type DirectoryUserConfig } from "./directory-user-config.js";
|
|
3
|
+
import type { UserDirectoryOptions } from "./user-data-directories.js";
|
|
4
|
+
import { type DirectoryRegistry } from "./directory-registry.js";
|
|
5
|
+
/** Node assembly only. Policy modules receive explicit filesystem, clock and process ports.
|
|
6
|
+
* Settings live outside the selected home so changing the setting never loses the pointer.
|
|
7
|
+
* Reading a missing home/catalog does not create it. Opt-in registry recovery may prune known
|
|
8
|
+
* pollution on load; read-only hosts omit that policy. Mutations use the durable JSON owner.
|
|
9
|
+
* @complexity O(n) registry records, O(1) settings/lease operations.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createNodeDirectoryWorkspace({ homeDir, defaultFolder, envName, settingsFile, registryName, runningName, userConfig, registryRecovery }: {
|
|
12
|
+
homeDir: string;
|
|
13
|
+
defaultFolder: string;
|
|
14
|
+
envName: string;
|
|
15
|
+
settingsFile: string;
|
|
16
|
+
registryName: string;
|
|
17
|
+
runningName: string;
|
|
18
|
+
userConfig?: DirectoryUserConfigPolicy;
|
|
19
|
+
/** Opt-in host recovery of known test pollution; missing user directories alone never qualify. */
|
|
20
|
+
registryRecovery?: {
|
|
21
|
+
fixtureIdPattern: RegExp;
|
|
22
|
+
temporaryRoots?: readonly string[];
|
|
23
|
+
};
|
|
24
|
+
}, { env, cwd, ports, canonicalPath, ...directoryOptions }?: UserDirectoryOptions & {
|
|
25
|
+
env?: NodeJS.ProcessEnv;
|
|
26
|
+
cwd?: string;
|
|
27
|
+
ports?: DurableJsonPorts;
|
|
28
|
+
canonicalPath?: (required: {
|
|
29
|
+
path: string;
|
|
30
|
+
}) => string;
|
|
31
|
+
}): {
|
|
32
|
+
homeDir: string;
|
|
33
|
+
config: (_required: Record<string, never>, _options?: {}) => {
|
|
34
|
+
defaultRoots: string[];
|
|
35
|
+
extraRoots: string[];
|
|
36
|
+
configPath: string;
|
|
37
|
+
appData: string;
|
|
38
|
+
warning: {
|
|
39
|
+
provider: "OneDrive" | "Dropbox" | "Google Drive";
|
|
40
|
+
message: string;
|
|
41
|
+
localHome: string;
|
|
42
|
+
} | null;
|
|
43
|
+
overrides: {
|
|
44
|
+
sitesHome: boolean;
|
|
45
|
+
siteRoots: boolean;
|
|
46
|
+
};
|
|
47
|
+
sitesHome: string;
|
|
48
|
+
siteRoots: string[];
|
|
49
|
+
databases: "inSite" | "appData";
|
|
50
|
+
};
|
|
51
|
+
updateConfig: (required: {
|
|
52
|
+
patch: Partial<DirectoryUserConfig>;
|
|
53
|
+
}, options?: {}) => {
|
|
54
|
+
defaultRoots: string[];
|
|
55
|
+
extraRoots: string[];
|
|
56
|
+
configPath: string;
|
|
57
|
+
appData: string;
|
|
58
|
+
warning: {
|
|
59
|
+
provider: "OneDrive" | "Dropbox" | "Google Drive";
|
|
60
|
+
message: string;
|
|
61
|
+
localHome: string;
|
|
62
|
+
} | null;
|
|
63
|
+
overrides: {
|
|
64
|
+
sitesHome: boolean;
|
|
65
|
+
siteRoots: boolean;
|
|
66
|
+
};
|
|
67
|
+
sitesHome: string;
|
|
68
|
+
siteRoots: string[];
|
|
69
|
+
databases: "inSite" | "appData";
|
|
70
|
+
};
|
|
71
|
+
updateRoots: (required: {
|
|
72
|
+
add?: readonly string[];
|
|
73
|
+
remove?: readonly string[];
|
|
74
|
+
}, options?: {}) => {
|
|
75
|
+
defaultRoots: string[];
|
|
76
|
+
extraRoots: string[];
|
|
77
|
+
configPath: string;
|
|
78
|
+
appData: string;
|
|
79
|
+
warning: {
|
|
80
|
+
provider: "OneDrive" | "Dropbox" | "Google Drive";
|
|
81
|
+
message: string;
|
|
82
|
+
localHome: string;
|
|
83
|
+
} | null;
|
|
84
|
+
overrides: {
|
|
85
|
+
sitesHome: boolean;
|
|
86
|
+
siteRoots: boolean;
|
|
87
|
+
};
|
|
88
|
+
sitesHome: string;
|
|
89
|
+
siteRoots: string[];
|
|
90
|
+
databases: "inSite" | "appData";
|
|
91
|
+
};
|
|
92
|
+
roots: (_required: Record<string, never>, _options?: {}) => string[];
|
|
93
|
+
databasePath: (required: {
|
|
94
|
+
dir: string;
|
|
95
|
+
id: string;
|
|
96
|
+
filename: string;
|
|
97
|
+
placement?: DirectoryUserConfig["databases"];
|
|
98
|
+
}, options?: {}) => string;
|
|
99
|
+
/** Only the implicit nested home may create its application parent below mounted Documents.
|
|
100
|
+
* Custom homes still require their immediate parent, so an absent volume is never fabricated.
|
|
101
|
+
*/
|
|
102
|
+
homeAvailable: (_required: Record<string, never>, _options?: {}) => boolean;
|
|
103
|
+
legacyHomes: (_required: Record<string, never>, _options?: {}) => string[];
|
|
104
|
+
/** Commit a verified move's catalog and every injected path reference as one locked unit.
|
|
105
|
+
* Uses the registry owner to preserve identity/collision rules; settings retain env precedence.
|
|
106
|
+
* @complexity O(total catalog/reference bytes), with rollback snapshots in memory.
|
|
107
|
+
*/
|
|
108
|
+
relocate({ from, to, entry, references }: {
|
|
109
|
+
from: string;
|
|
110
|
+
to: string;
|
|
111
|
+
entry: import("./directory-registry.js").DirectoryRecord;
|
|
112
|
+
references?: readonly DurableJsonChange[];
|
|
113
|
+
}, _options?: {}): void;
|
|
114
|
+
home: (_required: Record<string, never>, _options?: {}) => string;
|
|
115
|
+
setHome({ path: dir }: {
|
|
116
|
+
path: string;
|
|
117
|
+
}, _options?: {}): string;
|
|
118
|
+
registry: (_required: Record<string, never>, _options?: {}) => DirectoryRegistry;
|
|
119
|
+
running: ({ path: dir }: {
|
|
120
|
+
path: string;
|
|
121
|
+
}, _options?: {}) => import("./directory-running-lock.js").DirectoryRunningLock;
|
|
122
|
+
claim({ path: dir, app, port, scheme }: {
|
|
123
|
+
path: string;
|
|
124
|
+
app: string;
|
|
125
|
+
port?: number;
|
|
126
|
+
scheme?: "http" | "https";
|
|
127
|
+
}, _options?: {}): {
|
|
128
|
+
acquired: boolean;
|
|
129
|
+
record: import("./directory-running-lock.js").DirectoryRunningRecord;
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
//# sourceMappingURL=directory-workspace.d.ts.map
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { createDurableJsonFile, createNodeDurableJsonPorts, updateDurableJsonFiles } from "./durable-json.js";
|
|
6
|
+
import { createDirectoryUserConfig, validateDirectoryUserConfig } from "./directory-user-config.js";
|
|
7
|
+
import { resolveDirectoryHome } from "./directory-home.js";
|
|
8
|
+
import { createDirectoryRegistry } from "./directory-registry.js";
|
|
9
|
+
import { createDirectoryRunningLock } from "./directory-running-lock.js";
|
|
10
|
+
/** Node assembly only. Policy modules receive explicit filesystem, clock and process ports.
|
|
11
|
+
* Settings live outside the selected home so changing the setting never loses the pointer.
|
|
12
|
+
* Reading a missing home/catalog does not create it. Opt-in registry recovery may prune known
|
|
13
|
+
* pollution on load; read-only hosts omit that policy. Mutations use the durable JSON owner.
|
|
14
|
+
* @complexity O(n) registry records, O(1) settings/lease operations.
|
|
15
|
+
*/
|
|
16
|
+
export function createNodeDirectoryWorkspace({ homeDir, defaultFolder, envName, settingsFile, registryName, runningName, userConfig, registryRecovery }, { env = process.env, cwd = process.cwd(), ports = createNodeDurableJsonPorts({}), canonicalPath = canonicalNodeDirectory, ...directoryOptions } = {}) {
|
|
17
|
+
const fileAt = (filePath) => createDurableJsonFile({ ...ports, filePath, notices: {
|
|
18
|
+
prefix: "Directory workspace", lockTimeout: ({ lockPath }) => `Timed out waiting for file lock: ${lockPath}`,
|
|
19
|
+
} }, { staleMs: Infinity });
|
|
20
|
+
const config = userConfig ? createDirectoryUserConfig({ homeDir, defaultFolder, envName, policy: userConfig,
|
|
21
|
+
fileAt, canonicalPath, removeFile: ({ filePath }) => ports.filesystem.rmSync(filePath, { force: true }), isDirectory: (file) => ports.filesystem.statSync(file).isDirectory(), exists: (file) => { try {
|
|
22
|
+
ports.filesystem.statSync(file);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error.code === "ENOENT")
|
|
27
|
+
return false;
|
|
28
|
+
throw error;
|
|
29
|
+
} },
|
|
30
|
+
}, { env, cwd, report: (message) => ports.report.error({ message }), ...directoryOptions }) : undefined;
|
|
31
|
+
const settings = fileAt(config?.filePath ?? settingsFile);
|
|
32
|
+
function readSetting() {
|
|
33
|
+
const read = settings.read({});
|
|
34
|
+
if (read.state === "missing")
|
|
35
|
+
return undefined;
|
|
36
|
+
if (read.state !== "ok" || typeof read.value?.home !== "string")
|
|
37
|
+
throw new Error("The directory home setting is unreadable.");
|
|
38
|
+
return read.value.home;
|
|
39
|
+
}
|
|
40
|
+
const legacyHome = () => canonicalPath({ path: resolveDirectoryHome({ homeDir, defaultFolder, envName, env, setting: env[envName] === undefined ? readSetting() : undefined }, { cwd }) });
|
|
41
|
+
const home = () => config?.read({}).sitesHome ?? legacyHome();
|
|
42
|
+
const fixturePattern = registryRecovery ? new RegExp(registryRecovery.fixtureIdPattern.source, registryRecovery.fixtureIdPattern.flags.replace(/[gy]/g, "")) : undefined;
|
|
43
|
+
const atHome = () => createDirectoryRegistry({ file: fileAt(path.join(config?.appData ?? home(), registryName)), canonicalPath }, registryRecovery ? {
|
|
44
|
+
discard: ({ entry }) => {
|
|
45
|
+
const absolute = canonicalPath({ path: entry.path });
|
|
46
|
+
// HOME may itself be under tmp (desktop E2E), and a selected sites home may live there.
|
|
47
|
+
// Neither is pollution, including temporarily unavailable descendants and symlink aliases.
|
|
48
|
+
const protectedPath = [homeDir, home()].some((root) => {
|
|
49
|
+
const relative = path.relative(canonicalPath({ path: root }), absolute);
|
|
50
|
+
return relative === "" || (!relative.startsWith(".." + path.sep) && relative !== ".." && !path.isAbsolute(relative));
|
|
51
|
+
});
|
|
52
|
+
if (protectedPath)
|
|
53
|
+
return false;
|
|
54
|
+
const temporary = (registryRecovery.temporaryRoots ?? ["/private/tmp", "/tmp", os.tmpdir()]).some((root) => {
|
|
55
|
+
const relative = path.relative(canonicalPath({ path: root }), absolute);
|
|
56
|
+
return relative === "" || (!relative.startsWith(".." + path.sep) && relative !== ".." && !path.isAbsolute(relative));
|
|
57
|
+
});
|
|
58
|
+
if (!temporary && !fixturePattern.test(entry.id))
|
|
59
|
+
return false;
|
|
60
|
+
// A live temp-path folder is a real site. Only ENOENT proves this candidate is stale.
|
|
61
|
+
try {
|
|
62
|
+
ports.filesystem.statSync(entry.path);
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (error.code === "ENOENT")
|
|
67
|
+
return true;
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
reportDiscarded: ({ entries }) => ports.report.error({ message: `Directory workspace: removed ${entries.length} test/temporary catalog entries: ${entries.map((entry) => entry.id).join(", ")}` }),
|
|
72
|
+
} : {});
|
|
73
|
+
// Read old catalogs without modifying sites. Import only under the settings lane on a mutation;
|
|
74
|
+
// dry-run and startup discovery can inspect the old state without creating app-data files.
|
|
75
|
+
const oldCatalogs = () => {
|
|
76
|
+
// An explicit host profile is an isolation boundary. Importing HOME's legacy settings/catalog
|
|
77
|
+
// would silently enroll the owner's sites (and potentially read real app-data) into QA profiles.
|
|
78
|
+
if (directoryOptions.appDataDir !== undefined)
|
|
79
|
+
return [];
|
|
80
|
+
if (!config || !userConfig?.legacySettingsFile)
|
|
81
|
+
return [];
|
|
82
|
+
const legacy = fileAt(userConfig.legacySettingsFile).read({});
|
|
83
|
+
if (legacy.state === "unreadable")
|
|
84
|
+
throw new Error("Legacy directory setting is unreadable; nothing was overwritten.");
|
|
85
|
+
const oldHome = legacy.state === "ok" ? legacy.value?.home : path.join(homeDir, userConfig.legacyDefaultFolder ?? defaultFolder);
|
|
86
|
+
if (typeof oldHome !== "string")
|
|
87
|
+
throw new Error("Legacy directory setting has an invalid home.");
|
|
88
|
+
return [...new Set([oldHome, ...config.legacyHomes({}), home()])].map((root) => createDirectoryRegistry({ file: fileAt(path.join(root, registryName)), canonicalPath }));
|
|
89
|
+
};
|
|
90
|
+
const importOldCatalog = () => {
|
|
91
|
+
const current = atHome();
|
|
92
|
+
if (!config || current.hasMigration({ migration: "app-data-catalog-v1" }))
|
|
93
|
+
return;
|
|
94
|
+
const previous = oldCatalogs();
|
|
95
|
+
current.merge({ entries: previous.flatMap((catalog) => catalog.list({})), migrations: previous.flatMap((catalog) => catalog.migrations({})), migration: "app-data-catalog-v1" });
|
|
96
|
+
};
|
|
97
|
+
const mutate = (operation) => settings.withLock({ critical: () => { importOldCatalog(); return operation(); } });
|
|
98
|
+
// Serialize pointer changes with catalog mutations. A caller that obtained a registry before
|
|
99
|
+
// settings changed must still write into the current home, never strand a late registration.
|
|
100
|
+
const registry = {
|
|
101
|
+
list: (required, options = {}) => {
|
|
102
|
+
const current = atHome();
|
|
103
|
+
if (!config || current.hasMigration({ migration: "app-data-catalog-v1" }))
|
|
104
|
+
return current.list(required, options);
|
|
105
|
+
const rows = [...oldCatalogs().flatMap((catalog) => catalog.list(required, options)), ...current.list(required, options)];
|
|
106
|
+
return [...new Map(rows.map((row) => [canonicalPath({ path: row.path }), row])).values()];
|
|
107
|
+
},
|
|
108
|
+
hasMigration: (required, options = {}) => atHome().hasMigration(required, options),
|
|
109
|
+
migrations: (required, options = {}) => atHome().migrations(required, options),
|
|
110
|
+
merge: (required, options = {}) => mutate(() => atHome().merge(required, options)),
|
|
111
|
+
opened: (required, options = {}) => mutate(() => atHome().opened(required, options)),
|
|
112
|
+
remove: (required, options = {}) => mutate(() => atHome().remove(required, options)),
|
|
113
|
+
relocate: (required, options = {}) => mutate(() => atHome().relocate(required, options)),
|
|
114
|
+
};
|
|
115
|
+
return {
|
|
116
|
+
homeDir,
|
|
117
|
+
config: (_required, _options = {}) => thisConfig(),
|
|
118
|
+
updateConfig: (required, options = {}) => {
|
|
119
|
+
if (!config)
|
|
120
|
+
throw new Error("User config policy is not configured.");
|
|
121
|
+
config.update(required, options);
|
|
122
|
+
return thisConfig();
|
|
123
|
+
},
|
|
124
|
+
updateRoots: (required, options = {}) => {
|
|
125
|
+
if (!config)
|
|
126
|
+
throw new Error("User config policy is not configured.");
|
|
127
|
+
config.updateRoots(required, options);
|
|
128
|
+
return thisConfig();
|
|
129
|
+
},
|
|
130
|
+
roots: (_required, _options = {}) => config?.roots({}) ?? [home()],
|
|
131
|
+
databasePath: (required, options = {}) => {
|
|
132
|
+
if (!config)
|
|
133
|
+
return path.join(required.dir, required.filename);
|
|
134
|
+
return config.databasePath(required, options);
|
|
135
|
+
},
|
|
136
|
+
/** Only the implicit nested home may create its application parent below mounted Documents.
|
|
137
|
+
* Custom homes still require their immediate parent, so an absent volume is never fabricated.
|
|
138
|
+
*/
|
|
139
|
+
homeAvailable: (_required, _options = {}) => {
|
|
140
|
+
const selected = home();
|
|
141
|
+
const parent = config?.legacyHomes({}, { includeMissing: true }).includes(path.dirname(selected)) ? path.dirname(path.dirname(selected)) : path.dirname(selected);
|
|
142
|
+
try {
|
|
143
|
+
return ports.filesystem.statSync(parent).isDirectory();
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
if (error.code === "ENOENT")
|
|
147
|
+
return false;
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
legacyHomes: (_required, _options = {}) => config?.legacyHomes({}) ?? [],
|
|
152
|
+
/** Commit a verified move's catalog and every injected path reference as one locked unit.
|
|
153
|
+
* Uses the registry owner to preserve identity/collision rules; settings retain env precedence.
|
|
154
|
+
* @complexity O(total catalog/reference bytes), with rollback snapshots in memory.
|
|
155
|
+
*/
|
|
156
|
+
relocate({ from, to, entry, references = [] }, _options = {}) {
|
|
157
|
+
if (!config)
|
|
158
|
+
throw new Error("Relocation requires user config policy.");
|
|
159
|
+
const repoint = (dir) => {
|
|
160
|
+
const current = canonicalPath({ path: dir });
|
|
161
|
+
if (current === path.dirname(home()) && config.legacyHomes({}).includes(current))
|
|
162
|
+
return home();
|
|
163
|
+
return current === from || current.startsWith(from + path.sep) ? path.join(to, path.relative(from, current)) : dir;
|
|
164
|
+
};
|
|
165
|
+
updateDurableJsonFiles({ fileAt, remove: ({ filePath }) => ports.filesystem.rmSync(filePath, { force: true }), changes: [
|
|
166
|
+
{ filePath: config.filePath, transform: (value) => {
|
|
167
|
+
if (value === undefined)
|
|
168
|
+
return undefined;
|
|
169
|
+
const saved = validateDirectoryUserConfig({ value });
|
|
170
|
+
// Persist only the saved pointer's nesting transition. Environment overrides belong
|
|
171
|
+
// to this process and must never become another host's permanent configuration.
|
|
172
|
+
const savedHome = saved.sitesHome === undefined ? undefined : resolveDirectoryHome({ homeDir, defaultFolder, envName, env: {}, setting: saved.sitesHome }, { cwd, ...(directoryOptions.platform ? { platform: directoryOptions.platform } : {}) });
|
|
173
|
+
const paths = directoryOptions.platform === "win32" ? path.win32 : path;
|
|
174
|
+
return { ...saved, ...(saved.sitesHome && savedHome && config.legacyHomes({}, { includeMissing: true }).includes(canonicalPath({ path: savedHome })) ? { sitesHome: paths.join(saved.sitesHome, userConfig.defaultSubfolder) } : {}),
|
|
175
|
+
...(saved.siteRoots ? { siteRoots: [...new Set(saved.siteRoots.map(repoint))] } : {}) };
|
|
176
|
+
} },
|
|
177
|
+
{ filePath: path.join(config.appData, registryName), transform: (value) => {
|
|
178
|
+
// A transaction-local file adapter lets the registry's existing validation and merge
|
|
179
|
+
// own this schema without re-acquiring the physical lock already held above.
|
|
180
|
+
let state = value;
|
|
181
|
+
const staged = createDirectoryRegistry({ canonicalPath, file: {
|
|
182
|
+
read: () => state === undefined ? { state: "missing" } : { state: "ok", value: state },
|
|
183
|
+
write: ({ value: next }) => { state = next; }, withLock: ({ critical }) => critical(),
|
|
184
|
+
tempPath: () => "", quarantine: () => false,
|
|
185
|
+
} });
|
|
186
|
+
if (!staged.hasMigration({ migration: "app-data-catalog-v1" })) {
|
|
187
|
+
const previous = oldCatalogs();
|
|
188
|
+
staged.merge({ entries: previous.flatMap((catalog) => catalog.list({})), migrations: previous.flatMap((catalog) => catalog.migrations({})), migration: "app-data-catalog-v1" });
|
|
189
|
+
}
|
|
190
|
+
const existing = staged.list({}).find((row) => row.path === from || row.id === entry.id);
|
|
191
|
+
if (existing && existing.path !== from && existing.path !== to)
|
|
192
|
+
throw new Error("Site identity is registered at an unrelated path.");
|
|
193
|
+
if (!existing)
|
|
194
|
+
staged.merge({ entries: [{ ...entry, path: from }] });
|
|
195
|
+
staged.relocate({ from, to });
|
|
196
|
+
return state;
|
|
197
|
+
} }, ...references,
|
|
198
|
+
] });
|
|
199
|
+
},
|
|
200
|
+
home: (_required, _options = {}) => home(),
|
|
201
|
+
setHome({ path: dir }, _options = {}) {
|
|
202
|
+
if (env[envName] !== undefined)
|
|
203
|
+
throw new Error(`${envName} overrides this setting. Unset it before changing the directory home.`);
|
|
204
|
+
if (config) {
|
|
205
|
+
config.update({ patch: { sitesHome: dir } });
|
|
206
|
+
return home();
|
|
207
|
+
}
|
|
208
|
+
const selected = canonicalPath({ path: resolveDirectoryHome({ homeDir, defaultFolder, envName, env: {}, setting: dir }, { cwd }) });
|
|
209
|
+
settings.withLock({ critical: () => {
|
|
210
|
+
const previous = createDirectoryRegistry({ file: fileAt(path.join(home(), registryName)), canonicalPath });
|
|
211
|
+
const target = createDirectoryRegistry({ file: fileAt(path.join(selected, registryName)), canonicalPath });
|
|
212
|
+
target.merge({ entries: previous.list({}), migrations: previous.migrations({}) });
|
|
213
|
+
settings.write({ value: { home: selected } });
|
|
214
|
+
} });
|
|
215
|
+
return home();
|
|
216
|
+
},
|
|
217
|
+
registry: (_required, _options = {}) => registry,
|
|
218
|
+
running: ({ path: dir }, _options = {}) => {
|
|
219
|
+
const filePath = config?.runningPath({ dir }) ?? path.join(canonicalPath({ path: dir }), runningName);
|
|
220
|
+
const current = createDirectoryRunningLock({ file: fileAt(filePath), isAlive: ports.process.isAlive,
|
|
221
|
+
remove: () => ports.filesystem.rmSync(filePath, { force: true }) }, { ...(ports.process.bootIdentity ? { bootIdentity: ports.process.bootIdentity } : {}) });
|
|
222
|
+
if (!config)
|
|
223
|
+
return current;
|
|
224
|
+
const legacyPath = path.join(canonicalPath({ path: dir }), runningName);
|
|
225
|
+
const legacy = createDirectoryRunningLock({ file: fileAt(legacyPath), isAlive: ports.process.isAlive,
|
|
226
|
+
remove: () => ports.filesystem.rmSync(legacyPath, { force: true }) }, { ...(ports.process.bootIdentity ? { bootIdentity: ports.process.bootIdentity } : {}) });
|
|
227
|
+
return { ...current,
|
|
228
|
+
// Older servers may still own a site-local lease. Observe it without moving/editing it;
|
|
229
|
+
// claim serializes against new clients and refuses the old live owner.
|
|
230
|
+
peek: (_required, _options = {}) => legacy.peek({}) ?? current.peek({}),
|
|
231
|
+
read: (_required, _options = {}) => legacy.peek({}) ?? current.read({}),
|
|
232
|
+
claim: (required, options = {}) => settings.withLock({ critical: () => {
|
|
233
|
+
const existing = legacy.peek({});
|
|
234
|
+
if (existing)
|
|
235
|
+
return { acquired: false, record: existing };
|
|
236
|
+
return current.claim(required, options);
|
|
237
|
+
} }),
|
|
238
|
+
};
|
|
239
|
+
},
|
|
240
|
+
claim({ path: dir, app, port = 0, scheme = "http" }, _options = {}) {
|
|
241
|
+
const lock = this.running({ path: dir });
|
|
242
|
+
return lock.claim({ record: { pid: ports.process.pid, port, app, scheme, started: new Date(ports.clock.nowMs()).toISOString(), token: randomUUID() } });
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
function thisConfig() {
|
|
246
|
+
if (!config)
|
|
247
|
+
throw new Error("User config policy is not configured.");
|
|
248
|
+
return { ...config.read({}), defaultRoots: config.defaultRoots({}), extraRoots: config.extraRoots({}), configPath: config.filePath, appData: config.appData, warning: config.syncWarning({}),
|
|
249
|
+
overrides: { sitesHome: env[envName] !== undefined, siteRoots: env[userConfig.rootsEnvName] !== undefined } };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/** Canonicalize existing ancestors without creating the selected directory. */
|
|
253
|
+
function canonicalNodeDirectory({ path: dir }, _options = {}) {
|
|
254
|
+
try {
|
|
255
|
+
return fs.realpathSync(dir);
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
if (error.code !== "ENOENT")
|
|
259
|
+
throw error;
|
|
260
|
+
// Resolve the nearest existing ancestor too: a not-yet-created home may sit below a
|
|
261
|
+
// symlink (notably /var on macOS) and must compare equal before and after creation.
|
|
262
|
+
const absolute = path.resolve(dir), parent = path.dirname(absolute);
|
|
263
|
+
return parent === absolute ? absolute : path.join(canonicalNodeDirectory({ path: parent }), path.basename(absolute));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
//# sourceMappingURL=directory-workspace.js.map
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Recovery policy stays with each store: salvage, restart, or leave untouched is not decided here.
|
|
6
6
|
* Native effects are Electron-free so desktop, CLI, and bridge processes can share the mechanism.
|
|
7
7
|
*/
|
|
8
|
+
import { type BootIdentity } from "./boot-identity.js";
|
|
8
9
|
import nodeFs from "node:fs";
|
|
9
10
|
import type { Clock } from "@jini-ai/core/primitives";
|
|
10
11
|
export type JsonFileRead = {
|
|
@@ -36,6 +37,7 @@ export interface DurableJsonPorts {
|
|
|
36
37
|
isAlive(required: {
|
|
37
38
|
pid: number;
|
|
38
39
|
}): boolean;
|
|
40
|
+
bootIdentity?: () => BootIdentity | undefined;
|
|
39
41
|
};
|
|
40
42
|
random: {
|
|
41
43
|
token(_required: Record<string, never>): string;
|
|
@@ -86,4 +88,23 @@ export declare function salvageJsonPrefix({ text }: {
|
|
|
86
88
|
}, { maxAttempts }?: {
|
|
87
89
|
maxAttempts?: number;
|
|
88
90
|
}): unknown;
|
|
91
|
+
export interface DurableJsonChange {
|
|
92
|
+
filePath: string;
|
|
93
|
+
/** Missing optional references stay missing; transforms must reject unreadable schemas. */
|
|
94
|
+
transform: (value: unknown | undefined) => unknown | undefined;
|
|
95
|
+
}
|
|
96
|
+
/** Update related JSON pointers under their existing locks, with atomic replacement per file.
|
|
97
|
+
* Compute every change before writing; a failed write restores prior bytes before releasing locks.
|
|
98
|
+
* The host supplies schema transforms, so this owner knows nothing about product catalogs.
|
|
99
|
+
* @throws Unreadable input, transform, lock or persistence failures (including rollback failures).
|
|
100
|
+
* @example updateDurableJsonFiles({ changes, fileAt, remove });
|
|
101
|
+
* @complexity O(total JSON bytes), O(total JSON bytes) rollback storage, one lock per file.
|
|
102
|
+
*/
|
|
103
|
+
export declare function updateDurableJsonFiles({ changes, fileAt, remove }: {
|
|
104
|
+
changes: readonly DurableJsonChange[];
|
|
105
|
+
fileAt: (path: string) => DurableJsonFile;
|
|
106
|
+
remove: (required: {
|
|
107
|
+
filePath: string;
|
|
108
|
+
}) => void;
|
|
109
|
+
}, _options?: {}): void;
|
|
89
110
|
//# sourceMappingURL=durable-json.d.ts.map
|