@hachej/boring-files 0.0.0-stage → 0.2.1

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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +215 -2
  3. package/dist/client.d.ts +12 -0
  4. package/dist/client.js +1 -0
  5. package/dist/contracts.d.ts +194 -0
  6. package/dist/contracts.js +1 -0
  7. package/dist/git.d.ts +105 -0
  8. package/dist/git.js +205 -0
  9. package/dist/index.d.ts +3 -0
  10. package/dist/index.js +1 -0
  11. package/dist/journal.d.ts +83 -0
  12. package/dist/journal.js +124 -0
  13. package/dist/node-http.d.ts +23 -0
  14. package/dist/node-http.js +102 -0
  15. package/dist/platform.d.ts +22 -0
  16. package/dist/platform.js +157 -0
  17. package/dist/publication-input.d.ts +15 -0
  18. package/dist/publication-input.js +164 -0
  19. package/dist/remote-handler.d.ts +11 -0
  20. package/dist/remote-handler.js +63 -0
  21. package/dist/remote-protocol.d.ts +34 -0
  22. package/dist/remote-protocol.js +154 -0
  23. package/dist/remote.d.ts +26 -0
  24. package/dist/remote.js +92 -0
  25. package/dist/request-guard.d.ts +20 -0
  26. package/dist/request-guard.js +75 -0
  27. package/dist/revision-contracts.d.ts +89 -0
  28. package/dist/revision-contracts.js +8 -0
  29. package/dist/revision-handler.d.ts +14 -0
  30. package/dist/revision-handler.js +213 -0
  31. package/dist/revision-protocol.d.ts +22 -0
  32. package/dist/revision-protocol.js +106 -0
  33. package/dist/revision.d.ts +8 -0
  34. package/dist/revision.js +145 -0
  35. package/dist/sqlite-batch.d.ts +19 -0
  36. package/dist/sqlite-batch.js +7 -0
  37. package/dist/sqlite-durable-object.d.ts +24 -0
  38. package/dist/sqlite-durable-object.js +49 -0
  39. package/dist/sqlite-filesystem.d.ts +16 -0
  40. package/dist/sqlite-filesystem.js +423 -0
  41. package/dist/sqlite-settings.d.ts +61 -0
  42. package/dist/sqlite-settings.js +104 -0
  43. package/dist/sqlite.d.ts +37 -0
  44. package/dist/sqlite.js +50 -0
  45. package/dist/text.d.ts +16 -0
  46. package/dist/text.js +29 -0
  47. package/dist/workspace-catalog.d.ts +17 -0
  48. package/dist/workspace-catalog.js +147 -0
  49. package/dist/workspace-identity.d.ts +8 -0
  50. package/dist/workspace-identity.js +1 -0
  51. package/dist/workspace.d.ts +108 -0
  52. package/dist/workspace.js +649 -0
  53. package/package.json +119 -4
