@jive-sh/skill-sync 0.1.2-latest-linux-arm64 → 0.1.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.
@@ -0,0 +1,135 @@
1
+ import { createServer } from "node:http";
2
+ import { spawn } from "node:child_process";
3
+ import { existsSync, rmSync, writeFileSync } from "node:fs";
4
+ import { Command } from "commander";
5
+ import { CONFIG_PATH, TOKEN_PATH, loadConfig, loadSettings, loadState, saveState } from "../internal/config";
6
+ import { syncOnce } from "../internal/sync";
7
+ /**
8
+ * The CLI that accompanies the daemon.
9
+ *
10
+ * The daemon never authenticates anyone — it reads a token the CLI left behind and nothing more. Signing
11
+ * in needs a browser and a person, which a process started at login has neither of, and a daemon that
12
+ * could prompt for credentials would be a daemon that prompts you at every login.
13
+ */
14
+ const program = new Command("skill-sync")
15
+ .description("keep one skills directory identical across every machine you work on");
16
+ program
17
+ .command("login")
18
+ .description("verify with your passkey in the browser and store this machine's sync token")
19
+ .action(async () => {
20
+ // A missing token is exactly what we are here to fix, so only the settings have to be usable.
21
+ const settings = loadSettings();
22
+ if (settings === undefined) {
23
+ console.error(`Fill in ${CONFIG_PATH} with an endpoint, a machine name and a skillsDir first.`);
24
+ process.exit(1);
25
+ }
26
+ const token = await captureToken(settings.endpoint, settings.machine);
27
+ writeFileSync(TOKEN_PATH, `${JSON.stringify({ token }, null, 2)}\n`, { mode: 0o600 });
28
+ console.log(`Signed in. Token saved to ${TOKEN_PATH}`);
29
+ });
30
+ program
31
+ .command("logout")
32
+ .description("forget this machine's sync token")
33
+ .action(async () => {
34
+ const loaded = loadConfig();
35
+ if (loaded.ok) {
36
+ // Revoke first: if this fails, the token is still on disk and you can try again. Deleting it first
37
+ // would leave a live token on the server that nobody can revoke any more.
38
+ const response = await fetch(`${loaded.config.endpoint}/cli/token`, {
39
+ method: "DELETE",
40
+ headers: { authorization: `Bearer ${loaded.config.token}` },
41
+ });
42
+ console.log(response.ok ? "Revoked on the server." : `Server revoke failed (${response.status}).`);
43
+ }
44
+ rmSync(TOKEN_PATH, { force: true });
45
+ console.log(`Removed ${TOKEN_PATH}`);
46
+ });
47
+ program
48
+ .command("status")
49
+ .description("show what this machine is configured to sync, and whether it can")
50
+ .action(async () => {
51
+ const loaded = loadConfig();
52
+ if (!loaded.ok) {
53
+ console.log(`Not ready: ${loaded.reason}`);
54
+ return;
55
+ }
56
+ const { endpoint, skillsDir, machine } = loaded.config;
57
+ console.log(`machine ${machine}`);
58
+ console.log(`skills ${skillsDir}${existsSync(skillsDir) ? "" : " (does not exist)"}`);
59
+ console.log(`endpoint ${endpoint}`);
60
+ console.log(`settings ${CONFIG_PATH}`);
61
+ console.log(`cursor ${loadState().cursor}`);
62
+ const response = await fetch(`${endpoint}/files?since=0`, {
63
+ headers: { authorization: `Bearer ${loaded.config.token}` },
64
+ });
65
+ console.log(response.ok
66
+ ? `server reachable, ${(await response.json()).files.length} file(s)`
67
+ : `server ${response.status} ${response.statusText} — try \`skill-sync login\``);
68
+ });
69
+ program
70
+ .command("sync")
71
+ .description("run one sync cycle now instead of waiting for the daemon")
72
+ .action(async () => {
73
+ const loaded = loadConfig();
74
+ if (!loaded.ok) {
75
+ console.error(`Not ready: ${loaded.reason}`);
76
+ process.exit(1);
77
+ }
78
+ const state = loadState();
79
+ const report = await syncOnce(loaded.config, state);
80
+ saveState(state);
81
+ for (const path of report.pulled)
82
+ console.log(`pulled ${path}`);
83
+ for (const path of report.pushed)
84
+ console.log(`pushed ${path}`);
85
+ for (const path of report.deletedLocally)
86
+ console.log(`deleted ${path}`);
87
+ for (const path of report.conflicts)
88
+ console.log(`CONFLICT ${path} — your copy is at ${path}.local`);
89
+ for (const path of report.rejected)
90
+ console.log(`REJECTED ${path}`);
91
+ if (Object.values(report).every((list) => list.length === 0))
92
+ console.log("already in sync");
93
+ });
94
+ program.parse();
95
+ if (process.argv.length <= 2)
96
+ program.help();
97
+ /**
98
+ * Opens the browser at the service's login endpoint and waits for it to come back with a token.
99
+ *
100
+ * The browser does the authenticating, with a passkey for the service's domain; this only has to be
101
+ * listening when it finishes. The listener is bound to 127.0.0.1 so nothing off this machine can receive
102
+ * the redirect, and the port is whatever the OS hands out so two machines — or two attempts — never
103
+ * contend for one.
104
+ */
105
+ async function captureToken(endpoint, machine) {
106
+ return await new Promise((resolve, reject) => {
107
+ const server = createServer((request, response) => {
108
+ const token = new URL(request.url ?? "/", "http://127.0.0.1").searchParams.get("token");
109
+ response.writeHead(200, { "content-type": "text/html" });
110
+ response.end(token
111
+ ? "<p>Signed in. You can close this tab.</p>"
112
+ : "<p>No token in the redirect. Check the service logs.</p>");
113
+ server.close();
114
+ token ? resolve(token) : reject(new Error("the redirect carried no token"));
115
+ });
116
+ server.listen(0, "127.0.0.1", () => {
117
+ const port = server.address().port;
118
+ const url = `${endpoint}/cli/login?port=${port}&machine=${encodeURIComponent(machine)}`;
119
+ console.log(`Opening ${url}`);
120
+ openBrowser(url);
121
+ });
122
+ setTimeout(() => {
123
+ server.close();
124
+ reject(new Error("timed out waiting for the browser"));
125
+ }, 180_000).unref();
126
+ });
127
+ }
128
+ function openBrowser(url) {
129
+ const command = process.platform === "darwin" ? "open"
130
+ : process.platform === "win32" ? "cmd"
131
+ : "xdg-open";
132
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
133
+ spawn(command, args, { stdio: "ignore", detached: true }).unref();
134
+ }
135
+ //# sourceMappingURL=skill-sync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skill-sync.js","sourceRoot":"","sources":["../../../../src/entrypoints/skill-sync.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * What the service needs from this package.
3
+ *
4
+ * The daemon and CLI are the product; this entrypoint exists so the service can share the path rules
5
+ * and wire types rather than restate them. A rule restated is a rule that drifts, and these two are
6
+ * exactly the pair that must not: one validates what the other writes to disk.
7
+ */
8
+ export { invalidPathReason, isValidPath } from "./internal/path";
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,135 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdirSync, readFileSync, writeFileSync, existsSync, readdirSync, statSync } from "node:fs";
3
+ import { homedir, hostname } from "node:os";
4
+ import { join, relative, sep } from "node:path";
5
+ /**
6
+ * Per-machine settings and sync state, both under `~/.jive/`.
7
+ *
8
+ * A file rather than environment variables: which directory holds this machine's skills, and which
9
+ * machine this is, differ per computer and have to survive a login, which is exactly what a config file
10
+ * is for and exactly what an environment variable is not.
11
+ */
12
+ const JIVE_HOME = join(homedir(), ".jive");
13
+ export const CONFIG_PATH = join(JIVE_HOME, "skill-sync.json");
14
+ /**
15
+ * The token lives on its own, outside `~/.jive/`, because it is the one part that must not travel with
16
+ * the rest. Endpoint, machine name and skills directory are settings you could paste into a gist; this
17
+ * is a credential, and keeping it in a separate file makes "do not copy this one" a property of the
18
+ * filesystem rather than a thing you have to remember.
19
+ */
20
+ export const TOKEN_PATH = join(homedir(), ".skill-sync-token.json");
21
+ const STATE_PATH = join(JIVE_HOME, "skill-sync-state.json");
22
+ /** Managed by Claude's own cloud sync; see `Config.exclude`. */
23
+ const DEFAULT_EXCLUDE = ["synced"];
24
+ /**
25
+ * The settings alone, with no token required.
26
+ *
27
+ * `login` needs these precisely when the token is missing, so it cannot go through `loadConfig` — that
28
+ * reports "not configured" for a missing token and would refuse to run the command that supplies one.
29
+ */
30
+ export function loadSettings() {
31
+ loadConfig();
32
+ const settings = readJson(CONFIG_PATH);
33
+ if (!settings.endpoint || !settings.machine || !settings.skillsDir)
34
+ return undefined;
35
+ return { endpoint: settings.endpoint, machine: settings.machine, skillsDir: settings.skillsDir };
36
+ }
37
+ /** Reads both files, writing a template for whichever is missing, and says what is still needed. */
38
+ export function loadConfig() {
39
+ if (!existsSync(CONFIG_PATH)) {
40
+ mkdirSync(JIVE_HOME, { recursive: true });
41
+ writeFileSync(CONFIG_PATH, `${JSON.stringify({
42
+ endpoint: "https://skill-sync.jive.sh",
43
+ skillsDir: detectSkillsDir(),
44
+ machine: machineName(),
45
+ exclude: DEFAULT_EXCLUDE,
46
+ }, null, 2)}\n`);
47
+ }
48
+ if (!existsSync(TOKEN_PATH)) {
49
+ // 0600 from the moment it exists, rather than written open and tightened afterwards — there is no
50
+ // window in between for anything else on the machine to read it.
51
+ writeFileSync(TOKEN_PATH, `${JSON.stringify({ token: "" }, null, 2)}\n`, { mode: 0o600 });
52
+ }
53
+ const settings = readJson(CONFIG_PATH);
54
+ const token = readJson(TOKEN_PATH).token;
55
+ if (!settings.endpoint || !settings.skillsDir || !settings.machine) {
56
+ return { ok: false, reason: `${CONFIG_PATH} needs an endpoint, a skillsDir and a machine name` };
57
+ }
58
+ if (!token) {
59
+ return { ok: false, reason: `${TOKEN_PATH} needs the sync token` };
60
+ }
61
+ return { ok: true, config: { exclude: DEFAULT_EXCLUDE, ...settings, token } };
62
+ }
63
+ function readJson(path) {
64
+ try {
65
+ return JSON.parse(readFileSync(path, "utf-8"));
66
+ }
67
+ catch {
68
+ return {};
69
+ }
70
+ }
71
+ export function loadState() {
72
+ try {
73
+ return JSON.parse(readFileSync(STATE_PATH, "utf-8"));
74
+ }
75
+ catch {
76
+ return { cursor: 0, known: {} };
77
+ }
78
+ }
79
+ export function saveState(state) {
80
+ mkdirSync(JIVE_HOME, { recursive: true });
81
+ writeFileSync(STATE_PATH, `${JSON.stringify(state, 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 directory this machine actually has, rather than the one it ought to have.
89
+ *
90
+ * Claude's home is not always `~/.claude` — an account with a suffixed home (`~/.claude-alex`) keeps its
91
+ * skills there, and writing the conventional path into the template just hands the user a wrong value to
92
+ * notice and correct. Pick the one that exists; fall back to the conventional path only if none does.
93
+ */
94
+ function detectSkillsDir() {
95
+ const candidates = readdirSync(homedir())
96
+ .filter((entry) => entry === ".claude" || entry.startsWith(".claude-"))
97
+ .map((entry) => join(homedir(), entry, "skills"))
98
+ .filter((dir) => existsSync(dir))
99
+ .sort();
100
+ return candidates[0] ?? join(homedir(), ".claude", "skills");
101
+ }
102
+ export function hashBytes(bytes) {
103
+ return createHash("sha256").update(bytes).digest("hex");
104
+ }
105
+ /** Suffix for the copy kept when an incoming change would have overwritten a local edit. */
106
+ export const CONFLICT_SUFFIX = ".local";
107
+ /**
108
+ * Every file under the skills directory, as paths relative to it with forward slashes.
109
+ *
110
+ * Normalised because the same skill synced from Windows and macOS must be one row, not two.
111
+ *
112
+ * Conflict copies are skipped. They are this machine's evidence of a collision, not content anyone else
113
+ * asked for, and syncing them spreads one machine's mess to every other machine.
114
+ */
115
+ export function listLocalFiles(skillsDir, exclude = DEFAULT_EXCLUDE) {
116
+ if (!existsSync(skillsDir))
117
+ return [];
118
+ const found = [];
119
+ const walk = (dir, top) => {
120
+ for (const entry of readdirSync(dir)) {
121
+ if (entry.startsWith("."))
122
+ continue;
123
+ if (top && exclude.includes(entry))
124
+ continue;
125
+ const full = join(dir, entry);
126
+ if (statSync(full).isDirectory())
127
+ walk(full, false);
128
+ else if (!entry.endsWith(CONFLICT_SUFFIX))
129
+ found.push(relative(skillsDir, full).split(sep).join("/"));
130
+ }
131
+ };
132
+ walk(skillsDir, true);
133
+ return found.sort();
134
+ }
135
+ //# 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,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAEhD;;;;;;GAMG;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;;;;;GAKG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,wBAAwB,CAAC,CAAC;AACpE,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,EAAE,uBAAuB,CAAC,CAAC;AAE5D,gEAAgE;AAChE,MAAM,eAAe,GAAG,CAAC,QAAQ,CAAU,CAAC;AA6B5C;;;;;GAKG;AACH,MAAM,UAAU,YAAY;IAC1B,UAAU,EAAE,CAAC;IACb,MAAM,QAAQ,GAAG,QAAQ,CAAkB,WAAW,CAAC,CAAC;IACxD,IAAI,CAAC,QAAQ,CAAC,QAAQ,IAAI,CAAC,QAAQ,CAAC,OAAO,IAAI,CAAC,QAAQ,CAAC,SAAS;QAAE,OAAO,SAAS,CAAC;IACrF,OAAO,EAAE,QAAQ,EAAE,QAAQ,CAAC,QAAQ,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,SAAS,EAAE,QAAQ,CAAC,SAAS,EAAE,CAAC;AACnG,CAAC;AAED,oGAAoG;AACpG,MAAM,UAAU,UAAU;IACxB,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,SAAS,EAAE,eAAe,EAAE;YAC5B,OAAO,EAAE,WAAW,EAAE;YACtB,OAAO,EAAE,eAAe;SACzB,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;IACnB,CAAC;IAED,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5B,kGAAkG;QAClG,iEAAiE;QACjE,aAAa,CAAC,UAAU,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC5F,CAAC;IAED,MAAM,QAAQ,GAAG,QAAQ,CAAkB,WAAW,CAAC,CAAC;IACxD,MAAM,KAAK,GAAG,QAAQ,CAAqB,UAAU,CAAC,CAAC,KAAK,CAAC;IAE7D,IAAI,CAAC,QAAQ,CAAC,QAAQ,IAAI,CAAC,QAAQ,CAAC,SAAS,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;QACnE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,WAAW,oDAAoD,EAAE,CAAC;IACnG,CAAC;IACD,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,UAAU,uBAAuB,EAAE,CAAC;IACrE,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,eAAe,EAAE,GAAG,QAAQ,EAAE,KAAK,EAAY,EAAE,CAAC;AAC1F,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,MAAM,UAAU,SAAS;IACvB,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAU,CAAC;IAChE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;IAClC,CAAC;AACH,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAY;IACpC,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1C,aAAa,CAAC,UAAU,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;AACnE,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;;;;;;GAMG;AACH,SAAS,eAAe;IACtB,MAAM,UAAU,GAAG,WAAW,CAAC,OAAO,EAAE,CAAC;SACtC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC;SACtE,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;SAChD,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;SAChC,IAAI,EAAE,CAAC;IACV,OAAO,UAAU,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;AAC/D,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,4FAA4F;AAC5F,MAAM,CAAC,MAAM,eAAe,GAAG,QAAQ,CAAC;AAExC;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,SAAiB,EAAE,UAAiC,eAAe;IAChG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;QAAE,OAAO,EAAE,CAAC;IAEtC,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;iBAC/C,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,eAAe,CAAC;gBAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QACxG,CAAC;IACH,CAAC,CAAC;IACF,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IACtB,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC;AACtB,CAAC"}
@@ -0,0 +1,50 @@
1
+ import { spawn } from "node:child_process";
2
+ /**
3
+ * A desktop notification, best effort.
4
+ *
5
+ * Best effort is the whole contract: a daemon that cannot tell you about a conflict is worse than one
6
+ * that cannot sync, but a daemon that *dies* because a notification helper is missing is worse than
7
+ * both. Every failure here is swallowed.
8
+ *
9
+ * This works because these daemons are registered per-user and run inside the logged-in session — a
10
+ * LaunchAgent, a `systemctl --user` unit, a scheduled task. A system-wide service would have no desktop
11
+ * to post to.
12
+ */
13
+ /** No shell anywhere below: the arguments carry file paths, and a path is not a safe thing to interpolate. */
14
+ function run(command, args) {
15
+ try {
16
+ spawn(command, [...args], { stdio: "ignore", detached: true }).unref();
17
+ }
18
+ catch {
19
+ // Swallowed on purpose; see above.
20
+ }
21
+ }
22
+ export function notify(title, body) {
23
+ switch (process.platform) {
24
+ case "darwin":
25
+ // AppleScript string literals take double quotes and backslashes as escapes, and nothing else.
26
+ return run("osascript", [
27
+ "-e",
28
+ `display notification "${escapeAppleScript(body)}" with title "${escapeAppleScript(title)}"`,
29
+ ]);
30
+ case "linux":
31
+ return run("notify-send", [title, body]);
32
+ case "win32":
33
+ return run("powershell", [
34
+ "-NoProfile", "-NonInteractive", "-Command",
35
+ "[reflection.assembly]::LoadWithPartialName('System.Windows.Forms') > $null;"
36
+ + "$n = New-Object System.Windows.Forms.NotifyIcon;"
37
+ + "$n.Icon = [System.Drawing.SystemIcons]::Warning; $n.Visible = $true;"
38
+ + `$n.ShowBalloonTip(10000, '${escapeSingleQuotes(title)}', '${escapeSingleQuotes(body)}', 'Warning')`,
39
+ ]);
40
+ default:
41
+ return;
42
+ }
43
+ }
44
+ function escapeAppleScript(value) {
45
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
46
+ }
47
+ function escapeSingleQuotes(value) {
48
+ return value.replace(/'/g, "''");
49
+ }
50
+ //# sourceMappingURL=notify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notify.js","sourceRoot":"","sources":["../../../../src/internal/notify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3C;;;;;;;;;;GAUG;AAEH,8GAA8G;AAC9G,SAAS,GAAG,CAAC,OAAe,EAAE,IAA2B;IACvD,IAAI,CAAC;QACH,KAAK,CAAC,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;IACzE,CAAC;IAAC,MAAM,CAAC;QACP,mCAAmC;IACrC,CAAC;AACH,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,KAAa,EAAE,IAAY;IAChD,QAAQ,OAAO,CAAC,QAAQ,EAAE,CAAC;QACzB,KAAK,QAAQ;YACX,+FAA+F;YAC/F,OAAO,GAAG,CAAC,WAAW,EAAE;gBACtB,IAAI;gBACJ,yBAAyB,iBAAiB,CAAC,IAAI,CAAC,iBAAiB,iBAAiB,CAAC,KAAK,CAAC,GAAG;aAC7F,CAAC,CAAC;QACL,KAAK,OAAO;YACV,OAAO,GAAG,CAAC,aAAa,EAAE,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC;QAC3C,KAAK,OAAO;YACV,OAAO,GAAG,CAAC,YAAY,EAAE;gBACvB,YAAY,EAAE,iBAAiB,EAAE,UAAU;gBAC3C,6EAA6E;sBAC3E,kDAAkD;sBAClD,sEAAsE;sBACtE,6BAA6B,kBAAkB,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,IAAI,CAAC,eAAe;aACvG,CAAC,CAAC;QACL;YACE,OAAO;IACX,CAAC;AACH,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAa;IACtC,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;AAC3D,CAAC;AAED,SAAS,kBAAkB,CAAC,KAAa;IACvC,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AACnC,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,131 @@
1
+ import { mkdirSync, readFileSync, rmSync, writeFileSync, existsSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { CONFLICT_SUFFIX, hashBytes, listLocalFiles } from "./config";
4
+ import { invalidPathReason } from "./path";
5
+ export async function syncOnce(config, state) {
6
+ const report = { pulled: [], pushed: [], deletedLocally: [], conflicts: [], rejected: [] };
7
+ const present = new Set(listLocalFiles(config.skillsDir, config.exclude));
8
+ /**
9
+ * Files deleted here since the last cycle, worked out before anything is fetched.
10
+ *
11
+ * A push does not advance this machine's cursor, so the next pull sees its own earlier write as news.
12
+ * With the file now gone from disk that looked like "remote has it, we do not" and the pull put it
13
+ * straight back, undoing the delete every time. Deciding what was deleted up front, and refusing to
14
+ * recreate those paths, is what lets the push below turn them into tombstones.
15
+ */
16
+ const pendingDeletes = new Set(Object.keys(state.known).filter((path) => !present.has(path)));
17
+ // `present` is passed through both passes and kept current: the pull removes tombstoned files and adds
18
+ // new ones, and a push that read a stale snapshot would try to open a file the pull had just deleted.
19
+ await pull(config, state, report, pendingDeletes, present);
20
+ await push(config, state, report, present);
21
+ return report;
22
+ }
23
+ async function pull(config, state, report, pendingDeletes, present) {
24
+ const response = await call(config, `/files?since=${state.cursor}`);
25
+ const body = await response.json();
26
+ for (const file of body.files) {
27
+ // The server is not trusted to supply a path either: one bad row would otherwise write outside the
28
+ // skills directory on every machine that syncs.
29
+ const badPath = invalidPathReason(file.path);
30
+ if (badPath) {
31
+ report.rejected.push(`${file.path} (${badPath})`);
32
+ continue;
33
+ }
34
+ const local = join(config.skillsDir, file.path);
35
+ // Deleted here and not yet reported; the push pass owns this path for the rest of the cycle.
36
+ if (pendingDeletes.has(file.path))
37
+ continue;
38
+ if (file.deleted) {
39
+ if (existsSync(local)) {
40
+ // Never destroy content this machine never agreed with the server on. A file written here that
41
+ // the server happens to have tombstoned — a fresh machine, or one that was offline through the
42
+ // delete — is not the same file, and deleting it silently would lose work nobody could recover.
43
+ const localHash = hashBytes(readFileSync(local));
44
+ if (localHash !== state.known[file.path]) {
45
+ writeFileSync(`${local}${CONFLICT_SUFFIX}`, readFileSync(local));
46
+ report.conflicts.push(file.path);
47
+ }
48
+ rmSync(local, { force: true });
49
+ report.deletedLocally.push(file.path);
50
+ }
51
+ present.delete(file.path);
52
+ delete state.known[file.path];
53
+ continue;
54
+ }
55
+ const localHash = existsSync(local) ? hashBytes(readFileSync(local)) : undefined;
56
+ if (localHash === file.hash) {
57
+ // Already identical — record agreement so the push pass does not treat it as a local edit.
58
+ state.known[file.path] = file.hash;
59
+ continue;
60
+ }
61
+ // A local edit we have not pushed yet would be destroyed by overwriting. Keep it beside the incoming
62
+ // copy and let the push pass raise the conflict rather than deciding which one wins here.
63
+ if (localHash !== undefined && localHash !== state.known[file.path]) {
64
+ writeFileSync(`${local}${CONFLICT_SUFFIX}`, readFileSync(local));
65
+ report.conflicts.push(file.path);
66
+ }
67
+ const content = new Uint8Array(await (await call(config, `/content/${file.hash}`)).arrayBuffer());
68
+ mkdirSync(dirname(local), { recursive: true });
69
+ writeFileSync(local, content);
70
+ state.known[file.path] = file.hash;
71
+ present.add(file.path);
72
+ report.pulled.push(file.path);
73
+ }
74
+ state.cursor = body.cursor;
75
+ }
76
+ async function push(config, state, report, present) {
77
+ for (const path of present) {
78
+ const bytes = readFileSync(join(config.skillsDir, path));
79
+ const hash = hashBytes(bytes);
80
+ if (state.known[path] === hash)
81
+ continue;
82
+ const accepted = await put(config, {
83
+ path,
84
+ baseHash: state.known[path] ?? null,
85
+ hash,
86
+ content: Buffer.from(bytes).toString("base64"),
87
+ });
88
+ if (accepted) {
89
+ state.known[path] = hash;
90
+ report.pushed.push(path);
91
+ }
92
+ else {
93
+ report.conflicts.push(path);
94
+ }
95
+ }
96
+ // A path we previously agreed on that is no longer on disk was deleted here, and the server needs the
97
+ // tombstone or the next machine to sync will upload it back.
98
+ for (const path of Object.keys(state.known)) {
99
+ if (present.has(path))
100
+ continue;
101
+ if (await put(config, { path, baseHash: state.known[path], hash: null, content: null })) {
102
+ delete state.known[path];
103
+ report.pushed.push(`${path} (deleted)`);
104
+ }
105
+ else {
106
+ report.conflicts.push(path);
107
+ }
108
+ }
109
+ }
110
+ /** True when the server accepted the write; false means another machine got there first. */
111
+ async function put(config, body) {
112
+ const response = await fetch(`${config.endpoint}/files`, {
113
+ method: "PUT",
114
+ headers: { authorization: `Bearer ${config.token}`, "content-type": "application/json" },
115
+ body: JSON.stringify({ ...body, machine: config.machine }),
116
+ });
117
+ if (response.status === 409)
118
+ return false;
119
+ if (!response.ok)
120
+ throw new Error(`PUT /files ${body.path}: ${response.status} ${await response.text()}`);
121
+ return true;
122
+ }
123
+ async function call(config, path) {
124
+ const response = await fetch(`${config.endpoint}${path}`, {
125
+ headers: { authorization: `Bearer ${config.token}` },
126
+ });
127
+ if (!response.ok)
128
+ throw new Error(`GET ${path}: ${response.status} ${await response.text()}`);
129
+ return response;
130
+ }
131
+ //# 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,7 @@
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
+ //# sourceMappingURL=wire.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wire.js","sourceRoot":"","sources":["../../../../src/internal/wire.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}