@jive-sh/skill-sync 0.4.2-latest-darwin-x64 → 0.4.2
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 +60 -0
- package/bin/package.json +1 -0
- package/bin/skill-sync +571 -0
- package/bin/skill-sync-daemon +571 -0
- package/dist/daemons/skill-sync-daemon.js +374 -0
- package/dist/daemons/skill-sync-daemon.js.map +1 -0
- package/dist/entrypoints/skill-sync.js +133 -0
- package/dist/entrypoints/skill-sync.js.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/config.js +130 -0
- package/dist/internal/config.js.map +1 -0
- package/dist/internal/control.js +101 -0
- package/dist/internal/control.js.map +1 -0
- package/dist/internal/folders.js +48 -0
- package/dist/internal/folders.js.map +1 -0
- package/dist/internal/path.js +36 -0
- package/dist/internal/path.js.map +1 -0
- package/dist/internal/sync.js +160 -0
- package/dist/internal/sync.js.map +1 -0
- package/dist/internal/wire.js +13 -0
- package/dist/internal/wire.js.map +1 -0
- package/install.cjs +571 -0
- package/package.json +39 -11
- package/src/daemons/skill-sync-daemon.ts +368 -0
- package/src/entrypoints/skill-sync.ts +135 -0
- package/src/index.ts +15 -0
- package/src/internal/config.ts +168 -0
- package/src/internal/control.ts +127 -0
- package/src/internal/folders.ts +52 -0
- package/src/internal/path.ts +32 -0
- package/src/internal/sync.ts +196 -0
- package/src/internal/wire.ts +154 -0
- package/multi-call-binary +0 -4
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync, readdirSync, statSync } from "node:fs";
|
|
3
|
+
import { homedir, hostname, userInfo } from "node:os";
|
|
4
|
+
import { join, relative, sep } from "node:path";
|
|
5
|
+
/**
|
|
6
|
+
* Per-machine settings and sync state, both under `~/.jive/`. The sync token is not here: it lives only in
|
|
7
|
+
* the running daemon's memory.
|
|
8
|
+
*
|
|
9
|
+
* A file rather than environment variables: the endpoint and which machine this is differ per computer
|
|
10
|
+
* and have to survive a login, which is exactly what a config file is for and exactly what an environment
|
|
11
|
+
* variable is not. Which folders to sync is not in here: they are detected, and the choice to leave one
|
|
12
|
+
* out is kept on the server so the dashboard can change it and a reinstalled daemon still remembers it.
|
|
13
|
+
*/
|
|
14
|
+
const JIVE_HOME = join(homedir(), ".jive");
|
|
15
|
+
export const CONFIG_PATH = join(JIVE_HOME, "skill-sync.json");
|
|
16
|
+
const STATE_PATH = join(JIVE_HOME, "skill-sync-state.json");
|
|
17
|
+
/**
|
|
18
|
+
* Where versions before 0.3 kept the sync token. The token now lives only in the running daemon's memory,
|
|
19
|
+
* so the daemon deletes this file whenever it finds one: a credential left on disk is one anything that
|
|
20
|
+
* can read the home directory can copy.
|
|
21
|
+
*/
|
|
22
|
+
export const LEGACY_TOKEN_PATH = join(homedir(), ".skill-sync-token.json");
|
|
23
|
+
/**
|
|
24
|
+
* How the CLI reaches the running daemon: a Unix socket in `~/.jive/`, or a named pipe on Windows. Only
|
|
25
|
+
* this user can open either, which is what lets the CLI hand the daemon work that involves the token.
|
|
26
|
+
*/
|
|
27
|
+
export const CONTROL_SOCKET = process.platform === "win32"
|
|
28
|
+
? `\\\\.\\pipe\\jive-skill-sync-${userInfo().username}`
|
|
29
|
+
: join(JIVE_HOME, "skill-sync.sock");
|
|
30
|
+
/** Managed by Claude's own cloud sync; see `Settings.exclude`. */
|
|
31
|
+
const DEFAULT_EXCLUDE = ["synced"];
|
|
32
|
+
/**
|
|
33
|
+
* The settings, writing a template on first run. Undefined when the file lacks an endpoint or a machine
|
|
34
|
+
* name, which only happens if someone edited it.
|
|
35
|
+
*/
|
|
36
|
+
export function loadSettings() {
|
|
37
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
38
|
+
mkdirSync(JIVE_HOME, { recursive: true });
|
|
39
|
+
writeFileSync(CONFIG_PATH, `${JSON.stringify({
|
|
40
|
+
endpoint: "https://skill-sync.jive.sh",
|
|
41
|
+
machine: machineName(),
|
|
42
|
+
exclude: DEFAULT_EXCLUDE,
|
|
43
|
+
}, null, 2)}\n`);
|
|
44
|
+
}
|
|
45
|
+
const settings = readJson(CONFIG_PATH);
|
|
46
|
+
if (!settings.endpoint || !settings.machine)
|
|
47
|
+
return undefined;
|
|
48
|
+
return { endpoint: settings.endpoint, machine: settings.machine, exclude: settings.exclude ?? DEFAULT_EXCLUDE };
|
|
49
|
+
}
|
|
50
|
+
function readJson(path) {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Every folder's sync state.
|
|
60
|
+
*
|
|
61
|
+
* Before folders were detected the file held one folder's `{ cursor, known }`, for the folder then named
|
|
62
|
+
* by `skillsDir` in the settings. That state is carried over to that folder: dropping it would make the
|
|
63
|
+
* folder look never-synced, and every file in it that differs from the server would become a conflict.
|
|
64
|
+
*/
|
|
65
|
+
export function loadStates() {
|
|
66
|
+
const stored = readJson(STATE_PATH);
|
|
67
|
+
if (stored.folders !== undefined)
|
|
68
|
+
return stored.folders;
|
|
69
|
+
const legacyFolder = readJson(CONFIG_PATH).skillsDir;
|
|
70
|
+
if (legacyFolder !== undefined && stored.cursor !== undefined && stored.known !== undefined) {
|
|
71
|
+
return { [legacyFolder]: { cursor: stored.cursor, known: stored.known } };
|
|
72
|
+
}
|
|
73
|
+
return {};
|
|
74
|
+
}
|
|
75
|
+
export function stateFor(states, folder) {
|
|
76
|
+
states[folder] ??= { cursor: 0, known: {} };
|
|
77
|
+
return states[folder];
|
|
78
|
+
}
|
|
79
|
+
export function saveStates(states) {
|
|
80
|
+
mkdirSync(JIVE_HOME, { recursive: true });
|
|
81
|
+
writeFileSync(STATE_PATH, `${JSON.stringify({ folders: states }, null, 2)}\n`);
|
|
82
|
+
}
|
|
83
|
+
/** `HOSTNAME` is not set on macOS, so the template was writing "unknown" on exactly this machine. */
|
|
84
|
+
function machineName() {
|
|
85
|
+
return hostname().replace(/\.local$/, "").replace(/[^A-Za-z0-9-]+/g, "-") || "unknown";
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The `skills` folder of every agent home on this machine: Claude's `~/.claude` and each
|
|
89
|
+
* `~/.claude-<name>`, and Codex's `~/.agents`, whose `skills` folder is where Codex reads a user's skills.
|
|
90
|
+
*
|
|
91
|
+
* Every home is a candidate, whether or not it has a `skills` folder yet, so a Claude account added
|
|
92
|
+
* later joins without anyone listing it. Which of them actually sync is the user's choice on the
|
|
93
|
+
* dashboard; a folder that is synced and missing is created.
|
|
94
|
+
*/
|
|
95
|
+
export function detectFolders() {
|
|
96
|
+
return readdirSync(homedir())
|
|
97
|
+
.filter((entry) => entry === ".claude" || entry.startsWith(".claude-") || entry === ".agents")
|
|
98
|
+
.filter((entry) => statSync(join(homedir(), entry)).isDirectory())
|
|
99
|
+
.map((entry) => join(homedir(), entry, "skills"))
|
|
100
|
+
.sort();
|
|
101
|
+
}
|
|
102
|
+
export function hashBytes(bytes) {
|
|
103
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Every file under a synced folder, as paths relative to it with forward slashes.
|
|
107
|
+
*
|
|
108
|
+
* Normalised because the same skill synced from Windows and macOS must be one row, not two.
|
|
109
|
+
*/
|
|
110
|
+
export function listLocalFiles(folder, exclude = DEFAULT_EXCLUDE) {
|
|
111
|
+
if (!existsSync(folder))
|
|
112
|
+
return [];
|
|
113
|
+
const found = [];
|
|
114
|
+
const walk = (dir, top) => {
|
|
115
|
+
for (const entry of readdirSync(dir)) {
|
|
116
|
+
if (entry.startsWith("."))
|
|
117
|
+
continue;
|
|
118
|
+
if (top && exclude.includes(entry))
|
|
119
|
+
continue;
|
|
120
|
+
const full = join(dir, entry);
|
|
121
|
+
if (statSync(full).isDirectory())
|
|
122
|
+
walk(full, false);
|
|
123
|
+
else
|
|
124
|
+
found.push(relative(folder, full).split(sep).join("/"));
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
walk(folder, true);
|
|
128
|
+
return found.sort();
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../../../src/internal/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACpG,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACtD,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAEhD;;;;;;;;GAQG;AAEH,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,CAAC,CAAC;AAC3C,MAAM,CAAC,MAAM,WAAW,GAAG,IAAI,CAAC,SAAS,EAAE,iBAAiB,CAAC,CAAC;AAC9D,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,EAAE,uBAAuB,CAAC,CAAC;AAC5D;;;;GAIG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,wBAAwB,CAAC,CAAC;AAC3E;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO;IACxD,CAAC,CAAC,gCAAgC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IACvD,CAAC,CAAC,IAAI,CAAC,SAAS,EAAE,iBAAiB,CAAC,CAAC;AAEvC,kEAAkE;AAClE,MAAM,eAAe,GAAG,CAAC,QAAQ,CAAU,CAAC;AAoC5C;;;GAGG;AACH,MAAM,UAAU,YAAY;IAC1B,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QAC7B,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1C,aAAa,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC;YAC3C,QAAQ,EAAE,4BAA4B;YACtC,OAAO,EAAE,WAAW,EAAE;YACtB,OAAO,EAAE,eAAe;SACzB,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;IACnB,CAAC;IACD,MAAM,QAAQ,GAAG,QAAQ,CAAoB,WAAW,CAAC,CAAC;IAC1D,IAAI,CAAC,QAAQ,CAAC,QAAQ,IAAI,CAAC,QAAQ,CAAC,OAAO;QAAE,OAAO,SAAS,CAAC;IAC9D,OAAO,EAAE,QAAQ,EAAE,QAAQ,CAAC,QAAQ,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,IAAI,eAAe,EAAE,CAAC;AAClH,CAAC;AAED,SAAS,QAAQ,CAAI,IAAY;IAC/B,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAM,CAAC;IACtD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAO,CAAC;IACjB,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,UAAU;IACxB,MAAM,MAAM,GAAG,QAAQ,CAAwC,UAAU,CAAC,CAAC;IAC3E,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,OAAO,CAAC;IACxD,MAAM,YAAY,GAAG,QAAQ,CAAyB,WAAW,CAAC,CAAC,SAAS,CAAC;IAC7E,IAAI,YAAY,KAAK,SAAS,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAC5F,OAAO,EAAE,CAAC,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,EAAE,CAAC;IAC5E,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,MAAc,EAAE,MAAc;IACrD,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;IAC5C,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,MAAc;IACvC,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1C,aAAa,CAAC,UAAU,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;AACjF,CAAC;AAED,qGAAqG;AACrG,SAAS,WAAW;IAClB,OAAO,QAAQ,EAAE,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,iBAAiB,EAAE,GAAG,CAAC,IAAI,SAAS,CAAC;AACzF,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,WAAW,CAAC,OAAO,EAAE,CAAC;SAC1B,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,UAAU,CAAC,UAAU,CAAC,IAAI,KAAK,KAAK,SAAS,CAAC;SAC7F,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,KAAK,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;SACjE,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;SAChD,IAAI,EAAE,CAAC;AACZ,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAiB;IACzC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC1D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,MAAc,EAAE,UAAiC,eAAe;IAC7F,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,CAAC;IAEnC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,IAAI,GAAG,CAAC,GAAW,EAAE,GAAY,EAAQ,EAAE;QAC/C,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC;YACrC,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC;gBAAE,SAAS;YACpC,IAAI,GAAG,IAAI,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC;gBAAE,SAAS;YAC7C,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;YAC9B,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE;gBAAE,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;;gBAC/C,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QAC/D,CAAC;IACH,CAAC,CAAC;IACF,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACnB,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC;AACtB,CAAC"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { createConnection, createServer } from "node:net";
|
|
2
|
+
import { chmodSync, rmSync } from "node:fs";
|
|
3
|
+
import { CONTROL_SOCKET } from "./config";
|
|
4
|
+
const isFinal = (event) => event.type !== "open";
|
|
5
|
+
/** The daemon is not running, as opposed to running and refusing the request. */
|
|
6
|
+
export class DaemonUnreachable extends Error {
|
|
7
|
+
}
|
|
8
|
+
/** Sends one request and calls `onEvent` for each event until the final one, which it resolves with. */
|
|
9
|
+
export function request(message, onEvent) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
let settled = false;
|
|
12
|
+
const socket = createConnection(CONTROL_SOCKET);
|
|
13
|
+
socket.on("connect", () => socket.write(`${JSON.stringify(message)}\n`));
|
|
14
|
+
socket.on("error", (cause) => {
|
|
15
|
+
if (settled)
|
|
16
|
+
return;
|
|
17
|
+
settled = true;
|
|
18
|
+
reject(cause.code === "ENOENT" || cause.code === "ECONNREFUSED"
|
|
19
|
+
? new DaemonUnreachable(`the skill-sync daemon is not running (${cause.code} on ${CONTROL_SOCKET})`)
|
|
20
|
+
: cause);
|
|
21
|
+
});
|
|
22
|
+
readLines(socket, (line) => {
|
|
23
|
+
const event = JSON.parse(line);
|
|
24
|
+
onEvent(event);
|
|
25
|
+
if (isFinal(event) && !settled) {
|
|
26
|
+
settled = true;
|
|
27
|
+
socket.end();
|
|
28
|
+
resolve(event);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
socket.on("close", () => {
|
|
32
|
+
if (!settled) {
|
|
33
|
+
settled = true;
|
|
34
|
+
reject(new Error("the daemon closed the connection without answering"));
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Listens on the control socket. Resolves false, without listening, when another live daemon already
|
|
41
|
+
* holds it: two installs of skill-sync mean two daemons, and the second stands down rather than fight.
|
|
42
|
+
*/
|
|
43
|
+
export async function serve(handle) {
|
|
44
|
+
if (await answers())
|
|
45
|
+
return false;
|
|
46
|
+
// Nothing answered, so a socket file left here is from a daemon that died; it would block the listen.
|
|
47
|
+
if (process.platform !== "win32")
|
|
48
|
+
rmSync(CONTROL_SOCKET, { force: true });
|
|
49
|
+
const server = createServer((socket) => {
|
|
50
|
+
const reply = {
|
|
51
|
+
send: (event) => {
|
|
52
|
+
if (socket.writable)
|
|
53
|
+
socket.write(`${JSON.stringify(event)}\n`);
|
|
54
|
+
if (isFinal(event))
|
|
55
|
+
socket.end();
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
readLines(socket, (line) => {
|
|
59
|
+
try {
|
|
60
|
+
handle(JSON.parse(line), reply);
|
|
61
|
+
}
|
|
62
|
+
catch (cause) {
|
|
63
|
+
reply.send({ type: "error", message: cause instanceof Error ? cause.message : String(cause) });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
socket.on("error", () => undefined); // A CLI that went away mid-request; nothing to tell it.
|
|
67
|
+
});
|
|
68
|
+
await new Promise((resolve, reject) => {
|
|
69
|
+
server.once("error", reject);
|
|
70
|
+
server.listen(CONTROL_SOCKET, () => resolve());
|
|
71
|
+
});
|
|
72
|
+
// Only this user may talk to the daemon that holds their token.
|
|
73
|
+
if (process.platform !== "win32")
|
|
74
|
+
chmodSync(CONTROL_SOCKET, 0o600);
|
|
75
|
+
return server;
|
|
76
|
+
}
|
|
77
|
+
/** Whether a live daemon is listening on the control socket. */
|
|
78
|
+
function answers() {
|
|
79
|
+
return new Promise((resolve) => {
|
|
80
|
+
const socket = createConnection(CONTROL_SOCKET);
|
|
81
|
+
socket.on("connect", () => {
|
|
82
|
+
socket.end();
|
|
83
|
+
resolve(true);
|
|
84
|
+
});
|
|
85
|
+
socket.on("error", () => resolve(false));
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function readLines(socket, onLine) {
|
|
89
|
+
let buffer = "";
|
|
90
|
+
socket.on("data", (chunk) => {
|
|
91
|
+
buffer += chunk.toString();
|
|
92
|
+
let newline;
|
|
93
|
+
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
94
|
+
const line = buffer.slice(0, newline).trim();
|
|
95
|
+
buffer = buffer.slice(newline + 1);
|
|
96
|
+
if (line !== "")
|
|
97
|
+
onLine(line);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=control.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"control.js","sourceRoot":"","sources":["../../../../src/internal/control.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { detectFolders, hashBytes, listLocalFiles } from "./config";
|
|
4
|
+
import { skillOf, } from "./wire";
|
|
5
|
+
/**
|
|
6
|
+
* What this machine tells the service about its folders, and what it hears back.
|
|
7
|
+
*
|
|
8
|
+
* The daemon and the CLI both register, so this lives in one place: they must describe a folder the same
|
|
9
|
+
* way, or the dashboard would show whichever of them spoke last.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* The skills on disk in one folder, with each file's hash. Hashing every file on each check is cheap at
|
|
13
|
+
* skills' size, and it is what lets the dashboard show whether this folder's copy is the synced one.
|
|
14
|
+
*/
|
|
15
|
+
export function localSkills(folder, config) {
|
|
16
|
+
const byName = new Map();
|
|
17
|
+
for (const path of listLocalFiles(folder, config.exclude)) {
|
|
18
|
+
const name = skillOf(path);
|
|
19
|
+
let hash;
|
|
20
|
+
try {
|
|
21
|
+
hash = hashBytes(readFileSync(join(folder, path)));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
continue; // Deleted between listing and reading; the next check reports the folder as it is.
|
|
25
|
+
}
|
|
26
|
+
byName.set(name, [...(byName.get(name) ?? []), { path, hash }]);
|
|
27
|
+
}
|
|
28
|
+
return [...byName].map(([name, files]) => ({ name, files }));
|
|
29
|
+
}
|
|
30
|
+
/** Everything registration sends, so a caller can tell when it has changed and is worth re-sending. */
|
|
31
|
+
export function describe(config) {
|
|
32
|
+
return { folders: detectFolders().map((path) => ({ path, skills: localSkills(path, config) })) };
|
|
33
|
+
}
|
|
34
|
+
/** Registers this machine's folders and skills; returns the folders to sync and the skills in each. */
|
|
35
|
+
export async function register(config, request) {
|
|
36
|
+
const response = await fetch(`${config.endpoint}/folders`, {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers: authHeaders(config, { "content-type": "application/json" }),
|
|
39
|
+
body: JSON.stringify(request),
|
|
40
|
+
});
|
|
41
|
+
if (!response.ok)
|
|
42
|
+
throw new Error(`POST /folders: ${response.status} ${await response.text()}`);
|
|
43
|
+
return (await response.json()).folders;
|
|
44
|
+
}
|
|
45
|
+
export function authHeaders(config, extra) {
|
|
46
|
+
return { authorization: `Bearer ${config.token}`, "x-skill-sync-machine": config.machine, ...extra };
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=folders.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"folders.js","sourceRoot":"","sources":["../../../../src/internal/folders.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,cAAc,EAAe,MAAM,UAAU,CAAC;AACjF,OAAO,EACL,OAAO,GACR,MAAM,QAAQ,CAAC;AAEhB;;;;;GAKG;AAEH;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,MAAc,EAAE,MAAc;IACxD,MAAM,MAAM,GAAG,IAAI,GAAG,EAAsB,CAAC;IAC7C,KAAK,MAAM,IAAI,IAAI,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;QAC1D,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAC3B,IAAI,IAAY,CAAC;QACjB,IAAI,CAAC;YACH,IAAI,GAAG,SAAS,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;QACrD,CAAC;QAAC,MAAM,CAAC;YACP,SAAS,CAAC,mFAAmF;QAC/F,CAAC;QACD,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IAClE,CAAC;IACD,OAAO,CAAC,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;AAC/D,CAAC;AAED,uGAAuG;AACvG,MAAM,UAAU,QAAQ,CAAC,MAAc;IACrC,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;AACnG,CAAC;AAED,uGAAuG;AACvG,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,MAAc,EAAE,OAAuB;IACpE,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,MAAM,CAAC,QAAQ,UAAU,EAAE;QACzD,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC;QACpE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;KAC9B,CAAC,CAAC;IACH,IAAI,CAAC,QAAQ,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,kBAAkB,QAAQ,CAAC,MAAM,IAAI,MAAM,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IAChG,OAAO,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAsB,CAAA,CAAC,OAAO,CAAC;AAC5D,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,MAAc,EAAE,KAA6B;IACvE,OAAO,EAAE,aAAa,EAAE,UAAU,MAAM,CAAC,KAAK,EAAE,EAAE,sBAAsB,EAAE,MAAM,CAAC,OAAO,EAAE,GAAG,KAAK,EAAE,CAAC;AACvG,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation for the one piece of untrusted input that becomes a filesystem path.
|
|
3
|
+
*
|
|
4
|
+
* Shared by the Worker and the daemon on purpose, and deliberately free of any Node import so the Worker
|
|
5
|
+
* can bundle it. Both ends check: the server so one compromised client cannot poison what every machine
|
|
6
|
+
* pulls, and the daemon because a server it does not control is not a thing to take paths from either.
|
|
7
|
+
*
|
|
8
|
+
* Without this, a pushed path of `../../.ssh/authorized_keys` is joined to the skills directory and
|
|
9
|
+
* written wherever the daemon's user can write.
|
|
10
|
+
*/
|
|
11
|
+
/** Long enough for any real nesting, short enough that nothing can be used to exhaust anything. */
|
|
12
|
+
const MAX_LENGTH = 512;
|
|
13
|
+
export function invalidPathReason(path) {
|
|
14
|
+
if (path.length === 0 || path.length > MAX_LENGTH)
|
|
15
|
+
return "path is empty or too long";
|
|
16
|
+
if (path.includes("\0"))
|
|
17
|
+
return "path contains a null byte";
|
|
18
|
+
if (path.includes("\\"))
|
|
19
|
+
return "path must use forward slashes";
|
|
20
|
+
if (path.startsWith("/"))
|
|
21
|
+
return "path must be relative";
|
|
22
|
+
// A Windows drive letter is absolute on the machine that matters even though it has no leading slash.
|
|
23
|
+
if (/^[A-Za-z]:/.test(path))
|
|
24
|
+
return "path must be relative";
|
|
25
|
+
for (const segment of path.split("/")) {
|
|
26
|
+
if (segment === "")
|
|
27
|
+
return "path has an empty segment";
|
|
28
|
+
if (segment === "." || segment === "..")
|
|
29
|
+
return "path must not contain . or .. segments";
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
export function isValidPath(path) {
|
|
34
|
+
return invalidPathReason(path) === undefined;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=path.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path.js","sourceRoot":"","sources":["../../../../src/internal/path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,mGAAmG;AACnG,MAAM,UAAU,GAAG,GAAG,CAAC;AAEvB,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,MAAM,GAAG,UAAU;QAAE,OAAO,2BAA2B,CAAC;IACtF,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,2BAA2B,CAAC;IAC5D,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,+BAA+B,CAAC;IAChE,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,uBAAuB,CAAC;IACzD,sGAAsG;IACtG,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,uBAAuB,CAAC;IAE5D,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACtC,IAAI,OAAO,KAAK,EAAE;YAAE,OAAO,2BAA2B,CAAC;QACvD,IAAI,OAAO,KAAK,GAAG,IAAI,OAAO,KAAK,IAAI;YAAE,OAAO,wCAAwC,CAAC;IAC3F,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,iBAAiB,CAAC,IAAI,CAAC,KAAK,SAAS,CAAC;AAC/C,CAAC"}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmdirSync, rmSync, statSync, utimesSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { hashBytes, listLocalFiles } from "./config";
|
|
4
|
+
import { invalidPathReason } from "./path";
|
|
5
|
+
import { skillOf } from "./wire";
|
|
6
|
+
/** True when a report has nothing in it worth telling anyone. */
|
|
7
|
+
export function isQuiet(report) {
|
|
8
|
+
return [report.pulled, report.pushed, report.deletedLocally, report.rejected].every((list) => list.length === 0);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* `skills` is the synced list. Every other skill in the folder is local to it: never pushed, never
|
|
12
|
+
* overwritten, never deleted by a sync.
|
|
13
|
+
*/
|
|
14
|
+
export async function syncOnce(config, folder, state, skills) {
|
|
15
|
+
const report = { pulled: [], pushed: [], deletedLocally: [], rejected: [] };
|
|
16
|
+
mkdirSync(folder, { recursive: true });
|
|
17
|
+
// A skill added to the list since last cycle may have server files older than the cursor; re-read from the start.
|
|
18
|
+
if ([...skills].some((skill) => !(state.skills ?? []).includes(skill)))
|
|
19
|
+
state.cursor = 0;
|
|
20
|
+
state.skills = [...skills].sort();
|
|
21
|
+
// A skill removed from the list stops being tracked here. Left in `known`, its files would look deleted
|
|
22
|
+
// from this folder and the push would delete them on every machine.
|
|
23
|
+
for (const path of Object.keys(state.known)) {
|
|
24
|
+
if (!skills.has(skillOf(path)))
|
|
25
|
+
delete state.known[path];
|
|
26
|
+
}
|
|
27
|
+
const present = new Set(listLocalFiles(folder, config.exclude).filter((path) => skills.has(skillOf(path))));
|
|
28
|
+
// Files deleted here since the last cycle. When they were deleted is not recorded anywhere, so the
|
|
29
|
+
// deletion counts as an edit made now: the pull leaves these paths alone and the push deletes them.
|
|
30
|
+
const deletedHere = new Set(Object.keys(state.known).filter((path) => !present.has(path)));
|
|
31
|
+
const ctx = { config, folder, state, report, skills };
|
|
32
|
+
await pull(ctx, deletedHere, present);
|
|
33
|
+
await push(ctx, present);
|
|
34
|
+
return report;
|
|
35
|
+
}
|
|
36
|
+
/** When this file was last edited here, to the millisecond the server stores. */
|
|
37
|
+
const editedAt = (file) => Math.floor(statSync(file).mtimeMs);
|
|
38
|
+
async function pull(ctx, deletedHere, present) {
|
|
39
|
+
const { config, folder, state, report } = ctx;
|
|
40
|
+
const body = await (await call(config, `/files?since=${state.cursor}`)).json();
|
|
41
|
+
for (const file of body.files) {
|
|
42
|
+
// The server is not trusted to supply a path either: one bad row would otherwise write outside the
|
|
43
|
+
// skills directory on every machine that syncs.
|
|
44
|
+
const badPath = invalidPathReason(file.path);
|
|
45
|
+
if (badPath) {
|
|
46
|
+
report.rejected.push(`${file.path} (${badPath})`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (!ctx.skills.has(skillOf(file.path)))
|
|
50
|
+
continue;
|
|
51
|
+
// Deleted here, which is newer than anything the server has; the push sends the deletion.
|
|
52
|
+
if (deletedHere.has(file.path))
|
|
53
|
+
continue;
|
|
54
|
+
const local = join(folder, file.path);
|
|
55
|
+
const exists = existsSync(local);
|
|
56
|
+
const localHash = exists ? hashBytes(readFileSync(local)) : undefined;
|
|
57
|
+
if (localHash === (file.hash ?? undefined)) {
|
|
58
|
+
if (file.hash === null)
|
|
59
|
+
delete state.known[file.path];
|
|
60
|
+
else
|
|
61
|
+
state.known[file.path] = file.hash;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
// Edited here since this folder last agreed with the server, and later than the server's version:
|
|
65
|
+
// this folder's edit wins, and the push sends it.
|
|
66
|
+
if (exists && localHash !== state.known[file.path] && editedAt(local) > file.editedAt)
|
|
67
|
+
continue;
|
|
68
|
+
await apply(ctx, file, present);
|
|
69
|
+
}
|
|
70
|
+
state.cursor = body.cursor;
|
|
71
|
+
}
|
|
72
|
+
async function push(ctx, present) {
|
|
73
|
+
const { config, folder, state, report } = ctx;
|
|
74
|
+
for (const path of [...present]) {
|
|
75
|
+
const file = join(folder, path);
|
|
76
|
+
const bytes = readFileSync(file);
|
|
77
|
+
const hash = hashBytes(bytes);
|
|
78
|
+
if (state.known[path] === hash)
|
|
79
|
+
continue;
|
|
80
|
+
const later = await put(config, { path, hash, content: Buffer.from(bytes).toString("base64"), editedAt: editedAt(file) });
|
|
81
|
+
if (later === undefined) {
|
|
82
|
+
state.known[path] = hash;
|
|
83
|
+
report.pushed.push(path);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
await apply(ctx, { path, ...later }, present);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// A path this folder agreed on that is no longer on disk was deleted here. The server keeps the
|
|
90
|
+
// deletion, or the next machine to sync would upload the file again.
|
|
91
|
+
for (const path of Object.keys(state.known)) {
|
|
92
|
+
if (present.has(path))
|
|
93
|
+
continue;
|
|
94
|
+
const later = await put(config, { path, hash: null, content: null, editedAt: Date.now() });
|
|
95
|
+
if (later === undefined) {
|
|
96
|
+
delete state.known[path];
|
|
97
|
+
report.pushed.push(`${path} (deleted)`);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
await apply(ctx, { path, ...later }, present);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Writes the server's version of a file here: its bytes with its edit time, or a deletion. Used by the
|
|
106
|
+
* pull, and by the push when the server refuses a write because it holds a later edit, so a refused write
|
|
107
|
+
* settles at once rather than waiting for a pull that may never list the file again.
|
|
108
|
+
*/
|
|
109
|
+
async function apply(ctx, file, present) {
|
|
110
|
+
const { config, folder, state, report } = ctx;
|
|
111
|
+
const local = join(folder, file.path);
|
|
112
|
+
if (file.hash === null) {
|
|
113
|
+
rmSync(local, { force: true });
|
|
114
|
+
removeEmptyParents(folder, local);
|
|
115
|
+
present.delete(file.path);
|
|
116
|
+
delete state.known[file.path];
|
|
117
|
+
report.deletedLocally.push(file.path);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const content = new Uint8Array(await (await call(config, `/content/${file.hash}`)).arrayBuffer());
|
|
121
|
+
mkdirSync(dirname(local), { recursive: true });
|
|
122
|
+
writeFileSync(local, content);
|
|
123
|
+
utimesSync(local, new Date(), new Date(file.editedAt));
|
|
124
|
+
state.known[file.path] = file.hash;
|
|
125
|
+
present.add(file.path);
|
|
126
|
+
report.pulled.push(file.path);
|
|
127
|
+
}
|
|
128
|
+
/** A deleted skill leaves no empty directory behind, which would still show up as a skill. */
|
|
129
|
+
function removeEmptyParents(folder, file) {
|
|
130
|
+
for (let dir = dirname(file); dir !== folder && dir.startsWith(folder); dir = dirname(dir)) {
|
|
131
|
+
if (readdirSync(dir).length > 0)
|
|
132
|
+
return;
|
|
133
|
+
rmdirSync(dir);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** Undefined when the server took the write; otherwise the later version it holds instead. */
|
|
137
|
+
async function put(config, body) {
|
|
138
|
+
const request = { ...body, machine: config.machine };
|
|
139
|
+
const response = await fetch(`${config.endpoint}/files`, {
|
|
140
|
+
method: "PUT",
|
|
141
|
+
headers: { authorization: `Bearer ${config.token}`, "content-type": "application/json" },
|
|
142
|
+
body: JSON.stringify(request),
|
|
143
|
+
});
|
|
144
|
+
if (response.status === 409) {
|
|
145
|
+
const current = await response.json();
|
|
146
|
+
return { hash: current.currentHash, editedAt: current.currentEditedAt };
|
|
147
|
+
}
|
|
148
|
+
if (!response.ok)
|
|
149
|
+
throw new Error(`PUT /files ${body.path}: ${response.status} ${await response.text()}`);
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
async function call(config, path) {
|
|
153
|
+
const response = await fetch(`${config.endpoint}${path}`, {
|
|
154
|
+
headers: { authorization: `Bearer ${config.token}` },
|
|
155
|
+
});
|
|
156
|
+
if (!response.ok)
|
|
157
|
+
throw new Error(`GET ${path}: ${response.status} ${await response.text()}`);
|
|
158
|
+
return response;
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=sync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync.js","sourceRoot":"","sources":["../../../../src/internal/sync.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shapes the daemon and the service exchange.
|
|
3
|
+
*
|
|
4
|
+
* Declared in the library and imported by the service so there is one definition rather than two that
|
|
5
|
+
* drift. The service depends on the library; nothing goes the other way.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* A skill is a top-level entry of a skills folder — usually a directory like `jive/` holding `SKILL.md`
|
|
9
|
+
* and its files — and it is the unit of opting in: all of its files sync, or none do, so a skill never
|
|
10
|
+
* arrives half-present on another machine.
|
|
11
|
+
*/
|
|
12
|
+
export const skillOf = (path) => path.split("/")[0];
|
|
13
|
+
//# sourceMappingURL=wire.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wire.js","sourceRoot":"","sources":["../../../../src/internal/wire.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAgCH;;;;GAIG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,IAAY,EAAU,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC"}
|