memoir-cli 1.3.0 → 1.4.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/bin/memoir.js +14 -0
- package/package.json +1 -1
- package/src/adapters/restore.js +27 -9
- package/src/commands/view.js +176 -0
package/bin/memoir.js
CHANGED
|
@@ -7,6 +7,7 @@ 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
9
|
import { statusCommand } from '../src/commands/status.js';
|
|
10
|
+
import { viewCommand } from '../src/commands/view.js';
|
|
10
11
|
|
|
11
12
|
const VERSION = '1.2.0';
|
|
12
13
|
|
|
@@ -72,6 +73,19 @@ program
|
|
|
72
73
|
}
|
|
73
74
|
});
|
|
74
75
|
|
|
76
|
+
program
|
|
77
|
+
.command('view')
|
|
78
|
+
.alias('ls')
|
|
79
|
+
.description('Preview what files are in your backup')
|
|
80
|
+
.action(async () => {
|
|
81
|
+
try {
|
|
82
|
+
await viewCommand();
|
|
83
|
+
} catch (err) {
|
|
84
|
+
console.error(chalk.red('\n✖ Error:'), err.message);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
75
89
|
program
|
|
76
90
|
.command('migrate')
|
|
77
91
|
.description('Translate memory between AI providers')
|
package/package.json
CHANGED
package/src/adapters/restore.js
CHANGED
|
@@ -5,7 +5,7 @@ import os from 'os';
|
|
|
5
5
|
import inquirer from 'inquirer';
|
|
6
6
|
import { adapters } from '../adapters/index.js';
|
|
7
7
|
|
|
8
|
-
async function copyMissing(src, dest,
|
|
8
|
+
async function copyMissing(src, dest, changes) {
|
|
9
9
|
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
10
10
|
for (const entry of entries) {
|
|
11
11
|
const srcPath = path.join(src, entry.name);
|
|
@@ -13,12 +13,13 @@ async function copyMissing(src, dest, spinner) {
|
|
|
13
13
|
|
|
14
14
|
if (entry.isDirectory()) {
|
|
15
15
|
await fs.ensureDir(destPath);
|
|
16
|
-
await copyMissing(srcPath, destPath,
|
|
16
|
+
await copyMissing(srcPath, destPath, changes);
|
|
17
17
|
} else {
|
|
18
18
|
if (await fs.pathExists(destPath)) {
|
|
19
|
-
|
|
19
|
+
changes.skipped.push(destPath);
|
|
20
20
|
} else {
|
|
21
21
|
await fs.copy(srcPath, destPath);
|
|
22
|
+
changes.added.push(destPath);
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
}
|
|
@@ -33,7 +34,7 @@ export async function restoreMemories(sourceDir, spinner) {
|
|
|
33
34
|
if (await fs.pathExists(backupDir)) {
|
|
34
35
|
spinner.stop();
|
|
35
36
|
|
|
36
|
-
console.log('
|
|
37
|
+
console.log('\n' + chalk.yellow(`⚠ Found backup for ${chalk.bold(adapter.name)}.`));
|
|
37
38
|
const { confirm } = await inquirer.prompt([
|
|
38
39
|
{
|
|
39
40
|
type: 'confirm',
|
|
@@ -46,24 +47,41 @@ export async function restoreMemories(sourceDir, spinner) {
|
|
|
46
47
|
spinner.start();
|
|
47
48
|
|
|
48
49
|
if (confirm) {
|
|
50
|
+
const changes = { added: [], skipped: [] };
|
|
51
|
+
|
|
49
52
|
if (adapter.customExtract) {
|
|
50
|
-
// Restore individual files — only add missing ones
|
|
51
53
|
const files = await fs.readdir(backupDir);
|
|
52
54
|
for (const file of files) {
|
|
53
55
|
const dest = path.join(adapter.source, file);
|
|
54
56
|
if (await fs.pathExists(dest)) {
|
|
55
|
-
|
|
56
|
-
spinner.start();
|
|
57
|
+
changes.skipped.push(dest);
|
|
57
58
|
} else {
|
|
58
59
|
await fs.copy(path.join(backupDir, file), dest);
|
|
60
|
+
changes.added.push(dest);
|
|
59
61
|
}
|
|
60
62
|
}
|
|
61
63
|
} else {
|
|
62
64
|
spinner.text = `Restoring ${chalk.cyan(adapter.name)} memory to ${adapter.source}...`;
|
|
63
65
|
await fs.ensureDir(adapter.source);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
await copyMissing(backupDir, adapter.source, changes);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Show summary of changes
|
|
70
|
+
spinner.stop();
|
|
71
|
+
if (changes.added.length > 0) {
|
|
72
|
+
console.log(chalk.green.bold(`\n ✔ ${adapter.name} — ${changes.added.length} file(s) added:`));
|
|
73
|
+
for (const f of changes.added) {
|
|
74
|
+
console.log(chalk.green(` + ${f}`));
|
|
75
|
+
}
|
|
66
76
|
}
|
|
77
|
+
if (changes.skipped.length > 0) {
|
|
78
|
+
console.log(chalk.gray(` ⏭ ${changes.skipped.length} file(s) already existed (kept yours)`));
|
|
79
|
+
}
|
|
80
|
+
if (changes.added.length === 0 && changes.skipped.length > 0) {
|
|
81
|
+
console.log(chalk.gray(` Nothing new to add — you're already up to date.`));
|
|
82
|
+
}
|
|
83
|
+
spinner.start();
|
|
84
|
+
|
|
67
85
|
restoredAny = true;
|
|
68
86
|
} else {
|
|
69
87
|
spinner.info(chalk.gray(`Skipped restoring ${adapter.name}.`));
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import boxen from 'boxen';
|
|
7
|
+
import inquirer from 'inquirer';
|
|
8
|
+
import { execSync } from 'child_process';
|
|
9
|
+
import { loadConfig } from '../config.js';
|
|
10
|
+
import { adapters } from '../adapters/index.js';
|
|
11
|
+
|
|
12
|
+
async function listFiles(dir, prefix = '') {
|
|
13
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
14
|
+
const files = [];
|
|
15
|
+
for (const entry of entries) {
|
|
16
|
+
if (entry.name === '.git') continue;
|
|
17
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
18
|
+
if (entry.isDirectory()) {
|
|
19
|
+
files.push(...await listFiles(path.join(dir, entry.name), rel));
|
|
20
|
+
} else {
|
|
21
|
+
const stat = await fs.stat(path.join(dir, entry.name));
|
|
22
|
+
files.push({ path: rel, size: stat.size });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return files;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function formatSize(bytes) {
|
|
29
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
30
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
31
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isBinaryFile(filePath) {
|
|
35
|
+
const binaryExts = ['.png', '.jpg', '.jpeg', '.gif', '.ico', '.woff', '.woff2', '.ttf', '.eot', '.zip', '.tar', '.gz', '.db', '.sqlite'];
|
|
36
|
+
return binaryExts.includes(path.extname(filePath).toLowerCase());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function viewCommand() {
|
|
40
|
+
const config = await loadConfig();
|
|
41
|
+
if (!config) {
|
|
42
|
+
console.log(chalk.red('\n✖ Not configured yet. Run: memoir init\n'));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const spinner = ora('Fetching backup...').start();
|
|
47
|
+
const stagingDir = path.join(os.tmpdir(), `memoir-view-${Date.now()}`);
|
|
48
|
+
await fs.ensureDir(stagingDir);
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
if (config.provider === 'git') {
|
|
52
|
+
execSync(`git clone --depth 1 ${config.gitRepo} .`, { cwd: stagingDir, stdio: 'ignore' });
|
|
53
|
+
} else {
|
|
54
|
+
const resolvedSource = config.localPath.replace(/^~/, os.homedir());
|
|
55
|
+
await fs.copy(resolvedSource, stagingDir);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
spinner.stop();
|
|
59
|
+
|
|
60
|
+
console.log('\n' + boxen(
|
|
61
|
+
chalk.cyan.bold('Your Memoir Backup'),
|
|
62
|
+
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderStyle: 'round', borderColor: 'cyan' }
|
|
63
|
+
));
|
|
64
|
+
|
|
65
|
+
let totalFiles = 0;
|
|
66
|
+
const viewableFiles = [];
|
|
67
|
+
|
|
68
|
+
for (const adapter of adapters) {
|
|
69
|
+
const adapterDir = path.join(stagingDir, adapter.name.toLowerCase().replace(/ /g, '-'));
|
|
70
|
+
if (await fs.pathExists(adapterDir)) {
|
|
71
|
+
const files = await listFiles(adapterDir);
|
|
72
|
+
totalFiles += files.length;
|
|
73
|
+
|
|
74
|
+
console.log('\n' + chalk.green.bold(` ${adapter.name}`) + chalk.gray(` (${files.length} files)`));
|
|
75
|
+
|
|
76
|
+
for (const file of files) {
|
|
77
|
+
const localPath = path.join(adapter.source, file.path);
|
|
78
|
+
const backupPath = path.join(adapterDir, file.path);
|
|
79
|
+
const existsLocally = await fs.pathExists(localPath);
|
|
80
|
+
|
|
81
|
+
let status;
|
|
82
|
+
let diffType;
|
|
83
|
+
if (!existsLocally) {
|
|
84
|
+
status = chalk.yellow(' (new — not on this machine)');
|
|
85
|
+
diffType = 'new';
|
|
86
|
+
} else if (isBinaryFile(file.path)) {
|
|
87
|
+
status = chalk.gray(' (binary)');
|
|
88
|
+
diffType = 'binary';
|
|
89
|
+
} else {
|
|
90
|
+
// Compare content
|
|
91
|
+
const localContent = await fs.readFile(localPath, 'utf8');
|
|
92
|
+
const backupContent = await fs.readFile(backupPath, 'utf8');
|
|
93
|
+
if (localContent === backupContent) {
|
|
94
|
+
status = chalk.gray(' (identical)');
|
|
95
|
+
diffType = 'same';
|
|
96
|
+
} else {
|
|
97
|
+
status = chalk.cyan(' (different)');
|
|
98
|
+
diffType = 'different';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log(chalk.white(` ${file.path}`) + chalk.gray(` ${formatSize(file.size)}`) + status);
|
|
103
|
+
|
|
104
|
+
if (diffType !== 'binary' && diffType !== 'same') {
|
|
105
|
+
viewableFiles.push({ file, adapter, adapterDir, localPath, diffType });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (totalFiles === 0) {
|
|
112
|
+
console.log(chalk.yellow('\n No backed up files found.\n'));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log(chalk.gray(`\n ${totalFiles} total files in backup\n`));
|
|
117
|
+
|
|
118
|
+
// Offer to view diffs
|
|
119
|
+
if (viewableFiles.length > 0) {
|
|
120
|
+
const { wantDiff } = await inquirer.prompt([
|
|
121
|
+
{
|
|
122
|
+
type: 'confirm',
|
|
123
|
+
name: 'wantDiff',
|
|
124
|
+
message: 'View file contents/diffs?',
|
|
125
|
+
default: true
|
|
126
|
+
}
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
if (wantDiff) {
|
|
130
|
+
const choices = viewableFiles.map(vf => ({
|
|
131
|
+
name: `${vf.adapter.name}/${vf.file.path}` + (vf.diffType === 'new' ? chalk.yellow(' (new)') : chalk.cyan(' (different)')),
|
|
132
|
+
value: vf
|
|
133
|
+
}));
|
|
134
|
+
|
|
135
|
+
const { filesToView } = await inquirer.prompt([
|
|
136
|
+
{
|
|
137
|
+
type: 'checkbox',
|
|
138
|
+
name: 'filesToView',
|
|
139
|
+
message: 'Select files to view:',
|
|
140
|
+
choices
|
|
141
|
+
}
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
for (const vf of filesToView) {
|
|
145
|
+
const backupPath = path.join(vf.adapterDir, vf.file.path);
|
|
146
|
+
const backupContent = await fs.readFile(backupPath, 'utf8');
|
|
147
|
+
|
|
148
|
+
console.log('\n' + boxen(
|
|
149
|
+
chalk.cyan.bold(`${vf.adapter.name}/${vf.file.path}`),
|
|
150
|
+
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderStyle: 'single', borderColor: 'cyan' }
|
|
151
|
+
));
|
|
152
|
+
|
|
153
|
+
if (vf.diffType === 'new') {
|
|
154
|
+
console.log(chalk.yellow('\n This file only exists in backup:\n'));
|
|
155
|
+
console.log(chalk.green(backupContent.split('\n').map(l => ' + ' + l).join('\n')));
|
|
156
|
+
} else {
|
|
157
|
+
// Show side by side: local vs backup
|
|
158
|
+
const localContent = await fs.readFile(vf.localPath, 'utf8');
|
|
159
|
+
const localLines = localContent.split('\n');
|
|
160
|
+
const backupLines = backupContent.split('\n');
|
|
161
|
+
|
|
162
|
+
console.log(chalk.gray('\n ── Local (this machine) ──'));
|
|
163
|
+
console.log(localLines.map(l => ' ' + l).join('\n'));
|
|
164
|
+
|
|
165
|
+
console.log(chalk.cyan('\n ── Backup (remote) ──'));
|
|
166
|
+
console.log(backupLines.map(l => ' ' + l).join('\n'));
|
|
167
|
+
}
|
|
168
|
+
console.log('');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
} finally {
|
|
174
|
+
await fs.remove(stagingDir);
|
|
175
|
+
}
|
|
176
|
+
}
|