@skaile/workspaces 0.53.0 → 1.0.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 (137) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/{asset-feeds-OEGJ6NOF.js → asset-feeds-677S5NX7.js} +7 -7
  3. package/dist/{asset-feeds-OEGJ6NOF.js.map → asset-feeds-677S5NX7.js.map} +1 -1
  4. package/dist/asset-manager/index.js +5 -5
  5. package/dist/asset-manager/installer.js +4 -4
  6. package/dist/{chunk-VGT2R662.js → chunk-43JDPQJ4.js} +3 -3
  7. package/dist/{chunk-VGT2R662.js.map → chunk-43JDPQJ4.js.map} +1 -1
  8. package/dist/{chunk-EOINYN5T.js → chunk-DW5MIJK3.js} +3 -3
  9. package/dist/{chunk-EOINYN5T.js.map → chunk-DW5MIJK3.js.map} +1 -1
  10. package/dist/{chunk-VE2UIJ7Y.js → chunk-EXNTTJGP.js} +3 -43
  11. package/dist/chunk-EXNTTJGP.js.map +1 -0
  12. package/dist/{chunk-7ZWLZ5H3.js → chunk-FLRHP3UC.js} +3 -3
  13. package/dist/{chunk-7ZWLZ5H3.js.map → chunk-FLRHP3UC.js.map} +1 -1
  14. package/dist/{chunk-V4BGVOJC.js → chunk-K5JIAHDV.js} +6 -6
  15. package/dist/{chunk-V4BGVOJC.js.map → chunk-K5JIAHDV.js.map} +1 -1
  16. package/dist/{chunk-3IHBGDCG.js → chunk-KSISPZV3.js} +2 -2
  17. package/dist/{chunk-3IHBGDCG.js.map → chunk-KSISPZV3.js.map} +1 -1
  18. package/dist/{chunk-WD2NRI3T.js → chunk-PLQVAEM2.js} +2 -2
  19. package/dist/{chunk-WD2NRI3T.js.map → chunk-PLQVAEM2.js.map} +1 -1
  20. package/dist/{chunk-TCNYSHPM.js → chunk-SFVEHSCI.js} +4 -4
  21. package/dist/{chunk-TCNYSHPM.js.map → chunk-SFVEHSCI.js.map} +1 -1
  22. package/dist/{chunk-QLLFCQU5.js → chunk-WSQ5J7CE.js} +403 -1469
  23. package/dist/chunk-WSQ5J7CE.js.map +1 -0
  24. package/dist/{chunk-BXC5547R.js → chunk-XD7GYMAU.js} +3 -3
  25. package/dist/{chunk-BXC5547R.js.map → chunk-XD7GYMAU.js.map} +1 -1
  26. package/dist/cli/index.js +30 -32
  27. package/dist/cli/index.js.map +1 -1
  28. package/dist/connectors/config.js +4 -4
  29. package/dist/connectors/index.js +5 -7
  30. package/dist/connectors/src/config.d.ts +1 -74
  31. package/dist/connectors/src/config.d.ts.map +1 -1
  32. package/dist/connectors/src/connector-manager.d.ts +13 -137
  33. package/dist/connectors/src/connector-manager.d.ts.map +1 -1
  34. package/dist/connectors/src/connector-types.d.ts +3 -25
  35. package/dist/connectors/src/connector-types.d.ts.map +1 -1
  36. package/dist/connectors/src/fleet-utils.d.ts +2 -30
  37. package/dist/connectors/src/fleet-utils.d.ts.map +1 -1
  38. package/dist/connectors/src/fs-utils.d.ts +1 -2
  39. package/dist/connectors/src/fs-utils.d.ts.map +1 -1
  40. package/dist/connectors/src/index.d.ts +2 -12
  41. package/dist/connectors/src/index.d.ts.map +1 -1
  42. package/dist/connectors/src/watcher.d.ts +1 -5
  43. package/dist/connectors/src/watcher.d.ts.map +1 -1
  44. package/dist/core/index.js +3 -3
  45. package/dist/deploy/index.js +3 -3
  46. package/dist/{ensure-sources-GJOXWVWO.js → ensure-sources-OCMHK7LT.js} +5 -5
  47. package/dist/{ensure-sources-GJOXWVWO.js.map → ensure-sources-OCMHK7LT.js.map} +1 -1
  48. package/dist/factory-assets/connectors/deploy.js +5 -7
  49. package/dist/factory-assets/connectors/devserver.js +5 -7
  50. package/dist/factory-assets/connectors/flow/adapter.js +5 -7
  51. package/dist/factory-assets/connectors/flow/engine.js +3 -3
  52. package/dist/factory-assets/connectors/flow/run-flow.js +6 -8
  53. package/dist/factory-assets/connectors/flow.js +5 -7
  54. package/dist/factory-assets/connectors/git.js +5 -7
  55. package/dist/factory-assets/connectors/gmail.js +5 -7
  56. package/dist/factory-assets/connectors/googledrive/CONNECTOR.md +14 -128
  57. package/dist/factory-assets/connectors/googledrive/driver.d.ts +6 -13
  58. package/dist/factory-assets/connectors/googledrive/driver.d.ts.map +1 -1
  59. package/dist/factory-assets/connectors/googledrive.js +5 -7
  60. package/dist/factory-assets/connectors/local.js +5 -7
  61. package/dist/factory-assets/connectors/mattermost.js +5 -7
  62. package/dist/factory-assets/connectors/memory.js +5 -7
  63. package/dist/factory-assets/connectors/minio.js +5 -7
  64. package/dist/factory-assets/connectors/postgres.js +5 -7
  65. package/dist/factory-assets/connectors/s3.js +5 -7
  66. package/dist/factory-assets/connectors/sharepoint/CONNECTOR.md +13 -159
  67. package/dist/factory-assets/connectors/sharepoint/driver.d.ts +6 -12
  68. package/dist/factory-assets/connectors/sharepoint/driver.d.ts.map +1 -1
  69. package/dist/factory-assets/connectors/sharepoint.js +5 -7
  70. package/dist/factory-assets/connectors/sqlite.js +5 -7
  71. package/dist/factory-assets/connectors/static-server.js +5 -7
  72. package/dist/factory-assets/connectors/tunnel.js +5 -7
  73. package/dist/factory-assets/connectors/webdav/CONNECTOR.md +13 -118
  74. package/dist/factory-assets/connectors/webdav/driver.d.ts +6 -15
  75. package/dist/factory-assets/connectors/webdav/driver.d.ts.map +1 -1
  76. package/dist/factory-assets/connectors/webdav.js +5 -7
  77. package/dist/factory-assets/connectors/xstate-store.js +5 -7
  78. package/dist/factory-assets/connectors/xstate.js +5 -7
  79. package/dist/{flows-AGPIE6JM.js → flows-36PZESON.js} +4 -4
  80. package/dist/{flows-AGPIE6JM.js.map → flows-36PZESON.js.map} +1 -1
  81. package/dist/library/index.js +4 -4
  82. package/dist/library/install.js +3 -3
  83. package/dist/{open-library-ERKRGH4B.js → open-library-WRFYABFP.js} +7 -7
  84. package/dist/{open-library-ERKRGH4B.js.map → open-library-WRFYABFP.js.map} +1 -1
  85. package/dist/{paths-D6RUQ522.js → paths-EUNVMONC.js} +5 -5
  86. package/dist/{paths-D6RUQ522.js.map → paths-EUNVMONC.js.map} +1 -1
  87. package/dist/{plugin-store-SGNJTBYS.js → plugin-store-35KIMX6W.js} +5 -5
  88. package/dist/{plugin-store-SGNJTBYS.js.map → plugin-store-35KIMX6W.js.map} +1 -1
  89. package/dist/runner/index.js +7 -9
  90. package/dist/sdk/asset-manager.js +5 -5
  91. package/dist/sdk/core.js +3 -3
  92. package/dist/sdk/flow.js +3 -3
  93. package/dist/sdk/index.js +7 -9
  94. package/dist/sdk/index.js.map +1 -1
  95. package/dist/sdk/runner.js +7 -9
  96. package/dist/{setup-IV2JFW3T.js → setup-HM6HATAC.js} +5 -5
  97. package/dist/{setup-IV2JFW3T.js.map → setup-HM6HATAC.js.map} +1 -1
  98. package/dist/{source-FGOMQMZT.js → source-EZ5Y3CKP.js} +8 -8
  99. package/dist/{source-FGOMQMZT.js.map → source-EZ5Y3CKP.js.map} +1 -1
  100. package/dist/{store-client-GJT2XQ5Z.js → store-client-BUUF7BPK.js} +5 -5
  101. package/dist/{store-client-GJT2XQ5Z.js.map → store-client-BUUF7BPK.js.map} +1 -1
  102. package/dist/tui/index.js +7 -9
  103. package/dist/tui/index.js.map +1 -1
  104. package/dist/workspace-plugin/adapters/mcp.js +3 -3
  105. package/dist/workspace-plugin/adapters/omp.js +3 -3
  106. package/dist/workspace-plugin/index.js +1 -1
  107. package/package.json +1 -13
  108. package/dist/chunk-OVRSNIKJ.js +0 -72
  109. package/dist/chunk-OVRSNIKJ.js.map +0 -1
  110. package/dist/chunk-QLLFCQU5.js.map +0 -1
  111. package/dist/chunk-RAIRWFAN.js +0 -843
  112. package/dist/chunk-RAIRWFAN.js.map +0 -1
  113. package/dist/chunk-VE2UIJ7Y.js.map +0 -1
  114. package/dist/connectors/rclone-config.d.ts +0 -2
  115. package/dist/connectors/rclone-config.js +0 -4
  116. package/dist/connectors/rclone-config.js.map +0 -1
  117. package/dist/connectors/rclone.d.ts +0 -2
  118. package/dist/connectors/rclone.js +0 -5
  119. package/dist/connectors/rclone.js.map +0 -1
  120. package/dist/connectors/src/fleet-health-gate.d.ts +0 -105
  121. package/dist/connectors/src/fleet-health-gate.d.ts.map +0 -1
  122. package/dist/connectors/src/mount-placeholder.d.ts +0 -131
  123. package/dist/connectors/src/mount-placeholder.d.ts.map +0 -1
  124. package/dist/connectors/src/rclone-config/googledrive.d.ts +0 -59
  125. package/dist/connectors/src/rclone-config/googledrive.d.ts.map +0 -1
  126. package/dist/connectors/src/rclone-config/index.d.ts +0 -9
  127. package/dist/connectors/src/rclone-config/index.d.ts.map +0 -1
  128. package/dist/connectors/src/rclone-config/onedrive.d.ts +0 -48
  129. package/dist/connectors/src/rclone-config/onedrive.d.ts.map +0 -1
  130. package/dist/connectors/src/rclone-config/sanitize.d.ts +0 -9
  131. package/dist/connectors/src/rclone-config/sanitize.d.ts.map +0 -1
  132. package/dist/connectors/src/rclone-config/webdav.d.ts +0 -50
  133. package/dist/connectors/src/rclone-config/webdav.d.ts.map +0 -1
  134. package/dist/connectors/src/rclone-process-manager.d.ts +0 -316
  135. package/dist/connectors/src/rclone-process-manager.d.ts.map +0 -1
  136. package/dist/connectors/src/rclone-token-refresh.d.ts +0 -84
  137. package/dist/connectors/src/rclone-token-refresh.d.ts.map +0 -1
