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.
@@ -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
+ }
@@ -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}\n`));
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
+ }
@@ -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
- const onlyFilter = options.only ? options.only.split(',').map(t => t.trim().toLowerCase()) : null;
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) {
@@ -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(
@@ -92,7 +92,7 @@ async function injectHandoff(content, tool) {
92
92
  }
93
93
 
94
94
  export async function resumeCommand(options = {}) {
95
- const config = await getConfig();
95
+ const config = await getConfig(options.profile);
96
96
 
97
97
  if (!config) {
98
98
  console.log('\n' + boxen(
@@ -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();
@@ -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 lines = [];
27
- let detected = 0;
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
- lines.push(chalk.green(' ✔ ') + chalk.white(adapter.name));
44
- detected++;
43
+ foundTools.push(chalk.green(' ✔ ') + chalk.white(adapter.name));
45
44
  } else {
46
- lines.push(chalk.gray(' ○ ' + adapter.name));
45
+ notFound.push(adapter.name);
47
46
  }
48
47
  }
49
48
 
50
- const summary = detected > 0
51
- ? chalk.white(`${detected} tool${detected !== 1 ? 's' : ''} ready to sync`)
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 }
@@ -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;