@archildata/just-bash 0.1.13 → 0.8.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.
package/dist/index.cjs CHANGED
@@ -5,9 +5,6 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __esm = (fn, res) => function __init() {
9
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
- };
11
8
  var __export = (target, all) => {
12
9
  for (var name in all)
13
10
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -30,499 +27,615 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
27
  ));
31
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
29
 
33
- // src/ArchilFs.ts
34
- var ArchilFs_exports = {};
35
- __export(ArchilFs_exports, {
36
- ArchilFs: () => ArchilFs
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ ArchilFs: () => ArchilFs,
34
+ createArchilCommand: () => createArchilCommand,
35
+ createArchilFs: () => createArchilFs
37
36
  });
38
- var import_debug, debug, ArchilFs;
39
- var init_ArchilFs = __esm({
40
- "src/ArchilFs.ts"() {
41
- "use strict";
42
- import_debug = __toESM(require("debug"), 1);
43
- debug = (0, import_debug.default)("archil:fs");
44
- ArchilFs = class {
45
- client;
46
- user;
47
- rootInodeId = 1;
48
- /**
49
- * Create a new ArchilFs adapter
50
- *
51
- * @param client - Connected ArchilClient instance
52
- * @param options - Optional configuration
53
- * @param options.user - Unix user context for permission checks
54
- */
55
- constructor(client, options) {
56
- this.client = client;
57
- this.user = options?.user;
58
- }
59
- // ========================================================================
60
- // Path Resolution
61
- // ========================================================================
62
- /**
63
- * Normalize a path (remove . and .., ensure leading /)
64
- */
65
- normalizePath(path) {
66
- if (!path || path === "") {
67
- return "/";
68
- }
69
- if (!path.startsWith("/")) {
70
- path = "/" + path;
71
- }
72
- const parts = path.split("/").filter((p) => p !== "" && p !== ".");
73
- const result = [];
74
- for (const part of parts) {
75
- if (part === "..") {
76
- result.pop();
77
- } else {
78
- result.push(part);
79
- }
80
- }
81
- return "/" + result.join("/");
82
- }
83
- /**
84
- * Resolve a path to its inode ID, walking the directory tree
85
- */
86
- async resolve(path) {
87
- const normalizedPath = this.normalizePath(path);
88
- const parts = normalizedPath.split("/").filter((p) => p !== "");
89
- let currentInodeId = this.rootInodeId;
90
- for (const part of parts) {
91
- const response = await this.client.lookupInode(currentInodeId, part, { user: this.user });
92
- if (response.inodeId === -1) {
93
- throw new Error(`ENOENT: no such file or directory, '${path}'`);
94
- }
95
- currentInodeId = response.inodeId;
96
- }
97
- const attributes = await this.client.getAttributes(currentInodeId, { user: this.user });
98
- return { inodeId: currentInodeId, attributes };
37
+ module.exports = __toCommonJS(index_exports);
38
+
39
+ // src/ArchilFs.ts
40
+ var import_debug = __toESM(require("debug"), 1);
41
+ var debug = (0, import_debug.default)("archil:fs");
42
+ var ArchilFs = class _ArchilFs {
43
+ client;
44
+ user;
45
+ rootInodeId = 1;
46
+ constructor(client, options) {
47
+ this.client = client;
48
+ this.user = options?.user;
49
+ }
50
+ /**
51
+ * Create an ArchilFs adapter, optionally rooted at a subdirectory.
52
+ *
53
+ * The subdirectory path is resolved eagerly: if any component does not
54
+ * exist or is not a directory, this method throws immediately.
55
+ *
56
+ * @param client - Connected ArchilClient instance
57
+ * @param options - Optional configuration
58
+ * @param options.user - Unix user context for permission checks
59
+ * @param options.subdirectory - Subdirectory to treat as the root of the filesystem
60
+ */
61
+ static async create(client, options) {
62
+ const fs = new _ArchilFs(client, { user: options?.user });
63
+ if (options?.subdirectory) {
64
+ const resolved = await fs.resolveFollow(options.subdirectory);
65
+ if (resolved.attributes.inodeType !== "Directory") {
66
+ throw new Error(`ENOTDIR: subdirectory '${options.subdirectory}' is not a directory`);
99
67
  }
100
- /**
101
- * Resolve parent directory and get child name
102
- */
103
- async resolveParent(path) {
104
- debug("resolveParent raw path=%j (bytes: %o)", path, Buffer.from(path));
105
- const normalizedPath = this.normalizePath(path);
106
- const lastSlash = normalizedPath.lastIndexOf("/");
107
- const parentPath = lastSlash === 0 ? "/" : normalizedPath.substring(0, lastSlash);
108
- const name = normalizedPath.substring(lastSlash + 1);
109
- debug("resolveParent extracted name=%j (bytes: %o)", name, Buffer.from(name));
110
- const { inodeId: parentInodeId } = await this.resolve(parentPath);
111
- return { parentInodeId, name };
68
+ fs.rootInodeId = resolved.inodeId;
69
+ debug("resolved subdirectory '%s' to inode %d", options.subdirectory, fs.rootInodeId);
70
+ }
71
+ return fs;
72
+ }
73
+ // ========================================================================
74
+ // Path Resolution
75
+ // ========================================================================
76
+ /**
77
+ * Normalize a path (remove . and .., ensure leading /)
78
+ */
79
+ normalizePath(path) {
80
+ if (!path || path === "") {
81
+ return "/";
82
+ }
83
+ if (!path.startsWith("/")) {
84
+ path = "/" + path;
85
+ }
86
+ const parts = path.split("/").filter((p) => p !== "" && p !== ".");
87
+ const result = [];
88
+ for (const part of parts) {
89
+ if (part === "..") {
90
+ result.pop();
91
+ } else {
92
+ result.push(part);
112
93
  }
113
- /**
114
- * Convert InodeAttributes to FsStat
115
- */
116
- toStat(attrs) {
117
- return {
118
- isFile: attrs.inodeType === "File",
119
- isDirectory: attrs.inodeType === "Directory",
120
- isSymlink: attrs.inodeType === "Symlink",
121
- mode: attrs.mode,
122
- size: Number(attrs.size),
123
- mtime: new Date(attrs.mtimeMs)
124
- };
94
+ }
95
+ return "/" + result.join("/");
96
+ }
97
+ static MAX_SYMLINKS = 40;
98
+ /**
99
+ * Resolve a path to its inode ID, walking the directory tree.
100
+ * Follows symlinks on intermediate components but NOT on the final
101
+ * component (matching lstat/readlink semantics).
102
+ */
103
+ async resolve(path, symlinkDepth = 0) {
104
+ const normalizedPath = this.normalizePath(path);
105
+ const parts = normalizedPath.split("/").filter((p) => p !== "");
106
+ let currentInodeId = this.rootInodeId;
107
+ let resolvedPath = "/";
108
+ for (let i = 0; i < parts.length; i++) {
109
+ const part = parts[i];
110
+ const response = await this.client.lookupInode(currentInodeId, part, { user: this.user });
111
+ if (response === null) {
112
+ throw new Error(`ENOENT: no such file or directory, '${path}'`);
125
113
  }
126
- // ========================================================================
127
- // IFileSystem Implementation - Read Operations
128
- // ========================================================================
129
- resolvePath(base, ...paths) {
130
- debug("resolvePath base=%s paths=%o", base, paths);
131
- let result = base;
132
- for (const p of paths) {
133
- if (p.startsWith("/")) {
134
- result = p;
135
- } else {
136
- result = result.endsWith("/") ? result + p : result + "/" + p;
137
- }
114
+ const isLast = i === parts.length - 1;
115
+ if (!isLast && response.attributes.inodeType === "Symlink" && response.attributes.symlinkTarget) {
116
+ if (symlinkDepth >= _ArchilFs.MAX_SYMLINKS) {
117
+ throw new Error(`ELOOP: too many levels of symbolic links, '${path}'`);
138
118
  }
139
- const normalized = this.normalizePath(result);
140
- debug("resolvePath result=%s", normalized);
141
- return normalized;
119
+ const targetPath = response.attributes.symlinkTarget.startsWith("/") ? response.attributes.symlinkTarget : this.resolvePath(resolvedPath, response.attributes.symlinkTarget);
120
+ const remaining = parts.slice(i + 1).join("/");
121
+ const fullPath = remaining ? targetPath + "/" + remaining : targetPath;
122
+ return this.resolve(fullPath, symlinkDepth + 1);
142
123
  }
143
- async readFile(path, encoding) {
144
- debug("readFile path=%s encoding=%s", path, encoding);
145
- try {
146
- const buffer = await this.readFileBuffer(path);
147
- debug("readFile got buffer length=%d", buffer.length);
148
- const decoderEncoding = encoding === "binary" ? "latin1" : encoding || "utf-8";
149
- debug("readFile using decoder encoding=%s", decoderEncoding);
150
- const decoder = new TextDecoder(decoderEncoding);
151
- const result = decoder.decode(buffer);
152
- debug("readFile decoded to string length=%d", result.length);
153
- return result;
154
- } catch (err) {
155
- debug("readFile FAILED: %O", err);
156
- throw err;
157
- }
124
+ resolvedPath = resolvedPath === "/" ? "/" + part : resolvedPath + "/" + part;
125
+ currentInodeId = response.inodeId;
126
+ }
127
+ const attributes = await this.client.getAttributes(currentInodeId, { user: this.user });
128
+ return { inodeId: currentInodeId, attributes };
129
+ }
130
+ /**
131
+ * Resolve a path, following symlinks on ALL components including the
132
+ * final one (like stat(2)). Use resolve() when you need lstat semantics.
133
+ */
134
+ async resolveFollow(path, symlinkDepth = 0) {
135
+ if (symlinkDepth >= _ArchilFs.MAX_SYMLINKS) {
136
+ throw new Error(`ELOOP: too many levels of symbolic links, '${path}'`);
137
+ }
138
+ const resolved = await this.resolve(path, symlinkDepth);
139
+ if (resolved.attributes.inodeType === "Symlink" && resolved.attributes.symlinkTarget) {
140
+ const targetPath = resolved.attributes.symlinkTarget.startsWith("/") ? resolved.attributes.symlinkTarget : this.resolvePath(path, "..", resolved.attributes.symlinkTarget);
141
+ return this.resolveFollow(targetPath, symlinkDepth + 1);
142
+ }
143
+ return resolved;
144
+ }
145
+ /**
146
+ * Resolve parent directory and get child name
147
+ */
148
+ async resolveParent(path) {
149
+ debug("resolveParent raw path=%j (bytes: %o)", path, Buffer.from(path));
150
+ const normalizedPath = this.normalizePath(path);
151
+ const lastSlash = normalizedPath.lastIndexOf("/");
152
+ const parentPath = lastSlash === 0 ? "/" : normalizedPath.substring(0, lastSlash);
153
+ const name = normalizedPath.substring(lastSlash + 1);
154
+ debug("resolveParent extracted name=%j (bytes: %o)", name, Buffer.from(name));
155
+ const { inodeId: parentInodeId } = await this.resolve(parentPath);
156
+ return { parentInodeId, name };
157
+ }
158
+ /**
159
+ * Convert InodeAttributes to FsStat
160
+ */
161
+ toStat(attrs) {
162
+ return {
163
+ isFile: attrs.inodeType === "File",
164
+ isDirectory: attrs.inodeType === "Directory",
165
+ isSymbolicLink: attrs.inodeType === "Symlink",
166
+ mode: attrs.mode,
167
+ size: Number(attrs.size),
168
+ mtime: new Date(attrs.mtimeMs)
169
+ };
170
+ }
171
+ static DIR_PAGE_SIZE = 1e3;
172
+ /**
173
+ * Read all directory entries using the paginated API.
174
+ */
175
+ async readAllDirectoryEntries(inodeId) {
176
+ const handle = await this.client.openDirectory(inodeId, { user: this.user });
177
+ try {
178
+ const allEntries = [];
179
+ let cursor;
180
+ for (; ; ) {
181
+ const page = await this.client.readDirectory(
182
+ inodeId,
183
+ handle,
184
+ _ArchilFs.DIR_PAGE_SIZE,
185
+ cursor,
186
+ { user: this.user }
187
+ );
188
+ allEntries.push(...page.entries);
189
+ if (!page.nextCursor) break;
190
+ cursor = page.nextCursor;
158
191
  }
159
- async readFileBuffer(path) {
160
- debug("readFileBuffer path=%s", path);
161
- const { inodeId, attributes } = await this.resolve(path);
162
- debug("readFileBuffer resolved inodeId=%d type=%s size=%d", inodeId, attributes.inodeType, attributes.size);
163
- if (attributes.inodeType !== "File") {
164
- throw new Error(`EISDIR: illegal operation on a directory, read '${path}'`);
165
- }
166
- const size = Number(attributes.size);
167
- if (size === 0) {
168
- debug("readFileBuffer file is empty, returning empty buffer");
169
- return new Uint8Array(0);
170
- }
171
- const MAX_CHUNK = 4 * 1024 * 1024;
172
- if (size <= MAX_CHUNK) {
173
- const buffer = await this.client.readInode(inodeId, 0, size, { user: this.user });
174
- return new Uint8Array(buffer);
175
- }
176
- const result = new Uint8Array(size);
177
- let offset = 0;
178
- while (offset < size) {
179
- const chunkSize = Math.min(MAX_CHUNK, size - offset);
180
- const chunk = await this.client.readInode(inodeId, offset, chunkSize, { user: this.user });
181
- result.set(new Uint8Array(chunk), offset);
182
- offset += chunkSize;
183
- }
184
- return result;
192
+ return allEntries;
193
+ } finally {
194
+ this.client.closeDirectory(inodeId, handle);
195
+ }
196
+ }
197
+ // ========================================================================
198
+ // IFileSystem Implementation - Read Operations
199
+ // ========================================================================
200
+ resolvePath(base, ...paths) {
201
+ debug("resolvePath base=%s paths=%o", base, paths);
202
+ let result = base;
203
+ for (const p of paths) {
204
+ if (p.startsWith("/")) {
205
+ result = p;
206
+ } else {
207
+ result = result.endsWith("/") ? result + p : result + "/" + p;
185
208
  }
186
- async readdir(path) {
187
- debug("readdir path=%s", path);
188
- const { inodeId, attributes } = await this.resolve(path);
189
- if (attributes.inodeType !== "Directory") {
190
- throw new Error(`ENOTDIR: not a directory, scandir '${path}'`);
191
- }
192
- const entries = await this.client.readDirectory(inodeId, { user: this.user });
193
- for (const e of entries) {
194
- debug("readdir entry name=%j (bytes: %o)", e.name, Buffer.from(e.name));
195
- }
196
- return entries.map((e) => e.name).filter((name) => name !== "." && name !== "..");
209
+ }
210
+ const normalized = this.normalizePath(result);
211
+ debug("resolvePath result=%s", normalized);
212
+ return normalized;
213
+ }
214
+ async readFile(path, encoding) {
215
+ debug("readFile path=%s encoding=%s", path, encoding);
216
+ try {
217
+ const buffer = await this.readFileBuffer(path);
218
+ debug("readFile got buffer length=%d", buffer.length);
219
+ const enc = encoding || "utf-8";
220
+ let result;
221
+ if (enc === "base64" || enc === "hex" || enc === "binary") {
222
+ result = Buffer.from(buffer).toString(enc);
223
+ } else {
224
+ result = new TextDecoder(enc).decode(buffer);
197
225
  }
198
- async readdirWithFileTypes(path) {
199
- const { inodeId, attributes } = await this.resolve(path);
200
- if (attributes.inodeType !== "Directory") {
201
- throw new Error(`ENOTDIR: not a directory, scandir '${path}'`);
202
- }
203
- const entries = await this.client.readDirectory(inodeId, { user: this.user });
204
- return entries.filter((e) => e.name !== "." && e.name !== "..").map((e) => ({
205
- name: e.name,
206
- isFile: e.inodeType === "File",
207
- isDirectory: e.inodeType === "Directory",
208
- isSymlink: e.inodeType === "Symlink"
209
- }));
226
+ debug("readFile decoded to string length=%d", result.length);
227
+ return result;
228
+ } catch (err) {
229
+ debug("readFile FAILED: %O", err);
230
+ throw err;
231
+ }
232
+ }
233
+ async readFileBuffer(path) {
234
+ debug("readFileBuffer path=%s", path);
235
+ const { inodeId, attributes } = await this.resolveFollow(path);
236
+ debug("readFileBuffer resolved inodeId=%d type=%s size=%d", inodeId, attributes.inodeType, attributes.size);
237
+ if (attributes.inodeType !== "File") {
238
+ throw new Error(`EISDIR: illegal operation on a directory, read '${path}'`);
239
+ }
240
+ const size = Number(attributes.size);
241
+ if (size === 0) {
242
+ debug("readFileBuffer file is empty, returning empty buffer");
243
+ return new Uint8Array(0);
244
+ }
245
+ const MAX_CHUNK = 4 * 1024 * 1024;
246
+ if (size <= MAX_CHUNK) {
247
+ const buffer = await this.client.readInode(inodeId, 0, size, { user: this.user });
248
+ return new Uint8Array(buffer);
249
+ }
250
+ const result = new Uint8Array(size);
251
+ let offset = 0;
252
+ while (offset < size) {
253
+ const chunkSize = Math.min(MAX_CHUNK, size - offset);
254
+ const chunk = await this.client.readInode(inodeId, offset, chunkSize, { user: this.user });
255
+ result.set(new Uint8Array(chunk), offset);
256
+ offset += chunkSize;
257
+ }
258
+ return result;
259
+ }
260
+ async readdir(path) {
261
+ debug("readdir path=%s", path);
262
+ const { inodeId, attributes } = await this.resolveFollow(path);
263
+ if (attributes.inodeType !== "Directory") {
264
+ throw new Error(`ENOTDIR: not a directory, scandir '${path}'`);
265
+ }
266
+ const entries = await this.readAllDirectoryEntries(inodeId);
267
+ for (const e of entries) {
268
+ debug("readdir entry name=%j (bytes: %o)", e.name, Buffer.from(e.name));
269
+ }
270
+ return entries.map((e) => e.name).filter((name) => name !== "." && name !== "..");
271
+ }
272
+ async readdirWithFileTypes(path) {
273
+ const { inodeId, attributes } = await this.resolveFollow(path);
274
+ if (attributes.inodeType !== "Directory") {
275
+ throw new Error(`ENOTDIR: not a directory, scandir '${path}'`);
276
+ }
277
+ const entries = await this.readAllDirectoryEntries(inodeId);
278
+ return entries.filter((e) => e.name !== "." && e.name !== "..").map((e) => ({
279
+ name: e.name,
280
+ isFile: e.inodeType === "File",
281
+ isDirectory: e.inodeType === "Directory",
282
+ isSymbolicLink: e.inodeType === "Symlink"
283
+ }));
284
+ }
285
+ async stat(path) {
286
+ debug("stat path=%s", path);
287
+ const { attributes } = await this.resolveFollow(path);
288
+ return this.toStat(attributes);
289
+ }
290
+ async lstat(path) {
291
+ debug("lstat path=%s", path);
292
+ const { attributes } = await this.resolve(path);
293
+ return this.toStat(attributes);
294
+ }
295
+ async exists(path) {
296
+ debug("exists path=%s", path);
297
+ try {
298
+ await this.resolve(path);
299
+ debug("exists path=%s -> true", path);
300
+ return true;
301
+ } catch {
302
+ debug("exists path=%s -> false", path);
303
+ return false;
304
+ }
305
+ }
306
+ async readlink(path) {
307
+ const { attributes } = await this.resolve(path);
308
+ if (attributes.inodeType !== "Symlink") {
309
+ throw new Error(`EINVAL: invalid argument, readlink '${path}'`);
310
+ }
311
+ return attributes.symlinkTarget || "";
312
+ }
313
+ async realpath(path, symlinkDepth = 0) {
314
+ const normalizedPath = this.normalizePath(path);
315
+ const parts = normalizedPath.split("/").filter((p) => p !== "");
316
+ let resolvedPath = "/";
317
+ let currentInodeId = this.rootInodeId;
318
+ for (const part of parts) {
319
+ const response = await this.client.lookupInode(currentInodeId, part, { user: this.user });
320
+ if (response === null) {
321
+ throw new Error(`ENOENT: no such file or directory, realpath '${path}'`);
210
322
  }
211
- async stat(path) {
212
- debug("stat path=%s", path);
213
- const { attributes } = await this.resolve(path);
214
- if (attributes.inodeType === "Symlink" && attributes.symlinkTarget) {
215
- const targetPath = attributes.symlinkTarget.startsWith("/") ? attributes.symlinkTarget : this.resolvePath(path, "..", attributes.symlinkTarget);
216
- return this.stat(targetPath);
323
+ const attrs = response.attributes;
324
+ if (attrs.inodeType === "Symlink" && attrs.symlinkTarget) {
325
+ if (symlinkDepth >= _ArchilFs.MAX_SYMLINKS) {
326
+ throw new Error(`ELOOP: too many levels of symbolic links, '${path}'`);
217
327
  }
218
- return this.toStat(attributes);
328
+ const targetPath = attrs.symlinkTarget.startsWith("/") ? attrs.symlinkTarget : this.resolvePath(resolvedPath, attrs.symlinkTarget);
329
+ const resolved = await this.realpath(targetPath, symlinkDepth + 1);
330
+ resolvedPath = resolved;
331
+ const { inodeId } = await this.resolve(resolved);
332
+ currentInodeId = inodeId;
333
+ } else {
334
+ resolvedPath = resolvedPath === "/" ? "/" + part : resolvedPath + "/" + part;
335
+ currentInodeId = response.inodeId;
336
+ }
337
+ }
338
+ return resolvedPath;
339
+ }
340
+ // ========================================================================
341
+ // IFileSystem Implementation - Write Operations
342
+ // ========================================================================
343
+ async writeFile(path, content) {
344
+ debug("writeFile path=%s contentLength=%d", path, content.length);
345
+ const data = typeof content === "string" ? new TextEncoder().encode(content) : content;
346
+ let resolved;
347
+ try {
348
+ resolved = await this.resolveFollow(path);
349
+ } catch (err) {
350
+ if (err instanceof Error && err.message.includes("ENOENT")) {
351
+ resolved = null;
352
+ } else {
353
+ throw err;
219
354
  }
220
- async lstat(path) {
221
- debug("lstat path=%s", path);
222
- const { attributes } = await this.resolve(path);
223
- return this.toStat(attributes);
355
+ }
356
+ if (resolved !== null) {
357
+ debug("writeFile resolved existing file path=%s inodeId=%d", path, resolved.inodeId);
358
+ if (resolved.attributes.inodeType !== "File") {
359
+ throw new Error(`EISDIR: illegal operation on a directory, write '${path}'`);
224
360
  }
225
- async exists(path) {
226
- debug("exists path=%s", path);
361
+ const realPath = await this.realpath(path);
362
+ const { parentInodeId: parentInodeId2, name: name2 } = await this.resolveParent(realPath);
363
+ const tmpName = `.~tmp-${Math.random().toString(36).slice(2)}`;
364
+ debug("writeFile atomic overwrite via temp=%s", tmpName);
365
+ const tmpResult = await this.client.create(
366
+ parentInodeId2,
367
+ tmpName,
368
+ {
369
+ inodeType: "File",
370
+ uid: resolved.attributes.uid,
371
+ gid: resolved.attributes.gid,
372
+ mode: resolved.attributes.mode
373
+ },
374
+ { user: this.user }
375
+ );
376
+ try {
377
+ await this.client.writeData(tmpResult.inodeId, 0, Buffer.from(data), { user: this.user });
378
+ await this.client.rename(parentInodeId2, tmpName, parentInodeId2, name2, { user: this.user });
379
+ debug("writeFile atomic overwrite succeeded");
380
+ } catch (writeErr) {
227
381
  try {
228
- await this.resolve(path);
229
- debug("exists path=%s -> true", path);
230
- return true;
382
+ await this.client.unlink(parentInodeId2, tmpName, { user: this.user });
231
383
  } catch {
232
- debug("exists path=%s -> false", path);
233
- return false;
234
384
  }
385
+ throw writeErr;
235
386
  }
236
- async readlink(path) {
237
- const { attributes } = await this.resolve(path);
238
- if (attributes.inodeType !== "Symlink") {
239
- throw new Error(`EINVAL: invalid argument, readlink '${path}'`);
240
- }
241
- return attributes.symlinkTarget || "";
242
- }
243
- async realpath(path) {
244
- const normalizedPath = this.normalizePath(path);
245
- const parts = normalizedPath.split("/").filter((p) => p !== "");
246
- let resolvedPath = "/";
247
- let currentInodeId = this.rootInodeId;
248
- for (const part of parts) {
249
- const response = await this.client.lookupInode(currentInodeId, part, { user: this.user });
250
- if (response.inodeId === -1) {
251
- throw new Error(`ENOENT: no such file or directory, realpath '${path}'`);
252
- }
253
- const attrs = response.attributes;
254
- if (attrs.inodeType === "Symlink" && attrs.symlinkTarget) {
255
- const targetPath = attrs.symlinkTarget.startsWith("/") ? attrs.symlinkTarget : this.resolvePath(resolvedPath, attrs.symlinkTarget);
256
- const resolved = await this.realpath(targetPath);
257
- resolvedPath = resolved;
258
- const { inodeId } = await this.resolve(resolved);
259
- currentInodeId = inodeId;
260
- } else {
261
- resolvedPath = resolvedPath === "/" ? "/" + part : resolvedPath + "/" + part;
262
- currentInodeId = response.inodeId;
263
- }
264
- }
265
- return resolvedPath;
387
+ return;
388
+ }
389
+ debug("writeFile file doesn't exist, creating: %s", path);
390
+ const { parentInodeId, name } = await this.resolveParent(path);
391
+ debug("writeFile resolved parent parentInodeId=%d name=%s", parentInodeId, name);
392
+ const result = await this.client.create(
393
+ parentInodeId,
394
+ name,
395
+ {
396
+ inodeType: "File",
397
+ uid: this.user?.uid ?? 0,
398
+ gid: this.user?.gid ?? 0,
399
+ mode: 420
400
+ },
401
+ { user: this.user }
402
+ );
403
+ debug("writeFile create succeeded inodeId=%d", result.inodeId);
404
+ await this.client.writeData(result.inodeId, 0, Buffer.from(data), { user: this.user });
405
+ debug("writeFile write succeeded");
406
+ }
407
+ async appendFile(path, content) {
408
+ const data = typeof content === "string" ? new TextEncoder().encode(content) : content;
409
+ let inodeId;
410
+ let size;
411
+ try {
412
+ const resolved = await this.resolveFollow(path);
413
+ if (resolved.attributes.inodeType !== "File") {
414
+ throw new Error(`EISDIR: illegal operation on a directory, write '${path}'`);
266
415
  }
267
- // ========================================================================
268
- // IFileSystem Implementation - Write Operations
269
- // ========================================================================
270
- async writeFile(path, content) {
271
- debug("writeFile path=%s contentLength=%d", path, content.length);
272
- const data = typeof content === "string" ? new TextEncoder().encode(content) : content;
273
- let inodeId;
274
- try {
275
- const resolved = await this.resolve(path);
276
- inodeId = resolved.inodeId;
277
- debug("writeFile resolved existing file path=%s inodeId=%d", path, inodeId);
278
- if (resolved.attributes.inodeType !== "File") {
279
- throw new Error(`EISDIR: illegal operation on a directory, write '${path}'`);
280
- }
281
- } catch (err) {
282
- if (err instanceof Error && err.message.includes("EISDIR")) {
283
- throw err;
284
- }
285
- debug("writeFile file doesn't exist, creating: %s", path);
286
- const { parentInodeId, name } = await this.resolveParent(path);
287
- debug("writeFile resolved parent parentInodeId=%d name=%s", parentInodeId, name);
288
- const now = Date.now();
289
- debug("writeFile calling create parent=%d name=%s", parentInodeId, name);
290
- try {
291
- inodeId = await this.client.create(
292
- parentInodeId,
293
- name,
294
- {
295
- inodeId: 0,
296
- // Will be assigned by the filesystem
297
- inodeType: "File",
298
- size: 0,
299
- uid: this.user?.uid ?? 0,
300
- gid: this.user?.gid ?? 0,
301
- mode: 420,
302
- nlink: 1,
303
- ctimeMs: now,
304
- atimeMs: now,
305
- mtimeMs: now,
306
- btimeMs: now,
307
- rdev: void 0,
308
- symlinkTarget: void 0
309
- },
310
- { user: this.user }
311
- );
312
- debug("writeFile create succeeded inodeId=%d", inodeId);
313
- } catch (createErr) {
314
- debug("writeFile create FAILED: %O", createErr);
315
- throw createErr;
316
- }
317
- }
318
- debug("writeFile writing %d bytes to inodeId=%d", data.length, inodeId);
319
- await this.client.writeData(inodeId, 0, Buffer.from(data), { user: this.user });
320
- debug("writeFile write succeeded");
416
+ inodeId = resolved.inodeId;
417
+ size = Number(resolved.attributes.size);
418
+ } catch (err) {
419
+ if (err instanceof Error && err.message.includes("ENOENT")) {
420
+ await this.writeFile(path, data);
421
+ return;
321
422
  }
322
- async appendFile(path, content) {
323
- let existing;
324
- try {
325
- existing = await this.readFileBuffer(path);
326
- } catch {
327
- existing = new Uint8Array(0);
423
+ throw err;
424
+ }
425
+ await this.client.writeData(inodeId, size, Buffer.from(data), { user: this.user });
426
+ }
427
+ async mkdir(path, options) {
428
+ const normalizedPath = this.normalizePath(path);
429
+ if (options?.recursive) {
430
+ const parts = normalizedPath.split("/").filter((p) => p !== "");
431
+ let currentPath = "";
432
+ for (const part of parts) {
433
+ currentPath += "/" + part;
434
+ const exists = await this.exists(currentPath);
435
+ if (exists) {
436
+ continue;
328
437
  }
329
- const data = typeof content === "string" ? new TextEncoder().encode(content) : content;
330
- const combined = new Uint8Array(existing.length + data.length);
331
- combined.set(existing, 0);
332
- combined.set(data, existing.length);
333
- await this.writeFile(path, combined);
438
+ await this.mkdirSingle(currentPath);
334
439
  }
335
- async mkdir(path, options) {
336
- const normalizedPath = this.normalizePath(path);
337
- if (options?.recursive) {
338
- const parts = normalizedPath.split("/").filter((p) => p !== "");
339
- let currentPath = "";
340
- for (const part of parts) {
341
- currentPath += "/" + part;
342
- const exists = await this.exists(currentPath);
343
- if (exists) {
344
- continue;
345
- }
346
- await this.mkdirSingle(currentPath);
347
- }
348
- } else {
349
- await this.mkdirSingle(normalizedPath);
440
+ } else {
441
+ await this.mkdirSingle(normalizedPath);
442
+ }
443
+ }
444
+ async mkdirSingle(path) {
445
+ const { parentInodeId, name } = await this.resolveParent(path);
446
+ await this.client.create(
447
+ parentInodeId,
448
+ name,
449
+ {
450
+ inodeType: "Directory",
451
+ uid: this.user?.uid ?? 0,
452
+ gid: this.user?.gid ?? 0,
453
+ mode: 493
454
+ },
455
+ { user: this.user }
456
+ );
457
+ }
458
+ async rm(path, options) {
459
+ let resolved;
460
+ try {
461
+ resolved = await this.resolve(path);
462
+ } catch (err) {
463
+ if (err instanceof Error && err.message.includes("ENOENT")) {
464
+ if (options?.force) {
465
+ return;
350
466
  }
467
+ throw err;
351
468
  }
352
- async mkdirSingle(path) {
353
- const { parentInodeId, name } = await this.resolveParent(path);
354
- const now = Date.now();
355
- await this.client.create(
356
- parentInodeId,
357
- name,
358
- {
359
- inodeId: 0,
360
- // Will be assigned by the filesystem
361
- inodeType: "Directory",
362
- size: 4096,
363
- uid: this.user?.uid ?? 0,
364
- gid: this.user?.gid ?? 0,
365
- mode: 493,
366
- nlink: 2,
367
- ctimeMs: now,
368
- atimeMs: now,
369
- mtimeMs: now,
370
- btimeMs: now,
371
- rdev: void 0,
372
- symlinkTarget: void 0
373
- },
374
- { user: this.user }
375
- );
376
- }
377
- async rm(path, options) {
378
- let resolved;
379
- try {
380
- resolved = await this.resolve(path);
381
- } catch {
382
- if (options?.force) {
383
- return;
384
- }
385
- throw new Error(`ENOENT: no such file or directory, rm '${path}'`);
386
- }
387
- if (resolved.attributes.inodeType === "Directory") {
388
- if (!options?.recursive) {
389
- throw new Error(`EISDIR: illegal operation on a directory, rm '${path}'`);
390
- }
391
- const entries = await this.readdir(path);
392
- for (const entry of entries) {
393
- await this.rm(this.resolvePath(path, entry), options);
394
- }
395
- }
396
- const { parentInodeId, name } = await this.resolveParent(path);
397
- await this.client.unlink(parentInodeId, name, { user: this.user });
469
+ throw err;
470
+ }
471
+ if (resolved.attributes.inodeType === "Directory") {
472
+ if (!options?.recursive) {
473
+ throw new Error(`EISDIR: illegal operation on a directory, rm '${path}'`);
398
474
  }
399
- async cp(src, dest, options) {
400
- const srcResolved = await this.resolve(src);
401
- if (srcResolved.attributes.inodeType === "Directory") {
402
- if (!options?.recursive) {
403
- throw new Error(`EISDIR: illegal operation on a directory, cp '${src}'`);
404
- }
405
- await this.mkdir(dest, { recursive: true });
406
- const entries = await this.readdir(src);
407
- for (const entry of entries) {
408
- await this.cp(
409
- this.resolvePath(src, entry),
410
- this.resolvePath(dest, entry),
411
- options
412
- );
413
- }
414
- } else {
415
- const content = await this.readFileBuffer(src);
416
- await this.writeFile(dest, content);
417
- }
475
+ const entries = await this.readdir(path);
476
+ for (const entry of entries) {
477
+ await this.rm(this.resolvePath(path, entry), options);
418
478
  }
419
- async mv(src, dest) {
420
- const { parentInodeId: srcParentInodeId, name: srcName } = await this.resolveParent(src);
421
- const { parentInodeId: destParentInodeId, name: destName } = await this.resolveParent(dest);
422
- await this.client.rename(
423
- srcParentInodeId,
424
- srcName,
425
- destParentInodeId,
426
- destName,
427
- { user: this.user }
428
- );
479
+ }
480
+ const { parentInodeId, name } = await this.resolveParent(path);
481
+ await this.client.unlink(parentInodeId, name, { user: this.user });
482
+ }
483
+ async cp(src, dest, options) {
484
+ const srcResolved = await this.resolveFollow(src);
485
+ if (srcResolved.attributes.inodeType === "Directory") {
486
+ if (!options?.recursive) {
487
+ throw new Error(`EISDIR: illegal operation on a directory, cp '${src}'`);
429
488
  }
430
- async symlink(target, path) {
431
- const { parentInodeId, name } = await this.resolveParent(path);
432
- const now = Date.now();
433
- await this.client.create(
434
- parentInodeId,
435
- name,
436
- {
437
- inodeId: 0,
438
- // Will be assigned by the filesystem
439
- inodeType: "Symlink",
440
- size: target.length,
441
- uid: this.user?.uid ?? 0,
442
- gid: this.user?.gid ?? 0,
443
- mode: 511,
444
- nlink: 1,
445
- ctimeMs: now,
446
- atimeMs: now,
447
- mtimeMs: now,
448
- btimeMs: now,
449
- rdev: void 0,
450
- symlinkTarget: target
451
- },
452
- { user: this.user }
489
+ await this.mkdir(dest, { recursive: true });
490
+ const entries = await this.readdir(src);
491
+ for (const entry of entries) {
492
+ await this.cp(
493
+ this.resolvePath(src, entry),
494
+ this.resolvePath(dest, entry),
495
+ options
453
496
  );
454
497
  }
455
- async link(existingPath, newPath) {
456
- throw new Error(
457
- "Hard link operations not yet implemented. The archil-node bindings need to expose link for hard links."
458
- );
498
+ } else {
499
+ const content = await this.readFileBuffer(src);
500
+ await this.writeFile(dest, content);
501
+ }
502
+ }
503
+ async mv(src, dest) {
504
+ const { parentInodeId: srcParentInodeId, name: srcName } = await this.resolveParent(src);
505
+ const { parentInodeId: destParentInodeId, name: destName } = await this.resolveParent(dest);
506
+ await this.client.rename(
507
+ srcParentInodeId,
508
+ srcName,
509
+ destParentInodeId,
510
+ destName,
511
+ { user: this.user }
512
+ );
513
+ }
514
+ async symlink(target, path) {
515
+ const { parentInodeId, name } = await this.resolveParent(path);
516
+ await this.client.create(
517
+ parentInodeId,
518
+ name,
519
+ {
520
+ inodeType: "Symlink",
521
+ uid: this.user?.uid ?? 0,
522
+ gid: this.user?.gid ?? 0,
523
+ mode: 511,
524
+ symlinkTarget: target
525
+ },
526
+ { user: this.user }
527
+ );
528
+ }
529
+ async link(existingPath, newPath) {
530
+ throw new Error(
531
+ "Hard link operations not yet implemented. The archil-node bindings need to expose link for hard links."
532
+ );
533
+ }
534
+ async chmod(path, mode) {
535
+ const { inodeId } = await this.resolveFollow(path);
536
+ await this.client.setattr(inodeId, { mode }, { user: this.user ?? { uid: 0, gid: 0 } });
537
+ }
538
+ async utimes(path, atime, mtime) {
539
+ debug("utimes path=%s atime=%s mtime=%s", path, atime.toISOString(), mtime.toISOString());
540
+ const { inodeId } = await this.resolveFollow(path);
541
+ debug("utimes resolved path=%s inodeId=%d", path, inodeId);
542
+ const atimeMs = atime.getTime();
543
+ const mtimeMs = mtime.getTime();
544
+ debug("utimes calling setattr inodeId=%d atimeMs=%d mtimeMs=%d", inodeId, atimeMs, mtimeMs);
545
+ await this.client.setattr(
546
+ inodeId,
547
+ { atimeMs, mtimeMs },
548
+ { user: this.user ?? { uid: 0, gid: 0 } }
549
+ );
550
+ debug("utimes setattr succeeded inodeId=%d", inodeId);
551
+ }
552
+ // ========================================================================
553
+ // Utility Methods
554
+ // ========================================================================
555
+ getAllPaths() {
556
+ return [];
557
+ }
558
+ /**
559
+ * Resolve a path to its inode ID (public wrapper for delegation operations)
560
+ */
561
+ async resolveInodeId(path) {
562
+ const { inodeId } = await this.resolve(path);
563
+ return inodeId;
564
+ }
565
+ };
566
+
567
+ // src/commands.ts
568
+ var import_just_bash = require("just-bash");
569
+ function createArchilCommand(client, fs) {
570
+ return (0, import_just_bash.defineCommand)("archil", async (args, ctx) => {
571
+ const ok = (stdout) => ({ stdout, stderr: "", exitCode: 0 });
572
+ const fail = (stderr) => ({ stdout: "", stderr, exitCode: 1 });
573
+ const subcommand = args[0];
574
+ if (subcommand === "checkout") {
575
+ const rest = args.slice(1);
576
+ const force = rest.includes("--force") || rest.includes("-f");
577
+ const pathArg = rest.find((a) => !a.startsWith("-"));
578
+ if (!pathArg) {
579
+ return fail("Usage: archil checkout [--force|-f] <path>\n");
459
580
  }
460
- async chmod(path, mode) {
461
- const { inodeId } = await this.resolve(path);
462
- await this.client.setattr(inodeId, { mode }, { user: this.user ?? { uid: 0, gid: 0 } });
581
+ const fullPath = fs.resolvePath(ctx.cwd, pathArg);
582
+ try {
583
+ const inodeId = await fs.resolveInodeId(fullPath);
584
+ await client.checkout(inodeId, { force });
585
+ return ok(`Checked out: ${fullPath} (inode ${inodeId})${force ? " (forced)" : ""}
586
+ `);
587
+ } catch (err) {
588
+ return fail(`Failed to checkout ${fullPath}: ${err instanceof Error ? err.message : err}
589
+ `);
463
590
  }
464
- async utimes(path, atime, mtime) {
465
- debug("utimes path=%s atime=%d mtime=%d", path, atime, mtime);
466
- const { inodeId } = await this.resolve(path);
467
- debug("utimes resolved path=%s inodeId=%d", path, inodeId);
468
- const atimeMs = atime === -1 ? -1 : Math.floor(atime * 1e3);
469
- const mtimeMs = mtime === -1 ? -1 : Math.floor(mtime * 1e3);
470
- debug("utimes calling setattr inodeId=%d atimeMs=%d mtimeMs=%d", inodeId, atimeMs, mtimeMs);
471
- await this.client.setattr(
472
- inodeId,
473
- { atimeMs, mtimeMs },
474
- { user: this.user ?? { uid: 0, gid: 0 } }
475
- );
476
- debug("utimes setattr succeeded inodeId=%d", inodeId);
591
+ }
592
+ if (subcommand === "checkin") {
593
+ const pathArg = args[1];
594
+ if (!pathArg) {
595
+ return fail("Usage: archil checkin <path>\n");
477
596
  }
478
- // ========================================================================
479
- // Utility Methods
480
- // ========================================================================
481
- async getAllPaths() {
482
- const paths = [];
483
- const walk = async (dirPath) => {
484
- paths.push(dirPath);
485
- try {
486
- const entries = await this.readdirWithFileTypes(dirPath);
487
- for (const entry of entries) {
488
- const fullPath = this.resolvePath(dirPath, entry.name);
489
- if (entry.isDirectory) {
490
- await walk(fullPath);
491
- } else {
492
- paths.push(fullPath);
493
- }
494
- }
495
- } catch {
496
- }
497
- };
498
- await walk("/");
499
- return paths;
597
+ const fullPath = fs.resolvePath(ctx.cwd, pathArg);
598
+ try {
599
+ const inodeId = await fs.resolveInodeId(fullPath);
600
+ await client.checkin(inodeId);
601
+ return ok(`Checked in: ${fullPath} (inode ${inodeId})
602
+ `);
603
+ } catch (err) {
604
+ return fail(`Failed to checkin ${fullPath}: ${err instanceof Error ? err.message : err}
605
+ `);
500
606
  }
501
- /**
502
- * Resolve a path to its inode ID (public wrapper for delegation operations)
503
- */
504
- async resolveInodeId(path) {
505
- const { inodeId } = await this.resolve(path);
506
- return inodeId;
607
+ }
608
+ if (subcommand === "list-delegations" || subcommand === "delegations") {
609
+ try {
610
+ const delegations = client.listDelegations();
611
+ if (delegations.length === 0) {
612
+ return ok("No delegations held\n");
613
+ }
614
+ const lines = delegations.map((d) => ` inode ${d.inodeId}: ${d.state}`);
615
+ return ok("Delegations:\n" + lines.join("\n") + "\n");
616
+ } catch (err) {
617
+ return fail(`Failed to list delegations: ${err instanceof Error ? err.message : err}
618
+ `);
507
619
  }
508
- };
509
- }
510
- });
620
+ }
621
+ if (subcommand === "help" || !subcommand) {
622
+ return ok(
623
+ "Archil commands:\n archil checkout [--force|-f] <path> - Acquire write delegation\n archil checkin <path> - Release write delegation\n archil list-delegations - Show held delegations\n archil help - Show this help message\n\nThe --force flag revokes any existing delegation from other clients.\n"
624
+ );
625
+ }
626
+ return fail(`Unknown archil command: ${subcommand}
627
+ Run 'archil help' for available commands
628
+ `);
629
+ });
630
+ }
511
631
 
512
632
  // src/index.ts
513
- var index_exports = {};
514
- __export(index_exports, {
515
- ArchilFs: () => ArchilFs,
516
- createArchilFs: () => createArchilFs
517
- });
518
- module.exports = __toCommonJS(index_exports);
519
- init_ArchilFs();
520
- function createArchilFs(client, options) {
521
- const { ArchilFs: ArchilFs2 } = (init_ArchilFs(), __toCommonJS(ArchilFs_exports));
522
- return new ArchilFs2(client, options);
633
+ async function createArchilFs(client, options) {
634
+ return ArchilFs.create(client, options);
523
635
  }
524
636
  // Annotate the CommonJS export names for ESM import in node:
525
637
  0 && (module.exports = {
526
638
  ArchilFs,
639
+ createArchilCommand,
527
640
  createArchilFs
528
641
  });