memoir-cli 3.17.1 → 3.17.3
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/package.json +1 -1
- package/src/cloud/auth.js +45 -114
- package/src/commands/login.js +28 -139
- package/src/commands/upgrade.js +4 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memoir-cli",
|
|
3
|
-
"version": "3.17.
|
|
3
|
+
"version": "3.17.3",
|
|
4
4
|
"mcpName": "io.github.camgitt/memoir",
|
|
5
5
|
"description": "Portable, project-scoped memory and session handoffs for coding agents. Readable files, MCP recall, and optional user-passphrase encrypted backups.",
|
|
6
6
|
"main": "src/index.js",
|
package/src/cloud/auth.js
CHANGED
|
@@ -11,7 +11,7 @@ const AUTH_FILE = path.join(configDir, 'auth.json');
|
|
|
11
11
|
|
|
12
12
|
async function supaFetch(endpoint, options = {}) {
|
|
13
13
|
const url = `${SUPABASE_URL}${endpoint}`;
|
|
14
|
-
|
|
14
|
+
return fetch(url, {
|
|
15
15
|
...options,
|
|
16
16
|
headers: {
|
|
17
17
|
'apikey': SUPABASE_ANON_KEY,
|
|
@@ -19,42 +19,27 @@ async function supaFetch(endpoint, options = {}) {
|
|
|
19
19
|
...options.headers,
|
|
20
20
|
},
|
|
21
21
|
});
|
|
22
|
-
return res;
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
// GoTrue's REST API takes the post-email landing URL as a `redirect_to`
|
|
26
|
-
// QUERY PARAM. `options.emailRedirectTo` / `options.redirectTo` in the JSON
|
|
27
|
-
// body is supabase-js's client shape — sent raw it is silently ignored and
|
|
28
|
-
// the email links fall back to the project's Site URL. Both URLs below must
|
|
29
|
-
// also be on the Auth → URL Configuration → Redirect URLs allow-list.
|
|
30
24
|
const CONFIRMED_URL = 'https://memoir.sh/confirmed';
|
|
31
25
|
const RESET_URL = 'https://memoir.sh/reset-password';
|
|
32
26
|
|
|
33
27
|
export async function signUp(email, password) {
|
|
34
|
-
const res = await supaFetch(`/auth/v1/signup?redirect_to=${encodeURIComponent(CONFIRMED_URL)}`, {
|
|
35
|
-
method: 'POST',
|
|
36
|
-
body: JSON.stringify({ email, password }),
|
|
37
|
-
});
|
|
28
|
+
const res = await supaFetch(`/auth/v1/signup?redirect_to=${encodeURIComponent(CONFIRMED_URL)}`, { method: 'POST', body: JSON.stringify({ email, password }) });
|
|
38
29
|
const data = await res.json();
|
|
39
30
|
if (!res.ok) throw new Error(data.error_description || data.msg || 'Sign up failed');
|
|
40
31
|
return data;
|
|
41
32
|
}
|
|
42
33
|
|
|
43
34
|
export async function signIn(email, password) {
|
|
44
|
-
const res = await supaFetch('/auth/v1/token?grant_type=password', {
|
|
45
|
-
method: 'POST',
|
|
46
|
-
body: JSON.stringify({ email, password }),
|
|
47
|
-
});
|
|
35
|
+
const res = await supaFetch('/auth/v1/token?grant_type=password', { method: 'POST', body: JSON.stringify({ email, password }) });
|
|
48
36
|
const data = await res.json();
|
|
49
37
|
if (!res.ok) throw new Error(data.error_description || data.msg || 'Sign in failed');
|
|
50
38
|
return data;
|
|
51
39
|
}
|
|
52
40
|
|
|
53
41
|
export async function refreshSession(refreshToken) {
|
|
54
|
-
const res = await supaFetch('/auth/v1/token?grant_type=refresh_token', {
|
|
55
|
-
method: 'POST',
|
|
56
|
-
body: JSON.stringify({ refresh_token: refreshToken }),
|
|
57
|
-
});
|
|
42
|
+
const res = await supaFetch('/auth/v1/token?grant_type=refresh_token', { method: 'POST', body: JSON.stringify({ refresh_token: refreshToken }) });
|
|
58
43
|
const data = await res.json();
|
|
59
44
|
if (!res.ok) throw new Error(data.error_description || data.msg || 'Token refresh failed');
|
|
60
45
|
return data;
|
|
@@ -62,127 +47,73 @@ export async function refreshSession(refreshToken) {
|
|
|
62
47
|
|
|
63
48
|
export async function saveSession(session) {
|
|
64
49
|
await fs.ensureDir(configDir);
|
|
65
|
-
const payload = {
|
|
66
|
-
access_token: session.access_token,
|
|
67
|
-
refresh_token: session.refresh_token,
|
|
68
|
-
expires_at: Date.now() + (session.expires_in * 1000),
|
|
69
|
-
user: {
|
|
70
|
-
id: session.user.id,
|
|
71
|
-
email: session.user.email,
|
|
72
|
-
},
|
|
73
|
-
};
|
|
50
|
+
const payload = { access_token: session.access_token, refresh_token: session.refresh_token, expires_at: Date.now() + (session.expires_in * 1000), user: { id: session.user.id, email: session.user.email } };
|
|
74
51
|
await fs.writeFile(AUTH_FILE, JSON.stringify(payload, null, 2), { mode: 0o600 });
|
|
75
52
|
return payload;
|
|
76
53
|
}
|
|
77
54
|
|
|
78
55
|
export async function getSession() {
|
|
79
56
|
if (!await fs.pathExists(AUTH_FILE)) return null;
|
|
80
|
-
|
|
81
57
|
const stored = await fs.readJson(AUTH_FILE);
|
|
82
|
-
|
|
83
|
-
// If token expires within 60 seconds, refresh
|
|
84
58
|
if (stored.expires_at < Date.now() + 60000) {
|
|
85
|
-
try {
|
|
86
|
-
|
|
87
|
-
return await saveSession(refreshed);
|
|
88
|
-
} catch {
|
|
89
|
-
// Refresh failed — session is dead
|
|
90
|
-
await fs.remove(AUTH_FILE);
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
59
|
+
try { return await saveSession(await refreshSession(stored.refresh_token)); }
|
|
60
|
+
catch { await fs.remove(AUTH_FILE); return null; }
|
|
93
61
|
}
|
|
94
|
-
|
|
95
62
|
return stored;
|
|
96
63
|
}
|
|
97
64
|
|
|
98
|
-
export async function logout() {
|
|
99
|
-
|
|
100
|
-
await fs.remove(AUTH_FILE);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export async function isLoggedIn() {
|
|
105
|
-
const session = await getSession();
|
|
106
|
-
return !!session;
|
|
107
|
-
}
|
|
65
|
+
export async function logout() { if (await fs.pathExists(AUTH_FILE)) await fs.remove(AUTH_FILE); }
|
|
66
|
+
export async function isLoggedIn() { return !!(await getSession()); }
|
|
108
67
|
|
|
68
|
+
// A failed plan lookup is not evidence that a user is on Free. Callers use
|
|
69
|
+
// this value to choose destructive retention limits, so fail closed instead of
|
|
70
|
+
// silently downgrading a Pro account and pruning its history.
|
|
109
71
|
export async function getSubscription(session) {
|
|
110
|
-
const res = await supaFetch('/rest/v1/subscriptions?select=*&user_id=eq.' + session.user.id, {
|
|
111
|
-
headers: {
|
|
112
|
-
'Authorization': `Bearer ${session.access_token}`,
|
|
113
|
-
},
|
|
72
|
+
const res = await supaFetch('/rest/v1/subscriptions?select=*&user_id=eq.' + encodeURIComponent(session.user.id), {
|
|
73
|
+
headers: { 'Authorization': `Bearer ${session.access_token}` },
|
|
114
74
|
});
|
|
75
|
+
if (!res.ok) throw new Error(`Could not verify subscription (${res.status}). Existing backups were not pruned.`);
|
|
115
76
|
const data = await res.json();
|
|
116
|
-
if (!
|
|
77
|
+
if (!Array.isArray(data)) throw new Error('Could not verify subscription. Existing backups were not pruned.');
|
|
78
|
+
if (!data.length) return { status: 'free' };
|
|
117
79
|
return data[0];
|
|
118
80
|
}
|
|
119
81
|
|
|
120
82
|
export async function resetPassword(email) {
|
|
121
|
-
const res = await supaFetch(`/auth/v1/recover?redirect_to=${encodeURIComponent(RESET_URL)}`, {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
83
|
+
const res = await supaFetch(`/auth/v1/recover?redirect_to=${encodeURIComponent(RESET_URL)}`, { method: 'POST', body: JSON.stringify({ email }) });
|
|
84
|
+
if (!res.ok) { const data = await res.json(); throw new Error(data.error_description || data.msg || 'Password reset failed'); }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function requireDelete(res, label) {
|
|
88
|
+
if (res.ok || res.status === 404) return;
|
|
89
|
+
let detail = '';
|
|
90
|
+
try { detail = (await res.text()).trim(); } catch {}
|
|
91
|
+
throw new Error(`${label} failed (${res.status})${detail ? ': ' + detail : ''}. Nothing else was reported as deleted; retry account deletion.`);
|
|
129
92
|
}
|
|
130
93
|
|
|
94
|
+
// Deletes the data the current user is authorized to delete. Auth-user removal
|
|
95
|
+
// itself requires a privileged server-side endpoint and must never be attempted
|
|
96
|
+
// with a service-role key from this CLI.
|
|
131
97
|
export async function deleteAccount(session) {
|
|
132
|
-
|
|
133
|
-
const backupsRes = await fetch(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
for (const backup of backups) {
|
|
146
|
-
// Delete storage object
|
|
147
|
-
await fetch(`${SUPABASE_URL}/storage/v1/object/${STORAGE_BUCKET}/${backup.storage_path}`, {
|
|
148
|
-
method: 'DELETE',
|
|
149
|
-
headers: {
|
|
150
|
-
'Authorization': `Bearer ${session.access_token}`,
|
|
151
|
-
'apikey': SUPABASE_ANON_KEY,
|
|
152
|
-
},
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
// Delete metadata row
|
|
156
|
-
await fetch(`${SUPABASE_URL}/rest/v1/backups?id=eq.${backup.id}`, {
|
|
157
|
-
method: 'DELETE',
|
|
158
|
-
headers: {
|
|
159
|
-
'Authorization': `Bearer ${session.access_token}`,
|
|
160
|
-
'apikey': SUPABASE_ANON_KEY,
|
|
161
|
-
},
|
|
162
|
-
});
|
|
163
|
-
}
|
|
98
|
+
const headers = { 'Authorization': `Bearer ${session.access_token}`, 'apikey': SUPABASE_ANON_KEY };
|
|
99
|
+
const backupsRes = await fetch(`${SUPABASE_URL}/rest/v1/backups?select=*&user_id=eq.${encodeURIComponent(session.user.id)}`, { headers });
|
|
100
|
+
if (!backupsRes.ok) throw new Error(`Could not enumerate backups (${backupsRes.status}); account deletion stopped safely.`);
|
|
101
|
+
const backups = await backupsRes.json();
|
|
102
|
+
|
|
103
|
+
for (const backup of backups) {
|
|
104
|
+
if (typeof backup.storage_path !== 'string' || !backup.storage_path.startsWith(session.user.id + '/')) throw new Error('Unsafe backup path; account deletion stopped.');
|
|
105
|
+
const object = await fetch(`${SUPABASE_URL}/storage/v1/object/${STORAGE_BUCKET}`, {
|
|
106
|
+
method: 'DELETE', headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify({ prefixes: [backup.storage_path] }),
|
|
107
|
+
});
|
|
108
|
+
await requireDelete(object, 'Backup object deletion');
|
|
109
|
+
const row = await fetch(`${SUPABASE_URL}/rest/v1/backups?id=eq.${encodeURIComponent(backup.id)}`, { method: 'DELETE', headers });
|
|
110
|
+
await requireDelete(row, 'Backup metadata deletion');
|
|
164
111
|
}
|
|
165
112
|
|
|
166
|
-
|
|
167
|
-
await fetch(`${SUPABASE_URL}/rest/v1/
|
|
168
|
-
method: 'DELETE',
|
|
169
|
-
headers: {
|
|
170
|
-
'Authorization': `Bearer ${session.access_token}`,
|
|
171
|
-
'apikey': SUPABASE_ANON_KEY,
|
|
172
|
-
},
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
// 3. Delete subscription
|
|
176
|
-
await fetch(`${SUPABASE_URL}/rest/v1/subscriptions?user_id=eq.${session.user.id}`, {
|
|
177
|
-
method: 'DELETE',
|
|
178
|
-
headers: {
|
|
179
|
-
'Authorization': `Bearer ${session.access_token}`,
|
|
180
|
-
'apikey': SUPABASE_ANON_KEY,
|
|
181
|
-
},
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
// 4. Sign out locally
|
|
113
|
+
await requireDelete(await fetch(`${SUPABASE_URL}/rest/v1/shared_links?user_id=eq.${encodeURIComponent(session.user.id)}`, { method: 'DELETE', headers }), 'Shared-link deletion');
|
|
114
|
+
await requireDelete(await fetch(`${SUPABASE_URL}/rest/v1/subscriptions?user_id=eq.${encodeURIComponent(session.user.id)}`, { method: 'DELETE', headers }), 'Subscription deletion');
|
|
185
115
|
await logout();
|
|
116
|
+
return { localDataDeleted: true, authUserDeleted: false };
|
|
186
117
|
}
|
|
187
118
|
|
|
188
119
|
export { AUTH_FILE, supaFetch };
|
package/src/commands/login.js
CHANGED
|
@@ -5,169 +5,58 @@ import inquirer from 'inquirer';
|
|
|
5
5
|
import { signIn, signUp, saveSession, getSession, logout, getSubscription, resetPassword, deleteAccount } from '../cloud/auth.js';
|
|
6
6
|
|
|
7
7
|
export async function loginCommand(options = {}) {
|
|
8
|
-
// Check if already logged in
|
|
9
8
|
const existing = await getSession();
|
|
10
9
|
if (existing) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
gradient.pastel(' memoir cloud ') + '\n\n' +
|
|
14
|
-
|
|
15
|
-
chalk.
|
|
16
|
-
|
|
17
|
-
) + '\n');
|
|
10
|
+
try {
|
|
11
|
+
const sub = await getSubscription(existing);
|
|
12
|
+
console.log('\n' + boxen(gradient.pastel(' memoir cloud ') + '\n\n' + chalk.green('✔ Already logged in as ') + chalk.cyan(existing.user.email) + '\n' + chalk.gray('Plan: ') + (sub.status === 'pro' ? chalk.green('Pro') : chalk.yellow('Free')), { padding: 1, borderStyle: 'round', borderColor: 'green', dimBorder: true }) + '\n');
|
|
13
|
+
} catch (error) {
|
|
14
|
+
console.log('\n' + boxen(chalk.green('✔ Logged in as ') + chalk.cyan(existing.user.email) + '\n' + chalk.yellow('Plan could not be verified right now. No backup history will be pruned until it can be verified.') + '\n' + chalk.gray(error.message), { padding: 1, borderStyle: 'round', borderColor: 'yellow' }) + '\n');
|
|
15
|
+
}
|
|
18
16
|
return;
|
|
19
17
|
}
|
|
20
18
|
|
|
21
19
|
let action, email, password;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (options.email && options.password) {
|
|
25
|
-
action = options.signup ? 'signup' : 'signin';
|
|
26
|
-
email = options.email;
|
|
27
|
-
password = options.password;
|
|
28
|
-
} else {
|
|
20
|
+
if (options.email && options.password) { action = options.signup ? 'signup' : 'signin'; email = options.email; password = options.password; }
|
|
21
|
+
else {
|
|
29
22
|
console.log();
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
name: 'action',
|
|
34
|
-
message: 'Sign in or create account?',
|
|
35
|
-
choices: [
|
|
36
|
-
{ name: 'Sign in (existing account)', value: 'signin' },
|
|
37
|
-
{ name: 'Create account', value: 'signup' },
|
|
38
|
-
],
|
|
39
|
-
}]);
|
|
40
|
-
action = actionAnswer.action;
|
|
41
|
-
|
|
42
|
-
const emailAnswer = await inquirer.prompt([{
|
|
43
|
-
type: 'input',
|
|
44
|
-
name: 'email',
|
|
45
|
-
message: 'Email:',
|
|
46
|
-
validate: v => v.includes('@') ? true : 'Enter a valid email',
|
|
47
|
-
}]);
|
|
48
|
-
email = emailAnswer.email;
|
|
49
|
-
|
|
50
|
-
const passwordAnswer = await inquirer.prompt([{
|
|
51
|
-
type: 'password',
|
|
52
|
-
name: 'password',
|
|
53
|
-
message: 'Password:',
|
|
54
|
-
mask: '*',
|
|
55
|
-
validate: v => v.length >= 6 ? true : 'Password must be at least 6 characters',
|
|
56
|
-
}]);
|
|
57
|
-
password = passwordAnswer.password;
|
|
23
|
+
action = (await inquirer.prompt([{ type: 'list', name: 'action', message: 'Sign in or create account?', choices: [{ name: 'Sign in (existing account)', value: 'signin' }, { name: 'Create account', value: 'signup' }] }])).action;
|
|
24
|
+
email = (await inquirer.prompt([{ type: 'input', name: 'email', message: 'Email:', validate: v => v.includes('@') ? true : 'Enter a valid email' }])).email;
|
|
25
|
+
password = (await inquirer.prompt([{ type: 'password', name: 'password', message: 'Password:', mask: '*', validate: v => v.length >= 6 ? true : 'Password must be at least 6 characters' }])).password;
|
|
58
26
|
}
|
|
59
27
|
|
|
60
28
|
try {
|
|
61
29
|
let session;
|
|
62
|
-
|
|
63
30
|
if (action === 'signup') {
|
|
64
31
|
const result = await signUp(email, password);
|
|
65
|
-
if (result.access_token)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
) + '\n');
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
} else {
|
|
77
|
-
const result = await signIn(email, password);
|
|
78
|
-
session = await saveSession(result);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const sub = await getSubscription(session);
|
|
82
|
-
|
|
83
|
-
console.log('\n' + boxen(
|
|
84
|
-
gradient.pastel(' memoir cloud ') + '\n\n' +
|
|
85
|
-
chalk.green('✔ Logged in as ') + chalk.cyan(session.user.email) + '\n' +
|
|
86
|
-
chalk.gray('Plan: ') + (sub.status === 'pro' ? chalk.green('Pro') : chalk.yellow('Free')) + '\n\n' +
|
|
87
|
-
chalk.gray('Try: ') + chalk.cyan('memoir cloud push') + chalk.gray(' to back up to the cloud'),
|
|
88
|
-
{ padding: 1, borderStyle: 'round', borderColor: 'green', dimBorder: true }
|
|
89
|
-
) + '\n');
|
|
90
|
-
|
|
91
|
-
} catch (error) {
|
|
92
|
-
console.log('\n' + boxen(
|
|
93
|
-
chalk.red('✖ ' + error.message),
|
|
94
|
-
{ padding: 1, borderStyle: 'round', borderColor: 'red' }
|
|
95
|
-
) + '\n');
|
|
96
|
-
}
|
|
32
|
+
if (result.access_token) session = await saveSession(result);
|
|
33
|
+
else { console.log('\n' + boxen(chalk.green('✔ Account created!') + '\n\n' + chalk.white('Check your email to confirm, then run ') + chalk.cyan('memoir login') + chalk.white(' again.'), { padding: 1, borderStyle: 'round', borderColor: 'green' }) + '\n'); return; }
|
|
34
|
+
} else session = await saveSession(await signIn(email, password));
|
|
35
|
+
|
|
36
|
+
let plan = 'Unknown';
|
|
37
|
+
try { const sub = await getSubscription(session); plan = sub.status === 'pro' ? 'Pro' : 'Free'; } catch {}
|
|
38
|
+
console.log('\n' + boxen(gradient.pastel(' memoir cloud ') + '\n\n' + chalk.green('✔ Logged in as ') + chalk.cyan(session.user.email) + '\n' + chalk.gray('Plan: ') + (plan === 'Pro' ? chalk.green(plan) : plan === 'Free' ? chalk.yellow(plan) : chalk.yellow(plan + ' — will retry before retention cleanup')) + '\n\n' + chalk.gray('Try: ') + chalk.cyan('memoir cloud push') + chalk.gray(' to back up to the cloud'), { padding: 1, borderStyle: 'round', borderColor: 'green', dimBorder: true }) + '\n');
|
|
39
|
+
} catch (error) { console.log('\n' + boxen(chalk.red('✖ ' + error.message), { padding: 1, borderStyle: 'round', borderColor: 'red' }) + '\n'); }
|
|
97
40
|
}
|
|
98
41
|
|
|
99
42
|
export async function forgotPasswordCommand(options = {}) {
|
|
100
43
|
let email = options.email;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
type: 'input',
|
|
105
|
-
name: 'email',
|
|
106
|
-
message: 'Email:',
|
|
107
|
-
validate: v => v.includes('@') ? true : 'Enter a valid email',
|
|
108
|
-
}]);
|
|
109
|
-
email = emailAnswer.email;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
await resetPassword(email);
|
|
114
|
-
console.log('\n' + boxen(
|
|
115
|
-
chalk.green('✔ Password reset email sent!') + '\n\n' +
|
|
116
|
-
chalk.white('Check ') + chalk.cyan(email) + chalk.white(' for a reset link.'),
|
|
117
|
-
{ padding: 1, borderStyle: 'round', borderColor: 'green', dimBorder: true }
|
|
118
|
-
) + '\n');
|
|
119
|
-
} catch (error) {
|
|
120
|
-
console.log('\n' + boxen(
|
|
121
|
-
chalk.red('✖ ' + error.message),
|
|
122
|
-
{ padding: 1, borderStyle: 'round', borderColor: 'red' }
|
|
123
|
-
) + '\n');
|
|
124
|
-
}
|
|
44
|
+
if (!email) email = (await inquirer.prompt([{ type: 'input', name: 'email', message: 'Email:', validate: v => v.includes('@') ? true : 'Enter a valid email' }])).email;
|
|
45
|
+
try { await resetPassword(email); console.log('\n' + boxen(chalk.green('✔ Password reset email sent!') + '\n\n' + chalk.white('Check ') + chalk.cyan(email) + chalk.white(' for a reset link.'), { padding: 1, borderStyle: 'round', borderColor: 'green', dimBorder: true }) + '\n'); }
|
|
46
|
+
catch (error) { console.log('\n' + boxen(chalk.red('✖ ' + error.message), { padding: 1, borderStyle: 'round', borderColor: 'red' }) + '\n'); }
|
|
125
47
|
}
|
|
126
48
|
|
|
127
|
-
export async function logoutCommand() {
|
|
128
|
-
await logout();
|
|
129
|
-
console.log('\n' + boxen(
|
|
130
|
-
chalk.green('✔ Logged out'),
|
|
131
|
-
{ padding: 1, borderStyle: 'round', borderColor: 'green', dimBorder: true }
|
|
132
|
-
) + '\n');
|
|
133
|
-
}
|
|
49
|
+
export async function logoutCommand() { await logout(); console.log('\n' + boxen(chalk.green('✔ Logged out'), { padding: 1, borderStyle: 'round', borderColor: 'green', dimBorder: true }) + '\n'); }
|
|
134
50
|
|
|
135
51
|
export async function deleteAccountCommand(options = {}) {
|
|
136
52
|
const session = await getSession();
|
|
137
|
-
if (!session) {
|
|
138
|
-
console.log('\n' + boxen(
|
|
139
|
-
chalk.red('✖ Not logged in. Run ') + chalk.cyan('memoir login') + chalk.red(' first.'),
|
|
140
|
-
{ padding: 1, borderStyle: 'round', borderColor: 'red' }
|
|
141
|
-
) + '\n');
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
53
|
+
if (!session) { console.log('\n' + boxen(chalk.red('✖ Not logged in. Run ') + chalk.cyan('memoir login') + chalk.red(' first.'), { padding: 1, borderStyle: 'round', borderColor: 'red' }) + '\n'); return; }
|
|
145
54
|
if (!options.confirm) {
|
|
146
|
-
const answer = await inquirer.prompt([{
|
|
147
|
-
|
|
148
|
-
name: 'confirmation',
|
|
149
|
-
message: 'Type DELETE to confirm account deletion:',
|
|
150
|
-
}]);
|
|
151
|
-
|
|
152
|
-
if (answer.confirmation !== 'DELETE') {
|
|
153
|
-
console.log('\n' + chalk.gray(' Account deletion cancelled.') + '\n');
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
55
|
+
const answer = await inquirer.prompt([{ type: 'input', name: 'confirmation', message: 'Type DELETE to remove your Memoir cloud data and sign out:' }]);
|
|
56
|
+
if (answer.confirmation !== 'DELETE') { console.log('\n' + chalk.gray(' Account-data deletion cancelled.') + '\n'); return; }
|
|
156
57
|
}
|
|
157
|
-
|
|
158
58
|
try {
|
|
159
59
|
await deleteAccount(session);
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
gradient.pastel(' memoir cloud ') + '\n\n' +
|
|
163
|
-
chalk.green('✔ Account deleted.') + '\n' +
|
|
164
|
-
chalk.gray('All backups, shared links, and data have been removed.'),
|
|
165
|
-
{ padding: 1, borderStyle: 'round', borderColor: 'green', dimBorder: true }
|
|
166
|
-
) + '\n');
|
|
167
|
-
} catch (error) {
|
|
168
|
-
console.log('\n' + boxen(
|
|
169
|
-
chalk.red('✖ ' + error.message),
|
|
170
|
-
{ padding: 1, borderStyle: 'round', borderColor: 'red' }
|
|
171
|
-
) + '\n');
|
|
172
|
-
}
|
|
60
|
+
console.log('\n' + boxen(gradient.pastel(' memoir cloud ') + '\n\n' + chalk.green('✔ Memoir cloud data deleted and this device signed out.') + '\n' + chalk.gray('Backups, shared links, and subscription data accessible to this account were removed.') + '\n\n' + chalk.yellow('Your authentication identity may still exist. Full auth-account removal requires the hosted account-deletion service.'), { padding: 1, borderStyle: 'round', borderColor: 'green', dimBorder: true }) + '\n');
|
|
61
|
+
} catch (error) { console.log('\n' + boxen(chalk.red('✖ Deletion incomplete: ' + error.message) + '\n' + chalk.gray('Retry the command. Memoir will not report success for an incomplete deletion.'), { padding: 1, borderStyle: 'round', borderColor: 'red' }) + '\n'); }
|
|
173
62
|
}
|
package/src/commands/upgrade.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { execFile } from 'child_process';
|
|
1
2
|
import chalk from 'chalk';
|
|
2
3
|
import boxen from 'boxen';
|
|
3
4
|
import gradient from 'gradient-string';
|
|
@@ -18,13 +19,10 @@ async function createCheckoutSession(session) {
|
|
|
18
19
|
return data.url;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
// No shell
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// being one. execFile passes the URL as a single argument; on Windows
|
|
25
|
-
// rundll32's URL handler avoids cmd.exe parsing `&` inside the query string.
|
|
22
|
+
// No shell interpolation: execFile passes the URL as a discrete argument
|
|
23
|
+
// array; on Windows rundll32's URL handler avoids cmd.exe parsing `&` inside
|
|
24
|
+
// the query string.
|
|
26
25
|
function openUrl(url) {
|
|
27
|
-
const { execFile } = require('child_process');
|
|
28
26
|
const platform = process.platform;
|
|
29
27
|
const [cmd, args] = platform === 'darwin' ? ['open', [url]]
|
|
30
28
|
: platform === 'win32' ? ['rundll32', ['url.dll,FileProtocolHandler', url]]
|