@zapier/zapier-sdk 0.8.2 → 0.9.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.
Files changed (104) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +10 -33
  3. package/dist/api/client.d.ts.map +1 -1
  4. package/dist/api/client.js +1 -2
  5. package/dist/api/polling.d.ts +36 -6
  6. package/dist/api/polling.d.ts.map +1 -1
  7. package/dist/api/polling.js +132 -28
  8. package/dist/api/polling.test.d.ts +2 -0
  9. package/dist/api/polling.test.d.ts.map +1 -0
  10. package/dist/api/polling.test.js +318 -0
  11. package/dist/api/types.d.ts +1 -2
  12. package/dist/api/types.d.ts.map +1 -1
  13. package/dist/index.cjs +489 -252
  14. package/dist/index.d.mts +182 -187
  15. package/dist/index.d.ts +1 -2
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +0 -1
  18. package/dist/index.mjs +486 -251
  19. package/dist/plugins/apps/index.d.ts +4 -0
  20. package/dist/plugins/apps/index.d.ts.map +1 -1
  21. package/dist/plugins/getApp/index.d.ts +2 -7
  22. package/dist/plugins/getApp/index.d.ts.map +1 -1
  23. package/dist/plugins/getApp/index.js +9 -9
  24. package/dist/plugins/getApp/index.test.js +1 -1
  25. package/dist/plugins/getAuthentication/index.test.js +1 -1
  26. package/dist/plugins/listActions/index.d.ts +2 -4
  27. package/dist/plugins/listActions/index.d.ts.map +1 -1
  28. package/dist/plugins/listActions/index.js +1 -1
  29. package/dist/plugins/listActions/index.test.js +4 -4
  30. package/dist/plugins/listApps/index.d.ts +4 -7
  31. package/dist/plugins/listApps/index.d.ts.map +1 -1
  32. package/dist/plugins/listApps/index.js +33 -17
  33. package/dist/plugins/listApps/index.test.js +22 -2
  34. package/dist/plugins/listAuthentications/index.d.ts +2 -4
  35. package/dist/plugins/listAuthentications/index.d.ts.map +1 -1
  36. package/dist/plugins/listAuthentications/index.js +4 -0
  37. package/dist/plugins/listAuthentications/index.test.js +39 -13
  38. package/dist/plugins/listAuthentications/schemas.d.ts +3 -0
  39. package/dist/plugins/listAuthentications/schemas.d.ts.map +1 -1
  40. package/dist/plugins/listAuthentications/schemas.js +4 -0
  41. package/dist/plugins/manifest/index.d.ts +25 -9
  42. package/dist/plugins/manifest/index.d.ts.map +1 -1
  43. package/dist/plugins/manifest/index.js +239 -67
  44. package/dist/plugins/manifest/index.test.js +426 -171
  45. package/dist/plugins/manifest/schemas.d.ts +5 -1
  46. package/dist/plugins/manifest/schemas.d.ts.map +1 -1
  47. package/dist/plugins/manifest/schemas.js +1 -0
  48. package/dist/sdk.d.ts +5 -11
  49. package/dist/sdk.d.ts.map +1 -1
  50. package/dist/sdk.js +1 -4
  51. package/dist/types/plugin.d.ts +1 -0
  52. package/dist/types/plugin.d.ts.map +1 -1
  53. package/dist/types/sdk.d.ts +6 -3
  54. package/dist/types/sdk.d.ts.map +1 -1
  55. package/dist/utils/domain-utils.d.ts +16 -0
  56. package/dist/utils/domain-utils.d.ts.map +1 -1
  57. package/dist/utils/domain-utils.js +46 -27
  58. package/dist/utils/domain-utils.test.js +157 -3
  59. package/dist/utils/file-utils.d.ts +4 -0
  60. package/dist/utils/file-utils.d.ts.map +1 -0
  61. package/dist/utils/file-utils.js +74 -0
  62. package/dist/utils/file-utils.test.d.ts +2 -0
  63. package/dist/utils/file-utils.test.d.ts.map +1 -0
  64. package/dist/utils/file-utils.test.js +51 -0
  65. package/package.json +1 -1
  66. package/src/api/client.ts +5 -4
  67. package/src/api/polling.test.ts +405 -0
  68. package/src/api/polling.ts +224 -44
  69. package/src/api/types.ts +1 -2
  70. package/src/index.ts +1 -1
  71. package/src/plugins/apps/index.ts +9 -2
  72. package/src/plugins/getApp/index.test.ts +1 -1
  73. package/src/plugins/getApp/index.ts +12 -14
  74. package/src/plugins/getAuthentication/index.test.ts +1 -1
  75. package/src/plugins/listActions/index.test.ts +8 -7
  76. package/src/plugins/listActions/index.ts +3 -3
  77. package/src/plugins/listApps/index.test.ts +23 -2
  78. package/src/plugins/listApps/index.ts +46 -25
  79. package/src/plugins/listAuthentications/index.test.ts +52 -15
  80. package/src/plugins/listAuthentications/index.ts +7 -2
  81. package/src/plugins/listAuthentications/schemas.ts +4 -0
  82. package/src/plugins/manifest/index.test.ts +503 -197
  83. package/src/plugins/manifest/index.ts +338 -82
  84. package/src/plugins/manifest/schemas.ts +9 -2
  85. package/src/sdk.ts +1 -5
  86. package/src/types/plugin.ts +3 -0
  87. package/src/types/sdk.ts +26 -21
  88. package/src/utils/domain-utils.test.ts +196 -2
  89. package/src/utils/domain-utils.ts +68 -35
  90. package/src/utils/file-utils.test.ts +73 -0
  91. package/src/utils/file-utils.ts +94 -0
  92. package/tsconfig.tsbuildinfo +1 -1
  93. package/dist/plugins/lockVersion/index.d.ts +0 -24
  94. package/dist/plugins/lockVersion/index.d.ts.map +0 -1
  95. package/dist/plugins/lockVersion/index.js +0 -72
  96. package/dist/plugins/lockVersion/index.test.d.ts +0 -2
  97. package/dist/plugins/lockVersion/index.test.d.ts.map +0 -1
  98. package/dist/plugins/lockVersion/index.test.js +0 -129
  99. package/dist/plugins/lockVersion/schemas.d.ts +0 -10
  100. package/dist/plugins/lockVersion/schemas.d.ts.map +0 -1
  101. package/dist/plugins/lockVersion/schemas.js +0 -6
  102. package/src/plugins/lockVersion/index.test.ts +0 -176
  103. package/src/plugins/lockVersion/index.ts +0 -112
  104. package/src/plugins/lockVersion/schemas.ts +0 -9
