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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 cares0
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.ko.md ADDED
@@ -0,0 +1,203 @@
1
+ # claudesync
2
+
3
+ [Claude Code](https://docs.anthropic.com/en/docs/claude-code) 설정을 GitHub Gist로 동기화합니다.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/claudesync)](https://www.npmjs.com/package/claudesync)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
7
+ [![Node.js](https://img.shields.io/badge/Node.js-18%2B-green)](https://nodejs.org)
8
+
9
+ [English](./README.md)
10
+
11
+ ## 왜 만들었나요?
12
+
13
+ Claude Code의 설정(`~/.claude/`)은 기기 간 동기화가 안 됩니다. 커스텀 인스트럭션, 훅, 스킬, 키바인딩 등을 여러 머신에서 매번 다시 설정해야 하죠.
14
+
15
+ **claudesync**는 이 설정들을 비공개 GitHub Gist에 저장해서, 어디서든 한 줄로 복원할 수 있게 해줍니다.
16
+
17
+ - **카테고리별 동기화** — `--only hooks`, `--only skills` 등 필요한 것만 골라서
18
+ - **diff 미리보기** — 적용 전에 뭐가 바뀌는지 정확히 확인
19
+ - **버전 관리** — 리비전 탐색, 원하는 시점으로 롤백
20
+ - **자동 동기화** — OS 스케줄러를 활용한 주기적 push/pull
21
+ - **비밀 탐지** — push 전에 API 키, 토큰, 개인 키를 자동 스캔
22
+ - **암호화** — AES-256-GCM 선택적 파일 암호화
23
+ - **토큰 안전 저장** — macOS Keychain / Linux libsecret / 암호화 파일 fallback
24
+ - **런타임 의존성 제로** — Node.js 내장 모듈만 사용
25
+
26
+ ## 설치
27
+
28
+ ```bash
29
+ npm install -g claudesync
30
+ ```
31
+
32
+ <details>
33
+ <summary>소스에서 빌드</summary>
34
+
35
+ ```bash
36
+ git clone https://github.com/cares0/claudesync.git
37
+ cd claudesync
38
+ npm install
39
+ npm run build
40
+ npm link
41
+ ```
42
+
43
+ </details>
44
+
45
+ **Node.js 18+** 필요.
46
+
47
+ ## 시작하기
48
+
49
+ ```bash
50
+ # 1. GitHub 인증 (브라우저 열림)
51
+ claudesync init
52
+
53
+ # 2. 설정 업로드
54
+ claudesync push -m "첫 동기화"
55
+
56
+ # 3. 다른 머신에서 가져오기
57
+ claudesync init
58
+ claudesync pull
59
+ ```
60
+
61
+ 실행 예시:
62
+
63
+ ```
64
+ $ claudesync push -m "첫 동기화"
65
+ ~/.claude/ 스캔 중...
66
+
67
+ 변경사항:
68
+ + [settings] settings.json
69
+ + [settings] keybindings.json
70
+ + [instructions] CLAUDE.md
71
+ + [hooks] hooks/pre-tool-use.sh
72
+ + [skills] skills/my-skill.md
73
+
74
+ 5개 파일 업로드. 계속할까요? (Y/n) y
75
+
76
+ ✔ Gist에 푸시 완료 (5개 파일)
77
+ https://gist.github.com/you/abc123
78
+ ```
79
+
80
+ ## 동기화 대상
81
+
82
+ | 카테고리 | 파일 |
83
+ |----------|------|
84
+ | **settings** | `settings.json`, `keybindings.json`, `policy-limits.json`, `remote-settings.json` |
85
+ | **instructions** | `CLAUDE.md` |
86
+ | **hooks** | `hooks/` 디렉토리 |
87
+ | **skills** | `skills/` 디렉토리 |
88
+ | **plugins** | `plugins/installed_plugins.json`, `known_marketplaces.json`, `blocklist.json` |
89
+ | **teams** | `teams/` 디렉토리 |
90
+ | **ui** | `statusline-command.sh` |
91
+
92
+ 대화 로그, 세션, 캐시, `~/.claude.json` 등은 동기화하지 **않습니다**.
93
+
94
+ ## 명령어
95
+
96
+ ### 인증
97
+
98
+ ```bash
99
+ claudesync init # OAuth Device Flow (브라우저)
100
+ claudesync init --token # PAT 직접 입력
101
+ claudesync link <gist-id> # 기존 Gist 연결
102
+ ```
103
+
104
+ ### 동기화
105
+
106
+ ```bash
107
+ claudesync push # Gist에 업로드
108
+ claudesync push -m "메시지" # 메시지와 함께 업로드
109
+ claudesync push --only hooks # 단일 카테고리
110
+ claudesync push --encrypt # 암호화 후 업로드
111
+ claudesync push --force # 확인 없이 실행
112
+
113
+ claudesync pull # Gist에서 가져오기 (diff 먼저 표시)
114
+ claudesync pull --only skills # 단일 카테고리
115
+ claudesync pull --force # 확인 없이 실행
116
+ ```
117
+
118
+ ### 비교
119
+
120
+ ```bash
121
+ claudesync diff # 전체 diff: 로컬 vs 원격
122
+ claudesync diff --only settings # 단일 카테고리 diff
123
+ claudesync status # 인증/동기화 상태
124
+ claudesync list # 동기화 대상 파일 목록
125
+ claudesync list --only hooks # 단일 카테고리 목록
126
+ ```
127
+
128
+ ### 히스토리
129
+
130
+ ```bash
131
+ claudesync history # 최근 리비전 (최대 10개)
132
+ claudesync rollback <version> # 특정 리비전으로 복원
133
+ ```
134
+
135
+ ### 자동 동기화
136
+
137
+ ```bash
138
+ claudesync auto # 대화형 설정 (방향, 간격, 카테고리)
139
+ claudesync auto status # 현재 설정 및 최근 로그
140
+ claudesync auto disable # 자동 동기화 중지 및 해제
141
+ ```
142
+
143
+ **launchd** (macOS), **systemd** (Linux), **Task Scheduler** (Windows)를 통해 주기적 동기화를 설정합니다. push/pull 방향, 간격(최소 60초), 카테고리 필터, 암호화, 충돌 정책(덮어쓰기 / 건너뛰기 / 백업)을 지원합니다.
144
+
145
+ ### 설정
146
+
147
+ ```bash
148
+ claudesync config list # 현재 설정 보기
149
+ claudesync config lang ko # 한국어로 변경
150
+ claudesync config lang en # 영어로 변경
151
+ ```
152
+
153
+ ## 옵션
154
+
155
+ | 플래그 | 설명 |
156
+ |--------|------|
157
+ | `-m, --message <msg>` | 동기화 메시지 (`history`에서 확인 가능) |
158
+ | `--only <category>` | 필터: `settings` / `instructions` / `hooks` / `skills` / `plugins` / `teams` / `ui` |
159
+ | `--force` | 확인 프롬프트 건너뛰기 |
160
+ | `--encrypt` | AES-256-GCM으로 암호화 후 업로드 |
161
+ | `--lang ko\|en` | 출력 언어 변경 |
162
+ | `-h, --help` | 도움말 |
163
+ | `-v, --version` | 버전 정보 |
164
+
165
+ ## 작동 방식
166
+
167
+ 설정 파일은 하나의 **비공개 GitHub Gist**에 저장됩니다. 각 파일이 Gist 파일에 1:1로 매핑되며, 디렉토리 구분자는 `--`로 변환됩니다 (예: `hooks/pre-tool-use.sh` → `hooks--pre-tool-use.sh`).
168
+
169
+ Gist 안의 `_meta.json`에 파일 매핑, 카테고리, 암호화 여부, 동기화 시각, 머신 정보, push 메시지가 기록됩니다. Gist의 내장 리비전 히스토리가 `history`와 `rollback`을 지원합니다.
170
+
171
+ ## 보안
172
+
173
+ - **토큰 저장** — macOS Keychain, Linux libsecret, 또는 `chmod 600` 파일
174
+ - **비밀 스캔** — 매 push마다 15개 정규식 패턴 + Shannon 엔트로피 분석
175
+ - **수동 제외** — `# claudesync:redact`를 줄에 추가하면 항상 제외
176
+ - **암호화** — `--encrypt` 옵션으로 AES-256-GCM + scrypt 키 유도 적용
177
+ - **비공개 Gist** — 모든 Gist는 비공개로 생성
178
+ - **경로 순회 방지** — pull/rollback 시 위험한 경로 차단
179
+ - **동시 실행 잠금** — 파일 기반 락으로 자동 동기화 중복 실행 방지
180
+
181
+ ## 자주 묻는 질문
182
+
183
+ **내 설정이 외부에 노출되지는 않나요?**
184
+ Gist는 비공개로 생성됩니다. GitHub 토큰이나 직접 URL이 없으면 접근할 수 없습니다. `--encrypt`로 추가 보호가 가능합니다.
185
+
186
+ **프로젝트별 설정도 동기화할 수 있나요?**
187
+ 아직은 전역 설정(`~/.claude/`)만 지원합니다.
188
+
189
+ **두 머신에서 설정을 각각 수정했으면 어떻게 되나요?**
190
+ `pull` 하면 diff를 먼저 보여줍니다. 기존 파일은 `.bak`으로 백업한 뒤 덮어씁니다.
191
+
192
+ **OAuth 말고 다른 인증 방법은?**
193
+ `claudesync init --token`으로 [Personal Access Token](https://github.com/settings/tokens)을 직접 입력할 수 있습니다. `gist` 권한만 필요합니다.
194
+
195
+ **여러 머신에서 자동 push를 하면 충돌이 생기지 않나요?**
196
+ 자동 동기화는 **primary device**를 추적합니다. primary로 지정된 머신만 자동으로 push하므로 충돌이 방지됩니다.
197
+
198
+ **Windows에서도 되나요?**
199
+ 네. 자동 동기화는 Windows Task Scheduler를 사용합니다. 토큰 저장은 암호화 파일로 fallback됩니다.
200
+
201
+ ## 라이선스
202
+
203
+ [MIT](LICENSE)
package/README.md ADDED
@@ -0,0 +1,203 @@
1
+ # claudesync
2
+
3
+ Sync your [Claude Code](https://docs.anthropic.com/en/docs/claude-code) settings across machines via GitHub Gist.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/claudesync)](https://www.npmjs.com/package/claudesync)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
7
+ [![Node.js](https://img.shields.io/badge/Node.js-18%2B-green)](https://nodejs.org)
8
+
9
+ [한국어](./README.ko.md)
10
+
11
+ ## Why?
12
+
13
+ Claude Code stores settings in `~/.claude/` — instructions, hooks, skills, keybindings, and more. These don't sync between machines.
14
+
15
+ **claudesync** saves them to a private GitHub Gist so you can restore your setup anywhere with one command.
16
+
17
+ - **Selective sync** — push/pull by category (`--only hooks`, `--only skills`)
18
+ - **Diff preview** — see exactly what changes before applying
19
+ - **Version history** — browse revisions and rollback to any state
20
+ - **Auto sync** — scheduled push/pull with OS-native schedulers
21
+ - **Secret detection** — scans for API keys, tokens, private keys before upload
22
+ - **Encryption** — optional AES-256-GCM encryption for all files
23
+ - **Secure token storage** — macOS Keychain / Linux libsecret / encrypted file fallback
24
+ - **Zero runtime dependencies** — Node.js built-ins only
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ npm install -g claudesync
30
+ ```
31
+
32
+ <details>
33
+ <summary>Build from source</summary>
34
+
35
+ ```bash
36
+ git clone https://github.com/cares0/claudesync.git
37
+ cd claudesync
38
+ npm install
39
+ npm run build
40
+ npm link
41
+ ```
42
+
43
+ </details>
44
+
45
+ Requires **Node.js 18+**.
46
+
47
+ ## Quick Start
48
+
49
+ ```bash
50
+ # 1. Authenticate with GitHub (opens browser)
51
+ claudesync init
52
+
53
+ # 2. Upload your settings
54
+ claudesync push -m "initial sync"
55
+
56
+ # 3. On another machine — pull them down
57
+ claudesync init
58
+ claudesync pull
59
+ ```
60
+
61
+ Example output:
62
+
63
+ ```
64
+ $ claudesync push -m "initial sync"
65
+ Scanning ~/.claude/ ...
66
+
67
+ Changes:
68
+ + [settings] settings.json
69
+ + [settings] keybindings.json
70
+ + [instructions] CLAUDE.md
71
+ + [hooks] hooks/pre-tool-use.sh
72
+ + [skills] skills/my-skill.md
73
+
74
+ 5 files to upload. Proceed? (Y/n) y
75
+
76
+ ✔ Pushed to Gist (5 files)
77
+ https://gist.github.com/you/abc123
78
+ ```
79
+
80
+ ## What Gets Synced
81
+
82
+ | Category | Files |
83
+ |----------|-------|
84
+ | **settings** | `settings.json`, `keybindings.json`, `policy-limits.json`, `remote-settings.json` |
85
+ | **instructions** | `CLAUDE.md` |
86
+ | **hooks** | `hooks/` directory |
87
+ | **skills** | `skills/` directory |
88
+ | **plugins** | `plugins/installed_plugins.json`, `known_marketplaces.json`, `blocklist.json` |
89
+ | **teams** | `teams/` directory |
90
+ | **ui** | `statusline-command.sh` |
91
+
92
+ Conversation logs, sessions, caches, and `~/.claude.json` are **never** synced.
93
+
94
+ ## Commands
95
+
96
+ ### Auth
97
+
98
+ ```bash
99
+ claudesync init # OAuth Device Flow (browser)
100
+ claudesync init --token # Manual PAT input
101
+ claudesync link <gist-id> # Link an existing Gist
102
+ ```
103
+
104
+ ### Sync
105
+
106
+ ```bash
107
+ claudesync push # Upload to Gist
108
+ claudesync push -m "message" # With a sync message
109
+ claudesync push --only hooks # Single category
110
+ claudesync push --encrypt # Encrypt before upload
111
+ claudesync push --force # Skip confirmation
112
+
113
+ claudesync pull # Download from Gist (shows diff first)
114
+ claudesync pull --only skills # Single category
115
+ claudesync pull --force # Skip confirmation
116
+ ```
117
+
118
+ ### Compare
119
+
120
+ ```bash
121
+ claudesync diff # Full diff: local vs remote
122
+ claudesync diff --only settings # Diff single category
123
+ claudesync status # Auth & sync status
124
+ claudesync list # List all syncable local files
125
+ claudesync list --only hooks # List single category
126
+ ```
127
+
128
+ ### History
129
+
130
+ ```bash
131
+ claudesync history # Recent revisions (last 10)
132
+ claudesync rollback <version> # Restore a specific revision
133
+ ```
134
+
135
+ ### Auto Sync
136
+
137
+ ```bash
138
+ claudesync auto # Interactive setup (direction, interval, categories)
139
+ claudesync auto status # Show current auto-sync config & recent logs
140
+ claudesync auto disable # Stop and unregister auto-sync
141
+ ```
142
+
143
+ Sets up periodic sync via **launchd** (macOS), **systemd** (Linux), or **Task Scheduler** (Windows). Supports push/pull direction, configurable interval (min 60s), category filter, encryption, and conflict policies (overwrite / skip / backup).
144
+
145
+ ### Config
146
+
147
+ ```bash
148
+ claudesync config list # Show all settings
149
+ claudesync config lang ko # Set language to Korean
150
+ claudesync config lang en # Set language to English
151
+ ```
152
+
153
+ ## Options
154
+
155
+ | Flag | Description |
156
+ |------|-------------|
157
+ | `-m, --message <msg>` | Sync message (shown in `history`) |
158
+ | `--only <category>` | Filter: `settings` / `instructions` / `hooks` / `skills` / `plugins` / `teams` / `ui` |
159
+ | `--force` | Skip confirmation prompts |
160
+ | `--encrypt` | AES-256-GCM encrypt files before upload |
161
+ | `--lang ko\|en` | Set display language |
162
+ | `-h, --help` | Show help |
163
+ | `-v, --version` | Show version |
164
+
165
+ ## How It Works
166
+
167
+ Settings are stored as files in a single **private GitHub Gist**. Each file maps 1:1 to a Gist file, with directory separators converted to `--` (e.g. `hooks/pre-tool-use.sh` becomes `hooks--pre-tool-use.sh`).
168
+
169
+ A `_meta.json` file in the Gist tracks file mappings, categories, encryption status, sync timestamps, machine info, and push messages. Gist's built-in revision history powers `history` and `rollback`.
170
+
171
+ ## Security
172
+
173
+ - **Token storage** — macOS Keychain, Linux libsecret, or file with `chmod 600`
174
+ - **Secret scanning** — 15 regex patterns + Shannon entropy analysis on every push
175
+ - **Manual redaction** — add `# claudesync:redact` to any line to always exclude it
176
+ - **Encryption** — `--encrypt` applies AES-256-GCM with scrypt key derivation
177
+ - **Private Gists** — all Gists are created as private
178
+ - **Path traversal protection** — blocks unsafe paths on pull/rollback
179
+ - **Concurrent lock** — file-based lock prevents overlapping auto-sync runs
180
+
181
+ ## FAQ
182
+
183
+ **Is my data safe?**
184
+ Gists are private. Only someone with your GitHub token or the direct URL can see them. Use `--encrypt` for extra protection.
185
+
186
+ **Can I sync project-specific settings?**
187
+ Not yet — only global `~/.claude/` settings are supported.
188
+
189
+ **What if settings changed on both machines?**
190
+ `pull` shows a diff first. Existing files are backed up as `.bak` before overwriting.
191
+
192
+ **Can I skip OAuth?**
193
+ Yes. `claudesync init --token` accepts a [Personal Access Token](https://github.com/settings/tokens) with `gist` scope.
194
+
195
+ **What about multiple machines pushing?**
196
+ Auto-sync tracks a **primary device** for push mode. Only the primary machine pushes automatically, preventing conflicts.
197
+
198
+ **Does it work on Windows?**
199
+ Yes. Auto-sync uses Windows Task Scheduler. Token storage falls back to an encrypted file.
200
+
201
+ ## License
202
+
203
+ [MIT](LICENSE)
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../dist/cli.js';
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ machineName,
4
+ platformString
5
+ } from "./chunk-VBOSEAEH.js";
6
+
7
+ // src/core/gist.ts
8
+ import { hostname } from "os";
9
+
10
+ // src/types.ts
11
+ var CATEGORIES = [
12
+ "settings",
13
+ "instructions",
14
+ "hooks",
15
+ "skills",
16
+ "plugins",
17
+ "teams",
18
+ "ui"
19
+ ];
20
+
21
+ // src/core/gist.ts
22
+ var API = "https://api.github.com";
23
+ var GIST_DESC = "claudesync: Claude Code settings";
24
+ var META_FILE = "_meta.json";
25
+ function headers(token) {
26
+ return {
27
+ Authorization: `token ${token}`,
28
+ Accept: "application/vnd.github+json",
29
+ "User-Agent": "claudesync",
30
+ "X-GitHub-Api-Version": "2022-11-28"
31
+ };
32
+ }
33
+ async function apiRequest(url, token, options) {
34
+ const res = await fetch(url, {
35
+ ...options,
36
+ headers: { ...headers(token), ...options?.headers }
37
+ });
38
+ if (!res.ok) {
39
+ const body = await res.text();
40
+ throw new Error(`GitHub API ${res.status}: ${body}`);
41
+ }
42
+ return res.json();
43
+ }
44
+ async function findGist(token) {
45
+ const gists = await apiRequest(`${API}/gists?per_page=100`, token);
46
+ return gists.find((g) => g.description === GIST_DESC && g.files[META_FILE]) ?? null;
47
+ }
48
+ async function getGist(token, gistId) {
49
+ return apiRequest(`${API}/gists/${gistId}`, token);
50
+ }
51
+ async function createGist(token, files, encryptedFiles, message, primaryDevice) {
52
+ const meta = buildMeta(files, encryptedFiles, message, primaryDevice);
53
+ const gistFiles = {
54
+ [META_FILE]: { content: JSON.stringify(meta, null, 2) }
55
+ };
56
+ for (const f of files) {
57
+ gistFiles[f.gistFilename] = { content: f.content };
58
+ }
59
+ return apiRequest(`${API}/gists`, token, {
60
+ method: "POST",
61
+ body: JSON.stringify({
62
+ description: GIST_DESC,
63
+ public: false,
64
+ files: gistFiles
65
+ })
66
+ });
67
+ }
68
+ async function updateGist(token, gistId, files, deletedFiles, encryptedFiles, message, primaryDevice) {
69
+ const meta = buildMeta(files, encryptedFiles, message, primaryDevice);
70
+ const gistFiles = {
71
+ [META_FILE]: { content: JSON.stringify(meta, null, 2) }
72
+ };
73
+ for (const f of files) {
74
+ gistFiles[f.gistFilename] = { content: f.content };
75
+ }
76
+ if (deletedFiles) {
77
+ for (const name of deletedFiles) {
78
+ gistFiles[name] = null;
79
+ }
80
+ }
81
+ return apiRequest(`${API}/gists/${gistId}`, token, {
82
+ method: "PATCH",
83
+ body: JSON.stringify({ files: gistFiles })
84
+ });
85
+ }
86
+ async function getHistory(token, gistId) {
87
+ const gist = await apiRequest(`${API}/gists/${gistId}`, token);
88
+ return gist.history ?? [];
89
+ }
90
+ async function getGistAtRevision(token, gistId, sha) {
91
+ return apiRequest(`${API}/gists/${gistId}/${sha}`, token);
92
+ }
93
+ function parseMeta(gist) {
94
+ const metaFile = gist.files[META_FILE];
95
+ if (!metaFile?.content) return null;
96
+ try {
97
+ return JSON.parse(metaFile.content);
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+ function buildMeta(files, encryptedFiles, message, primaryDevice) {
103
+ const fileMap = {};
104
+ for (const f of files) {
105
+ fileMap[f.gistFilename] = {
106
+ path: f.relativePath,
107
+ category: f.category,
108
+ ...encryptedFiles?.has(f.gistFilename) && { encrypted: true }
109
+ };
110
+ }
111
+ return {
112
+ version: 1,
113
+ tool: "claudesync",
114
+ last_sync: {
115
+ machine: machineName(),
116
+ hostname: getHostname(),
117
+ platform: platformString(),
118
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
119
+ file_count: files.length,
120
+ ...message && { message }
121
+ },
122
+ file_map: fileMap,
123
+ categories: [...CATEGORIES],
124
+ ...primaryDevice && { primary_device: primaryDevice }
125
+ };
126
+ }
127
+ function getHostname() {
128
+ try {
129
+ return hostname();
130
+ } catch {
131
+ return "unknown";
132
+ }
133
+ }
134
+
135
+ export {
136
+ CATEGORIES,
137
+ findGist,
138
+ getGist,
139
+ createGist,
140
+ updateGist,
141
+ getHistory,
142
+ getGistAtRevision,
143
+ parseMeta
144
+ };
145
+ //# sourceMappingURL=chunk-45GTBXRR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/gist.ts","../src/types.ts"],"sourcesContent":["import { hostname } from 'node:os';\nimport type { Gist, GistRevision, SyncMeta, ScannedFile, PrimaryDevice } from '../types.js';\nimport { machineName, platformString } from '../utils/paths.js';\nimport { CATEGORIES } from '../types.js';\n\nconst API = 'https://api.github.com';\nconst GIST_DESC = 'claudesync: Claude Code settings';\nconst META_FILE = '_meta.json';\n\nfunction headers(token: string): Record<string, string> {\n return {\n Authorization: `token ${token}`,\n Accept: 'application/vnd.github+json',\n 'User-Agent': 'claudesync',\n 'X-GitHub-Api-Version': '2022-11-28',\n };\n}\n\nasync function apiRequest<T>(url: string, token: string, options?: RequestInit): Promise<T> {\n const res = await fetch(url, {\n ...options,\n headers: { ...headers(token), ...options?.headers },\n });\n if (!res.ok) {\n const body = await res.text();\n throw new Error(`GitHub API ${res.status}: ${body}`);\n }\n return res.json() as Promise<T>;\n}\n\n// ── Find existing claudesync Gist ───────────────────────────\nexport async function findGist(token: string): Promise<Gist | null> {\n const gists = await apiRequest<Gist[]>(`${API}/gists?per_page=100`, token);\n return gists.find((g) => g.description === GIST_DESC && g.files[META_FILE]) ?? null;\n}\n\n// ── Get Gist by ID ──────────────────────────────────────────\nexport async function getGist(token: string, gistId: string): Promise<Gist> {\n return apiRequest<Gist>(`${API}/gists/${gistId}`, token);\n}\n\n// ── Create new Gist ─────────────────────────────────────────\nexport async function createGist(\n token: string,\n files: ScannedFile[],\n encryptedFiles?: Set<string>,\n message?: string,\n primaryDevice?: PrimaryDevice,\n): Promise<Gist> {\n const meta = buildMeta(files, encryptedFiles, message, primaryDevice);\n const gistFiles: Record<string, { content: string }> = {\n [META_FILE]: { content: JSON.stringify(meta, null, 2) },\n };\n\n for (const f of files) {\n gistFiles[f.gistFilename] = { content: f.content };\n }\n\n return apiRequest<Gist>(`${API}/gists`, token, {\n method: 'POST',\n body: JSON.stringify({\n description: GIST_DESC,\n public: false,\n files: gistFiles,\n }),\n });\n}\n\n// ── Update existing Gist (partial) ─────────────────────────\nexport async function updateGist(\n token: string,\n gistId: string,\n files: ScannedFile[],\n deletedFiles?: string[],\n encryptedFiles?: Set<string>,\n message?: string,\n primaryDevice?: PrimaryDevice,\n): Promise<Gist> {\n const meta = buildMeta(files, encryptedFiles, message, primaryDevice);\n const gistFiles: Record<string, { content: string } | null> = {\n [META_FILE]: { content: JSON.stringify(meta, null, 2) },\n };\n\n for (const f of files) {\n gistFiles[f.gistFilename] = { content: f.content };\n }\n\n // Mark deleted files as null to remove from Gist\n if (deletedFiles) {\n for (const name of deletedFiles) {\n gistFiles[name] = null;\n }\n }\n\n return apiRequest<Gist>(`${API}/gists/${gistId}`, token, {\n method: 'PATCH',\n body: JSON.stringify({ files: gistFiles }),\n });\n}\n\n// ── Get Gist revision history ───────────────────────────────\nexport async function getHistory(token: string, gistId: string): Promise<GistRevision[]> {\n const gist = await apiRequest<Gist>(`${API}/gists/${gistId}`, token);\n return gist.history ?? [];\n}\n\n// ── Get Gist at a specific revision ─────────────────────────\nexport async function getGistAtRevision(\n token: string,\n gistId: string,\n sha: string,\n): Promise<Gist> {\n return apiRequest<Gist>(`${API}/gists/${gistId}/${sha}`, token);\n}\n\n// ── Parse meta from Gist ────────────────────────────────────\nexport function parseMeta(gist: Gist): SyncMeta | null {\n const metaFile = gist.files[META_FILE];\n if (!metaFile?.content) return null;\n try {\n return JSON.parse(metaFile.content) as SyncMeta;\n } catch {\n return null;\n }\n}\n\n// ── Build _meta.json ────────────────────────────────────────\nfunction buildMeta(files: ScannedFile[], encryptedFiles?: Set<string>, message?: string, primaryDevice?: PrimaryDevice): SyncMeta {\n const fileMap: SyncMeta['file_map'] = {};\n for (const f of files) {\n fileMap[f.gistFilename] = {\n path: f.relativePath,\n category: f.category,\n ...(encryptedFiles?.has(f.gistFilename) && { encrypted: true }),\n };\n }\n\n return {\n version: 1,\n tool: 'claudesync',\n last_sync: {\n machine: machineName(),\n hostname: getHostname(),\n platform: platformString(),\n timestamp: new Date().toISOString(),\n file_count: files.length,\n ...(message && { message }),\n },\n file_map: fileMap,\n categories: [...CATEGORIES],\n ...(primaryDevice && { primary_device: primaryDevice }),\n };\n}\n\nfunction getHostname(): string {\n try {\n return hostname();\n } catch {\n return 'unknown';\n }\n}\n","// ── Categories ──────────────────────────────────────────────\nexport const CATEGORIES = [\n 'settings',\n 'instructions',\n 'hooks',\n 'skills',\n 'plugins',\n 'teams',\n 'ui',\n] as const;\n\nexport type Category = (typeof CATEGORIES)[number];\n\n// ── Gist _meta.json ─────────────────────────────────────────\nexport interface SyncMeta {\n version: number;\n tool: 'claudesync';\n last_sync: {\n machine: string;\n hostname: string;\n platform: string;\n timestamp: string;\n file_count: number;\n message?: string;\n };\n file_map: Record<string, FileMapEntry>;\n categories: Category[];\n primary_device?: PrimaryDevice;\n}\n\nexport interface FileMapEntry {\n path: string; // relative to ~/.claude/, e.g. \"hooks/pre-tool-use.sh\"\n category: Category;\n encrypted?: boolean;\n}\n\n// ── Scanner ─────────────────────────────────────────────────\nexport interface ScannedFile {\n /** Absolute path on disk */\n absolutePath: string;\n /** Relative path from ~/.claude/ */\n relativePath: string;\n /** Gist-safe filename (slashes → --) */\n gistFilename: string;\n /** Category for selective sync */\n category: Category;\n /** File content (utf-8) */\n content: string;\n}\n\n// ── Gist API ────────────────────────────────────────────────\nexport interface GistFile {\n filename: string;\n content: string;\n truncated?: boolean;\n raw_url?: string;\n size?: number;\n}\n\nexport interface Gist {\n id: string;\n description: string;\n public: boolean;\n files: Record<string, GistFile>;\n html_url: string;\n created_at: string;\n updated_at: string;\n history?: GistRevision[];\n}\n\nexport interface GistRevision {\n version: string;\n user: { login: string } | null;\n committed_at: string;\n change_status: {\n total: number;\n additions: number;\n deletions: number;\n };\n}\n\n// ── Conflict ────────────────────────────────────────────────\nexport type ConflictAction = 'keep-local' | 'use-remote' | 'show-diff' | 'skip';\n\nexport interface FileChange {\n gistFilename: string;\n relativePath: string;\n category: Category;\n status: 'added' | 'modified' | 'deleted' | 'unchanged';\n localContent?: string;\n remoteContent?: string;\n}\n\n// ── Auth ────────────────────────────────────────────────────\nexport interface AuthConfig {\n token: string;\n gist_id?: string;\n machine_name?: string;\n}\n\n// ── CLI ─────────────────────────────────────────────────────\nexport interface PushOptions {\n only?: Category;\n force?: boolean;\n encrypt?: boolean;\n message?: string;\n}\n\nexport interface PullOptions {\n only?: Category;\n force?: boolean;\n}\n\nexport interface DiffOptions {\n only?: Category;\n}\n\n// ── Auto Sync ──────────────────────────────────────────────\nexport type AutoDirection = 'push' | 'pull';\n\nexport type PullConflictPolicy = 'overwrite' | 'skip' | 'backup';\n\nexport interface AutoConfig {\n direction: AutoDirection;\n interval_seconds: number;\n categories: Category[];\n encrypt: boolean;\n enabled: boolean;\n created_at: string;\n conflict_policy?: PullConflictPolicy;\n}\n\nexport interface PrimaryDevice {\n machine_id: string;\n machine: string;\n hostname: string;\n platform: string;\n registered_at: string;\n}\n"],"mappings":"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","names":[]}