claude-settings-sync 0.3.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.
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ claudeDir,
4
+ toGistFilename
5
+ } from "./chunk-VBOSEAEH.js";
6
+
7
+ // src/core/scanner.ts
8
+ import { readFileSync, readdirSync, statSync, existsSync } from "fs";
9
+ import { join, relative } from "path";
10
+ var SYNC_TARGETS = [
11
+ // settings
12
+ { path: "settings.json", category: "settings" },
13
+ { path: "keybindings.json", category: "settings" },
14
+ { path: "policy-limits.json", category: "settings" },
15
+ { path: "remote-settings.json", category: "settings" },
16
+ // instructions
17
+ { path: "CLAUDE.md", category: "instructions" },
18
+ // hooks
19
+ { path: "hooks", category: "hooks", isDir: true },
20
+ // skills
21
+ { path: "skills", category: "skills", isDir: true },
22
+ // plugins
23
+ { path: "plugins/installed_plugins.json", category: "plugins" },
24
+ { path: "plugins/known_marketplaces.json", category: "plugins" },
25
+ { path: "plugins/blocklist.json", category: "plugins" },
26
+ // teams
27
+ { path: "teams", category: "teams", isDir: true },
28
+ // ui
29
+ { path: "statusline-command.sh", category: "ui" }
30
+ ];
31
+ var IGNORED_FILES = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db", ".gitkeep"]);
32
+ var IGNORED_EXTENSIONS = /* @__PURE__ */ new Set([".swp", ".swo", ".tmp", ".bak"]);
33
+ function shouldIgnore(name) {
34
+ if (IGNORED_FILES.has(name)) return true;
35
+ for (const ext of IGNORED_EXTENSIONS) {
36
+ if (name.endsWith(ext)) return true;
37
+ }
38
+ return false;
39
+ }
40
+ function collectDir(dirPath, baseDir, category) {
41
+ const files = [];
42
+ if (!existsSync(dirPath)) return files;
43
+ const entries = readdirSync(dirPath, { withFileTypes: true });
44
+ for (const entry of entries) {
45
+ if (shouldIgnore(entry.name)) continue;
46
+ const fullPath = join(dirPath, entry.name);
47
+ if (entry.isDirectory()) {
48
+ files.push(...collectDir(fullPath, baseDir, category));
49
+ } else if (entry.isFile()) {
50
+ const relPath = relative(baseDir, fullPath);
51
+ try {
52
+ const content = readFileSync(fullPath, "utf-8");
53
+ files.push({
54
+ absolutePath: fullPath,
55
+ relativePath: relPath,
56
+ gistFilename: toGistFilename(relPath),
57
+ category,
58
+ content
59
+ });
60
+ } catch {
61
+ }
62
+ }
63
+ }
64
+ return files;
65
+ }
66
+ function scanFiles(filterCategory) {
67
+ const base = claudeDir();
68
+ const files = [];
69
+ for (const target of SYNC_TARGETS) {
70
+ if (filterCategory && target.category !== filterCategory) continue;
71
+ const fullPath = join(base, target.path);
72
+ if (!existsSync(fullPath)) continue;
73
+ if (target.isDir) {
74
+ files.push(...collectDir(fullPath, base, target.category));
75
+ } else {
76
+ const stat = statSync(fullPath);
77
+ if (!stat.isFile()) continue;
78
+ try {
79
+ const content = readFileSync(fullPath, "utf-8");
80
+ files.push({
81
+ absolutePath: fullPath,
82
+ relativePath: target.path,
83
+ gistFilename: toGistFilename(target.path),
84
+ category: target.category,
85
+ content
86
+ });
87
+ } catch {
88
+ }
89
+ }
90
+ }
91
+ return files;
92
+ }
93
+ function listTargets() {
94
+ return [...SYNC_TARGETS];
95
+ }
96
+
97
+ export {
98
+ scanFiles,
99
+ listTargets
100
+ };
101
+ //# sourceMappingURL=chunk-BRTRPVT7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/scanner.ts"],"sourcesContent":["import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';\nimport { join, relative } from 'node:path';\nimport { claudeDir, toGistFilename } from '../utils/paths.js';\nimport type { Category, ScannedFile } from '../types.js';\n\ninterface SyncTarget {\n path: string; // relative to ~/.claude/\n category: Category;\n isDir?: boolean;\n}\n\nconst SYNC_TARGETS: SyncTarget[] = [\n // settings\n { path: 'settings.json', category: 'settings' },\n { path: 'keybindings.json', category: 'settings' },\n { path: 'policy-limits.json', category: 'settings' },\n { path: 'remote-settings.json', category: 'settings' },\n // instructions\n { path: 'CLAUDE.md', category: 'instructions' },\n // hooks\n { path: 'hooks', category: 'hooks', isDir: true },\n // skills\n { path: 'skills', category: 'skills', isDir: true },\n // plugins\n { path: 'plugins/installed_plugins.json', category: 'plugins' },\n { path: 'plugins/known_marketplaces.json', category: 'plugins' },\n { path: 'plugins/blocklist.json', category: 'plugins' },\n // teams\n { path: 'teams', category: 'teams', isDir: true },\n // ui\n { path: 'statusline-command.sh', category: 'ui' },\n];\n\nconst IGNORED_FILES = new Set(['.DS_Store', 'Thumbs.db', '.gitkeep']);\nconst IGNORED_EXTENSIONS = new Set(['.swp', '.swo', '.tmp', '.bak']);\n\nfunction shouldIgnore(name: string): boolean {\n if (IGNORED_FILES.has(name)) return true;\n for (const ext of IGNORED_EXTENSIONS) {\n if (name.endsWith(ext)) return true;\n }\n return false;\n}\n\n/** Recursively collect files from a directory */\nfunction collectDir(dirPath: string, baseDir: string, category: Category): ScannedFile[] {\n const files: ScannedFile[] = [];\n if (!existsSync(dirPath)) return files;\n\n const entries = readdirSync(dirPath, { withFileTypes: true });\n for (const entry of entries) {\n if (shouldIgnore(entry.name)) continue;\n const fullPath = join(dirPath, entry.name);\n if (entry.isDirectory()) {\n files.push(...collectDir(fullPath, baseDir, category));\n } else if (entry.isFile()) {\n const relPath = relative(baseDir, fullPath);\n try {\n const content = readFileSync(fullPath, 'utf-8');\n files.push({\n absolutePath: fullPath,\n relativePath: relPath,\n gistFilename: toGistFilename(relPath),\n category,\n content,\n });\n } catch {\n // skip unreadable files\n }\n }\n }\n return files;\n}\n\n/** Scan ~/.claude/ for syncable files */\nexport function scanFiles(filterCategory?: Category): ScannedFile[] {\n const base = claudeDir();\n const files: ScannedFile[] = [];\n\n for (const target of SYNC_TARGETS) {\n if (filterCategory && target.category !== filterCategory) continue;\n\n const fullPath = join(base, target.path);\n if (!existsSync(fullPath)) continue;\n\n if (target.isDir) {\n files.push(...collectDir(fullPath, base, target.category));\n } else {\n const stat = statSync(fullPath);\n if (!stat.isFile()) continue;\n try {\n const content = readFileSync(fullPath, 'utf-8');\n files.push({\n absolutePath: fullPath,\n relativePath: target.path,\n gistFilename: toGistFilename(target.path),\n category: target.category,\n content,\n });\n } catch {\n // skip unreadable\n }\n }\n }\n\n return files;\n}\n\n/** List all sync targets for display */\nexport function listTargets(): SyncTarget[] {\n return [...SYNC_TARGETS];\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/utils/paths.ts
4
+ import { homedir, hostname } from "os";
5
+ import { join, resolve, normalize } from "path";
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
7
+ import { randomUUID } from "crypto";
8
+ function claudeDir() {
9
+ return join(homedir(), ".claude");
10
+ }
11
+ function configDir() {
12
+ const dir = join(homedir(), ".claudesync");
13
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
14
+ return dir;
15
+ }
16
+ function authFilePath() {
17
+ return join(configDir(), "auth.json");
18
+ }
19
+ function toGistFilename(relativePath) {
20
+ return relativePath.replace(/\//g, "--");
21
+ }
22
+ function fromGistFilename(gistFilename) {
23
+ return gistFilename.replace(/--/g, "/");
24
+ }
25
+ function isPathSafe(relativePath) {
26
+ const normalized = normalize(relativePath);
27
+ if (normalized.startsWith("..") || normalized.startsWith("/")) return false;
28
+ const abs = resolve(claudeDir(), normalized);
29
+ return abs.startsWith(claudeDir());
30
+ }
31
+ function machineName() {
32
+ return process.env.CLAUDESYNC_MACHINE || hostname().split(".")[0];
33
+ }
34
+ function platformString() {
35
+ return `${process.platform}-${process.arch}`;
36
+ }
37
+ function machineIdPath() {
38
+ return join(configDir(), "machine-id");
39
+ }
40
+ function getMachineId() {
41
+ const path = machineIdPath();
42
+ if (existsSync(path)) {
43
+ const id2 = readFileSync(path, "utf-8").trim();
44
+ if (id2) return id2;
45
+ }
46
+ const id = randomUUID();
47
+ writeFileSync(path, id, "utf-8");
48
+ return id;
49
+ }
50
+ function autoConfigPath() {
51
+ return join(configDir(), "auto.json");
52
+ }
53
+ function autoLogPath() {
54
+ return join(configDir(), "auto.log");
55
+ }
56
+ function lockFilePath() {
57
+ return join(configDir(), "sync.lock");
58
+ }
59
+ function pendingNotificationsPath() {
60
+ return join(configDir(), "notifications.json");
61
+ }
62
+ function userConfigPath() {
63
+ return join(configDir(), "config.json");
64
+ }
65
+
66
+ export {
67
+ claudeDir,
68
+ authFilePath,
69
+ toGistFilename,
70
+ fromGistFilename,
71
+ isPathSafe,
72
+ machineName,
73
+ platformString,
74
+ getMachineId,
75
+ autoConfigPath,
76
+ autoLogPath,
77
+ lockFilePath,
78
+ pendingNotificationsPath,
79
+ userConfigPath
80
+ };
81
+ //# sourceMappingURL=chunk-VBOSEAEH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/paths.ts"],"sourcesContent":["import { homedir, hostname } from 'node:os';\nimport { join, resolve, relative, normalize } from 'node:path';\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { randomUUID } from 'node:crypto';\n\n/** ~/.claude */\nexport function claudeDir(): string {\n return join(homedir(), '.claude');\n}\n\n/** ~/.claudesync (our config dir) */\nexport function configDir(): string {\n const dir = join(homedir(), '.claudesync');\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n return dir;\n}\n\n/** ~/.claudesync/auth.json */\nexport function authFilePath(): string {\n return join(configDir(), 'auth.json');\n}\n\n/** Convert a relative path (from ~/.claude/) to a Gist-safe filename.\n * e.g. \"hooks/pre-tool-use.sh\" → \"hooks--pre-tool-use.sh\" */\nexport function toGistFilename(relativePath: string): string {\n return relativePath.replace(/\\//g, '--');\n}\n\n/** Convert a Gist filename back to a relative path.\n * e.g. \"hooks--pre-tool-use.sh\" → \"hooks/pre-tool-use.sh\" */\nexport function fromGistFilename(gistFilename: string): string {\n return gistFilename.replace(/--/g, '/');\n}\n\n/** Validate that a path doesn't escape ~/.claude/ (traversal prevention) */\nexport function isPathSafe(relativePath: string): boolean {\n const normalized = normalize(relativePath);\n if (normalized.startsWith('..') || normalized.startsWith('/')) return false;\n const abs = resolve(claudeDir(), normalized);\n return abs.startsWith(claudeDir());\n}\n\n/** Get the machine name: env var or hostname */\nexport function machineName(): string {\n return process.env.CLAUDESYNC_MACHINE || hostname().split('.')[0];\n}\n\n/** Get platform string (e.g. \"darwin-arm64\") */\nexport function platformString(): string {\n return `${process.platform}-${process.arch}`;\n}\n\n/** ~/.claudesync/machine-id */\nexport function machineIdPath(): string {\n return join(configDir(), 'machine-id');\n}\n\n/** Get or create a persistent unique machine ID */\nexport function getMachineId(): string {\n const path = machineIdPath();\n if (existsSync(path)) {\n const id = readFileSync(path, 'utf-8').trim();\n if (id) return id;\n }\n const id = randomUUID();\n writeFileSync(path, id, 'utf-8');\n return id;\n}\n\n/** ~/.claudesync/auto.json */\nexport function autoConfigPath(): string {\n return join(configDir(), 'auto.json');\n}\n\n/** ~/.claudesync/auto.log */\nexport function autoLogPath(): string {\n return join(configDir(), 'auto.log');\n}\n\n/** ~/.claudesync/sync.lock */\nexport function lockFilePath(): string {\n return join(configDir(), 'sync.lock');\n}\n\n/** ~/.claudesync/notifications.json */\nexport function pendingNotificationsPath(): string {\n return join(configDir(), 'notifications.json');\n}\n\n/** ~/.claudesync/config.json */\nexport function userConfigPath(): string {\n return join(configDir(), 'config.json');\n}\n"],"mappings":";;;AAAA,SAAS,SAAS,gBAAgB;AAClC,SAAS,MAAM,SAAmB,iBAAiB;AACnD,SAAS,YAAY,WAAW,cAAc,qBAAqB;AACnE,SAAS,kBAAkB;AAGpB,SAAS,YAAoB;AAClC,SAAO,KAAK,QAAQ,GAAG,SAAS;AAClC;AAGO,SAAS,YAAoB;AAClC,QAAM,MAAM,KAAK,QAAQ,GAAG,aAAa;AACzC,MAAI,CAAC,WAAW,GAAG,EAAG,WAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AACxD,SAAO;AACT;AAGO,SAAS,eAAuB;AACrC,SAAO,KAAK,UAAU,GAAG,WAAW;AACtC;AAIO,SAAS,eAAe,cAA8B;AAC3D,SAAO,aAAa,QAAQ,OAAO,IAAI;AACzC;AAIO,SAAS,iBAAiB,cAA8B;AAC7D,SAAO,aAAa,QAAQ,OAAO,GAAG;AACxC;AAGO,SAAS,WAAW,cAA+B;AACxD,QAAM,aAAa,UAAU,YAAY;AACzC,MAAI,WAAW,WAAW,IAAI,KAAK,WAAW,WAAW,GAAG,EAAG,QAAO;AACtE,QAAM,MAAM,QAAQ,UAAU,GAAG,UAAU;AAC3C,SAAO,IAAI,WAAW,UAAU,CAAC;AACnC;AAGO,SAAS,cAAsB;AACpC,SAAO,QAAQ,IAAI,sBAAsB,SAAS,EAAE,MAAM,GAAG,EAAE,CAAC;AAClE;AAGO,SAAS,iBAAyB;AACvC,SAAO,GAAG,QAAQ,QAAQ,IAAI,QAAQ,IAAI;AAC5C;AAGO,SAAS,gBAAwB;AACtC,SAAO,KAAK,UAAU,GAAG,YAAY;AACvC;AAGO,SAAS,eAAuB;AACrC,QAAM,OAAO,cAAc;AAC3B,MAAI,WAAW,IAAI,GAAG;AACpB,UAAMA,MAAK,aAAa,MAAM,OAAO,EAAE,KAAK;AAC5C,QAAIA,IAAI,QAAOA;AAAA,EACjB;AACA,QAAM,KAAK,WAAW;AACtB,gBAAc,MAAM,IAAI,OAAO;AAC/B,SAAO;AACT;AAGO,SAAS,iBAAyB;AACvC,SAAO,KAAK,UAAU,GAAG,WAAW;AACtC;AAGO,SAAS,cAAsB;AACpC,SAAO,KAAK,UAAU,GAAG,UAAU;AACrC;AAGO,SAAS,eAAuB;AACrC,SAAO,KAAK,UAAU,GAAG,WAAW;AACtC;AAGO,SAAS,2BAAmC;AACjD,SAAO,KAAK,UAAU,GAAG,oBAAoB;AAC/C;AAGO,SAAS,iBAAyB;AACvC,SAAO,KAAK,UAAU,GAAG,aAAa;AACxC;","names":["id"]}