parrot-blackbox 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -20
- package/package.json +2 -2
- package/src/cli.js +81 -1
- package/src/commands/remote.js +137 -0
- package/src/commands/setup.js +84 -92
package/README.md
CHANGED
|
@@ -61,48 +61,93 @@ sudo apt install rclone timeshift git curl
|
|
|
61
61
|
|
|
62
62
|
---
|
|
63
63
|
|
|
64
|
-
## Install
|
|
64
|
+
## Install (streamlined)
|
|
65
65
|
|
|
66
66
|
```bash
|
|
67
67
|
npm install -g parrot-blackbox
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
Run the setup wizard:
|
|
70
|
+
Run the setup wizard — it does the whole install for you:
|
|
71
71
|
|
|
72
72
|
```bash
|
|
73
73
|
parrot-blackbox
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
**
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
76
|
+
**What the wizard does, start to finish:**
|
|
77
|
+
1. **Checks + auto-installs** `rclone`, `timeshift`, `git`, `curl` (interactive
|
|
78
|
+
sudo prompt, the gitswitch/theamify way).
|
|
79
|
+
2. **Adds your cloud accounts for you** — pick MEGA or Google Drive and enter
|
|
80
|
+
your email/password; parrot-blackbox creates the rclone remote and registers
|
|
81
|
+
it in the pool. No raw 68-option `rclone config` menu needed.
|
|
82
|
+
3. Confirms the **schedule** (snapshot every Saturday 22:00).
|
|
83
|
+
4. Installs the **always-on service** (systemd / cron).
|
|
84
|
+
5. Offers to run your **first snapshot** right away.
|
|
82
85
|
|
|
83
86
|
---
|
|
84
87
|
|
|
85
88
|
## Quick start (the important bit)
|
|
86
89
|
|
|
87
90
|
```bash
|
|
88
|
-
# 1. Install
|
|
91
|
+
# 1. Install + run the wizard (installs tools, adds accounts, first snapshot):
|
|
89
92
|
npm install -g parrot-blackbox
|
|
90
93
|
parrot-blackbox
|
|
91
94
|
|
|
92
|
-
# 2.
|
|
93
|
-
|
|
95
|
+
# 2. If you have MORE MEGA / Drive accounts, add them the same guided way:
|
|
96
|
+
parrot-blackbox remote add mega # prompts for email/password
|
|
97
|
+
parrot-blackbox remote add gdrive # opens browser OAuth
|
|
98
|
+
parrot-blackbox remote list # see remotes + pool registration
|
|
94
99
|
|
|
95
|
-
# 3.
|
|
96
|
-
|
|
97
|
-
parrot-blackbox account add gdrive my-drive-1
|
|
98
|
-
parrot-blackbox account list # see the whole pool + quota
|
|
99
|
-
|
|
100
|
-
# 4. Force your FIRST snapshot backup right now (do this before a fresh
|
|
101
|
-
# install — it captures the whole system and uploads it to the cloud):
|
|
100
|
+
# 3. Force your FIRST snapshot backup right now (or again later — it captures
|
|
101
|
+
# the whole system and uploads it to the cloud):
|
|
102
102
|
parrot-blackbox force
|
|
103
103
|
```
|
|
104
104
|
|
|
105
105
|
`parrot-blackbox status` shows the pool, network state, daemon state, last run
|
|
106
|
+
and pending backups.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Managing cloud accounts (rclone remotes)
|
|
111
|
+
|
|
112
|
+
Every MEGA / Google Drive login becomes one rclone remote = one pool account.
|
|
113
|
+
`parrot-blackbox` manages them for you — you never need to use the raw rclone
|
|
114
|
+
menu except for advanced edits.
|
|
115
|
+
|
|
116
|
+
**Add** (guided — creates the remote AND saves it in the pool):
|
|
117
|
+
```bash
|
|
118
|
+
parrot-blackbox remote add mega # prompts: name, email, password, 2FA (optional)
|
|
119
|
+
parrot-blackbox remote add gdrive # opens browser OAuth for Google
|
|
120
|
+
parrot-blackbox remote add mega mega-1 # or pass the name directly
|
|
121
|
+
# non-interactive (CI): PBB_MEGA_USER=me@x.com PBB_MEGA_PASS=secret parrot-blackbox remote add mega
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**List** (which remotes exist + are registered):
|
|
125
|
+
```bash
|
|
126
|
+
parrot-blackbox remote list
|
|
127
|
+
parrot-blackbox account list # pool usage & free space
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Edit / rename** any remote (advanced, full rclone editor):
|
|
131
|
+
```bash
|
|
132
|
+
parrot-blackbox remote config # pick the remote → e) Edit, r) Rename, c) Copy
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Delete** a remote entirely (removes the rclone entry AND its pool account):
|
|
136
|
+
```bash
|
|
137
|
+
parrot-blackbox remote remove <name>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Already configured rclone remotes?** (e.g. an old `mega:` you created long
|
|
141
|
+
ago) — register them into the pool without recreating:
|
|
142
|
+
```bash
|
|
143
|
+
parrot-blackbox account add mega mega # provider + existing remote name
|
|
144
|
+
parrot-blackbox account list
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
rclone stores secrets encrypted in `~/.config/rclone/rclone.conf`; parrot-blackbox
|
|
148
|
+
only ever stores the **remote name + provider + optional quota**, never your
|
|
149
|
+
passwords, and the allocator's manifests are safe to share.
|
|
150
|
+
|
|
106
151
|
---
|
|
107
152
|
|
|
108
153
|
## Commands
|
|
@@ -119,10 +164,14 @@ parrot-blackbox force
|
|
|
119
164
|
| `parrot-blackbox restore` | Interactive restore wizard `[sudo]` |
|
|
120
165
|
| `parrot-blackbox restore files <id> <dir>` | Recover a file backup into a folder |
|
|
121
166
|
| `parrot-blackbox restore snapshot <id> --yes` | Overwrite the whole system from a cloud snapshot `[sudo]` |
|
|
122
|
-
| `parrot-blackbox account add <mega\|gdrive> <remote>` |
|
|
167
|
+
| `parrot-blackbox account add <mega\|gdrive> <remote>` | Register an existing rclone remote into the pool |
|
|
123
168
|
| `parrot-blackbox account list` | Pool summary + per-account quota |
|
|
124
|
-
| `parrot-blackbox account remove <id>` | Remove an account |
|
|
169
|
+
| `parrot-blackbox account remove <id>` | Remove an account from the pool |
|
|
125
170
|
| `parrot-blackbox account quota <id> <GiB>` | Override an account's quota |
|
|
171
|
+
| `parrot-blackbox remote add <mega\|gdrive> [name]` | ⭐ Add a cloud account (guided — sets up rclone FOR you) |
|
|
172
|
+
| `parrot-blackbox remote list` | Show rclone remotes + pool registration |
|
|
173
|
+
| `parrot-blackbox remote remove <name>` | Delete a remote (rclone + pool) |
|
|
174
|
+
| `parrot-blackbox remote config` | Open the full rclone config editor (advanced) |
|
|
126
175
|
| `parrot-blackbox daemon start\|stop\|status` | Background automation |
|
|
127
176
|
| `parrot-blackbox schedule install\|remove` | systemd / cron always-on setup |
|
|
128
177
|
| `parrot-blackbox doctor` | Full diagnostics |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "parrot-blackbox",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "parrot-blackbox — crash-proof, multi-cloud backup & recovery automation for Parrot OS. Daily/weekly off-disk backups with automatic catch-up, smart storage across many MEGA + Google Drive accounts, Timeshift snapshot backups and one-command restore.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/cli.js",
|
|
@@ -55,4 +55,4 @@
|
|
|
55
55
|
"execa": "^8.0.1",
|
|
56
56
|
"picocolors": "^1.1.1"
|
|
57
57
|
}
|
|
58
|
-
}
|
|
58
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import pc from 'picocolors';
|
|
|
3
3
|
import * as p from '@clack/prompts';
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
5
|
import { runSetup } from './commands/setup.js';
|
|
6
|
+
import { guidedRemoteAdd, deleteRemote, registerRemotesAsAccounts, remoteStatus } from './commands/remote.js';
|
|
6
7
|
import { runDoctor, runStatus, runUninstallWizard } from './commands/manage.js';
|
|
7
8
|
import { installService, removeService } from './commands/service.js';
|
|
8
9
|
import { runDueJobs } from './daemon/scheduler.js';
|
|
@@ -43,10 +44,14 @@ ${pc.bold('Usage:')}
|
|
|
43
44
|
parrot-blackbox snapshot prune Delete snapshots beyond the keep limit ${pc.dim('[sudo]')}
|
|
44
45
|
parrot-blackbox list [files] List cloud file backups
|
|
45
46
|
parrot-blackbox restore Restore a snapshot or file backup ${pc.dim('[sudo]')}
|
|
46
|
-
parrot-blackbox account add Add a MEGA / Google Drive account
|
|
47
|
+
parrot-blackbox account add Add a MEGA / Google Drive account (remote must already exist)
|
|
47
48
|
parrot-blackbox account list Show the storage pool & usage
|
|
48
49
|
parrot-blackbox account remove <id> Remove an account from the pool
|
|
49
50
|
parrot-blackbox account quota <id> <GiB> Override an account quota
|
|
51
|
+
parrot-blackbox remote add <mega|gdrive> [name] ⭐ Add a cloud account (sets up rclone FOR you)
|
|
52
|
+
parrot-blackbox remote list Show rclone remotes + pool status
|
|
53
|
+
parrot-blackbox remote remove <name> Delete a remote AND drop it from the pool
|
|
54
|
+
parrot-blackbox remote config Open the full rclone config editor (advanced)
|
|
50
55
|
parrot-blackbox daemon start|stop|status Background automation
|
|
51
56
|
parrot-blackbox schedule install|remove systemd / cron always-on setup
|
|
52
57
|
parrot-blackbox doctor Full diagnostics
|
|
@@ -250,6 +255,77 @@ async function accountCommands(rest) {
|
|
|
250
255
|
}
|
|
251
256
|
}
|
|
252
257
|
|
|
258
|
+
/** Manage rclone remotes (the cloud logins) + their pool registration. */
|
|
259
|
+
async function remoteCommands(rest) {
|
|
260
|
+
const [sub, ...args] = rest;
|
|
261
|
+
switch (sub) {
|
|
262
|
+
case 'list':
|
|
263
|
+
case 'ls': {
|
|
264
|
+
const statuses = await remoteStatus();
|
|
265
|
+
if (statuses.length === 0) {
|
|
266
|
+
console.log(pc.yellow('\nNo rclone remotes configured yet. Add one with: parrot-blackbox remote add <mega|gdrive>\n'));
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
console.log(`\n${pc.bold('rclone remotes:')}`);
|
|
270
|
+
for (const s of statuses) {
|
|
271
|
+
console.log(` - ${pc.bold(s.name)}${s.registered ? pc.green(' ✔ registered in pool') : pc.dim(` not registered (provider: ${s.provider})`)}`);
|
|
272
|
+
}
|
|
273
|
+
console.log();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
case 'add': {
|
|
277
|
+
const provider = args[0];
|
|
278
|
+
if (provider && !['mega', 'gdrive'].includes(provider)) {
|
|
279
|
+
console.log(pc.yellow('provider must be mega or gdrive'));
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
let res;
|
|
283
|
+
if (!provider) {
|
|
284
|
+
// Interactive provider pick.
|
|
285
|
+
const choice = await p.select({
|
|
286
|
+
message: 'Provider?',
|
|
287
|
+
options: [
|
|
288
|
+
{ value: 'mega', label: 'MEGA (20 GB free tier)' },
|
|
289
|
+
{ value: 'gdrive', label: 'Google Drive (10 GB free tier)' },
|
|
290
|
+
],
|
|
291
|
+
});
|
|
292
|
+
if (p.isCancel(choice)) return;
|
|
293
|
+
res = await guidedRemoteAdd({ provider: choice, name: args[1], userArg: process.env.PBB_MEGA_USER || args[2], passArg: process.env.PBB_MEGA_PASS });
|
|
294
|
+
} else {
|
|
295
|
+
res = await guidedRemoteAdd({ provider, name: args[1], userArg: process.env.PBB_MEGA_USER || args[2], passArg: process.env.PBB_MEGA_PASS });
|
|
296
|
+
}
|
|
297
|
+
if (res.ok) console.log(pc.green(`✔ Added ${pc.bold(res.name)} (${res.provider}) — connected & registered.`));
|
|
298
|
+
else if (res.cancelled) console.log(pc.dim('Cancelled — nothing changed.'));
|
|
299
|
+
else console.log(pc.red(`✖ ${res.error}`));
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
case 'remove':
|
|
303
|
+
case 'rm': {
|
|
304
|
+
if (!args[0]) { console.log(pc.yellow('Usage: parrot-blackbox remote remove <remote-name>')); return; }
|
|
305
|
+
try {
|
|
306
|
+
await deleteRemote(args[0]);
|
|
307
|
+
console.log(pc.green(`✔ Removed remote ${args[0]} (rclone config + pool).`));
|
|
308
|
+
} catch (e) {
|
|
309
|
+
console.log(pc.red(`✖ ${e.message}`));
|
|
310
|
+
}
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
case 'config':
|
|
314
|
+
console.log(pc.dim('Opening rclone config — pick a remote, then e) Edit, d) Delete, r) Rename, c) Copy.'));
|
|
315
|
+
await import('execa').then(({ execa }) => execa('rclone', ['config'], { stdio: 'inherit' }));
|
|
316
|
+
return;
|
|
317
|
+
case 'register': {
|
|
318
|
+
// Hook so the wizard's multiselect is also reachable standalone.
|
|
319
|
+
console.log(pc.dim('Registering existing remotes into the pool…'));
|
|
320
|
+
if (!process.stdin.isTTY) console.log(pc.yellow('No interactive terminal — use `parrot-blackbox account add <provider> <remote>` instead.'));
|
|
321
|
+
else registerRemotesAsAccounts();
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
default:
|
|
325
|
+
console.log(pc.yellow('remote subcommands: add <mega|gdrive> [name] | list | remove <name> | config | register'));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
253
329
|
function writeConfigQuiet(cfg) {
|
|
254
330
|
saveConfig(cfg);
|
|
255
331
|
}
|
|
@@ -349,6 +425,10 @@ const main = defineCommand({
|
|
|
349
425
|
case 'accounts':
|
|
350
426
|
return accountCommands(rest);
|
|
351
427
|
|
|
428
|
+
case 'remote':
|
|
429
|
+
case 'remotes':
|
|
430
|
+
return remoteCommands(rest);
|
|
431
|
+
|
|
352
432
|
case 'daemon': {
|
|
353
433
|
const [sub] = rest;
|
|
354
434
|
if (sub === 'start') {
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote (cloud account) management — the streamlined path that creates the
|
|
3
|
+
* rclone remote FOR you (no 68-option rclone menu) and registers it in the
|
|
4
|
+
* parrot-blackbox pool in one step.
|
|
5
|
+
*
|
|
6
|
+
* - `remote add` → `rclone config create` / `reconnect`, then `addAccount`
|
|
7
|
+
* - `remote list` → rclone remotes + which are registered
|
|
8
|
+
* - `remote remove` → `rclone config delete` + drop from the pool
|
|
9
|
+
* - `remote config` → the full manual rclone config editor (advanced)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as p from '@clack/prompts';
|
|
13
|
+
import pc from 'picocolors';
|
|
14
|
+
import { execa } from 'execa';
|
|
15
|
+
import { listRemotes } from '../storage/rclone.js';
|
|
16
|
+
import { addAccount, removeAccount, listAccounts } from '../storage/accounts.js';
|
|
17
|
+
|
|
18
|
+
/** Create a MEGA remote non-interactively (password obscured, OAuth-free). */
|
|
19
|
+
export async function createMegaRemote({ name, user, pass, twofa = '' }) {
|
|
20
|
+
const obscured = await execa('rclone', ['obscure', pass], { reject: false });
|
|
21
|
+
if (obscured.exitCode !== 0) throw new Error('could not obscure the MEGA password');
|
|
22
|
+
const enc = obscured.stdout.trim();
|
|
23
|
+
const args = ['config', 'create', name, 'mega', 'user', user, 'pass', enc, 'use_https', 'true'];
|
|
24
|
+
if (twofa) args.push('2fa', twofa);
|
|
25
|
+
const res = await execa('rclone', args, { reject: false });
|
|
26
|
+
if (res.exitCode !== 0) throw new Error(`rclone config create failed: ${res.stderr?.trim()}`);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Create a Google Drive remote non-interactively, then trigger the OAuth browser flow. */
|
|
31
|
+
export async function createGdriveRemote(name) {
|
|
32
|
+
const c = await execa('rclone', ['config', 'create', name, 'drive', 'scope', 'drive'], { reject: false });
|
|
33
|
+
if (c.exitCode !== 0) throw new Error(`rclone config create failed: ${c.stderr?.trim()}`);
|
|
34
|
+
p.log.step('Open the browser to authorize Google Drive when it appears…');
|
|
35
|
+
const r = await execa('rclone', ['config', 'reconnect', `${name}:`], { stdio: 'inherit', reject: false });
|
|
36
|
+
if (r.exitCode !== 0) throw new Error(`Google Drive authorization failed: ${r.stderr?.trim()}`);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Delete an rclone remote and drop it from the pool. */
|
|
41
|
+
export async function deleteRemote(name) {
|
|
42
|
+
const res = await execa('rclone', ['config', 'delete', name], { reject: false });
|
|
43
|
+
if (res.exitCode !== 0) {
|
|
44
|
+
throw new Error(`rclone config delete failed: ${res.stderr?.trim()}`);
|
|
45
|
+
}
|
|
46
|
+
removeAccount(name);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Guided account add (used by `remote add` and the wizard).
|
|
52
|
+
* When stdin is not a TTY, reads credentials from args/env so it can be scripted.
|
|
53
|
+
*/
|
|
54
|
+
export async function guidedRemoteAdd({ provider, name, userArg, passArg }) {
|
|
55
|
+
if (!['mega', 'gdrive'].includes(provider)) {
|
|
56
|
+
return { ok: false, cancelled: false, error: `unknown provider "${provider}" — use mega or gdrive` };
|
|
57
|
+
}
|
|
58
|
+
if (!name) {
|
|
59
|
+
const existing = listAccounts().length;
|
|
60
|
+
const n = provider === 'mega' ? `mega-${existing + 1}` : `gdrive-${existing + 1}`;
|
|
61
|
+
name = process.stdin.isTTY ? await p.text({ message: `rclone remote name (${provider}):`, initialValue: n }) : n;
|
|
62
|
+
if (p.isCancel(name)) return { ok: false, cancelled: true };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (provider === 'mega') {
|
|
66
|
+
const user = userArg || (process.stdin.isTTY ? await p.text({ message: 'MEGA account email:', initialValue: '' }) : process.env.PBB_MEGA_USER);
|
|
67
|
+
if (!user) return { ok: false, cancelled: false, error: 'a MEGA email is required (pass it as an argument or run interactively)' };
|
|
68
|
+
if (p.isCancel(user)) return { ok: false, cancelled: true };
|
|
69
|
+
const pass = passArg || (process.stdin.isTTY ? await p.password({ message: 'MEGA password:' }) : process.env.PBB_MEGA_PASS);
|
|
70
|
+
if (!pass) return { ok: false, cancelled: false, error: 'a MEGA password is required' };
|
|
71
|
+
if (p.isCancel(pass)) return { ok: false, cancelled: true };
|
|
72
|
+
let twofa = process.env.PBB_MEGA_2FA || '';
|
|
73
|
+
if (!twofa && process.stdin.isTTY) {
|
|
74
|
+
const t = await p.text({ message: 'MEGA 2FA code (leave empty if none):', initialValue: '' });
|
|
75
|
+
if (p.isCancel(t)) return { ok: false, cancelled: true };
|
|
76
|
+
twofa = t || '';
|
|
77
|
+
}
|
|
78
|
+
await createMegaRemote({ name, user, pass, twofa });
|
|
79
|
+
} else {
|
|
80
|
+
await createGdriveRemote(name);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const res = await addAccount({ provider, remote: name });
|
|
84
|
+
if (!res.ok) {
|
|
85
|
+
// Remote exists in rclone but could not be registered — say why (e.g. dup).
|
|
86
|
+
return { ok: false, cancelled: false, error: res.error };
|
|
87
|
+
}
|
|
88
|
+
return { ok: true, cancelled: false, name, provider };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Register rclone remotes that already exist as pool accounts (wizard multiselect). */
|
|
92
|
+
export async function registerRemotesAsAccounts() {
|
|
93
|
+
const remotes = await listRemotes();
|
|
94
|
+
if (remotes.length === 0) {
|
|
95
|
+
p.log.warn('No rclone remotes found yet. Create one with `parrot-blackbox remote add`, `rclone config`, then re-run setup.');
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const existing = new Set(listAccounts().map((a) => a.remote));
|
|
99
|
+
const candidates = remotes.filter((r) => !existing.has(r));
|
|
100
|
+
if (candidates.length === 0) {
|
|
101
|
+
p.log.message(pc.dim('All existing remotes are already registered in the pool.'));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const toAdd = await p.multiselect({
|
|
105
|
+
message: 'Select remotes to add to the backup pool:',
|
|
106
|
+
options: candidates.map((r) => ({ value: r, label: r })),
|
|
107
|
+
required: false,
|
|
108
|
+
});
|
|
109
|
+
if (p.isCancel(toAdd) || toAdd.length === 0) {
|
|
110
|
+
p.log.message(pc.dim('No remotes added.'));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
for (const remote of toAdd) {
|
|
114
|
+
const provider = await p.select({
|
|
115
|
+
message: `Provider for "${remote}"?`,
|
|
116
|
+
options: [
|
|
117
|
+
{ value: 'mega', label: 'MEGA (20 GB free tier)' },
|
|
118
|
+
{ value: 'gdrive', label: 'Google Drive (10 GB free tier)' },
|
|
119
|
+
],
|
|
120
|
+
});
|
|
121
|
+
if (p.isCancel(provider)) continue;
|
|
122
|
+
const res = await addAccount({ provider, remote });
|
|
123
|
+
if (res.ok) p.log.success(`Added ${pc.cyan(remote)} (${provider}).`);
|
|
124
|
+
else p.log.warn(res.error);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Remotes we know about + registration status. */
|
|
129
|
+
export async function remoteStatus() {
|
|
130
|
+
const remotes = await listRemotes();
|
|
131
|
+
const accounts = listAccounts();
|
|
132
|
+
return remotes.map((r) => ({
|
|
133
|
+
name: r,
|
|
134
|
+
provider: accounts.find((a) => a.remote === r)?.provider || '—',
|
|
135
|
+
registered: accounts.some((a) => a.remote === r),
|
|
136
|
+
}));
|
|
137
|
+
}
|
package/src/commands/setup.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Interactive setup wizard (default command, like gitswitch/theamify).
|
|
3
|
-
* Walks:
|
|
4
|
-
*
|
|
3
|
+
* Walks: tools check + AUTO-INSTALL → storage pool (guided MEGA/Drive remote
|
|
4
|
+
* creation, no raw 68-option rclone menus) → schedule → always-on service →
|
|
5
|
+
* optional first snapshot.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import * as p from '@clack/prompts';
|
|
8
9
|
import pc from 'picocolors';
|
|
9
10
|
import { execa } from 'execa';
|
|
10
11
|
import { loadConfig, journal, hasCommandSync } from '../core/store.js';
|
|
11
|
-
import {
|
|
12
|
-
import { addAccount, listAccounts, refreshAccounts, poolSummary } from '../storage/accounts.js';
|
|
12
|
+
import { listAccounts, refreshAccounts, poolSummary } from '../storage/accounts.js';
|
|
13
13
|
import { installService } from './service.js';
|
|
14
14
|
import { runDueJobs } from '../daemon/scheduler.js';
|
|
15
15
|
import { isOnline } from '../util/network.js';
|
|
16
16
|
import { bytesHuman } from '../util/misc.js';
|
|
17
|
+
import { guidedRemoteAdd, registerRemotesAsAccounts } from './remote.js';
|
|
17
18
|
|
|
18
19
|
const REQUIRED = [
|
|
19
20
|
{ bin: 'rclone', pkg: 'rclone', why: 'talks to MEGA / Google Drive (cloud storage)' },
|
|
@@ -28,12 +29,7 @@ function detectPackageManager() {
|
|
|
28
29
|
return order.find((name) => hasCommandSync(name)) || null;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
/**
|
|
32
|
-
* Auto-install the tools the system needs for snapshot backup & restore.
|
|
33
|
-
* Prompts per missing tool, then runs the package-manager install with an
|
|
34
|
-
* interactive sudo prompt (spinner released first, Ctrl+C safe).
|
|
35
|
-
* @returns {Promise<string[]>} tools that were freshly installed
|
|
36
|
-
*/
|
|
32
|
+
/** Auto-install the tools needed for snapshot backup & restore (theamify-style). */
|
|
37
33
|
async function ensureSystemTools() {
|
|
38
34
|
const missing = REQUIRED.filter((t) => !hasCommandSync(t.bin));
|
|
39
35
|
if (missing.length === 0) return [];
|
|
@@ -59,7 +55,6 @@ async function ensureSystemTools() {
|
|
|
59
55
|
p.log.step(`Installing ${pc.cyan(tool.bin)}…`);
|
|
60
56
|
const s = p.spinner();
|
|
61
57
|
s.start(`Installing ${tool.bin}…`);
|
|
62
|
-
// Release the spinner FIRST so the sudo password prompt is visible & interruptible.
|
|
63
58
|
s.stop('');
|
|
64
59
|
try {
|
|
65
60
|
const args = pm === 'pacman' ? ['-S', '--noconfirm', tool.pkg] : [pm, 'install', '-y', tool.pkg];
|
|
@@ -77,58 +72,55 @@ async function ensureSystemTools() {
|
|
|
77
72
|
}
|
|
78
73
|
return installed;
|
|
79
74
|
}
|
|
80
|
-
export async function runSetup() {
|
|
81
|
-
p.intro(pc.bgYellow(pc.black(' parrot-blackbox setup ')));
|
|
82
75
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
p.note('Checking & installing the tools needed for snapshot backup + restore…', 'Step 1/5 — tools');
|
|
86
|
-
const missing = REQUIRED.filter((t) => !hasCommandSync(t.bin));
|
|
87
|
-
if (missing.length === 0) {
|
|
88
|
-
p.log.success(`All tools present: ${REQUIRED.map((r) => r.bin).join(', ')}`);
|
|
89
|
-
} else {
|
|
90
|
-
const installed = await ensureSystemTools();
|
|
91
|
-
const stillMissing = REQUIRED.filter((t) => !hasCommandSync(t.bin));
|
|
92
|
-
if (installed.length) p.log.success(`Installed: ${installed.join(', ')}`);
|
|
93
|
-
if (stillMissing.length) {
|
|
94
|
-
p.log.warn(`Still missing: ${stillMissing.map((m) => m.bin).join(', ')}`);
|
|
95
|
-
} else {
|
|
96
|
-
p.log.success(`All required tools now present: ${REQUIRED.map((r) => r.bin).join(', ')}`);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
if (!hasCommandSync('timeshift')) {
|
|
100
|
-
p.log.message(pc.dim('Timeshift missing = snapshot backup & restore are unavailable. Run `parrot-blackbox` again after installing it.'));
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// 2. Accounts
|
|
104
|
-
p.note('Accounts are rclone remotes — one per MEGA or Google Drive login.', 'Step 2/5 — storage pool');
|
|
76
|
+
/** Wizard step: connect cloud accounts (guided or manual). */
|
|
77
|
+
async function storagePoolStep() {
|
|
105
78
|
const existing = listAccounts();
|
|
106
|
-
|
|
79
|
+
p.note(
|
|
80
|
+
'One MEGA or Google Drive login = one rclone remote = one pool account.',
|
|
81
|
+
'Step 2/5 — storage pool',
|
|
82
|
+
);
|
|
83
|
+
const action = await p.select({
|
|
107
84
|
message: existing.length
|
|
108
|
-
? `You already have ${existing.length} account(s).
|
|
109
|
-
: 'No accounts yet —
|
|
110
|
-
|
|
85
|
+
? `You already have ${existing.length} account(s). What next?`
|
|
86
|
+
: 'No accounts yet — how do you want to add your first cloud account?',
|
|
87
|
+
options: [
|
|
88
|
+
{ value: 'guided', label: 'Add a cloud account (guided — parrot-blackbox sets up rclone for you)', hint: 'recommended' },
|
|
89
|
+
{ value: 'manual', label: 'Configure rclone myself, then register the remote' },
|
|
90
|
+
existing.length ? { value: 'skip', label: 'Skip — accounts look fine' } : { value: 'skip', label: 'Skip for now' },
|
|
91
|
+
],
|
|
111
92
|
});
|
|
112
|
-
if (p.isCancel(
|
|
113
|
-
|
|
114
|
-
if (needMore) {
|
|
115
|
-
const want = await p.select({
|
|
116
|
-
message: 'How do you want to add the account?',
|
|
117
|
-
options: [
|
|
118
|
-
{ value: 'wizard', label: 'Run `rclone config` (recommended — handles MEGA + Google OAuth)', hint: 'authenticates in your browser' },
|
|
119
|
-
{ value: 'manual', label: 'I already created the rclone remote myself' },
|
|
120
|
-
],
|
|
121
|
-
});
|
|
122
|
-
if (p.isCancel(want)) { p.cancel('Aborted.'); process.exit(0); }
|
|
93
|
+
if (p.isCancel(action)) { p.cancel('Aborted.'); process.exit(0); }
|
|
123
94
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
95
|
+
if (action === 'guided') {
|
|
96
|
+
const wantMore = true;
|
|
97
|
+
while (wantMore) {
|
|
98
|
+
const provider = await p.select({
|
|
99
|
+
message: 'Which provider?',
|
|
100
|
+
options: [
|
|
101
|
+
{ value: 'mega', label: 'MEGA (20 GB free tier)' },
|
|
102
|
+
{ value: 'gdrive', label: 'Google Drive (10 GB free tier)' },
|
|
103
|
+
],
|
|
104
|
+
});
|
|
105
|
+
if (p.isCancel(provider)) break;
|
|
106
|
+
const res = await guidedRemoteAdd({ provider });
|
|
107
|
+
if (res.ok) p.log.success(`✔ ${pc.bold(res.name)} (${res.provider}) added to the pool.`);
|
|
108
|
+
else if (res.error) p.log.warn(res.error);
|
|
109
|
+
if (res.cancelled) break;
|
|
110
|
+
const another = await p.confirm({ message: 'Add another account?', initialValue: true });
|
|
111
|
+
if (p.isCancel(another) || !another) break;
|
|
127
112
|
}
|
|
128
|
-
|
|
113
|
+
} else if (action === 'manual') {
|
|
114
|
+
p.log.step('Starting rclone config — choose 39 (Mega) or 24 (Google Drive). Then come back.');
|
|
115
|
+
await execa('rclone', ['config'], { stdio: 'inherit' });
|
|
116
|
+
await registerRemotesAsAccounts();
|
|
117
|
+
} else {
|
|
118
|
+
p.log.message(pc.dim('Carrying on with the accounts you have.'));
|
|
129
119
|
}
|
|
120
|
+
}
|
|
130
121
|
|
|
131
|
-
|
|
122
|
+
/** Wizard step: schedule overview. */
|
|
123
|
+
async function scheduleStep() {
|
|
132
124
|
const cfg = loadConfig();
|
|
133
125
|
p.note(
|
|
134
126
|
` Snapshot backup: ${pc.bold('every Saturday at 22:00')} (keep ${cfg.jobs.snapshots.keep}, local + cloud)\n` +
|
|
@@ -136,8 +128,10 @@ export async function runSetup() {
|
|
|
136
128
|
` Missed backups : caught up automatically in order when WiFi returns`,
|
|
137
129
|
'Step 3/5 — schedule',
|
|
138
130
|
);
|
|
131
|
+
}
|
|
139
132
|
|
|
140
|
-
|
|
133
|
+
/** Wizard step: install the always-on service. */
|
|
134
|
+
async function serviceStep() {
|
|
141
135
|
const install = await p.confirm({
|
|
142
136
|
message: 'Install the always-on background service (systemd / cron fallback)?',
|
|
143
137
|
initialValue: true,
|
|
@@ -148,47 +142,16 @@ export async function runSetup() {
|
|
|
148
142
|
p.log.success(`Always-on service installed via ${pc.cyan(backend)}.`);
|
|
149
143
|
p.log.message(pc.dim('It survives reboots — a missed Saturday 22:00 run fires as soon as the machine is back online.'));
|
|
150
144
|
}
|
|
145
|
+
}
|
|
151
146
|
|
|
152
|
-
|
|
147
|
+
/** Wizard step: first snapshot now? */
|
|
148
|
+
async function firstBackupStep() {
|
|
153
149
|
const first = await p.confirm({
|
|
154
150
|
message: 'Run the FIRST snapshot backup right now? (recommended before a fresh install)',
|
|
155
151
|
initialValue: true,
|
|
156
152
|
});
|
|
157
153
|
if (p.isCancel(first)) { p.cancel('Aborted.'); process.exit(0); }
|
|
158
154
|
if (first) {
|
|
159
|
-
/** Register discovered rclone remotes as accounts (multi-select). */
|
|
160
|
-
async function registerAccountsFlow() {
|
|
161
|
-
const remotes = await listRemotes();
|
|
162
|
-
if (remotes.length === 0) {
|
|
163
|
-
p.log.warn('No rclone remotes found yet. Create one with `rclone config` or re-run setup.');
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
const toAdd = await p.multiselect({
|
|
167
|
-
message: 'Select which rclone remotes to add to the backup pool (one per account):',
|
|
168
|
-
options: remotes.map((r) => ({ value: r, label: r })),
|
|
169
|
-
required: false,
|
|
170
|
-
});
|
|
171
|
-
if (p.isCancel(toAdd) || toAdd.length === 0) {
|
|
172
|
-
p.log.message(pc.dim('No accounts added.'));
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
for (const remote of toAdd) {
|
|
176
|
-
const provider = await p.select({
|
|
177
|
-
message: `Provider for "${remote}"?`,
|
|
178
|
-
options: [
|
|
179
|
-
{ value: 'mega', label: 'MEGA (20 GB free tier)' },
|
|
180
|
-
{ value: 'gdrive', label: 'Google Drive (10 GB free tier)' },
|
|
181
|
-
],
|
|
182
|
-
});
|
|
183
|
-
if (p.isCancel(provider)) continue;
|
|
184
|
-
const res = await addAccount({ provider, remote });
|
|
185
|
-
if (res.ok) p.log.success(`Added ${pc.cyan(remote)} (${provider}).`);
|
|
186
|
-
else p.log.warn(res.error);
|
|
187
|
-
}
|
|
188
|
-
const accounts = await refreshAccounts();
|
|
189
|
-
p.log.success(poolSummary(accounts).text);
|
|
190
|
-
journal('setup', `accounts registered: ${accounts.map((a) => a.remote).join(',')}`);
|
|
191
|
-
}
|
|
192
155
|
if (!(await isOnline())) {
|
|
193
156
|
p.log.warn('Offline right now — the backup stays pending and will run automatically when online.');
|
|
194
157
|
} else {
|
|
@@ -209,6 +172,35 @@ async function registerAccountsFlow() {
|
|
|
209
172
|
}
|
|
210
173
|
}
|
|
211
174
|
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function runSetup() {
|
|
178
|
+
p.intro(pc.bgYellow(pc.black(' parrot-blackbox setup ')));
|
|
179
|
+
|
|
180
|
+
// 1. Dependencies — check the system and INSTALL anything missing.
|
|
181
|
+
p.note('Checking & installing the tools needed for snapshot backup + restore…', 'Step 1/5 — tools');
|
|
182
|
+
const missing = REQUIRED.filter((t) => !hasCommandSync(t.bin));
|
|
183
|
+
if (missing.length === 0) {
|
|
184
|
+
p.log.success(`All tools present: ${REQUIRED.map((r) => r.bin).join(', ')}`);
|
|
185
|
+
} else {
|
|
186
|
+
const installed = await ensureSystemTools();
|
|
187
|
+
const stillMissing = REQUIRED.filter((t) => !hasCommandSync(t.bin));
|
|
188
|
+
if (installed.length) p.log.success(`Installed: ${installed.join(', ')}`);
|
|
189
|
+
p.log[stillMissing.length ? 'warn' : 'success'](stillMissing.length
|
|
190
|
+
? `Still missing: ${stillMissing.map((m) => m.bin).join(', ')}`
|
|
191
|
+
: `All required tools now present: ${REQUIRED.map((r) => r.bin).join(', ')}`);
|
|
192
|
+
}
|
|
212
193
|
|
|
213
|
-
|
|
214
|
-
|
|
194
|
+
// 2. Storage pool (guided remote creation or manual registration).
|
|
195
|
+
await storagePoolStep();
|
|
196
|
+
const accs = await refreshAccounts();
|
|
197
|
+
if (accs.length) p.log.success(poolSummary(accs).text);
|
|
198
|
+
|
|
199
|
+
// 3–5. Schedule, service, first backup.
|
|
200
|
+
await scheduleStep();
|
|
201
|
+
await serviceStep();
|
|
202
|
+
await firstBackupStep();
|
|
203
|
+
|
|
204
|
+
journal('setup', 'wizard completed');
|
|
205
|
+
p.outro(pc.green('Setup complete. Run `parrot-blackbox status` next, and `parrot-blackbox remote list` to manage accounts.'));
|
|
206
|
+
}
|