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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-backup",
3
- "version": "1.0.1",
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": {
@@ -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
- return `https://${config.github.pat}@github.com/${config.github.repo}.git`;
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
- // 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
- }
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
- 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}`);
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.info(`Repo exists: github.com/${config.github.repo}`);
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: ${new Date().toISOString()}`);
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]);
@@ -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: GitHub PAT ───────────────────────────────────────────────────
15
- console.log(chalk.bold.underline('Step 1 of 4 — GitHub Personal Access Token') + '\n');
16
- console.log('claude-code-backup needs a PAT with ' + chalk.cyan('"repo"') + ' scope to create');
17
- console.log('and push to a private GitHub repository on your behalf.\n');
18
- console.log(chalk.bold(' Create your token here:'));
19
- console.log(' ' + chalk.underline.blue('https://github.com/settings/tokens/new') + '\n');
20
- console.log(chalk.dim(' Instructions:'));
21
- console.log(chalk.dim(' 1. Note name → e.g. "claude-code-backup"'));
22
- console.log(chalk.dim(' 2. Expiration → your preference (No expiration is fine)'));
23
- console.log(chalk.dim(' 3. Scopes → tick ') + chalk.cyan('repo') + chalk.dim(' (the top-level checkbox covers everything needed)'));
24
- console.log(chalk.dim(' 4. Click "Generate token" and copy the value\n'));
25
- console.log(chalk.dim(' Fine-grained token alternative:'));
26
- console.log(chalk.dim(' ' + chalk.underline('https://github.com/settings/personal-access-tokens/new')));
27
- console.log(chalk.dim(' Repository permissions: Contents (Read & Write), Metadata (Read)\n'));
28
-
29
- const { pat } = await inquirer.prompt([
30
- {
31
- type: 'password',
32
- name: 'pat',
33
- message: 'Paste your GitHub PAT:',
34
- default: existing?.github?.pat || '',
35
- validate: v => v.trim().length > 0 || 'PAT is required',
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,
@@ -33,7 +33,8 @@ export function saveConfig(config) {
33
33
 
34
34
  export function requireConfig() {
35
35
  const config = loadConfig();
36
- if (!config || !config.github?.pat || !config.github?.repo) {
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
  }