memoir-cli 1.1.1 → 1.2.1
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/README.md +5 -3
- package/bin/memoir.js +30 -10
- package/package.json +1 -1
- package/src/adapters/index.js +63 -4
- package/src/adapters/restore.js +16 -8
- package/src/commands/push.js +46 -12
- package/src/commands/restore.js +22 -8
- package/src/commands/status.js +63 -0
- package/src/providers/index.js +37 -11
package/README.md
CHANGED
|
@@ -28,10 +28,12 @@ No locked-in SaaS, no lost context, no complex shell scripts.
|
|
|
28
28
|
|
|
29
29
|
### Supported Integrations
|
|
30
30
|
- [x] **Gemini CLI**
|
|
31
|
-
- [x] **Claude
|
|
31
|
+
- [x] **Claude Code**
|
|
32
32
|
- [x] **OpenAI Codex CLI**
|
|
33
|
-
- [
|
|
34
|
-
- [
|
|
33
|
+
- [x] **Cursor**
|
|
34
|
+
- [x] **GitHub Copilot**
|
|
35
|
+
- [x] **Windsurf**
|
|
36
|
+
- [x] **Aider**
|
|
35
37
|
|
|
36
38
|
---
|
|
37
39
|
|
package/bin/memoir.js
CHANGED
|
@@ -6,17 +6,25 @@ import gradient from 'gradient-string';
|
|
|
6
6
|
import { initCommand } from '../src/commands/init.js';
|
|
7
7
|
import { pushCommand } from '../src/commands/push.js';
|
|
8
8
|
import { restoreCommand } from '../src/commands/restore.js';
|
|
9
|
+
import { statusCommand } from '../src/commands/status.js';
|
|
9
10
|
|
|
10
|
-
const VERSION = '1.
|
|
11
|
+
const VERSION = '1.2.0';
|
|
12
|
+
|
|
13
|
+
// Custom help banner
|
|
14
|
+
program.addHelpText('beforeAll', '\n' + boxen(
|
|
15
|
+
gradient.pastel.multiline(' memoir ') + '\n' +
|
|
16
|
+
chalk.gray(' Your AI remembers everything.'),
|
|
17
|
+
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderStyle: 'round', borderColor: 'cyan', dimBorder: true }
|
|
18
|
+
) + '\n');
|
|
11
19
|
|
|
12
20
|
program
|
|
13
21
|
.name('memoir')
|
|
14
|
-
.description('
|
|
22
|
+
.description(chalk.white('Sync your AI memory across every device.'))
|
|
15
23
|
.version(VERSION);
|
|
16
24
|
|
|
17
25
|
program
|
|
18
26
|
.command('init')
|
|
19
|
-
.description('
|
|
27
|
+
.description('Set up memoir with your storage provider')
|
|
20
28
|
.action(async () => {
|
|
21
29
|
try {
|
|
22
30
|
await initCommand();
|
|
@@ -29,7 +37,7 @@ program
|
|
|
29
37
|
program
|
|
30
38
|
.command('push')
|
|
31
39
|
.alias('remember')
|
|
32
|
-
.description('
|
|
40
|
+
.description('Back up your AI memory to the cloud')
|
|
33
41
|
.action(async () => {
|
|
34
42
|
try {
|
|
35
43
|
await pushCommand();
|
|
@@ -42,7 +50,7 @@ program
|
|
|
42
50
|
program
|
|
43
51
|
.command('restore')
|
|
44
52
|
.alias('pull')
|
|
45
|
-
.description('Restore your AI memory
|
|
53
|
+
.description('Restore your AI memory on this machine')
|
|
46
54
|
.action(async () => {
|
|
47
55
|
try {
|
|
48
56
|
await restoreCommand();
|
|
@@ -52,15 +60,27 @@ program
|
|
|
52
60
|
}
|
|
53
61
|
});
|
|
54
62
|
|
|
63
|
+
program
|
|
64
|
+
.command('status')
|
|
65
|
+
.description('See what AI tools are on this machine')
|
|
66
|
+
.action(async () => {
|
|
67
|
+
try {
|
|
68
|
+
await statusCommand();
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.error(chalk.red('\n✖ Error:'), err.message);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
55
75
|
program
|
|
56
76
|
.command('migrate')
|
|
57
|
-
.description('
|
|
77
|
+
.description('Translate memory between AI providers')
|
|
58
78
|
.action(() => {
|
|
59
79
|
console.log('\n' + boxen(
|
|
60
|
-
gradient.pastel('memoir migrate
|
|
61
|
-
chalk.white('
|
|
62
|
-
chalk.white('
|
|
63
|
-
chalk.cyan('
|
|
80
|
+
gradient.pastel(' memoir migrate ') + '\n\n' +
|
|
81
|
+
chalk.white('Instantly translate your context between') + '\n' +
|
|
82
|
+
chalk.white('Claude, Gemini, Codex, and more.') + '\n\n' +
|
|
83
|
+
chalk.cyan.bold('Coming soon.'),
|
|
64
84
|
{ padding: 1, borderStyle: 'round', borderColor: 'yellow', align: 'center' }
|
|
65
85
|
) + '\n');
|
|
66
86
|
});
|
package/package.json
CHANGED
package/src/adapters/index.js
CHANGED
|
@@ -5,6 +5,9 @@ import chalk from 'chalk';
|
|
|
5
5
|
|
|
6
6
|
const home = os.homedir();
|
|
7
7
|
|
|
8
|
+
const isWin = process.platform === 'win32';
|
|
9
|
+
const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
|
10
|
+
|
|
8
11
|
export const adapters = [
|
|
9
12
|
{
|
|
10
13
|
name: 'Gemini CLI',
|
|
@@ -31,21 +34,77 @@ export const adapters = [
|
|
|
31
34
|
const ignored = ['.git', 'sessions', 'cache'];
|
|
32
35
|
return !ignored.includes(basename) && !basename.endsWith('.key') && basename !== '.env';
|
|
33
36
|
}
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'Cursor',
|
|
40
|
+
source: isWin
|
|
41
|
+
? path.join(appData, 'Cursor', 'User')
|
|
42
|
+
: path.join(home, 'Library', 'Application Support', 'Cursor', 'User'),
|
|
43
|
+
filter: (src) => {
|
|
44
|
+
const basename = path.basename(src);
|
|
45
|
+
const ignored = ['globalStorage', 'workspaceStorage', 'CachedData', 'Cache', 'GPUCache', 'logs', 'History', 'Backups', 'snippets'];
|
|
46
|
+
return !ignored.includes(basename);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'GitHub Copilot',
|
|
51
|
+
source: isWin
|
|
52
|
+
? path.join(appData, 'GitHub Copilot')
|
|
53
|
+
: path.join(home, '.config', 'github-copilot'),
|
|
54
|
+
filter: (src) => {
|
|
55
|
+
const basename = path.basename(src);
|
|
56
|
+
const ignored = ['hosts.json', 'apps.json', 'versions.json'];
|
|
57
|
+
return !ignored.includes(basename);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'Windsurf',
|
|
62
|
+
source: isWin
|
|
63
|
+
? path.join(appData, 'Windsurf', 'User')
|
|
64
|
+
: path.join(home, 'Library', 'Application Support', 'Windsurf', 'User'),
|
|
65
|
+
filter: (src) => {
|
|
66
|
+
const basename = path.basename(src);
|
|
67
|
+
const ignored = ['workspaceStorage', 'CachedData', 'Cache', 'GPUCache', 'logs', 'History', 'Backups', 'memories', 'snippets'];
|
|
68
|
+
return !ignored.includes(basename);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'Aider',
|
|
73
|
+
source: home,
|
|
74
|
+
customExtract: true,
|
|
75
|
+
files: ['.aider.conf.yml', '.aider.system-prompt.md'],
|
|
76
|
+
filter: () => true
|
|
34
77
|
}
|
|
35
78
|
];
|
|
36
79
|
|
|
37
80
|
export async function extractMemories(stagingDir, spinner) {
|
|
38
81
|
let foundAny = false;
|
|
39
|
-
|
|
82
|
+
|
|
40
83
|
for (const adapter of adapters) {
|
|
41
|
-
if (
|
|
84
|
+
if (adapter.customExtract) {
|
|
85
|
+
// Handle tools with individual files (e.g. Aider)
|
|
86
|
+
const dest = path.join(stagingDir, adapter.name.toLowerCase().replace(/ /g, '-'));
|
|
87
|
+
let foundFile = false;
|
|
88
|
+
for (const file of adapter.files) {
|
|
89
|
+
const filePath = path.join(adapter.source, file);
|
|
90
|
+
if (await fs.pathExists(filePath)) {
|
|
91
|
+
if (!foundFile) {
|
|
92
|
+
spinner.text = `Found ${chalk.cyan(adapter.name)} config... copying to staging`;
|
|
93
|
+
await fs.ensureDir(dest);
|
|
94
|
+
foundFile = true;
|
|
95
|
+
}
|
|
96
|
+
await fs.copy(filePath, path.join(dest, file));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (foundFile) foundAny = true;
|
|
100
|
+
} else if (await fs.pathExists(adapter.source)) {
|
|
42
101
|
spinner.text = `Found ${chalk.cyan(adapter.name)} memory... copying to staging`;
|
|
43
|
-
const dest = path.join(stagingDir, adapter.name.toLowerCase().replace(
|
|
102
|
+
const dest = path.join(stagingDir, adapter.name.toLowerCase().replace(/ /g, '-'));
|
|
44
103
|
await fs.ensureDir(dest);
|
|
45
104
|
await fs.copy(adapter.source, dest, { filter: adapter.filter });
|
|
46
105
|
foundAny = true;
|
|
47
106
|
}
|
|
48
107
|
}
|
|
49
|
-
|
|
108
|
+
|
|
50
109
|
return foundAny;
|
|
51
110
|
}
|
package/src/adapters/restore.js
CHANGED
|
@@ -9,11 +9,11 @@ export async function restoreMemories(sourceDir, spinner) {
|
|
|
9
9
|
let restoredAny = false;
|
|
10
10
|
|
|
11
11
|
for (const adapter of adapters) {
|
|
12
|
-
const backupDir = path.join(sourceDir, adapter.name.toLowerCase().replace(
|
|
13
|
-
|
|
12
|
+
const backupDir = path.join(sourceDir, adapter.name.toLowerCase().replace(/ /g, '-'));
|
|
13
|
+
|
|
14
14
|
if (await fs.pathExists(backupDir)) {
|
|
15
15
|
spinner.stop();
|
|
16
|
-
|
|
16
|
+
|
|
17
17
|
console.log('\\n' + chalk.yellow(`⚠ Found backup for ${chalk.bold(adapter.name)}.`));
|
|
18
18
|
const { confirm } = await inquirer.prompt([
|
|
19
19
|
{
|
|
@@ -25,12 +25,20 @@ export async function restoreMemories(sourceDir, spinner) {
|
|
|
25
25
|
]);
|
|
26
26
|
|
|
27
27
|
spinner.start();
|
|
28
|
-
|
|
28
|
+
|
|
29
29
|
if (confirm) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
if (adapter.customExtract) {
|
|
31
|
+
// Restore individual files back to their original locations
|
|
32
|
+
const files = await fs.readdir(backupDir);
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
const dest = path.join(adapter.source, file);
|
|
35
|
+
await fs.copy(path.join(backupDir, file), dest, { overwrite: true });
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
spinner.text = `Restoring ${chalk.cyan(adapter.name)} memory to ${adapter.source}...`;
|
|
39
|
+
await fs.ensureDir(adapter.source);
|
|
40
|
+
await fs.copy(backupDir, adapter.source, { overwrite: true });
|
|
41
|
+
}
|
|
34
42
|
restoredAny = true;
|
|
35
43
|
} else {
|
|
36
44
|
spinner.info(chalk.gray(`Skipped restoring ${adapter.name}.`));
|
package/src/commands/push.js
CHANGED
|
@@ -3,48 +3,82 @@ import fs from 'fs-extra';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import os from 'os';
|
|
5
5
|
import ora from 'ora';
|
|
6
|
+
import boxen from 'boxen';
|
|
7
|
+
import gradient from 'gradient-string';
|
|
6
8
|
import { getConfig } from '../config.js';
|
|
7
|
-
import { extractMemories } from '../adapters/index.js';
|
|
9
|
+
import { extractMemories, adapters } from '../adapters/index.js';
|
|
8
10
|
import { syncToLocal, syncToGit } from '../providers/index.js';
|
|
9
11
|
|
|
10
12
|
export async function pushCommand() {
|
|
11
13
|
const config = await getConfig();
|
|
12
|
-
|
|
14
|
+
|
|
13
15
|
if (!config) {
|
|
14
|
-
console.log('
|
|
15
|
-
|
|
16
|
+
console.log('\n' + boxen(
|
|
17
|
+
chalk.red('✖ Not configured yet\n\n') +
|
|
18
|
+
chalk.white('Run ') + chalk.cyan.bold('memoir init') + chalk.white(' to get started.'),
|
|
19
|
+
{ padding: 1, borderStyle: 'round', borderColor: 'red' }
|
|
20
|
+
) + '\n');
|
|
16
21
|
return;
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
console.log();
|
|
20
|
-
const spinner = ora('
|
|
25
|
+
const spinner = ora({ text: chalk.gray('Scanning for AI tools...'), spinner: 'dots' }).start();
|
|
21
26
|
|
|
22
|
-
// Create a temporary staging directory
|
|
23
27
|
const stagingDir = path.join(os.tmpdir(), `memoir-staging-${Date.now()}`);
|
|
24
28
|
await fs.ensureDir(stagingDir);
|
|
25
29
|
|
|
26
30
|
try {
|
|
27
|
-
spinner.text = 'Scanning system for AI configurations...';
|
|
28
|
-
|
|
29
|
-
// Pass spinner so adapter can update it
|
|
30
31
|
const foundAny = await extractMemories(stagingDir, spinner);
|
|
31
|
-
|
|
32
|
+
|
|
32
33
|
if (!foundAny) {
|
|
33
|
-
spinner.
|
|
34
|
+
spinner.stop();
|
|
35
|
+
console.log('\n' + boxen(
|
|
36
|
+
chalk.yellow('No AI tools detected on this machine.\n\n') +
|
|
37
|
+
chalk.gray('Supported: Claude, Gemini, Codex, Cursor, Copilot, Windsurf, Aider'),
|
|
38
|
+
{ padding: 1, borderStyle: 'round', borderColor: 'yellow' }
|
|
39
|
+
) + '\n');
|
|
34
40
|
return;
|
|
35
41
|
}
|
|
36
42
|
|
|
43
|
+
// Count what was found
|
|
44
|
+
const found = [];
|
|
45
|
+
for (const adapter of adapters) {
|
|
46
|
+
if (adapter.customExtract) {
|
|
47
|
+
for (const file of adapter.files) {
|
|
48
|
+
if (await fs.pathExists(path.join(adapter.source, file))) {
|
|
49
|
+
found.push(adapter.name);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} else if (await fs.pathExists(adapter.source)) {
|
|
54
|
+
found.push(adapter.name);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
spinner.text = chalk.gray('Uploading to ' + (config.provider === 'git' ? 'GitHub' : 'local storage') + '...');
|
|
59
|
+
|
|
37
60
|
if (config.provider === 'local' || config.provider.includes('local')) {
|
|
38
61
|
await syncToLocal(config, stagingDir, spinner);
|
|
39
62
|
} else if (config.provider === 'git' || config.provider.includes('git')) {
|
|
40
63
|
await syncToGit(config, stagingDir, spinner);
|
|
41
64
|
} else {
|
|
42
65
|
spinner.fail(chalk.red(`Unknown provider: ${config.provider}`));
|
|
66
|
+
return;
|
|
43
67
|
}
|
|
68
|
+
|
|
69
|
+
spinner.stop();
|
|
70
|
+
|
|
71
|
+
// Success output
|
|
72
|
+
const toolList = found.map(t => chalk.cyan(' ✔ ' + t)).join('\n');
|
|
73
|
+
console.log('\n' + boxen(
|
|
74
|
+
gradient.pastel(' Backed up! ') + '\n\n' +
|
|
75
|
+
toolList + '\n\n' +
|
|
76
|
+
chalk.gray(`${found.length} tool${found.length !== 1 ? 's' : ''} synced to ${config.provider === 'git' ? 'GitHub' : 'local storage'}`),
|
|
77
|
+
{ padding: 1, borderStyle: 'round', borderColor: 'green', dimBorder: true }
|
|
78
|
+
) + '\n');
|
|
44
79
|
} catch (error) {
|
|
45
80
|
spinner.fail(chalk.red('Sync failed: ') + error.message);
|
|
46
81
|
} finally {
|
|
47
|
-
// Clean up staging directory
|
|
48
82
|
await fs.remove(stagingDir);
|
|
49
83
|
}
|
|
50
84
|
}
|
package/src/commands/restore.js
CHANGED
|
@@ -3,22 +3,26 @@ import fs from 'fs-extra';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import os from 'os';
|
|
5
5
|
import ora from 'ora';
|
|
6
|
+
import boxen from 'boxen';
|
|
7
|
+
import gradient from 'gradient-string';
|
|
6
8
|
import { getConfig } from '../config.js';
|
|
7
9
|
import { fetchFromLocal, fetchFromGit } from '../providers/restore.js';
|
|
8
10
|
|
|
9
11
|
export async function restoreCommand() {
|
|
10
12
|
const config = await getConfig();
|
|
11
|
-
|
|
13
|
+
|
|
12
14
|
if (!config) {
|
|
13
|
-
console.log('
|
|
14
|
-
|
|
15
|
+
console.log('\n' + boxen(
|
|
16
|
+
chalk.red('✖ Not configured yet\n\n') +
|
|
17
|
+
chalk.white('Run ') + chalk.cyan.bold('memoir init') + chalk.white(' to get started.'),
|
|
18
|
+
{ padding: 1, borderStyle: 'round', borderColor: 'red' }
|
|
19
|
+
) + '\n');
|
|
15
20
|
return;
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
console.log();
|
|
19
|
-
const spinner = ora('
|
|
24
|
+
const spinner = ora({ text: chalk.gray('Fetching memories from ' + (config.provider === 'git' ? 'GitHub' : 'local storage') + '...'), spinner: 'dots' }).start();
|
|
20
25
|
|
|
21
|
-
// Create a temporary staging directory to hold the downloaded files
|
|
22
26
|
const stagingDir = path.join(os.tmpdir(), `memoir-restore-${Date.now()}`);
|
|
23
27
|
await fs.ensureDir(stagingDir);
|
|
24
28
|
|
|
@@ -34,16 +38,26 @@ export async function restoreCommand() {
|
|
|
34
38
|
return;
|
|
35
39
|
}
|
|
36
40
|
|
|
41
|
+
spinner.stop();
|
|
42
|
+
|
|
37
43
|
if (restored) {
|
|
38
|
-
|
|
44
|
+
console.log('\n' + boxen(
|
|
45
|
+
gradient.pastel(' Restored! ') + '\n\n' +
|
|
46
|
+
chalk.white('Your AI tools have their memories back.') + '\n' +
|
|
47
|
+
chalk.gray('They remember everything.'),
|
|
48
|
+
{ padding: 1, borderStyle: 'round', borderColor: 'green', dimBorder: true }
|
|
49
|
+
) + '\n');
|
|
39
50
|
} else {
|
|
40
|
-
|
|
51
|
+
console.log('\n' + boxen(
|
|
52
|
+
chalk.yellow('No memories were restored.\n\n') +
|
|
53
|
+
chalk.gray('Run ') + chalk.cyan('memoir push') + chalk.gray(' on another machine first.'),
|
|
54
|
+
{ padding: 1, borderStyle: 'round', borderColor: 'yellow' }
|
|
55
|
+
) + '\n');
|
|
41
56
|
}
|
|
42
57
|
|
|
43
58
|
} catch (error) {
|
|
44
59
|
spinner.fail(chalk.red('Restore failed: ') + error.message);
|
|
45
60
|
} finally {
|
|
46
|
-
// Clean up staging directory
|
|
47
61
|
await fs.remove(stagingDir);
|
|
48
62
|
}
|
|
49
63
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import boxen from 'boxen';
|
|
5
|
+
import gradient from 'gradient-string';
|
|
6
|
+
import { getConfig } from '../config.js';
|
|
7
|
+
import { adapters } from '../adapters/index.js';
|
|
8
|
+
|
|
9
|
+
export async function statusCommand() {
|
|
10
|
+
const config = await getConfig();
|
|
11
|
+
|
|
12
|
+
console.log();
|
|
13
|
+
|
|
14
|
+
// Config status
|
|
15
|
+
let configLine;
|
|
16
|
+
if (config) {
|
|
17
|
+
const provider = config.provider === 'git'
|
|
18
|
+
? chalk.cyan(config.gitRepo)
|
|
19
|
+
: chalk.cyan(config.localPath);
|
|
20
|
+
configLine = chalk.green('✔ Connected') + chalk.gray(' → ') + provider;
|
|
21
|
+
} else {
|
|
22
|
+
configLine = chalk.red('✖ Not configured') + chalk.gray(' → run ') + chalk.cyan('memoir init');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Detected tools
|
|
26
|
+
const lines = [];
|
|
27
|
+
let detected = 0;
|
|
28
|
+
|
|
29
|
+
for (const adapter of adapters) {
|
|
30
|
+
let found = false;
|
|
31
|
+
if (adapter.customExtract) {
|
|
32
|
+
for (const file of adapter.files) {
|
|
33
|
+
if (await fs.pathExists(path.join(adapter.source, file))) {
|
|
34
|
+
found = true;
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
found = await fs.pathExists(adapter.source);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (found) {
|
|
43
|
+
lines.push(chalk.green(' ✔ ') + chalk.white(adapter.name));
|
|
44
|
+
detected++;
|
|
45
|
+
} else {
|
|
46
|
+
lines.push(chalk.gray(' ○ ' + adapter.name));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const summary = detected > 0
|
|
51
|
+
? chalk.white(`${detected} tool${detected !== 1 ? 's' : ''} ready to sync`)
|
|
52
|
+
: chalk.yellow('No AI tools detected');
|
|
53
|
+
|
|
54
|
+
console.log(boxen(
|
|
55
|
+
gradient.pastel(' memoir status ') + '\n\n' +
|
|
56
|
+
configLine + '\n\n' +
|
|
57
|
+
chalk.bold.white('AI Tools') + '\n' +
|
|
58
|
+
lines.join('\n') + '\n\n' +
|
|
59
|
+
chalk.gray('─'.repeat(30)) + '\n' +
|
|
60
|
+
summary,
|
|
61
|
+
{ padding: 1, borderStyle: 'round', borderColor: 'cyan', dimBorder: true }
|
|
62
|
+
) + '\n');
|
|
63
|
+
}
|
package/src/providers/index.js
CHANGED
|
@@ -24,20 +24,46 @@ export async function syncToGit(config, stagingDir, spinner) {
|
|
|
24
24
|
|
|
25
25
|
spinner.text = `Authenticating and syncing with Git remote: ${chalk.cyan(repoUrl)}`;
|
|
26
26
|
|
|
27
|
+
// Clone existing repo to preserve history, then replace contents
|
|
28
|
+
const gitDir = path.join(os.tmpdir(), `memoir-git-${Date.now()}`);
|
|
29
|
+
await fs.ensureDir(gitDir);
|
|
30
|
+
|
|
27
31
|
try {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
try {
|
|
33
|
+
execSync(`git clone --depth 10 ${repoUrl} .`, { cwd: gitDir, stdio: 'ignore' });
|
|
34
|
+
// Remove old files so deleted configs don't persist
|
|
35
|
+
const files = await fs.readdir(gitDir);
|
|
36
|
+
for (const f of files) {
|
|
37
|
+
if (f !== '.git') await fs.remove(path.join(gitDir, f));
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
// Repo is empty or doesn't exist yet — init fresh
|
|
41
|
+
execSync('git init', { cwd: gitDir, stdio: 'ignore' });
|
|
42
|
+
execSync('git branch -m main', { cwd: gitDir, stdio: 'ignore' });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Copy staged memories into the git dir
|
|
46
|
+
await fs.copy(stagingDir, gitDir);
|
|
47
|
+
|
|
48
|
+
execSync('git add -A', { cwd: gitDir, stdio: 'ignore' });
|
|
49
|
+
execSync('git config user.name "memoir"', { cwd: gitDir, stdio: 'ignore' });
|
|
50
|
+
execSync('git config user.email "bot@memoir.dev"', { cwd: gitDir, stdio: 'ignore' });
|
|
51
|
+
|
|
52
|
+
const timestamp = new Date().toISOString().split('T')[0];
|
|
53
|
+
try {
|
|
54
|
+
execSync(`git commit -m "memoir backup ${timestamp}"`, { cwd: gitDir, stdio: 'ignore' });
|
|
55
|
+
} catch {
|
|
56
|
+
spinner.succeed(chalk.green('Already up to date! ') + chalk.gray('No changes to push.'));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
35
60
|
spinner.text = `Pushing data to ${chalk.cyan(repoUrl)}...`;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
61
|
+
execSync(`git push ${repoUrl} main`, { cwd: gitDir, stdio: 'ignore' });
|
|
62
|
+
|
|
39
63
|
spinner.succeed(chalk.green('Sync complete! ') + chalk.gray('(Uploaded securely to GitHub)'));
|
|
40
64
|
} catch (err) {
|
|
41
|
-
throw new Error('Failed to push to git repository. Ensure your
|
|
65
|
+
throw new Error('Failed to push to git repository. Ensure your credentials are configured and the repository exists.');
|
|
66
|
+
} finally {
|
|
67
|
+
await fs.remove(gitDir);
|
|
42
68
|
}
|
|
43
69
|
}
|