memoir-cli 1.4.5 → 1.5.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/CLAUDE.md +1 -0
- package/TODO.md +39 -0
- package/bin/memoir.js +16 -0
- package/package.json +2 -2
- package/src/adapters/index.js +65 -12
- package/src/adapters/restore.js +95 -21
- package/src/commands/init.js +13 -28
- package/src/commands/push.js +23 -1
- package/src/commands/restore.js +9 -5
package/CLAUDE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Old Claude Instructions
|
package/TODO.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# memoir Roadmap & TODO
|
|
2
|
+
|
|
3
|
+
This list tracks planned features, identified bugs, and architectural improvements for the `memoir` CLI.
|
|
4
|
+
|
|
5
|
+
## 🔴 High Priority: Security, Reliability & Bug Fixes
|
|
6
|
+
- [ ] **`memoir doctor` Command:** Implement a diagnostic utility to verify:
|
|
7
|
+
- [ ] Correct installation of supported AI tools (Claude, Cursor, etc.).
|
|
8
|
+
- [ ] File permissions for all memory directories.
|
|
9
|
+
- [ ] Git connectivity and API key validity (Gemini).
|
|
10
|
+
- [ ] Environment variable health.
|
|
11
|
+
- [ ] **Secret & PII Guard:** Add a pre-push scan to detect API keys or PII in memory files. Implement a `redact` flag.
|
|
12
|
+
- [ ] **Linux Path Support:** Add path detection for Cursor and Windsurf on Linux (currently macOS/Windows only).
|
|
13
|
+
- [ ] **Aider Local Discovery:** Update the Aider adapter to look for `.aider/` in the current project repo, not just global config.
|
|
14
|
+
- [ ] **Robust Claude Pathing:** Replace fragile string-replacement in `src/tools/claude.js` with more reliable hashing/matching for `~/.claude/projects`.
|
|
15
|
+
- [ ] **Add `-y` / `--yes` Flags:** Enable non-interactive mode for `push`, `restore`, and `migrate` to support automation.
|
|
16
|
+
|
|
17
|
+
## 🟡 Medium Priority: UX & Workflow
|
|
18
|
+
- [ ] **Local LLM Migration (Privacy):**
|
|
19
|
+
- [ ] Add support for **Ollama** and **LM Studio** as translation engines in `migrate`.
|
|
20
|
+
- [ ] Add a `--local` flag to `migrate` to bypass external APIs.
|
|
21
|
+
- [ ] **`memoir watch` (Background Sync):** Create a lightweight daemon to detect changes in local memory files and trigger auto-sync.
|
|
22
|
+
- [ ] **Project Bootstrapping (`memoir init --template`):** Seed new projects with "Golden Rule" templates (e.g., "Strict TypeScript," "React/Tailwind Best Practices").
|
|
23
|
+
- [ ] **Interactive Merge/Diff:** Replace "Overwrite/Append" in `migrate` with a side-by-side diff view.
|
|
24
|
+
- [ ] **Silent Mode:** Add a `--silent` flag to suppress all output except errors.
|
|
25
|
+
|
|
26
|
+
## 🟢 Low Priority: Intelligence & Advanced Features
|
|
27
|
+
- [ ] **Unified Memory Format (UMF):** Architect an internal JSON schema to represent "Coding Context" to simplify adding new tool adapters.
|
|
28
|
+
- [ ] **Cross-Tool Search:** Implement `memoir search <query>` to find specific instructions across all tool backups.
|
|
29
|
+
- [ ] **Context Compression:** AI-powered `memoir optimize` command to summarize long instruction files and save tokens.
|
|
30
|
+
- [ ] **Organization Sync:** Support for shared "Team Memories" stored in a central repository.
|
|
31
|
+
- [ ] **Memory Analytics:** `memoir stats` to visualize the growth and "personality" of your AI instructions over time.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 📝 Technical Observations & Bug Log
|
|
36
|
+
* **Circular Dependency:** `package.json` lists `memoir-cli` as its own dependency. Needs cleanup.
|
|
37
|
+
* **Git Performance:** `push` currently clones the entire repo every time. Optimize with `git clone --depth 1` or local cache.
|
|
38
|
+
* **Performance:** Refactor `fs.readFileSync` inside loops (e.g., in `src/tools/claude.js`) to use `fs.promises` for better handling of large projects.
|
|
39
|
+
* **CLI Friction:** The `migrate` command's interactive prompt for multiple files can be tedious; batching confirmations would improve UX.
|
package/bin/memoir.js
CHANGED
|
@@ -14,6 +14,22 @@ import { createRequire } from 'module';
|
|
|
14
14
|
const require = createRequire(import.meta.url);
|
|
15
15
|
const { version: VERSION } = require('../package.json');
|
|
16
16
|
|
|
17
|
+
// Show quick start when run with no args
|
|
18
|
+
if (process.argv.length <= 2) {
|
|
19
|
+
console.log('\n' + boxen(
|
|
20
|
+
gradient.pastel.multiline(' memoir ') + '\n' +
|
|
21
|
+
chalk.gray(' Your AI remembers everything.') + '\n\n' +
|
|
22
|
+
chalk.white.bold('Quick Start:') + '\n' +
|
|
23
|
+
chalk.cyan(' memoir init ') + chalk.gray('— first-time setup') + '\n' +
|
|
24
|
+
chalk.cyan(' memoir push ') + chalk.gray('— back up your AI memory') + '\n' +
|
|
25
|
+
chalk.cyan(' memoir restore ') + chalk.gray('— restore on a new machine') + '\n' +
|
|
26
|
+
chalk.cyan(' memoir status ') + chalk.gray('— see detected AI tools') + '\n\n' +
|
|
27
|
+
chalk.gray(`v${VERSION}`),
|
|
28
|
+
{ padding: 1, borderStyle: 'round', borderColor: 'cyan', dimBorder: true }
|
|
29
|
+
) + '\n');
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
|
|
17
33
|
// Custom help banner
|
|
18
34
|
program.addHelpText('beforeAll', '\n' + boxen(
|
|
19
35
|
gradient.pastel.multiline(' memoir ') + '\n' +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memoir-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "Sync and translate AI memory across devices and tools. Back up Claude, Gemini, Codex, Cursor, Copilot, Windsurf, and Aider configs. Migrate instructions between AI coding assistants with one command.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"fs-extra": "^11.2.0",
|
|
56
56
|
"gradient-string": "^3.0.0",
|
|
57
57
|
"inquirer": "^9.2.15",
|
|
58
|
-
|
|
58
|
+
|
|
59
59
|
"open": "^11.0.0",
|
|
60
60
|
"ora": "^7.0.1"
|
|
61
61
|
}
|
package/src/adapters/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
|
+
import nodeFs from 'node:fs';
|
|
2
3
|
import path from 'path';
|
|
3
4
|
import os from 'os';
|
|
4
5
|
import chalk from 'chalk';
|
|
@@ -14,18 +15,42 @@ export const adapters = [
|
|
|
14
15
|
icon: '🔵',
|
|
15
16
|
source: path.join(home, '.gemini'),
|
|
16
17
|
filter: (src) => {
|
|
18
|
+
const geminiDir = path.join(home, '.gemini');
|
|
19
|
+
const rel = path.relative(geminiDir, src);
|
|
20
|
+
if (src === geminiDir) return true;
|
|
21
|
+
// Only sync config/settings files — skip caches, history, auth, sandbox, etc.
|
|
22
|
+
const allowed = ['settings.json', 'projects.json', 'state.json', 'installation_id', 'trustedFolders.json', '.gitignore', 'GEMINI.md'];
|
|
17
23
|
const basename = path.basename(src);
|
|
18
|
-
|
|
19
|
-
return !ignored.includes(basename);
|
|
24
|
+
return allowed.includes(basename) && !rel.includes(path.sep);
|
|
20
25
|
}
|
|
21
26
|
},
|
|
22
27
|
{
|
|
23
28
|
name: 'Claude CLI',
|
|
24
29
|
icon: '🟣',
|
|
25
30
|
source: path.join(home, '.claude'),
|
|
26
|
-
filter: (src) => {
|
|
31
|
+
filter: (src, dest) => {
|
|
27
32
|
const basename = path.basename(src);
|
|
28
|
-
|
|
33
|
+
const claudeDir = path.join(home, '.claude');
|
|
34
|
+
const rel = path.relative(claudeDir, src);
|
|
35
|
+
// Root dir itself
|
|
36
|
+
if (src === claudeDir) return true;
|
|
37
|
+
// Only allow these top-level dirs
|
|
38
|
+
const topDir = rel.split(path.sep)[0];
|
|
39
|
+
const allowedDirs = ['projects', 'settings'];
|
|
40
|
+
const allowedFiles = ['settings.json', 'settings.local.json'];
|
|
41
|
+
// Allow specific top-level config files
|
|
42
|
+
if (!rel.includes(path.sep) && allowedFiles.includes(basename)) return true;
|
|
43
|
+
// Allow projects dir (contains memory .md files)
|
|
44
|
+
if (topDir === 'projects') {
|
|
45
|
+
// Allow directory traversal
|
|
46
|
+
try { if (nodeFs.statSync(src).isDirectory()) return true; } catch {}
|
|
47
|
+
// Only sync memory markdown files
|
|
48
|
+
return basename.endsWith('.md');
|
|
49
|
+
}
|
|
50
|
+
// Allow settings dir
|
|
51
|
+
if (topDir === 'settings') return true;
|
|
52
|
+
// Block everything else
|
|
53
|
+
return false;
|
|
29
54
|
}
|
|
30
55
|
},
|
|
31
56
|
{
|
|
@@ -33,9 +58,13 @@ export const adapters = [
|
|
|
33
58
|
icon: '🟢',
|
|
34
59
|
source: path.join(home, '.codex'),
|
|
35
60
|
filter: (src) => {
|
|
61
|
+
const codexDir = path.join(home, '.codex');
|
|
62
|
+
const rel = path.relative(codexDir, src);
|
|
63
|
+
if (src === codexDir) return true;
|
|
36
64
|
const basename = path.basename(src);
|
|
37
|
-
|
|
38
|
-
|
|
65
|
+
// Only sync config files
|
|
66
|
+
const allowed = ['config.json', 'settings.json', 'instructions.md'];
|
|
67
|
+
return allowed.includes(basename) && !rel.includes(path.sep);
|
|
39
68
|
}
|
|
40
69
|
},
|
|
41
70
|
{
|
|
@@ -45,9 +74,19 @@ export const adapters = [
|
|
|
45
74
|
? path.join(appData, 'Cursor', 'User')
|
|
46
75
|
: path.join(home, 'Library', 'Application Support', 'Cursor', 'User'),
|
|
47
76
|
filter: (src) => {
|
|
77
|
+
const cursorDir = isWin
|
|
78
|
+
? path.join(appData, 'Cursor', 'User')
|
|
79
|
+
: path.join(home, 'Library', 'Application Support', 'Cursor', 'User');
|
|
80
|
+
const rel = path.relative(cursorDir, src);
|
|
81
|
+
if (src === cursorDir) return true;
|
|
48
82
|
const basename = path.basename(src);
|
|
49
|
-
|
|
50
|
-
|
|
83
|
+
// Only sync settings and keybindings — not extensions, cache, storage
|
|
84
|
+
const allowed = ['settings.json', 'keybindings.json', 'rules'];
|
|
85
|
+
const topDir = rel.split(path.sep)[0];
|
|
86
|
+
if (allowed.includes(basename) && !rel.includes(path.sep)) return true;
|
|
87
|
+
// Allow rules directory (cursor rules)
|
|
88
|
+
if (topDir === 'rules') return true;
|
|
89
|
+
return false;
|
|
51
90
|
}
|
|
52
91
|
},
|
|
53
92
|
{
|
|
@@ -57,9 +96,14 @@ export const adapters = [
|
|
|
57
96
|
? path.join(appData, 'GitHub Copilot')
|
|
58
97
|
: path.join(home, '.config', 'github-copilot'),
|
|
59
98
|
filter: (src) => {
|
|
99
|
+
const copilotDir = isWin
|
|
100
|
+
? path.join(appData, 'GitHub Copilot')
|
|
101
|
+
: path.join(home, '.config', 'github-copilot');
|
|
102
|
+
if (src === copilotDir) return true;
|
|
60
103
|
const basename = path.basename(src);
|
|
61
|
-
|
|
62
|
-
|
|
104
|
+
// Only sync config — skip auth tokens and version files
|
|
105
|
+
const allowed = ['settings.json', 'config.json'];
|
|
106
|
+
return allowed.includes(basename);
|
|
63
107
|
}
|
|
64
108
|
},
|
|
65
109
|
{
|
|
@@ -69,9 +113,18 @@ export const adapters = [
|
|
|
69
113
|
? path.join(appData, 'Windsurf', 'User')
|
|
70
114
|
: path.join(home, 'Library', 'Application Support', 'Windsurf', 'User'),
|
|
71
115
|
filter: (src) => {
|
|
116
|
+
const windsurfDir = isWin
|
|
117
|
+
? path.join(appData, 'Windsurf', 'User')
|
|
118
|
+
: path.join(home, 'Library', 'Application Support', 'Windsurf', 'User');
|
|
119
|
+
const rel = path.relative(windsurfDir, src);
|
|
120
|
+
if (src === windsurfDir) return true;
|
|
72
121
|
const basename = path.basename(src);
|
|
73
|
-
|
|
74
|
-
|
|
122
|
+
// Only sync settings and keybindings
|
|
123
|
+
const allowed = ['settings.json', 'keybindings.json', 'rules'];
|
|
124
|
+
const topDir = rel.split(path.sep)[0];
|
|
125
|
+
if (allowed.includes(basename) && !rel.includes(path.sep)) return true;
|
|
126
|
+
if (topDir === 'rules') return true;
|
|
127
|
+
return false;
|
|
75
128
|
}
|
|
76
129
|
},
|
|
77
130
|
{
|
package/src/adapters/restore.js
CHANGED
|
@@ -5,7 +5,30 @@ import os from 'os';
|
|
|
5
5
|
import inquirer from 'inquirer';
|
|
6
6
|
import { adapters } from '../adapters/index.js';
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
// Claude CLI stores projects under paths like `projects/-Users-camarthur/`
|
|
9
|
+
// This converts the path from the backup machine to match the current machine
|
|
10
|
+
function remapProjectPath(backupDir, adapterSource) {
|
|
11
|
+
const projectsDir = path.join(backupDir, 'projects');
|
|
12
|
+
if (!fs.existsSync(projectsDir)) return null;
|
|
13
|
+
|
|
14
|
+
const entries = fs.readdirSync(projectsDir);
|
|
15
|
+
// Find the backed-up home dir key (e.g., "-Users-camarthur")
|
|
16
|
+
const oldHomeKey = entries.find(e => {
|
|
17
|
+
return fs.statSync(path.join(projectsDir, e)).isDirectory();
|
|
18
|
+
});
|
|
19
|
+
if (!oldHomeKey) return null;
|
|
20
|
+
|
|
21
|
+
// Build the current machine's home dir key
|
|
22
|
+
// Claude uses the homedir path with / replaced by - and leading -
|
|
23
|
+
const home = os.homedir();
|
|
24
|
+
const newHomeKey = '-' + home.replace(/^\//, '').replace(/\\/g, '-').replace(/\//g, '-').replace(/:/g, '');
|
|
25
|
+
|
|
26
|
+
if (oldHomeKey === newHomeKey) return null; // Same machine, no remap needed
|
|
27
|
+
|
|
28
|
+
return { oldHomeKey, newHomeKey };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function syncFiles(src, dest, changes) {
|
|
9
32
|
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
10
33
|
for (const entry of entries) {
|
|
11
34
|
const srcPath = path.join(src, entry.name);
|
|
@@ -13,10 +36,18 @@ async function copyMissing(src, dest, changes) {
|
|
|
13
36
|
|
|
14
37
|
if (entry.isDirectory()) {
|
|
15
38
|
await fs.ensureDir(destPath);
|
|
16
|
-
await
|
|
39
|
+
await syncFiles(srcPath, destPath, changes);
|
|
17
40
|
} else {
|
|
18
41
|
if (await fs.pathExists(destPath)) {
|
|
19
|
-
|
|
42
|
+
// Compare modification times — update if backup is newer
|
|
43
|
+
const srcStat = await fs.stat(srcPath);
|
|
44
|
+
const destStat = await fs.stat(destPath);
|
|
45
|
+
if (srcStat.mtimeMs > destStat.mtimeMs) {
|
|
46
|
+
await fs.copy(srcPath, destPath);
|
|
47
|
+
changes.updated.push(destPath);
|
|
48
|
+
} else {
|
|
49
|
+
changes.skipped.push(destPath);
|
|
50
|
+
}
|
|
20
51
|
} else {
|
|
21
52
|
await fs.copy(srcPath, destPath);
|
|
22
53
|
changes.added.push(destPath);
|
|
@@ -27,6 +58,7 @@ async function copyMissing(src, dest, changes) {
|
|
|
27
58
|
|
|
28
59
|
export async function restoreMemories(sourceDir, spinner) {
|
|
29
60
|
let restoredAny = false;
|
|
61
|
+
const allResults = [];
|
|
30
62
|
|
|
31
63
|
for (const adapter of adapters) {
|
|
32
64
|
const backupDir = path.join(sourceDir, adapter.name.toLowerCase().replace(/ /g, '-'));
|
|
@@ -34,61 +66,103 @@ export async function restoreMemories(sourceDir, spinner) {
|
|
|
34
66
|
if (await fs.pathExists(backupDir)) {
|
|
35
67
|
spinner.stop();
|
|
36
68
|
|
|
37
|
-
console.log('\n' + chalk.
|
|
69
|
+
console.log('\n' + chalk.cyan(`${adapter.icon} Found backup for ${chalk.bold(adapter.name)}`));
|
|
70
|
+
console.log(chalk.gray(` Will restore to: ${adapter.source}`));
|
|
38
71
|
const { confirm } = await inquirer.prompt([
|
|
39
72
|
{
|
|
40
73
|
type: 'confirm',
|
|
41
74
|
name: 'confirm',
|
|
42
|
-
message: `Restore ${adapter.name}
|
|
43
|
-
default:
|
|
75
|
+
message: `Restore ${adapter.name}?`,
|
|
76
|
+
default: true
|
|
44
77
|
}
|
|
45
78
|
]);
|
|
46
79
|
|
|
47
80
|
spinner.start();
|
|
48
81
|
|
|
49
82
|
if (confirm) {
|
|
50
|
-
const changes = { added: [], skipped: [] };
|
|
83
|
+
const changes = { added: [], updated: [], skipped: [] };
|
|
84
|
+
|
|
85
|
+
// Remap Claude project paths from source machine to this machine
|
|
86
|
+
if (adapter.name === 'Claude CLI') {
|
|
87
|
+
const remap = remapProjectPath(backupDir, adapter.source);
|
|
88
|
+
if (remap) {
|
|
89
|
+
spinner.stop();
|
|
90
|
+
console.log(chalk.gray(` Remapping project path: ${remap.oldHomeKey} → ${remap.newHomeKey}`));
|
|
91
|
+
spinner.start();
|
|
92
|
+
// Rename the directory in staging so it restores to the right place
|
|
93
|
+
const oldDir = path.join(backupDir, 'projects', remap.oldHomeKey);
|
|
94
|
+
const newDir = path.join(backupDir, 'projects', remap.newHomeKey);
|
|
95
|
+
if (await fs.pathExists(oldDir) && !(await fs.pathExists(newDir))) {
|
|
96
|
+
await fs.move(oldDir, newDir);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
51
100
|
|
|
52
101
|
if (adapter.customExtract) {
|
|
53
102
|
const files = await fs.readdir(backupDir);
|
|
54
103
|
for (const file of files) {
|
|
55
|
-
const
|
|
56
|
-
if (await fs.pathExists(
|
|
57
|
-
|
|
104
|
+
const destFile = path.join(adapter.source, file);
|
|
105
|
+
if (await fs.pathExists(destFile)) {
|
|
106
|
+
const srcStat = await fs.stat(path.join(backupDir, file));
|
|
107
|
+
const destStat = await fs.stat(destFile);
|
|
108
|
+
if (srcStat.mtimeMs > destStat.mtimeMs) {
|
|
109
|
+
await fs.copy(path.join(backupDir, file), destFile);
|
|
110
|
+
changes.updated.push(destFile);
|
|
111
|
+
} else {
|
|
112
|
+
changes.skipped.push(destFile);
|
|
113
|
+
}
|
|
58
114
|
} else {
|
|
59
|
-
await fs.copy(path.join(backupDir, file),
|
|
60
|
-
changes.added.push(
|
|
115
|
+
await fs.copy(path.join(backupDir, file), destFile);
|
|
116
|
+
changes.added.push(destFile);
|
|
61
117
|
}
|
|
62
118
|
}
|
|
63
119
|
} else {
|
|
64
|
-
spinner.text = `Restoring ${chalk.cyan(adapter.name)}
|
|
120
|
+
spinner.text = `Restoring ${chalk.cyan(adapter.name)} to ${adapter.source}...`;
|
|
65
121
|
await fs.ensureDir(adapter.source);
|
|
66
|
-
await
|
|
122
|
+
await syncFiles(backupDir, adapter.source, changes);
|
|
67
123
|
}
|
|
68
124
|
|
|
69
125
|
// Show summary of changes
|
|
70
126
|
spinner.stop();
|
|
71
|
-
|
|
72
|
-
|
|
127
|
+
const totalChanged = changes.added.length + changes.updated.length;
|
|
128
|
+
if (totalChanged > 0) {
|
|
129
|
+
console.log(chalk.green.bold(`\n ${adapter.icon} ${adapter.name} — ${totalChanged} file(s) restored to ${chalk.underline(adapter.source)}`));
|
|
73
130
|
for (const f of changes.added) {
|
|
74
|
-
console.log(chalk.green(` + ${f}`));
|
|
131
|
+
console.log(chalk.green(` + ${path.basename(f)}`) + chalk.gray(` (new)`));
|
|
132
|
+
}
|
|
133
|
+
for (const f of changes.updated) {
|
|
134
|
+
console.log(chalk.yellow(` ↻ ${path.basename(f)}`) + chalk.gray(` (updated)`));
|
|
75
135
|
}
|
|
76
136
|
}
|
|
77
137
|
if (changes.skipped.length > 0) {
|
|
78
|
-
console.log(chalk.gray(` ⏭ ${changes.skipped.length} file(s) already
|
|
138
|
+
console.log(chalk.gray(` ⏭ ${changes.skipped.length} file(s) already up to date`));
|
|
79
139
|
}
|
|
80
|
-
if (
|
|
81
|
-
console.log(chalk.gray(`
|
|
140
|
+
if (totalChanged === 0) {
|
|
141
|
+
console.log(chalk.gray(` ✔ ${adapter.name} — already up to date`));
|
|
82
142
|
}
|
|
83
143
|
spinner.start();
|
|
84
144
|
|
|
145
|
+
allResults.push({ name: adapter.name, icon: adapter.icon, dest: adapter.source, added: changes.added.length, updated: changes.updated.length });
|
|
85
146
|
restoredAny = true;
|
|
86
147
|
} else {
|
|
87
|
-
spinner.info(chalk.gray(`Skipped
|
|
148
|
+
spinner.info(chalk.gray(`Skipped ${adapter.name}.`));
|
|
88
149
|
spinner.start();
|
|
89
150
|
}
|
|
90
151
|
}
|
|
91
152
|
}
|
|
92
153
|
|
|
154
|
+
// Final recap
|
|
155
|
+
if (allResults.length > 0) {
|
|
156
|
+
spinner.stop();
|
|
157
|
+
console.log('\n' + chalk.gray('─'.repeat(40)));
|
|
158
|
+
console.log(chalk.bold.white('\n Restore Summary:\n'));
|
|
159
|
+
for (const r of allResults) {
|
|
160
|
+
const count = r.added + r.updated;
|
|
161
|
+
console.log(` ${r.icon} ${chalk.white(r.name)}`);
|
|
162
|
+
console.log(chalk.gray(` ${count} file(s) → ${r.dest}`));
|
|
163
|
+
}
|
|
164
|
+
console.log('');
|
|
165
|
+
}
|
|
166
|
+
|
|
93
167
|
return restoredAny;
|
|
94
168
|
}
|
package/src/commands/init.js
CHANGED
|
@@ -67,38 +67,23 @@ export async function initCommand() {
|
|
|
67
67
|
}]);
|
|
68
68
|
config.localPath = localPath;
|
|
69
69
|
} else {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
70
|
+
const answers = await inquirer.prompt([
|
|
71
|
+
{
|
|
72
|
+
type: 'input',
|
|
73
|
+
name: 'username',
|
|
74
|
+
message: 'GitHub username:',
|
|
75
|
+
default: detectedUser || undefined,
|
|
76
|
+
validate: (input) => input.trim() ? true : 'Required'
|
|
77
|
+
},
|
|
78
|
+
{
|
|
76
79
|
type: 'input',
|
|
77
80
|
name: 'repo',
|
|
78
|
-
message:
|
|
81
|
+
message: 'Repo name:',
|
|
79
82
|
default: 'ai-memory',
|
|
80
83
|
validate: (input) => input.trim() ? true : 'Required'
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
{
|
|
85
|
-
type: 'input',
|
|
86
|
-
name: 'username',
|
|
87
|
-
message: 'GitHub username:',
|
|
88
|
-
validate: (input) => input.trim() ? true : 'Required'
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
type: 'input',
|
|
92
|
-
name: 'repo',
|
|
93
|
-
message: 'Repo name:',
|
|
94
|
-
default: 'ai-memory',
|
|
95
|
-
validate: (input) => input.trim() ? true : 'Required'
|
|
96
|
-
}
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const answers = await inquirer.prompt(prompts);
|
|
101
|
-
const username = (answers.username || detectedUser).trim();
|
|
84
|
+
}
|
|
85
|
+
]);
|
|
86
|
+
const username = answers.username.trim();
|
|
102
87
|
const repo = answers.repo.trim();
|
|
103
88
|
|
|
104
89
|
config.gitRepo = `https://github.com/${username}/${repo}.git`;
|
package/src/commands/push.js
CHANGED
|
@@ -68,12 +68,34 @@ export async function pushCommand() {
|
|
|
68
68
|
|
|
69
69
|
spinner.stop();
|
|
70
70
|
|
|
71
|
+
// Count total files
|
|
72
|
+
let totalFiles = 0;
|
|
73
|
+
for (const adapter of adapters) {
|
|
74
|
+
const adapterDir = path.join(stagingDir, adapter.name.toLowerCase().replace(/ /g, '-'));
|
|
75
|
+
if (await fs.pathExists(adapterDir)) {
|
|
76
|
+
const countDir = async (dir) => {
|
|
77
|
+
let c = 0;
|
|
78
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
79
|
+
for (const e of entries) {
|
|
80
|
+
if (e.isDirectory()) c += await countDir(path.join(dir, e.name));
|
|
81
|
+
else c++;
|
|
82
|
+
}
|
|
83
|
+
return c;
|
|
84
|
+
};
|
|
85
|
+
totalFiles += await countDir(adapterDir);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const dest = config.provider === 'git' ? config.gitRepo : config.localPath;
|
|
90
|
+
|
|
71
91
|
// Success output
|
|
72
92
|
const toolList = found.map(t => chalk.cyan(' ✔ ' + t)).join('\n');
|
|
73
93
|
console.log('\n' + boxen(
|
|
74
94
|
gradient.pastel(' Backed up! ') + '\n\n' +
|
|
75
95
|
toolList + '\n\n' +
|
|
76
|
-
chalk.
|
|
96
|
+
chalk.white(`${totalFiles} files from ${found.length} tool${found.length !== 1 ? 's' : ''}`) + '\n' +
|
|
97
|
+
chalk.gray(`→ ${dest}`) + '\n\n' +
|
|
98
|
+
chalk.gray('Restore on another machine with: ') + chalk.cyan('memoir restore'),
|
|
77
99
|
{ padding: 1, borderStyle: 'round', borderColor: 'green', dimBorder: true }
|
|
78
100
|
) + '\n');
|
|
79
101
|
} catch (error) {
|
package/src/commands/restore.js
CHANGED
|
@@ -41,16 +41,20 @@ export async function restoreCommand() {
|
|
|
41
41
|
spinner.stop();
|
|
42
42
|
|
|
43
43
|
if (restored) {
|
|
44
|
-
console.log(
|
|
45
|
-
gradient.pastel('
|
|
44
|
+
console.log(boxen(
|
|
45
|
+
gradient.pastel(' Done! ') + '\n\n' +
|
|
46
46
|
chalk.white('Your AI tools have their memories back.') + '\n' +
|
|
47
|
-
chalk.gray('
|
|
47
|
+
chalk.gray('Restart your AI tools to pick up the changes.'),
|
|
48
48
|
{ padding: 1, borderStyle: 'round', borderColor: 'green', dimBorder: true }
|
|
49
49
|
) + '\n');
|
|
50
50
|
} else {
|
|
51
51
|
console.log('\n' + boxen(
|
|
52
|
-
chalk.yellow('
|
|
53
|
-
chalk.
|
|
52
|
+
chalk.yellow('Nothing was restored.\n\n') +
|
|
53
|
+
chalk.white('This can happen if:\n') +
|
|
54
|
+
chalk.gray(' 1. You haven\'t run ') + chalk.cyan('memoir push') + chalk.gray(' on another machine yet\n') +
|
|
55
|
+
chalk.gray(' 2. You skipped all the restore prompts\n') +
|
|
56
|
+
chalk.gray(' 3. The backup repo is empty\n\n') +
|
|
57
|
+
chalk.gray('Try: ') + chalk.cyan('memoir view') + chalk.gray(' to see what\'s in your backup'),
|
|
54
58
|
{ padding: 1, borderStyle: 'round', borderColor: 'yellow' }
|
|
55
59
|
) + '\n');
|
|
56
60
|
}
|