@immediately-run/sdk 0.16.0 → 0.18.0
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/boot.cjs +2 -7
- package/dist/boot.cjs.map +1 -1
- package/dist/boot.d.cts +4 -1
- package/dist/boot.d.ts +4 -1
- package/dist/boot.js +2 -7
- package/dist/boot.js.map +1 -1
- package/dist/components/FileRouter.cjs +1 -1
- package/dist/components/FileRouter.cjs.map +1 -1
- package/dist/components/FileRouter.js +1 -1
- package/dist/components/FileRouter.js.map +1 -1
- package/dist/components/defaults.cjs +15 -1
- package/dist/components/defaults.cjs.map +1 -1
- package/dist/components/defaults.d.cts +12 -0
- package/dist/components/defaults.d.ts +12 -0
- package/dist/components/defaults.js +15 -1
- package/dist/components/defaults.js.map +1 -1
- package/dist/debug.cjs +168 -0
- package/dist/debug.cjs.map +1 -0
- package/dist/debug.d.cts +22 -0
- package/dist/debug.d.ts +22 -0
- package/dist/debug.js +141 -0
- package/dist/debug.js.map +1 -0
- package/dist/fs.cjs +214 -0
- package/dist/fs.cjs.map +1 -0
- package/dist/fs.d.cts +109 -0
- package/dist/fs.d.ts +109 -0
- package/dist/fs.js +187 -0
- package/dist/fs.js.map +1 -0
- package/dist/index.cjs +4 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/version.cjs +1 -1
- package/dist/version.cjs.map +1 -1
- package/dist/version.d.cts +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
package/dist/debug.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { createPushChannel } from "./pushChannel";
|
|
2
|
+
import { sendMessage, addListener } from "./sandboxUtils";
|
|
3
|
+
const enabledChannel = createPushChannel({
|
|
4
|
+
pushType: "debug-enabled",
|
|
5
|
+
requestType: "request-debug-enabled",
|
|
6
|
+
initial: false,
|
|
7
|
+
parse: (msg) => typeof msg.enabled === "boolean" ? msg.enabled : void 0
|
|
8
|
+
});
|
|
9
|
+
const isDebugEnabled = () => enabledChannel.get();
|
|
10
|
+
const useDebugEnabled = () => enabledChannel.use();
|
|
11
|
+
const MAX_DATA_BYTES = 16 * 1024;
|
|
12
|
+
function safeData(data) {
|
|
13
|
+
if (data === void 0) return void 0;
|
|
14
|
+
try {
|
|
15
|
+
const json = JSON.stringify(data);
|
|
16
|
+
if (json === void 0) return "[unserializable]";
|
|
17
|
+
if (json.length > MAX_DATA_BYTES) return `[truncated ${json.length}B]`;
|
|
18
|
+
return JSON.parse(json);
|
|
19
|
+
} catch {
|
|
20
|
+
return "[unserializable]";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function log(level, message, data) {
|
|
24
|
+
if (!enabledChannel.get()) return;
|
|
25
|
+
try {
|
|
26
|
+
sendMessage("debug-log", { level, message: String(message), data: safeData(data) });
|
|
27
|
+
} catch {
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const ATTR_ALLOW = /* @__PURE__ */ new Set(["role", "aria-hidden", "data-theme", "data-active", "href", "type", "hidden"]);
|
|
31
|
+
const MAX_NODES = 2e3;
|
|
32
|
+
const MAX_DEPTH = 25;
|
|
33
|
+
const MAX_TEXT = 200;
|
|
34
|
+
function round(n) {
|
|
35
|
+
return Math.round(n);
|
|
36
|
+
}
|
|
37
|
+
function snapshotDom(params) {
|
|
38
|
+
if (typeof document === "undefined") return null;
|
|
39
|
+
const root = params.selector ? document.querySelector(params.selector) : document.body;
|
|
40
|
+
if (!root) return null;
|
|
41
|
+
const maxDepth = Math.min(params.maxDepth ?? MAX_DEPTH, MAX_DEPTH);
|
|
42
|
+
const maxNodes = Math.min(params.maxNodes ?? MAX_NODES, MAX_NODES);
|
|
43
|
+
let budget = maxNodes;
|
|
44
|
+
const walk = (el, depth) => {
|
|
45
|
+
budget--;
|
|
46
|
+
const r = el.getBoundingClientRect();
|
|
47
|
+
const classes = el.classList.length ? [...el.classList] : void 0;
|
|
48
|
+
const attrs = {};
|
|
49
|
+
for (const name of el.getAttributeNames()) {
|
|
50
|
+
if (ATTR_ALLOW.has(name)) attrs[name] = el.getAttribute(name) ?? "";
|
|
51
|
+
}
|
|
52
|
+
const ownText = [...el.childNodes].filter((n) => n.nodeType === 3).map((n) => (n.textContent ?? "").trim()).join(" ").trim();
|
|
53
|
+
const node = {
|
|
54
|
+
tag: el.tagName.toLowerCase(),
|
|
55
|
+
...el.id ? { id: el.id } : {},
|
|
56
|
+
...classes ? { classes } : {},
|
|
57
|
+
...Object.keys(attrs).length ? { attrs } : {},
|
|
58
|
+
rect: { x: round(r.x), y: round(r.y), w: round(r.width), h: round(r.height) },
|
|
59
|
+
...ownText ? { text: ownText.slice(0, MAX_TEXT) } : {}
|
|
60
|
+
};
|
|
61
|
+
if (depth < maxDepth && el.children.length && budget > 0) {
|
|
62
|
+
const children = [];
|
|
63
|
+
for (const child of el.children) {
|
|
64
|
+
if (budget <= 0) {
|
|
65
|
+
node.truncated = true;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
children.push(walk(child, depth + 1));
|
|
69
|
+
}
|
|
70
|
+
if (children.length) node.children = children;
|
|
71
|
+
} else if (el.children.length) {
|
|
72
|
+
node.truncated = true;
|
|
73
|
+
}
|
|
74
|
+
return node;
|
|
75
|
+
};
|
|
76
|
+
return walk(root, 0);
|
|
77
|
+
}
|
|
78
|
+
function computedStyle(params) {
|
|
79
|
+
if (typeof document === "undefined") return null;
|
|
80
|
+
const el = document.querySelector(params.selector);
|
|
81
|
+
if (!el) return null;
|
|
82
|
+
const cs = getComputedStyle(el);
|
|
83
|
+
const out = {};
|
|
84
|
+
for (const p of params.props.slice(0, 50)) out[p] = cs.getPropertyValue(p) || cs[p]?.toString?.() || "";
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
function rects(params) {
|
|
88
|
+
if (typeof document === "undefined") return [];
|
|
89
|
+
return [...document.querySelectorAll(params.selector)].slice(0, 200).map((el) => {
|
|
90
|
+
const r = el.getBoundingClientRect();
|
|
91
|
+
return { x: round(r.x), y: round(r.y), w: round(r.width), h: round(r.height) };
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
let responderStarted = false;
|
|
95
|
+
function startResponder() {
|
|
96
|
+
if (responderStarted || typeof window === "undefined") return;
|
|
97
|
+
responderStarted = true;
|
|
98
|
+
addListener("debug-query", (msg) => {
|
|
99
|
+
if (!enabledChannel.get()) return;
|
|
100
|
+
const id = msg.id;
|
|
101
|
+
const method = msg.method;
|
|
102
|
+
const params = msg.params ?? {};
|
|
103
|
+
let ok = true;
|
|
104
|
+
let result = null;
|
|
105
|
+
let error;
|
|
106
|
+
try {
|
|
107
|
+
switch (method) {
|
|
108
|
+
case "snapshotDom":
|
|
109
|
+
result = snapshotDom(params);
|
|
110
|
+
break;
|
|
111
|
+
case "computedStyle":
|
|
112
|
+
result = computedStyle(params);
|
|
113
|
+
break;
|
|
114
|
+
case "rect":
|
|
115
|
+
result = rects(params);
|
|
116
|
+
break;
|
|
117
|
+
default:
|
|
118
|
+
ok = false;
|
|
119
|
+
error = `unknown debug method: ${String(method)}`;
|
|
120
|
+
}
|
|
121
|
+
} catch (e) {
|
|
122
|
+
ok = false;
|
|
123
|
+
error = e instanceof Error ? e.message : String(e);
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
sendMessage("debug-query-result", { id, ok, result, error });
|
|
127
|
+
} catch {
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
enabledChannel.onChange((enabled) => {
|
|
132
|
+
if (enabled) startResponder();
|
|
133
|
+
});
|
|
134
|
+
const debug = { log, isEnabled: isDebugEnabled };
|
|
135
|
+
export {
|
|
136
|
+
debug,
|
|
137
|
+
isDebugEnabled,
|
|
138
|
+
log,
|
|
139
|
+
useDebugEnabled
|
|
140
|
+
};
|
|
141
|
+
//# sourceMappingURL=debug.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/debug.ts"],"sourcesContent":["// System-app devtools — the app-facing surface (plan: docs/plans/system-app-devtools.md).\n//\n// Two opt-in, DEV-ONLY instruments for debugging a sandboxed UI-as-app region:\n// 1. `debug.log(...)` — an app→host one-way log surfaced in the host dev panel\n// / CLI `/debug` stream (instead of hand-fishing console output out of a\n// cross-origin iframe's devtools).\n// 2. a READ-ONLY DOM/layout responder the host can query from outside — the\n// thing a cross-origin screenshot can't reliably give you (a blank capture\n// is ambiguous between a real 0-height collapse and a paint artifact).\n//\n// SECURITY (the gating constraint — see the plan's §0):\n// - Both are inert unless the HOST signals dev mode via the `debug-enabled`\n// channel. The host only sets it for a dev/override session (the `ir-dev-*`\n// deep link) or an explicit operator developer-mode. A published app served\n// to a normal user gets `enabled:false` → `debug.log` is a no-op and the\n// responder never answers. Production isolation is therefore unchanged.\n// - The responder is READ-ONLY with a fixed vocabulary (snapshotDom /\n// computedStyle / rect). There is deliberately NO eval bridge — that would\n// turn a debug aid into remote code execution into the sandbox.\n// - The responder reads only its OWN `document` (it lives in its own opaque\n// iframe and cannot reach a sibling app), so there is no app↔app leak even\n// in dev.\n// - Output is bounded (node/depth/text caps) so a query can't exfiltrate an\n// unbounded payload or wedge the app.\n//\n// Apps that want the strongest guarantee can additionally guard their own usage\n// behind `import.meta.env.DEV` so the calls are tree-shaken from prod bundles;\n// the runtime gate here is the backstop that holds regardless.\n\nimport { createPushChannel } from './pushChannel';\nimport { sendMessage, addListener } from './sandboxUtils';\n\n/** Severity of a {@link debug.log} entry. */\nexport type DebugLevel = 'debug' | 'info' | 'warn' | 'error';\n\n// ── Dev gate ────────────────────────────────────────────────────────────────\n// The host pushes `debug-enabled:true` only for a dev/override session. Until\n// then (and always in production) it stays false and every instrument is inert.\nconst enabledChannel = createPushChannel<boolean>({\n pushType: 'debug-enabled',\n requestType: 'request-debug-enabled',\n initial: false,\n parse: (msg) => (typeof msg.enabled === 'boolean' ? msg.enabled : undefined),\n});\n\n/** Is the host dev-debug surface active for this session? `false` in production. */\nexport const isDebugEnabled = (): boolean => enabledChannel.get();\n\n/** React hook: whether the host dev-debug surface is active (re-renders on change).\n * Handy for showing a debug affordance only when it would do something. */\nexport const useDebugEnabled = (): boolean => enabledChannel.use();\n\n// ── 1. App→host debug log ─────────────────────────────────────────────────────\n// Best-effort: a value that can't be structured-cloned is replaced with a marker\n// rather than throwing — `debug.log` must never break the app.\nconst MAX_DATA_BYTES = 16 * 1024;\n\nfunction safeData(data: unknown): unknown {\n if (data === undefined) return undefined;\n try {\n const json = JSON.stringify(data);\n if (json === undefined) return '[unserializable]';\n if (json.length > MAX_DATA_BYTES) return `[truncated ${json.length}B]`;\n return JSON.parse(json);\n } catch {\n return '[unserializable]';\n }\n}\n\n/**\n * Emit a structured debug entry to the host dev surface. A NO-OP unless the host\n * has enabled the dev-debug session ({@link isDebugEnabled}); in production it\n * does nothing and sends nothing.\n *\n * debug.log('info', 'mounted', { activeFile });\n */\nexport function log(level: DebugLevel, message: string, data?: unknown): void {\n if (!enabledChannel.get()) return; // inert in prod / non-dev sessions\n try {\n sendMessage('debug-log', { level, message: String(message), data: safeData(data) });\n } catch {\n /* transport not ready — drop silently; logging must never throw */\n }\n}\n\n// ── 2. Read-only DOM / layout responder ───────────────────────────────────────\n// The host sends `debug-query` { id, method, params }; we reply with\n// `debug-query-result` { id, ok, result | error }. Only ever active while the dev\n// gate is enabled. Vocabulary is fixed and read-only.\n\ninterface DomNode {\n tag: string;\n id?: string;\n classes?: string[];\n attrs?: Record<string, string>;\n rect?: { x: number; y: number; w: number; h: number };\n text?: string;\n children?: DomNode[];\n truncated?: true;\n}\n\nconst ATTR_ALLOW = new Set(['role', 'aria-hidden', 'data-theme', 'data-active', 'href', 'type', 'hidden']);\nconst MAX_NODES = 2000;\nconst MAX_DEPTH = 25;\nconst MAX_TEXT = 200;\n\nfunction round(n: number): number {\n return Math.round(n);\n}\n\nfunction snapshotDom(params: { selector?: string; maxDepth?: number; maxNodes?: number }): DomNode | null {\n if (typeof document === 'undefined') return null;\n const root = params.selector ? document.querySelector(params.selector) : document.body;\n if (!root) return null;\n const maxDepth = Math.min(params.maxDepth ?? MAX_DEPTH, MAX_DEPTH);\n const maxNodes = Math.min(params.maxNodes ?? MAX_NODES, MAX_NODES);\n let budget = maxNodes;\n\n const walk = (el: Element, depth: number): DomNode => {\n budget--;\n const r = el.getBoundingClientRect();\n const classes = el.classList.length ? [...el.classList] : undefined;\n const attrs: Record<string, string> = {};\n for (const name of el.getAttributeNames()) {\n if (ATTR_ALLOW.has(name)) attrs[name] = el.getAttribute(name) ?? '';\n }\n // Direct text (not descendants') so a leaf's label is visible without dumping\n // the whole subtree's text.\n const ownText = [...el.childNodes]\n .filter((n) => n.nodeType === 3)\n .map((n) => (n.textContent ?? '').trim())\n .join(' ')\n .trim();\n const node: DomNode = {\n tag: el.tagName.toLowerCase(),\n ...(el.id ? { id: el.id } : {}),\n ...(classes ? { classes } : {}),\n ...(Object.keys(attrs).length ? { attrs } : {}),\n rect: { x: round(r.x), y: round(r.y), w: round(r.width), h: round(r.height) },\n ...(ownText ? { text: ownText.slice(0, MAX_TEXT) } : {}),\n };\n if (depth < maxDepth && el.children.length && budget > 0) {\n const children: DomNode[] = [];\n for (const child of el.children) {\n if (budget <= 0) {\n node.truncated = true;\n break;\n }\n children.push(walk(child, depth + 1));\n }\n if (children.length) node.children = children;\n } else if (el.children.length) {\n node.truncated = true;\n }\n return node;\n };\n\n return walk(root, 0);\n}\n\nfunction computedStyle(params: { selector: string; props: string[] }): Record<string, string> | null {\n if (typeof document === 'undefined') return null;\n const el = document.querySelector(params.selector);\n if (!el) return null;\n const cs = getComputedStyle(el);\n const out: Record<string, string> = {};\n for (const p of params.props.slice(0, 50)) out[p] = cs.getPropertyValue(p) || cs[p as keyof CSSStyleDeclaration]?.toString?.() || '';\n return out;\n}\n\nfunction rects(params: { selector: string }): Array<{ x: number; y: number; w: number; h: number }> {\n if (typeof document === 'undefined') return [];\n return [...document.querySelectorAll(params.selector)].slice(0, 200).map((el) => {\n const r = el.getBoundingClientRect();\n return { x: round(r.x), y: round(r.y), w: round(r.width), h: round(r.height) };\n });\n}\n\nlet responderStarted = false;\n\n/** Wire the read-only DOM/layout responder. Idempotent; called lazily once the\n * dev gate turns on. No effect when `document` is absent (non-browser realm). */\nfunction startResponder(): void {\n if (responderStarted || typeof window === 'undefined') return;\n responderStarted = true;\n addListener('debug-query', (msg: { id?: unknown; method?: unknown; params?: unknown }) => {\n if (!enabledChannel.get()) return; // gate: ignore unless dev-enabled\n const id = msg.id;\n const method = msg.method;\n const params = (msg.params ?? {}) as Record<string, unknown>;\n let ok = true;\n let result: unknown = null;\n let error: string | undefined;\n try {\n switch (method) {\n case 'snapshotDom':\n result = snapshotDom(params as never);\n break;\n case 'computedStyle':\n result = computedStyle(params as never);\n break;\n case 'rect':\n result = rects(params as never);\n break;\n default:\n ok = false;\n error = `unknown debug method: ${String(method)}`;\n }\n } catch (e) {\n ok = false;\n error = e instanceof Error ? e.message : String(e);\n }\n try {\n sendMessage('debug-query-result', { id, ok, result, error });\n } catch {\n /* transport gone — nothing to do */\n }\n });\n}\n\n// Start the responder as soon as the gate flips on (and not before).\nenabledChannel.onChange((enabled) => {\n if (enabled) startResponder();\n});\n\n/** The dev-only debug surface. Inert unless the host enables it ({@link isDebugEnabled}). */\nexport const debug = { log, isEnabled: isDebugEnabled } as const;\n"],"mappings":"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","names":[]}
|
package/dist/fs.cjs
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var fs_exports = {};
|
|
20
|
+
__export(fs_exports, {
|
|
21
|
+
fsAvailable: () => fsAvailable,
|
|
22
|
+
openAppFs: () => openAppFs,
|
|
23
|
+
openFs: () => openFs,
|
|
24
|
+
sandboxFs: () => sandboxFs
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(fs_exports);
|
|
27
|
+
var import_mounts = require("./mounts");
|
|
28
|
+
const hasFs = (fs) => typeof fs?.promises?.readFile === "function" || typeof fs?.readFile === "function";
|
|
29
|
+
function sandboxFs() {
|
|
30
|
+
try {
|
|
31
|
+
const shared = globalThis.__sandpackSharedFs;
|
|
32
|
+
if (hasFs(shared)) return shared;
|
|
33
|
+
} catch {
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const layers = module?.evaluation?.module?.bundler?.fs?.layers;
|
|
37
|
+
if (Array.isArray(layers)) {
|
|
38
|
+
for (const layer of layers) {
|
|
39
|
+
const fs = layer?.boundContext?.fs;
|
|
40
|
+
if (hasFs(fs)) return fs;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
function fsAvailable() {
|
|
48
|
+
return sandboxFs() != null;
|
|
49
|
+
}
|
|
50
|
+
const ERRNO = {
|
|
51
|
+
ENOENT: "not-found",
|
|
52
|
+
EROFS: "read-only",
|
|
53
|
+
EACCES: "not-permitted",
|
|
54
|
+
EPERM: "not-permitted",
|
|
55
|
+
EEXIST: "exists",
|
|
56
|
+
ENOTEMPTY: "not-empty"
|
|
57
|
+
};
|
|
58
|
+
const fsError = (code, message) => {
|
|
59
|
+
const err = new Error(message);
|
|
60
|
+
err.code = code;
|
|
61
|
+
return err;
|
|
62
|
+
};
|
|
63
|
+
const mapError = (e) => {
|
|
64
|
+
const errno = e?.code;
|
|
65
|
+
const code = (errno ? ERRNO[errno] : void 0) ?? "unknown";
|
|
66
|
+
const err = new Error(e?.message ?? "fs operation failed");
|
|
67
|
+
err.code = code;
|
|
68
|
+
return err;
|
|
69
|
+
};
|
|
70
|
+
const decoder = new TextDecoder();
|
|
71
|
+
const encoder = new TextEncoder();
|
|
72
|
+
const resolveUnder = (root, relPath) => {
|
|
73
|
+
if (relPath.startsWith("/")) {
|
|
74
|
+
throw fsError("invalid-path", `expected a mount-relative path, got absolute "${relPath}"`);
|
|
75
|
+
}
|
|
76
|
+
const parts = [];
|
|
77
|
+
for (const seg of relPath.split("/")) {
|
|
78
|
+
if (seg === "" || seg === ".") continue;
|
|
79
|
+
if (seg === "..") {
|
|
80
|
+
throw fsError("invalid-path", `"${relPath}" escapes the mount root`);
|
|
81
|
+
}
|
|
82
|
+
parts.push(seg);
|
|
83
|
+
}
|
|
84
|
+
const base = root.endsWith("/") ? root.slice(0, -1) : root;
|
|
85
|
+
return parts.length ? `${base}/${parts.join("/")}` : base;
|
|
86
|
+
};
|
|
87
|
+
const writableAt = (mount, relPath) => {
|
|
88
|
+
const path = "/" + relPath.split("/").filter((s) => s && s !== ".").join("/");
|
|
89
|
+
const rules = mount.rules;
|
|
90
|
+
if (rules && rules.length) {
|
|
91
|
+
let best;
|
|
92
|
+
for (const r of rules) {
|
|
93
|
+
const sub = r.subtree.endsWith("/") ? r.subtree : r.subtree + "/";
|
|
94
|
+
if (path === r.subtree || path.startsWith(sub) || r.subtree === "/") {
|
|
95
|
+
if (!best || r.subtree.length > best.subtree.length) best = r;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (best) return best.mode === "rw";
|
|
99
|
+
}
|
|
100
|
+
return (mount.mode ?? "rw") === "rw";
|
|
101
|
+
};
|
|
102
|
+
const promisesOf = (port) => port.promises ?? port;
|
|
103
|
+
function openFs(mount) {
|
|
104
|
+
const root = mount.path;
|
|
105
|
+
const port = () => {
|
|
106
|
+
const p = sandboxFs();
|
|
107
|
+
if (!p) throw fsError("unavailable", "immediately.run: sandbox filesystem unavailable");
|
|
108
|
+
return promisesOf(p);
|
|
109
|
+
};
|
|
110
|
+
const api = {
|
|
111
|
+
mount,
|
|
112
|
+
async readFile(relPath, encoding) {
|
|
113
|
+
const p = port();
|
|
114
|
+
const abs = resolveUnder(root, relPath);
|
|
115
|
+
try {
|
|
116
|
+
const data = await p.readFile(abs);
|
|
117
|
+
const bytes = typeof data === "string" ? encoder.encode(data) : data;
|
|
118
|
+
return encoding === "utf8" ? decoder.decode(bytes) : bytes;
|
|
119
|
+
} catch (e) {
|
|
120
|
+
throw mapError(e);
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
async writeFile(relPath, data) {
|
|
124
|
+
const p = port();
|
|
125
|
+
const abs = resolveUnder(root, relPath);
|
|
126
|
+
try {
|
|
127
|
+
await p.writeFile(abs, typeof data === "string" ? encoder.encode(data) : data);
|
|
128
|
+
} catch (e) {
|
|
129
|
+
throw mapError(e);
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
async readdir(relPath = "") {
|
|
133
|
+
const p = port();
|
|
134
|
+
const abs = resolveUnder(root, relPath);
|
|
135
|
+
try {
|
|
136
|
+
const entries = await p.readdir(abs, { withFileTypes: true });
|
|
137
|
+
return entries.map(
|
|
138
|
+
(d) => typeof d === "string" ? { name: d, kind: "file" } : { name: d.name, kind: d.isDirectory?.() ? "dir" : "file" }
|
|
139
|
+
);
|
|
140
|
+
} catch (e) {
|
|
141
|
+
throw mapError(e);
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
async stat(relPath) {
|
|
145
|
+
const p = port();
|
|
146
|
+
const abs = resolveUnder(root, relPath);
|
|
147
|
+
try {
|
|
148
|
+
const s = await p.stat(abs);
|
|
149
|
+
return {
|
|
150
|
+
kind: s.isDirectory?.() ? "dir" : "file",
|
|
151
|
+
size: typeof s.size === "number" ? s.size : 0,
|
|
152
|
+
mtimeMs: typeof s.mtimeMs === "number" ? s.mtimeMs : void 0
|
|
153
|
+
};
|
|
154
|
+
} catch (e) {
|
|
155
|
+
throw mapError(e);
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
async exists(relPath) {
|
|
159
|
+
try {
|
|
160
|
+
await api.stat(relPath);
|
|
161
|
+
return true;
|
|
162
|
+
} catch (e) {
|
|
163
|
+
if (e.code === "not-found") return false;
|
|
164
|
+
if (e.code === "unavailable" || e.code === "invalid-path") {
|
|
165
|
+
throw e;
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
async mkdir(relPath, opts) {
|
|
171
|
+
const p = port();
|
|
172
|
+
const abs = resolveUnder(root, relPath);
|
|
173
|
+
try {
|
|
174
|
+
await p.mkdir(abs, { recursive: opts?.recursive ?? false });
|
|
175
|
+
} catch (e) {
|
|
176
|
+
throw mapError(e);
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
async rm(relPath, opts) {
|
|
180
|
+
const p = port();
|
|
181
|
+
const abs = resolveUnder(root, relPath);
|
|
182
|
+
try {
|
|
183
|
+
await p.rm(abs, { recursive: opts?.recursive ?? false });
|
|
184
|
+
} catch (e) {
|
|
185
|
+
throw mapError(e);
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
async rename(fromRel, toRel) {
|
|
189
|
+
const p = port();
|
|
190
|
+
const from = resolveUnder(root, fromRel);
|
|
191
|
+
const to = resolveUnder(root, toRel);
|
|
192
|
+
try {
|
|
193
|
+
await p.rename(from, to);
|
|
194
|
+
} catch (e) {
|
|
195
|
+
throw mapError(e);
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
canWrite(relPath = "") {
|
|
199
|
+
return writableAt(mount, relPath);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
return api;
|
|
203
|
+
}
|
|
204
|
+
function openAppFs() {
|
|
205
|
+
return openFs({ path: (0, import_mounts.getAppMountPath)(), type: "repo" });
|
|
206
|
+
}
|
|
207
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
208
|
+
0 && (module.exports = {
|
|
209
|
+
fsAvailable,
|
|
210
|
+
openAppFs,
|
|
211
|
+
openFs,
|
|
212
|
+
sandboxFs
|
|
213
|
+
});
|
|
214
|
+
//# sourceMappingURL=fs.cjs.map
|
package/dist/fs.cjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/fs.ts"],"sourcesContent":["// Typed, discoverable filesystem access — the app-facing surface for the ZenFS\n// mount ports (SDK_FS_SURFACE_SPEC; FILESYSTEM_SPEC §2 the ZenFS-shaped contract).\n//\n// The single most important thing an app does — read/write files in its mounts —\n// previously had NO SDK surface: apps reached an ambient `globalThis.__sandpackSharedFs`\n// by hand-rolling the same accessor (editor/file-explorer `src/fs/mountFs.ts`, \"keep the\n// two in sync\"), with a documented footgun (`module.evaluation.module.bundler.fs` is the\n// WRONG object — it has no `promises`/`stat`). This module is that accessor's ONE home,\n// typed and documented.\n//\n// It adds NO authority: the ZenFS port is already minted and chroot/`ro`-enforced\n// host-side (FILESYSTEM_SPEC §2, UI_AS_APPS §8.7). This is typing + discoverability +\n// de-duplication only. `fs` is a Resource PORT (a byte channel), not a host-brokered RPC,\n// so — unlike the `invoke()` catalog surface — it is hand-written, not gate-table-derived.\nimport type { SandboxMount, MountRule } from './mounts';\nimport { getAppMountPath } from './mounts';\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\n/** The node-compatible promises surface the sandbox ZenFS exposes (the subset we use). */\ninterface NodeFsPromises {\n readFile(path: string, encoding?: any): Promise<string | Uint8Array>;\n writeFile(path: string, data: string | Uint8Array): Promise<void>;\n readdir(path: string, opts?: any): Promise<any[]>;\n stat(path: string): Promise<any>;\n mkdir(path: string, opts?: any): Promise<unknown>;\n rm(path: string, opts?: any): Promise<void>;\n rename(from: string, to: string): Promise<void>;\n}\n\n/** The resolved sandbox ZenFS handle (node-compatible, `/`-rooted). Opaque to apps —\n * reach it through {@link openFs}; the raw handle is the {@link sandboxFs} escape hatch. */\nexport interface SandboxFsPort {\n promises?: NodeFsPromises;\n readFile?: NodeFsPromises['readFile'];\n}\n\nconst hasFs = (fs: any): boolean =>\n typeof fs?.promises?.readFile === 'function' || typeof fs?.readFile === 'function';\n\n/**\n * The resolved sandbox ZenFS, or `null` when unavailable. The ONE home for the\n * resolution order previously duplicated in every app's `mountFs.ts`:\n *\n * 1. `globalThis.__sandpackSharedFs` — the `/`-rooted bound ZenFS the sandbox publishes.\n * 2. fallback: the first `module.evaluation.module.bundler.fs.layers[].boundContext.fs`\n * whose surface has `readFile` (the bundler ZenFS-layer bound context).\n * 3. else `null` (local `vite dev` / before boot).\n *\n * Prefer {@link openFs}; reach for this only when a system app spans mounts in absolute\n * `/mnt/{hash}` paths (the file explorer / editor).\n */\nexport function sandboxFs(): SandboxFsPort | null {\n try {\n const shared = (globalThis as any).__sandpackSharedFs;\n if (hasFs(shared)) return shared as SandboxFsPort;\n } catch {\n /* not in the sandbox */\n }\n try {\n // @ts-ignore - `module` is injected by the sandbox runtime (see sandboxUtils transport).\n const layers = module?.evaluation?.module?.bundler?.fs?.layers;\n if (Array.isArray(layers)) {\n for (const layer of layers) {\n const fs = layer?.boundContext?.fs;\n if (hasFs(fs)) return fs as SandboxFsPort;\n }\n }\n } catch {\n /* not in the sandbox */\n }\n return null;\n}\n\n/** Is the sandbox filesystem reachable at all? `false` in local `vite dev` and before\n * boot — gate file affordances on it so an app degrades instead of throwing. */\nexport function fsAvailable(): boolean {\n return sandboxFs() != null;\n}\n\n/** A directory entry from {@link MountFs.readdir}. */\nexport interface DirEntry {\n name: string;\n kind: 'file' | 'dir';\n}\n\n/** A stat result from {@link MountFs.stat}. */\nexport interface FileStat {\n kind: 'file' | 'dir';\n size: number;\n mtimeMs?: number;\n}\n\n/** An error from a {@link MountFs} operation, carrying a machine-readable `.code`\n * (mapped from the ZenFS errno) so an app branches on `.code`, never on a message. */\nexport interface FsError extends Error {\n code:\n | 'not-found' // ENOENT\n | 'read-only' // EROFS — a `ro` mount / downgraded role; NEVER surface as UX (gate with canWrite)\n | 'not-permitted' // EACCES\n | 'exists' // EEXIST\n | 'not-empty' // ENOTEMPTY\n | 'invalid-path' // a `..` segment / absolute escape was passed as a relPath\n | 'unavailable' // no sandbox fs (local dev / pre-boot)\n | 'unknown';\n}\n\nconst ERRNO: Record<string, FsError['code']> = {\n ENOENT: 'not-found',\n EROFS: 'read-only',\n EACCES: 'not-permitted',\n EPERM: 'not-permitted',\n EEXIST: 'exists',\n ENOTEMPTY: 'not-empty',\n};\n\nconst fsError = (code: FsError['code'], message: string): FsError => {\n const err = new Error(message) as FsError;\n err.code = code;\n return err;\n};\n\nconst mapError = (e: unknown): FsError => {\n const errno = (e as { code?: string } | null)?.code;\n const code: FsError['code'] = (errno ? ERRNO[errno] : undefined) ?? 'unknown';\n const err = new Error((e as Error)?.message ?? 'fs operation failed') as FsError;\n err.code = code;\n return err;\n};\n\nconst decoder = new TextDecoder();\nconst encoder = new TextEncoder();\n\n// Join a mount-RELATIVE path under the mount root, rejecting `..` escapes and absolute\n// paths (CLAUDE.md security rule 3 — don't probe for escapes). The host chroot is the\n// real enforcer; this keeps an honest app from accidentally naming outside its grant.\nconst resolveUnder = (root: string, relPath: string): string => {\n if (relPath.startsWith('/')) {\n throw fsError('invalid-path', `expected a mount-relative path, got absolute \"${relPath}\"`);\n }\n const parts: string[] = [];\n for (const seg of relPath.split('/')) {\n if (seg === '' || seg === '.') continue;\n if (seg === '..') {\n throw fsError('invalid-path', `\"${relPath}\" escapes the mount root`);\n }\n parts.push(seg);\n }\n const base = root.endsWith('/') ? root.slice(0, -1) : root;\n return parts.length ? `${base}/${parts.join('/')}` : base;\n};\n\n// The longest matching `rules` subtree governs a path (mounts.ts MountRule); fall back to\n// the whole-mount `mode`. A CLIENT-SIDE hint mirroring the host rule — EROFS stays\n// authoritative (the host re-checks live policy on every write).\nconst writableAt = (mount: SandboxMount, relPath: string): boolean => {\n const path = '/' + relPath.split('/').filter((s) => s && s !== '.').join('/');\n const rules: MountRule[] | undefined = mount.rules;\n if (rules && rules.length) {\n let best: MountRule | undefined;\n for (const r of rules) {\n const sub = r.subtree.endsWith('/') ? r.subtree : r.subtree + '/';\n if (path === r.subtree || path.startsWith(sub) || r.subtree === '/') {\n if (!best || r.subtree.length > best.subtree.length) best = r;\n }\n }\n if (best) return best.mode === 'rw';\n }\n return (mount.mode ?? 'rw') === 'rw';\n};\n\n/** A mount-anchored, typed filesystem view. All paths are RELATIVE to the mount root;\n * the accessor resolves them under `mount.path`. Async-only (ZenFS rides a MessagePort).\n * Obtain one with {@link openFs}. */\nexport interface MountFs {\n /** The mount this view is anchored to (read `mode`/`rules` for writability). */\n readonly mount: SandboxMount;\n /** Read a file as UTF-8 text (`encoding: 'utf8'`) or raw bytes (omit encoding). */\n readFile(relPath: string, encoding: 'utf8'): Promise<string>;\n readFile(relPath: string): Promise<Uint8Array>;\n /** Write text or bytes, creating or truncating the file. Throws `read-only` on a `ro` mount. */\n writeFile(relPath: string, data: string | Uint8Array): Promise<void>;\n /** List a directory (the mount root when `relPath` is omitted). */\n readdir(relPath?: string): Promise<DirEntry[]>;\n /** Stat a path. Throws `not-found` if absent. */\n stat(relPath: string): Promise<FileStat>;\n /** Does `relPath` exist? Never throws on absence. */\n exists(relPath: string): Promise<boolean>;\n /** Create a directory (pass `{ recursive: true }` to make parents). */\n mkdir(relPath: string, opts?: { recursive?: boolean }): Promise<void>;\n /** Remove a file, or a directory with `{ recursive: true }`. */\n rm(relPath: string, opts?: { recursive?: boolean }): Promise<void>;\n /** Rename/move within the mount. */\n rename(fromRel: string, toRel: string): Promise<void>;\n /** Client-side writability hint for `relPath` (mount `mode` ∩ longest-matching `rule`),\n * so an app can hide an \"edit\" affordance instead of catching `read-only`\n * (EDITOR_FIRST_EDITING_SPEC §3). Re-evaluate on `onMountsChange` — a role downgrade\n * flips it. EROFS from the host stays authoritative. */\n canWrite(relPath?: string): boolean;\n}\n\nconst promisesOf = (port: SandboxFsPort): NodeFsPromises =>\n (port.promises ?? (port as unknown as NodeFsPromises));\n\n/**\n * Open a typed, mount-anchored filesystem view (SDK_FS_SURFACE_SPEC §2.1). Pure-client:\n * resolves the ambient ZenFS once ({@link sandboxFs}) and binds it to `mount.path`, so you\n * read/write with paths RELATIVE to the mount root — you cannot accidentally name a path\n * outside your grant (a `..`/absolute path throws `invalid-path`; the host chroot is the\n * real enforcer).\n *\n * ```ts\n * import { mountSpace } from '@immediately-run/sdk';\n * import { openFs } from '@immediately-run/sdk/fs';\n * const fs = openFs(await mountSpace({ spaceId }));\n * const text = await fs.readFile('notes/idea.mdx', 'utf8');\n * if (fs.canWrite('notes/idea.mdx')) await fs.writeFile('notes/idea.mdx', text);\n * ```\n *\n * Throws {@link FsError} `unavailable` if the sandbox fs is not present (local `vite dev`\n * / before boot — gate with {@link fsAvailable}). Per-op failures throw {@link FsError}\n * with a mapped `.code` (`not-found`, `read-only`, …).\n */\nexport function openFs(mount: SandboxMount): MountFs {\n const root = mount.path;\n\n const port = (): NodeFsPromises => {\n const p = sandboxFs();\n if (!p) throw fsError('unavailable', 'immediately.run: sandbox filesystem unavailable');\n return promisesOf(p);\n };\n\n const api: MountFs = {\n mount,\n async readFile(relPath: string, encoding?: 'utf8'): Promise<any> {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n const data = await p.readFile(abs);\n const bytes =\n typeof data === 'string' ? encoder.encode(data) : (data as Uint8Array);\n return encoding === 'utf8' ? decoder.decode(bytes) : bytes;\n } catch (e) {\n throw mapError(e);\n }\n },\n async writeFile(relPath, data) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n await p.writeFile(abs, typeof data === 'string' ? encoder.encode(data) : data);\n } catch (e) {\n throw mapError(e);\n }\n },\n async readdir(relPath = '') {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n const entries = await p.readdir(abs, { withFileTypes: true });\n return entries.map((d: any) =>\n typeof d === 'string'\n ? ({ name: d, kind: 'file' } as DirEntry)\n : ({ name: d.name, kind: d.isDirectory?.() ? 'dir' : 'file' } as DirEntry),\n );\n } catch (e) {\n throw mapError(e);\n }\n },\n async stat(relPath) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n const s: any = await p.stat(abs);\n return {\n kind: s.isDirectory?.() ? 'dir' : 'file',\n size: typeof s.size === 'number' ? s.size : 0,\n mtimeMs: typeof s.mtimeMs === 'number' ? s.mtimeMs : undefined,\n };\n } catch (e) {\n throw mapError(e);\n }\n },\n async exists(relPath) {\n try {\n await api.stat(relPath);\n return true;\n } catch (e) {\n if ((e as FsError).code === 'not-found') return false;\n if ((e as FsError).code === 'unavailable' || (e as FsError).code === 'invalid-path') {\n throw e;\n }\n return false;\n }\n },\n async mkdir(relPath, opts) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n await p.mkdir(abs, { recursive: opts?.recursive ?? false });\n } catch (e) {\n throw mapError(e);\n }\n },\n async rm(relPath, opts) {\n const p = port();\n const abs = resolveUnder(root, relPath);\n try {\n await p.rm(abs, { recursive: opts?.recursive ?? false });\n } catch (e) {\n throw mapError(e);\n }\n },\n async rename(fromRel, toRel) {\n const p = port();\n const from = resolveUnder(root, fromRel);\n const to = resolveUnder(root, toRel);\n try {\n await p.rename(from, to);\n } catch (e) {\n throw mapError(e);\n }\n },\n canWrite(relPath = '') {\n return writableAt(mount, relPath);\n },\n };\n return api;\n}\n\n/** Open a mount-anchored view of this app's OWN repository working tree — a convenience\n * over {@link openFs} using `getAppMountPath()` (FILE_SHARING_SPEC §11.2). */\nexport function openAppFs(): MountFs {\n return openFs({ path: getAppMountPath(), type: 'repo' } as SandboxMount);\n}\n"],"mappings":"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","names":[]}
|
package/dist/fs.d.cts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { SandboxMount } from './mounts.cjs';
|
|
2
|
+
import './tasks.cjs';
|
|
3
|
+
|
|
4
|
+
/** The node-compatible promises surface the sandbox ZenFS exposes (the subset we use). */
|
|
5
|
+
interface NodeFsPromises {
|
|
6
|
+
readFile(path: string, encoding?: any): Promise<string | Uint8Array>;
|
|
7
|
+
writeFile(path: string, data: string | Uint8Array): Promise<void>;
|
|
8
|
+
readdir(path: string, opts?: any): Promise<any[]>;
|
|
9
|
+
stat(path: string): Promise<any>;
|
|
10
|
+
mkdir(path: string, opts?: any): Promise<unknown>;
|
|
11
|
+
rm(path: string, opts?: any): Promise<void>;
|
|
12
|
+
rename(from: string, to: string): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
/** The resolved sandbox ZenFS handle (node-compatible, `/`-rooted). Opaque to apps —
|
|
15
|
+
* reach it through {@link openFs}; the raw handle is the {@link sandboxFs} escape hatch. */
|
|
16
|
+
interface SandboxFsPort {
|
|
17
|
+
promises?: NodeFsPromises;
|
|
18
|
+
readFile?: NodeFsPromises['readFile'];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The resolved sandbox ZenFS, or `null` when unavailable. The ONE home for the
|
|
22
|
+
* resolution order previously duplicated in every app's `mountFs.ts`:
|
|
23
|
+
*
|
|
24
|
+
* 1. `globalThis.__sandpackSharedFs` — the `/`-rooted bound ZenFS the sandbox publishes.
|
|
25
|
+
* 2. fallback: the first `module.evaluation.module.bundler.fs.layers[].boundContext.fs`
|
|
26
|
+
* whose surface has `readFile` (the bundler ZenFS-layer bound context).
|
|
27
|
+
* 3. else `null` (local `vite dev` / before boot).
|
|
28
|
+
*
|
|
29
|
+
* Prefer {@link openFs}; reach for this only when a system app spans mounts in absolute
|
|
30
|
+
* `/mnt/{hash}` paths (the file explorer / editor).
|
|
31
|
+
*/
|
|
32
|
+
declare function sandboxFs(): SandboxFsPort | null;
|
|
33
|
+
/** Is the sandbox filesystem reachable at all? `false` in local `vite dev` and before
|
|
34
|
+
* boot — gate file affordances on it so an app degrades instead of throwing. */
|
|
35
|
+
declare function fsAvailable(): boolean;
|
|
36
|
+
/** A directory entry from {@link MountFs.readdir}. */
|
|
37
|
+
interface DirEntry {
|
|
38
|
+
name: string;
|
|
39
|
+
kind: 'file' | 'dir';
|
|
40
|
+
}
|
|
41
|
+
/** A stat result from {@link MountFs.stat}. */
|
|
42
|
+
interface FileStat {
|
|
43
|
+
kind: 'file' | 'dir';
|
|
44
|
+
size: number;
|
|
45
|
+
mtimeMs?: number;
|
|
46
|
+
}
|
|
47
|
+
/** An error from a {@link MountFs} operation, carrying a machine-readable `.code`
|
|
48
|
+
* (mapped from the ZenFS errno) so an app branches on `.code`, never on a message. */
|
|
49
|
+
interface FsError extends Error {
|
|
50
|
+
code: 'not-found' | 'read-only' | 'not-permitted' | 'exists' | 'not-empty' | 'invalid-path' | 'unavailable' | 'unknown';
|
|
51
|
+
}
|
|
52
|
+
/** A mount-anchored, typed filesystem view. All paths are RELATIVE to the mount root;
|
|
53
|
+
* the accessor resolves them under `mount.path`. Async-only (ZenFS rides a MessagePort).
|
|
54
|
+
* Obtain one with {@link openFs}. */
|
|
55
|
+
interface MountFs {
|
|
56
|
+
/** The mount this view is anchored to (read `mode`/`rules` for writability). */
|
|
57
|
+
readonly mount: SandboxMount;
|
|
58
|
+
/** Read a file as UTF-8 text (`encoding: 'utf8'`) or raw bytes (omit encoding). */
|
|
59
|
+
readFile(relPath: string, encoding: 'utf8'): Promise<string>;
|
|
60
|
+
readFile(relPath: string): Promise<Uint8Array>;
|
|
61
|
+
/** Write text or bytes, creating or truncating the file. Throws `read-only` on a `ro` mount. */
|
|
62
|
+
writeFile(relPath: string, data: string | Uint8Array): Promise<void>;
|
|
63
|
+
/** List a directory (the mount root when `relPath` is omitted). */
|
|
64
|
+
readdir(relPath?: string): Promise<DirEntry[]>;
|
|
65
|
+
/** Stat a path. Throws `not-found` if absent. */
|
|
66
|
+
stat(relPath: string): Promise<FileStat>;
|
|
67
|
+
/** Does `relPath` exist? Never throws on absence. */
|
|
68
|
+
exists(relPath: string): Promise<boolean>;
|
|
69
|
+
/** Create a directory (pass `{ recursive: true }` to make parents). */
|
|
70
|
+
mkdir(relPath: string, opts?: {
|
|
71
|
+
recursive?: boolean;
|
|
72
|
+
}): Promise<void>;
|
|
73
|
+
/** Remove a file, or a directory with `{ recursive: true }`. */
|
|
74
|
+
rm(relPath: string, opts?: {
|
|
75
|
+
recursive?: boolean;
|
|
76
|
+
}): Promise<void>;
|
|
77
|
+
/** Rename/move within the mount. */
|
|
78
|
+
rename(fromRel: string, toRel: string): Promise<void>;
|
|
79
|
+
/** Client-side writability hint for `relPath` (mount `mode` ∩ longest-matching `rule`),
|
|
80
|
+
* so an app can hide an "edit" affordance instead of catching `read-only`
|
|
81
|
+
* (EDITOR_FIRST_EDITING_SPEC §3). Re-evaluate on `onMountsChange` — a role downgrade
|
|
82
|
+
* flips it. EROFS from the host stays authoritative. */
|
|
83
|
+
canWrite(relPath?: string): boolean;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Open a typed, mount-anchored filesystem view (SDK_FS_SURFACE_SPEC §2.1). Pure-client:
|
|
87
|
+
* resolves the ambient ZenFS once ({@link sandboxFs}) and binds it to `mount.path`, so you
|
|
88
|
+
* read/write with paths RELATIVE to the mount root — you cannot accidentally name a path
|
|
89
|
+
* outside your grant (a `..`/absolute path throws `invalid-path`; the host chroot is the
|
|
90
|
+
* real enforcer).
|
|
91
|
+
*
|
|
92
|
+
* ```ts
|
|
93
|
+
* import { mountSpace } from '@immediately-run/sdk';
|
|
94
|
+
* import { openFs } from '@immediately-run/sdk/fs';
|
|
95
|
+
* const fs = openFs(await mountSpace({ spaceId }));
|
|
96
|
+
* const text = await fs.readFile('notes/idea.mdx', 'utf8');
|
|
97
|
+
* if (fs.canWrite('notes/idea.mdx')) await fs.writeFile('notes/idea.mdx', text);
|
|
98
|
+
* ```
|
|
99
|
+
*
|
|
100
|
+
* Throws {@link FsError} `unavailable` if the sandbox fs is not present (local `vite dev`
|
|
101
|
+
* / before boot — gate with {@link fsAvailable}). Per-op failures throw {@link FsError}
|
|
102
|
+
* with a mapped `.code` (`not-found`, `read-only`, …).
|
|
103
|
+
*/
|
|
104
|
+
declare function openFs(mount: SandboxMount): MountFs;
|
|
105
|
+
/** Open a mount-anchored view of this app's OWN repository working tree — a convenience
|
|
106
|
+
* over {@link openFs} using `getAppMountPath()` (FILE_SHARING_SPEC §11.2). */
|
|
107
|
+
declare function openAppFs(): MountFs;
|
|
108
|
+
|
|
109
|
+
export { type DirEntry, type FileStat, type FsError, type MountFs, type SandboxFsPort, fsAvailable, openAppFs, openFs, sandboxFs };
|
package/dist/fs.d.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { SandboxMount } from './mounts.js';
|
|
2
|
+
import './tasks.js';
|
|
3
|
+
|
|
4
|
+
/** The node-compatible promises surface the sandbox ZenFS exposes (the subset we use). */
|
|
5
|
+
interface NodeFsPromises {
|
|
6
|
+
readFile(path: string, encoding?: any): Promise<string | Uint8Array>;
|
|
7
|
+
writeFile(path: string, data: string | Uint8Array): Promise<void>;
|
|
8
|
+
readdir(path: string, opts?: any): Promise<any[]>;
|
|
9
|
+
stat(path: string): Promise<any>;
|
|
10
|
+
mkdir(path: string, opts?: any): Promise<unknown>;
|
|
11
|
+
rm(path: string, opts?: any): Promise<void>;
|
|
12
|
+
rename(from: string, to: string): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
/** The resolved sandbox ZenFS handle (node-compatible, `/`-rooted). Opaque to apps —
|
|
15
|
+
* reach it through {@link openFs}; the raw handle is the {@link sandboxFs} escape hatch. */
|
|
16
|
+
interface SandboxFsPort {
|
|
17
|
+
promises?: NodeFsPromises;
|
|
18
|
+
readFile?: NodeFsPromises['readFile'];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The resolved sandbox ZenFS, or `null` when unavailable. The ONE home for the
|
|
22
|
+
* resolution order previously duplicated in every app's `mountFs.ts`:
|
|
23
|
+
*
|
|
24
|
+
* 1. `globalThis.__sandpackSharedFs` — the `/`-rooted bound ZenFS the sandbox publishes.
|
|
25
|
+
* 2. fallback: the first `module.evaluation.module.bundler.fs.layers[].boundContext.fs`
|
|
26
|
+
* whose surface has `readFile` (the bundler ZenFS-layer bound context).
|
|
27
|
+
* 3. else `null` (local `vite dev` / before boot).
|
|
28
|
+
*
|
|
29
|
+
* Prefer {@link openFs}; reach for this only when a system app spans mounts in absolute
|
|
30
|
+
* `/mnt/{hash}` paths (the file explorer / editor).
|
|
31
|
+
*/
|
|
32
|
+
declare function sandboxFs(): SandboxFsPort | null;
|
|
33
|
+
/** Is the sandbox filesystem reachable at all? `false` in local `vite dev` and before
|
|
34
|
+
* boot — gate file affordances on it so an app degrades instead of throwing. */
|
|
35
|
+
declare function fsAvailable(): boolean;
|
|
36
|
+
/** A directory entry from {@link MountFs.readdir}. */
|
|
37
|
+
interface DirEntry {
|
|
38
|
+
name: string;
|
|
39
|
+
kind: 'file' | 'dir';
|
|
40
|
+
}
|
|
41
|
+
/** A stat result from {@link MountFs.stat}. */
|
|
42
|
+
interface FileStat {
|
|
43
|
+
kind: 'file' | 'dir';
|
|
44
|
+
size: number;
|
|
45
|
+
mtimeMs?: number;
|
|
46
|
+
}
|
|
47
|
+
/** An error from a {@link MountFs} operation, carrying a machine-readable `.code`
|
|
48
|
+
* (mapped from the ZenFS errno) so an app branches on `.code`, never on a message. */
|
|
49
|
+
interface FsError extends Error {
|
|
50
|
+
code: 'not-found' | 'read-only' | 'not-permitted' | 'exists' | 'not-empty' | 'invalid-path' | 'unavailable' | 'unknown';
|
|
51
|
+
}
|
|
52
|
+
/** A mount-anchored, typed filesystem view. All paths are RELATIVE to the mount root;
|
|
53
|
+
* the accessor resolves them under `mount.path`. Async-only (ZenFS rides a MessagePort).
|
|
54
|
+
* Obtain one with {@link openFs}. */
|
|
55
|
+
interface MountFs {
|
|
56
|
+
/** The mount this view is anchored to (read `mode`/`rules` for writability). */
|
|
57
|
+
readonly mount: SandboxMount;
|
|
58
|
+
/** Read a file as UTF-8 text (`encoding: 'utf8'`) or raw bytes (omit encoding). */
|
|
59
|
+
readFile(relPath: string, encoding: 'utf8'): Promise<string>;
|
|
60
|
+
readFile(relPath: string): Promise<Uint8Array>;
|
|
61
|
+
/** Write text or bytes, creating or truncating the file. Throws `read-only` on a `ro` mount. */
|
|
62
|
+
writeFile(relPath: string, data: string | Uint8Array): Promise<void>;
|
|
63
|
+
/** List a directory (the mount root when `relPath` is omitted). */
|
|
64
|
+
readdir(relPath?: string): Promise<DirEntry[]>;
|
|
65
|
+
/** Stat a path. Throws `not-found` if absent. */
|
|
66
|
+
stat(relPath: string): Promise<FileStat>;
|
|
67
|
+
/** Does `relPath` exist? Never throws on absence. */
|
|
68
|
+
exists(relPath: string): Promise<boolean>;
|
|
69
|
+
/** Create a directory (pass `{ recursive: true }` to make parents). */
|
|
70
|
+
mkdir(relPath: string, opts?: {
|
|
71
|
+
recursive?: boolean;
|
|
72
|
+
}): Promise<void>;
|
|
73
|
+
/** Remove a file, or a directory with `{ recursive: true }`. */
|
|
74
|
+
rm(relPath: string, opts?: {
|
|
75
|
+
recursive?: boolean;
|
|
76
|
+
}): Promise<void>;
|
|
77
|
+
/** Rename/move within the mount. */
|
|
78
|
+
rename(fromRel: string, toRel: string): Promise<void>;
|
|
79
|
+
/** Client-side writability hint for `relPath` (mount `mode` ∩ longest-matching `rule`),
|
|
80
|
+
* so an app can hide an "edit" affordance instead of catching `read-only`
|
|
81
|
+
* (EDITOR_FIRST_EDITING_SPEC §3). Re-evaluate on `onMountsChange` — a role downgrade
|
|
82
|
+
* flips it. EROFS from the host stays authoritative. */
|
|
83
|
+
canWrite(relPath?: string): boolean;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Open a typed, mount-anchored filesystem view (SDK_FS_SURFACE_SPEC §2.1). Pure-client:
|
|
87
|
+
* resolves the ambient ZenFS once ({@link sandboxFs}) and binds it to `mount.path`, so you
|
|
88
|
+
* read/write with paths RELATIVE to the mount root — you cannot accidentally name a path
|
|
89
|
+
* outside your grant (a `..`/absolute path throws `invalid-path`; the host chroot is the
|
|
90
|
+
* real enforcer).
|
|
91
|
+
*
|
|
92
|
+
* ```ts
|
|
93
|
+
* import { mountSpace } from '@immediately-run/sdk';
|
|
94
|
+
* import { openFs } from '@immediately-run/sdk/fs';
|
|
95
|
+
* const fs = openFs(await mountSpace({ spaceId }));
|
|
96
|
+
* const text = await fs.readFile('notes/idea.mdx', 'utf8');
|
|
97
|
+
* if (fs.canWrite('notes/idea.mdx')) await fs.writeFile('notes/idea.mdx', text);
|
|
98
|
+
* ```
|
|
99
|
+
*
|
|
100
|
+
* Throws {@link FsError} `unavailable` if the sandbox fs is not present (local `vite dev`
|
|
101
|
+
* / before boot — gate with {@link fsAvailable}). Per-op failures throw {@link FsError}
|
|
102
|
+
* with a mapped `.code` (`not-found`, `read-only`, …).
|
|
103
|
+
*/
|
|
104
|
+
declare function openFs(mount: SandboxMount): MountFs;
|
|
105
|
+
/** Open a mount-anchored view of this app's OWN repository working tree — a convenience
|
|
106
|
+
* over {@link openFs} using `getAppMountPath()` (FILE_SHARING_SPEC §11.2). */
|
|
107
|
+
declare function openAppFs(): MountFs;
|
|
108
|
+
|
|
109
|
+
export { type DirEntry, type FileStat, type FsError, type MountFs, type SandboxFsPort, fsAvailable, openAppFs, openFs, sandboxFs };
|