tensorlake 0.5.77 → 0.5.88

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/index.js CHANGED
@@ -19,7 +19,7 @@ var SDK_VERSION, API_URL, API_KEY, NAMESPACE, SANDBOX_PROXY_URL, DEFAULT_HTTP_TI
19
19
  var init_defaults = __esm({
20
20
  "src/defaults.ts"() {
21
21
  "use strict";
22
- SDK_VERSION = "0.5.77";
22
+ SDK_VERSION = "0.5.88";
23
23
  API_URL = process.env.TENSORLAKE_API_URL ?? "https://api.tensorlake.ai";
24
24
  API_KEY = process.env.TENSORLAKE_API_KEY ?? void 0;
25
25
  NAMESPACE = process.env.INDEXIFY_NAMESPACE ?? "default";
@@ -6488,6 +6488,691 @@ async function deleteFileSystem(fileSystemId) {
6488
6488
  }
6489
6489
  }
6490
6490
 
6491
+ // src/filesystem.ts
6492
+ init_defaults();
6493
+ import { execFile } from "child_process";
6494
+ import { randomBytes } from "crypto";
6495
+ import { accessSync, constants } from "fs";
6496
+ import { homedir } from "os";
6497
+ import { join } from "path";
6498
+ import { promisify } from "util";
6499
+
6500
+ // src/filesystem-models.ts
6501
+ var GIT_MODE_DIR = 16384;
6502
+ var GIT_MODE_SYMLINK = 40960;
6503
+ function trimSlashes(path2) {
6504
+ let start = 0;
6505
+ let end = path2.length;
6506
+ while (start < end && path2.charCodeAt(start) === 47) start += 1;
6507
+ while (end > start && path2.charCodeAt(end - 1) === 47) end -= 1;
6508
+ return path2.slice(start, end);
6509
+ }
6510
+ function fileEntryFromWire(entry, dirPath) {
6511
+ const mode = entry.mode ?? 33188;
6512
+ const prefix = trimSlashes(dirPath);
6513
+ return {
6514
+ name: entry.name,
6515
+ path: prefix ? `${prefix}/${entry.name}` : entry.name,
6516
+ contentId: entry.oid ?? "",
6517
+ kind: mode === GIT_MODE_DIR ? "directory" : mode === GIT_MODE_SYMLINK ? "symlink" : "file",
6518
+ executable: mode === 33261,
6519
+ size: entry.size ?? null,
6520
+ isDir: mode === GIT_MODE_DIR,
6521
+ isSymlink: mode === GIT_MODE_SYMLINK
6522
+ };
6523
+ }
6524
+ function mountStatusFromRaw(raw, localPath) {
6525
+ const mounted = "mounted" in raw ? Boolean(raw.mounted) : "active" in raw ? Boolean(raw.active) : true;
6526
+ const path2 = String(raw.path || raw.mount_path || localPath || "");
6527
+ const filesystem = raw.filesystem || raw.file_system || null;
6528
+ return { path: path2, filesystem, mounted, raw };
6529
+ }
6530
+ var FilesystemError = class extends Error {
6531
+ constructor(message) {
6532
+ super(message);
6533
+ this.name = "FilesystemError";
6534
+ }
6535
+ };
6536
+ var FilesystemNotFoundError = class extends FilesystemError {
6537
+ filesystemName;
6538
+ constructor(name) {
6539
+ super(`Filesystem not found: ${name}`);
6540
+ this.name = "FilesystemNotFoundError";
6541
+ this.filesystemName = name;
6542
+ }
6543
+ };
6544
+ var FileNotFoundInFilesystemError = class extends FilesystemError {
6545
+ filesystem;
6546
+ path;
6547
+ constructor(filesystem, path2) {
6548
+ super(`File not found in filesystem ${filesystem}: ${path2}`);
6549
+ this.name = "FileNotFoundInFilesystemError";
6550
+ this.filesystem = filesystem;
6551
+ this.path = path2;
6552
+ }
6553
+ };
6554
+ var FilesystemAPIError = class extends FilesystemError {
6555
+ statusCode;
6556
+ constructor(statusCode, message) {
6557
+ super(`API error (status ${statusCode}): ${message}`);
6558
+ this.name = "FilesystemAPIError";
6559
+ this.statusCode = statusCode;
6560
+ }
6561
+ };
6562
+ var MountError = class extends FilesystemError {
6563
+ constructor(message) {
6564
+ super(message);
6565
+ this.name = "MountError";
6566
+ }
6567
+ };
6568
+ var CliNotFoundError = class extends MountError {
6569
+ constructor(detail) {
6570
+ super(
6571
+ `Mount operations require the Tensorlake CLI (\`tl\`) with \`tl fs\` support: ${detail}. Install or upgrade it with: curl -fsSL https://tensorlake.ai/install.sh | sh`
6572
+ );
6573
+ this.name = "CliNotFoundError";
6574
+ }
6575
+ };
6576
+
6577
+ // src/filesystem.ts
6578
+ init_native_sandbox();
6579
+ init_sandbox_image();
6580
+ var execFileAsync = promisify(execFile);
6581
+ var FILESYSTEM_REPO_KIND = "filesystem";
6582
+ function translateNativeError2(error, notFound) {
6583
+ const message = error instanceof Error ? error.message : String(error);
6584
+ let payload;
6585
+ try {
6586
+ payload = JSON.parse(message);
6587
+ } catch {
6588
+ return error instanceof Error ? error : new FilesystemError(message);
6589
+ }
6590
+ if (!payload || typeof payload.category !== "string") {
6591
+ return error instanceof Error ? error : new FilesystemError(message);
6592
+ }
6593
+ const detail = typeof payload.message === "string" ? payload.message : message;
6594
+ if (payload.category === "connection") {
6595
+ return new FilesystemError(detail);
6596
+ }
6597
+ if (payload.status === 404 && notFound !== void 0) {
6598
+ return notFound;
6599
+ }
6600
+ if (typeof payload.status === "number") {
6601
+ return new FilesystemAPIError(payload.status, detail);
6602
+ }
6603
+ return new FilesystemError(detail);
6604
+ }
6605
+ async function callNative2(fn, notFound) {
6606
+ try {
6607
+ return await fn();
6608
+ } catch (error) {
6609
+ throw translateNativeError2(error, notFound);
6610
+ }
6611
+ }
6612
+ var FsCli = class {
6613
+ binary = null;
6614
+ envOverrides;
6615
+ constructor(envOverrides) {
6616
+ this.envOverrides = envOverrides;
6617
+ }
6618
+ async findCli() {
6619
+ const candidates = [];
6620
+ if (process.env.TENSORLAKE_CLI) {
6621
+ candidates.push(process.env.TENSORLAKE_CLI);
6622
+ }
6623
+ candidates.push("tl");
6624
+ const installed = join(homedir(), ".tensorlake", "bin", "tl");
6625
+ try {
6626
+ accessSync(installed, constants.F_OK);
6627
+ candidates.push(installed);
6628
+ } catch {
6629
+ }
6630
+ let unsupported = null;
6631
+ for (const candidate of candidates) {
6632
+ try {
6633
+ await execFileAsync(candidate, ["fs", "--help"], { timeout: 15e3 });
6634
+ return candidate;
6635
+ } catch (error) {
6636
+ if (error.code !== "ENOENT") {
6637
+ unsupported = candidate;
6638
+ }
6639
+ }
6640
+ }
6641
+ throw new CliNotFoundError(
6642
+ unsupported ? `\`tl\` at ${unsupported} does not support \`tl fs\` (upgrade required)` : "`tl` was not found on PATH"
6643
+ );
6644
+ }
6645
+ async run(args, timeoutMs = 3e5) {
6646
+ if (this.binary === null) {
6647
+ this.binary = await this.findCli();
6648
+ }
6649
+ try {
6650
+ const { stdout } = await execFileAsync(this.binary, ["fs", ...args], {
6651
+ timeout: timeoutMs,
6652
+ env: { ...process.env, ...this.envOverrides },
6653
+ maxBuffer: 16 * 1024 * 1024
6654
+ });
6655
+ return stdout;
6656
+ } catch (error) {
6657
+ const failure = error;
6658
+ if (failure.killed) {
6659
+ throw new MountError(`\`tl fs ${args[0]}\` timed out after ${timeoutMs}ms`);
6660
+ }
6661
+ const detail = (failure.stderr || failure.stdout || String(error)).trim();
6662
+ throw new MountError(`\`tl fs ${args.join(" ")}\` failed: ${detail}`);
6663
+ }
6664
+ }
6665
+ // Flags always precede a `--` end-of-options separator so caller-supplied
6666
+ // names/paths can never be parsed as CLI flags (e.g. a path literally
6667
+ // named "--discard" must stay a path, not become the destructive flag).
6668
+ async mount(filesystem, localPath, readonly) {
6669
+ const args = ["mount"];
6670
+ if (readonly) args.push("--ro");
6671
+ args.push("--", filesystem, localPath);
6672
+ await this.run(args);
6673
+ }
6674
+ async unmount(localPath, discard) {
6675
+ const args = ["unmount"];
6676
+ if (discard) args.push("--discard");
6677
+ args.push("--", localPath);
6678
+ await this.run(args);
6679
+ }
6680
+ async snapshot(localPath, message) {
6681
+ const args = ["snapshot"];
6682
+ if (message) args.push(`--message=${message}`);
6683
+ args.push("--", localPath);
6684
+ await this.run(args);
6685
+ }
6686
+ async status(localPath) {
6687
+ const args = ["status", "--json"];
6688
+ if (localPath) args.push("--", localPath);
6689
+ const output = await this.run(args, 6e4);
6690
+ try {
6691
+ const payload = JSON.parse(output);
6692
+ return typeof payload === "object" && payload !== null ? payload : { status: payload };
6693
+ } catch {
6694
+ throw new MountError(
6695
+ `\`tl fs status --json\` returned invalid JSON: ${output.slice(0, 200)}`
6696
+ );
6697
+ }
6698
+ }
6699
+ };
6700
+ var FilesystemClient = class {
6701
+ native;
6702
+ cli;
6703
+ /**
6704
+ * Any option left unset is resolved from the environment
6705
+ * (`TENSORLAKE_API_KEY` / `TENSORLAKE_PAT`, `TENSORLAKE_API_URL`,
6706
+ * `TENSORLAKE_ORGANIZATION_ID`, `TENSORLAKE_PROJECT_ID`).
6707
+ */
6708
+ constructor(options = {}) {
6709
+ const context = buildContextFromEnv();
6710
+ const token = options.apiKey ?? context.apiKey ?? context.personalAccessToken;
6711
+ if (!token) {
6712
+ throw new FilesystemError(
6713
+ "Missing TENSORLAKE_API_KEY or TENSORLAKE_PAT credentials."
6714
+ );
6715
+ }
6716
+ const organizationId = options.organizationId ?? context.organizationId;
6717
+ const projectId = options.projectId ?? context.projectId;
6718
+ if (!organizationId || !projectId) {
6719
+ throw new FilesystemError(
6720
+ "Filesystem operations require organization and project context (TENSORLAKE_ORGANIZATION_ID and TENSORLAKE_PROJECT_ID)."
6721
+ );
6722
+ }
6723
+ if (options.nativeClient) {
6724
+ this.native = options.nativeClient;
6725
+ } else {
6726
+ const binding = loadNativeSandboxBinding();
6727
+ if (typeof binding.NativeRepositoryClient !== "function") {
6728
+ throw new FilesystemError(
6729
+ "native binding does not export the repository client; rebuild with 'npm run build:native'"
6730
+ );
6731
+ }
6732
+ this.native = new binding.NativeRepositoryClient(
6733
+ options.apiUrl ?? context.apiUrl,
6734
+ token,
6735
+ organizationId,
6736
+ projectId,
6737
+ `tensorlake-typescript-sdk/${SDK_VERSION}`,
6738
+ null
6739
+ );
6740
+ }
6741
+ const envOverrides = {
6742
+ TENSORLAKE_ORGANIZATION_ID: organizationId,
6743
+ TENSORLAKE_PROJECT_ID: projectId,
6744
+ // The CLI must target the same deployment the data plane does, or a
6745
+ // mount could resolve a same-named filesystem in the wrong environment.
6746
+ TENSORLAKE_API_URL: options.apiUrl ?? context.apiUrl
6747
+ };
6748
+ if (options.apiKey) {
6749
+ envOverrides.TENSORLAKE_API_KEY = options.apiKey;
6750
+ }
6751
+ this.cli = new FsCli(envOverrides);
6752
+ }
6753
+ // -- lifecycle --------------------------------------------------------------
6754
+ /** Create a new filesystem and return a handle to it. */
6755
+ async create(name) {
6756
+ if (typeof name !== "string") {
6757
+ throw new TypeError("name must be a string");
6758
+ }
6759
+ if (name.length === 0) {
6760
+ throw new FilesystemError("filesystem name must not be empty");
6761
+ }
6762
+ const raw = await callNative2(() => this.native.createFilesystem(name));
6763
+ const created = JSON.parse(raw);
6764
+ return new Filesystem(
6765
+ this,
6766
+ this.native,
6767
+ this.cli,
6768
+ name,
6769
+ created.default_branch || "main"
6770
+ );
6771
+ }
6772
+ /**
6773
+ * Fork a filesystem at its live head or an explicit retained snapshot.
6774
+ * The server shares immutable content and publishes only metadata.
6775
+ */
6776
+ async fork(name, baseFilesystem, snapshot) {
6777
+ if (!name || !baseFilesystem) {
6778
+ throw new FilesystemError(
6779
+ "fork and base filesystem names must not be empty"
6780
+ );
6781
+ }
6782
+ await callNative2(
6783
+ () => this.native.forkFilesystem(
6784
+ name,
6785
+ baseFilesystem,
6786
+ snapshot ?? null
6787
+ ),
6788
+ new FilesystemNotFoundError(baseFilesystem)
6789
+ );
6790
+ return new Filesystem(this, this.native, this.cli, name, "main");
6791
+ }
6792
+ /** Return a handle to an existing filesystem (verifies it exists). */
6793
+ async get(name) {
6794
+ const traced = await callNative2(
6795
+ () => this.native.filesystemMeta(name),
6796
+ new FilesystemNotFoundError(name)
6797
+ );
6798
+ const meta = JSON.parse(traced.json);
6799
+ if ((meta.kind ?? FILESYSTEM_REPO_KIND) !== FILESYSTEM_REPO_KIND) {
6800
+ throw new FilesystemNotFoundError(name);
6801
+ }
6802
+ return new Filesystem(
6803
+ this,
6804
+ this.native,
6805
+ this.cli,
6806
+ name,
6807
+ meta.default_branch || "main"
6808
+ );
6809
+ }
6810
+ /** List all filesystems in the project. */
6811
+ async list() {
6812
+ const traced = await callNative2(() => this.native.listFilesystems());
6813
+ const page = JSON.parse(traced.json);
6814
+ return (page.repos ?? []).map((repo) => ({
6815
+ name: String(repo.name ?? ""),
6816
+ fullName: String(repo.full_name ?? ""),
6817
+ status: String(repo.status ?? ""),
6818
+ kind: String(repo.kind ?? FILESYSTEM_REPO_KIND)
6819
+ }));
6820
+ }
6821
+ /** Permanently delete a filesystem and all its snapshots. */
6822
+ async delete(name) {
6823
+ await callNative2(
6824
+ () => this.native.deleteFilesystem(name),
6825
+ new FilesystemNotFoundError(name)
6826
+ );
6827
+ }
6828
+ // -- local mounts -------------------------------------------------------------
6829
+ /** Mount a filesystem to a local path (requires the `tl` CLI). */
6830
+ async mount(name, localPath, readonly = false) {
6831
+ await this.cli.mount(name, localPath, readonly);
6832
+ return new FilesystemMount(this, this.cli, name, localPath, readonly);
6833
+ }
6834
+ /**
6835
+ * Unmount a locally mounted filesystem.
6836
+ *
6837
+ * `discard: true` drops local changes that were not yet uploaded.
6838
+ */
6839
+ async unmount(localPath, discard = false) {
6840
+ await this.cli.unmount(localPath, discard);
6841
+ }
6842
+ /** Status of a local mount (defaults to the mount containing CWD). */
6843
+ async mountStatus(localPath) {
6844
+ return mountStatusFromRaw(await this.cli.status(localPath), localPath);
6845
+ }
6846
+ };
6847
+ function toBuffer(data) {
6848
+ return typeof data === "string" ? Buffer.from(data, "utf-8") : Buffer.from(data);
6849
+ }
6850
+ var Filesystem = class {
6851
+ client;
6852
+ native;
6853
+ cli;
6854
+ name;
6855
+ defaultBranch;
6856
+ /** @internal — obtain instances via `FilesystemClient.create()` / `.get()`. */
6857
+ constructor(client, native, cli, name, defaultBranch = null) {
6858
+ this.client = client;
6859
+ this.native = native;
6860
+ this.cli = cli;
6861
+ this.name = name;
6862
+ this.defaultBranch = defaultBranch;
6863
+ }
6864
+ /**
6865
+ * The filesystem's default branch — the target of every write and the
6866
+ * default version of every read, so writes and `status()` can never
6867
+ * silently disagree on a non-"main" filesystem.
6868
+ */
6869
+ async branch() {
6870
+ if (!this.defaultBranch) {
6871
+ const traced = await callNative2(
6872
+ () => this.native.filesystemMeta(this.name),
6873
+ new FilesystemNotFoundError(this.name)
6874
+ );
6875
+ const meta = JSON.parse(traced.json);
6876
+ this.defaultBranch = meta.default_branch || "main";
6877
+ }
6878
+ return this.defaultBranch;
6879
+ }
6880
+ // -- writes -------------------------------------------------------------------
6881
+ /** Write one file. Returns the snapshot (version) the write produced. */
6882
+ async writeFile(path2, data, message) {
6883
+ return await this.writeFiles(/* @__PURE__ */ new Map([[path2, data]]), message);
6884
+ }
6885
+ /** Write several files (and/or delete paths) in one atomic snapshot. */
6886
+ async writeFiles(files, message, deletes = []) {
6887
+ const entries = files instanceof Map ? [...files.entries()] : Object.entries(files);
6888
+ if (entries.length === 0 && deletes.length === 0) {
6889
+ throw new FilesystemError("nothing to write: no files or deletions given");
6890
+ }
6891
+ const writes = entries.map(([path2, data]) => ({
6892
+ path: path2,
6893
+ content: toBuffer(data)
6894
+ }));
6895
+ const resolvedMessage = message || `write ${writes.length} file(s) via SDK`;
6896
+ return await this.publishChanges(
6897
+ writes,
6898
+ deletes,
6899
+ [],
6900
+ [],
6901
+ resolvedMessage
6902
+ );
6903
+ }
6904
+ /**
6905
+ * Stream one local file directly to object storage in bounded parts.
6906
+ * This is the memory-bounded API for multi-GiB files.
6907
+ */
6908
+ async writeFileFromPath(path2, localPath, message) {
6909
+ return await this.writeFilesFromPaths(
6910
+ /* @__PURE__ */ new Map([[path2, localPath]]),
6911
+ message
6912
+ );
6913
+ }
6914
+ /**
6915
+ * Atomically publish local files without loading their complete payloads
6916
+ * into JavaScript or Rust memory.
6917
+ */
6918
+ async writeFilesFromPaths(files, message) {
6919
+ const entries = files instanceof Map ? [...files.entries()] : Object.entries(files);
6920
+ if (entries.length === 0) {
6921
+ throw new FilesystemError("nothing to write: no local files given");
6922
+ }
6923
+ const writes = entries.map(([path2, localPath]) => ({ path: path2, localPath }));
6924
+ const resolvedMessage = message || `write ${writes.length} local file(s) via SDK`;
6925
+ const branch = await this.branch();
6926
+ const traced = await callNative2(
6927
+ () => this.native.pushFilesystemPaths(
6928
+ this.name,
6929
+ writes,
6930
+ resolvedMessage,
6931
+ branch,
6932
+ randomBytes(16).toString("hex")
6933
+ ),
6934
+ new FilesystemNotFoundError(this.name)
6935
+ );
6936
+ const report = JSON.parse(traced.json);
6937
+ const versionId = String(report.version_id ?? "");
6938
+ const previousVersionId = report.previous_version_id == null ? null : String(report.previous_version_id);
6939
+ return {
6940
+ versionId,
6941
+ previousVersionId,
6942
+ created: previousVersionId !== versionId,
6943
+ message: resolvedMessage
6944
+ };
6945
+ }
6946
+ /**
6947
+ * Move a file or directory without downloading or re-uploading its bytes.
6948
+ * The source removal and destination creation publish as one atomic snapshot.
6949
+ */
6950
+ async moveFile(from, to, message) {
6951
+ return await this.publishChanges(
6952
+ [],
6953
+ [],
6954
+ [{ from, to }],
6955
+ [],
6956
+ message || `move ${from} to ${to} via SDK`
6957
+ );
6958
+ }
6959
+ /**
6960
+ * Copy a file or directory by reusing immutable content references. No
6961
+ * payload bytes traverse the client, API server, or object store.
6962
+ */
6963
+ async copyFile(from, to, message) {
6964
+ return await this.publishChanges(
6965
+ [],
6966
+ [],
6967
+ [],
6968
+ [{ from, to }],
6969
+ message || `copy ${from} to ${to} via SDK`
6970
+ );
6971
+ }
6972
+ async publishChanges(writes, deletes, moves, copies, message) {
6973
+ const branch = await this.branch();
6974
+ const traced = await callNative2(
6975
+ () => this.native.pushFilesystemFiles(
6976
+ this.name,
6977
+ writes,
6978
+ deletes,
6979
+ moves,
6980
+ copies,
6981
+ message,
6982
+ branch,
6983
+ // One key per logical write: a retried submit reattaches to the
6984
+ // same durable commit job instead of double-committing.
6985
+ randomBytes(16).toString("hex")
6986
+ ),
6987
+ new FilesystemNotFoundError(this.name)
6988
+ );
6989
+ const report = JSON.parse(traced.json);
6990
+ const versionId = String(report.version_id ?? "");
6991
+ const previousVersionId = report.previous_version_id == null ? null : String(report.previous_version_id);
6992
+ return {
6993
+ versionId,
6994
+ previousVersionId,
6995
+ created: previousVersionId !== versionId,
6996
+ message
6997
+ };
6998
+ }
6999
+ /** Delete one file. Returns the durable version the deletion produced. */
7000
+ async deleteFile(path2, message) {
7001
+ return await this.writeFiles(
7002
+ /* @__PURE__ */ new Map(),
7003
+ message || `delete ${path2} via SDK`,
7004
+ [path2]
7005
+ );
7006
+ }
7007
+ /**
7008
+ * Return the filesystem's current version as a snapshot.
7009
+ *
7010
+ * Writes publish durable live versions. This retains the current head
7011
+ * permanently in one metadata-only server operation without changing or
7012
+ * copying any content.
7013
+ */
7014
+ async snapshot(message = "snapshot via SDK") {
7015
+ const resolvedMessage = message || "snapshot via SDK";
7016
+ const traced = await callNative2(
7017
+ () => this.native.retainFilesystemSnapshot(
7018
+ this.name,
7019
+ resolvedMessage,
7020
+ randomBytes(16).toString("hex")
7021
+ ),
7022
+ new FilesystemNotFoundError(this.name)
7023
+ );
7024
+ const retained = JSON.parse(traced.json);
7025
+ return {
7026
+ id: String(retained.snapshot_id ?? ""),
7027
+ message: retained.message || resolvedMessage
7028
+ };
7029
+ }
7030
+ /** List explicitly retained snapshots without reading filesystem contents. */
7031
+ async listSnapshots() {
7032
+ const traced = await callNative2(
7033
+ () => this.native.listFilesystemSnapshots(this.name),
7034
+ new FilesystemNotFoundError(this.name)
7035
+ );
7036
+ const payload = JSON.parse(traced.json);
7037
+ return (payload.snapshots ?? []).map((snapshot) => ({
7038
+ id: String(snapshot.snapshot_id ?? ""),
7039
+ createdAt: new Date(Number(snapshot.created_at_ms ?? 0)),
7040
+ message: String(snapshot.message ?? "")
7041
+ }));
7042
+ }
7043
+ /** Delete one permanent retention point; shared/head-reachable bytes remain. */
7044
+ async deleteSnapshot(snapshot) {
7045
+ await callNative2(
7046
+ () => this.native.deleteFilesystemSnapshot(this.name, snapshot),
7047
+ new FilesystemNotFoundError(this.name)
7048
+ );
7049
+ }
7050
+ // -- reads --------------------------------------------------------------------
7051
+ /**
7052
+ * Read a file's bytes at `version` (the current `main` head or a retained
7053
+ * snapshot id; defaults to the filesystem's current head).
7054
+ */
7055
+ async readFile(path2, version) {
7056
+ return (await this.readFileWithMetadata(path2, { version })).data;
7057
+ }
7058
+ /**
7059
+ * Read bytes and immutable metadata in one request. A range is executed by
7060
+ * Artifact Storage, so callers never download the unrequested remainder.
7061
+ */
7062
+ async readFileWithMetadata(path2, options = {}) {
7063
+ if (trimSlashes(path2) === "") {
7064
+ throw new FilesystemError("file path must not be empty");
7065
+ }
7066
+ const range = options.range;
7067
+ if (range !== void 0 && (!Number.isSafeInteger(range.offset) || range.offset < 0 || !Number.isSafeInteger(range.length) || range.length <= 0)) {
7068
+ throw new FilesystemError(
7069
+ "range requires a non-negative integer offset and positive integer length"
7070
+ );
7071
+ }
7072
+ const resolvedVersion = options.version || await this.branch();
7073
+ const traced = await callNative2(
7074
+ () => this.native.readFilesystemFile(
7075
+ this.name,
7076
+ path2,
7077
+ resolvedVersion,
7078
+ range?.offset,
7079
+ range?.length
7080
+ ),
7081
+ new FileNotFoundInFilesystemError(this.name, path2)
7082
+ );
7083
+ const fullSize = traced.fullSize;
7084
+ if (!traced.contentId || fullSize === void 0 || !Number.isSafeInteger(fullSize) || fullSize < 0 || traced.data.byteLength > fullSize) {
7085
+ throw new FilesystemError(
7086
+ "filesystem read response omitted content identity or total size"
7087
+ );
7088
+ }
7089
+ return {
7090
+ data: new Uint8Array(traced.data),
7091
+ contentId: traced.contentId,
7092
+ size: fullSize
7093
+ };
7094
+ }
7095
+ /** Read a file as UTF-8 text at `version`. Throws on non-UTF-8 content. */
7096
+ async readText(path2, version) {
7097
+ return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
7098
+ await this.readFile(path2, version)
7099
+ );
7100
+ }
7101
+ /** List one directory (non-recursive) at `version`. */
7102
+ async listFiles(dirPath = "", version) {
7103
+ const resolvedVersion = version || await this.branch();
7104
+ const traced = await callNative2(
7105
+ () => this.native.listFilesystemTree(this.name, dirPath, resolvedVersion),
7106
+ new FileNotFoundInFilesystemError(this.name, dirPath || "/")
7107
+ );
7108
+ const page = JSON.parse(traced.json);
7109
+ return (page.entries ?? []).map((entry) => fileEntryFromWire(entry, dirPath));
7110
+ }
7111
+ // -- status -------------------------------------------------------------------
7112
+ /** Remote status: identity plus the current native version. */
7113
+ async status() {
7114
+ const metaTraced = await callNative2(
7115
+ () => this.native.filesystemMeta(this.name),
7116
+ new FilesystemNotFoundError(this.name)
7117
+ );
7118
+ const meta = JSON.parse(metaTraced.json);
7119
+ const defaultBranch = String(meta.default_branch || "main");
7120
+ this.defaultBranch = defaultBranch;
7121
+ let versionId = null;
7122
+ let generation = null;
7123
+ try {
7124
+ const refTraced = await callNative2(
7125
+ () => this.native.filesystemRefStatus(this.name, defaultBranch)
7126
+ );
7127
+ const ref = JSON.parse(refTraced.json);
7128
+ versionId = ref.resolved_commit || ref.oid || null;
7129
+ generation = typeof ref.generation === "number" ? ref.generation : null;
7130
+ } catch (error) {
7131
+ if (!(error instanceof FilesystemAPIError) || error.statusCode !== 404) {
7132
+ throw error;
7133
+ }
7134
+ }
7135
+ return {
7136
+ name: this.name,
7137
+ status: String(meta.status ?? ""),
7138
+ versionId,
7139
+ generation
7140
+ };
7141
+ }
7142
+ // -- mounts -------------------------------------------------------------------
7143
+ /** Mount this filesystem to a local path (requires the `tl` CLI). */
7144
+ async mount(localPath, readonly = false) {
7145
+ return await this.client.mount(this.name, localPath, readonly);
7146
+ }
7147
+ };
7148
+ var FilesystemMount = class {
7149
+ client;
7150
+ cli;
7151
+ filesystem;
7152
+ path;
7153
+ readonly;
7154
+ /** @internal — obtain instances via `FilesystemClient.mount()`. */
7155
+ constructor(client, cli, filesystem, path2, readonly) {
7156
+ this.client = client;
7157
+ this.cli = cli;
7158
+ this.filesystem = filesystem;
7159
+ this.path = path2;
7160
+ this.readonly = readonly;
7161
+ }
7162
+ /** Flush pending local changes into a durable snapshot. */
7163
+ async snapshot(message) {
7164
+ await this.cli.snapshot(this.path, message);
7165
+ }
7166
+ /** Local mount status as reported by the mount daemon. */
7167
+ async status() {
7168
+ return await this.client.mountStatus(this.path);
7169
+ }
7170
+ /** Unmount; `discard: true` drops changes not yet uploaded. */
7171
+ async unmount(discard = false) {
7172
+ await this.client.unmount(this.path, discard);
7173
+ }
7174
+ };
7175
+
6491
7176
  // src/index.ts
6492
7177
  init_image();
6493
7178
  init_url();
@@ -6495,11 +7180,20 @@ init_errors();
6495
7180
  init_models();
6496
7181
  export {
6497
7182
  APIClient,
7183
+ CliNotFoundError,
6498
7184
  CloudClient,
6499
7185
  ContainerState,
6500
7186
  Desktop,
7187
+ FileNotFoundInFilesystemError,
7188
+ Filesystem,
7189
+ FilesystemAPIError,
7190
+ FilesystemClient,
7191
+ FilesystemError,
7192
+ FilesystemMount,
7193
+ FilesystemNotFoundError,
6501
7194
  Image,
6502
7195
  ImageBuildOperationType,
7196
+ MountError,
6503
7197
  OutputMode,
6504
7198
  PoolInUseError,
6505
7199
  PoolNotFoundError,