memoir-cli 2.1.0 → 2.2.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 +65 -3
- package/package.json +1 -1
- package/src/adapters/index.js +74 -0
- package/src/adapters/restore.js +119 -24
- package/src/commands/doctor.js +223 -0
- package/src/commands/init.js +19 -1
- package/src/commands/status.js +17 -9
- package/src/tools/cline.js +72 -0
- package/src/tools/continuedev.js +55 -0
- package/src/tools/index.js +4 -1
- package/src/tools/zed.js +50 -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 { doctorCommand } from '../src/commands/doctor.js';
|
|
10
11
|
import { viewCommand } from '../src/commands/view.js';
|
|
11
12
|
import { diffCommand } from '../src/commands/diff.js';
|
|
12
13
|
import { migrateCommand } from '../src/commands/migrate.js';
|
|
@@ -26,11 +27,17 @@ async function checkForUpdate() {
|
|
|
26
27
|
clearTimeout(timeout);
|
|
27
28
|
const data = await res.json();
|
|
28
29
|
const latest = data.version;
|
|
29
|
-
if
|
|
30
|
+
// Only notify if remote is actually newer (not just different)
|
|
31
|
+
const isNewer = (a, b) => {
|
|
32
|
+
const [a1, a2, a3] = a.split('.').map(Number);
|
|
33
|
+
const [b1, b2, b3] = b.split('.').map(Number);
|
|
34
|
+
return a1 > b1 || (a1 === b1 && a2 > b2) || (a1 === b1 && a2 === b2 && a3 > b3);
|
|
35
|
+
};
|
|
36
|
+
if (latest && isNewer(latest, VERSION)) {
|
|
30
37
|
console.log(
|
|
31
38
|
'\n' + boxen(
|
|
32
39
|
chalk.yellow(`Update available: ${VERSION} → ${chalk.green.bold(latest)}`) + '\n' +
|
|
33
|
-
chalk.gray('Run: ') + chalk.cyan('
|
|
40
|
+
chalk.gray('Run: ') + chalk.cyan('memoir update'),
|
|
34
41
|
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderStyle: 'round', borderColor: 'yellow', dimBorder: true }
|
|
35
42
|
)
|
|
36
43
|
);
|
|
@@ -49,7 +56,8 @@ if (process.argv.length <= 2) {
|
|
|
49
56
|
chalk.cyan(' memoir restore ') + chalk.gray('— restore on a new machine') + '\n' +
|
|
50
57
|
chalk.cyan(' memoir snapshot ') + chalk.gray('— capture your current session') + '\n' +
|
|
51
58
|
chalk.cyan(' memoir resume ') + chalk.gray('— pick up where you left off') + '\n' +
|
|
52
|
-
chalk.cyan(' memoir status ') + chalk.gray('— see detected AI tools') + '\n
|
|
59
|
+
chalk.cyan(' memoir status ') + chalk.gray('— see detected AI tools') + '\n' +
|
|
60
|
+
chalk.cyan(' memoir update ') + chalk.gray('— update to latest version') + '\n\n' +
|
|
53
61
|
chalk.gray(' Tip: use --only claude,gemini to sync specific tools') + '\n\n' +
|
|
54
62
|
chalk.gray(`v${VERSION}`),
|
|
55
63
|
{ padding: 1, borderStyle: 'round', borderColor: 'cyan', dimBorder: true }
|
|
@@ -122,6 +130,19 @@ program
|
|
|
122
130
|
}
|
|
123
131
|
});
|
|
124
132
|
|
|
133
|
+
program
|
|
134
|
+
.command('doctor')
|
|
135
|
+
.alias('diagnose')
|
|
136
|
+
.description('Diagnose common issues with your memoir setup')
|
|
137
|
+
.action(async () => {
|
|
138
|
+
try {
|
|
139
|
+
await doctorCommand();
|
|
140
|
+
} catch (err) {
|
|
141
|
+
console.error(chalk.red('\n✖ Error:'), err.message);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
125
146
|
program
|
|
126
147
|
.command('view')
|
|
127
148
|
.alias('ls')
|
|
@@ -177,6 +198,47 @@ program
|
|
|
177
198
|
}
|
|
178
199
|
});
|
|
179
200
|
|
|
201
|
+
program
|
|
202
|
+
.command('update')
|
|
203
|
+
.alias('upgrade')
|
|
204
|
+
.description('Update memoir to the latest version')
|
|
205
|
+
.action(async () => {
|
|
206
|
+
try {
|
|
207
|
+
const res = await fetch('https://registry.npmjs.org/memoir-cli/latest');
|
|
208
|
+
const data = await res.json();
|
|
209
|
+
const latest = data.version;
|
|
210
|
+
|
|
211
|
+
if (latest === VERSION) {
|
|
212
|
+
console.log('\n' + boxen(
|
|
213
|
+
chalk.green('✔ Already up to date!') + '\n' +
|
|
214
|
+
chalk.gray(`v${VERSION}`),
|
|
215
|
+
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderStyle: 'round', borderColor: 'green', dimBorder: true }
|
|
216
|
+
) + '\n');
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
console.log('\n' + chalk.cyan(`Updating memoir ${VERSION} → ${chalk.green.bold(latest)}...`) + '\n');
|
|
221
|
+
|
|
222
|
+
const { execSync } = await import('child_process');
|
|
223
|
+
// Detect package manager — prefer the one that installed memoir
|
|
224
|
+
const execPath = process.argv[1] || '';
|
|
225
|
+
const useBun = execPath.includes('.bun') || process.env.BUN_INSTALL;
|
|
226
|
+
const cmd = useBun ? 'bun install -g memoir-cli' : 'npm install -g memoir-cli';
|
|
227
|
+
|
|
228
|
+
execSync(cmd, { stdio: 'inherit' });
|
|
229
|
+
|
|
230
|
+
console.log('\n' + boxen(
|
|
231
|
+
gradient.pastel(' Updated! ') + '\n\n' +
|
|
232
|
+
chalk.white(`memoir ${VERSION} → ${chalk.green.bold(latest)}`),
|
|
233
|
+
{ padding: 1, borderStyle: 'round', borderColor: 'green', dimBorder: true }
|
|
234
|
+
) + '\n');
|
|
235
|
+
} catch (err) {
|
|
236
|
+
console.error(chalk.red('\n✖ Update failed:'), err.message);
|
|
237
|
+
console.log(chalk.gray('Try manually: ') + chalk.cyan('npm install -g memoir-cli'));
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
180
242
|
program
|
|
181
243
|
.command('migrate')
|
|
182
244
|
.description('Translate memory between AI tools (Claude, Gemini, Codex, Cursor, etc.)')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memoir-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Sync AI memory across devices. Back up and restore Claude, Gemini, Codex, Cursor, Copilot, Windsurf configs. Snapshot coding sessions and resume on another machine. Migrate instructions between AI assistants.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
package/src/adapters/index.js
CHANGED
|
@@ -127,6 +127,80 @@ export const adapters = [
|
|
|
127
127
|
return false;
|
|
128
128
|
}
|
|
129
129
|
},
|
|
130
|
+
{
|
|
131
|
+
name: 'Zed',
|
|
132
|
+
icon: '🔶',
|
|
133
|
+
source: isWin
|
|
134
|
+
? path.join(appData, 'Zed')
|
|
135
|
+
: path.join(home, '.config', 'zed'),
|
|
136
|
+
filter: (src) => {
|
|
137
|
+
const zedDir = isWin
|
|
138
|
+
? path.join(appData, 'Zed')
|
|
139
|
+
: path.join(home, '.config', 'zed');
|
|
140
|
+
const rel = path.relative(zedDir, src);
|
|
141
|
+
if (src === zedDir) return true;
|
|
142
|
+
const basename = path.basename(src);
|
|
143
|
+
// Skip known heavy/non-config directories
|
|
144
|
+
const skipDirs = ['extensions', 'themes', 'logs', 'db', 'copilot', 'node', 'languages'];
|
|
145
|
+
const topDir = rel.split(path.sep)[0];
|
|
146
|
+
if (skipDirs.includes(topDir)) return false;
|
|
147
|
+
// Only sync specific config files in root
|
|
148
|
+
const allowed = ['settings.json', 'keymap.json', 'tasks.json'];
|
|
149
|
+
if (allowed.includes(basename) && !rel.includes(path.sep)) return true;
|
|
150
|
+
// Allow .md files in root
|
|
151
|
+
if (basename.endsWith('.md') && !rel.includes(path.sep)) return true;
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: 'Cline',
|
|
157
|
+
icon: '🤖',
|
|
158
|
+
source: isWin
|
|
159
|
+
? path.join(appData, 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev')
|
|
160
|
+
: path.join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev'),
|
|
161
|
+
filter: (src) => {
|
|
162
|
+
const clineDir = isWin
|
|
163
|
+
? path.join(appData, 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev')
|
|
164
|
+
: path.join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev');
|
|
165
|
+
const rel = path.relative(clineDir, src);
|
|
166
|
+
if (src === clineDir) return true;
|
|
167
|
+
const basename = path.basename(src);
|
|
168
|
+
const topDir = rel.split(path.sep)[0];
|
|
169
|
+
// Skip known heavy/non-config directories
|
|
170
|
+
const skipDirs = ['tasks', 'checkpoints', '.cache', 'images'];
|
|
171
|
+
if (skipDirs.includes(topDir)) return false;
|
|
172
|
+
// Allow settings/ and rules/ directories
|
|
173
|
+
if (topDir === 'settings' || topDir === 'rules') return true;
|
|
174
|
+
// Allow .md files in root
|
|
175
|
+
if (basename.endsWith('.md') && !rel.includes(path.sep)) return true;
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: 'Continue.dev',
|
|
181
|
+
icon: '🔄',
|
|
182
|
+
source: isWin
|
|
183
|
+
? path.join(process.env.USERPROFILE || home, '.continue')
|
|
184
|
+
: path.join(home, '.continue'),
|
|
185
|
+
filter: (src) => {
|
|
186
|
+
const continueDir = isWin
|
|
187
|
+
? path.join(process.env.USERPROFILE || home, '.continue')
|
|
188
|
+
: path.join(home, '.continue');
|
|
189
|
+
const rel = path.relative(continueDir, src);
|
|
190
|
+
if (src === continueDir) return true;
|
|
191
|
+
const basename = path.basename(src);
|
|
192
|
+
// Skip known heavy/non-config directories
|
|
193
|
+
const skipDirs = ['sessions', 'dev_data', 'logs', 'index', 'cache', 'types'];
|
|
194
|
+
const topDir = rel.split(path.sep)[0];
|
|
195
|
+
if (skipDirs.includes(topDir)) return false;
|
|
196
|
+
// Only sync specific config files in root
|
|
197
|
+
const allowed = ['config.json', 'config.ts', 'config.yaml', '.continuerules'];
|
|
198
|
+
if (allowed.includes(basename) && !rel.includes(path.sep)) return true;
|
|
199
|
+
// Allow .md files in root
|
|
200
|
+
if (basename.endsWith('.md') && !rel.includes(path.sep)) return true;
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
},
|
|
130
204
|
{
|
|
131
205
|
name: 'Aider',
|
|
132
206
|
icon: '🔧',
|
package/src/adapters/restore.js
CHANGED
|
@@ -5,27 +5,115 @@ import os from 'os';
|
|
|
5
5
|
import inquirer from 'inquirer';
|
|
6
6
|
import { adapters } from '../adapters/index.js';
|
|
7
7
|
|
|
8
|
+
// Detect the local home key by looking at what Claude has ALREADY created
|
|
9
|
+
// on this machine, rather than trying to compute the encoding ourselves.
|
|
10
|
+
// Claude's path encoding varies across platforms and versions, so detection
|
|
11
|
+
// is the only reliable approach.
|
|
12
|
+
function detectLocalHomeKey(adapterSource) {
|
|
13
|
+
const localProjectsDir = path.join(adapterSource, 'projects');
|
|
14
|
+
if (!fs.existsSync(localProjectsDir)) return null;
|
|
15
|
+
|
|
16
|
+
const entries = fs.readdirSync(localProjectsDir)
|
|
17
|
+
.filter(e => fs.statSync(path.join(localProjectsDir, e)).isDirectory());
|
|
18
|
+
if (entries.length === 0) return null;
|
|
19
|
+
|
|
20
|
+
// Find dirs with a memory/ subfolder that aren't sub-projects of another dir
|
|
21
|
+
const candidates = entries.filter(entry => {
|
|
22
|
+
const hasMemory = fs.existsSync(path.join(localProjectsDir, entry, 'memory'));
|
|
23
|
+
if (!hasMemory) return false;
|
|
24
|
+
// A sub-project dir starts with another dir + '-'
|
|
25
|
+
const isSubProject = entries.some(other =>
|
|
26
|
+
other !== entry && entry.startsWith(other + '-')
|
|
27
|
+
);
|
|
28
|
+
return !isSubProject;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (candidates.length === 1) return candidates[0];
|
|
32
|
+
|
|
33
|
+
if (candidates.length > 1) {
|
|
34
|
+
// Multiple home-key candidates (e.g. encoding changed between Claude versions)
|
|
35
|
+
// Pick the most recently modified one — that's what Claude is actively using
|
|
36
|
+
return candidates.sort((a, b) => {
|
|
37
|
+
const aDir = path.join(localProjectsDir, a, 'memory');
|
|
38
|
+
const bDir = path.join(localProjectsDir, b, 'memory');
|
|
39
|
+
return fs.statSync(bDir).mtimeMs - fs.statSync(aDir).mtimeMs;
|
|
40
|
+
})[0];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// No dir has memory/ — fall back to shortest dir that's a prefix of others
|
|
44
|
+
const prefixDirs = entries.filter(entry =>
|
|
45
|
+
entries.some(other => other !== entry && other.startsWith(entry + '-'))
|
|
46
|
+
).sort((a, b) => a.length - b.length);
|
|
47
|
+
|
|
48
|
+
return prefixDirs[0] || entries[0];
|
|
49
|
+
}
|
|
50
|
+
|
|
8
51
|
// Claude CLI stores projects under paths like `projects/-Users-camarthur/`
|
|
9
|
-
// This
|
|
10
|
-
function
|
|
52
|
+
// This remaps ALL foreign machine dirs to match the current machine.
|
|
53
|
+
function remapProjectPaths(backupDir, adapterSource) {
|
|
11
54
|
const projectsDir = path.join(backupDir, 'projects');
|
|
12
|
-
if (!fs.existsSync(projectsDir)) return
|
|
55
|
+
if (!fs.existsSync(projectsDir)) return [];
|
|
13
56
|
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
57
|
+
const backupEntries = fs.readdirSync(projectsDir)
|
|
58
|
+
.filter(e => fs.statSync(path.join(projectsDir, e)).isDirectory());
|
|
59
|
+
if (backupEntries.length === 0) return [];
|
|
60
|
+
|
|
61
|
+
// Step 1: Detect the local home key from existing Claude dirs
|
|
62
|
+
let localHomeKey = detectLocalHomeKey(adapterSource);
|
|
63
|
+
|
|
64
|
+
// Step 2: Fallback — compute from homedir (only for fresh installs)
|
|
65
|
+
if (!localHomeKey) {
|
|
66
|
+
const home = os.homedir();
|
|
67
|
+
// Use the same encoding Claude uses: path with separators → dashes
|
|
68
|
+
localHomeKey = '-' + home.replace(/^\//, '').replace(/\\/g, '-').replace(/\//g, '-').replace(/:/g, '');
|
|
69
|
+
}
|
|
20
70
|
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
const
|
|
24
|
-
|
|
71
|
+
// Step 3: Identify foreign home keys in the backup
|
|
72
|
+
// A "home key" is a dir that: has memory/, OR is a prefix of other dirs, AND is not a sub-project
|
|
73
|
+
const foreignHomeKeys = new Set();
|
|
74
|
+
|
|
75
|
+
for (const entry of backupEntries) {
|
|
76
|
+
// Skip dirs that already belong to this machine
|
|
77
|
+
if (entry === localHomeKey || entry.startsWith(localHomeKey + '-')) continue;
|
|
78
|
+
|
|
79
|
+
// Is this a sub-project of another backup dir? Then skip — its parent handles it
|
|
80
|
+
const isSubProject = backupEntries.some(other =>
|
|
81
|
+
other !== entry && entry.startsWith(other + '-')
|
|
82
|
+
);
|
|
83
|
+
if (isSubProject) continue;
|
|
84
|
+
|
|
85
|
+
// Has memory/ subfolder = definitely a home key
|
|
86
|
+
const hasMemory = fs.existsSync(path.join(projectsDir, entry, 'memory'));
|
|
87
|
+
// Is a prefix of other dirs = likely a home key
|
|
88
|
+
const isPrefix = backupEntries.some(other =>
|
|
89
|
+
other !== entry && other.startsWith(entry + '-')
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
if (hasMemory || isPrefix) {
|
|
93
|
+
foreignHomeKeys.add(entry);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
25
96
|
|
|
26
|
-
|
|
97
|
+
// Step 4: Build remaps — remap each foreign home key and its sub-projects
|
|
98
|
+
const remaps = [];
|
|
99
|
+
const processed = new Set();
|
|
100
|
+
|
|
101
|
+
for (const foreignKey of foreignHomeKeys) {
|
|
102
|
+
// Find all dirs belonging to this foreign home key
|
|
103
|
+
for (const dir of backupEntries) {
|
|
104
|
+
if (processed.has(dir)) continue;
|
|
105
|
+
if (dir !== foreignKey && !dir.startsWith(foreignKey + '-')) continue;
|
|
106
|
+
|
|
107
|
+
processed.add(dir);
|
|
108
|
+
const suffix = dir.slice(foreignKey.length); // "" or "-alfred" etc.
|
|
109
|
+
const newName = localHomeKey + suffix;
|
|
110
|
+
if (dir !== newName) {
|
|
111
|
+
remaps.push({ oldName: dir, newName });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
27
115
|
|
|
28
|
-
return
|
|
116
|
+
return remaps;
|
|
29
117
|
}
|
|
30
118
|
|
|
31
119
|
async function syncFiles(src, dest, changes) {
|
|
@@ -97,17 +185,24 @@ export async function restoreMemories(sourceDir, spinner, onlyFilter = null, aut
|
|
|
97
185
|
|
|
98
186
|
// Remap Claude project paths from source machine to this machine
|
|
99
187
|
if (adapter.name === 'Claude CLI') {
|
|
100
|
-
const
|
|
101
|
-
if (
|
|
188
|
+
const remaps = remapProjectPaths(backupDir, adapter.source);
|
|
189
|
+
if (remaps.length > 0) {
|
|
102
190
|
spinner.stop();
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
191
|
+
for (const remap of remaps) {
|
|
192
|
+
console.log(chalk.gray(` Remapping: ${remap.oldName} → ${remap.newName}`));
|
|
193
|
+
const oldDir = path.join(backupDir, 'projects', remap.oldName);
|
|
194
|
+
const newDir = path.join(backupDir, 'projects', remap.newName);
|
|
195
|
+
if (await fs.pathExists(oldDir)) {
|
|
196
|
+
if (await fs.pathExists(newDir)) {
|
|
197
|
+
// Merge into existing directory
|
|
198
|
+
await syncFiles(oldDir, newDir, { added: [], updated: [], skipped: [] });
|
|
199
|
+
await fs.remove(oldDir);
|
|
200
|
+
} else {
|
|
201
|
+
await fs.move(oldDir, newDir);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
110
204
|
}
|
|
205
|
+
spinner.start();
|
|
111
206
|
}
|
|
112
207
|
}
|
|
113
208
|
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import boxen from 'boxen';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import gradient from 'gradient-string';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import { execSync } from 'child_process';
|
|
9
|
+
import { getConfig } from '../config.js';
|
|
10
|
+
import { adapters } from '../adapters/index.js';
|
|
11
|
+
|
|
12
|
+
const SECRET_PATTERNS = [
|
|
13
|
+
{ pattern: /sk-[a-zA-Z0-9]{20,}/, label: 'OpenAI/Stripe secret key' },
|
|
14
|
+
{ pattern: /key-[a-zA-Z0-9]{20,}/, label: 'API key' },
|
|
15
|
+
{ pattern: /ghp_[a-zA-Z0-9]{36,}/, label: 'GitHub personal access token' },
|
|
16
|
+
{ pattern: /gho_[a-zA-Z0-9]{36,}/, label: 'GitHub OAuth token' },
|
|
17
|
+
{ pattern: /AKIA[0-9A-Z]{16}/, label: 'AWS access key' },
|
|
18
|
+
{ pattern: /Bearer\s+[a-zA-Z0-9._\-]{20,}/, label: 'Bearer token' },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const SENSITIVE_FILENAMES = ['.env', 'credentials', 'token.json'];
|
|
22
|
+
|
|
23
|
+
function formatSize(bytes) {
|
|
24
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
25
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}kb`;
|
|
26
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}mb`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function collectFiles(dir, filter) {
|
|
30
|
+
const files = [];
|
|
31
|
+
async function walk(d) {
|
|
32
|
+
let entries;
|
|
33
|
+
try {
|
|
34
|
+
entries = await fs.readdir(d, { withFileTypes: true });
|
|
35
|
+
} catch { return; }
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
const fullPath = path.join(d, entry.name);
|
|
38
|
+
if (entry.isDirectory()) {
|
|
39
|
+
if (filter && !filter(fullPath)) continue;
|
|
40
|
+
await walk(fullPath);
|
|
41
|
+
} else {
|
|
42
|
+
if (filter && !filter(fullPath)) continue;
|
|
43
|
+
files.push(fullPath);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
await walk(dir);
|
|
48
|
+
return files;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function scanForSecrets(files) {
|
|
52
|
+
const warnings = [];
|
|
53
|
+
for (const filePath of files) {
|
|
54
|
+
const basename = path.basename(filePath);
|
|
55
|
+
if (SENSITIVE_FILENAMES.includes(basename)) {
|
|
56
|
+
warnings.push({ file: filePath, reason: `Sensitive filename: ${basename}` });
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const stat = await fs.stat(filePath);
|
|
61
|
+
// Skip files larger than 1MB
|
|
62
|
+
if (stat.size > 1024 * 1024) continue;
|
|
63
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
64
|
+
for (const { pattern, label } of SECRET_PATTERNS) {
|
|
65
|
+
if (pattern.test(content)) {
|
|
66
|
+
warnings.push({ file: filePath, reason: label });
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
// Skip unreadable files
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return warnings;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function doctorCommand() {
|
|
78
|
+
const spinner = ora({ text: 'Running diagnostics...', color: 'cyan' }).start();
|
|
79
|
+
const lines = [];
|
|
80
|
+
let passCount = 0;
|
|
81
|
+
let warnCount = 0;
|
|
82
|
+
let failCount = 0;
|
|
83
|
+
|
|
84
|
+
const pass = (msg) => { passCount++; return chalk.green(' ✔ ') + msg; };
|
|
85
|
+
const warn = (msg) => { warnCount++; return chalk.yellow(' ⚠ ') + msg; };
|
|
86
|
+
const fail = (msg) => { failCount++; return chalk.red(' ✖ ') + msg; };
|
|
87
|
+
|
|
88
|
+
// 1. Config check
|
|
89
|
+
spinner.text = 'Checking configuration...';
|
|
90
|
+
const config = await getConfig();
|
|
91
|
+
if (config) {
|
|
92
|
+
const providerLabel = config.provider === 'git' ? 'git' : 'local';
|
|
93
|
+
const dest = config.provider === 'git' ? config.gitRepo : config.localPath;
|
|
94
|
+
lines.push(pass(`Config: ${chalk.cyan(providerLabel)} → ${chalk.gray(dest)}`));
|
|
95
|
+
} else {
|
|
96
|
+
lines.push(fail(`Config: not initialized — run ${chalk.cyan('memoir init')}`));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 2. Git check
|
|
100
|
+
spinner.text = 'Checking git...';
|
|
101
|
+
let gitInstalled = false;
|
|
102
|
+
try {
|
|
103
|
+
execSync('git --version', { stdio: 'pipe' });
|
|
104
|
+
gitInstalled = true;
|
|
105
|
+
lines.push(pass('Git: installed'));
|
|
106
|
+
} catch {
|
|
107
|
+
lines.push(fail('Git: not installed'));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (config?.provider === 'git' && gitInstalled && config.gitRepo) {
|
|
111
|
+
spinner.text = 'Testing remote connectivity...';
|
|
112
|
+
try {
|
|
113
|
+
execSync(`git ls-remote ${config.gitRepo} HEAD`, { stdio: 'pipe', timeout: 10000 });
|
|
114
|
+
lines.push(pass(`Remote: ${chalk.gray(config.gitRepo)} reachable`));
|
|
115
|
+
} catch {
|
|
116
|
+
lines.push(fail(`Remote: cannot reach ${chalk.gray(config.gitRepo)}`));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 3. AI Tools scan
|
|
121
|
+
spinner.text = 'Scanning AI tools...';
|
|
122
|
+
lines.push('');
|
|
123
|
+
lines.push(chalk.bold.white(' AI Tools'));
|
|
124
|
+
|
|
125
|
+
const allSyncFiles = [];
|
|
126
|
+
let totalSize = 0;
|
|
127
|
+
|
|
128
|
+
for (const adapter of adapters) {
|
|
129
|
+
let found = false;
|
|
130
|
+
let fileCount = 0;
|
|
131
|
+
let size = 0;
|
|
132
|
+
let adapterFiles = [];
|
|
133
|
+
|
|
134
|
+
if (adapter.customExtract) {
|
|
135
|
+
for (const file of adapter.files) {
|
|
136
|
+
const filePath = path.join(adapter.source, file);
|
|
137
|
+
if (await fs.pathExists(filePath)) {
|
|
138
|
+
found = true;
|
|
139
|
+
fileCount++;
|
|
140
|
+
try {
|
|
141
|
+
const stat = await fs.stat(filePath);
|
|
142
|
+
size += stat.size;
|
|
143
|
+
adapterFiles.push(filePath);
|
|
144
|
+
} catch {}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} else if (await fs.pathExists(adapter.source)) {
|
|
148
|
+
found = true;
|
|
149
|
+
adapterFiles = await collectFiles(adapter.source, adapter.filter);
|
|
150
|
+
fileCount = adapterFiles.length;
|
|
151
|
+
for (const f of adapterFiles) {
|
|
152
|
+
try {
|
|
153
|
+
const stat = await fs.stat(f);
|
|
154
|
+
size += stat.size;
|
|
155
|
+
} catch {}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (found) {
|
|
160
|
+
lines.push(pass(`${adapter.name}: ${chalk.gray(`${fileCount} files, ${formatSize(size)}`)}`));
|
|
161
|
+
allSyncFiles.push(...adapterFiles);
|
|
162
|
+
totalSize += size;
|
|
163
|
+
} else {
|
|
164
|
+
lines.push(chalk.gray(' ○ ') + chalk.gray(adapter.name + ': not found'));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 4. Secrets scan
|
|
169
|
+
spinner.text = 'Scanning for secrets...';
|
|
170
|
+
lines.push('');
|
|
171
|
+
lines.push(chalk.bold.white(' Security'));
|
|
172
|
+
|
|
173
|
+
const secretWarnings = await scanForSecrets(allSyncFiles);
|
|
174
|
+
if (secretWarnings.length === 0) {
|
|
175
|
+
lines.push(pass('No secrets detected in sync files'));
|
|
176
|
+
} else {
|
|
177
|
+
lines.push(warn(`${secretWarnings.length} potential secret${secretWarnings.length !== 1 ? 's' : ''} found:`));
|
|
178
|
+
for (const w of secretWarnings.slice(0, 5)) {
|
|
179
|
+
lines.push(chalk.yellow(' → ') + chalk.gray(path.basename(w.file)) + chalk.yellow(` (${w.reason})`));
|
|
180
|
+
}
|
|
181
|
+
if (secretWarnings.length > 5) {
|
|
182
|
+
lines.push(chalk.gray(` ...and ${secretWarnings.length - 5} more`));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 5. Disk usage
|
|
187
|
+
lines.push('');
|
|
188
|
+
lines.push(chalk.bold.white(' Disk'));
|
|
189
|
+
lines.push(pass(`Total backup size: ${chalk.cyan(formatSize(totalSize))} across ${chalk.cyan(allSyncFiles.length)} files`));
|
|
190
|
+
|
|
191
|
+
// 6. Last sync
|
|
192
|
+
if (config?.provider === 'git' && gitInstalled && config.gitRepo) {
|
|
193
|
+
spinner.text = 'Checking last sync...';
|
|
194
|
+
lines.push('');
|
|
195
|
+
lines.push(chalk.bold.white(' Last Sync'));
|
|
196
|
+
try {
|
|
197
|
+
const tmpDir = path.join(os.tmpdir(), 'memoir-doctor-' + Date.now());
|
|
198
|
+
execSync(`git clone --depth 1 ${config.gitRepo} ${tmpDir}`, { stdio: 'pipe', timeout: 15000 });
|
|
199
|
+
const lastCommit = execSync('git log -1 --format=%cr', { cwd: tmpDir, stdio: 'pipe' }).toString().trim();
|
|
200
|
+
const lastMsg = execSync('git log -1 --format=%s', { cwd: tmpDir, stdio: 'pipe' }).toString().trim();
|
|
201
|
+
await fs.remove(tmpDir);
|
|
202
|
+
lines.push(pass(`Last backup: ${chalk.cyan(lastCommit)} — ${chalk.gray(lastMsg)}`));
|
|
203
|
+
} catch {
|
|
204
|
+
lines.push(warn('Could not determine last sync time'));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
spinner.stop();
|
|
209
|
+
|
|
210
|
+
// Summary
|
|
211
|
+
const summaryParts = [];
|
|
212
|
+
if (passCount > 0) summaryParts.push(chalk.green(`${passCount} passed`));
|
|
213
|
+
if (warnCount > 0) summaryParts.push(chalk.yellow(`${warnCount} warning${warnCount !== 1 ? 's' : ''}`));
|
|
214
|
+
if (failCount > 0) summaryParts.push(chalk.red(`${failCount} failed`));
|
|
215
|
+
|
|
216
|
+
console.log('\n' + boxen(
|
|
217
|
+
gradient.pastel(' memoir doctor ') + '\n\n' +
|
|
218
|
+
lines.join('\n') + '\n\n' +
|
|
219
|
+
chalk.gray('─'.repeat(36)) + '\n' +
|
|
220
|
+
' ' + summaryParts.join(chalk.gray(' · ')),
|
|
221
|
+
{ padding: 1, borderStyle: 'round', borderColor: failCount > 0 ? 'red' : warnCount > 0 ? 'yellow' : 'green', dimBorder: true }
|
|
222
|
+
) + '\n');
|
|
223
|
+
}
|
package/src/commands/init.js
CHANGED
|
@@ -87,7 +87,25 @@ export async function initCommand() {
|
|
|
87
87
|
const repo = answers.repo.trim();
|
|
88
88
|
|
|
89
89
|
config.gitRepo = `https://github.com/${username}/${repo}.git`;
|
|
90
|
-
console.log(chalk.gray(` → ${config.gitRepo}
|
|
90
|
+
console.log(chalk.gray(` → ${config.gitRepo}`));
|
|
91
|
+
|
|
92
|
+
// Auto-create the repo if gh CLI is available and repo doesn't exist
|
|
93
|
+
if (direction === 'upload') {
|
|
94
|
+
try {
|
|
95
|
+
execFileSync('gh', ['repo', 'view', `${username}/${repo}`], { stdio: 'ignore' });
|
|
96
|
+
console.log(chalk.gray(' ✔ Repo exists\n'));
|
|
97
|
+
} catch {
|
|
98
|
+
// Repo doesn't exist — try to create it
|
|
99
|
+
try {
|
|
100
|
+
execFileSync('gh', ['repo', 'create', `${username}/${repo}`, '--private', '--description', 'AI memory backup (memoir-cli)'], { stdio: 'ignore' });
|
|
101
|
+
console.log(chalk.green(' ✔ Created private repo\n'));
|
|
102
|
+
} catch {
|
|
103
|
+
console.log(chalk.yellow(' ⚠ Could not auto-create repo. Create it manually on GitHub.\n'));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
console.log('');
|
|
108
|
+
}
|
|
91
109
|
}
|
|
92
110
|
|
|
93
111
|
await saveConfig(config);
|
package/src/commands/status.js
CHANGED
|
@@ -23,8 +23,8 @@ export async function statusCommand() {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
// Detected tools
|
|
26
|
-
const
|
|
27
|
-
|
|
26
|
+
const foundTools = [];
|
|
27
|
+
const notFound = [];
|
|
28
28
|
|
|
29
29
|
for (const adapter of adapters) {
|
|
30
30
|
let found = false;
|
|
@@ -40,22 +40,30 @@ export async function statusCommand() {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
if (found) {
|
|
43
|
-
|
|
44
|
-
detected++;
|
|
43
|
+
foundTools.push(chalk.green(' ✔ ') + chalk.white(adapter.name));
|
|
45
44
|
} else {
|
|
46
|
-
|
|
45
|
+
notFound.push(adapter.name);
|
|
47
46
|
}
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
const
|
|
51
|
-
?
|
|
52
|
-
: chalk.yellow('No AI tools detected');
|
|
49
|
+
const lines = foundTools.length > 0
|
|
50
|
+
? foundTools
|
|
51
|
+
: [chalk.yellow(' No AI tools detected')];
|
|
52
|
+
|
|
53
|
+
const summary = foundTools.length > 0
|
|
54
|
+
? chalk.white(`${foundTools.length} tool${foundTools.length !== 1 ? 's' : ''} ready to sync`)
|
|
55
|
+
: chalk.gray(`Supports: ${adapters.map(a => a.name).join(', ')}`);
|
|
56
|
+
|
|
57
|
+
// Show not-found tools as a compact line if there are found tools
|
|
58
|
+
const notFoundLine = foundTools.length > 0 && notFound.length > 0
|
|
59
|
+
? '\n' + chalk.gray(` Also supports: ${notFound.join(', ')}`)
|
|
60
|
+
: '';
|
|
53
61
|
|
|
54
62
|
console.log(boxen(
|
|
55
63
|
gradient.pastel(' memoir status ') + '\n\n' +
|
|
56
64
|
configLine + '\n\n' +
|
|
57
65
|
chalk.bold.white('AI Tools') + '\n' +
|
|
58
|
-
lines.join('\n') + '\n\n' +
|
|
66
|
+
lines.join('\n') + notFoundLine + '\n\n' +
|
|
59
67
|
chalk.gray('─'.repeat(30)) + '\n' +
|
|
60
68
|
summary,
|
|
61
69
|
{ padding: 1, borderStyle: 'round', borderColor: 'cyan', dimBorder: true }
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
const home = os.homedir();
|
|
6
|
+
const cwd = process.cwd();
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
key: 'cline',
|
|
10
|
+
name: 'Cline',
|
|
11
|
+
icon: '🤖',
|
|
12
|
+
format: 'Configuration and rules for Cline AI coding assistant. Includes settings for AI behavior and custom rules files for project-specific instructions.',
|
|
13
|
+
|
|
14
|
+
discover() {
|
|
15
|
+
const files = [];
|
|
16
|
+
const clineDir = process.platform === 'win32'
|
|
17
|
+
? path.join(process.env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev')
|
|
18
|
+
: path.join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev');
|
|
19
|
+
|
|
20
|
+
// Check for .clinerules in project
|
|
21
|
+
const projectFile = path.join(cwd, '.clinerules');
|
|
22
|
+
if (fs.existsSync(projectFile)) {
|
|
23
|
+
files.push({ filePath: projectFile, content: fs.readFileSync(projectFile, 'utf-8'), scope: 'project' });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Discover settings and rules from Cline extension storage
|
|
27
|
+
if (fs.existsSync(clineDir)) {
|
|
28
|
+
const scanDir = (dir, scope) => {
|
|
29
|
+
try {
|
|
30
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
const filePath = path.join(dir, entry.name);
|
|
33
|
+
if (entry.isFile()) {
|
|
34
|
+
files.push({ filePath, content: fs.readFileSync(filePath, 'utf-8'), scope });
|
|
35
|
+
} else if (entry.isDirectory()) {
|
|
36
|
+
scanDir(filePath, scope);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} catch {}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const settingsDir = path.join(clineDir, 'settings');
|
|
43
|
+
if (fs.existsSync(settingsDir)) {
|
|
44
|
+
scanDir(settingsDir, 'user');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const rulesDir = path.join(clineDir, 'rules');
|
|
48
|
+
if (fs.existsSync(rulesDir)) {
|
|
49
|
+
scanDir(rulesDir, 'user');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Discover .md files in root
|
|
53
|
+
try {
|
|
54
|
+
const entries = fs.readdirSync(clineDir);
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
if (entry.endsWith('.md')) {
|
|
57
|
+
const filePath = path.join(clineDir, entry);
|
|
58
|
+
if (fs.statSync(filePath).isFile()) {
|
|
59
|
+
files.push({ filePath, content: fs.readFileSync(filePath, 'utf-8'), scope: 'user' });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch {}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return files;
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
targetPath() {
|
|
70
|
+
return path.join(cwd, '.clinerules');
|
|
71
|
+
}
|
|
72
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
const home = os.homedir();
|
|
6
|
+
const cwd = process.cwd();
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
key: 'continuedev',
|
|
10
|
+
name: 'Continue.dev',
|
|
11
|
+
icon: '🔄',
|
|
12
|
+
format: 'JSON, TypeScript, or YAML configuration for Continue.dev AI assistant. Includes config files for model selection, context providers, and slash commands. Supports .continuerules for project-specific instructions.',
|
|
13
|
+
|
|
14
|
+
discover() {
|
|
15
|
+
const files = [];
|
|
16
|
+
const continueDir = process.platform === 'win32'
|
|
17
|
+
? path.join(process.env.USERPROFILE || home, '.continue')
|
|
18
|
+
: path.join(home, '.continue');
|
|
19
|
+
|
|
20
|
+
// Check for .continuerules in project
|
|
21
|
+
const projectFile = path.join(cwd, '.continuerules');
|
|
22
|
+
if (fs.existsSync(projectFile)) {
|
|
23
|
+
files.push({ filePath: projectFile, content: fs.readFileSync(projectFile, 'utf-8'), scope: 'project' });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (fs.existsSync(continueDir)) {
|
|
27
|
+
const configFiles = ['config.json', 'config.ts', 'config.yaml', '.continuerules'];
|
|
28
|
+
for (const file of configFiles) {
|
|
29
|
+
const filePath = path.join(continueDir, file);
|
|
30
|
+
if (fs.existsSync(filePath)) {
|
|
31
|
+
files.push({ filePath, content: fs.readFileSync(filePath, 'utf-8'), scope: 'user' });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Discover .md files in root
|
|
36
|
+
try {
|
|
37
|
+
const entries = fs.readdirSync(continueDir);
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
if (entry.endsWith('.md')) {
|
|
40
|
+
const filePath = path.join(continueDir, entry);
|
|
41
|
+
if (fs.statSync(filePath).isFile()) {
|
|
42
|
+
files.push({ filePath, content: fs.readFileSync(filePath, 'utf-8'), scope: 'user' });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} catch {}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return files;
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
targetPath() {
|
|
53
|
+
return path.join(cwd, '.continuerules');
|
|
54
|
+
}
|
|
55
|
+
};
|
package/src/tools/index.js
CHANGED
|
@@ -4,10 +4,13 @@ import codex from './codex.js';
|
|
|
4
4
|
import cursor from './cursor.js';
|
|
5
5
|
import copilot from './copilot.js';
|
|
6
6
|
import windsurf from './windsurf.js';
|
|
7
|
+
import zed from './zed.js';
|
|
8
|
+
import cline from './cline.js';
|
|
9
|
+
import continuedev from './continuedev.js';
|
|
7
10
|
import aider from './aider.js';
|
|
8
11
|
|
|
9
12
|
const registry = {};
|
|
10
|
-
for (const tool of [claude, gemini, codex, cursor, copilot, windsurf, aider]) {
|
|
13
|
+
for (const tool of [claude, gemini, codex, cursor, copilot, windsurf, zed, cline, continuedev, aider]) {
|
|
11
14
|
registry[tool.key] = tool;
|
|
12
15
|
}
|
|
13
16
|
|
package/src/tools/zed.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
const home = os.homedir();
|
|
6
|
+
const cwd = process.cwd();
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
key: 'zed',
|
|
10
|
+
name: 'Zed',
|
|
11
|
+
icon: '🔶',
|
|
12
|
+
format: 'JSON or markdown configuration files for Zed editor. Includes settings.json for editor preferences, keymap.json for keybindings, and tasks.json for task runner configs.',
|
|
13
|
+
|
|
14
|
+
discover() {
|
|
15
|
+
const files = [];
|
|
16
|
+
const zedDir = process.platform === 'win32'
|
|
17
|
+
? path.join(process.env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'Zed')
|
|
18
|
+
: path.join(home, '.config', 'zed');
|
|
19
|
+
|
|
20
|
+
if (fs.existsSync(zedDir)) {
|
|
21
|
+
const configFiles = ['settings.json', 'keymap.json', 'tasks.json'];
|
|
22
|
+
for (const file of configFiles) {
|
|
23
|
+
const filePath = path.join(zedDir, file);
|
|
24
|
+
if (fs.existsSync(filePath)) {
|
|
25
|
+
files.push({ filePath, content: fs.readFileSync(filePath, 'utf-8'), scope: 'user' });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Also discover .md files in root
|
|
29
|
+
try {
|
|
30
|
+
const entries = fs.readdirSync(zedDir);
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
if (entry.endsWith('.md')) {
|
|
33
|
+
const filePath = path.join(zedDir, entry);
|
|
34
|
+
if (fs.statSync(filePath).isFile()) {
|
|
35
|
+
files.push({ filePath, content: fs.readFileSync(filePath, 'utf-8'), scope: 'user' });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} catch {}
|
|
40
|
+
}
|
|
41
|
+
return files;
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
targetPath() {
|
|
45
|
+
const zedDir = process.platform === 'win32'
|
|
46
|
+
? path.join(process.env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'Zed')
|
|
47
|
+
: path.join(home, '.config', 'zed');
|
|
48
|
+
return path.join(zedDir, 'settings.json');
|
|
49
|
+
}
|
|
50
|
+
};
|