parrot-blackbox 1.0.0 → 1.0.2
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 +318 -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,342 @@ 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
|
+
There are **two ways** to connect them:
|
|
114
|
+
|
|
115
|
+
- **Guided (recommended):** `parrot-blackbox` creates the rclone remote *for
|
|
116
|
+
you* — no menus. `parrot-blackbox remote add mega` prompts for the remote
|
|
117
|
+
name, email, password and optional 2FA, then registers it.
|
|
118
|
+
- **Manual:** configure remote(s) yourself with the raw `rclone config` — the
|
|
119
|
+
**complete step-by-step walkthrough of every prompt and every letter is
|
|
120
|
+
documented below** in the next section.
|
|
121
|
+
|
|
122
|
+
**Add** (guided — creates the remote AND saves it in the pool):
|
|
123
|
+
```bash
|
|
124
|
+
parrot-blackbox remote add mega # prompts: name, email, password, 2FA (optional)
|
|
125
|
+
parrot-blackbox remote add gdrive # opens browser OAuth for Google
|
|
126
|
+
parrot-blackbox remote add mega mega-1 # or pass the name directly
|
|
127
|
+
# non-interactive (CI): PBB_MEGA_USER=me@x.com PBB_MEGA_PASS=secret parrot-blackbox remote add mega
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**List** (which remotes exist + are registered):
|
|
131
|
+
```bash
|
|
132
|
+
parrot-blackbox remote list
|
|
133
|
+
parrot-blackbox account list # pool usage & free space
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Edit / rename** any remote (advanced, full rclone editor):
|
|
137
|
+
```bash
|
|
138
|
+
parrot-blackbox remote config # pick the remote → e) Edit, r) Rename, c) Copy
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Delete** a remote entirely (removes the rclone entry AND its pool account):
|
|
142
|
+
```bash
|
|
143
|
+
parrot-blackbox remote remove <name>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Already configured rclone remotes?** (e.g. an old `mega:` you created long
|
|
147
|
+
ago) — register them into the pool without recreating:
|
|
148
|
+
```bash
|
|
149
|
+
parrot-blackbox account add mega mega # provider + existing remote name
|
|
150
|
+
parrot-blackbox account list
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
rclone stores secrets encrypted in `~/.config/rclone/rclone.conf`; parrot-blackbox
|
|
154
|
+
only ever stores the **remote name + provider + optional quota**, never your
|
|
155
|
+
passwords, and the allocator's manifests are safe to share.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## 📖 The full `rclone config` manual walkthrough (every letter, every prompt)
|
|
160
|
+
|
|
161
|
+
> The wizard's *"Configure rclone myself, then register the remote"* path (and
|
|
162
|
+
> the `parrot-blackbox remote config` command) open rclone's interactive menu.
|
|
163
|
+
> This is that menu, explained **prompt by prompt** so you never get lost.
|
|
164
|
+
> The quick alternative that skips all of this: `parrot-blackbox remote add mega`
|
|
165
|
+
> (or `gdrive`) creates the remote for you automatically.
|
|
166
|
+
|
|
167
|
+
### 1. The main menu — what each letter does
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
Current remotes:
|
|
171
|
+
|
|
172
|
+
Name Type
|
|
173
|
+
==== ====
|
|
174
|
+
mega mega
|
|
175
|
+
|
|
176
|
+
e) Edit existing remote
|
|
177
|
+
n) New remote
|
|
178
|
+
d) Delete remote
|
|
179
|
+
r) Rename remote
|
|
180
|
+
c) Copy remote
|
|
181
|
+
s) Set configuration password
|
|
182
|
+
q) Quit config
|
|
183
|
+
e/n/d/r/c/s/q>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
| Letter | Word | What it does |
|
|
187
|
+
|---|---|---|
|
|
188
|
+
| `e` | **Edit** | Change an **existing** remote's login (email / password / 2FA) or storage type |
|
|
189
|
+
| `n` | **New** | **Create a brand-new remote** → the flow below |
|
|
190
|
+
| `d` | **Delete** | Remove a remote from the config — rclone asks `y` to confirm |
|
|
191
|
+
| `r` | **Rename** | Give an existing remote a different name |
|
|
192
|
+
| `c` | **Copy** | Duplicate an existing remote under a new name (handy for a 2nd account of the same type) |
|
|
193
|
+
| `s` | **Set config password** | Lock the whole `rclone.conf` with a passphrase (optional; skip unless you want it) |
|
|
194
|
+
| `q` | **Quit** | Leave the menu → you return to parrot-blackbox |
|
|
195
|
+
|
|
196
|
+
> **The letters you'll use most:** `n` (new remote), `e` (fix a wrong password),
|
|
197
|
+
> `d` (remove an account you don't use), `q` (done).
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
### 2. Adding a MEGA remote (storage number **39**)
|
|
202
|
+
|
|
203
|
+
Start the menu with `n`, then follow exactly:
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
e/n/d/r/c/s/q> n ⬅ type the letter n and press Enter
|
|
207
|
+
Enter name for new remote.
|
|
208
|
+
name> mega-account-1 ⬅ type the remote name and press Enter
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
> **What the name is:** a label for this login. Use something meaningful per
|
|
212
|
+
> account, e.g. `mega-account-1`, `mega-2`, `mega-main`. You'll type this exact
|
|
213
|
+
> name into parrot-blackbox later.
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
Option Storage.
|
|
217
|
+
Type of storage to configure.
|
|
218
|
+
...long numbered list of 68 providers...
|
|
219
|
+
Storage> 39 ⬅ type 39 for Mega and press Enter
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
> **The storage number for each provider:**
|
|
223
|
+
> | Provider | Number to type | (what it says in the list) |
|
|
224
|
+
> |---|---|---|
|
|
225
|
+
> | **Mega** (the one you use) | **`39`** | `39 / Mega - Tier 2 \ (mega)` |
|
|
226
|
+
> | **Google Drive** | **`24`** | `24 / Google Drive - Tier 1 \ (drive)` |
|
|
227
|
+
|
|
228
|
+
Next rclone asks for login details — this means **the email + password of the
|
|
229
|
+
cloud account** you want to back up to:
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
Option user.
|
|
233
|
+
User name.
|
|
234
|
+
Enter a value.
|
|
235
|
+
user> info.donartkins.ke@gmail.com ⬅ type the account's EMAIL and press Enter
|
|
236
|
+
|
|
237
|
+
Option pass.
|
|
238
|
+
Password.
|
|
239
|
+
Choose an alternative below.
|
|
240
|
+
y) Yes, type in my own password
|
|
241
|
+
g) Generate random password
|
|
242
|
+
y/g> y ⬅ type y to enter your own password
|
|
243
|
+
|
|
244
|
+
Enter the password:
|
|
245
|
+
password: ******** ⬅ type the account PASSWORD (invisible)
|
|
246
|
+
Confirm the password:
|
|
247
|
+
password: ******** ⬅ type it again to confirm
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
> **What this means:** `user` = the **email address** of your MEGA account.
|
|
251
|
+
> `pass` = that account's **password**. rclone encrypts it before saving it —
|
|
252
|
+
> you'll see `*** ENCRYPTED ***` later.
|
|
253
|
+
|
|
254
|
+
After the password, optional but present for accounts that have it:
|
|
255
|
+
|
|
256
|
+
```
|
|
257
|
+
Option 2fa.
|
|
258
|
+
The 2FA code of your MEGA account if the account is set up with one
|
|
259
|
+
Enter a value. Press Enter to leave empty.
|
|
260
|
+
2fa> ⬅ if you use 2FA on this MEGA account, enter the
|
|
261
|
+
current 6-digit code here; if not, press Enter
|
|
262
|
+
```
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
### 3. Advanced config — ⚠️ **SAY NO** (this is the mistake you kept making)
|
|
266
|
+
|
|
267
|
+
rclone then asks `Edit advanced config?` **twice**. You almost always want
|
|
268
|
+
**`n` (No)** here — parrot-blackbox works fine with the defaults:
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
Edit advanced config?
|
|
272
|
+
y) Yes
|
|
273
|
+
n) No (default)
|
|
274
|
+
y/n> n ⬅ type n and press Enter ✅ SAY NO
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
If you mistakenly choose `y`, rclone will march you through `debug`,
|
|
278
|
+
`hard_delete`, `use_https`, `encoding`, `description`… — you can press **Enter**
|
|
279
|
+
on every single one to accept defaults, then it asks `Edit advanced config?`
|
|
280
|
+
again → answer `n` the second time. But the easy path is simply **`n` the
|
|
281
|
+
first time**.
|
|
282
|
+
|
|
283
|
+
When it finishes you get a confirmation:
|
|
284
|
+
|
|
285
|
+
```
|
|
286
|
+
Configuration complete.
|
|
287
|
+
Options:
|
|
288
|
+
- type: mega
|
|
289
|
+
- user: info.donartkins.ke@gmail.com
|
|
290
|
+
- pass: *** ENCRYPTED ***
|
|
291
|
+
Keep this "mega-account-1" remote?
|
|
292
|
+
y) Yes this is OK (default)
|
|
293
|
+
e) Edit this remote
|
|
294
|
+
d) Delete this remote
|
|
295
|
+
y/e/d> y ⬅ type y to save this remote
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
> **Important:** this confirmation `y` ("keep this remote") is DIFFERENT from
|
|
299
|
+
> the advanced-config prompt. Here you DO want `y` — it saves the remote you
|
|
300
|
+
> just created.
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
### 4. Adding a Google Drive remote (storage number **24**)
|
|
305
|
+
|
|
306
|
+
Same `n` flow, but the two differences are the storage number and the auth step:
|
|
307
|
+
|
|
308
|
+
```
|
|
309
|
+
e/n/d/r/c/s/q> n ⬅ n for new
|
|
310
|
+
name> gdrive-account-1 ⬅ the remote name
|
|
311
|
+
Storage> 24 ⬅ 24 = Google Drive
|
|
312
|
+
|
|
313
|
+
-- rclone then prints a big "Use auto config?" section --
|
|
314
|
+
Use auto config?
|
|
315
|
+
y) Yes
|
|
316
|
+
n) No
|
|
317
|
+
y/n> y ⬅ y (opens your browser automatically)
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
rclone opens a **browser page asking you to sign in to Google** and grant
|
|
321
|
+
access — log in, allow, done. Back in the terminal:
|
|
322
|
+
|
|
323
|
+
```
|
|
324
|
+
Edit advanced config?
|
|
325
|
+
y) Yes
|
|
326
|
+
n) No (default)
|
|
327
|
+
y/n> n ⬅ ✅ NO
|
|
328
|
+
|
|
329
|
+
Keep this "gdrive-account-1" remote?
|
|
330
|
+
y) Yes this is OK (default)
|
|
331
|
+
y/e/d> y ⬅ y to keep it
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
> If the browser doesn't open (headless), choose the manual option rclone gives
|
|
335
|
+
> you and paste the printed verification code into `http://localhost:53682` —
|
|
336
|
+
> but on your desktop the browser usually just opens.
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
### 5. After you quit (`q`) — register the remotes
|
|
341
|
+
|
|
342
|
+
You arrive back in parrot-blackbox with the message
|
|
343
|
+
`Starting rclone config — follow its prompts, then come back.` Once rclone's
|
|
344
|
+
menu is closed (`q`), the wizard automatically shows the remotes it found and
|
|
345
|
+
lets you pick which to register into the pool:
|
|
346
|
+
|
|
347
|
+
```
|
|
348
|
+
Select which rclone remotes to add to the backup pool: ⬅ tick your new names
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Each one is then tagged with its provider as you type it into rclone. If you
|
|
352
|
+
quit the wizard before registering, that's fine — either re-run
|
|
353
|
+
`parrot-blackbox` or:
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
parrot-blackbox account add mega mega-account-1 # provider + the remote name you chose in rclone
|
|
357
|
+
parrot-blackbox remote list # confirm it's now registered
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
### 6. Editing / renaming / deleting a remote later (the `e` `r` `d` letters)
|
|
363
|
+
|
|
364
|
+
Fix a wrong password, rename, or delete an old account:
|
|
365
|
+
|
|
366
|
+
```
|
|
367
|
+
e/n/d/r/c/s/q> e ⬅ e = Edit an existing remote
|
|
368
|
+
Choose a number from below ... ⬅ pick the remote to edit
|
|
369
|
+
Edit remote
|
|
370
|
+
u) Update current
|
|
371
|
+
y) Yes type in my own password
|
|
372
|
+
g) Generate random password
|
|
373
|
+
q) Quit config
|
|
374
|
+
u> y ⬅ choose what to change (u = update this field)
|
|
375
|
+
Name or type of remote > user ⬅ which field: user, pass, 2fa, type, ...
|
|
376
|
+
user> new@email.com ⬅ the new value
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Rename:
|
|
380
|
+
```
|
|
381
|
+
e/n/d/r/c/s/q> r ⬅ r = Rename
|
|
382
|
+
Old name> mega-account-1 ⬅ current name
|
|
383
|
+
New name> mega-backup-1 ⬅ new name
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Delete:
|
|
387
|
+
```
|
|
388
|
+
e/n/d/r/c/s/q> d ⬅ d = Delete
|
|
389
|
+
Choose a number from below ... ⬅ pick the remote
|
|
390
|
+
y) Yes this is OK
|
|
391
|
+
e) Edit this remote
|
|
392
|
+
y/e> y ⬅ y confirms permanent deletion
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
> Any rename/delete also must be mirrored in parrot-blackbox's pool:
|
|
396
|
+
> `parrot-blackbox remote list` shows what's registered, and
|
|
397
|
+
> `parrot-blackbox remote remove <name>` removes an entry from BOTH rclone and
|
|
398
|
+
> the pool in one go.
|
|
399
|
+
|
|
106
400
|
---
|
|
107
401
|
|
|
108
402
|
## Commands
|
|
@@ -119,10 +413,14 @@ parrot-blackbox force
|
|
|
119
413
|
| `parrot-blackbox restore` | Interactive restore wizard `[sudo]` |
|
|
120
414
|
| `parrot-blackbox restore files <id> <dir>` | Recover a file backup into a folder |
|
|
121
415
|
| `parrot-blackbox restore snapshot <id> --yes` | Overwrite the whole system from a cloud snapshot `[sudo]` |
|
|
122
|
-
| `parrot-blackbox account add <mega\|gdrive> <remote>` |
|
|
416
|
+
| `parrot-blackbox account add <mega\|gdrive> <remote>` | Register an existing rclone remote into the pool |
|
|
123
417
|
| `parrot-blackbox account list` | Pool summary + per-account quota |
|
|
124
|
-
| `parrot-blackbox account remove <id>` | Remove an account |
|
|
418
|
+
| `parrot-blackbox account remove <id>` | Remove an account from the pool |
|
|
125
419
|
| `parrot-blackbox account quota <id> <GiB>` | Override an account's quota |
|
|
420
|
+
| `parrot-blackbox remote add <mega\|gdrive> [name]` | ⭐ Add a cloud account (guided — sets up rclone FOR you) |
|
|
421
|
+
| `parrot-blackbox remote list` | Show rclone remotes + pool registration |
|
|
422
|
+
| `parrot-blackbox remote remove <name>` | Delete a remote (rclone + pool) |
|
|
423
|
+
| `parrot-blackbox remote config` | Open the full rclone config editor (advanced) |
|
|
126
424
|
| `parrot-blackbox daemon start\|stop\|status` | Background automation |
|
|
127
425
|
| `parrot-blackbox schedule install\|remove` | systemd / cron always-on setup |
|
|
128
426
|
| `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.2",
|
|
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
|
+
}
|