claude-code-backup 1.0.2 → 1.0.4

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.2",
3
+ "version": "1.0.4",
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,18 +1,54 @@
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
+ // Explicit PAT auth: embed as x-access-token:<PAT> (correct GitHub OAuth format,
19
+ // no credential prompt needed so it works in non-TTY/background service contexts)
20
+ if (config.auth_method === 'pat' && config.github?.pat) {
21
+ return `https://x-access-token:${config.github.pat}@github.com/${config.github.repo}.git`;
22
+ }
23
+ // System auth or legacy config: bare URL, credential helper (osxkeychain/gh) handles it
24
+ return `https://github.com/${config.github.repo}.git`;
25
+ }
26
+
27
+ async function getApiToken(config) {
28
+ if (config.github?.pat) return config.github.pat;
29
+
30
+ // Try git credential helper (osxkeychain, gh, gnome-keyring, etc.)
31
+ try {
32
+ const { stdout } = await execAsync(
33
+ 'printf "protocol=https\\nhost=github.com\\n" | git credential fill'
34
+ );
35
+ const match = stdout.match(/^password=(.+)$/m);
36
+ if (match?.[1]?.trim()) return match[1].trim();
37
+ } catch {}
38
+
39
+ // Try gh CLI
40
+ try {
41
+ const { stdout } = await execAsync('gh auth token 2>/dev/null');
42
+ if (stdout.trim()) return stdout.trim();
43
+ } catch {}
44
+
45
+ return null;
12
46
  }
13
47
 
14
48
  function repoGit() {
15
- return simpleGit(REPO_DIR);
49
+ return simpleGit(REPO_DIR, {
50
+ config: [],
51
+ }).env({ ...process.env, GIT_TERMINAL_PROMPT: '0' });
16
52
  }
17
53
 
18
54
  async function configureGit(g) {
@@ -438,29 +474,34 @@ Copy the files you need back from there manually.
438
474
  }
439
475
 
440
476
  export async function ensureRepo(config) {
441
- const octokit = new Octokit({ auth: config.github.pat });
442
477
  const [owner, repo] = config.github.repo.split('/');
478
+ const token = await getApiToken(config);
443
479
 
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
- }
480
+ if (token) {
481
+ const octokit = new Octokit({ auth: token });
482
+ let repoExists = false;
483
+ try {
484
+ await octokit.repos.get({ owner, repo });
485
+ repoExists = true;
486
+ } catch (err) {
487
+ if (err.status !== 404) throw err;
488
+ }
452
489
 
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}`);
490
+ if (!repoExists) {
491
+ const spin = spinner('Creating private GitHub repo...').start();
492
+ await octokit.repos.createForAuthenticatedUser({
493
+ name: repo,
494
+ private: true,
495
+ description: 'Claude Code backup — memory, settings, commands',
496
+ auto_init: true,
497
+ });
498
+ spin.succeed(`Created: github.com/${config.github.repo}`);
499
+ } else {
500
+ log.info(`Repo exists: github.com/${config.github.repo}`);
501
+ }
462
502
  } else {
463
- log.info(`Repo exists: github.com/${config.github.repo}`);
503
+ log.warn('No GitHub API token found — skipping repo creation check.');
504
+ log.dim(' Make sure the repo exists at github.com and your system git credentials are set up.');
464
505
  }
465
506
 
466
507
  // Clone locally if not already done
@@ -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
  }