claude-code-backup 1.0.1 → 1.0.3
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/package.json +1 -1
- package/src/backends/github.js +66 -21
- package/src/commands/init.js +74 -25
- package/src/core/config.js +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-backup",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Backup and auto-sync Claude Code memory, settings, and CLAUDE.md files to a private GitHub repo — with real-time file watching and macOS launchd auto-start",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/backends/github.js
CHANGED
|
@@ -1,14 +1,47 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, copyFileSync, writeFileSync, readFileSync } from 'fs';
|
|
2
2
|
import { join, dirname } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
import { exec } from 'child_process';
|
|
4
6
|
import simpleGit from 'simple-git';
|
|
5
7
|
import { Octokit } from '@octokit/rest';
|
|
6
8
|
import { REPO_DIR } from '../core/config.js';
|
|
7
9
|
import { collectFiles, buildManifest } from '../core/collector.js';
|
|
8
10
|
import { log, spinner } from '../utils/logger.js';
|
|
9
11
|
|
|
12
|
+
const execAsync = promisify(exec);
|
|
13
|
+
|
|
10
14
|
function remoteUrl(config) {
|
|
11
|
-
|
|
15
|
+
if (config.auth_method === 'ssh') {
|
|
16
|
+
return `git@github.com:${config.github.repo}.git`;
|
|
17
|
+
}
|
|
18
|
+
// Legacy or explicit PAT auth: embed token in URL
|
|
19
|
+
if (config.github?.pat) {
|
|
20
|
+
return `https://${config.github.pat}@github.com/${config.github.repo}.git`;
|
|
21
|
+
}
|
|
22
|
+
// System auth (osxkeychain, gh CLI, etc.) — credential helper handles it
|
|
23
|
+
return `https://github.com/${config.github.repo}.git`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function getApiToken(config) {
|
|
27
|
+
if (config.github?.pat) return config.github.pat;
|
|
28
|
+
|
|
29
|
+
// Try git credential helper (osxkeychain, gh, gnome-keyring, etc.)
|
|
30
|
+
try {
|
|
31
|
+
const { stdout } = await execAsync(
|
|
32
|
+
'printf "protocol=https\\nhost=github.com\\n" | git credential fill'
|
|
33
|
+
);
|
|
34
|
+
const match = stdout.match(/^password=(.+)$/m);
|
|
35
|
+
if (match?.[1]?.trim()) return match[1].trim();
|
|
36
|
+
} catch {}
|
|
37
|
+
|
|
38
|
+
// Try gh CLI
|
|
39
|
+
try {
|
|
40
|
+
const { stdout } = await execAsync('gh auth token 2>/dev/null');
|
|
41
|
+
if (stdout.trim()) return stdout.trim();
|
|
42
|
+
} catch {}
|
|
43
|
+
|
|
44
|
+
return null;
|
|
12
45
|
}
|
|
13
46
|
|
|
14
47
|
function repoGit() {
|
|
@@ -438,29 +471,34 @@ Copy the files you need back from there manually.
|
|
|
438
471
|
}
|
|
439
472
|
|
|
440
473
|
export async function ensureRepo(config) {
|
|
441
|
-
const octokit = new Octokit({ auth: config.github.pat });
|
|
442
474
|
const [owner, repo] = config.github.repo.split('/');
|
|
475
|
+
const token = await getApiToken(config);
|
|
443
476
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
477
|
+
if (token) {
|
|
478
|
+
const octokit = new Octokit({ auth: token });
|
|
479
|
+
let repoExists = false;
|
|
480
|
+
try {
|
|
481
|
+
await octokit.repos.get({ owner, repo });
|
|
482
|
+
repoExists = true;
|
|
483
|
+
} catch (err) {
|
|
484
|
+
if (err.status !== 404) throw err;
|
|
485
|
+
}
|
|
452
486
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
487
|
+
if (!repoExists) {
|
|
488
|
+
const spin = spinner('Creating private GitHub repo...').start();
|
|
489
|
+
await octokit.repos.createForAuthenticatedUser({
|
|
490
|
+
name: repo,
|
|
491
|
+
private: true,
|
|
492
|
+
description: 'Claude Code backup — memory, settings, commands',
|
|
493
|
+
auto_init: true,
|
|
494
|
+
});
|
|
495
|
+
spin.succeed(`Created: github.com/${config.github.repo}`);
|
|
496
|
+
} else {
|
|
497
|
+
log.info(`Repo exists: github.com/${config.github.repo}`);
|
|
498
|
+
}
|
|
462
499
|
} else {
|
|
463
|
-
log.
|
|
500
|
+
log.warn('No GitHub API token found — skipping repo creation check.');
|
|
501
|
+
log.dim(' Make sure the repo exists at github.com and your system git credentials are set up.');
|
|
464
502
|
}
|
|
465
503
|
|
|
466
504
|
// Clone locally if not already done
|
|
@@ -477,6 +515,13 @@ export async function ensureRepo(config) {
|
|
|
477
515
|
writeFileSync(join(REPO_DIR, 'README.md'), buildReadme(config), 'utf8');
|
|
478
516
|
}
|
|
479
517
|
|
|
518
|
+
function localTimestamp() {
|
|
519
|
+
const d = new Date();
|
|
520
|
+
const pad = n => String(n).padStart(2, '0');
|
|
521
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +
|
|
522
|
+
`${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
523
|
+
}
|
|
524
|
+
|
|
480
525
|
export async function push(config, commitMessage) {
|
|
481
526
|
if (!existsSync(REPO_DIR)) {
|
|
482
527
|
log.error('Repo not set up. Run: claude-code-backup init');
|
|
@@ -524,7 +569,7 @@ export async function push(config, commitMessage) {
|
|
|
524
569
|
|
|
525
570
|
spin.text = `Committing ${status.files.length} change(s)...`;
|
|
526
571
|
await g.add('-A');
|
|
527
|
-
await g.commit(commitMessage || `backup: ${
|
|
572
|
+
await g.commit(commitMessage || `backup: ${localTimestamp()}`);
|
|
528
573
|
|
|
529
574
|
spin.text = 'Pushing to GitHub...';
|
|
530
575
|
await g.raw(['push', '-u', 'origin', config.github.branch]);
|
package/src/commands/init.js
CHANGED
|
@@ -2,39 +2,87 @@ import inquirer from 'inquirer';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
import { join } from 'path';
|
|
5
|
+
import { promisify } from 'util';
|
|
6
|
+
import { exec } from 'child_process';
|
|
5
7
|
import { loadConfig, saveConfig } from '../core/config.js';
|
|
6
8
|
import { ensureRepo } from '../backends/github.js';
|
|
7
9
|
import { log } from '../utils/logger.js';
|
|
8
10
|
|
|
11
|
+
const execAsync = promisify(exec);
|
|
12
|
+
|
|
13
|
+
async function detectAuth() {
|
|
14
|
+
// 1. SSH key present
|
|
15
|
+
try {
|
|
16
|
+
const { stdout } = await execAsync('ls ~/.ssh/id_*.pub 2>/dev/null | head -1');
|
|
17
|
+
if (stdout.trim()) {
|
|
18
|
+
return { method: 'ssh', label: `SSH key (${stdout.trim().split('/').pop()})` };
|
|
19
|
+
}
|
|
20
|
+
} catch {}
|
|
21
|
+
|
|
22
|
+
// 2. gh CLI authenticated
|
|
23
|
+
try {
|
|
24
|
+
const { stdout } = await execAsync('gh auth status 2>&1');
|
|
25
|
+
if (/Logged in/i.test(stdout)) {
|
|
26
|
+
return { method: 'system', label: 'GitHub CLI (gh auth)' };
|
|
27
|
+
}
|
|
28
|
+
} catch {}
|
|
29
|
+
|
|
30
|
+
// 3. git credential helper has a GitHub token
|
|
31
|
+
try {
|
|
32
|
+
const { stdout } = await execAsync(
|
|
33
|
+
'printf "protocol=https\\nhost=github.com\\n" | git credential fill 2>/dev/null'
|
|
34
|
+
);
|
|
35
|
+
const match = stdout.match(/^password=(.+)$/m);
|
|
36
|
+
if (match?.[1]?.trim()) {
|
|
37
|
+
const helper = await execAsync('git config --global credential.helper 2>/dev/null')
|
|
38
|
+
.then(r => r.stdout.trim() || 'system')
|
|
39
|
+
.catch(() => 'system');
|
|
40
|
+
return { method: 'system', label: `git credential helper (${helper})` };
|
|
41
|
+
}
|
|
42
|
+
} catch {}
|
|
43
|
+
|
|
44
|
+
return { method: 'pat', label: null };
|
|
45
|
+
}
|
|
46
|
+
|
|
9
47
|
export async function runInit() {
|
|
10
48
|
log.header('Claude Backup — Setup Wizard');
|
|
11
49
|
|
|
12
50
|
const existing = loadConfig();
|
|
13
51
|
|
|
14
|
-
// ── Step 1:
|
|
15
|
-
console.log(chalk.bold.underline('Step 1 of 4 — GitHub
|
|
16
|
-
console.log(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
52
|
+
// ── Step 1: Auth detection ───────────────────────────────────────────────
|
|
53
|
+
console.log(chalk.bold.underline('Step 1 of 4 — GitHub Authentication') + '\n');
|
|
54
|
+
console.log(chalk.dim(' Checking for existing GitHub credentials...\n'));
|
|
55
|
+
|
|
56
|
+
const detected = await detectAuth();
|
|
57
|
+
let pat = '';
|
|
58
|
+
let authMethod = detected.method;
|
|
59
|
+
|
|
60
|
+
if (detected.method !== 'pat') {
|
|
61
|
+
console.log(chalk.green(' ✔') + ' Found: ' + chalk.cyan(detected.label));
|
|
62
|
+
console.log(chalk.dim(' Git will authenticate automatically — no token needed.\n'));
|
|
63
|
+
} else {
|
|
64
|
+
console.log(chalk.yellow(' ✗') + ' No system GitHub auth detected.\n');
|
|
65
|
+
console.log('claude-code-backup needs a PAT with ' + chalk.cyan('"repo"') + ' scope to create');
|
|
66
|
+
console.log('and push to a private GitHub repository on your behalf.\n');
|
|
67
|
+
console.log(chalk.bold(' Create your token here:'));
|
|
68
|
+
console.log(' ' + chalk.underline.blue('https://github.com/settings/tokens/new') + '\n');
|
|
69
|
+
console.log(chalk.dim(' Instructions:'));
|
|
70
|
+
console.log(chalk.dim(' 1. Note name → e.g. "claude-code-backup"'));
|
|
71
|
+
console.log(chalk.dim(' 2. Expiration → your preference (No expiration is fine)'));
|
|
72
|
+
console.log(chalk.dim(' 3. Scopes → tick ') + chalk.cyan('repo') + chalk.dim(' (the top-level checkbox)'));
|
|
73
|
+
console.log(chalk.dim(' 4. Click "Generate token" and copy the value\n'));
|
|
74
|
+
|
|
75
|
+
const { patInput } = await inquirer.prompt([
|
|
76
|
+
{
|
|
77
|
+
type: 'password',
|
|
78
|
+
name: 'patInput',
|
|
79
|
+
message: 'Paste your GitHub PAT:',
|
|
80
|
+
default: existing?.github?.pat || '',
|
|
81
|
+
validate: v => v.trim().length > 0 || 'PAT is required',
|
|
82
|
+
},
|
|
83
|
+
]);
|
|
84
|
+
pat = patInput.trim();
|
|
85
|
+
}
|
|
38
86
|
|
|
39
87
|
// ── Step 2: Repo & branch ────────────────────────────────────────────────
|
|
40
88
|
console.log('\n' + chalk.bold.underline('Step 2 of 4 — Repository & Branch') + '\n');
|
|
@@ -111,10 +159,11 @@ export async function runInit() {
|
|
|
111
159
|
const config = {
|
|
112
160
|
backend: 'github',
|
|
113
161
|
github: {
|
|
114
|
-
pat: pat.trim(),
|
|
115
162
|
repo: repo.trim(),
|
|
116
163
|
branch: branch.trim(),
|
|
164
|
+
...(pat ? { pat } : {}),
|
|
117
165
|
},
|
|
166
|
+
auth_method: authMethod,
|
|
118
167
|
watched_dirs,
|
|
119
168
|
claude_md_dirs,
|
|
120
169
|
exclude,
|
package/src/core/config.js
CHANGED
|
@@ -33,7 +33,8 @@ export function saveConfig(config) {
|
|
|
33
33
|
|
|
34
34
|
export function requireConfig() {
|
|
35
35
|
const config = loadConfig();
|
|
36
|
-
|
|
36
|
+
// Require repo; require PAT only for legacy configs without auth_method
|
|
37
|
+
if (!config || !config.github?.repo || (!config.auth_method && !config.github?.pat)) {
|
|
37
38
|
console.error('Not configured. Run: claude-code-backup init');
|
|
38
39
|
process.exit(1);
|
|
39
40
|
}
|