@@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest";
2
2
  import {
3
3
  groupVersionedAppKeysByType,
4
4
  groupAppKeysByType,
5
+ toAppLocator,
5
6
  } from "./domain-utils";
6
7
 
7
8
  describe("domain-utils", () => {
@@ -134,11 +135,13 @@ describe("domain-utils", () => {
134
135
  "FormatterCLIAPI@1.0.0",
135
136
  "slack@2.1.0", // exact duplicate
136
137
  "FormatterCLIAPI@1.0.0", // exact duplicate
138
+ "slack@2.1.1",
139
+ "FormatterCLIAPI@1.0.1",
137
140
  ]);
138
141
 
139
142
  expect(result).toEqual({
140
- selectedApi: ["FormatterCLIAPI@1.0.0"],
141
- slug: ["slack@2.1.0"],
143
+ selectedApi: ["FormatterCLIAPI@1.0.0", "FormatterCLIAPI@1.0.1"],
144
+ slug: ["slack@2.1.0", "slack@2.1.1"],
142
145
  });
143
146
  });
144
147
  });
@@ -236,4 +239,195 @@ describe("domain-utils", () => {
236
239
  });
237
240
  });
238
241
  });
242
+
243
+ describe("toAppLocator", () => {
244
+ it("should reject UUID app keys", () => {
245
+ expect(() => {
246
+ toAppLocator("61e47557-af91-4b0c-a3e0-c28606357664");
247
+ }).toThrow(
248
+ "UUID app keys are not supported. Use app slug or implementation ID instead of: 61e47557-af91-4b0c-a3e0-c28606357664",
249
+ );
250
+ });
251
+
252
+ it("should reject UUID app keys with versions", () => {
253
+ expect(() => {
254
+ toAppLocator("61e47557-af91-4b0c-a3e0-c28606357664@1.0.0");
255
+ }).toThrow(
256
+ "UUID app keys are not supported. Use app slug or implementation ID instead of: 61e47557-af91-4b0c-a3e0-c28606357664@1.0.0",
257
+ );
258
+ });
259
+
260
+ it("should reject uppercase UUID app keys", () => {
261
+ expect(() => {
262
+ toAppLocator("61E47557-AF91-4B0C-A3E0-C28606357664");
263
+ }).toThrow(
264
+ "UUID app keys are not supported. Use app slug or implementation ID instead of: 61E47557-AF91-4B0C-A3E0-C28606357664",
265
+ );
266
+ });
267
+
268
+ it("should handle simple slug without version", () => {
269
+ const result = toAppLocator("slack");
270
+
271
+ expect(result).toEqual({
272
+ lookupAppKey: "slack",
273
+ slug: "slack",
274
+ implementationName: undefined,
275
+ version: undefined,
276
+ });
277
+ });
278
+
279
+ it("should handle slug with version", () => {
280
+ const result = toAppLocator("slack@1.0.0");
281
+
282
+ expect(result).toEqual({
283
+ lookupAppKey: "slack",
284
+ slug: "slack",
285
+ implementationName: undefined,
286
+ version: "1.0.0",
287
+ });
288
+ });
289
+
290
+ it("should handle dashified slug", () => {
291
+ const result = toAppLocator("google-sheets");
292
+
293
+ expect(result).toEqual({
294
+ lookupAppKey: "google-sheets",
295
+ slug: "google-sheets",
296
+ implementationName: undefined,
297
+ version: undefined,
298
+ });
299
+ });
300
+
301
+ it("should handle dashified slug with version", () => {
302
+ const result = toAppLocator("google-sheets@2.1.3");
303
+
304
+ expect(result).toEqual({
305
+ lookupAppKey: "google-sheets",
306
+ slug: "google-sheets",
307
+ implementationName: undefined,
308
+ version: "2.1.3",
309
+ });
310
+ });
311
+
312
+ it("should handle snake_cased slug and convert to dash", () => {
313
+ const result = toAppLocator("google_sheets");
314
+
315
+ expect(result).toEqual({
316
+ lookupAppKey: "google_sheets",
317
+ slug: "google-sheets",
318
+ implementationName: undefined,
319
+ version: undefined,
320
+ });
321
+ });
322
+
323
+ it("should handle snake_cased slug with version and convert to dash", () => {
324
+ const result = toAppLocator("google_sheets@2.1.3");
325
+
326
+ expect(result).toEqual({
327
+ lookupAppKey: "google_sheets",
328
+ slug: "google-sheets",
329
+ implementationName: undefined,
330
+ version: "2.1.3",
331
+ });
332
+ });
333
+
334
+ it("should handle leading underscore snake_cased slug", () => {
335
+ const result = toAppLocator("_100hires_ats");
336
+
337
+ expect(result).toEqual({
338
+ lookupAppKey: "_100hires_ats",
339
+ slug: "100hires-ats",
340
+ implementationName: undefined,
341
+ version: undefined,
342
+ });
343
+ });
344
+
345
+ it("should handle leading underscore snake_cased slug with version", () => {
346
+ const result = toAppLocator("_100hires_ats@3.0.0");
347
+
348
+ expect(result).toEqual({
349
+ lookupAppKey: "_100hires_ats",
350
+ slug: "100hires-ats",
351
+ implementationName: undefined,
352
+ version: "3.0.0",
353
+ });
354
+ });
355
+
356
+ it("should handle implementation name without version", () => {
357
+ const result = toAppLocator("SlackCLIAPI");
358
+
359
+ expect(result).toEqual({
360
+ lookupAppKey: "SlackCLIAPI",
361
+ slug: undefined,
362
+ implementationName: "SlackCLIAPI",
363
+ version: undefined,
364
+ });
365
+ });
366
+
367
+ it("should handle implementation name with version", () => {
368
+ const result = toAppLocator("SlackCLIAPI@1.29.0");
369
+
370
+ expect(result).toEqual({
371
+ lookupAppKey: "SlackCLIAPI",
372
+ slug: undefined,
373
+ implementationName: "SlackCLIAPI",
374
+ version: "1.29.0",
375
+ });
376
+ });
377
+
378
+ it("should handle edge cases with complex version numbers", () => {
379
+ const result = toAppLocator("slack@1.2.3-beta.4+build.5");
380
+
381
+ expect(result).toEqual({
382
+ lookupAppKey: "slack",
383
+ slug: "slack",
384
+ implementationName: undefined,
385
+ version: "1.2.3-beta.4+build.5",
386
+ });
387
+ });
388
+
389
+ it("should handle single character app keys", () => {
390
+ const result = toAppLocator("a");
391
+
392
+ expect(result).toEqual({
393
+ lookupAppKey: "a",
394
+ slug: "a",
395
+ implementationName: undefined,
396
+ version: undefined,
397
+ });
398
+ });
399
+
400
+ it("should handle app keys with numbers", () => {
401
+ const result = toAppLocator("app123");
402
+
403
+ expect(result).toEqual({
404
+ lookupAppKey: "app123",
405
+ slug: "app123",
406
+ implementationName: undefined,
407
+ version: undefined,
408
+ });
409
+ });
410
+
411
+ it("should handle mixed case app keys that aren't implementation names", () => {
412
+ const result = toAppLocator("SlackBot");
413
+
414
+ expect(result).toEqual({
415
+ lookupAppKey: "SlackBot",
416
+ slug: undefined,
417
+ implementationName: "SlackBot",
418
+ version: undefined,
419
+ });
420
+ });
421
+
422
+ it("should handle empty version (app key ending with @)", () => {
423
+ const result = toAppLocator("slack@");
424
+
425
+ expect(result).toEqual({
426
+ lookupAppKey: "slack",
427
+ slug: "slack",
428
+ implementationName: undefined,
429
+ version: "",
430
+ });
431
+ });
432
+ });
239
433
  });
