clawvault 3.0.0 → 3.1.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.
- package/README.md +156 -105
- package/bin/clawvault.js +0 -2
- package/bin/register-core-commands.js +20 -2
- package/dist/{chunk-3D6BCTP6.js → chunk-33UGEQRT.js} +70 -145
- package/dist/{chunk-ZVVFWOLW.js → chunk-3WRJEKN4.js} +1 -1
- package/dist/{chunk-DEFFDRVP.js → chunk-3ZIH425O.js} +3 -70
- package/dist/{chunk-K234IDRJ.js → chunk-D2H45LON.js} +1 -0
- package/dist/{chunk-YKTA5JOJ.js → chunk-H62BP7RI.js} +3 -3
- package/dist/{chunk-WGRQ6HDV.js → chunk-LI4O6NVK.js} +1 -1
- package/dist/{chunk-7R7O6STJ.js → chunk-OCGVIN3L.js} +1 -1
- package/dist/{chunk-GAJV4IGR.js → chunk-YCUNCH2I.js} +3 -7
- package/dist/cli/index.cjs +10 -1459
- package/dist/cli/index.js +5 -8
- package/dist/commands/compat.cjs +70 -145
- package/dist/commands/compat.js +1 -1
- package/dist/commands/context.cjs +1 -0
- package/dist/commands/context.js +3 -3
- package/dist/commands/doctor.cjs +68 -144
- package/dist/commands/doctor.js +4 -4
- package/dist/commands/embed.js +2 -2
- package/dist/commands/setup.cjs +2 -69
- package/dist/commands/setup.d.cts +0 -1
- package/dist/commands/setup.d.ts +0 -1
- package/dist/commands/setup.js +2 -2
- package/dist/commands/sleep.cjs +1 -0
- package/dist/commands/sleep.js +2 -2
- package/dist/commands/status.cjs +1 -0
- package/dist/commands/status.js +2 -2
- package/dist/commands/wake.cjs +1 -0
- package/dist/commands/wake.js +2 -2
- package/dist/index.cjs +447 -2600
- package/dist/index.d.cts +0 -4
- package/dist/index.d.ts +0 -4
- package/dist/index.js +8 -69
- package/dist/plugin/index.cjs +3 -3
- package/dist/plugin/index.js +10 -10
- package/package.json +11 -17
- package/bin/register-tailscale-commands.js +0 -106
- package/dist/chunk-IVRIKYFE.js +0 -520
- package/dist/chunk-THRJVD4L.js +0 -373
- package/dist/chunk-TIGW564L.js +0 -628
- package/dist/commands/tailscale.cjs +0 -1532
- package/dist/commands/tailscale.d.cts +0 -52
- package/dist/commands/tailscale.d.ts +0 -52
- package/dist/commands/tailscale.js +0 -26
- package/dist/lib/canvas-layout.cjs +0 -136
- package/dist/lib/canvas-layout.d.cts +0 -31
- package/dist/lib/canvas-layout.d.ts +0 -31
- package/dist/lib/canvas-layout.js +0 -92
- package/dist/lib/tailscale.cjs +0 -1183
- package/dist/lib/tailscale.d.cts +0 -225
- package/dist/lib/tailscale.d.ts +0 -225
- package/dist/lib/tailscale.js +0 -50
- package/dist/lib/webdav.cjs +0 -568
- package/dist/lib/webdav.d.cts +0 -109
- package/dist/lib/webdav.d.ts +0 -109
- package/dist/lib/webdav.js +0 -35
- package/hooks/clawvault/HOOK.md +0 -83
- package/hooks/clawvault/handler.js +0 -879
- package/hooks/clawvault/handler.test.js +0 -354
package/dist/cli/index.cjs
CHANGED
|
@@ -712,6 +712,7 @@ function stripQmdNoise(raw) {
|
|
|
712
712
|
function parseQmdOutput(raw) {
|
|
713
713
|
const trimmed = stripQmdNoise(raw).trim();
|
|
714
714
|
if (!trimmed) return [];
|
|
715
|
+
if (trimmed.startsWith("No results") || trimmed.startsWith("No matches")) return [];
|
|
715
716
|
const direct = tryParseJson(trimmed);
|
|
716
717
|
const extracted = direct ? null : extractJsonPayload(trimmed);
|
|
717
718
|
const parsed = direct ?? (extracted ? tryParseJson(extracted) : null);
|
|
@@ -1465,8 +1466,8 @@ function toUnresolvedNodeId(raw) {
|
|
|
1465
1466
|
return `unresolved:${normalizeUnresolvedKey(raw)}`;
|
|
1466
1467
|
}
|
|
1467
1468
|
function titleFromNoteKey(noteKey) {
|
|
1468
|
-
const
|
|
1469
|
-
return
|
|
1469
|
+
const basename10 = noteKey.split("/").pop() ?? noteKey;
|
|
1470
|
+
return basename10.replace(/[-_]+/g, " ").replace(/\s+/g, " ").trim().replace(/\b\w/g, (char) => char.toUpperCase());
|
|
1470
1471
|
}
|
|
1471
1472
|
function inferNodeType(relativePath, frontmatter) {
|
|
1472
1473
|
const normalized = normalizeRelativePath(relativePath).toLowerCase();
|
|
@@ -4079,9 +4080,9 @@ function collectNodeAliases(node) {
|
|
|
4079
4080
|
aliases.add(node.title);
|
|
4080
4081
|
}
|
|
4081
4082
|
if (node.path) {
|
|
4082
|
-
const
|
|
4083
|
-
aliases.add(
|
|
4084
|
-
aliases.add(
|
|
4083
|
+
const basename10 = path7.basename(node.path, ".md");
|
|
4084
|
+
aliases.add(basename10.replace(/[-_]+/g, " "));
|
|
4085
|
+
aliases.add(basename10);
|
|
4085
4086
|
}
|
|
4086
4087
|
return [...aliases].map((alias) => normalizeText(alias)).filter((alias) => alias.length >= 3);
|
|
4087
4088
|
}
|
|
@@ -7129,8 +7130,8 @@ var SessionWatcher = class {
|
|
|
7129
7130
|
this.fileOffsets.delete(resolved);
|
|
7130
7131
|
this.pendingPaths.delete(resolved);
|
|
7131
7132
|
});
|
|
7132
|
-
await new Promise((
|
|
7133
|
-
this.watcher?.once("ready", () =>
|
|
7133
|
+
await new Promise((resolve21, reject) => {
|
|
7134
|
+
this.watcher?.once("ready", () => resolve21());
|
|
7134
7135
|
this.watcher?.once("error", (error) => reject(error));
|
|
7135
7136
|
});
|
|
7136
7137
|
if (this.ignoreInitial) {
|
|
@@ -7895,12 +7896,12 @@ async function watchSessions(observer, watchPath) {
|
|
|
7895
7896
|
const watcher = new SessionWatcher(watchPath, observer);
|
|
7896
7897
|
await watcher.start();
|
|
7897
7898
|
console.log(`Watching session updates: ${watchPath}`);
|
|
7898
|
-
await new Promise((
|
|
7899
|
+
await new Promise((resolve21) => {
|
|
7899
7900
|
const shutdown = async () => {
|
|
7900
7901
|
process.off("SIGINT", onSigInt);
|
|
7901
7902
|
process.off("SIGTERM", onSigTerm);
|
|
7902
7903
|
await watcher.stop();
|
|
7903
|
-
|
|
7904
|
+
resolve21();
|
|
7904
7905
|
};
|
|
7905
7906
|
const onSigInt = () => {
|
|
7906
7907
|
void shutdown();
|
|
@@ -8567,1455 +8568,6 @@ function registerReweaveCommand(program) {
|
|
|
8567
8568
|
});
|
|
8568
8569
|
}
|
|
8569
8570
|
|
|
8570
|
-
// src/commands/tailscale.ts
|
|
8571
|
-
var path24 = __toESM(require("path"), 1);
|
|
8572
|
-
|
|
8573
|
-
// src/lib/tailscale.ts
|
|
8574
|
-
var import_child_process4 = require("child_process");
|
|
8575
|
-
var fs23 = __toESM(require("fs"), 1);
|
|
8576
|
-
var path23 = __toESM(require("path"), 1);
|
|
8577
|
-
var http = __toESM(require("http"), 1);
|
|
8578
|
-
var https = __toESM(require("https"), 1);
|
|
8579
|
-
|
|
8580
|
-
// src/lib/webdav.ts
|
|
8581
|
-
var fs22 = __toESM(require("fs"), 1);
|
|
8582
|
-
var path22 = __toESM(require("path"), 1);
|
|
8583
|
-
var WEBDAV_PREFIX = "/webdav";
|
|
8584
|
-
var BLOCKED_PATHS = [
|
|
8585
|
-
".clawvault",
|
|
8586
|
-
".git",
|
|
8587
|
-
".obsidian",
|
|
8588
|
-
"node_modules"
|
|
8589
|
-
];
|
|
8590
|
-
var SUPPORTED_METHODS = ["GET", "PUT", "DELETE", "MKCOL", "PROPFIND", "OPTIONS", "HEAD", "MOVE", "COPY"];
|
|
8591
|
-
function toRequestSegments(requestPath) {
|
|
8592
|
-
return requestPath.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
8593
|
-
}
|
|
8594
|
-
function isWithinRoot(fullPath, rootPath) {
|
|
8595
|
-
const resolvedRoot = path22.resolve(rootPath);
|
|
8596
|
-
const relative5 = path22.relative(resolvedRoot, fullPath);
|
|
8597
|
-
return !(relative5.startsWith("..") || path22.isAbsolute(relative5));
|
|
8598
|
-
}
|
|
8599
|
-
function isPathSafe(requestPath, rootPath) {
|
|
8600
|
-
const pathParts = toRequestSegments(requestPath);
|
|
8601
|
-
if (pathParts.includes("..")) {
|
|
8602
|
-
return false;
|
|
8603
|
-
}
|
|
8604
|
-
const normalizedRelativePath = path22.normalize(pathParts.join(path22.sep));
|
|
8605
|
-
const fullPath = path22.resolve(rootPath, normalizedRelativePath);
|
|
8606
|
-
if (!isWithinRoot(fullPath, rootPath)) {
|
|
8607
|
-
return false;
|
|
8608
|
-
}
|
|
8609
|
-
for (const part of pathParts) {
|
|
8610
|
-
if (BLOCKED_PATHS.includes(part)) {
|
|
8611
|
-
return false;
|
|
8612
|
-
}
|
|
8613
|
-
}
|
|
8614
|
-
return true;
|
|
8615
|
-
}
|
|
8616
|
-
function resolveWebDAVPath(requestPath, rootPath) {
|
|
8617
|
-
const pathParts = toRequestSegments(requestPath);
|
|
8618
|
-
if (pathParts.includes("..")) {
|
|
8619
|
-
return null;
|
|
8620
|
-
}
|
|
8621
|
-
const normalizedRelativePath = path22.normalize(pathParts.join(path22.sep));
|
|
8622
|
-
const fullPath = path22.resolve(rootPath, normalizedRelativePath);
|
|
8623
|
-
if (!isWithinRoot(fullPath, rootPath)) {
|
|
8624
|
-
return null;
|
|
8625
|
-
}
|
|
8626
|
-
return fullPath;
|
|
8627
|
-
}
|
|
8628
|
-
function checkAuth(req, auth) {
|
|
8629
|
-
if (!auth) {
|
|
8630
|
-
return true;
|
|
8631
|
-
}
|
|
8632
|
-
const authHeader = req.headers.authorization;
|
|
8633
|
-
if (!authHeader || !authHeader.startsWith("Basic ")) {
|
|
8634
|
-
return false;
|
|
8635
|
-
}
|
|
8636
|
-
const base64Credentials = authHeader.slice(6);
|
|
8637
|
-
const credentials = Buffer.from(base64Credentials, "base64").toString("utf-8");
|
|
8638
|
-
const [username, password] = credentials.split(":");
|
|
8639
|
-
return username === auth.username && password === auth.password;
|
|
8640
|
-
}
|
|
8641
|
-
function escapeXml(str) {
|
|
8642
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
8643
|
-
}
|
|
8644
|
-
function formatWebDAVDate(date) {
|
|
8645
|
-
return date.toUTCString();
|
|
8646
|
-
}
|
|
8647
|
-
function generatePropfindEntry(href, stats, isCollection) {
|
|
8648
|
-
const resourceType = isCollection ? "<D:resourcetype><D:collection/></D:resourcetype>" : "<D:resourcetype/>";
|
|
8649
|
-
const contentLength = stats && !isCollection ? `<D:getcontentlength>${stats.size}</D:getcontentlength>` : "";
|
|
8650
|
-
const lastModified = stats ? `<D:getlastmodified>${formatWebDAVDate(stats.mtime)}</D:getlastmodified>` : "";
|
|
8651
|
-
const etag = stats ? `<D:getetag>"${stats.mtime.getTime().toString(16)}-${stats.size.toString(16)}"</D:getetag>` : "";
|
|
8652
|
-
const contentType = !isCollection ? "<D:getcontenttype>application/octet-stream</D:getcontenttype>" : "";
|
|
8653
|
-
return ` <D:response>
|
|
8654
|
-
<D:href>${escapeXml(href)}</D:href>
|
|
8655
|
-
<D:propstat>
|
|
8656
|
-
<D:prop>
|
|
8657
|
-
${resourceType}
|
|
8658
|
-
${contentLength}
|
|
8659
|
-
${lastModified}
|
|
8660
|
-
${etag}
|
|
8661
|
-
${contentType}
|
|
8662
|
-
</D:prop>
|
|
8663
|
-
<D:status>HTTP/1.1 200 OK</D:status>
|
|
8664
|
-
</D:propstat>
|
|
8665
|
-
</D:response>`;
|
|
8666
|
-
}
|
|
8667
|
-
function generatePropfindResponse(entries) {
|
|
8668
|
-
const responseEntries = entries.map(
|
|
8669
|
-
(e) => generatePropfindEntry(e.href, e.stats, e.isCollection)
|
|
8670
|
-
).join("\n");
|
|
8671
|
-
return `<?xml version="1.0" encoding="utf-8"?>
|
|
8672
|
-
<D:multistatus xmlns:D="DAV:">
|
|
8673
|
-
${responseEntries}
|
|
8674
|
-
</D:multistatus>`;
|
|
8675
|
-
}
|
|
8676
|
-
function handleOptions(res, prefix) {
|
|
8677
|
-
res.writeHead(200, {
|
|
8678
|
-
"Allow": SUPPORTED_METHODS.join(", "),
|
|
8679
|
-
"DAV": "1, 2",
|
|
8680
|
-
"Content-Length": "0",
|
|
8681
|
-
"Access-Control-Allow-Origin": "*",
|
|
8682
|
-
"Access-Control-Allow-Methods": SUPPORTED_METHODS.join(", "),
|
|
8683
|
-
"Access-Control-Allow-Headers": "Content-Type, Depth, Destination, Overwrite, Authorization",
|
|
8684
|
-
"MS-Author-Via": "DAV"
|
|
8685
|
-
});
|
|
8686
|
-
res.end();
|
|
8687
|
-
}
|
|
8688
|
-
function handleHead(res, filePath) {
|
|
8689
|
-
try {
|
|
8690
|
-
const stats = fs22.statSync(filePath);
|
|
8691
|
-
if (stats.isDirectory()) {
|
|
8692
|
-
res.writeHead(200, {
|
|
8693
|
-
"Content-Type": "httpd/unix-directory",
|
|
8694
|
-
"Last-Modified": formatWebDAVDate(stats.mtime),
|
|
8695
|
-
"ETag": `"${stats.mtime.getTime().toString(16)}"`,
|
|
8696
|
-
"Access-Control-Allow-Origin": "*"
|
|
8697
|
-
});
|
|
8698
|
-
} else {
|
|
8699
|
-
res.writeHead(200, {
|
|
8700
|
-
"Content-Type": "application/octet-stream",
|
|
8701
|
-
"Content-Length": stats.size.toString(),
|
|
8702
|
-
"Last-Modified": formatWebDAVDate(stats.mtime),
|
|
8703
|
-
"ETag": `"${stats.mtime.getTime().toString(16)}-${stats.size.toString(16)}"`,
|
|
8704
|
-
"Access-Control-Allow-Origin": "*"
|
|
8705
|
-
});
|
|
8706
|
-
}
|
|
8707
|
-
res.end();
|
|
8708
|
-
} catch (err) {
|
|
8709
|
-
res.writeHead(404, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
8710
|
-
res.end("Not Found");
|
|
8711
|
-
}
|
|
8712
|
-
}
|
|
8713
|
-
function handleGet(res, filePath) {
|
|
8714
|
-
try {
|
|
8715
|
-
const stats = fs22.statSync(filePath);
|
|
8716
|
-
if (stats.isDirectory()) {
|
|
8717
|
-
const entries = fs22.readdirSync(filePath);
|
|
8718
|
-
const listing = entries.join("\n");
|
|
8719
|
-
res.writeHead(200, {
|
|
8720
|
-
"Content-Type": "text/plain",
|
|
8721
|
-
"Content-Length": Buffer.byteLength(listing).toString(),
|
|
8722
|
-
"Access-Control-Allow-Origin": "*"
|
|
8723
|
-
});
|
|
8724
|
-
res.end(listing);
|
|
8725
|
-
} else {
|
|
8726
|
-
const content = fs22.readFileSync(filePath);
|
|
8727
|
-
res.writeHead(200, {
|
|
8728
|
-
"Content-Type": "application/octet-stream",
|
|
8729
|
-
"Content-Length": content.length.toString(),
|
|
8730
|
-
"Last-Modified": formatWebDAVDate(stats.mtime),
|
|
8731
|
-
"ETag": `"${stats.mtime.getTime().toString(16)}-${stats.size.toString(16)}"`,
|
|
8732
|
-
"Access-Control-Allow-Origin": "*"
|
|
8733
|
-
});
|
|
8734
|
-
res.end(content);
|
|
8735
|
-
}
|
|
8736
|
-
} catch (err) {
|
|
8737
|
-
res.writeHead(404, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
8738
|
-
res.end("Not Found");
|
|
8739
|
-
}
|
|
8740
|
-
}
|
|
8741
|
-
function handlePut(res, filePath, body) {
|
|
8742
|
-
try {
|
|
8743
|
-
const exists = fs22.existsSync(filePath);
|
|
8744
|
-
const dir = path22.dirname(filePath);
|
|
8745
|
-
if (!fs22.existsSync(dir)) {
|
|
8746
|
-
fs22.mkdirSync(dir, { recursive: true });
|
|
8747
|
-
}
|
|
8748
|
-
fs22.writeFileSync(filePath, body);
|
|
8749
|
-
const status = exists ? 204 : 201;
|
|
8750
|
-
res.writeHead(status, {
|
|
8751
|
-
"Content-Length": "0",
|
|
8752
|
-
"Access-Control-Allow-Origin": "*"
|
|
8753
|
-
});
|
|
8754
|
-
res.end();
|
|
8755
|
-
} catch (err) {
|
|
8756
|
-
res.writeHead(500, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
8757
|
-
res.end(`Error: ${err}`);
|
|
8758
|
-
}
|
|
8759
|
-
}
|
|
8760
|
-
function handleDelete(res, filePath) {
|
|
8761
|
-
try {
|
|
8762
|
-
if (!fs22.existsSync(filePath)) {
|
|
8763
|
-
res.writeHead(404, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
8764
|
-
res.end("Not Found");
|
|
8765
|
-
return;
|
|
8766
|
-
}
|
|
8767
|
-
const stats = fs22.statSync(filePath);
|
|
8768
|
-
if (stats.isDirectory()) {
|
|
8769
|
-
fs22.rmSync(filePath, { recursive: true });
|
|
8770
|
-
} else {
|
|
8771
|
-
fs22.unlinkSync(filePath);
|
|
8772
|
-
}
|
|
8773
|
-
res.writeHead(204, {
|
|
8774
|
-
"Content-Length": "0",
|
|
8775
|
-
"Access-Control-Allow-Origin": "*"
|
|
8776
|
-
});
|
|
8777
|
-
res.end();
|
|
8778
|
-
} catch (err) {
|
|
8779
|
-
res.writeHead(500, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
8780
|
-
res.end(`Error: ${err}`);
|
|
8781
|
-
}
|
|
8782
|
-
}
|
|
8783
|
-
function handleMkcol(res, filePath) {
|
|
8784
|
-
try {
|
|
8785
|
-
if (fs22.existsSync(filePath)) {
|
|
8786
|
-
res.writeHead(405, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
8787
|
-
res.end("Resource already exists");
|
|
8788
|
-
return;
|
|
8789
|
-
}
|
|
8790
|
-
const parent = path22.dirname(filePath);
|
|
8791
|
-
if (!fs22.existsSync(parent)) {
|
|
8792
|
-
res.writeHead(409, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
8793
|
-
res.end("Parent directory does not exist");
|
|
8794
|
-
return;
|
|
8795
|
-
}
|
|
8796
|
-
fs22.mkdirSync(filePath);
|
|
8797
|
-
res.writeHead(201, {
|
|
8798
|
-
"Content-Length": "0",
|
|
8799
|
-
"Access-Control-Allow-Origin": "*"
|
|
8800
|
-
});
|
|
8801
|
-
res.end();
|
|
8802
|
-
} catch (err) {
|
|
8803
|
-
res.writeHead(500, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
8804
|
-
res.end(`Error: ${err}`);
|
|
8805
|
-
}
|
|
8806
|
-
}
|
|
8807
|
-
function handlePropfind(res, filePath, webdavPath, prefix, depth) {
|
|
8808
|
-
try {
|
|
8809
|
-
if (!fs22.existsSync(filePath)) {
|
|
8810
|
-
res.writeHead(404, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
8811
|
-
res.end("Not Found");
|
|
8812
|
-
return;
|
|
8813
|
-
}
|
|
8814
|
-
const stats = fs22.statSync(filePath);
|
|
8815
|
-
const entries = [];
|
|
8816
|
-
const normalizedWebdavPath = webdavPath.startsWith("/") ? webdavPath : "/" + webdavPath;
|
|
8817
|
-
const href = prefix + normalizedWebdavPath;
|
|
8818
|
-
entries.push({
|
|
8819
|
-
href: href.endsWith("/") || stats.isDirectory() ? href : href,
|
|
8820
|
-
stats,
|
|
8821
|
-
isCollection: stats.isDirectory()
|
|
8822
|
-
});
|
|
8823
|
-
if (stats.isDirectory() && depth !== "0") {
|
|
8824
|
-
try {
|
|
8825
|
-
const children = fs22.readdirSync(filePath);
|
|
8826
|
-
for (const child of children) {
|
|
8827
|
-
if (BLOCKED_PATHS.includes(child)) {
|
|
8828
|
-
continue;
|
|
8829
|
-
}
|
|
8830
|
-
const childPath = path22.join(filePath, child);
|
|
8831
|
-
const childWebdavPath = normalizedWebdavPath.endsWith("/") ? normalizedWebdavPath + child : normalizedWebdavPath + "/" + child;
|
|
8832
|
-
try {
|
|
8833
|
-
const childStats = fs22.statSync(childPath);
|
|
8834
|
-
entries.push({
|
|
8835
|
-
href: prefix + childWebdavPath,
|
|
8836
|
-
stats: childStats,
|
|
8837
|
-
isCollection: childStats.isDirectory()
|
|
8838
|
-
});
|
|
8839
|
-
} catch {
|
|
8840
|
-
}
|
|
8841
|
-
}
|
|
8842
|
-
} catch {
|
|
8843
|
-
}
|
|
8844
|
-
}
|
|
8845
|
-
const xml = generatePropfindResponse(entries);
|
|
8846
|
-
res.writeHead(207, {
|
|
8847
|
-
"Content-Type": "application/xml; charset=utf-8",
|
|
8848
|
-
"Content-Length": Buffer.byteLength(xml).toString(),
|
|
8849
|
-
"Access-Control-Allow-Origin": "*"
|
|
8850
|
-
});
|
|
8851
|
-
res.end(xml);
|
|
8852
|
-
} catch (err) {
|
|
8853
|
-
res.writeHead(500, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
8854
|
-
res.end(`Error: ${err}`);
|
|
8855
|
-
}
|
|
8856
|
-
}
|
|
8857
|
-
function handleMove(res, sourcePath, destinationPath, overwrite) {
|
|
8858
|
-
try {
|
|
8859
|
-
if (!fs22.existsSync(sourcePath)) {
|
|
8860
|
-
res.writeHead(404, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
8861
|
-
res.end("Source not found");
|
|
8862
|
-
return;
|
|
8863
|
-
}
|
|
8864
|
-
if (!destinationPath) {
|
|
8865
|
-
res.writeHead(400, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
8866
|
-
res.end("Destination header required");
|
|
8867
|
-
return;
|
|
8868
|
-
}
|
|
8869
|
-
const destExists = fs22.existsSync(destinationPath);
|
|
8870
|
-
if (destExists && !overwrite) {
|
|
8871
|
-
res.writeHead(412, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
8872
|
-
res.end("Destination exists and Overwrite is F");
|
|
8873
|
-
return;
|
|
8874
|
-
}
|
|
8875
|
-
const destDir = path22.dirname(destinationPath);
|
|
8876
|
-
if (!fs22.existsSync(destDir)) {
|
|
8877
|
-
fs22.mkdirSync(destDir, { recursive: true });
|
|
8878
|
-
}
|
|
8879
|
-
if (destExists) {
|
|
8880
|
-
const destStats = fs22.statSync(destinationPath);
|
|
8881
|
-
if (destStats.isDirectory()) {
|
|
8882
|
-
fs22.rmSync(destinationPath, { recursive: true });
|
|
8883
|
-
} else {
|
|
8884
|
-
fs22.unlinkSync(destinationPath);
|
|
8885
|
-
}
|
|
8886
|
-
}
|
|
8887
|
-
fs22.renameSync(sourcePath, destinationPath);
|
|
8888
|
-
const status = destExists ? 204 : 201;
|
|
8889
|
-
res.writeHead(status, {
|
|
8890
|
-
"Content-Length": "0",
|
|
8891
|
-
"Access-Control-Allow-Origin": "*"
|
|
8892
|
-
});
|
|
8893
|
-
res.end();
|
|
8894
|
-
} catch (err) {
|
|
8895
|
-
res.writeHead(500, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
8896
|
-
res.end(`Error: ${err}`);
|
|
8897
|
-
}
|
|
8898
|
-
}
|
|
8899
|
-
function handleCopy(res, sourcePath, destinationPath, overwrite) {
|
|
8900
|
-
try {
|
|
8901
|
-
if (!fs22.existsSync(sourcePath)) {
|
|
8902
|
-
res.writeHead(404, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
8903
|
-
res.end("Source not found");
|
|
8904
|
-
return;
|
|
8905
|
-
}
|
|
8906
|
-
if (!destinationPath) {
|
|
8907
|
-
res.writeHead(400, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
8908
|
-
res.end("Destination header required");
|
|
8909
|
-
return;
|
|
8910
|
-
}
|
|
8911
|
-
const destExists = fs22.existsSync(destinationPath);
|
|
8912
|
-
if (destExists && !overwrite) {
|
|
8913
|
-
res.writeHead(412, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
8914
|
-
res.end("Destination exists and Overwrite is F");
|
|
8915
|
-
return;
|
|
8916
|
-
}
|
|
8917
|
-
const destDir = path22.dirname(destinationPath);
|
|
8918
|
-
if (!fs22.existsSync(destDir)) {
|
|
8919
|
-
fs22.mkdirSync(destDir, { recursive: true });
|
|
8920
|
-
}
|
|
8921
|
-
const sourceStats = fs22.statSync(sourcePath);
|
|
8922
|
-
if (sourceStats.isDirectory()) {
|
|
8923
|
-
copyDirRecursive(sourcePath, destinationPath);
|
|
8924
|
-
} else {
|
|
8925
|
-
fs22.copyFileSync(sourcePath, destinationPath);
|
|
8926
|
-
}
|
|
8927
|
-
const status = destExists ? 204 : 201;
|
|
8928
|
-
res.writeHead(status, {
|
|
8929
|
-
"Content-Length": "0",
|
|
8930
|
-
"Access-Control-Allow-Origin": "*"
|
|
8931
|
-
});
|
|
8932
|
-
res.end();
|
|
8933
|
-
} catch (err) {
|
|
8934
|
-
res.writeHead(500, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
8935
|
-
res.end(`Error: ${err}`);
|
|
8936
|
-
}
|
|
8937
|
-
}
|
|
8938
|
-
function copyDirRecursive(src, dest) {
|
|
8939
|
-
if (!fs22.existsSync(dest)) {
|
|
8940
|
-
fs22.mkdirSync(dest, { recursive: true });
|
|
8941
|
-
}
|
|
8942
|
-
const entries = fs22.readdirSync(src, { withFileTypes: true });
|
|
8943
|
-
for (const entry of entries) {
|
|
8944
|
-
const srcPath = path22.join(src, entry.name);
|
|
8945
|
-
const destPath = path22.join(dest, entry.name);
|
|
8946
|
-
if (entry.isDirectory()) {
|
|
8947
|
-
copyDirRecursive(srcPath, destPath);
|
|
8948
|
-
} else {
|
|
8949
|
-
fs22.copyFileSync(srcPath, destPath);
|
|
8950
|
-
}
|
|
8951
|
-
}
|
|
8952
|
-
}
|
|
8953
|
-
function parseDestinationHeader(destinationHeader, prefix, rootPath) {
|
|
8954
|
-
if (!destinationHeader) {
|
|
8955
|
-
return null;
|
|
8956
|
-
}
|
|
8957
|
-
try {
|
|
8958
|
-
let destPath;
|
|
8959
|
-
if (destinationHeader.startsWith("http://") || destinationHeader.startsWith("https://")) {
|
|
8960
|
-
const url = new URL(destinationHeader);
|
|
8961
|
-
destPath = decodeURIComponent(url.pathname);
|
|
8962
|
-
} else {
|
|
8963
|
-
destPath = decodeURIComponent(destinationHeader);
|
|
8964
|
-
}
|
|
8965
|
-
if (destPath.startsWith(prefix)) {
|
|
8966
|
-
destPath = destPath.slice(prefix.length);
|
|
8967
|
-
}
|
|
8968
|
-
return resolveWebDAVPath(destPath, rootPath);
|
|
8969
|
-
} catch {
|
|
8970
|
-
return null;
|
|
8971
|
-
}
|
|
8972
|
-
}
|
|
8973
|
-
function createWebDAVHandler(config) {
|
|
8974
|
-
const { rootPath, prefix = WEBDAV_PREFIX, auth } = config;
|
|
8975
|
-
return async (req, res) => {
|
|
8976
|
-
const rawUrl = req.url || "/";
|
|
8977
|
-
if (rawUrl.includes("..")) {
|
|
8978
|
-
if (rawUrl.startsWith(prefix)) {
|
|
8979
|
-
res.writeHead(403, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
8980
|
-
res.end("Forbidden");
|
|
8981
|
-
return true;
|
|
8982
|
-
}
|
|
8983
|
-
}
|
|
8984
|
-
const url = new URL(rawUrl, `http://${req.headers.host || "localhost"}`);
|
|
8985
|
-
const pathname = decodeURIComponent(url.pathname);
|
|
8986
|
-
if (!pathname.startsWith(prefix)) {
|
|
8987
|
-
return false;
|
|
8988
|
-
}
|
|
8989
|
-
let webdavPath = pathname.slice(prefix.length);
|
|
8990
|
-
if (!webdavPath.startsWith("/")) {
|
|
8991
|
-
webdavPath = "/" + webdavPath;
|
|
8992
|
-
}
|
|
8993
|
-
if (req.method === "OPTIONS") {
|
|
8994
|
-
handleOptions(res, prefix);
|
|
8995
|
-
return true;
|
|
8996
|
-
}
|
|
8997
|
-
if (!checkAuth(req, auth)) {
|
|
8998
|
-
res.writeHead(401, {
|
|
8999
|
-
"WWW-Authenticate": 'Basic realm="ClawVault WebDAV"',
|
|
9000
|
-
"Content-Type": "text/plain",
|
|
9001
|
-
"Access-Control-Allow-Origin": "*"
|
|
9002
|
-
});
|
|
9003
|
-
res.end("Unauthorized");
|
|
9004
|
-
return true;
|
|
9005
|
-
}
|
|
9006
|
-
if (!isPathSafe(webdavPath, rootPath)) {
|
|
9007
|
-
res.writeHead(403, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
9008
|
-
res.end("Forbidden");
|
|
9009
|
-
return true;
|
|
9010
|
-
}
|
|
9011
|
-
const filePath = resolveWebDAVPath(webdavPath, rootPath);
|
|
9012
|
-
if (!filePath) {
|
|
9013
|
-
res.writeHead(403, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
9014
|
-
res.end("Forbidden");
|
|
9015
|
-
return true;
|
|
9016
|
-
}
|
|
9017
|
-
const depth = req.headers.depth || "infinity";
|
|
9018
|
-
const overwrite = req.headers.overwrite?.toUpperCase() !== "F";
|
|
9019
|
-
const destinationHeader = req.headers.destination;
|
|
9020
|
-
switch (req.method) {
|
|
9021
|
-
case "HEAD":
|
|
9022
|
-
handleHead(res, filePath);
|
|
9023
|
-
return true;
|
|
9024
|
-
case "GET":
|
|
9025
|
-
handleGet(res, filePath);
|
|
9026
|
-
return true;
|
|
9027
|
-
case "PUT": {
|
|
9028
|
-
const chunks = [];
|
|
9029
|
-
for await (const chunk of req) {
|
|
9030
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
9031
|
-
}
|
|
9032
|
-
const body = Buffer.concat(chunks);
|
|
9033
|
-
handlePut(res, filePath, body);
|
|
9034
|
-
return true;
|
|
9035
|
-
}
|
|
9036
|
-
case "DELETE":
|
|
9037
|
-
handleDelete(res, filePath);
|
|
9038
|
-
return true;
|
|
9039
|
-
case "MKCOL":
|
|
9040
|
-
handleMkcol(res, filePath);
|
|
9041
|
-
return true;
|
|
9042
|
-
case "PROPFIND":
|
|
9043
|
-
handlePropfind(res, filePath, webdavPath, prefix, depth);
|
|
9044
|
-
return true;
|
|
9045
|
-
case "MOVE": {
|
|
9046
|
-
const destPath = parseDestinationHeader(destinationHeader, prefix, rootPath);
|
|
9047
|
-
if (destPath && destinationHeader) {
|
|
9048
|
-
const destWebdavPath = destinationHeader.includes(prefix) ? destinationHeader.slice(destinationHeader.indexOf(prefix) + prefix.length) : destinationHeader;
|
|
9049
|
-
if (!isPathSafe(destWebdavPath, rootPath)) {
|
|
9050
|
-
res.writeHead(403, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
9051
|
-
res.end("Forbidden");
|
|
9052
|
-
return true;
|
|
9053
|
-
}
|
|
9054
|
-
}
|
|
9055
|
-
handleMove(res, filePath, destPath, overwrite);
|
|
9056
|
-
return true;
|
|
9057
|
-
}
|
|
9058
|
-
case "COPY": {
|
|
9059
|
-
const destPath = parseDestinationHeader(destinationHeader, prefix, rootPath);
|
|
9060
|
-
if (destPath && destinationHeader) {
|
|
9061
|
-
const destWebdavPath = destinationHeader.includes(prefix) ? destinationHeader.slice(destinationHeader.indexOf(prefix) + prefix.length) : destinationHeader;
|
|
9062
|
-
if (!isPathSafe(destWebdavPath, rootPath)) {
|
|
9063
|
-
res.writeHead(403, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
9064
|
-
res.end("Forbidden");
|
|
9065
|
-
return true;
|
|
9066
|
-
}
|
|
9067
|
-
}
|
|
9068
|
-
handleCopy(res, filePath, destPath, overwrite);
|
|
9069
|
-
return true;
|
|
9070
|
-
}
|
|
9071
|
-
default:
|
|
9072
|
-
res.writeHead(405, {
|
|
9073
|
-
"Allow": SUPPORTED_METHODS.join(", "),
|
|
9074
|
-
"Content-Type": "text/plain",
|
|
9075
|
-
"Access-Control-Allow-Origin": "*"
|
|
9076
|
-
});
|
|
9077
|
-
res.end("Method Not Allowed");
|
|
9078
|
-
return true;
|
|
9079
|
-
}
|
|
9080
|
-
};
|
|
9081
|
-
}
|
|
9082
|
-
|
|
9083
|
-
// src/lib/tailscale.ts
|
|
9084
|
-
var crypto = __toESM(require("crypto"), 1);
|
|
9085
|
-
var DEFAULT_SERVE_PORT = 8384;
|
|
9086
|
-
var CLAWVAULT_SERVE_PATH = "/.clawvault";
|
|
9087
|
-
function hasTailscale() {
|
|
9088
|
-
const probe = (0, import_child_process4.spawnSync)("tailscale", ["version"], {
|
|
9089
|
-
stdio: "pipe",
|
|
9090
|
-
encoding: "utf-8",
|
|
9091
|
-
timeout: 5e3
|
|
9092
|
-
});
|
|
9093
|
-
return !probe.error && probe.status === 0;
|
|
9094
|
-
}
|
|
9095
|
-
function getTailscaleVersion() {
|
|
9096
|
-
const result = (0, import_child_process4.spawnSync)("tailscale", ["version"], {
|
|
9097
|
-
stdio: "pipe",
|
|
9098
|
-
encoding: "utf-8",
|
|
9099
|
-
timeout: 5e3
|
|
9100
|
-
});
|
|
9101
|
-
if (result.error || result.status !== 0) {
|
|
9102
|
-
return null;
|
|
9103
|
-
}
|
|
9104
|
-
const lines = result.stdout.trim().split("\n");
|
|
9105
|
-
return lines[0] || null;
|
|
9106
|
-
}
|
|
9107
|
-
function getTailscaleStatus() {
|
|
9108
|
-
const status = {
|
|
9109
|
-
installed: false,
|
|
9110
|
-
running: false,
|
|
9111
|
-
connected: false,
|
|
9112
|
-
peers: []
|
|
9113
|
-
};
|
|
9114
|
-
if (!hasTailscale()) {
|
|
9115
|
-
status.error = "Tailscale CLI not found. Install from https://tailscale.com/download";
|
|
9116
|
-
return status;
|
|
9117
|
-
}
|
|
9118
|
-
status.installed = true;
|
|
9119
|
-
const result = (0, import_child_process4.spawnSync)("tailscale", ["status", "--json"], {
|
|
9120
|
-
stdio: "pipe",
|
|
9121
|
-
encoding: "utf-8",
|
|
9122
|
-
timeout: 1e4
|
|
9123
|
-
});
|
|
9124
|
-
if (result.error) {
|
|
9125
|
-
status.error = `Failed to get Tailscale status: ${result.error.message}`;
|
|
9126
|
-
return status;
|
|
9127
|
-
}
|
|
9128
|
-
if (result.status !== 0) {
|
|
9129
|
-
status.error = result.stderr?.trim() || "Tailscale daemon not running";
|
|
9130
|
-
return status;
|
|
9131
|
-
}
|
|
9132
|
-
try {
|
|
9133
|
-
const data = JSON.parse(result.stdout);
|
|
9134
|
-
status.running = true;
|
|
9135
|
-
status.backendState = data.BackendState;
|
|
9136
|
-
status.connected = data.BackendState === "Running";
|
|
9137
|
-
status.tailnetName = data.CurrentTailnet?.Name;
|
|
9138
|
-
if (data.Self) {
|
|
9139
|
-
status.selfIP = data.Self.TailscaleIPs?.[0];
|
|
9140
|
-
status.selfHostname = data.Self.HostName;
|
|
9141
|
-
status.selfDNSName = data.Self.DNSName;
|
|
9142
|
-
}
|
|
9143
|
-
if (data.Peer) {
|
|
9144
|
-
for (const [_, peerData] of Object.entries(data.Peer)) {
|
|
9145
|
-
const peer = {
|
|
9146
|
-
hostname: peerData.HostName || "",
|
|
9147
|
-
dnsName: peerData.DNSName || "",
|
|
9148
|
-
tailscaleIPs: peerData.TailscaleIPs || [],
|
|
9149
|
-
online: peerData.Online || false,
|
|
9150
|
-
os: peerData.OS,
|
|
9151
|
-
exitNode: peerData.ExitNode,
|
|
9152
|
-
tags: peerData.Tags,
|
|
9153
|
-
lastSeen: peerData.LastSeen
|
|
9154
|
-
};
|
|
9155
|
-
status.peers.push(peer);
|
|
9156
|
-
}
|
|
9157
|
-
}
|
|
9158
|
-
} catch (err) {
|
|
9159
|
-
status.error = `Failed to parse Tailscale status: ${err}`;
|
|
9160
|
-
}
|
|
9161
|
-
return status;
|
|
9162
|
-
}
|
|
9163
|
-
function findPeer(hostname) {
|
|
9164
|
-
const status = getTailscaleStatus();
|
|
9165
|
-
if (!status.connected) {
|
|
9166
|
-
return null;
|
|
9167
|
-
}
|
|
9168
|
-
const normalizedSearch = hostname.toLowerCase();
|
|
9169
|
-
let peer = status.peers.find(
|
|
9170
|
-
(p) => p.hostname.toLowerCase() === normalizedSearch
|
|
9171
|
-
);
|
|
9172
|
-
if (peer) return peer;
|
|
9173
|
-
peer = status.peers.find(
|
|
9174
|
-
(p) => p.dnsName.toLowerCase().startsWith(normalizedSearch)
|
|
9175
|
-
);
|
|
9176
|
-
if (peer) return peer;
|
|
9177
|
-
peer = status.peers.find(
|
|
9178
|
-
(p) => p.hostname.toLowerCase().includes(normalizedSearch)
|
|
9179
|
-
);
|
|
9180
|
-
return peer || null;
|
|
9181
|
-
}
|
|
9182
|
-
function getOnlinePeers() {
|
|
9183
|
-
const status = getTailscaleStatus();
|
|
9184
|
-
return status.peers.filter((p) => p.online);
|
|
9185
|
-
}
|
|
9186
|
-
function resolvePeerIP(hostname) {
|
|
9187
|
-
const peer = findPeer(hostname);
|
|
9188
|
-
return peer?.tailscaleIPs[0] || null;
|
|
9189
|
-
}
|
|
9190
|
-
function calculateChecksum(filePath) {
|
|
9191
|
-
const content = fs23.readFileSync(filePath);
|
|
9192
|
-
return crypto.createHash("sha256").update(content).digest("hex");
|
|
9193
|
-
}
|
|
9194
|
-
function generateVaultManifest(vaultPath) {
|
|
9195
|
-
const configPath = path23.join(vaultPath, ".clawvault.json");
|
|
9196
|
-
if (!fs23.existsSync(configPath)) {
|
|
9197
|
-
throw new Error(`Not a ClawVault: ${vaultPath}`);
|
|
9198
|
-
}
|
|
9199
|
-
const config = JSON.parse(fs23.readFileSync(configPath, "utf-8"));
|
|
9200
|
-
const files = [];
|
|
9201
|
-
function walkDir(dir, relativePath = "") {
|
|
9202
|
-
const entries = fs23.readdirSync(dir, { withFileTypes: true });
|
|
9203
|
-
for (const entry of entries) {
|
|
9204
|
-
const fullPath = path23.join(dir, entry.name);
|
|
9205
|
-
const relPath = path23.join(relativePath, entry.name);
|
|
9206
|
-
if (entry.name.startsWith(".") && entry.name !== ".clawvault.json") {
|
|
9207
|
-
continue;
|
|
9208
|
-
}
|
|
9209
|
-
if (entry.name === "node_modules") {
|
|
9210
|
-
continue;
|
|
9211
|
-
}
|
|
9212
|
-
if (entry.isDirectory()) {
|
|
9213
|
-
walkDir(fullPath, relPath);
|
|
9214
|
-
} else if (entry.isFile() && (entry.name.endsWith(".md") || entry.name === ".clawvault.json")) {
|
|
9215
|
-
const stats = fs23.statSync(fullPath);
|
|
9216
|
-
const category = relativePath.split(path23.sep)[0] || "root";
|
|
9217
|
-
files.push({
|
|
9218
|
-
path: relPath,
|
|
9219
|
-
size: stats.size,
|
|
9220
|
-
modified: stats.mtime.toISOString(),
|
|
9221
|
-
checksum: calculateChecksum(fullPath),
|
|
9222
|
-
category
|
|
9223
|
-
});
|
|
9224
|
-
}
|
|
9225
|
-
}
|
|
9226
|
-
}
|
|
9227
|
-
walkDir(vaultPath);
|
|
9228
|
-
return {
|
|
9229
|
-
name: config.name,
|
|
9230
|
-
version: config.version || "1.0.0",
|
|
9231
|
-
lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9232
|
-
files
|
|
9233
|
-
};
|
|
9234
|
-
}
|
|
9235
|
-
function compareManifests(local, remote) {
|
|
9236
|
-
const localFiles = new Map(local.files.map((f) => [f.path, f]));
|
|
9237
|
-
const remoteFiles = new Map(remote.files.map((f) => [f.path, f]));
|
|
9238
|
-
const toPush = [];
|
|
9239
|
-
const toPull = [];
|
|
9240
|
-
const conflicts = [];
|
|
9241
|
-
const unchanged = [];
|
|
9242
|
-
for (const [filePath, localFile] of localFiles) {
|
|
9243
|
-
const remoteFile = remoteFiles.get(filePath);
|
|
9244
|
-
if (!remoteFile) {
|
|
9245
|
-
toPush.push(localFile);
|
|
9246
|
-
} else if (localFile.checksum === remoteFile.checksum) {
|
|
9247
|
-
unchanged.push(filePath);
|
|
9248
|
-
} else {
|
|
9249
|
-
const localTime = new Date(localFile.modified).getTime();
|
|
9250
|
-
const remoteTime = new Date(remoteFile.modified).getTime();
|
|
9251
|
-
if (localTime > remoteTime) {
|
|
9252
|
-
toPush.push(localFile);
|
|
9253
|
-
} else if (remoteTime > localTime) {
|
|
9254
|
-
toPull.push(remoteFile);
|
|
9255
|
-
} else {
|
|
9256
|
-
conflicts.push({ path: filePath, local: localFile, remote: remoteFile });
|
|
9257
|
-
}
|
|
9258
|
-
}
|
|
9259
|
-
}
|
|
9260
|
-
for (const [filePath, remoteFile] of remoteFiles) {
|
|
9261
|
-
if (!localFiles.has(filePath)) {
|
|
9262
|
-
toPull.push(remoteFile);
|
|
9263
|
-
}
|
|
9264
|
-
}
|
|
9265
|
-
return { toPush, toPull, conflicts, unchanged };
|
|
9266
|
-
}
|
|
9267
|
-
function serveVault(vaultPath, options = {}) {
|
|
9268
|
-
const port = options.port || DEFAULT_SERVE_PORT;
|
|
9269
|
-
const pathPrefix = options.pathPrefix || CLAWVAULT_SERVE_PATH;
|
|
9270
|
-
if (!fs23.existsSync(path23.join(vaultPath, ".clawvault.json"))) {
|
|
9271
|
-
throw new Error(`Not a ClawVault: ${vaultPath}`);
|
|
9272
|
-
}
|
|
9273
|
-
const webdavHandler = createWebDAVHandler({
|
|
9274
|
-
rootPath: vaultPath,
|
|
9275
|
-
prefix: WEBDAV_PREFIX,
|
|
9276
|
-
auth: options.webdavAuth
|
|
9277
|
-
});
|
|
9278
|
-
const server = http.createServer(async (req, res) => {
|
|
9279
|
-
const url = new URL(req.url || "/", `http://localhost:${port}`);
|
|
9280
|
-
const pathname = url.pathname;
|
|
9281
|
-
if (pathname.startsWith(WEBDAV_PREFIX)) {
|
|
9282
|
-
try {
|
|
9283
|
-
const handled = await webdavHandler(req, res);
|
|
9284
|
-
if (handled) return;
|
|
9285
|
-
} catch (err) {
|
|
9286
|
-
res.writeHead(500, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
9287
|
-
res.end(`WebDAV Error: ${err}`);
|
|
9288
|
-
return;
|
|
9289
|
-
}
|
|
9290
|
-
}
|
|
9291
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
9292
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
9293
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
9294
|
-
if (req.method === "OPTIONS") {
|
|
9295
|
-
res.writeHead(200);
|
|
9296
|
-
res.end();
|
|
9297
|
-
return;
|
|
9298
|
-
}
|
|
9299
|
-
if (pathname === `${pathPrefix}/health`) {
|
|
9300
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
9301
|
-
res.end(JSON.stringify({ status: "ok", vault: path23.basename(vaultPath) }));
|
|
9302
|
-
return;
|
|
9303
|
-
}
|
|
9304
|
-
if (pathname === `${pathPrefix}/manifest`) {
|
|
9305
|
-
try {
|
|
9306
|
-
const manifest = generateVaultManifest(vaultPath);
|
|
9307
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
9308
|
-
res.end(JSON.stringify(manifest));
|
|
9309
|
-
} catch (err) {
|
|
9310
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
9311
|
-
res.end(JSON.stringify({ error: String(err) }));
|
|
9312
|
-
}
|
|
9313
|
-
return;
|
|
9314
|
-
}
|
|
9315
|
-
if (pathname.startsWith(`${pathPrefix}/files/`)) {
|
|
9316
|
-
const relativePath = decodeURIComponent(pathname.slice(`${pathPrefix}/files/`.length));
|
|
9317
|
-
const filePath = path23.join(vaultPath, relativePath);
|
|
9318
|
-
const resolvedPath = path23.resolve(filePath);
|
|
9319
|
-
const resolvedVault = path23.resolve(vaultPath);
|
|
9320
|
-
if (!resolvedPath.startsWith(resolvedVault)) {
|
|
9321
|
-
res.writeHead(403, { "Content-Type": "application/json" });
|
|
9322
|
-
res.end(JSON.stringify({ error: "Access denied" }));
|
|
9323
|
-
return;
|
|
9324
|
-
}
|
|
9325
|
-
if (!fs23.existsSync(filePath)) {
|
|
9326
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
9327
|
-
res.end(JSON.stringify({ error: "File not found" }));
|
|
9328
|
-
return;
|
|
9329
|
-
}
|
|
9330
|
-
try {
|
|
9331
|
-
const content = fs23.readFileSync(filePath, "utf-8");
|
|
9332
|
-
const stats = fs23.statSync(filePath);
|
|
9333
|
-
res.writeHead(200, {
|
|
9334
|
-
"Content-Type": "text/markdown",
|
|
9335
|
-
"Content-Length": Buffer.byteLength(content),
|
|
9336
|
-
"Last-Modified": stats.mtime.toUTCString()
|
|
9337
|
-
});
|
|
9338
|
-
res.end(content);
|
|
9339
|
-
} catch (err) {
|
|
9340
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
9341
|
-
res.end(JSON.stringify({ error: String(err) }));
|
|
9342
|
-
}
|
|
9343
|
-
return;
|
|
9344
|
-
}
|
|
9345
|
-
if (pathname.startsWith(`${pathPrefix}/upload/`) && req.method === "POST") {
|
|
9346
|
-
const relativePath = decodeURIComponent(pathname.slice(`${pathPrefix}/upload/`.length));
|
|
9347
|
-
const filePath = path23.join(vaultPath, relativePath);
|
|
9348
|
-
const resolvedPath = path23.resolve(filePath);
|
|
9349
|
-
const resolvedVault = path23.resolve(vaultPath);
|
|
9350
|
-
if (!resolvedPath.startsWith(resolvedVault)) {
|
|
9351
|
-
res.writeHead(403, { "Content-Type": "application/json" });
|
|
9352
|
-
res.end(JSON.stringify({ error: "Access denied" }));
|
|
9353
|
-
return;
|
|
9354
|
-
}
|
|
9355
|
-
let body = "";
|
|
9356
|
-
req.on("data", (chunk) => {
|
|
9357
|
-
body += chunk;
|
|
9358
|
-
});
|
|
9359
|
-
req.on("end", () => {
|
|
9360
|
-
try {
|
|
9361
|
-
const dir = path23.dirname(filePath);
|
|
9362
|
-
if (!fs23.existsSync(dir)) {
|
|
9363
|
-
fs23.mkdirSync(dir, { recursive: true });
|
|
9364
|
-
}
|
|
9365
|
-
fs23.writeFileSync(filePath, body, "utf-8");
|
|
9366
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
9367
|
-
res.end(JSON.stringify({ success: true, path: relativePath }));
|
|
9368
|
-
} catch (err) {
|
|
9369
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
9370
|
-
res.end(JSON.stringify({ error: String(err) }));
|
|
9371
|
-
}
|
|
9372
|
-
});
|
|
9373
|
-
return;
|
|
9374
|
-
}
|
|
9375
|
-
if (pathname === pathPrefix || pathname === `${pathPrefix}/`) {
|
|
9376
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
9377
|
-
res.end(JSON.stringify({
|
|
9378
|
-
service: "clawvault-sync",
|
|
9379
|
-
version: "1.0.0",
|
|
9380
|
-
vault: path23.basename(vaultPath),
|
|
9381
|
-
endpoints: {
|
|
9382
|
-
health: `${pathPrefix}/health`,
|
|
9383
|
-
manifest: `${pathPrefix}/manifest`,
|
|
9384
|
-
files: `${pathPrefix}/files/<path>`,
|
|
9385
|
-
upload: `${pathPrefix}/upload/<path>`,
|
|
9386
|
-
webdav: `${WEBDAV_PREFIX}/`
|
|
9387
|
-
}
|
|
9388
|
-
}));
|
|
9389
|
-
return;
|
|
9390
|
-
}
|
|
9391
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
9392
|
-
res.end(JSON.stringify({ error: "Not found" }));
|
|
9393
|
-
});
|
|
9394
|
-
server.listen(port, "0.0.0.0");
|
|
9395
|
-
return {
|
|
9396
|
-
server,
|
|
9397
|
-
port,
|
|
9398
|
-
stop: () => new Promise((resolve23, reject) => {
|
|
9399
|
-
server.close((err) => {
|
|
9400
|
-
if (err) reject(err);
|
|
9401
|
-
else resolve23();
|
|
9402
|
-
});
|
|
9403
|
-
})
|
|
9404
|
-
};
|
|
9405
|
-
}
|
|
9406
|
-
async function fetchRemoteManifest(host, port = DEFAULT_SERVE_PORT, useHttps = false) {
|
|
9407
|
-
return new Promise((resolve23, reject) => {
|
|
9408
|
-
const protocol = useHttps ? https : http;
|
|
9409
|
-
const url = `${useHttps ? "https" : "http"}://${host}:${port}${CLAWVAULT_SERVE_PATH}/manifest`;
|
|
9410
|
-
const req = protocol.get(url, { timeout: 1e4 }, (res) => {
|
|
9411
|
-
let data = "";
|
|
9412
|
-
res.on("data", (chunk) => {
|
|
9413
|
-
data += chunk;
|
|
9414
|
-
});
|
|
9415
|
-
res.on("end", () => {
|
|
9416
|
-
if (res.statusCode !== 200) {
|
|
9417
|
-
reject(new Error(`Failed to fetch manifest: HTTP ${res.statusCode}`));
|
|
9418
|
-
return;
|
|
9419
|
-
}
|
|
9420
|
-
try {
|
|
9421
|
-
resolve23(JSON.parse(data));
|
|
9422
|
-
} catch (err) {
|
|
9423
|
-
reject(new Error(`Invalid manifest response: ${err}`));
|
|
9424
|
-
}
|
|
9425
|
-
});
|
|
9426
|
-
});
|
|
9427
|
-
req.on("error", reject);
|
|
9428
|
-
req.on("timeout", () => {
|
|
9429
|
-
req.destroy();
|
|
9430
|
-
reject(new Error("Request timed out"));
|
|
9431
|
-
});
|
|
9432
|
-
});
|
|
9433
|
-
}
|
|
9434
|
-
async function fetchRemoteFile(host, filePath, port = DEFAULT_SERVE_PORT, useHttps = false) {
|
|
9435
|
-
return new Promise((resolve23, reject) => {
|
|
9436
|
-
const protocol = useHttps ? https : http;
|
|
9437
|
-
const encodedPath = encodeURIComponent(filePath).replace(/%2F/g, "/");
|
|
9438
|
-
const url = `${useHttps ? "https" : "http"}://${host}:${port}${CLAWVAULT_SERVE_PATH}/files/${encodedPath}`;
|
|
9439
|
-
const req = protocol.get(url, { timeout: 3e4 }, (res) => {
|
|
9440
|
-
let data = "";
|
|
9441
|
-
res.on("data", (chunk) => {
|
|
9442
|
-
data += chunk;
|
|
9443
|
-
});
|
|
9444
|
-
res.on("end", () => {
|
|
9445
|
-
if (res.statusCode !== 200) {
|
|
9446
|
-
reject(new Error(`Failed to fetch file: HTTP ${res.statusCode}`));
|
|
9447
|
-
return;
|
|
9448
|
-
}
|
|
9449
|
-
resolve23(data);
|
|
9450
|
-
});
|
|
9451
|
-
});
|
|
9452
|
-
req.on("error", reject);
|
|
9453
|
-
req.on("timeout", () => {
|
|
9454
|
-
req.destroy();
|
|
9455
|
-
reject(new Error("Request timed out"));
|
|
9456
|
-
});
|
|
9457
|
-
});
|
|
9458
|
-
}
|
|
9459
|
-
async function pushFileToRemote(host, filePath, content, port = DEFAULT_SERVE_PORT, useHttps = false) {
|
|
9460
|
-
return new Promise((resolve23, reject) => {
|
|
9461
|
-
const protocol = useHttps ? https : http;
|
|
9462
|
-
const encodedPath = encodeURIComponent(filePath).replace(/%2F/g, "/");
|
|
9463
|
-
const url = new URL(`${useHttps ? "https" : "http"}://${host}:${port}${CLAWVAULT_SERVE_PATH}/upload/${encodedPath}`);
|
|
9464
|
-
const options = {
|
|
9465
|
-
hostname: url.hostname,
|
|
9466
|
-
port: url.port,
|
|
9467
|
-
path: url.pathname,
|
|
9468
|
-
method: "POST",
|
|
9469
|
-
headers: {
|
|
9470
|
-
"Content-Type": "text/markdown",
|
|
9471
|
-
"Content-Length": Buffer.byteLength(content)
|
|
9472
|
-
},
|
|
9473
|
-
timeout: 3e4
|
|
9474
|
-
};
|
|
9475
|
-
const req = protocol.request(options, (res) => {
|
|
9476
|
-
let data = "";
|
|
9477
|
-
res.on("data", (chunk) => {
|
|
9478
|
-
data += chunk;
|
|
9479
|
-
});
|
|
9480
|
-
res.on("end", () => {
|
|
9481
|
-
if (res.statusCode !== 200) {
|
|
9482
|
-
reject(new Error(`Failed to push file: HTTP ${res.statusCode}`));
|
|
9483
|
-
return;
|
|
9484
|
-
}
|
|
9485
|
-
resolve23();
|
|
9486
|
-
});
|
|
9487
|
-
});
|
|
9488
|
-
req.on("error", reject);
|
|
9489
|
-
req.on("timeout", () => {
|
|
9490
|
-
req.destroy();
|
|
9491
|
-
reject(new Error("Request timed out"));
|
|
9492
|
-
});
|
|
9493
|
-
req.write(content);
|
|
9494
|
-
req.end();
|
|
9495
|
-
});
|
|
9496
|
-
}
|
|
9497
|
-
async function syncWithPeer(vaultPath, options) {
|
|
9498
|
-
const startTime = Date.now();
|
|
9499
|
-
const result = {
|
|
9500
|
-
pushed: [],
|
|
9501
|
-
pulled: [],
|
|
9502
|
-
deleted: [],
|
|
9503
|
-
unchanged: [],
|
|
9504
|
-
errors: [],
|
|
9505
|
-
stats: {
|
|
9506
|
-
bytesTransferred: 0,
|
|
9507
|
-
filesProcessed: 0,
|
|
9508
|
-
duration: 0
|
|
9509
|
-
}
|
|
9510
|
-
};
|
|
9511
|
-
const {
|
|
9512
|
-
peer,
|
|
9513
|
-
port = DEFAULT_SERVE_PORT,
|
|
9514
|
-
direction = "bidirectional",
|
|
9515
|
-
dryRun = false,
|
|
9516
|
-
deleteOrphans = false,
|
|
9517
|
-
categories,
|
|
9518
|
-
https: useHttps = false
|
|
9519
|
-
} = options;
|
|
9520
|
-
let host = peer;
|
|
9521
|
-
if (!peer.match(/^\d+\.\d+\.\d+\.\d+$/)) {
|
|
9522
|
-
const resolvedIP = resolvePeerIP(peer);
|
|
9523
|
-
if (!resolvedIP) {
|
|
9524
|
-
result.errors.push(`Could not resolve peer: ${peer}`);
|
|
9525
|
-
result.stats.duration = Date.now() - startTime;
|
|
9526
|
-
return result;
|
|
9527
|
-
}
|
|
9528
|
-
host = resolvedIP;
|
|
9529
|
-
}
|
|
9530
|
-
try {
|
|
9531
|
-
const localManifest = generateVaultManifest(vaultPath);
|
|
9532
|
-
const remoteManifest = await fetchRemoteManifest(host, port, useHttps);
|
|
9533
|
-
let { toPush, toPull, conflicts, unchanged } = compareManifests(localManifest, remoteManifest);
|
|
9534
|
-
if (categories && categories.length > 0) {
|
|
9535
|
-
const categorySet = new Set(categories);
|
|
9536
|
-
toPush = toPush.filter((f) => categorySet.has(f.category));
|
|
9537
|
-
toPull = toPull.filter((f) => categorySet.has(f.category));
|
|
9538
|
-
}
|
|
9539
|
-
result.unchanged = unchanged;
|
|
9540
|
-
for (const conflict of conflicts) {
|
|
9541
|
-
result.errors.push(`Conflict: ${conflict.path} (local and remote have same timestamp but different content)`);
|
|
9542
|
-
}
|
|
9543
|
-
if (direction === "push" || direction === "bidirectional") {
|
|
9544
|
-
for (const file of toPush) {
|
|
9545
|
-
try {
|
|
9546
|
-
if (!dryRun) {
|
|
9547
|
-
const content = fs23.readFileSync(path23.join(vaultPath, file.path), "utf-8");
|
|
9548
|
-
await pushFileToRemote(host, file.path, content, port, useHttps);
|
|
9549
|
-
result.stats.bytesTransferred += file.size;
|
|
9550
|
-
}
|
|
9551
|
-
result.pushed.push(file.path);
|
|
9552
|
-
result.stats.filesProcessed++;
|
|
9553
|
-
} catch (err) {
|
|
9554
|
-
result.errors.push(`Failed to push ${file.path}: ${err}`);
|
|
9555
|
-
}
|
|
9556
|
-
}
|
|
9557
|
-
}
|
|
9558
|
-
if (direction === "pull" || direction === "bidirectional") {
|
|
9559
|
-
for (const file of toPull) {
|
|
9560
|
-
try {
|
|
9561
|
-
if (!dryRun) {
|
|
9562
|
-
const content = await fetchRemoteFile(host, file.path, port, useHttps);
|
|
9563
|
-
const filePath = path23.join(vaultPath, file.path);
|
|
9564
|
-
const dir = path23.dirname(filePath);
|
|
9565
|
-
if (!fs23.existsSync(dir)) {
|
|
9566
|
-
fs23.mkdirSync(dir, { recursive: true });
|
|
9567
|
-
}
|
|
9568
|
-
fs23.writeFileSync(filePath, content, "utf-8");
|
|
9569
|
-
result.stats.bytesTransferred += file.size;
|
|
9570
|
-
}
|
|
9571
|
-
result.pulled.push(file.path);
|
|
9572
|
-
result.stats.filesProcessed++;
|
|
9573
|
-
} catch (err) {
|
|
9574
|
-
result.errors.push(`Failed to pull ${file.path}: ${err}`);
|
|
9575
|
-
}
|
|
9576
|
-
}
|
|
9577
|
-
}
|
|
9578
|
-
if (deleteOrphans && direction === "pull") {
|
|
9579
|
-
const remoteFiles = new Set(remoteManifest.files.map((f) => f.path));
|
|
9580
|
-
for (const file of localManifest.files) {
|
|
9581
|
-
if (!remoteFiles.has(file.path)) {
|
|
9582
|
-
if (!categories || categories.includes(file.category)) {
|
|
9583
|
-
try {
|
|
9584
|
-
if (!dryRun) {
|
|
9585
|
-
fs23.unlinkSync(path23.join(vaultPath, file.path));
|
|
9586
|
-
}
|
|
9587
|
-
result.deleted.push(file.path);
|
|
9588
|
-
} catch (err) {
|
|
9589
|
-
result.errors.push(`Failed to delete ${file.path}: ${err}`);
|
|
9590
|
-
}
|
|
9591
|
-
}
|
|
9592
|
-
}
|
|
9593
|
-
}
|
|
9594
|
-
}
|
|
9595
|
-
} catch (err) {
|
|
9596
|
-
result.errors.push(`Sync failed: ${err}`);
|
|
9597
|
-
}
|
|
9598
|
-
result.stats.duration = Date.now() - startTime;
|
|
9599
|
-
return result;
|
|
9600
|
-
}
|
|
9601
|
-
function configureTailscaleServe(localPort, options = {}) {
|
|
9602
|
-
if (!hasTailscale()) {
|
|
9603
|
-
return null;
|
|
9604
|
-
}
|
|
9605
|
-
const args = ["serve"];
|
|
9606
|
-
if (options.funnel) {
|
|
9607
|
-
args.push("--bg");
|
|
9608
|
-
args.push("funnel");
|
|
9609
|
-
} else if (options.background) {
|
|
9610
|
-
args.push("--bg");
|
|
9611
|
-
}
|
|
9612
|
-
args.push(`localhost:${localPort}`);
|
|
9613
|
-
const proc = (0, import_child_process4.spawn)("tailscale", args, {
|
|
9614
|
-
stdio: "inherit",
|
|
9615
|
-
detached: options.background
|
|
9616
|
-
});
|
|
9617
|
-
if (options.background) {
|
|
9618
|
-
proc.unref();
|
|
9619
|
-
}
|
|
9620
|
-
return proc;
|
|
9621
|
-
}
|
|
9622
|
-
function stopTailscaleServe() {
|
|
9623
|
-
if (!hasTailscale()) {
|
|
9624
|
-
return false;
|
|
9625
|
-
}
|
|
9626
|
-
const result = (0, import_child_process4.spawnSync)("tailscale", ["serve", "off"], {
|
|
9627
|
-
stdio: "pipe",
|
|
9628
|
-
encoding: "utf-8",
|
|
9629
|
-
timeout: 5e3
|
|
9630
|
-
});
|
|
9631
|
-
return result.status === 0;
|
|
9632
|
-
}
|
|
9633
|
-
async function checkPeerClawVault(host, port = DEFAULT_SERVE_PORT) {
|
|
9634
|
-
try {
|
|
9635
|
-
const response = await new Promise((resolve23) => {
|
|
9636
|
-
const req = http.get(
|
|
9637
|
-
`http://${host}:${port}${CLAWVAULT_SERVE_PATH}/health`,
|
|
9638
|
-
{ timeout: 5e3 },
|
|
9639
|
-
(res) => {
|
|
9640
|
-
resolve23(res.statusCode === 200);
|
|
9641
|
-
}
|
|
9642
|
-
);
|
|
9643
|
-
req.on("error", () => resolve23(false));
|
|
9644
|
-
req.on("timeout", () => {
|
|
9645
|
-
req.destroy();
|
|
9646
|
-
resolve23(false);
|
|
9647
|
-
});
|
|
9648
|
-
});
|
|
9649
|
-
return response;
|
|
9650
|
-
} catch {
|
|
9651
|
-
return false;
|
|
9652
|
-
}
|
|
9653
|
-
}
|
|
9654
|
-
async function discoverClawVaultPeers(port = DEFAULT_SERVE_PORT) {
|
|
9655
|
-
const status = getTailscaleStatus();
|
|
9656
|
-
if (!status.connected) {
|
|
9657
|
-
return [];
|
|
9658
|
-
}
|
|
9659
|
-
const clawvaultPeers = [];
|
|
9660
|
-
const checkPromises = status.peers.filter((p) => p.online).map(async (peer) => {
|
|
9661
|
-
const ip = peer.tailscaleIPs[0];
|
|
9662
|
-
if (!ip) return;
|
|
9663
|
-
const isServing = await checkPeerClawVault(ip, port);
|
|
9664
|
-
if (isServing) {
|
|
9665
|
-
peer.clawvaultServing = true;
|
|
9666
|
-
peer.clawvaultPort = port;
|
|
9667
|
-
clawvaultPeers.push(peer);
|
|
9668
|
-
}
|
|
9669
|
-
});
|
|
9670
|
-
await Promise.all(checkPromises);
|
|
9671
|
-
return clawvaultPeers;
|
|
9672
|
-
}
|
|
9673
|
-
|
|
9674
|
-
// src/commands/tailscale.ts
|
|
9675
|
-
async function tailscaleStatusCommand(options = {}) {
|
|
9676
|
-
const status = getTailscaleStatus();
|
|
9677
|
-
if (options.json) {
|
|
9678
|
-
console.log(JSON.stringify(status, null, 2));
|
|
9679
|
-
return status;
|
|
9680
|
-
}
|
|
9681
|
-
if (!status.installed) {
|
|
9682
|
-
console.log("Tailscale: Not installed");
|
|
9683
|
-
console.log(" Install from: https://tailscale.com/download");
|
|
9684
|
-
return status;
|
|
9685
|
-
}
|
|
9686
|
-
const version = getTailscaleVersion();
|
|
9687
|
-
console.log(`Tailscale: ${version || "installed"}`);
|
|
9688
|
-
if (!status.running) {
|
|
9689
|
-
console.log(" Status: Daemon not running");
|
|
9690
|
-
if (status.error) {
|
|
9691
|
-
console.log(` Error: ${status.error}`);
|
|
9692
|
-
}
|
|
9693
|
-
return status;
|
|
9694
|
-
}
|
|
9695
|
-
console.log(` Status: ${status.backendState}`);
|
|
9696
|
-
if (status.connected) {
|
|
9697
|
-
console.log(` Tailnet: ${status.tailnetName || "unknown"}`);
|
|
9698
|
-
console.log(` Self IP: ${status.selfIP || "unknown"}`);
|
|
9699
|
-
console.log(` Hostname: ${status.selfHostname || "unknown"}`);
|
|
9700
|
-
if (status.selfDNSName) {
|
|
9701
|
-
console.log(` DNS Name: ${status.selfDNSName}`);
|
|
9702
|
-
}
|
|
9703
|
-
if (options.peers || status.peers.length > 0) {
|
|
9704
|
-
const onlinePeers = status.peers.filter((p) => p.online);
|
|
9705
|
-
const offlinePeers = status.peers.filter((p) => !p.online);
|
|
9706
|
-
console.log(`
|
|
9707
|
-
Peers (${onlinePeers.length} online, ${offlinePeers.length} offline):`);
|
|
9708
|
-
for (const peer of onlinePeers) {
|
|
9709
|
-
const ip = peer.tailscaleIPs[0] || "no-ip";
|
|
9710
|
-
const os2 = peer.os ? ` (${peer.os})` : "";
|
|
9711
|
-
const clawvault = peer.clawvaultServing ? " [ClawVault]" : "";
|
|
9712
|
-
console.log(` \u25CF ${peer.hostname}${os2} - ${ip}${clawvault}`);
|
|
9713
|
-
}
|
|
9714
|
-
if (options.peers) {
|
|
9715
|
-
for (const peer of offlinePeers) {
|
|
9716
|
-
const ip = peer.tailscaleIPs[0] || "no-ip";
|
|
9717
|
-
const os2 = peer.os ? ` (${peer.os})` : "";
|
|
9718
|
-
console.log(` \u25CB ${peer.hostname}${os2} - ${ip} [offline]`);
|
|
9719
|
-
}
|
|
9720
|
-
}
|
|
9721
|
-
}
|
|
9722
|
-
} else {
|
|
9723
|
-
console.log(" Status: Not connected to tailnet");
|
|
9724
|
-
if (status.error) {
|
|
9725
|
-
console.log(` Error: ${status.error}`);
|
|
9726
|
-
}
|
|
9727
|
-
}
|
|
9728
|
-
return status;
|
|
9729
|
-
}
|
|
9730
|
-
function registerTailscaleStatusCommand(program) {
|
|
9731
|
-
program.command("tailscale-status").alias("ts-status").description("Show Tailscale connection status and peers").option("--json", "Output as JSON").option("--peers", "Show all peers including offline").action(async (rawOptions) => {
|
|
9732
|
-
await tailscaleStatusCommand({
|
|
9733
|
-
json: rawOptions.json,
|
|
9734
|
-
peers: rawOptions.peers
|
|
9735
|
-
});
|
|
9736
|
-
});
|
|
9737
|
-
}
|
|
9738
|
-
async function tailscaleSyncCommand(options) {
|
|
9739
|
-
const vaultPath = resolveVaultPath({ explicitPath: options.vaultPath });
|
|
9740
|
-
const status = getTailscaleStatus();
|
|
9741
|
-
if (!status.installed) {
|
|
9742
|
-
const error = {
|
|
9743
|
-
pushed: [],
|
|
9744
|
-
pulled: [],
|
|
9745
|
-
deleted: [],
|
|
9746
|
-
unchanged: [],
|
|
9747
|
-
errors: ["Tailscale not installed. Install from https://tailscale.com/download"],
|
|
9748
|
-
stats: { bytesTransferred: 0, filesProcessed: 0, duration: 0 }
|
|
9749
|
-
};
|
|
9750
|
-
if (options.json) {
|
|
9751
|
-
console.log(JSON.stringify(error, null, 2));
|
|
9752
|
-
} else {
|
|
9753
|
-
console.error("Error: Tailscale not installed");
|
|
9754
|
-
}
|
|
9755
|
-
return error;
|
|
9756
|
-
}
|
|
9757
|
-
if (!status.connected) {
|
|
9758
|
-
const error = {
|
|
9759
|
-
pushed: [],
|
|
9760
|
-
pulled: [],
|
|
9761
|
-
deleted: [],
|
|
9762
|
-
unchanged: [],
|
|
9763
|
-
errors: ["Not connected to Tailscale. Run `tailscale up` to connect."],
|
|
9764
|
-
stats: { bytesTransferred: 0, filesProcessed: 0, duration: 0 }
|
|
9765
|
-
};
|
|
9766
|
-
if (options.json) {
|
|
9767
|
-
console.log(JSON.stringify(error, null, 2));
|
|
9768
|
-
} else {
|
|
9769
|
-
console.error("Error: Not connected to Tailscale");
|
|
9770
|
-
}
|
|
9771
|
-
return error;
|
|
9772
|
-
}
|
|
9773
|
-
const peer = findPeer(options.peer);
|
|
9774
|
-
if (!peer) {
|
|
9775
|
-
const error = {
|
|
9776
|
-
pushed: [],
|
|
9777
|
-
pulled: [],
|
|
9778
|
-
deleted: [],
|
|
9779
|
-
unchanged: [],
|
|
9780
|
-
errors: [`Peer not found: ${options.peer}`],
|
|
9781
|
-
stats: { bytesTransferred: 0, filesProcessed: 0, duration: 0 }
|
|
9782
|
-
};
|
|
9783
|
-
if (options.json) {
|
|
9784
|
-
console.log(JSON.stringify(error, null, 2));
|
|
9785
|
-
} else {
|
|
9786
|
-
console.error(`Error: Peer not found: ${options.peer}`);
|
|
9787
|
-
console.log("\nAvailable online peers:");
|
|
9788
|
-
for (const p of getOnlinePeers()) {
|
|
9789
|
-
console.log(` - ${p.hostname} (${p.tailscaleIPs[0]})`);
|
|
9790
|
-
}
|
|
9791
|
-
}
|
|
9792
|
-
return error;
|
|
9793
|
-
}
|
|
9794
|
-
if (!peer.online) {
|
|
9795
|
-
const error = {
|
|
9796
|
-
pushed: [],
|
|
9797
|
-
pulled: [],
|
|
9798
|
-
deleted: [],
|
|
9799
|
-
unchanged: [],
|
|
9800
|
-
errors: [`Peer is offline: ${peer.hostname}`],
|
|
9801
|
-
stats: { bytesTransferred: 0, filesProcessed: 0, duration: 0 }
|
|
9802
|
-
};
|
|
9803
|
-
if (options.json) {
|
|
9804
|
-
console.log(JSON.stringify(error, null, 2));
|
|
9805
|
-
} else {
|
|
9806
|
-
console.error(`Error: Peer is offline: ${peer.hostname}`);
|
|
9807
|
-
}
|
|
9808
|
-
return error;
|
|
9809
|
-
}
|
|
9810
|
-
const syncOptions = {
|
|
9811
|
-
peer: peer.tailscaleIPs[0],
|
|
9812
|
-
port: options.port || DEFAULT_SERVE_PORT,
|
|
9813
|
-
direction: options.direction || "bidirectional",
|
|
9814
|
-
dryRun: options.dryRun,
|
|
9815
|
-
deleteOrphans: options.deleteOrphans,
|
|
9816
|
-
categories: options.categories,
|
|
9817
|
-
https: options.https
|
|
9818
|
-
};
|
|
9819
|
-
if (!options.json && !options.dryRun) {
|
|
9820
|
-
console.log(`Syncing with ${peer.hostname} (${peer.tailscaleIPs[0]})...`);
|
|
9821
|
-
}
|
|
9822
|
-
const result = await syncWithPeer(vaultPath, syncOptions);
|
|
9823
|
-
if (options.json) {
|
|
9824
|
-
console.log(JSON.stringify(result, null, 2));
|
|
9825
|
-
} else {
|
|
9826
|
-
const prefix = options.dryRun ? "[dry-run] " : "";
|
|
9827
|
-
if (result.pushed.length > 0) {
|
|
9828
|
-
console.log(`
|
|
9829
|
-
${prefix}Pushed ${result.pushed.length} file(s):`);
|
|
9830
|
-
for (const file of result.pushed.slice(0, 10)) {
|
|
9831
|
-
console.log(` \u2192 ${file}`);
|
|
9832
|
-
}
|
|
9833
|
-
if (result.pushed.length > 10) {
|
|
9834
|
-
console.log(` ... and ${result.pushed.length - 10} more`);
|
|
9835
|
-
}
|
|
9836
|
-
}
|
|
9837
|
-
if (result.pulled.length > 0) {
|
|
9838
|
-
console.log(`
|
|
9839
|
-
${prefix}Pulled ${result.pulled.length} file(s):`);
|
|
9840
|
-
for (const file of result.pulled.slice(0, 10)) {
|
|
9841
|
-
console.log(` \u2190 ${file}`);
|
|
9842
|
-
}
|
|
9843
|
-
if (result.pulled.length > 10) {
|
|
9844
|
-
console.log(` ... and ${result.pulled.length - 10} more`);
|
|
9845
|
-
}
|
|
9846
|
-
}
|
|
9847
|
-
if (result.deleted.length > 0) {
|
|
9848
|
-
console.log(`
|
|
9849
|
-
${prefix}Deleted ${result.deleted.length} file(s):`);
|
|
9850
|
-
for (const file of result.deleted.slice(0, 10)) {
|
|
9851
|
-
console.log(` \u2717 ${file}`);
|
|
9852
|
-
}
|
|
9853
|
-
if (result.deleted.length > 10) {
|
|
9854
|
-
console.log(` ... and ${result.deleted.length - 10} more`);
|
|
9855
|
-
}
|
|
9856
|
-
}
|
|
9857
|
-
if (result.errors.length > 0) {
|
|
9858
|
-
console.log(`
|
|
9859
|
-
Errors (${result.errors.length}):`);
|
|
9860
|
-
for (const error of result.errors) {
|
|
9861
|
-
console.log(` ! ${error}`);
|
|
9862
|
-
}
|
|
9863
|
-
}
|
|
9864
|
-
console.log(`
|
|
9865
|
-
Summary:`);
|
|
9866
|
-
console.log(` Pushed: ${result.pushed.length}`);
|
|
9867
|
-
console.log(` Pulled: ${result.pulled.length}`);
|
|
9868
|
-
console.log(` Deleted: ${result.deleted.length}`);
|
|
9869
|
-
console.log(` Unchanged: ${result.unchanged.length}`);
|
|
9870
|
-
console.log(` Errors: ${result.errors.length}`);
|
|
9871
|
-
console.log(` Duration: ${result.stats.duration}ms`);
|
|
9872
|
-
console.log(` Transferred: ${formatBytes2(result.stats.bytesTransferred)}`);
|
|
9873
|
-
}
|
|
9874
|
-
return result;
|
|
9875
|
-
}
|
|
9876
|
-
function formatBytes2(bytes) {
|
|
9877
|
-
if (bytes === 0) return "0 B";
|
|
9878
|
-
const k = 1024;
|
|
9879
|
-
const sizes = ["B", "KB", "MB", "GB"];
|
|
9880
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
9881
|
-
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
|
9882
|
-
}
|
|
9883
|
-
function registerTailscaleSyncCommand(program) {
|
|
9884
|
-
program.command("tailscale-sync").alias("ts-sync").description("Sync vault with a peer on the Tailscale network").requiredOption("--peer <hostname>", "Peer hostname or IP to sync with").option("-v, --vault <path>", "Vault path").option("--port <number>", "Port on the peer", parseInt).option("--direction <dir>", "Sync direction: push, pull, or bidirectional", "bidirectional").option("--dry-run", "Show what would be synced without making changes").option("--delete-orphans", "Delete files that exist locally but not on peer (pull only)").option("--categories <list>", "Comma-separated list of categories to sync").option("--https", "Use HTTPS for connection").option("--json", "Output as JSON").action(async (rawOptions) => {
|
|
9885
|
-
await tailscaleSyncCommand({
|
|
9886
|
-
peer: rawOptions.peer,
|
|
9887
|
-
vaultPath: rawOptions.vault,
|
|
9888
|
-
port: rawOptions.port,
|
|
9889
|
-
direction: rawOptions.direction,
|
|
9890
|
-
dryRun: rawOptions.dryRun,
|
|
9891
|
-
deleteOrphans: rawOptions.deleteOrphans,
|
|
9892
|
-
categories: rawOptions.categories?.split(",").map((c) => c.trim()),
|
|
9893
|
-
https: rawOptions.https,
|
|
9894
|
-
json: rawOptions.json
|
|
9895
|
-
});
|
|
9896
|
-
});
|
|
9897
|
-
}
|
|
9898
|
-
var activeServeInstance = null;
|
|
9899
|
-
async function tailscaleServeCommand(options) {
|
|
9900
|
-
if (options.stop) {
|
|
9901
|
-
if (activeServeInstance) {
|
|
9902
|
-
await activeServeInstance.stop();
|
|
9903
|
-
activeServeInstance = null;
|
|
9904
|
-
console.log("ClawVault serve stopped.");
|
|
9905
|
-
}
|
|
9906
|
-
stopTailscaleServe();
|
|
9907
|
-
return;
|
|
9908
|
-
}
|
|
9909
|
-
const vaultPath = resolveVaultPath({ explicitPath: options.vaultPath });
|
|
9910
|
-
const port = options.port || DEFAULT_SERVE_PORT;
|
|
9911
|
-
const status = getTailscaleStatus();
|
|
9912
|
-
console.log(`Starting ClawVault serve...`);
|
|
9913
|
-
console.log(` Vault: ${path24.basename(vaultPath)}`);
|
|
9914
|
-
console.log(` Port: ${port}`);
|
|
9915
|
-
activeServeInstance = serveVault(vaultPath, { port });
|
|
9916
|
-
console.log(` Local URL: http://localhost:${port}/.clawvault`);
|
|
9917
|
-
if (status.connected) {
|
|
9918
|
-
console.log(` Tailscale URL: http://${status.selfIP}:${port}/.clawvault`);
|
|
9919
|
-
if (status.selfDNSName) {
|
|
9920
|
-
const dnsHost = status.selfDNSName.replace(/\.$/, "");
|
|
9921
|
-
console.log(` MagicDNS URL: http://${dnsHost}:${port}/.clawvault`);
|
|
9922
|
-
}
|
|
9923
|
-
if (options.funnel || options.background) {
|
|
9924
|
-
console.log("\nConfiguring Tailscale serve...");
|
|
9925
|
-
configureTailscaleServe(port, {
|
|
9926
|
-
funnel: options.funnel,
|
|
9927
|
-
background: options.background
|
|
9928
|
-
});
|
|
9929
|
-
if (options.funnel) {
|
|
9930
|
-
console.log(" Funnel enabled - vault is accessible from the public internet");
|
|
9931
|
-
}
|
|
9932
|
-
}
|
|
9933
|
-
} else {
|
|
9934
|
-
console.log("\n Note: Not connected to Tailscale. Only local access available.");
|
|
9935
|
-
}
|
|
9936
|
-
console.log("\nEndpoints:");
|
|
9937
|
-
console.log(` Health: /.clawvault/health`);
|
|
9938
|
-
console.log(` Manifest: /.clawvault/manifest`);
|
|
9939
|
-
console.log(` Files: /.clawvault/files/<path>`);
|
|
9940
|
-
if (!options.background) {
|
|
9941
|
-
console.log("\nPress Ctrl+C to stop serving.");
|
|
9942
|
-
process.on("SIGINT", async () => {
|
|
9943
|
-
console.log("\nStopping ClawVault serve...");
|
|
9944
|
-
if (activeServeInstance) {
|
|
9945
|
-
await activeServeInstance.stop();
|
|
9946
|
-
activeServeInstance = null;
|
|
9947
|
-
}
|
|
9948
|
-
stopTailscaleServe();
|
|
9949
|
-
process.exit(0);
|
|
9950
|
-
});
|
|
9951
|
-
await new Promise(() => {
|
|
9952
|
-
});
|
|
9953
|
-
}
|
|
9954
|
-
}
|
|
9955
|
-
function registerTailscaleServeCommand(program) {
|
|
9956
|
-
program.command("tailscale-serve").alias("ts-serve").description("Serve vault for sync over Tailscale").option("-v, --vault <path>", "Vault path").option("--port <number>", `Port to serve on (default: ${DEFAULT_SERVE_PORT})`, parseInt).option("--funnel", "Expose via Tailscale Funnel (public internet)").option("--background", "Run in background").option("--stop", "Stop serving").action(async (rawOptions) => {
|
|
9957
|
-
await tailscaleServeCommand({
|
|
9958
|
-
vaultPath: rawOptions.vault,
|
|
9959
|
-
port: rawOptions.port,
|
|
9960
|
-
funnel: rawOptions.funnel,
|
|
9961
|
-
background: rawOptions.background,
|
|
9962
|
-
stop: rawOptions.stop
|
|
9963
|
-
});
|
|
9964
|
-
});
|
|
9965
|
-
}
|
|
9966
|
-
async function tailscaleDiscoverCommand(options = {}) {
|
|
9967
|
-
const port = options.port || DEFAULT_SERVE_PORT;
|
|
9968
|
-
const status = getTailscaleStatus();
|
|
9969
|
-
if (!status.connected) {
|
|
9970
|
-
if (options.json) {
|
|
9971
|
-
console.log(JSON.stringify({ error: "Not connected to Tailscale", peers: [] }));
|
|
9972
|
-
} else {
|
|
9973
|
-
console.error("Error: Not connected to Tailscale");
|
|
9974
|
-
}
|
|
9975
|
-
return [];
|
|
9976
|
-
}
|
|
9977
|
-
if (!options.json) {
|
|
9978
|
-
console.log("Discovering ClawVault peers on tailnet...");
|
|
9979
|
-
}
|
|
9980
|
-
const peers = await discoverClawVaultPeers(port);
|
|
9981
|
-
if (options.json) {
|
|
9982
|
-
console.log(JSON.stringify({ peers }, null, 2));
|
|
9983
|
-
} else {
|
|
9984
|
-
if (peers.length === 0) {
|
|
9985
|
-
console.log("\nNo ClawVault peers found.");
|
|
9986
|
-
console.log(" Run `clawvault tailscale-serve` on other devices to enable sync.");
|
|
9987
|
-
} else {
|
|
9988
|
-
console.log(`
|
|
9989
|
-
Found ${peers.length} ClawVault peer(s):`);
|
|
9990
|
-
for (const peer of peers) {
|
|
9991
|
-
const ip = peer.tailscaleIPs[0] || "no-ip";
|
|
9992
|
-
const os2 = peer.os ? ` (${peer.os})` : "";
|
|
9993
|
-
console.log(` \u25CF ${peer.hostname}${os2}`);
|
|
9994
|
-
console.log(` IP: ${ip}`);
|
|
9995
|
-
console.log(` Port: ${peer.clawvaultPort}`);
|
|
9996
|
-
if (peer.dnsName) {
|
|
9997
|
-
console.log(` DNS: ${peer.dnsName.replace(/\.$/, "")}`);
|
|
9998
|
-
}
|
|
9999
|
-
}
|
|
10000
|
-
}
|
|
10001
|
-
}
|
|
10002
|
-
return peers;
|
|
10003
|
-
}
|
|
10004
|
-
function registerTailscaleDiscoverCommand(program) {
|
|
10005
|
-
program.command("tailscale-discover").alias("ts-discover").description("Discover ClawVault peers on the Tailscale network").option("--port <number>", `Port to check (default: ${DEFAULT_SERVE_PORT})`, parseInt).option("--json", "Output as JSON").action(async (rawOptions) => {
|
|
10006
|
-
await tailscaleDiscoverCommand({
|
|
10007
|
-
port: rawOptions.port,
|
|
10008
|
-
json: rawOptions.json
|
|
10009
|
-
});
|
|
10010
|
-
});
|
|
10011
|
-
}
|
|
10012
|
-
function registerTailscaleCommands(program) {
|
|
10013
|
-
registerTailscaleStatusCommand(program);
|
|
10014
|
-
registerTailscaleSyncCommand(program);
|
|
10015
|
-
registerTailscaleServeCommand(program);
|
|
10016
|
-
registerTailscaleDiscoverCommand(program);
|
|
10017
|
-
}
|
|
10018
|
-
|
|
10019
8571
|
// src/cli/index.ts
|
|
10020
8572
|
function registerCliCommands(program) {
|
|
10021
8573
|
registerContextCommand(program);
|
|
@@ -10024,7 +8576,6 @@ function registerCliCommands(program) {
|
|
|
10024
8576
|
registerReflectCommand(program);
|
|
10025
8577
|
registerEmbedCommand(program);
|
|
10026
8578
|
registerReweaveCommand(program);
|
|
10027
|
-
registerTailscaleCommands(program);
|
|
10028
8579
|
return program;
|
|
10029
8580
|
}
|
|
10030
8581
|
// Annotate the CommonJS export names for ESM import in node:
|