@@ -1,843 +0,0 @@
1
- import { createLogger } from './chunk-JB35MVBN.js';
2
- import { spawn } from 'child_process';
3
- import { randomBytes } from 'crypto';
4
- import { mkdir, chmod, writeFile, unlink, rename, cp, rm, stat, readdir, readFile } from 'fs/promises';
5
- import { tmpdir } from 'os';
6
- import path, { relative, join } from 'path';
7
- import { createConnection } from 'net';
8
- import { existsSync, readFileSync } from 'fs';
9
- import chokidar from 'chokidar';
10
-
11
- async function ensureDirMode(dir, log) {
12
- await mkdir(dir, { recursive: true });
13
- try {
14
- await chmod(dir, 504);
15
- } catch (err) {
16
- log?.debug("chmod 0770 skipped (EPERM \u2014 entrypoint heal expected)", {
17
- path: dir,
18
- error: err instanceof Error ? err.message : String(err)
19
- });
20
- }
21
- }
22
- var PortPool = class _PortPool {
23
- min;
24
- max;
25
- inUse = /* @__PURE__ */ new Set();
26
- constructor(range) {
27
- this.min = range[0];
28
- this.max = range[1];
29
- }
30
- /** Allocate the next free port from the range. Checks OS for collisions. */
31
- async allocate() {
32
- for (let port = this.min; port <= this.max; port++) {
33
- if (this.inUse.has(port)) continue;
34
- if (!await _PortPool.isPortFree(port)) {
35
- this.inUse.add(port);
36
- continue;
37
- }
38
- this.inUse.add(port);
39
- return port;
40
- }
41
- throw new Error(`No free port in range ${this.min}-${this.max}`);
42
- }
43
- /** Mark a port as in-use (for explicit port args or registered instances). */
44
- reserve(port) {
45
- this.inUse.add(port);
46
- }
47
- /** Return a port to the pool. */
48
- release(port) {
49
- this.inUse.delete(port);
50
- }
51
- /** Check if a port is currently tracked as in-use. */
52
- isInUse(port) {
53
- return this.inUse.has(port);
54
- }
55
- /** Quick TCP connect test — returns true if nothing is listening. */
56
- static isPortFree(port, host = "127.0.0.1") {
57
- return new Promise((resolve) => {
58
- const socket = createConnection({ port, host });
59
- socket.once("connect", () => {
60
- socket.destroy();
61
- resolve(false);
62
- });
63
- socket.once("error", () => {
64
- socket.destroy();
65
- resolve(true);
66
- });
67
- socket.setTimeout(500, () => {
68
- socket.destroy();
69
- resolve(true);
70
- });
71
- });
72
- }
73
- };
74
- function globToMatcher(pattern) {
75
- const segmentMatch = pattern.match(/^\*\*\/([^*/?]+)\/\*\*$/);
76
- if (segmentMatch) {
77
- const name = segmentMatch[1];
78
- return (p) => p.includes(`/${name}/`) || p.endsWith(`/${name}`) || p === name;
79
- }
80
- const dotSegment = pattern.match(/^\*\*\/(\.[\w#]+)(?:\/\*\*)?$/);
81
- if (dotSegment) {
82
- const name = dotSegment[1];
83
- return (p) => p.includes(`/${name}/`) || p.endsWith(`/${name}`) || p === name;
84
- }
85
- const extMatch = pattern.match(/^\*\*\/\*(\.\w+(?:\.\*)?)/);
86
- if (extMatch) {
87
- const suffix = extMatch[1];
88
- if (suffix.endsWith(".*")) {
89
- const base = suffix.slice(0, -2);
90
- return (p) => p.includes(base);
91
- }
92
- return (p) => p.endsWith(suffix);
93
- }
94
- const suffixMatch = pattern.match(/^\*\*\/\*(.+)$/);
95
- if (suffixMatch) {
96
- const sfx = suffixMatch[1];
97
- return (p) => p.endsWith(sfx);
98
- }
99
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/\{\{GLOBSTAR\}\}/g, ".*");
100
- const re = new RegExp(`^${escaped}$`);
101
- return (p) => re.test(p);
102
- }
103
- var DEFAULT_IGNORED = [
104
- (p) => p.includes("/.git/") || p.endsWith("/.git"),
105
- (p) => p.includes("/node_modules/") || p.endsWith("/node_modules"),
106
- (p) => p.includes("/.connectors/") || p.endsWith("/.connectors"),
107
- // Atomic-write tempfiles (Bun.write creates `<name>.tmp.<pid>.<ts>` then renames).
108
- // These vanish between readdir and fs.watch, causing EINVAL that crashes the process.
109
- (p) => /\.tmp\.\d/.test(p),
110
- // Editor swap/backup files that also race with save operations.
111
- (p) => p.endsWith(".swp"),
112
- (p) => p.endsWith(".swx"),
113
- (p) => p.includes("/.#"),
114
- (p) => p.endsWith("~")
115
- ];
116
- var DEFAULT_DEBOUNCE_MS = 150;
117
- function parseGitignore(rootDir) {
118
- const gitignorePath = join(rootDir, ".gitignore");
119
- if (!existsSync(gitignorePath)) return [];
120
- try {
121
- const content = readFileSync(gitignorePath, "utf-8");
122
- const matchers = [];
123
- for (const raw of content.split("\n")) {
124
- const line = raw.trim();
125
- if (!line || line.startsWith("#") || line.startsWith("!")) continue;
126
- let pattern = line;
127
- if (pattern.startsWith("/")) {
128
- pattern = pattern.slice(1);
129
- } else if (!pattern.includes("/")) {
130
- pattern = `**/${pattern}`;
131
- }
132
- if (pattern.endsWith("/")) {
133
- matchers.push(globToMatcher(`${pattern}**`));
134
- } else {
135
- matchers.push(globToMatcher(pattern));
136
- if (!pattern.includes("*") && !pattern.includes(".")) {
137
- matchers.push(globToMatcher(`${pattern}/**`));
138
- }
139
- }
140
- }
141
- return matchers;
142
- } catch {
143
- return [];
144
- }
145
- }
146
- var CHOKIDAR_EVENT_MAP = {
147
- add: "create",
148
- change: "edit",
149
- unlink: "delete",
150
- addDir: "create",
151
- unlinkDir: "delete"
152
- };
153
- async function createFsWatcher(rootDir, callback, options) {
154
- const log = createLogger({ kind: "connector", subkind: "watcher", instance: rootDir });
155
- const gitignorePatterns = parseGitignore(rootDir);
156
- const optionsIgnored = (options?.ignored ?? []).map(
157
- (entry) => typeof entry === "string" ? globToMatcher(entry) : entry
158
- );
159
- const ignored = [...DEFAULT_IGNORED, ...gitignorePatterns, ...optionsIgnored];
160
- const debounceMs = options?.debounceMs ?? DEFAULT_DEBOUNCE_MS;
161
- const watcher = chokidar.watch(rootDir, {
162
- ignoreInitial: true,
163
- ignored,
164
- awaitWriteFinish: { stabilityThreshold: debounceMs, pollInterval: 50 },
165
- usePolling: false
166
- });
167
- watcher.on("error", (err) => {
168
- const message = err instanceof Error ? err.message : String(err);
169
- log.warn("watcher error (non-fatal)", { message });
170
- });
171
- for (const [chokidarEvent, action] of Object.entries(CHOKIDAR_EVENT_MAP)) {
172
- watcher.on(
173
- chokidarEvent,
174
- (fullPath) => {
175
- const rel = relative(rootDir, fullPath);
176
- if (!rel || rel.startsWith("..")) return;
177
- const normalizedPath = rel.split("\\").join("/");
178
- callback({ path: normalizedPath, action, source: "filesystem" });
179
- }
180
- );
181
- }
182
- return {
183
- close: () => watcher.close()
184
- };
185
- }
186
-
187
- // connectors/src/rclone-process-manager.ts
188
- var RCLONE_RC_PORTS = new PortPool([55e3, 56e3]);
189
- var DEFAULT_VFS_WRITE_BACK = "30s";
190
- var DEFAULT_UPLOAD_STUCK_THRESHOLD = 3;
191
- var STABILIZE_THRESHOLD_MS = 1500;
192
- var STABILIZE_TIMEOUT_MS = 3e4;
193
- var STABILIZE_POLL_MS = DEFAULT_DEBOUNCE_MS;
194
- var HANDLES = /* @__PURE__ */ new Map();
195
- var RcloneProcessManager = class {
196
- /**
197
- * @param now Injectable clock (ms) used only to timestamp the quarantine dir
198
- * on a poisoned-cache reset, so the move-aside name is deterministic in tests.
199
- */
200
- constructor(now = Date.now) {
201
- this.now = now;
202
- }
203
- now;
204
- /**
205
- * Spawn an rclone FUSE mount with one-shot poisoned-cache recovery. Delegates
206
- * to {@link spawnOnce}; if that fails specifically because the persistent VFS
207
- * cache was poisoned (a prior container SIGKILLed mid-write left `vfs`/`vfsMeta`
208
- * referencing missing backing files), quarantine the cache and retry the mount
209
- * exactly once on a clean rebuild from the authoritative remote.
210
- *
211
- * The retry is single-shot and bounded: if the fresh-cache mount also fails the
212
- * cause is connectivity/token (not the cache), so the error propagates without
213
- * a second reset — never a reset/respawn loop. A non-poison failure, or a
214
- * poison failure with nothing to quarantine, propagates immediately.
215
- */
216
- async spawn(config) {
217
- try {
218
- return await this.spawnOnce(config);
219
- } catch (err) {
220
- if (!(err instanceof CachePoisonedMountError)) throw err;
221
- let quarantined;
222
- try {
223
- quarantined = await quarantineCacheDir(config.cacheDir, this.now());
224
- } catch (qErr) {
225
- config.log.warn("rclone vfs-cache quarantine failed \u2014 surfacing original mount error", {
226
- cacheDir: config.cacheDir,
227
- error: qErr instanceof Error ? qErr.message : String(qErr)
228
- });
229
- throw err;
230
- }
231
- if (!quarantined) {
232
- throw err;
233
- }
234
- config.log.warn(
235
- "rclone mount failed on a poisoned vfs cache \u2014 quarantined it and retrying the mount once",
236
- { cacheDir: config.cacheDir, quarantined }
237
- );
238
- return await this.spawnOnce(config);
239
- }
240
- }
241
- /**
242
- * Spawn an rclone FUSE mount. Resolves once the mountpoint is confirmed
243
- * present in `/proc/self/mounts`. Throws on timeout, on early exit, or
244
- * if invariants are violated. A readiness-timeout accompanied by the
245
- * poisoned-cache log signature throws {@link CachePoisonedMountError} so the
246
- * public {@link spawn} can apply its quarantine-and-retry recovery.
247
- */
248
- async spawnOnce(config) {
249
- if (!path.isAbsolute(config.mountPoint)) {
250
- throw new Error(`mountPoint must be absolute: ${config.mountPoint}`);
251
- }
252
- if (!path.isAbsolute(config.cacheDir)) {
253
- throw new Error(`cacheDir must be absolute: ${config.cacheDir}`);
254
- }
255
- await ensureDirMode(config.mountPoint, config.log);
256
- await ensureDirMode(config.cacheDir, config.log);
257
- const configPath = path.join(
258
- tmpdir(),
259
- `rclone-${config.mountId}-${randomBytes(6).toString("hex")}.conf`
260
- );
261
- await writeFile(configPath, config.rcloneConfig, { mode: 384 });
262
- const port = await RCLONE_RC_PORTS.allocate();
263
- const rcAddr = `127.0.0.1:${port}`;
264
- const rcloneLog = config.log.child({ subkind: `${config.driverName}+rclone` });
265
- config.log.info("rclone spawn starting", {
266
- mountPoint: config.mountPoint,
267
- remote: config.remote,
268
- vfsCacheMode: config.vfsCacheMode
269
- });
270
- const args = _buildRcloneArgs(config, { configPath, rcAddr });
271
- const proc = spawn("rclone", args, { stdio: ["ignore", "pipe", "pipe"] });
272
- const routeCtx = {
273
- stuckThreshold: config.uploadStuckThreshold ?? DEFAULT_UPLOAD_STUCK_THRESHOLD,
274
- alertedObjects: /* @__PURE__ */ new Set(),
275
- itemNotFoundCounts: /* @__PURE__ */ new Map(),
276
- onUploadStuck: (object, tries) => {
277
- rcloneLog.error(
278
- "rclone upload repeatedly failing \u2014 file may be stranded in this session's cache",
279
- void 0,
280
- { object, tries, event: "sync_status", phase: "error" }
281
- );
282
- config.onUploadStuck?.(object, tries);
283
- },
284
- onAuthError: config.onAuthError
285
- };
286
- const recentStderr = [];
287
- const stderrTail = (line) => {
288
- recentStderr.push(line);
289
- if (recentStderr.length > 20) recentStderr.shift();
290
- };
291
- let cachePoisonObserved = false;
292
- const noteCachePoison = (line) => {
293
- if (!cachePoisonObserved && _isCachePoisonLine(line)) cachePoisonObserved = true;
294
- };
295
- const stdoutHandler = (line) => {
296
- noteCachePoison(line);
297
- _routeRcloneLogLine(line, rcloneLog, routeCtx);
298
- };
299
- const stderrHandler = (line) => {
300
- stderrTail(line);
301
- noteCachePoison(line);
302
- _routeRcloneLogLine(line, rcloneLog, routeCtx);
303
- };
304
- if (proc.stdout) attachLineSplitter(proc.stdout, stdoutHandler);
305
- if (proc.stderr) attachLineSplitter(proc.stderr, stderrHandler);
306
- let earlyExitCode = null;
307
- let earlyExitSignal = null;
308
- const earlyExit = new Promise((resolve) => {
309
- proc.once("exit", (code, signal) => {
310
- earlyExitCode = code;
311
- earlyExitSignal = signal;
312
- resolve();
313
- });
314
- });
315
- const mountReady = waitFor(
316
- async () => {
317
- if (earlyExitCode !== null || earlyExitSignal !== null) return false;
318
- return await isMountpoint(config.mountPoint);
319
- },
320
- { timeoutMs: 15e3, intervalMs: 100 }
321
- );
322
- const ready = await Promise.race([mountReady, earlyExit.then(() => false)]);
323
- if (!ready) {
324
- try {
325
- if (proc.exitCode === null && proc.signalCode === null) {
326
- proc.kill("SIGKILL");
327
- }
328
- } catch {
329
- }
330
- await unlink(configPath).catch(() => {
331
- });
332
- RCLONE_RC_PORTS.release(port);
333
- const tail = recentStderr.length > 0 ? `
334
- stderr tail:
335
- ${recentStderr.slice(-5).join("\n")}` : "";
336
- const message = `rclone failed to mount ${config.remote} at ${config.mountPoint}${tail}`;
337
- if (cachePoisonObserved) {
338
- throw new CachePoisonedMountError(message);
339
- }
340
- throw new Error(message);
341
- }
342
- const pid = proc.pid;
343
- if (pid === void 0) {
344
- proc.kill("SIGKILL");
345
- await unlink(configPath).catch(() => {
346
- });
347
- RCLONE_RC_PORTS.release(port);
348
- throw new Error("rclone process has no pid after successful mount");
349
- }
350
- const entry = {
351
- proc,
352
- configPath,
353
- port,
354
- mountPoint: config.mountPoint,
355
- cacheDir: config.cacheDir,
356
- spawnConfig: config,
357
- log: config.log,
358
- rcloneLog,
359
- recentStderr
360
- };
361
- HANDLES.set(pid, entry);
362
- proc.on("exit", (code) => {
363
- if (HANDLES.has(pid)) {
364
- config.log.warn("rclone exited unexpectedly", { code, pid });
365
- }
366
- });
367
- config.log.info("rclone mountpoint confirmed", { pid });
368
- return { pid, mountPoint: config.mountPoint, cacheDir: config.cacheDir, rcAddr };
369
- }
370
- /**
371
- * Stop an rclone subprocess. Attempts a flush first, then SIGTERM, then
372
- * SIGKILL + fusermount3 if `graceMs` elapses without the mountpoint
373
- * disappearing.
374
- *
375
- * `graceMs` bounds both the flush stabilization gate and the SIGTERM→SIGKILL
376
- * window — it is *not* a single total wall-clock cap: the flush also waits on
377
- * rclone's `/vfs/stats` drain (its own 60s ceiling) between the two.
378
- */
379
- async stop(handle, opts) {
380
- const graceMs = opts?.graceMs ?? 3e4;
381
- const entry = HANDLES.get(handle.pid);
382
- if (!entry) {
383
- return;
384
- }
385
- try {
386
- await this.flush(handle, { stabilizeTimeoutMs: graceMs });
387
- } catch (err) {
388
- entry.log.warn("rclone flush before stop failed", {
389
- pid: handle.pid,
390
- error: err instanceof Error ? err.message : String(err)
391
- });
392
- }
393
- entry.log.info("rclone stop initiated", { pid: handle.pid });
394
- HANDLES.delete(handle.pid);
395
- try {
396
- entry.proc.kill("SIGTERM");
397
- } catch {
398
- }
399
- const unmounted = await waitFor(async () => !await isMountpoint(entry.mountPoint), {
400
- timeoutMs: graceMs,
401
- intervalMs: 200
402
- });
403
- if (!unmounted) {
404
- try {
405
- entry.proc.kill("SIGKILL");
406
- } catch {
407
- }
408
- await new Promise((resolve) => {
409
- const fuse = spawn("fusermount3", ["-u", entry.mountPoint], { stdio: "ignore" });
410
- fuse.once("exit", () => resolve());
411
- fuse.once("error", () => resolve());
412
- });
413
- entry.log.warn("rclone hard-killed", { pid: handle.pid });
414
- }
415
- RCLONE_RC_PORTS.release(entry.port);
416
- await unlink(entry.configPath).catch(() => {
417
- });
418
- }
419
- /**
420
- * Apply a freshly-rendered rclone INI to a running mount by recycling the
421
- * process: flush + stop the current rclone, then re-spawn at the same
422
- * mountpoint/cacheDir with the new config. Returns the new handle.
423
- *
424
- * rclone holds the OAuth token in memory (baked into the config at spawn);
425
- * there is no rc command to swap it on a live `onedrive` backend, so a
426
- * recycle is the reliable way to apply a refreshed token. The vfs cache dir
427
- * is reused, so the remount revalidates rather than re-downloads. During the
428
- * brief unmount window the read-only mount-placeholder barrier underneath is
429
- * exposed (writes fail with EROFS, not EIO) — see ConnectorManager's
430
- * read-only-until-ready guarantee.
431
- */
432
- async reconfigure(handle, rcloneConfig) {
433
- const entry = HANDLES.get(handle.pid);
434
- if (!entry) {
435
- throw new Error(`rclone handle not found (pid=${handle.pid})`);
436
- }
437
- const nextConfig = { ...entry.spawnConfig, rcloneConfig };
438
- entry.log.info("rclone reconfigure \u2014 recycling process to apply refreshed credential", {
439
- pid: handle.pid
440
- });
441
- await this.stop(handle, { graceMs: 3e4 });
442
- return this.spawn(nextConfig);
443
- }
444
- /**
445
- * Force rclone to upload pending writes, then wait until the in-use
446
- * counter reaches zero.
447
- *
448
- * Before issuing the refresh it waits for the vfs cache dir to go quiescent
449
- * (no write for `stabilityThresholdMs`, bounded by `stabilizeTimeoutMs`) so a
450
- * close/hibernate-triggered flush doesn't snapshot a half-written file and
451
- * trigger rclone's mid-write "sizes differ" abort. Pass `stabilize: false`
452
- * to skip the gate.
453
- */
454
- async flush(handle, opts) {
455
- const entry = HANDLES.get(handle.pid);
456
- if (!entry) {
457
- throw new Error(`rclone handle not found (pid=${handle.pid})`);
458
- }
459
- if (opts?.stabilize !== false) {
460
- const thresholdMs = opts?.stabilityThresholdMs ?? STABILIZE_THRESHOLD_MS;
461
- const timeoutMs = opts?.stabilizeTimeoutMs ?? STABILIZE_TIMEOUT_MS;
462
- const scanDir = await vfsContentDir(entry.cacheDir);
463
- const quiescent = await awaitQuiescent(scanDir, {
464
- thresholdMs,
465
- timeoutMs,
466
- pollMs: STABILIZE_POLL_MS
467
- });
468
- if (!quiescent) {
469
- entry.log.warn("rclone cache did not stabilize before flush \u2014 proceeding anyway", {
470
- pid: handle.pid,
471
- thresholdMs,
472
- timeoutMs
473
- });
474
- }
475
- }
476
- const refreshRes = await fetch(`http://${handle.rcAddr}/vfs/refresh?recursive=true`, {
477
- method: "POST"
478
- });
479
- if (!refreshRes.ok) {
480
- const body = await refreshRes.text().catch(() => "");
481
- throw new Error(`rclone /vfs/refresh failed: ${refreshRes.status} ${body}`);
482
- }
483
- const ok = await waitFor(
484
- async () => {
485
- const r = await fetch(`http://${handle.rcAddr}/vfs/stats`, { method: "GET" });
486
- if (!r.ok) return false;
487
- const body = await r.json();
488
- return body.inUse === 0;
489
- },
490
- { timeoutMs: 6e4, intervalMs: 500 }
491
- );
492
- if (!ok) {
493
- throw new Error(`rclone flush did not drain within 60s (pid=${handle.pid})`);
494
- }
495
- }
496
- /**
497
- * Authoritative remote byte size for a mount-relative path, via rclone's
498
- * `operations/stat` rc endpoint. Backs the mount read-path stale-tail gate:
499
- * `operations/stat` hits the backend object directly, so it returns the true
500
- * remote size even when the VFS cache is serving a non-truncated item (and
501
- * `statSync` on the FUSE mount reports the inflated length). Returns `null`
502
- * when the handle is unknown, the object is missing / a directory, or the
503
- * call fails — the caller then trusts the on-disk bytes.
504
- */
505
- async statSize(handle, relPath) {
506
- const entry = HANDLES.get(handle.pid);
507
- if (!entry) return null;
508
- return fetchAuthoritativeSize(handle.rcAddr, entry.spawnConfig.remote, relPath);
509
- }
510
- /** Liveness + cache-dir size. */
511
- async status(handle) {
512
- const entry = HANDLES.get(handle.pid);
513
- if (!entry) {
514
- return { alive: false, cacheBytes: 0 };
515
- }
516
- const alive = entry.proc.exitCode === null && entry.proc.signalCode === null;
517
- const cacheBytes = await dirSize(entry.cacheDir);
518
- return { alive, cacheBytes };
519
- }
520
- };
521
- var UPLOAD_FAIL_RE = /failed to upload.*?try #(\d+)/i;
522
- var UPLOAD_OK_RE = /upload succeeded/i;
523
- var AUTH_FAIL_RE = /invalid_client|couldn'?t fetch token|invalid_grant|InvalidAuthenticationToken/i;
524
- var ITEM_NOT_FOUND_RE = /itemNotFound/i;
525
- var CACHE_POISON_RE = /failed to reload item|failed to add virtual dir entry/i;
526
- var CachePoisonedMountError = class extends Error {
527
- constructor(message) {
528
- super(message);
529
- this.name = "CachePoisonedMountError";
530
- }
531
- };
532
- function _isCachePoisonLine(line) {
533
- return CACHE_POISON_RE.test(line);
534
- }
535
- async function quarantineCacheDir(cacheDir, nowMs) {
536
- const stamp = new Date(nowMs).toISOString().replace(/[:.]/g, "-");
537
- const quarantineDir = path.join(cacheDir, `.corrupt-${stamp}`);
538
- await mkdir(quarantineDir, { recursive: true });
539
- let moved = 0;
540
- for (const sub of ["vfs", "vfsMeta"]) {
541
- const src = path.join(cacheDir, sub);
542
- const dst = path.join(quarantineDir, sub);
543
- try {
544
- await rename(src, dst);
545
- } catch (err) {
546
- const code = err?.code;
547
- if (code === "ENOENT") continue;
548
- if (code === "EXDEV") {
549
- try {
550
- await cp(src, dst, { recursive: true });
551
- } catch (cpErr) {
552
- await rm(dst, { recursive: true, force: true }).catch(() => void 0);
553
- throw cpErr;
554
- }
555
- await rm(src, { recursive: true, force: true });
556
- } else {
557
- throw err;
558
- }
559
- }
560
- moved += 1;
561
- }
562
- if (moved === 0) {
563
- await rm(quarantineDir, { recursive: true, force: true }).catch(() => void 0);
564
- return null;
565
- }
566
- return quarantineDir;
567
- }
568
- function _routeRcloneLogLine(line, log, ctx) {
569
- const trimmed = line.trim();
570
- if (trimmed.length === 0) return;
571
- let parsed = null;
572
- try {
573
- const candidate = JSON.parse(trimmed);
574
- if (candidate && typeof candidate === "object") parsed = candidate;
575
- } catch {
576
- parsed = null;
577
- }
578
- if (!parsed) {
579
- if (ctx) {
580
- _trackUploadFailure(trimmed, void 0, ctx);
581
- if (AUTH_FAIL_RE.test(trimmed)) ctx.onAuthError?.();
582
- if (ITEM_NOT_FOUND_RE.test(trimmed)) _trackItemNotFound(void 0, trimmed, ctx);
583
- }
584
- log.info(trimmed);
585
- return;
586
- }
587
- const level = (parsed.level ?? "INFO").toUpperCase();
588
- const msg = parsed.msg ?? "";
589
- const data = {};
590
- if (parsed.object !== void 0) data.object = parsed.object;
591
- if (parsed.source !== void 0) data.source = parsed.source;
592
- if (ctx) {
593
- _trackUploadFailure(msg, parsed.object, ctx);
594
- const errText = typeof parsed.error === "string" ? parsed.error : "";
595
- if (AUTH_FAIL_RE.test(msg) || errText && AUTH_FAIL_RE.test(errText)) ctx.onAuthError?.();
596
- if (ITEM_NOT_FOUND_RE.test(msg) || errText && ITEM_NOT_FOUND_RE.test(errText)) {
597
- _trackItemNotFound(parsed.object, msg, ctx);
598
- }
599
- }
600
- switch (level) {
601
- case "DEBUG":
602
- log.debug(msg, data);
603
- break;
604
- case "INFO":
605
- case "NOTICE":
606
- log.info(msg, data);
607
- break;
608
- case "WARNING":
609
- log.warn(msg, data);
610
- break;
611
- case "ERROR":
612
- case "CRITICAL":
613
- case "EMERGENCY":
614
- case "ALERT":
615
- log.error(msg, parsed.error, data);
616
- break;
617
- default:
618
- log.info(msg, data);
619
- break;
620
- }
621
- }
622
- function _trackUploadFailure(msg, object, ctx) {
623
- const failMatch = UPLOAD_FAIL_RE.exec(msg);
624
- if (failMatch) {
625
- const tries = Number(failMatch[1]);
626
- const key = object ?? extractObjectFromMsg(msg) ?? "(unknown)";
627
- const threshold = ctx.stuckThreshold ?? DEFAULT_UPLOAD_STUCK_THRESHOLD;
628
- if (!ctx.alertedObjects) ctx.alertedObjects = /* @__PURE__ */ new Set();
629
- const alerted = ctx.alertedObjects;
630
- if (tries >= threshold && !alerted.has(key)) {
631
- alerted.add(key);
632
- ctx.onUploadStuck?.(key, tries);
633
- }
634
- return;
635
- }
636
- if (UPLOAD_OK_RE.test(msg)) {
637
- const key = object ?? extractObjectFromMsg(msg);
638
- if (key) {
639
- ctx.alertedObjects?.delete(key);
640
- ctx.itemNotFoundCounts?.delete(key);
641
- }
642
- }
643
- }
644
- function _trackItemNotFound(object, msg, ctx) {
645
- const key = object ?? extractObjectFromMsg(msg) ?? "(unknown)";
646
- if (!ctx.itemNotFoundCounts) ctx.itemNotFoundCounts = /* @__PURE__ */ new Map();
647
- if (!ctx.alertedObjects) ctx.alertedObjects = /* @__PURE__ */ new Set();
648
- const count = (ctx.itemNotFoundCounts.get(key) ?? 0) + 1;
649
- ctx.itemNotFoundCounts.set(key, count);
650
- const threshold = ctx.stuckThreshold ?? DEFAULT_UPLOAD_STUCK_THRESHOLD;
651
- if (count >= threshold && !ctx.alertedObjects.has(key)) {
652
- ctx.alertedObjects.add(key);
653
- ctx.onUploadStuck?.(key, count);
654
- }
655
- }
656
- function extractObjectFromMsg(msg) {
657
- const m = /(?:^|\s)([^\s:]+\/[^\s:]+):/.exec(msg);
658
- return m?.[1];
659
- }
660
- function _buildRcloneArgs(config, io) {
661
- const args = [
662
- "mount",
663
- config.remote,
664
- config.mountPoint,
665
- "--config",
666
- io.configPath,
667
- "--cache-dir",
668
- config.cacheDir,
669
- "--vfs-cache-mode",
670
- config.vfsCacheMode,
671
- "--vfs-cache-max-size",
672
- config.cacheMaxSize ?? "5G",
673
- "--vfs-cache-max-age",
674
- config.cacheMaxAge ?? "168h",
675
- "--vfs-write-back",
676
- config.vfsWriteBack ?? DEFAULT_VFS_WRITE_BACK,
677
- "--vfs-cache-poll-interval",
678
- "60s",
679
- "--rc",
680
- "--rc-addr",
681
- io.rcAddr,
682
- // loopback-only rc server — without no-auth, flush's POST /vfs/refresh 403s,
683
- // graceful stop fails, and the write-back cache is left dirty. Safe: nothing
684
- // external can reach 127.0.0.1:<port>.
685
- "--rc-no-auth",
686
- "--log-level",
687
- "INFO",
688
- "--use-json-log",
689
- "--allow-other",
690
- // The ro-tmpfs deferred-mount barrier is already mounted here; without this
691
- // rclone aborts with "directory already mounted". See mount-placeholder.ts.
692
- "--allow-non-empty"
693
- ];
694
- if (config.ignoreSizeCheck) {
695
- args.push("--ignore-size", "--ignore-checksum");
696
- }
697
- if (config.driverName.split("+")[0] === "sharepoint") {
698
- args.push("--onedrive-upload-cutoff", "4Mi");
699
- }
700
- return args;
701
- }
702
- function _operationsStatTarget(remote, relPath) {
703
- const colon = remote.indexOf(":");
704
- const fs = colon >= 0 ? remote.slice(0, colon + 1) : remote;
705
- const base = colon >= 0 ? remote.slice(colon + 1) : "";
706
- const joined = [base, relPath].map((s) => s.replace(/^\/+|\/+$/g, "")).filter((s) => s.length > 0).join("/");
707
- return { fs, remote: joined };
708
- }
709
- async function fetchAuthoritativeSize(rcAddr, remote, relPath, fetchImpl = fetch) {
710
- const target = _operationsStatTarget(remote, relPath);
711
- try {
712
- const res = await fetchImpl(`http://${rcAddr}/operations/stat`, {
713
- method: "POST",
714
- headers: { "content-type": "application/json" },
715
- body: JSON.stringify(target)
716
- });
717
- if (!res.ok) return null;
718
- const body = await res.json();
719
- const item = body.item;
720
- if (!item || item.IsDir === true) return null;
721
- return typeof item.Size === "number" && item.Size >= 0 ? item.Size : null;
722
- } catch {
723
- return null;
724
- }
725
- }
726
- async function awaitQuiescent(dir, opts) {
727
- const deadline = Date.now() + opts.timeoutMs;
728
- for (; ; ) {
729
- const newest = await newestMtimeMs(dir);
730
- if (newest === null || Date.now() - newest >= opts.thresholdMs) return true;
731
- if (Date.now() >= deadline) return false;
732
- await sleep(opts.pollMs);
733
- }
734
- }
735
- async function vfsContentDir(cacheDir) {
736
- const vfs = path.join(cacheDir, "vfs");
737
- try {
738
- return (await stat(vfs)).isDirectory() ? vfs : cacheDir;
739
- } catch {
740
- return cacheDir;
741
- }
742
- }
743
- async function newestMtimeMs(dir) {
744
- let newest = null;
745
- try {
746
- const entries = await readdir(dir, { withFileTypes: true });
747
- for (const e of entries) {
748
- const full = path.join(dir, String(e.name));
749
- if (e.isDirectory()) {
750
- const sub = await newestMtimeMs(full);
751
- if (sub !== null && (newest === null || sub > newest)) newest = sub;
752
- } else if (e.isFile()) {
753
- try {
754
- const s = await stat(full);
755
- if (newest === null || s.mtimeMs > newest) newest = s.mtimeMs;
756
- } catch {
757
- }
758
- }
759
- }
760
- } catch {
761
- return null;
762
- }
763
- return newest;
764
- }
765
- function _fuseMountPresent(procMountsText, mountPoint) {
766
- const resolved = path.resolve(mountPoint);
767
- for (const raw of procMountsText.split("\n")) {
768
- if (raw.length === 0) continue;
769
- const fields = raw.split(/\s+/);
770
- const mp = fields[1];
771
- const fstype = fields[2];
772
- if (!mp || !fstype) continue;
773
- const decoded = mp.replace(/\\040/g, " ");
774
- if (decoded === resolved && (fstype === "fuse" || fstype.startsWith("fuse."))) return true;
775
- }
776
- return false;
777
- }
778
- async function isMountpoint(mountPoint) {
779
- try {
780
- const text = await readFile("/proc/self/mounts", "utf8");
781
- return _fuseMountPresent(text, mountPoint);
782
- } catch {
783
- return false;
784
- }
785
- }
786
- async function waitFor(check, opts) {
787
- const deadline = Date.now() + opts.timeoutMs;
788
- while (Date.now() < deadline) {
789
- if (await check()) return true;
790
- await sleep(opts.intervalMs);
791
- }
792
- return await check();
793
- }
794
- function sleep(ms) {
795
- return new Promise((resolve) => {
796
- setTimeout(resolve, ms);
797
- });
798
- }
799
- function attachLineSplitter(stream, onLine) {
800
- let buffer = "";
801
- stream.setEncoding("utf8");
802
- stream.on("data", (chunk) => {
803
- buffer += chunk;
804
- let idx = buffer.indexOf("\n");
805
- while (idx >= 0) {
806
- const line = buffer.slice(0, idx);
807
- buffer = buffer.slice(idx + 1);
808
- if (line.length > 0) onLine(line);
809
- idx = buffer.indexOf("\n");
810
- }
811
- });
812
- stream.on("end", () => {
813
- if (buffer.length > 0) {
814
- onLine(buffer);
815
- buffer = "";
816
- }
817
- });
818
- }
819
- async function dirSize(dir) {
820
- let total = 0;
821
- try {
822
- const entries = await readdir(dir, { withFileTypes: true });
823
- for (const e of entries) {
824
- const full = path.join(dir, String(e.name));
825
- if (e.isDirectory()) {
826
- total += await dirSize(full);
827
- } else if (e.isFile()) {
828
- try {
829
- const s = await stat(full);
830
- total += s.size;
831
- } catch {
832
- }
833
- }
834
- }
835
- } catch {
836
- return 0;
837
- }
838
- return total;
839
- }
840
-
841
- export { CachePoisonedMountError, DEFAULT_UPLOAD_STUCK_THRESHOLD, DEFAULT_VFS_WRITE_BACK, PortPool, RcloneProcessManager, _buildRcloneArgs, _fuseMountPresent, _isCachePoisonLine, _operationsStatTarget, _routeRcloneLogLine, awaitQuiescent, createFsWatcher, ensureDirMode, fetchAuthoritativeSize, quarantineCacheDir };
842
- //# sourceMappingURL=chunk-RAIRWFAN.js.map
843
- //# sourceMappingURL=chunk-RAIRWFAN.js.map