@@ -204,40 +204,23 @@ export function groupVersionedAppKeysByType(appKeys: string[]): {
204
204
  const seenSlugs = new Set<string>();
205
205
 
206
206
  for (const key of appKeys) {
207
- // Split by @ to get base name for classification (but preserve full key in results)
208
- const [keyWithoutVersion, version] = splitVersionedKey(key);
209
-
210
- // Check if it's a UUID (canonical ID) - not supported
211
- const uuidRegex =
212
- /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
213
- if (uuidRegex.test(keyWithoutVersion)) {
214
- throw new Error(
215
- `UUID app keys are not supported. Use app slug or implementation ID instead of: ${key}`,
216
- );
217
- }
218
-
219
- // Check if it's a snake_case string
220
- if (isSnakeCasedSlug(keyWithoutVersion)) {
221
- const dashified = dashifySnakeCasedSlug(keyWithoutVersion);
222
- const slugWithVersion = version ? `${dashified}@${version}` : dashified;
223
- if (!seenSlugs.has(slugWithVersion)) {
224
- seenSlugs.add(slugWithVersion);
225
- result.slug.push(slugWithVersion); // Preserve full key including version
207
+ const appLocator = toAppLocator(key);
208
+
209
+ if (appLocator.slug) {
210
+ // For slugs, we need to reconstruct the versioned slug
211
+ const versionedSlug = appLocator.version
212
+ ? `${appLocator.slug}@${appLocator.version}`
213
+ : appLocator.slug;
214
+ if (!seenSlugs.has(versionedSlug)) {
215
+ seenSlugs.add(versionedSlug);
216
+ result.slug.push(versionedSlug);
217
+ }
218
+ } else {
219
+ // For implementation names (selectedApi)
220
+ if (!seenSelectedApi.has(key)) {
221
+ seenSelectedApi.add(key);
222
+ result.selectedApi.push(key);
226
223
  }
227
- continue;
228
- }
229
-
230
- // Check if it's a slug (lowercase and dashes)
231
- if (keyWithoutVersion.match(/^[a-z0-9]+(?:-[a-z0-9]+)*$/)) {
232
- seenSlugs.add(key);
233
- result.slug.push(key);
234
- continue;
235
- }
236
-
237
- // Everything else is a selected_api
238
- if (!seenSelectedApi.has(key)) {
239
- seenSelectedApi.add(key);
240
- result.selectedApi.push(key); // Preserve full key including version
241
224
  }
242
225
  }
243
226
 
@@ -259,7 +242,11 @@ export function groupAppKeysByType(appKeys: string[]): {
259
242
  };
260
243
  }
261
244
 
262
- function isSnakeCasedSlug(slug: string): boolean {
245
+ export function isSlug(slug: string): boolean {
246
+ return !!slug.match(/^[a-z0-9]+(?:-[a-z0-9]+)*$/);
247
+ }
248
+
249
+ export function isSnakeCasedSlug(slug: string): boolean {
263
250
  // Allow leading underscore for slugs starting with a number.
264
251
  if (slug.match(/^_[0-9]/)) {
265
252
  slug = slug.slice(1);
@@ -268,7 +255,7 @@ function isSnakeCasedSlug(slug: string): boolean {
268
255
  return !!slug.match(/^[a-z0-9]+(?:_[a-z0-9]+)*$/);
269
256
  }
270
257
 
271
- function dashifySnakeCasedSlug(slug: string): string {
258
+ export function dashifySnakeCasedSlug(slug: string): string {
272
259
  // Only dashify if it's a valid snake_cased slug.
273
260
  if (!isSnakeCasedSlug(slug)) {
274
261
  return slug;
@@ -281,3 +268,49 @@ function dashifySnakeCasedSlug(slug: string): string {
281
268
 
282
269
  return slug.replace(/_/g, "-");
283
270
  }
271
+
272
+ export interface AppLocator {
273
+ lookupAppKey: string;
274
+ slug?: string;
275
+ implementationName?: string;
276
+ version?: string;
277
+ }
278
+ export interface ResolvedAppLocator extends AppLocator {
279
+ implementationName: string;
280
+ }
281
+
282
+ export function isUuid(appKey: string): boolean {
283
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
284
+ appKey,
285
+ );
286
+ }
287
+
288
+ export function toAppLocator(appKey: string): AppLocator {
289
+ const [appKeyWithoutVersion, version] = splitVersionedKey(appKey);
290
+ if (isUuid(appKeyWithoutVersion)) {
291
+ throw new Error(
292
+ `UUID app keys are not supported. Use app slug or implementation ID instead of: ${appKey}`,
293
+ );
294
+ }
295
+ const slug = isSlug(appKeyWithoutVersion)
296
+ ? appKeyWithoutVersion
297
+ : isSnakeCasedSlug(appKeyWithoutVersion)
298
+ ? dashifySnakeCasedSlug(appKeyWithoutVersion)
299
+ : undefined;
300
+ return {
301
+ lookupAppKey: appKeyWithoutVersion,
302
+ slug,
303
+ implementationName: slug ? undefined : appKeyWithoutVersion,
304
+ version,
305
+ };
306
+ }
307
+
308
+ export function isResolvedAppLocator(
309
+ appLocator: AppLocator,
310
+ ): appLocator is ResolvedAppLocator {
311
+ return !!appLocator.implementationName;
312
+ }
313
+
314
+ export function toImplementationId(appLocator: ResolvedAppLocator): string {
315
+ return `${appLocator.implementationName}@${appLocator.version || "latest"}`;
316
+ }
@@ -0,0 +1,73 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { resolve, writeFile, readFile } from "./file-utils";
3
+
4
+ // Mock the dynamic imports to simulate missing modules
5
+ vi.mock("fs/promises", () => {
6
+ throw new Error("Module not found");
7
+ });
8
+
9
+ vi.mock("path", () => {
10
+ throw new Error("Module not found");
11
+ });
12
+
13
+ describe("file-utils", () => {
14
+ describe("resolve", () => {
15
+ it("should handle absolute paths", async () => {
16
+ expect(await resolve("/absolute/path")).toBe("/absolute/path");
17
+ });
18
+
19
+ it("should handle relative paths with ./", async () => {
20
+ expect(await resolve("./relative/path")).toBe("/relative/path");
21
+ expect(await resolve("./relative/path", "/base")).toBe(
22
+ "/base/relative/path",
23
+ );
24
+ expect(await resolve("./relative/path", "/base/")).toBe(
25
+ "/base/relative/path",
26
+ );
27
+ });
28
+
29
+ it("should handle parent directory paths with ../", async () => {
30
+ expect(await resolve("../parent/path")).toBe("/parent/path");
31
+ expect(await resolve("../../nested/parent/path")).toBe(
32
+ "/nested/parent/path",
33
+ );
34
+ expect(await resolve("../parent/path", "/base")).toBe(
35
+ "/base/parent/path",
36
+ );
37
+ });
38
+
39
+ it("should handle paths without prefix", async () => {
40
+ expect(await resolve("simple/path")).toBe("/simple/path");
41
+ expect(await resolve("simple/path", "/base")).toBe("/base/simple/path");
42
+ expect(await resolve("simple/path", "/base/")).toBe("/base/simple/path");
43
+ });
44
+ });
45
+
46
+ describe("writeFile and readFile with in-memory filesystem", () => {
47
+ it("should write and read files using in-memory filesystem when fs is not available", async () => {
48
+ const filePath = "/test/file.txt";
49
+ const content = "Hello, world!";
50
+
51
+ await writeFile(filePath, content);
52
+
53
+ const readContent = await readFile(filePath);
54
+ expect(readContent).toBe(content);
55
+ });
56
+
57
+ it("should throw error when reading non-existent file", async () => {
58
+ await expect(readFile("/non/existent/file.txt")).rejects.toThrow(
59
+ "File not found: /non/existent/file.txt",
60
+ );
61
+ });
62
+
63
+ it("should handle multiple files in in-memory filesystem", async () => {
64
+ await writeFile("/file1.txt", "content1");
65
+ await writeFile("/file2.txt", "content2");
66
+ await writeFile("/dir/file3.txt", "content3");
67
+
68
+ expect(await readFile("/file1.txt")).toBe("content1");
69
+ expect(await readFile("/file2.txt")).toBe("content2");
70
+ expect(await readFile("/dir/file3.txt")).toBe("content3");
71
+ });
72
+ });
73
+ });
@@ -0,0 +1,94 @@
1
+ // In-memory filesystem fallback for browser environments
2
+ const inMemoryFiles: { [path: string]: string } = {};
3
+
4
+ // Lazy-loaded modules
5
+ let fsPromises: any = null;
6
+ let pathModule: any = null;
7
+
8
+ async function loadFsPromises() {
9
+ if (fsPromises) return fsPromises;
10
+
11
+ try {
12
+ fsPromises = await import("fs/promises");
13
+ return fsPromises;
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ async function loadPathModule() {
20
+ if (pathModule) return pathModule;
21
+
22
+ try {
23
+ pathModule = await import("path");
24
+ return pathModule;
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ export async function resolve(
31
+ path: string,
32
+ basePath: string = "/",
33
+ ): Promise<string> {
34
+ const pathModule = await loadPathModule();
35
+
36
+ if (pathModule) {
37
+ return pathModule.resolve(path);
38
+ }
39
+
40
+ // Simple fallback path resolution assuming root "/" location
41
+ if (path.startsWith("/")) {
42
+ return path;
43
+ }
44
+
45
+ if (path.startsWith("./")) {
46
+ // Remove ./ prefix and join to base
47
+ const cleanPath = path.slice(2);
48
+ return basePath.endsWith("/")
49
+ ? basePath + cleanPath
50
+ : basePath + "/" + cleanPath;
51
+ }
52
+
53
+ if (path.startsWith("../")) {
54
+ // Remove ../ prefix since we're already at root
55
+ const cleanPath = path.replace(/^(\.\.\/)+/, "");
56
+ return basePath.endsWith("/")
57
+ ? basePath + cleanPath
58
+ : basePath + "/" + cleanPath;
59
+ }
60
+
61
+ // No slash prefix, join to base
62
+ return basePath.endsWith("/") ? basePath + path : basePath + "/" + path;
63
+ }
64
+
65
+ export async function writeFile(
66
+ filePath: string,
67
+ content: string,
68
+ ): Promise<void> {
69
+ const fs = await loadFsPromises();
70
+
71
+ if (fs) {
72
+ await fs.writeFile(filePath, content, "utf8");
73
+ return;
74
+ }
75
+
76
+ // Fallback to in-memory filesystem
77
+ inMemoryFiles[filePath] = content;
78
+ }
79
+
80
+ export async function readFile(filePath: string): Promise<string> {
81
+ const fs = await loadFsPromises();
82
+
83
+ if (fs) {
84
+ return await fs.readFile(filePath, "utf8");
85
+ }
86
+
87
+ // Fallback to in-memory filesystem
88
+ const content = inMemoryFiles[filePath];
89
+ if (content !== undefined) {
90
+ return content;
91
+ }
92
+
93
+ throw new Error(`File not found: ${filePath}`);
94
+ }