package/dist/sqlite.js ADDED
@@ -0,0 +1,50 @@
1
+ import { applySqliteSettings, resolveSqliteSettings } from './sqlite-settings.js';
2
+ export * from './sqlite-settings.js';
3
+ /**
4
+ * A `node:sqlite` database with `settings` applied (partial settings are filled from `sqliteSettings.localDisk`: WAL, full sync,
5
+ * 5 s busy timeout). For Pi's own durable storage on the same settings, hand it to Pi's public adapter:
6
+ * `SqliteStorage.open(new NodeSqliteDatabase(openNodeDatabase(file, sqliteSettings.networkFilesystem)))`. Throws
7
+ * `SqliteLockedError` when another connection holds the file under `lockingMode: 'exclusive'`. Loaded without a static import so
8
+ * other runtimes can bundle this module.
9
+ */
10
+ export function openNodeDatabase(filename, settings = {}) {
11
+ const resolved = resolveSqliteSettings(settings);
12
+ const sqlite = globalThis.process?.getBuiltinModule?.('node:sqlite');
13
+ if (!sqlite)
14
+ throw new Error('A filename needs Node.js with node:sqlite; pass a connection on other runtimes');
15
+ const db = new sqlite.DatabaseSync(filename);
16
+ try {
17
+ applySqliteSettings({ exec: sql => { db.exec(sql); }, get: sql => db.prepare(sql).get() }, resolved, filename);
18
+ }
19
+ catch (error) {
20
+ db.close();
21
+ throw error;
22
+ }
23
+ return db;
24
+ }
25
+ /** A `node:sqlite` file as a connection (`openNodeDatabase` with the same settings), `BEGIN IMMEDIATE` for writes. */
26
+ export function openNodeConnection(filename, settings = {}) {
27
+ const db = openNodeDatabase(filename, settings);
28
+ return {
29
+ exec: sql => { db.exec(sql); },
30
+ run: (sql, ...params) => { db.prepare(sql).run(...params); },
31
+ get: (sql, ...params) => db.prepare(sql).get(...params),
32
+ all: (sql, ...params) => db.prepare(sql).all(...params),
33
+ // A transaction opened inside another one joins it: the outer one commits or rolls back everything.
34
+ transaction: (mode, work) => {
35
+ if (db.isTransaction)
36
+ return work();
37
+ db.exec(mode === 'write' ? 'BEGIN IMMEDIATE' : 'BEGIN');
38
+ try {
39
+ const result = work();
40
+ db.exec('COMMIT');
41
+ return result;
42
+ }
43
+ catch (error) {
44
+ db.exec('ROLLBACK');
45
+ throw error;
46
+ }
47
+ },
48
+ close: () => { db.close(); },
49
+ };
50
+ }
package/dist/text.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ export interface TextEdit {
2
+ readonly find: string;
3
+ readonly replace: string;
4
+ }
5
+ export type TextEditResult = {
6
+ readonly kind: 'applied';
7
+ readonly text: string;
8
+ } | {
9
+ readonly kind: 'rejected';
10
+ readonly editIndex: number;
11
+ readonly reason: string;
12
+ };
13
+ export declare function parseTextEdits(value: unknown): readonly TextEdit[];
14
+ /** Each edit sees the preceding edit's result; ambiguous or missing matches refuse the entire transformation. */
15
+ export declare function applyTextEdits(text: string, input: readonly TextEdit[]): TextEditResult;
16
+ //# sourceMappingURL=text.d.ts.map
package/dist/text.js ADDED
@@ -0,0 +1,29 @@
1
+ export function parseTextEdits(value) {
2
+ if (!Array.isArray(value) || !value.length)
3
+ throw new TypeError('At least one exact text edit is required');
4
+ const edits = value;
5
+ return edits.map(item => {
6
+ if (!item || typeof item !== 'object' || !('find' in item) || !('replace' in item) || typeof item.find !== 'string' || !item.find.length || typeof item.replace !== 'string'
7
+ || Object.keys(item).some(key => key !== 'find' && key !== 'replace'))
8
+ throw new TypeError('Each text edit requires nonempty find and string replace');
9
+ return { find: item.find, replace: item.replace };
10
+ });
11
+ }
12
+ /** Each edit sees the preceding edit's result; ambiguous or missing matches refuse the entire transformation. */
13
+ export function applyTextEdits(text, input) {
14
+ if (typeof text !== 'string')
15
+ throw new TypeError('Expected text');
16
+ const edits = parseTextEdits(input);
17
+ let result = text;
18
+ for (const [editIndex, edit] of edits.entries()) {
19
+ const at = result.indexOf(edit.find);
20
+ if (at < 0)
21
+ return { kind: 'rejected', editIndex, reason: 'Exact text was not found' };
22
+ if (result.indexOf(edit.find, at + 1) >= 0)
23
+ return { kind: 'rejected', editIndex, reason: 'Exact text occurs more than once' };
24
+ result = result.slice(0, at) + edit.replace + result.slice(at + edit.find.length);
25
+ }
26
+ if (new TextDecoder('utf-8', { ignoreBOM: true }).decode(new TextEncoder().encode(result)) !== result)
27
+ return { kind: 'rejected', editIndex: edits.length - 1, reason: 'Result contains invalid Unicode' };
28
+ return { kind: 'applied', text: result };
29
+ }
@@ -0,0 +1,17 @@
1
+ import type { FileSystem } from '@earendil-works/pi-durable/env';
2
+ import type { FileEntry, WorkspaceCatalog, SavedRevision } from './revision-contracts.js';
3
+ export interface WorkspaceCatalogOptions {
4
+ /** Presentation defaults, never an access policy. False shows dependency/build/internal names and gitignored files. */
5
+ readonly defaults?: boolean;
6
+ /** Additional presentation filtering. Returning false prunes a directory and its descendants. */
7
+ readonly filter?: (entry: FileEntry) => boolean;
8
+ /** A traversal exceeding this bound fails explicitly; it never returns a silently incomplete page. */
9
+ readonly maxEntries?: number;
10
+ }
11
+ export declare function catalogPath(path: string, allowRoot?: boolean): string;
12
+ export declare function createWorkspaceCatalog(options: {
13
+ readonly fs: FileSystem;
14
+ readonly config?: WorkspaceCatalogOptions;
15
+ readonly saves: (path: string) => readonly SavedRevision[];
16
+ }): WorkspaceCatalog;
17
+ //# sourceMappingURL=workspace-catalog.d.ts.map
@@ -0,0 +1,147 @@
1
+ import ignore from 'ignore';
2
+ import { identifier, locator } from './publication-input.js';
3
+ import { RevisionError } from './revision-contracts.js';
4
+ const hidden = new Set(['.git', '.boring', 'node_modules', 'dist', 'build', '.cache', '.next', 'coverage', '__pycache__']);
5
+ const temporary = /^\.boring-[0-9a-f-]{36}\.tmp$/;
6
+ const inside = (root, path) => path === root || path.startsWith(root === '/' ? '/' : `${root}/`);
7
+ export function catalogPath(path, allowRoot = false) {
8
+ if (allowRoot && path === '')
9
+ return path;
10
+ return locator({ resource: { providerId: 'catalog', path }, view: { kind: 'published' } }).resource.path;
11
+ }
12
+ export function createWorkspaceCatalog(options) {
13
+ const { fs } = options, config = options.config ?? {};
14
+ const maximum = config.maxEntries ?? 100_000;
15
+ if (!Number.isSafeInteger(maximum) || maximum < 1)
16
+ throw new TypeError('Invalid catalog traversal bound');
17
+ const root = fs.cwd.replace(/\/$/, '') || '/';
18
+ const absolute = (path) => `${root === '/' ? '' : root}/${path}`;
19
+ const check = (access) => { if (access.signal?.aborted)
20
+ throw new RevisionError('aborted', 'File browsing was cancelled'); };
21
+ const context = (access) => ({ abortSignal: access.signal, value: () => undefined, toString: () => 'boring-catalog' });
22
+ async function contained(path, access) {
23
+ check(access);
24
+ const base = await fs.canonicalPath(root, context(access));
25
+ const resolved = await fs.canonicalPath(absolute(path), context(access));
26
+ check(access);
27
+ return base.ok && resolved.ok && inside(base.value, resolved.value);
28
+ }
29
+ async function rulesAt(path, directory, parents, access) {
30
+ if (config.defaults === false)
31
+ return parents;
32
+ const info = await fs.fileInfo(absolute(path), context(access));
33
+ if (!info.ok) {
34
+ if (info.error.code === 'not_found' || info.error.code === 'not_directory')
35
+ return parents;
36
+ throw new RevisionError('unavailable', 'Ignore rules could not be inspected');
37
+ }
38
+ if (info.value.kind !== 'file' || !await contained(path, access))
39
+ throw new RevisionError('denied', 'Ignore rules must be a file inside the workspace');
40
+ if (info.value.size > 1_048_576)
41
+ throw new RevisionError('unavailable', 'Ignore rules exceed the catalog limit');
42
+ const read = await fs.readBinaryFile(absolute(path), context(access));
43
+ check(access);
44
+ if (!read.ok)
45
+ throw new RevisionError('unavailable', 'Ignore rules could not be read');
46
+ return [...parents, { directory, matcher: ignore.default().add(new TextDecoder('utf-8', { fatal: true }).decode(read.value)) }];
47
+ }
48
+ function visible(entry, rules) {
49
+ const name = entry.path.slice(entry.path.lastIndexOf('/') + 1);
50
+ if (temporary.test(name))
51
+ return false;
52
+ if (config.defaults !== false) {
53
+ if (hidden.has(name))
54
+ return false;
55
+ let ignored = false;
56
+ for (const rule of rules) {
57
+ const relative = entry.path.slice(rule.directory ? rule.directory.length + 1 : 0) + (entry.kind === 'directory' ? '/' : '');
58
+ const result = rule.matcher.test(relative);
59
+ if (result.ignored)
60
+ ignored = true;
61
+ else if (result.unignored)
62
+ ignored = false;
63
+ }
64
+ if (ignored)
65
+ return false;
66
+ }
67
+ return config.filter?.(entry) !== false;
68
+ }
69
+ async function collect(directory, recursive, access) {
70
+ check(access);
71
+ if (!await contained(directory, access))
72
+ throw new RevisionError('denied', 'The directory is outside the workspace or unavailable');
73
+ const entries = [];
74
+ let scanned = 0;
75
+ async function walk(parent, rules) {
76
+ check(access);
77
+ const own = await rulesAt(parent ? `${parent}/.gitignore` : '.gitignore', parent, rules, access);
78
+ const listing = await fs.listDir(absolute(parent), context(access));
79
+ check(access);
80
+ if (!listing.ok)
81
+ throw new RevisionError('unavailable', 'The directory could not be listed');
82
+ for (const item of listing.value) {
83
+ check(access);
84
+ if (++scanned > maximum)
85
+ throw new RevisionError('unavailable', 'The workspace exceeds the catalog traversal limit');
86
+ if (item.kind !== 'file' && item.kind !== 'directory')
87
+ continue;
88
+ const path = catalogPath(parent ? `${parent}/${item.name}` : item.name);
89
+ const entry = { path, kind: item.kind, size: item.size };
90
+ if (!visible(entry, own))
91
+ continue;
92
+ if (!await contained(path, access))
93
+ throw new RevisionError('denied', 'A listed entry resolves outside the workspace');
94
+ entries.push(entry);
95
+ if (item.kind === 'directory' && (recursive || directory === path || directory.startsWith(`${path}/`)))
96
+ await walk(path, own);
97
+ }
98
+ }
99
+ // Walk from the root so parent ignore rules and directory exclusions also apply to a direct nested-directory request.
100
+ let initial = [];
101
+ if (config.defaults !== false) {
102
+ for (const path of ['.git', '.git/info']) {
103
+ const info = await fs.fileInfo(absolute(path), context(access));
104
+ if (info.ok && info.value.kind === 'symlink')
105
+ throw new RevisionError('denied', 'Repository ignore rules cannot use symbolic links');
106
+ }
107
+ initial = await rulesAt('.git/info/exclude', '', [], access);
108
+ }
109
+ await walk('', initial);
110
+ if (recursive)
111
+ return entries;
112
+ return entries.filter(entry => entry.path.slice(0, entry.path.lastIndexOf('/') + 1) === (directory ? `${directory}/` : ''));
113
+ }
114
+ function page(entries, key, cursor, limit) {
115
+ const count = limit ?? 100;
116
+ if (!Number.isSafeInteger(count) || count < 1 || count > 500)
117
+ throw new RevisionError('invalid', 'Page limit must be between 1 and 500');
118
+ let after = '';
119
+ if (cursor !== undefined) {
120
+ try {
121
+ const value = JSON.parse(decodeURIComponent(cursor));
122
+ if (!Array.isArray(value) || value.length !== 2 || value[0] !== key || typeof value[1] !== 'string')
123
+ throw new Error();
124
+ after = catalogPath(value[1]);
125
+ }
126
+ catch {
127
+ throw new RevisionError('invalid', 'Invalid catalog cursor');
128
+ }
129
+ }
130
+ const sorted = entries.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0).filter(entry => entry.path > after);
131
+ const selected = sorted.slice(0, count), last = selected.at(-1);
132
+ return { entries: selected, ...(last && sorted.length > count ? { cursor: encodeURIComponent(JSON.stringify([key, last.path])) } : {}) };
133
+ }
134
+ return {
135
+ list: async (request, access) => {
136
+ const directory = catalogPath(request.directory ?? '', true);
137
+ return page(await collect(directory, false, access), `list:${directory}`, request.cursor, request.limit);
138
+ },
139
+ search: async (request, access) => {
140
+ if (typeof request.query !== 'string' || request.query.length > 1024)
141
+ throw new RevisionError('invalid', 'Invalid filename query');
142
+ const query = request.query.toLowerCase();
143
+ return page((await collect('', true, access)).filter(entry => entry.kind === 'file' && entry.path.toLowerCase().includes(query)), `search:${query}`, request.cursor, request.limit);
144
+ },
145
+ history: async (path, access) => { check(access); identifier(access.scopeId); return options.saves(catalogPath(path)); },
146
+ };
147
+ }
@@ -0,0 +1,8 @@
1
+ /** The host's identity of the workspace (the same shape as `WorkspaceIdentity` in `@hachej/boring-execution`). Pi's environment `id` is only a namespace. */
2
+ export interface WorkspaceKey {
3
+ readonly providerId: string;
4
+ readonly instanceId: string;
5
+ readonly incarnation: string;
6
+ readonly viewId: string;
7
+ }
8
+ //# sourceMappingURL=workspace-identity.d.ts.map
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,108 @@
1
+ import type { FileSystem } from '@earendil-works/pi-durable/env';
2
+ import type { PublicationLookupResult, PublicationResult, ReadResult, ResourceAccess, ResourceProvider } from './contracts.js';
3
+ import type { WorkspaceCatalogOptions } from './workspace-catalog.js';
4
+ import type { ChangeFeedRequest, WorkspaceCatalog, WorkspaceChangeEvent } from './revision-contracts.js';
5
+ export type { ChangeFeedRequest, WorkspaceChangeEvent } from './revision-contracts.js';
6
+ export type { WorkspaceCatalogOptions } from './workspace-catalog.js';
7
+ import type { HistorySource, WorkspaceJournal } from './journal.js';
8
+ import type { WorkspaceKey } from './workspace-identity.js';
9
+ export type { WorkspaceKey } from './workspace-identity.js';
10
+ export interface WorkspaceChange {
11
+ readonly path: string;
12
+ /** `null` when the file is absent (or, from a watcher, when the path is a directory). */
13
+ readonly revision: string | null;
14
+ /** `publish`: a conditional write. `agent`: `record` (a Pi `write` or `edit`). `poll`: found by hashing. `watch`: the file system reported it. */
15
+ readonly source: 'publish' | 'agent' | 'poll' | 'watch';
16
+ }
17
+ /** What `record` is told about an agent write (ruling 2: observed, never receipted). */
18
+ export interface AgentWrite {
19
+ readonly path: string;
20
+ /** The file before the write, when it existed: its revision and the bytes of that revision (kept in history as the replaced version). */
21
+ readonly before?: {
22
+ readonly revision: string;
23
+ readonly bytes: Uint8Array;
24
+ } | null | undefined;
25
+ /** The revision the write produced, as the caller observed it right after. */
26
+ readonly after: string;
27
+ readonly source: 'agent';
28
+ /** The Pi conversation that wrote it, carried on the change event. */
29
+ readonly conversationId?: string | undefined;
30
+ }
31
+ /** Runs one piece of work at a time, in order. A failed piece does not stop the ones behind it. */
32
+ export interface WorkspaceQueue {
33
+ readonly run: <Value>(work: () => Promise<Value>) => Promise<Value>;
34
+ }
35
+ export interface WorkspaceResourceProvider extends ResourceProvider {
36
+ readonly identity: WorkspaceKey;
37
+ readonly catalog: WorkspaceCatalog;
38
+ readonly publication: {
39
+ readonly publish: (request: Parameters<NonNullable<ResourceProvider['publication']>['publish']>[0], access: ResourceAccess) => Promise<PublicationResult>;
40
+ };
41
+ readonly reconciliation: {
42
+ readonly lookup: (operationId: string, access: ResourceAccess) => Promise<PublicationLookupResult>;
43
+ };
44
+ /** The workspace's mutation queue. Native tool calls that must not interleave with a conditional write run through it. */
45
+ readonly queue: WorkspaceQueue;
46
+ /**
47
+ * Record a write the agent already made through the native tools (Pi `write`/`edit`, called by the file guard right after the
48
+ * tool succeeded): the file's history gains the written revision (source `agent`, and the replaced one when `before` carries its
49
+ * bytes) and the change feed an `agent` event. It writes history rows only: no publish, no receipt, no operation or intent rows,
50
+ * so agent writes are observed, not published. It does not enter `queue` (the guard already runs inside it), so it is safe to call from queued work.
51
+ * When the bytes there no longer have the `after` revision (a shell wrote in between), only the change event is recorded.
52
+ */
53
+ readonly record: (write: AgentWrite, access: ResourceAccess) => Promise<void>;
54
+ /**
55
+ * The change feed: every write this provider makes or records, every change `poll` finds and, while someone is listening, every
56
+ * change the file system's `watch` reports (the SQLite file system reports its own connection's writes at once). Ordered by
57
+ * `seq` and replayable from `since` within a bounded window; a gap yields a `resnapshot` event. The iterator ends when
58
+ * `request.signal` or `access.signal` aborts, or the provider closes. Events are invalidation hints: read the bytes again.
59
+ *
60
+ * After the last listener leaves, the watcher stays open for `watchLingerMs` and then closes. Changes made while no watcher was
61
+ * open are unknown, so the first listener afterwards makes the feed append a `resnapshot`: a client that reconnects after a
62
+ * longer absence reloads everything once, and one that stays connected is never affected.
63
+ */
64
+ readonly changes: (request: ChangeFeedRequest, access: ResourceAccess) => AsyncIterable<WorkspaceChangeEvent>;
65
+ /** The seq of the newest change event (the cursor of "now" for `changes({ since })`). */
66
+ readonly changeHead: () => number;
67
+ /** Names this provider's lifetime; a cursor from another one (`ChangeFeedRequest.incarnation`) is answered with a `resnapshot`. */
68
+ readonly changeIncarnation: string;
69
+ /** Ends every change stream, closes the file watcher and drops the change listeners. Idempotent; the provider still reads and publishes. */
70
+ readonly close: () => void;
71
+ /** Aborts when the provider closes, so a change stream nobody is reading still ends and releases its lease. */
72
+ readonly lifetime: AbortSignal;
73
+ /** Revisions still retained for a file, newest first. */
74
+ readonly history: (path: string) => string[];
75
+ /** The same revisions with the time each was saved (milliseconds since the epoch; 0 when unknown), newest first. */
76
+ readonly saves: (path: string) => {
77
+ readonly revision: string;
78
+ readonly savedAt: number;
79
+ readonly source: HistorySource;
80
+ }[];
81
+ /** Read the file as it is now and retain that revision in its history (what `present` shows). Serialised with writes. */
82
+ readonly keep: (path: string, access: ResourceAccess) => Promise<ReadResult>;
83
+ /** Provider writes and polled detections. The listener must not throw. */
84
+ readonly onChange: (listener: (change: WorkspaceChange) => void) => () => void;
85
+ /**
86
+ * Hash each path now and report those that differ from the last revision this provider wrote or polled. Finds shell writes.
87
+ * Without `paths`, every file of the workspace (except `.git` and `node_modules`): the first such call takes the baseline and
88
+ * reports nothing; later calls also report new and removed files. A host whose file system cannot `watch` calls it at the end of
89
+ * each agent turn (after `bash`), so the change feed still sees shell writes.
90
+ */
91
+ readonly poll: (paths?: readonly string[], signal?: AbortSignal) => Promise<WorkspaceChange[]>;
92
+ }
93
+ export interface WorkspaceProviderOptions {
94
+ readonly identity: WorkspaceKey;
95
+ readonly fs: FileSystem;
96
+ readonly journal: WorkspaceJournal;
97
+ readonly catalog?: WorkspaceCatalogOptions;
98
+ /** How many change events the feed retains for replay (default 1024). An older cursor gets a `resnapshot`. */
99
+ readonly changeBuffer?: number;
100
+ /** How long the watcher stays open after the last change-feed listener leaves, in milliseconds (default 30 000). */
101
+ readonly watchLingerMs?: number;
102
+ }
103
+ /** The Git blob id of `bytes` (`sha1("blob <length>\0" + bytes)`), computed without Git. */
104
+ export declare function blobRevision(bytes: Uint8Array): Promise<string>;
105
+ /** Whether a path is one of the provider's temporary files (a lister hides them). */
106
+ export declare const isTemporary: (path: string) => boolean;
107
+ export declare function createWorkspaceProvider(options: WorkspaceProviderOptions): WorkspaceResourceProvider;
108
+ //# sourceMappingURL=workspace.d.ts.map