memoir-cli 2.1.1 → 2.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/README.md +91 -113
- package/bin/memoir.js +137 -12
- package/package.json +7 -3
- package/src/adapters/index.js +75 -1
- package/src/commands/diff.js +2 -2
- package/src/commands/doctor.js +223 -0
- package/src/commands/init.js +19 -1
- package/src/commands/profile.js +199 -0
- package/src/commands/push.js +4 -2
- package/src/commands/restore.js +1 -1
- package/src/commands/resume.js +1 -1
- package/src/commands/snapshot.js +1 -1
- package/src/commands/status.js +19 -11
- package/src/commands/view.js +2 -2
- package/src/config.js +94 -7
- package/src/tools/chatgpt.js +24 -0
- package/src/tools/cline.js +72 -0
- package/src/tools/continuedev.js +55 -0
- package/src/tools/index.js +5 -1
- package/src/tools/zed.js +50 -0
|
@@ -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(options = {}) {
|
|
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(options.profile);
|
|
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);
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import boxen from 'boxen';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import { execFileSync } from 'child_process';
|
|
5
|
+
import {
|
|
6
|
+
getRawConfig, listProfiles, getActiveProfileName,
|
|
7
|
+
createProfile, switchProfile, deleteProfile
|
|
8
|
+
} from '../config.js';
|
|
9
|
+
|
|
10
|
+
function getGitHubUsername() {
|
|
11
|
+
try {
|
|
12
|
+
return execFileSync('gh', ['api', 'user', '--jq', '.login'], { encoding: 'utf8' }).trim();
|
|
13
|
+
} catch {
|
|
14
|
+
try {
|
|
15
|
+
return execFileSync('git', ['config', '--global', 'user.name'], { encoding: 'utf8' }).trim();
|
|
16
|
+
} catch { return ''; }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function profileListCommand() {
|
|
21
|
+
const profiles = await listProfiles();
|
|
22
|
+
const active = await getActiveProfileName();
|
|
23
|
+
const raw = await getRawConfig();
|
|
24
|
+
|
|
25
|
+
if (profiles.length === 0) {
|
|
26
|
+
console.log('\n' + chalk.yellow('No profiles configured. Run ') + chalk.cyan('memoir init') + chalk.yellow(' first.\n'));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.log();
|
|
31
|
+
console.log(chalk.bold.white(' Profiles:\n'));
|
|
32
|
+
|
|
33
|
+
for (const name of profiles) {
|
|
34
|
+
const isActive = name === active;
|
|
35
|
+
const marker = isActive ? chalk.green(' ✔ ') : chalk.gray(' ');
|
|
36
|
+
const label = isActive ? chalk.white.bold(name) : chalk.white(name);
|
|
37
|
+
|
|
38
|
+
// Get profile details
|
|
39
|
+
let detail = '';
|
|
40
|
+
if (raw.version >= 2 && raw.profiles?.[name]) {
|
|
41
|
+
const p = raw.profiles[name];
|
|
42
|
+
const dest = p.provider === 'git' ? p.gitRepo : p.localPath;
|
|
43
|
+
detail = chalk.gray(` → ${dest}`);
|
|
44
|
+
if (p.only) detail += chalk.gray(` (${p.only.join(', ')})`);
|
|
45
|
+
} else if (!raw.version) {
|
|
46
|
+
const dest = raw.provider === 'git' ? raw.gitRepo : raw.localPath;
|
|
47
|
+
detail = chalk.gray(` → ${dest}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log(`${marker}${label}${detail}`);
|
|
51
|
+
}
|
|
52
|
+
console.log();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function profileCreateCommand(name) {
|
|
56
|
+
const profiles = await listProfiles();
|
|
57
|
+
if (profiles.includes(name)) {
|
|
58
|
+
console.log(chalk.red(`\n✖ Profile "${name}" already exists.\n`));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log('\n' + chalk.cyan(`Creating profile: ${chalk.bold(name)}\n`));
|
|
63
|
+
|
|
64
|
+
const detectedUser = getGitHubUsername();
|
|
65
|
+
|
|
66
|
+
const { provider } = await inquirer.prompt([{
|
|
67
|
+
type: 'list',
|
|
68
|
+
name: 'provider',
|
|
69
|
+
message: 'Storage for this profile?',
|
|
70
|
+
choices: [
|
|
71
|
+
{ name: 'GitHub', value: 'git' },
|
|
72
|
+
{ name: 'Local folder', value: 'local' }
|
|
73
|
+
]
|
|
74
|
+
}]);
|
|
75
|
+
|
|
76
|
+
const profileConfig = { provider };
|
|
77
|
+
|
|
78
|
+
if (provider === 'local') {
|
|
79
|
+
const { localPath } = await inquirer.prompt([{
|
|
80
|
+
type: 'input',
|
|
81
|
+
name: 'localPath',
|
|
82
|
+
message: 'Save to:',
|
|
83
|
+
validate: (input) => input.trim() ? true : 'Required'
|
|
84
|
+
}]);
|
|
85
|
+
profileConfig.localPath = localPath;
|
|
86
|
+
} else {
|
|
87
|
+
const answers = await inquirer.prompt([
|
|
88
|
+
{
|
|
89
|
+
type: 'input',
|
|
90
|
+
name: 'username',
|
|
91
|
+
message: 'GitHub username:',
|
|
92
|
+
default: detectedUser || undefined,
|
|
93
|
+
validate: (input) => input.trim() ? true : 'Required'
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
type: 'input',
|
|
97
|
+
name: 'repo',
|
|
98
|
+
message: 'Repo name:',
|
|
99
|
+
default: `ai-memory-${name}`,
|
|
100
|
+
validate: (input) => input.trim() ? true : 'Required'
|
|
101
|
+
}
|
|
102
|
+
]);
|
|
103
|
+
const username = answers.username.trim();
|
|
104
|
+
const repo = answers.repo.trim();
|
|
105
|
+
profileConfig.gitRepo = `https://github.com/${username}/${repo}.git`;
|
|
106
|
+
|
|
107
|
+
// Auto-create repo if possible
|
|
108
|
+
try {
|
|
109
|
+
execFileSync('gh', ['repo', 'view', `${username}/${repo}`], { stdio: 'ignore' });
|
|
110
|
+
console.log(chalk.gray(` ✔ Repo exists`));
|
|
111
|
+
} catch {
|
|
112
|
+
try {
|
|
113
|
+
execFileSync('gh', ['repo', 'create', `${username}/${repo}`, '--private', '--description', `AI memory backup - ${name} (memoir-cli)`], { stdio: 'ignore' });
|
|
114
|
+
console.log(chalk.green(` ✔ Created private repo`));
|
|
115
|
+
} catch {
|
|
116
|
+
console.log(chalk.yellow(` ⚠ Could not auto-create repo. Create it manually on GitHub.`));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Ask which tools to sync (optional filter)
|
|
122
|
+
const { filterTools } = await inquirer.prompt([{
|
|
123
|
+
type: 'confirm',
|
|
124
|
+
name: 'filterTools',
|
|
125
|
+
message: 'Limit this profile to specific tools?',
|
|
126
|
+
default: false
|
|
127
|
+
}]);
|
|
128
|
+
|
|
129
|
+
if (filterTools) {
|
|
130
|
+
const { tools } = await inquirer.prompt([{
|
|
131
|
+
type: 'checkbox',
|
|
132
|
+
name: 'tools',
|
|
133
|
+
message: 'Which tools should this profile sync?',
|
|
134
|
+
choices: [
|
|
135
|
+
{ name: 'Claude Code', value: 'claude' },
|
|
136
|
+
{ name: 'Gemini CLI', value: 'gemini' },
|
|
137
|
+
{ name: 'OpenAI Codex', value: 'codex' },
|
|
138
|
+
{ name: 'Cursor', value: 'cursor' },
|
|
139
|
+
{ name: 'GitHub Copilot', value: 'copilot' },
|
|
140
|
+
{ name: 'Windsurf', value: 'windsurf' },
|
|
141
|
+
{ name: 'Zed', value: 'zed' },
|
|
142
|
+
{ name: 'Cline', value: 'cline' },
|
|
143
|
+
{ name: 'Continue.dev', value: 'continue' },
|
|
144
|
+
{ name: 'Aider', value: 'aider' }
|
|
145
|
+
]
|
|
146
|
+
}]);
|
|
147
|
+
if (tools.length > 0) {
|
|
148
|
+
profileConfig.only = tools;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
await createProfile(name, profileConfig);
|
|
153
|
+
|
|
154
|
+
// Ask if they want to switch to it
|
|
155
|
+
const { switchNow } = await inquirer.prompt([{
|
|
156
|
+
type: 'confirm',
|
|
157
|
+
name: 'switchNow',
|
|
158
|
+
message: `Switch to "${name}" now?`,
|
|
159
|
+
default: true
|
|
160
|
+
}]);
|
|
161
|
+
|
|
162
|
+
if (switchNow) {
|
|
163
|
+
await switchProfile(name);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.log('\n' + boxen(
|
|
167
|
+
chalk.green(`✔ Profile "${name}" created`) +
|
|
168
|
+
(switchNow ? chalk.gray(` (now active)`) : ''),
|
|
169
|
+
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderStyle: 'round', borderColor: 'green', dimBorder: true }
|
|
170
|
+
) + '\n');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function profileSwitchCommand(name) {
|
|
174
|
+
try {
|
|
175
|
+
await switchProfile(name);
|
|
176
|
+
console.log('\n' + chalk.green(`✔ Switched to profile "${name}"\n`));
|
|
177
|
+
} catch (err) {
|
|
178
|
+
console.log('\n' + chalk.red(`✖ ${err.message}\n`));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function profileDeleteCommand(name) {
|
|
183
|
+
try {
|
|
184
|
+
const { confirm } = await inquirer.prompt([{
|
|
185
|
+
type: 'confirm',
|
|
186
|
+
name: 'confirm',
|
|
187
|
+
message: `Delete profile "${name}"? This cannot be undone.`,
|
|
188
|
+
default: false
|
|
189
|
+
}]);
|
|
190
|
+
if (!confirm) {
|
|
191
|
+
console.log(chalk.gray('\nCancelled.\n'));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
await deleteProfile(name);
|
|
195
|
+
console.log('\n' + chalk.green(`✔ Profile "${name}" deleted\n`));
|
|
196
|
+
} catch (err) {
|
|
197
|
+
console.log('\n' + chalk.red(`✖ ${err.message}\n`));
|
|
198
|
+
}
|
|
199
|
+
}
|
package/src/commands/push.js
CHANGED
|
@@ -10,7 +10,7 @@ import { extractMemories, adapters } from '../adapters/index.js';
|
|
|
10
10
|
import { syncToLocal, syncToGit } from '../providers/index.js';
|
|
11
11
|
|
|
12
12
|
export async function pushCommand(options = {}) {
|
|
13
|
-
const config = await getConfig();
|
|
13
|
+
const config = await getConfig(options.profile);
|
|
14
14
|
|
|
15
15
|
if (!config) {
|
|
16
16
|
console.log('\n' + boxen(
|
|
@@ -28,7 +28,9 @@ export async function pushCommand(options = {}) {
|
|
|
28
28
|
await fs.ensureDir(stagingDir);
|
|
29
29
|
|
|
30
30
|
try {
|
|
31
|
-
|
|
31
|
+
// Profile-level tool filter (config.only) merged with CLI --only flag
|
|
32
|
+
const onlyRaw = options.only || (config.only ? config.only.join(',') : null);
|
|
33
|
+
const onlyFilter = onlyRaw ? onlyRaw.split(',').map(t => t.trim().toLowerCase()) : null;
|
|
32
34
|
const foundAny = await extractMemories(stagingDir, spinner, onlyFilter);
|
|
33
35
|
|
|
34
36
|
if (!foundAny) {
|
package/src/commands/restore.js
CHANGED
|
@@ -9,7 +9,7 @@ import { getConfig } from '../config.js';
|
|
|
9
9
|
import { fetchFromLocal, fetchFromGit } from '../providers/restore.js';
|
|
10
10
|
|
|
11
11
|
export async function restoreCommand(options = {}) {
|
|
12
|
-
const config = await getConfig();
|
|
12
|
+
const config = await getConfig(options.profile);
|
|
13
13
|
|
|
14
14
|
if (!config) {
|
|
15
15
|
console.log('\n' + boxen(
|
package/src/commands/resume.js
CHANGED
package/src/commands/snapshot.js
CHANGED
|
@@ -264,7 +264,7 @@ Keep it under 300 words total. Be specific about file names and features.`;
|
|
|
264
264
|
}
|
|
265
265
|
|
|
266
266
|
export async function snapshotCommand(options = {}) {
|
|
267
|
-
const config = await getConfig();
|
|
267
|
+
const config = await getConfig(options.profile);
|
|
268
268
|
|
|
269
269
|
console.log();
|
|
270
270
|
const spinner = ora({ text: chalk.gray('Finding latest session...'), spinner: 'dots' }).start();
|
package/src/commands/status.js
CHANGED
|
@@ -6,8 +6,8 @@ import gradient from 'gradient-string';
|
|
|
6
6
|
import { getConfig } from '../config.js';
|
|
7
7
|
import { adapters } from '../adapters/index.js';
|
|
8
8
|
|
|
9
|
-
export async function statusCommand() {
|
|
10
|
-
const config = await getConfig();
|
|
9
|
+
export async function statusCommand(options = {}) {
|
|
10
|
+
const config = await getConfig(options.profile);
|
|
11
11
|
|
|
12
12
|
console.log();
|
|
13
13
|
|
|
@@ -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 }
|
package/src/commands/view.js
CHANGED
|
@@ -36,8 +36,8 @@ function isBinaryFile(filePath) {
|
|
|
36
36
|
return binaryExts.includes(path.extname(filePath).toLowerCase());
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
export async function viewCommand() {
|
|
40
|
-
const config = await getConfig();
|
|
39
|
+
export async function viewCommand(options = {}) {
|
|
40
|
+
const config = await getConfig(options.profile);
|
|
41
41
|
if (!config) {
|
|
42
42
|
console.log(chalk.red('\n✖ Not configured yet. Run: memoir init\n'));
|
|
43
43
|
return;
|