memoir-cli 1.3.0 → 1.4.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/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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memoir-cli",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "description": "Your AI remembers everything. Sync it everywhere.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -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, spinner) {
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, spinner);
16
+ await copyMissing(srcPath, destPath, changes);
17
17
  } else {
18
18
  if (await fs.pathExists(destPath)) {
19
- // skip existing files
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('\\n' + chalk.yellow(`⚠ Found backup for ${chalk.bold(adapter.name)}.`));
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
- spinner.info(chalk.gray(` Skipped ${file} (already exists)`));
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
- // Merge only copy files that don't already exist
65
- await copyMissing(backupDir, adapter.source, spinner);
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 { getConfig } 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 getConfig();
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
+ }