@lifeaitools/clauth 1.30.6 → 1.30.7

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.
@@ -1,115 +0,0 @@
1
- // node --test cli/commands/scrub.test.js
2
- import assert from "node:assert/strict";
3
- import fs from "node:fs";
4
- import os from "node:os";
5
- import path from "node:path";
6
- import test from "node:test";
7
-
8
- import {
9
- PATTERNS,
10
- findTranscripts,
11
- scrubFile,
12
- isSecretLike,
13
- loadExtraPatterns,
14
- sessionTargets,
15
- } from "./scrub.js";
16
-
17
- const GH_TOKEN = "ghp_0123456789abcdefABCDEF0123456789abcd"; // ghp_ + 36 chars
18
- const VAULT_VALUE = "SuperSecretVaultValue_abc123XYZ"; // secret-like literal
19
- const CUSTOM_SECRET = "MYCORP-TOKEN-998877"; // only an editable pattern catches it
20
-
21
- function seedProjects() {
22
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-scrub-"));
23
- const sess = path.join(root, "proj", "session-uuid");
24
- const sidecarDir = path.join(sess, "tool-results");
25
- fs.mkdirSync(sidecarDir, { recursive: true });
26
-
27
- const jsonl = path.join(root, "proj", "session-uuid.jsonl");
28
- const txt = path.join(sidecarDir, "toolu_abc.txt");
29
- const body = `token=${GH_TOKEN} value=${VAULT_VALUE} custom=${CUSTOM_SECRET}\n`;
30
- fs.writeFileSync(jsonl, `{"x":"${body.trim()}"}\n`, "utf-8");
31
- fs.writeFileSync(txt, body, "utf-8"); // the sidecar that the old scrubber skipped
32
- return { root, jsonl, txt };
33
- }
34
-
35
- test("findTranscripts discovers .jsonl AND tool-results/*.txt sidecars", () => {
36
- const { root, jsonl, txt } = seedProjects();
37
- const found = findTranscripts(root);
38
- assert.ok(found.includes(jsonl), "should find the .jsonl transcript");
39
- assert.ok(found.includes(txt), "should find the .txt sidecar (the previously-skipped leak path)");
40
- });
41
-
42
- test("scrubFile redacts the github token in BOTH the jsonl and the sidecar", () => {
43
- const { jsonl, txt } = seedProjects();
44
- for (const f of [jsonl, txt]) {
45
- const n = scrubFile(f, { force: true, patterns: PATTERNS, literals: [] });
46
- assert.ok(n >= 1, `expected >=1 redaction in ${path.basename(f)}`);
47
- const after = fs.readFileSync(f, "utf-8");
48
- assert.ok(!after.includes(GH_TOKEN), "github token must be gone");
49
- assert.ok(after.includes("[GITHUB_TOKEN_REDACTED]"), "redaction marker present");
50
- assert.ok(after.includes("[CLAUTH-SCRUBBED]"), "file stamped as scrubbed");
51
- }
52
- });
53
-
54
- test("vault-value (literal) redaction removes an arbitrary secret regardless of format", () => {
55
- const { txt } = seedProjects();
56
- const n = scrubFile(txt, { force: true, patterns: [], literals: [{ name: "test-svc", value: VAULT_VALUE }] });
57
- assert.equal(n, 1, "exactly one literal occurrence redacted");
58
- const after = fs.readFileSync(txt, "utf-8");
59
- assert.ok(!after.includes(VAULT_VALUE), "vault value must be gone");
60
- assert.ok(after.includes("[CLAUTH:test-svc_REDACTED]"), "labelled vault redaction present");
61
- });
62
-
63
- test("editable patterns file catches a custom secret with no clauth release", () => {
64
- const cfg = path.join(os.tmpdir(), `clauth-scrub-patterns-${Date.now()}.json`);
65
- fs.writeFileSync(cfg, JSON.stringify([{ pattern: "MYCORP-TOKEN-\\d+", replacement: "[MYCORP_REDACTED]" }]), "utf-8");
66
- const extra = loadExtraPatterns(cfg);
67
- assert.equal(extra.length, 1, "one extra pattern loaded");
68
-
69
- const { txt } = seedProjects();
70
- const n = scrubFile(txt, { force: true, patterns: extra, literals: [] });
71
- assert.ok(n >= 1, "custom pattern matched");
72
- const after = fs.readFileSync(txt, "utf-8");
73
- assert.ok(!after.includes(CUSTOM_SECRET), "custom secret gone");
74
- assert.ok(after.includes("[MYCORP_REDACTED]"), "custom replacement applied");
75
- fs.rmSync(cfg, { force: true });
76
- });
77
-
78
- test("isSecretLike: accepts real tokens, rejects short/url/whitespace values", () => {
79
- assert.ok(isSecretLike(GH_TOKEN), "long token is secret-like");
80
- assert.ok(isSecretLike(VAULT_VALUE), "31-char no-space value is secret-like");
81
- assert.ok(!isSecretLike("short"), "short value rejected");
82
- assert.ok(!isSecretLike("https://research.regendevcorp.com/mcp"), "plain URL rejected");
83
- assert.ok(!isSecretLike("has spaces in it value"), "whitespace value rejected");
84
- assert.ok(!isSecretLike(""), "empty rejected");
85
- });
86
-
87
- test("sessionTargets returns ONLY the ending session's transcript + its sidecars", () => {
88
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-sess-"));
89
- const proj = path.join(root, "C--proj");
90
- const sid = "11112222-3333-4444-5555-666677778888";
91
- fs.mkdirSync(path.join(proj, sid, "tool-results"), { recursive: true });
92
- const transcript = path.join(proj, `${sid}.jsonl`);
93
- const sidecar = path.join(proj, sid, "tool-results", "toolu_x.txt");
94
- const otherSession = path.join(proj, "99990000-aaaa-bbbb-cccc-ddddeeeeffff.jsonl");
95
- fs.writeFileSync(transcript, "{}\n");
96
- fs.writeFileSync(sidecar, "tool output\n");
97
- fs.writeFileSync(otherSession, "{}\n"); // a DIFFERENT session — must NOT be included
98
-
99
- const targets = sessionTargets({ transcript_path: transcript, session_id: sid });
100
- assert.ok(targets.includes(transcript), "includes the session transcript");
101
- assert.ok(targets.includes(sidecar), "includes the session's sidecar");
102
- assert.ok(!targets.includes(otherSession), "does NOT include other sessions (session-only)");
103
- assert.equal(targets.length, 2, "exactly the 2 session files");
104
-
105
- assert.deepEqual(sessionTargets(null), [], "no hook input → no targets (caller falls back)");
106
- assert.deepEqual(sessionTargets({ transcript_path: path.join(root, "nope.jsonl") }), [], "missing file → none");
107
- });
108
-
109
- test("loadExtraPatterns tolerates a missing/malformed file", () => {
110
- assert.deepEqual(loadExtraPatterns(path.join(os.tmpdir(), "does-not-exist-xyz.json")), []);
111
- const bad = path.join(os.tmpdir(), `clauth-bad-${Date.now()}.json`);
112
- fs.writeFileSync(bad, "{ not json", "utf-8");
113
- assert.deepEqual(loadExtraPatterns(bad), [], "malformed json → empty, never throws");
114
- fs.rmSync(bad, { force: true });
115
- });
@@ -1,82 +0,0 @@
1
- import fs from "fs";
2
- import os from "os";
3
- import path from "path";
4
-
5
- function shellSingleQuote(value) {
6
- return `'${String(value ?? "").replace(/'/g, "''")}'`;
7
- }
8
-
9
- function posixShellQuote(value) {
10
- return `'${String(value ?? "").replace(/'/g, "'\\\"'\\\"'")}'`;
11
- }
12
-
13
- export function enrollmentScriptName(label, target = "windows") {
14
- const slug = String(label || "new-computer")
15
- .toLowerCase()
16
- .replace(/[^a-z0-9]+/g, "-")
17
- .replace(/^-+|-+$/g, "")
18
- .slice(0, 40) || "new-computer";
19
- return `clauth-enroll-${slug}${target === "linux" ? ".sh" : ".ps1"}`;
20
- }
21
-
22
- function windowsScript({ supabaseUrl, anonKey, enrollmentCode }) {
23
- return [
24
- "$ErrorActionPreference = 'Stop'",
25
- "$label = $env:COMPUTERNAME",
26
- "if (-not $label) { $label = [System.Net.Dns]::GetHostName() }",
27
- "npm install -g @lifeaitools/clauth@latest",
28
- ["clauth setup", `--supabase-url ${shellSingleQuote(supabaseUrl)}`, `--anon-key ${shellSingleQuote(anonKey)}`, `--enrollment-code ${shellSingleQuote(enrollmentCode)}`, "--label \"$label\""].join(" "),
29
- "clauth serve install",
30
- "$self = $PSCommandPath",
31
- "Start-Process powershell.exe -WindowStyle Hidden -ArgumentList @('-NoProfile','-Command',\"Start-Sleep -Seconds 2; & ('Remove' + '-Item') -LiteralPath '$self' -Force -ErrorAction SilentlyContinue\")",
32
- ].join("\r\n");
33
- }
34
-
35
- function linuxScript({ supabaseUrl, anonKey, enrollmentCode }) {
36
- return [
37
- "#!/usr/bin/env sh",
38
- "set -eu",
39
- "label=$(hostname)",
40
- "as_root() { if [ \"$(id -u)\" -eq 0 ]; then \"$@\"; elif command -v sudo >/dev/null; then sudo \"$@\"; else echo 'Root or sudo is required to install prerequisites.' >&2; exit 1; fi; }",
41
- "install_prerequisites() {",
42
- " if command -v apt-get >/dev/null; then as_root apt-get update; as_root apt-get install -y nodejs npm openssl;",
43
- " elif command -v dnf >/dev/null; then as_root dnf install -y nodejs npm openssl;",
44
- " else echo 'Headless enrollment supports systemd hosts with apt-get or dnf. Install Node.js 18+, npm, and openssl, then rerun this script.' >&2; exit 1; fi",
45
- "}",
46
- "if ! command -v systemctl >/dev/null || ! command -v loginctl >/dev/null; then echo 'Headless enrollment requires systemd and loginctl.' >&2; exit 1; fi",
47
- "if ! command -v node >/dev/null || ! command -v npm >/dev/null || ! command -v openssl >/dev/null; then install_prerequisites; fi",
48
- "node_major=$(node -p \"process.versions.node.split('.')[0]\")",
49
- "if [ \"$node_major\" -lt 18 ]; then echo 'Node.js 18+ is required.' >&2; exit 1; fi",
50
- "if ! npm install -g @lifeaitools/clauth@latest; then as_root npm install -g @lifeaitools/clauth@latest; fi",
51
- ["clauth setup", `--supabase-url ${posixShellQuote(supabaseUrl)}`, `--anon-key ${posixShellQuote(anonKey)}`, `--enrollment-code ${posixShellQuote(enrollmentCode)}`, '--label "$label"'].join(" "),
52
- "user_name=$(id -un)",
53
- "if loginctl enable-linger \"$user_name\"; then echo \"Linger enabled for $user_name.\"; elif as_root loginctl enable-linger \"$user_name\"; then echo \"Linger enabled for $user_name.\"; else echo 'Could not enable linger for unattended restart.' >&2; exit 1; fi",
54
- "if ! test -t 0; then echo 'Headless enrollment requires an interactive TTY to set the vault password.' >&2; exit 1; fi",
55
- "restore_echo() { stty echo 2>/dev/null || true; }",
56
- "trap restore_echo EXIT HUP INT TERM",
57
- "printf 'Re-enter the vault password to enable unattended restart: ' >&2",
58
- "stty -echo",
59
- "IFS= read -r vault_password",
60
- "stty echo",
61
- "trap - EXIT HUP INT TERM",
62
- "printf '\\n' >&2",
63
- "[ -n \"$vault_password\" ] || { echo 'A vault password is required for unattended restart.' >&2; exit 1; }",
64
- "printf %s \"$vault_password\" | clauth serve install --pw-stdin",
65
- "unset vault_password",
66
- "rm -f -- \"$0\"",
67
- ].join("\n");
68
- }
69
-
70
- export function writeEnrollmentScript({ supabaseUrl, anonKey, enrollmentCode, label, target = "windows", appDir } = {}) {
71
- if (!["windows", "linux"].includes(target)) throw new Error(`Unsupported enrollment target: ${target}`);
72
- const outputDir = appDir || (process.platform === "win32"
73
- ? path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "clauth")
74
- : path.join(os.homedir(), ".config", "clauth"));
75
- fs.mkdirSync(outputDir, { recursive: true });
76
- const scriptPath = path.join(outputDir, enrollmentScriptName(label, target));
77
- const script = target === "linux"
78
- ? linuxScript({ supabaseUrl, anonKey, enrollmentCode })
79
- : windowsScript({ supabaseUrl, anonKey, enrollmentCode });
80
- fs.writeFileSync(scriptPath, `${script}\n`, { encoding: "utf8", mode: target === "linux" ? 0o700 : undefined });
81
- return scriptPath;
82
- }
@@ -1,339 +0,0 @@
1
- // cli/webdav-service.js
2
- // WebDAV child-process supervisor — rclone serve webdav
3
- //
4
- // ISOLATION: This module has zero coupling to existing fs_* tool handlers.
5
- // Rollback: set webdav.enabled=false in the config block (or remove it)
6
- // + delete this file + remove the two-line import in serve.js.
7
- //
8
- // Design: lazy-start. The rclone child spawns on the first fs_dav_setup call.
9
- // Subsequent calls return the running child's status without restarting.
10
- // On child crash: exponential-backoff restart loop (2s → 5s → 15s → 60s).
11
-
12
- import { spawn, execSync } from "child_process";
13
- import { writeFileSync, readFileSync, unlinkSync, mkdirSync, readdirSync } from "fs";
14
- import { tmpdir, homedir } from "os";
15
- import { join } from "path";
16
- import crypto from "crypto";
17
- import * as api from "./api.js";
18
- import { deriveToken } from "./fingerprint.js";
19
-
20
- const WEBDAV_PORT = 8620;
21
- const WEBDAV_USER = "claude";
22
- const CRED_NAME = "webdav-claude";
23
- const RESTART_DELAYS = [2000, 5000, 15000, 60000];
24
-
25
- const APPDATA = process.env.APPDATA || join(homedir(), "AppData", "Roaming");
26
- const CONFIG_PATH = join(APPDATA, "clauth", "webdav-config.json");
27
-
28
- const DEFAULT_CONFIG = {
29
- enabled: true,
30
- port: WEBDAV_PORT,
31
- credential: CRED_NAME,
32
- upstreams: [],
33
- };
34
-
35
- // ── Config persistence ──────────────────────────────────────────
36
-
37
- export function loadConfig() {
38
- try {
39
- const raw = readFileSync(CONFIG_PATH, "utf8");
40
- const cfg = JSON.parse(raw);
41
- if (!Array.isArray(cfg.upstreams)) cfg.upstreams = [];
42
- return { ...DEFAULT_CONFIG, ...cfg };
43
- } catch {
44
- return { ...DEFAULT_CONFIG };
45
- }
46
- }
47
-
48
- export function saveConfig(cfg) {
49
- const dir = join(APPDATA, "clauth");
50
- mkdirSync(dir, { recursive: true });
51
- writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2), "utf8");
52
- _fsCfgCache = null;
53
- }
54
-
55
- export function addMount(name, mountPath) {
56
- const cfg = loadConfig();
57
- if (cfg.upstreams.some(u => u.name === name)) return { error: `Mount '${name}' already exists` };
58
- cfg.upstreams.push({ name, path: mountPath });
59
- saveConfig(cfg);
60
- return { ok: true, upstreams: cfg.upstreams };
61
- }
62
-
63
- export function removeMount(name) {
64
- const cfg = loadConfig();
65
- const before = cfg.upstreams.length;
66
- cfg.upstreams = cfg.upstreams.filter(u => u.name !== name);
67
- if (cfg.upstreams.length === before) return { error: `Mount '${name}' not found` };
68
- saveConfig(cfg);
69
- return { ok: true, upstreams: cfg.upstreams };
70
- }
71
-
72
- export function updateMount(name, updates) {
73
- const cfg = loadConfig();
74
- const mount = cfg.upstreams.find(u => u.name === name);
75
- if (!mount) return { error: `Mount '${name}' not found` };
76
- if (updates.credential !== undefined) cfg.credential = updates.credential;
77
- saveConfig(cfg);
78
- return { ok: true, upstreams: cfg.upstreams };
79
- }
80
-
81
- let _fsCfgCache = null;
82
-
83
- // ── Module state ────────────────────────────────────────────────
84
- let _child = null;
85
- let _configPath = null;
86
- let _status = "stopped"; // stopped | starting | running | error
87
- let _error = null;
88
- let _restartCount = 0;
89
- let _startedAt = null;
90
- let _vaultFn = null; // () => { password, machineHash }
91
- let _cfg = null;
92
-
93
- // ── Public API ───────────────────────────────────────────────────
94
-
95
- /**
96
- * Ensure the rclone WebDAV child is running.
97
- * Idempotent — safe to call multiple times.
98
- * Returns { ok: true } or { error: string }.
99
- */
100
- export async function ensureRunning(vaultFn, config) {
101
- if (vaultFn) _vaultFn = vaultFn;
102
- if (config) _cfg = config;
103
- if (!_cfg) _cfg = loadConfig();
104
-
105
- if (_status === "running" && _child) return { ok: true };
106
- if (_status === "starting") return { ok: true, message: "still starting" };
107
-
108
- return _startChild();
109
- }
110
-
111
- /** Current supervisor status — safe to call at any time. */
112
- export function getStatus() {
113
- return {
114
- status: _status,
115
- port: WEBDAV_PORT,
116
- url: _status === "running" ? `http://127.0.0.1:${WEBDAV_PORT}` : null,
117
- error: _error || null,
118
- restart_count: _restartCount,
119
- started_at: _startedAt || null,
120
- };
121
- }
122
-
123
- /** Kill the child and clean up the temp config file. */
124
- export function shutdown() {
125
- if (_child) {
126
- _child.removeAllListeners();
127
- _child.kill("SIGTERM");
128
- _child = null;
129
- }
130
- _cleanupConfig();
131
- _status = "stopped";
132
- }
133
-
134
- /**
135
- * rclone-compatible AES-CTR "obscure" — same algorithm as `rclone obscure`.
136
- * Used by fs_dav_setup to embed a non-plaintext credential in the mount script.
137
- * The result can be passed as RCLONE_CONFIG_xxx_PASS.
138
- */
139
- export function rcloneObscure(password) {
140
- // Key from rclone source: lib/obscure/obscure.go cryptKey
141
- const key = Buffer.from([
142
- 0x9c, 0x93, 0x5b, 0x48, 0x73, 0x0a, 0x55, 0x4d,
143
- 0x6b, 0xfd, 0x7c, 0x63, 0xc8, 0x86, 0xd9, 0x2d,
144
- 0x02, 0x01, 0x99, 0x87, 0x54, 0xd3, 0x9b, 0xeb,
145
- 0x40, 0x87, 0x3f, 0xa1, 0x3f, 0x7a, 0xbe, 0x5e,
146
- ]);
147
- const iv = crypto.randomBytes(16);
148
- const cipher = crypto.createCipheriv("aes-256-ctr", key, iv);
149
- const ct = Buffer.concat([cipher.update(Buffer.from(password, "utf8")), cipher.final()]);
150
- return Buffer.concat([iv, ct]).toString("base64url");
151
- }
152
-
153
- // ── Internal ─────────────────────────────────────────────────────
154
-
155
- async function _startChild() {
156
- _status = "starting";
157
- _error = null;
158
-
159
- if (!_cfg?.upstreams?.length) {
160
- _status = "error";
161
- _error = "No mount points configured — add them in the clauth dashboard";
162
- return { error: _error };
163
- }
164
-
165
- // 1. Resolve credential from vault
166
- let pass;
167
- try {
168
- const vault = _vaultFn?.();
169
- if (!vault?.password) {
170
- _status = "error";
171
- _error = "Vault is locked — unlock clauth first";
172
- return { error: _error };
173
- }
174
- const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
175
- const credName = _cfg.credential || CRED_NAME;
176
- const result = await api.retrieve(vault.password, vault.machineHash, token, timestamp, credName);
177
- if (!result.value) {
178
- _status = "error";
179
- _error = `Credential '${credName}' not found — register it in the vault first`;
180
- return { error: _error };
181
- }
182
- pass = result.value;
183
- } catch (err) {
184
- _status = "error";
185
- _error = `Credential fetch failed: ${err.message}`;
186
- return { error: _error };
187
- }
188
-
189
- // 2. Locate rclone
190
- const rclonePath = _findRclone();
191
- if (!rclonePath) {
192
- _status = "error";
193
- _error = "rclone not found — install with: winget install Rclone.Rclone";
194
- return { error: _error };
195
- }
196
-
197
- // 3. Write rclone config — combine backend with quoted entries for paths with spaces.
198
- // Per rclone docs: "dir=remote:path with space" (quote the whole entry).
199
- const mounts = _cfg.upstreams || [];
200
- const combineUpstreams = mounts
201
- .map(u => `"${u.name}=${u.path.replace(/\\/g, "/")}"`)
202
- .join(" ");
203
- const configContent = `[srv]\ntype = combine\nupstreams = ${combineUpstreams}\n`;
204
- _cleanupConfig();
205
- _configPath = join(tmpdir(), `.rclone-webdav-${process.pid}.conf`);
206
- try {
207
- writeFileSync(_configPath, configContent, { mode: 0o600 });
208
- } catch (err) {
209
- _status = "error";
210
- _error = `Config write failed: ${err.message}`;
211
- return { error: _error };
212
- }
213
-
214
- // 4. Spawn rclone
215
- return new Promise(resolve => {
216
- let settled = false;
217
- const settle = (val) => { if (!settled) { settled = true; resolve(val); } };
218
-
219
- try {
220
- _child = spawn(rclonePath, [
221
- "serve", "webdav", "srv:",
222
- "--config", _configPath,
223
- "--addr", `127.0.0.1:${WEBDAV_PORT}`,
224
- "--user", WEBDAV_USER,
225
- "--pass", pass,
226
- "--no-modtime",
227
- "--vfs-cache-mode", "full",
228
- "--vfs-write-back", "0s",
229
- "--ignore-size",
230
- "--cache-dir", join(process.env.TEMP || tmpdir(), "rclone-dav-cache"),
231
- "--transfers", "4",
232
- ], { stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
233
-
234
- } catch (spawnErr) {
235
- _child = null;
236
- _status = "error";
237
- _error = spawnErr.message;
238
- _cleanupConfig();
239
- settle({ error: spawnErr.message });
240
- return;
241
- }
242
-
243
- _child.stdout.on("data", () => {});
244
-
245
- _child.stderr.on("data", (chunk) => {
246
- const line = chunk.toString().trim();
247
- if (line.includes("Serving WebDAV") || line.includes("Listening on")) {
248
- _status = "running";
249
- _startedAt = new Date().toISOString();
250
- settle({ ok: true });
251
- }
252
- // Surface errors but don't stop on every stderr line — rclone logs normally to stderr
253
- if (/\berror\b/i.test(line) || /\bfatal\b/i.test(line)) {
254
- _error = line.slice(0, 300);
255
- }
256
- });
257
-
258
- _child.on("error", (err) => {
259
- _child = null;
260
- _status = "error";
261
- _error = err.message;
262
- _cleanupConfig();
263
- settle({ error: err.message });
264
- _scheduleRestart();
265
- });
266
-
267
- _child.on("exit", (code, signal) => {
268
- _child = null;
269
- if (signal === "SIGTERM" || signal === "SIGKILL" || code === 0) {
270
- _status = "stopped";
271
- _cleanupConfig();
272
- return;
273
- }
274
- _status = "error";
275
- _error = `rclone exited unexpectedly (code=${code} signal=${signal})`;
276
- _cleanupConfig();
277
- settle({ error: _error });
278
- _scheduleRestart();
279
- });
280
-
281
- // Optimistic fallback: if rclone doesn't print "Serving WebDAV" within 2s, assume running
282
- setTimeout(() => {
283
- if (_status === "starting") {
284
- _status = "running";
285
- _startedAt = _startedAt || new Date().toISOString();
286
- }
287
- settle({ ok: true });
288
- }, 2000);
289
- });
290
- }
291
-
292
- function _scheduleRestart() {
293
- const delay = RESTART_DELAYS[Math.min(_restartCount, RESTART_DELAYS.length - 1)];
294
- _restartCount++;
295
- setTimeout(() => {
296
- if (_status === "stopped") return; // intentionally stopped, don't restart
297
- _startChild();
298
- }, delay);
299
- }
300
-
301
- function _cleanupConfig() {
302
- if (_configPath) {
303
- try { unlinkSync(_configPath); } catch {}
304
- _configPath = null;
305
- }
306
- }
307
-
308
- function _findRclone() {
309
- const home = homedir();
310
- const wingetBase = process.env.LOCALAPPDATA
311
- ? join(process.env.LOCALAPPDATA, "Microsoft", "WinGet", "Packages")
312
- : null;
313
- const execOpts = { stdio: "pipe", timeout: 4000, windowsHide: true };
314
-
315
- const candidates = [
316
- "rclone",
317
- "C:\\Program Files\\rclone\\rclone.exe",
318
- join(home, "scoop", "apps", "rclone", "current", "rclone.exe"),
319
- "C:\\ProgramData\\scoop\\apps\\rclone\\current\\rclone.exe",
320
- ];
321
-
322
- // Winget installs into a version-stamped subdirectory — glob for it
323
- if (wingetBase) {
324
- const wingetPkg = join(wingetBase, "Rclone.Rclone_Microsoft.Winget.Source_8wekyb3d8bbwe");
325
- try {
326
- for (const d of readdirSync(wingetPkg)) {
327
- if (d.startsWith("rclone-")) candidates.push(join(wingetPkg, d, "rclone.exe"));
328
- }
329
- } catch {}
330
- }
331
-
332
- for (const c of candidates) {
333
- try {
334
- execSync(`"${c}" version`, execOpts);
335
- return c;
336
- } catch {}
337
- }
338
- return null;
339
- }