@floomhq/floom-mcp-sync 1.0.6 → 1.0.8

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.
@@ -0,0 +1,89 @@
1
+ import { constants } from "node:fs";
2
+ import { open } from "node:fs/promises";
3
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
4
+ import { readConfig } from "../lib/config.js";
5
+ import { sha256 } from "../lib/hash.js";
6
+ import { skillsDir } from "../lib/paths.js";
7
+ import { readSyncManifest, syncManifestPath } from "../lib/manifest.js";
8
+ const MANIFEST_SEGMENT_RE = /^[A-Za-z0-9._-]{1,128}$/;
9
+ export async function syncStatus() {
10
+ const cfg = await readConfig();
11
+ const root = skillsDir();
12
+ const manifest = await readSyncManifest();
13
+ const drift = { missing: [], changed: [], blocked: [] };
14
+ let upToDate = 0;
15
+ for (const [key, entry] of Object.entries(manifest.files)) {
16
+ let target;
17
+ try {
18
+ target = targetFromManifestKey(root, key);
19
+ }
20
+ catch (err) {
21
+ drift.blocked.push({ target: key, reason: err instanceof Error ? err.message : "invalid manifest target path" });
22
+ continue;
23
+ }
24
+ const state = await localState(target);
25
+ if (state.kind === "missing") {
26
+ drift.missing.push(key);
27
+ }
28
+ else if (state.kind === "blocked") {
29
+ drift.blocked.push({ target: key, reason: state.reason });
30
+ }
31
+ else if (state.hash === entry.hash) {
32
+ upToDate += 1;
33
+ }
34
+ else {
35
+ drift.changed.push(key);
36
+ }
37
+ }
38
+ return {
39
+ signed_in: cfg !== null,
40
+ skills_dir: root,
41
+ manifest_path: syncManifestPath(),
42
+ tracked_files: Object.keys(manifest.files).length,
43
+ up_to_date: upToDate,
44
+ missing: drift.missing.length,
45
+ changed: drift.changed.length,
46
+ blocked: drift.blocked.length,
47
+ drift,
48
+ };
49
+ }
50
+ async function localState(path) {
51
+ try {
52
+ const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
53
+ try {
54
+ const stat = await handle.stat();
55
+ if (!stat.isFile())
56
+ return { kind: "blocked", reason: "path is blocked by an existing local file or directory" };
57
+ return { kind: "file", hash: sha256(await handle.readFile()) };
58
+ }
59
+ finally {
60
+ await handle.close();
61
+ }
62
+ }
63
+ catch (err) {
64
+ const code = err.code;
65
+ if (code === "ENOENT")
66
+ return { kind: "missing" };
67
+ if (code === "ELOOP")
68
+ return { kind: "blocked", reason: "path is a symbolic link" };
69
+ if (code === "ENOTDIR" || code === "EISDIR") {
70
+ return { kind: "blocked", reason: "path is blocked by an existing local file or directory" };
71
+ }
72
+ throw err;
73
+ }
74
+ }
75
+ function targetFromManifestKey(root, key) {
76
+ if (!key || isAbsolute(key) || key.includes("\\") || key.length > 512) {
77
+ throw new Error("Invalid manifest target path");
78
+ }
79
+ const segments = key.split("/");
80
+ if (segments.some((segment) => segment === "." || segment === ".." || !MANIFEST_SEGMENT_RE.test(segment))) {
81
+ throw new Error("Invalid manifest target path");
82
+ }
83
+ const target = join(root, ...segments);
84
+ const relativeTarget = relative(resolve(root), resolve(target));
85
+ if (relativeTarget === ".." || relativeTarget.startsWith(`..${sep}`) || isAbsolute(relativeTarget)) {
86
+ throw new Error("Invalid manifest target path");
87
+ }
88
+ return target;
89
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floomhq/floom-mcp-sync",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "Lightweight Floom MCP server for installing, publishing, and startup-syncing skills.",
5
5
  "license": "MIT",
6
6
  "type": "module",