claude-code-backup 1.0.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 +21 -0
- package/README.md +473 -0
- package/bin/claude-backup.js +56 -0
- package/package.json +54 -0
- package/src/backends/github.js +583 -0
- package/src/commands/config-cmd.js +139 -0
- package/src/commands/init.js +138 -0
- package/src/commands/pull.js +161 -0
- package/src/commands/push.js +14 -0
- package/src/commands/service.js +168 -0
- package/src/commands/status.js +80 -0
- package/src/commands/watch.js +101 -0
- package/src/core/collector.js +87 -0
- package/src/core/config.js +54 -0
- package/src/utils/logger.js +15 -0
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, copyFileSync, writeFileSync, readFileSync } from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import simpleGit from 'simple-git';
|
|
5
|
+
import { Octokit } from '@octokit/rest';
|
|
6
|
+
import { REPO_DIR } from '../core/config.js';
|
|
7
|
+
import { collectFiles, buildManifest } from '../core/collector.js';
|
|
8
|
+
import { log, spinner } from '../utils/logger.js';
|
|
9
|
+
|
|
10
|
+
function remoteUrl(config) {
|
|
11
|
+
return `https://${config.github.pat}@github.com/${config.github.repo}.git`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function repoGit() {
|
|
15
|
+
return simpleGit(REPO_DIR);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function configureGit(g) {
|
|
19
|
+
await g.addConfig('user.email', 'claude-backup@local', false, 'local');
|
|
20
|
+
await g.addConfig('user.name', 'claude-backup', false, 'local');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function syncRemote(g, config) {
|
|
24
|
+
const remotes = await g.getRemotes();
|
|
25
|
+
if (remotes.find(r => r.name === 'origin')) {
|
|
26
|
+
await g.remote(['set-url', 'origin', remoteUrl(config)]);
|
|
27
|
+
} else {
|
|
28
|
+
await g.addRemote('origin', remoteUrl(config));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function buildReadme(config) {
|
|
33
|
+
const now = new Date().toISOString().slice(0, 10);
|
|
34
|
+
const watchedList = config.watched_dirs.map(d => `- \`${d}\``).join('\n');
|
|
35
|
+
const excludeList = config.exclude.map(e => `- \`${e}\``).join('\n');
|
|
36
|
+
const claudeMdDirs = config.claude_md_dirs || [];
|
|
37
|
+
const claudeMdList = claudeMdDirs.length
|
|
38
|
+
? claudeMdDirs.map(d => `- \`${d}/CLAUDE.md\``).join('\n')
|
|
39
|
+
: '_No project CLAUDE.md dirs configured yet._\n\nAdd one with: `claude-backup config add-project /path/to/project`';
|
|
40
|
+
|
|
41
|
+
return `# Claude Code Backup
|
|
42
|
+
|
|
43
|
+
> Auto-synced by [claude-backup](https://www.npmjs.com/package/claude-backup) — a CLI tool that watches your Claude Code data directories and commits every change to this private repo.
|
|
44
|
+
|
|
45
|
+
**Repo:** \`${config.github.repo}\` · **Branch:** \`${config.github.branch}\` · **Last setup:** ${now}
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## What Is This Repo?
|
|
50
|
+
|
|
51
|
+
This is a private, automated backup of all your [Claude Code](https://claude.ai/code) data — including:
|
|
52
|
+
|
|
53
|
+
| What | Why it matters |
|
|
54
|
+
|---|---|
|
|
55
|
+
| **Memory files** | Claude's persistent knowledge about you, your projects, and your preferences |
|
|
56
|
+
| **Settings** | Global and project-level Claude Code configuration |
|
|
57
|
+
| **Keybindings** | Your custom keyboard shortcuts |
|
|
58
|
+
| **Custom commands** | Slash commands and skills you've defined |
|
|
59
|
+
| **Project \`CLAUDE.md\` files** | Per-project instructions that Claude reads at the start of every session |
|
|
60
|
+
|
|
61
|
+
Without a backup, all of this is lost if you get a new machine, accidentally delete \`~/.claude/\`, or need to set up Claude Code on a second device.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Watched Directories
|
|
66
|
+
|
|
67
|
+
The following directories are fully mirrored here (recursive):
|
|
68
|
+
|
|
69
|
+
${watchedList}
|
|
70
|
+
|
|
71
|
+
Files excluded from backup:
|
|
72
|
+
|
|
73
|
+
${excludeList}
|
|
74
|
+
|
|
75
|
+
## Project CLAUDE.md Files
|
|
76
|
+
|
|
77
|
+
Only the \`CLAUDE.md\` file is collected from each of these project roots
|
|
78
|
+
(the rest of the project source code is not touched):
|
|
79
|
+
|
|
80
|
+
${claudeMdList}
|
|
81
|
+
|
|
82
|
+
Add or remove projects at any time:
|
|
83
|
+
\`\`\`bash
|
|
84
|
+
claude-backup config add-project /path/to/project
|
|
85
|
+
claude-backup config remove-project /path/to/project
|
|
86
|
+
\`\`\`
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Repo Structure
|
|
91
|
+
|
|
92
|
+
\`\`\`
|
|
93
|
+
/
|
|
94
|
+
├── README.md ← this file (regenerated on every push)
|
|
95
|
+
├── backup-manifest.json ← maps each folder label → original source path
|
|
96
|
+
│
|
|
97
|
+
├── Users_<name>_.claude/ ← full mirror of ~/.claude/
|
|
98
|
+
│ ├── settings.json ← global Claude Code settings
|
|
99
|
+
│ ├── keybindings.json ← custom keyboard shortcuts
|
|
100
|
+
│ ├── projects/ ← per-project memory and settings
|
|
101
|
+
│ │ └── <project-hash>/
|
|
102
|
+
│ │ └── memory/
|
|
103
|
+
│ │ ├── MEMORY.md ← memory index for that project
|
|
104
|
+
│ │ └── *.md ← individual memory files
|
|
105
|
+
│ └── commands/ ← custom slash commands / skills
|
|
106
|
+
│ └── *.md
|
|
107
|
+
│
|
|
108
|
+
├── claude_md_Users_<name>_myapp/ ← CLAUDE.md from a project root
|
|
109
|
+
│ └── CLAUDE.md
|
|
110
|
+
│
|
|
111
|
+
└── claude_md_Users_<name>_other/ ← CLAUDE.md from another project
|
|
112
|
+
└── CLAUDE.md
|
|
113
|
+
\`\`\`
|
|
114
|
+
|
|
115
|
+
**Directory labels** are created by replacing \`/\` with \`_\` in the source path.
|
|
116
|
+
Labels starting with \`claude_md_\` are project CLAUDE.md entries — only that single file
|
|
117
|
+
is stored, not the whole project. \`backup-manifest.json\` records the original path for
|
|
118
|
+
every label so restore always knows exactly where to copy files back.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Claude Code Memory — Quick Primer
|
|
123
|
+
|
|
124
|
+
Claude Code has a file-based memory system stored under \`~/.claude/projects/<hash>/memory/\`:
|
|
125
|
+
|
|
126
|
+
- **\`MEMORY.md\`** — the index file, loaded into every conversation with that project
|
|
127
|
+
- **\`*.md\` files** — individual memory entries (user profile, feedback, project context, references)
|
|
128
|
+
|
|
129
|
+
Memory entries have types:
|
|
130
|
+
| Type | Contains |
|
|
131
|
+
|---|---|
|
|
132
|
+
| \`user\` | Who you are, your role, expertise level |
|
|
133
|
+
| \`feedback\` | How Claude should behave — what to avoid, what to repeat |
|
|
134
|
+
| \`project\` | Active work, goals, deadlines, architectural decisions |
|
|
135
|
+
| \`reference\` | Where to find things — Linear projects, Grafana dashboards, Slack channels |
|
|
136
|
+
|
|
137
|
+
These files are the most valuable thing to back up because they accumulate over months and are impossible to recreate from scratch.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Installation
|
|
142
|
+
|
|
143
|
+
\`\`\`bash
|
|
144
|
+
npm install -g claude-backup
|
|
145
|
+
\`\`\`
|
|
146
|
+
|
|
147
|
+
Requires Node.js 18 or later.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Command Reference
|
|
152
|
+
|
|
153
|
+
### \`claude-backup init\`
|
|
154
|
+
|
|
155
|
+
Interactive setup wizard. Run this once (or again to reconfigure).
|
|
156
|
+
|
|
157
|
+
\`\`\`
|
|
158
|
+
$ claude-backup init
|
|
159
|
+
|
|
160
|
+
Step 1 of 3 — GitHub Personal Access Token
|
|
161
|
+
Create your token here: https://github.com/settings/tokens/new
|
|
162
|
+
Required scope: repo (top-level checkbox)
|
|
163
|
+
|
|
164
|
+
? Paste your GitHub PAT: ****
|
|
165
|
+
|
|
166
|
+
Step 2 of 3 — Repository & Branch
|
|
167
|
+
? GitHub repo name: yourname/claude-backup
|
|
168
|
+
? Branch name: main
|
|
169
|
+
|
|
170
|
+
Step 3 of 3 — Watched Directories & Filters
|
|
171
|
+
? Directories to watch: ~/.claude
|
|
172
|
+
? Files to exclude: settings.local.json, *.log, .DS_Store
|
|
173
|
+
? Debounce delay in ms: 2000
|
|
174
|
+
\`\`\`
|
|
175
|
+
|
|
176
|
+
What it does:
|
|
177
|
+
- Saves config to \`~/.config/claude-backup/config.json\` (chmod 600)
|
|
178
|
+
- Creates this GitHub repo as private if it doesn't exist
|
|
179
|
+
- Clones the repo locally to \`~/.config/claude-backup/repo/\`
|
|
180
|
+
- Writes this README into the repo
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
### \`claude-backup push\`
|
|
185
|
+
|
|
186
|
+
Manually push a backup right now.
|
|
187
|
+
|
|
188
|
+
\`\`\`bash
|
|
189
|
+
claude-backup push # auto commit message: "backup: <ISO timestamp>"
|
|
190
|
+
claude-backup push -m "before upgrade" # custom commit message
|
|
191
|
+
\`\`\`
|
|
192
|
+
|
|
193
|
+
What it does:
|
|
194
|
+
1. Walks all watched directories, applies exclude filters
|
|
195
|
+
2. Copies every file into the local repo clone under its label directory
|
|
196
|
+
3. Writes \`backup-manifest.json\` with the source-path mapping
|
|
197
|
+
4. \`git add -A\` → \`git commit\` → \`git push\`
|
|
198
|
+
5. Skips the push if nothing changed
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
### \`claude-backup pull\`
|
|
203
|
+
|
|
204
|
+
Restore files from this repo back to their original locations.
|
|
205
|
+
|
|
206
|
+
\`\`\`bash
|
|
207
|
+
claude-backup pull # restore the latest backup
|
|
208
|
+
claude-backup pull --history # browse commits, pick a specific version
|
|
209
|
+
claude-backup pull --dry-run # preview what would be restored — no files written
|
|
210
|
+
\`\`\`
|
|
211
|
+
|
|
212
|
+
**Safety:** before writing anything, \`pull\` creates a timestamped snapshot of your current state at \`~/.config/claude-backup/pre-restore-<timestamp>/\` so you can always undo.
|
|
213
|
+
|
|
214
|
+
Restore flow:
|
|
215
|
+
1. \`git fetch origin\` to get latest from GitHub
|
|
216
|
+
2. If \`--history\`: show commit list → you pick one
|
|
217
|
+
3. Checkout the selected commit's files into the local clone
|
|
218
|
+
4. Read \`backup-manifest.json\` to determine target paths
|
|
219
|
+
5. Ask for confirmation, show a preview of affected files
|
|
220
|
+
6. Create safety snapshot, then copy files to their source locations
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
### \`claude-backup watch\`
|
|
225
|
+
|
|
226
|
+
Start the real-time file watcher. Detects changes in watched directories and auto-pushes to GitHub.
|
|
227
|
+
|
|
228
|
+
\`\`\`bash
|
|
229
|
+
claude-backup watch # real-time via chokidar (recommended)
|
|
230
|
+
claude-backup watch --interval 5 # poll every 5 minutes instead
|
|
231
|
+
\`\`\`
|
|
232
|
+
|
|
233
|
+
The watcher:
|
|
234
|
+
- Uses [chokidar](https://github.com/paulmillr/chokidar) for efficient, cross-platform file watching
|
|
235
|
+
- Debounces rapid changes (default: 2000 ms) so a burst of saves becomes one commit
|
|
236
|
+
- Ignores \`.git\` subdirectories to avoid feedback loops
|
|
237
|
+
- Handles \`SIGTERM\` and \`SIGINT\` gracefully (important for launchd)
|
|
238
|
+
- Logs every sync with a timestamp
|
|
239
|
+
|
|
240
|
+
In normal use you don't run this directly — the launchd service runs it for you in the background.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
### \`claude-backup status\`
|
|
245
|
+
|
|
246
|
+
Show a summary of the current state.
|
|
247
|
+
|
|
248
|
+
\`\`\`
|
|
249
|
+
$ claude-backup status
|
|
250
|
+
|
|
251
|
+
Claude Backup Status
|
|
252
|
+
|
|
253
|
+
Watched dirs:
|
|
254
|
+
/Users/you/.claude
|
|
255
|
+
|
|
256
|
+
Files tracked: 42
|
|
257
|
+
|
|
258
|
+
GitHub: yourname/claude-backup [main]
|
|
259
|
+
Last backup: 2026-05-18 10:34:22
|
|
260
|
+
Commit: a3f9c1d backup: 2026-05-18T10:34:22.000Z
|
|
261
|
+
Sync status: Up to date
|
|
262
|
+
|
|
263
|
+
Auto-sync service: running (PID 1234)
|
|
264
|
+
\`\`\`
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
### \`claude-backup service <action>\`
|
|
269
|
+
|
|
270
|
+
Manage the macOS launchd background service that runs the file watcher automatically on every login.
|
|
271
|
+
|
|
272
|
+
\`\`\`bash
|
|
273
|
+
claude-backup service install # write plist + load service now
|
|
274
|
+
claude-backup service uninstall # stop service + remove plist
|
|
275
|
+
claude-backup service status # check if running, show PID
|
|
276
|
+
claude-backup service logs # tail the watcher log in real-time
|
|
277
|
+
\`\`\`
|
|
278
|
+
|
|
279
|
+
The service plist is installed at:
|
|
280
|
+
\`\`\`
|
|
281
|
+
~/Library/LaunchAgents/com.claude-backup.watch.plist
|
|
282
|
+
\`\`\`
|
|
283
|
+
|
|
284
|
+
Key plist settings:
|
|
285
|
+
- \`RunAtLoad: true\` — starts when the service is loaded (immediately on install)
|
|
286
|
+
- \`KeepAlive: true\` — launchd restarts the watcher if it ever crashes
|
|
287
|
+
- \`ThrottleInterval: 10\` — prevents rapid restart loops if something goes wrong
|
|
288
|
+
- Stdout → \`~/.config/claude-backup/watch.log\`
|
|
289
|
+
- Stderr → \`~/.config/claude-backup/watch.error.log\`
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
### \`claude-backup config <action>\`
|
|
294
|
+
|
|
295
|
+
View or modify configuration without re-running \`init\`.
|
|
296
|
+
|
|
297
|
+
\`\`\`bash
|
|
298
|
+
claude-backup config show # print current config (PAT masked)
|
|
299
|
+
claude-backup config set auto_sync.debounce_ms 3000 # change a value
|
|
300
|
+
claude-backup config add-dir /path/to/project # watch an extra directory
|
|
301
|
+
claude-backup config remove-dir /path/to/project # stop watching a directory
|
|
302
|
+
\`\`\`
|
|
303
|
+
|
|
304
|
+
Nested keys use dot notation. Values are auto-cast (numbers, booleans).
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## Auto-Sync Setup (Recommended)
|
|
309
|
+
|
|
310
|
+
After running \`init\` and \`push\`, install the background service:
|
|
311
|
+
|
|
312
|
+
\`\`\`bash
|
|
313
|
+
claude-backup service install
|
|
314
|
+
\`\`\`
|
|
315
|
+
|
|
316
|
+
From this point on:
|
|
317
|
+
- The watcher starts automatically every time you log in to macOS
|
|
318
|
+
- Any change to a watched directory is committed and pushed to GitHub within ~2 seconds
|
|
319
|
+
- If the watcher crashes, launchd restarts it automatically
|
|
320
|
+
- You never have to think about backups again
|
|
321
|
+
|
|
322
|
+
To verify it's working:
|
|
323
|
+
|
|
324
|
+
\`\`\`bash
|
|
325
|
+
claude-backup status # should show "running (PID ...)"
|
|
326
|
+
claude-backup service logs # live log of every sync
|
|
327
|
+
\`\`\`
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## Restore Guide
|
|
332
|
+
|
|
333
|
+
### Restore to the same machine (after accidental deletion)
|
|
334
|
+
|
|
335
|
+
\`\`\`bash
|
|
336
|
+
claude-backup pull
|
|
337
|
+
\`\`\`
|
|
338
|
+
|
|
339
|
+
Confirm the preview, and your files are back.
|
|
340
|
+
|
|
341
|
+
### Restore to a new machine
|
|
342
|
+
|
|
343
|
+
\`\`\`bash
|
|
344
|
+
# 1. Install Node.js (18+) and claude-backup
|
|
345
|
+
npm install -g claude-backup
|
|
346
|
+
|
|
347
|
+
# 2. Run init with the same GitHub repo
|
|
348
|
+
claude-backup init
|
|
349
|
+
# → Same repo name (yourname/claude-backup)
|
|
350
|
+
# → New GitHub PAT (generate a new one)
|
|
351
|
+
|
|
352
|
+
# 3. Pull the latest backup
|
|
353
|
+
claude-backup pull
|
|
354
|
+
|
|
355
|
+
# 4. Install the background service on the new machine
|
|
356
|
+
claude-backup service install
|
|
357
|
+
\`\`\`
|
|
358
|
+
|
|
359
|
+
### Restore a specific historical version
|
|
360
|
+
|
|
361
|
+
\`\`\`bash
|
|
362
|
+
claude-backup pull --history
|
|
363
|
+
\`\`\`
|
|
364
|
+
|
|
365
|
+
This shows a list of all commits in this repo. Select the one you want — the timestamp and commit message help identify when each backup was taken.
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
## Configuration Reference
|
|
370
|
+
|
|
371
|
+
Config file location: \`~/.config/claude-backup/config.json\`
|
|
372
|
+
|
|
373
|
+
| Key | Type | Default | Description |
|
|
374
|
+
|---|---|---|---|
|
|
375
|
+
| \`backend\` | string | \`"github"\` | Storage backend (currently only \`github\`) |
|
|
376
|
+
| \`github.repo\` | string | — | GitHub repo in \`owner/name\` format |
|
|
377
|
+
| \`github.branch\` | string | \`"main"\` | Branch to push to |
|
|
378
|
+
| \`github.pat\` | string | — | Personal Access Token (stored chmod 600) |
|
|
379
|
+
| \`watched_dirs\` | string[] | \`["~/.claude"]\` | Directories to fully mirror |
|
|
380
|
+
| \`claude_md_dirs\` | string[] | \`[]\` | Project roots — only \`CLAUDE.md\` is backed up from each |
|
|
381
|
+
| \`exclude\` | string[] | see below | Filenames/globs to skip |
|
|
382
|
+
| \`auto_sync.debounce_ms\` | number | \`2000\` | ms to wait after last change before pushing |
|
|
383
|
+
|
|
384
|
+
Default excludes: \`settings.local.json\`, \`*.log\`, \`.DS_Store\`
|
|
385
|
+
|
|
386
|
+
The local repo clone lives at: \`~/.config/claude-backup/repo/\`
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
|
|
390
|
+
## Security Notes
|
|
391
|
+
|
|
392
|
+
- This repo is **private** — only you and anyone you explicitly invite can see it
|
|
393
|
+
- Your GitHub PAT is stored locally at \`~/.config/claude-backup/config.json\` with \`chmod 600\` (owner-read only)
|
|
394
|
+
- The PAT is never committed to this repo
|
|
395
|
+
- \`settings.local.json\` is excluded by default because it may contain machine-specific or sensitive values
|
|
396
|
+
- If you ever rotate your PAT, run \`claude-backup config set github.pat <new-token>\` then \`claude-backup service install\` to restart the watcher with the new token
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Troubleshooting
|
|
401
|
+
|
|
402
|
+
**"Not configured. Run: claude-backup init"**
|
|
403
|
+
The config file is missing or incomplete. Re-run \`claude-backup init\`.
|
|
404
|
+
|
|
405
|
+
**Push fails with authentication error**
|
|
406
|
+
Your PAT may have expired or been revoked. Generate a new one at
|
|
407
|
+
https://github.com/settings/tokens/new and run:
|
|
408
|
+
\`\`\`bash
|
|
409
|
+
claude-backup config set github.pat <new-token>
|
|
410
|
+
\`\`\`
|
|
411
|
+
|
|
412
|
+
**Service shows "loaded but not running"**
|
|
413
|
+
Check the error log:
|
|
414
|
+
\`\`\`bash
|
|
415
|
+
claude-backup service logs
|
|
416
|
+
cat ~/.config/claude-backup/watch.error.log
|
|
417
|
+
\`\`\`
|
|
418
|
+
Then reinstall the service: \`claude-backup service install\`
|
|
419
|
+
|
|
420
|
+
**Files not being detected by the watcher**
|
|
421
|
+
Confirm the directory is in your watch list:
|
|
422
|
+
\`\`\`bash
|
|
423
|
+
claude-backup config show
|
|
424
|
+
claude-backup config add-dir /path/to/missing/dir
|
|
425
|
+
\`\`\`
|
|
426
|
+
|
|
427
|
+
**Restore wrote wrong files / I want to undo**
|
|
428
|
+
Every restore creates a safety snapshot before touching anything:
|
|
429
|
+
\`\`\`bash
|
|
430
|
+
ls ~/.config/claude-backup/pre-restore-*/
|
|
431
|
+
\`\`\`
|
|
432
|
+
Copy the files you need back from there manually.
|
|
433
|
+
|
|
434
|
+
---
|
|
435
|
+
|
|
436
|
+
*This README is auto-generated by \`claude-backup init\` and reflects the configuration at setup time.*
|
|
437
|
+
`;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export async function ensureRepo(config) {
|
|
441
|
+
const octokit = new Octokit({ auth: config.github.pat });
|
|
442
|
+
const [owner, repo] = config.github.repo.split('/');
|
|
443
|
+
|
|
444
|
+
// Check if repo exists, create it if not
|
|
445
|
+
let repoExists = false;
|
|
446
|
+
try {
|
|
447
|
+
await octokit.repos.get({ owner, repo });
|
|
448
|
+
repoExists = true;
|
|
449
|
+
} catch (err) {
|
|
450
|
+
if (err.status !== 404) throw err;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (!repoExists) {
|
|
454
|
+
const spin = spinner('Creating private GitHub repo...').start();
|
|
455
|
+
await octokit.repos.createForAuthenticatedUser({
|
|
456
|
+
name: repo,
|
|
457
|
+
private: true,
|
|
458
|
+
description: 'Claude Code backup — memory, settings, commands',
|
|
459
|
+
auto_init: true,
|
|
460
|
+
});
|
|
461
|
+
spin.succeed(`Created: github.com/${config.github.repo}`);
|
|
462
|
+
} else {
|
|
463
|
+
log.info(`Repo exists: github.com/${config.github.repo}`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Clone locally if not already done
|
|
467
|
+
if (!existsSync(REPO_DIR)) {
|
|
468
|
+
mkdirSync(REPO_DIR, { recursive: true });
|
|
469
|
+
const spin = spinner('Cloning repo...').start();
|
|
470
|
+
await simpleGit().clone(remoteUrl(config), REPO_DIR);
|
|
471
|
+
spin.succeed('Repo cloned to: ' + REPO_DIR);
|
|
472
|
+
} else {
|
|
473
|
+
log.info('Local repo clone already exists');
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Seed the README so it exists in the clone from day one
|
|
477
|
+
writeFileSync(join(REPO_DIR, 'README.md'), buildReadme(config), 'utf8');
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export async function push(config, commitMessage) {
|
|
481
|
+
if (!existsSync(REPO_DIR)) {
|
|
482
|
+
log.error('Repo not set up. Run: claude-backup init');
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const files = collectFiles(config);
|
|
487
|
+
if (files.length === 0) {
|
|
488
|
+
log.warn('No files found — check your watched_dirs in config');
|
|
489
|
+
return 0;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const spin = spinner(`Staging ${files.length} files...`).start();
|
|
493
|
+
|
|
494
|
+
// Copy all watched files into the repo mirror
|
|
495
|
+
for (const { src, dest } of files) {
|
|
496
|
+
const target = join(REPO_DIR, dest);
|
|
497
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
498
|
+
try {
|
|
499
|
+
copyFileSync(src, target);
|
|
500
|
+
} catch {
|
|
501
|
+
// Skip unreadable files silently
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Write the manifest so pull knows where to restore each label dir
|
|
506
|
+
writeFileSync(
|
|
507
|
+
join(REPO_DIR, 'backup-manifest.json'),
|
|
508
|
+
JSON.stringify(buildManifest(config), null, 2),
|
|
509
|
+
'utf8'
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
// Always regenerate README so it stays current
|
|
513
|
+
writeFileSync(join(REPO_DIR, 'README.md'), buildReadme(config), 'utf8');
|
|
514
|
+
|
|
515
|
+
const g = repoGit();
|
|
516
|
+
await configureGit(g);
|
|
517
|
+
await syncRemote(g, config);
|
|
518
|
+
|
|
519
|
+
const status = await g.status();
|
|
520
|
+
if (status.files.length === 0) {
|
|
521
|
+
spin.succeed('Everything up to date — nothing to push');
|
|
522
|
+
return 0;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
spin.text = `Committing ${status.files.length} change(s)...`;
|
|
526
|
+
await g.add('-A');
|
|
527
|
+
await g.commit(commitMessage || `backup: ${new Date().toISOString()}`);
|
|
528
|
+
|
|
529
|
+
spin.text = 'Pushing to GitHub...';
|
|
530
|
+
await g.raw(['push', '-u', 'origin', config.github.branch]);
|
|
531
|
+
|
|
532
|
+
spin.succeed(`Pushed ${status.files.length} file(s) → github.com/${config.github.repo}`);
|
|
533
|
+
return status.files.length;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export async function getStatus(config) {
|
|
537
|
+
if (!existsSync(REPO_DIR)) return null;
|
|
538
|
+
const g = repoGit();
|
|
539
|
+
try {
|
|
540
|
+
const logResult = await g.log(['--max-count=1']);
|
|
541
|
+
const status = await g.status();
|
|
542
|
+
return {
|
|
543
|
+
lastCommit: logResult.latest,
|
|
544
|
+
pending: status.files.length,
|
|
545
|
+
};
|
|
546
|
+
} catch {
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export async function getHistory(config, limit = 15) {
|
|
552
|
+
if (!existsSync(REPO_DIR)) return [];
|
|
553
|
+
const g = repoGit();
|
|
554
|
+
await syncRemote(g, config);
|
|
555
|
+
await g.fetch('origin');
|
|
556
|
+
const logResult = await g.log({ maxCount: limit });
|
|
557
|
+
return logResult.all;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
export async function fetchForRestore(config, ref) {
|
|
561
|
+
if (!existsSync(REPO_DIR)) {
|
|
562
|
+
log.error('Repo not set up. Run: claude-backup init');
|
|
563
|
+
process.exit(1);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const g = repoGit();
|
|
567
|
+
await configureGit(g);
|
|
568
|
+
await syncRemote(g, config);
|
|
569
|
+
|
|
570
|
+
const spin = spinner('Fetching from GitHub...').start();
|
|
571
|
+
await g.fetch('origin');
|
|
572
|
+
spin.stop();
|
|
573
|
+
|
|
574
|
+
// Checkout the target ref files into the working tree
|
|
575
|
+
const targetRef = ref || `origin/${config.github.branch}`;
|
|
576
|
+
await g.checkout([targetRef, '--', '.']);
|
|
577
|
+
|
|
578
|
+
// Read the manifest that was just checked out
|
|
579
|
+
const manifestPath = join(REPO_DIR, 'backup-manifest.json');
|
|
580
|
+
if (!existsSync(manifestPath)) return null;
|
|
581
|
+
|
|
582
|
+
return JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
583
|
+
}
|