parrot-blackbox 1.0.6 → 1.0.7

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "parrot-blackbox",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
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",
@@ -26,29 +26,49 @@ export class SudoDeferredError extends Error {
26
26
  }
27
27
 
28
28
  /**
29
- * Parse `timeshift --list` output into snapshots. Tolerant of the table format
30
- * used by Timeshift 22.x/23.x/24.x (leading "Num" column + header rows) AND the
31
- * older plain format:
29
+ * Parse `timeshift --list` output into snapshots. Tolerant of multiple formats:
32
30
  *
33
- * Next run: daily at 22:00
31
+ * Old format (Timeshift 22.x/23.x):
32
+ * 2026-08-29 22:00:01 W 2026-08-29_22-00-01 /timeshift/snapshots/...
33
+ *
34
+ * Table format with header (Timeshift 24.x):
34
35
  * Num Name Tags Description
35
- * 0 2026-08-31 16:00:01 W 2026-08-31_16-00-01 parrot-blackbox
36
+ * 0 2026-08-29 22:00:01 W 2026-08-29_22-00-01 parrot-blackbox
37
+ *
38
+ * Current format (Timeshift 24.06+):
39
+ * Num Name Tags Description
40
+ * 0 > 2026-09-01_21-22-39 W parrot-blackbox 2026-09-01T21:22:05
36
41
  *
37
42
  * @returns {Array<{name:string, date:string, time:string, tags:string, dir:?string}>}
38
43
  */
39
44
  export function parseTimeshiftList(stdout) {
40
45
  const out = [];
41
46
  for (const line of String(stdout).split('\n')) {
42
- // Optional leading Num column; date TIME tags NAME [description…]
43
- const m = /^\s*(?:\d+\s+)?(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})\s+([A-Za-z]{1,6})\s+(\S+)(?:\s+(.*))?$/.exec(line);
44
- if (!m) continue;
45
- const [, date, time, tags, dirOrSize, detail] = m;
46
- // Prefer the explicit dir-ish token (name contains _HH-MM-SS) over a rebuilt name.
47
- const name = /^[\w.-]+\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}$/.test(dirOrSize)
48
- ? dirOrSize
49
- : `${date}_${time.replace(/:/g, '-')}`;
50
- const dir = findPathInLine(line);
51
- out.push({ name, date, time, tags, dir, line: `${date} ${time} ${tags} ${dirOrSize}${detail ? ` ${detail}` : ''}` });
47
+ // Try current format first: Num > NAME Tags Description
48
+ // The NAME field is in YYYY-MM-DD_HH-MM-SS format
49
+ let m = /^\s*\d+\s+>?\s+(\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2})\s+([A-Za-z]{1,6})\s+(.*)$/.exec(line);
50
+ if (m) {
51
+ const [, name, tags, description] = m;
52
+ // Extract date and time from the name (YYYY-MM-DD_HH-MM-SS)
53
+ const date = name.slice(0, 10); // YYYY-MM-DD
54
+ const time = name.slice(11).replace(/-/g, ':'); // HH:MM:SS
55
+ const dir = findPathInLine(line);
56
+ out.push({ name, date, time, tags, dir, line: `${name} ${tags} ${description}` });
57
+ continue;
58
+ }
59
+
60
+ // Try older format: [Num] DATE TIME TAGS NAME [description]
61
+ m = /^\s*(?:\d+\s+)?(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})\s+([A-Za-z]{1,6})\s+(\S+)(?:\s+(.*))?$/.exec(line);
62
+ if (m) {
63
+ const [, date, time, tags, dirOrName, detail] = m;
64
+ // Prefer the explicit dir-ish token (name contains _HH-MM-SS) over a rebuilt name.
65
+ const name = /^[\w.-]+\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}$/.test(dirOrName)
66
+ ? dirOrName
67
+ : `${date}_${time.replace(/:/g, '-')}`;
68
+ const dir = findPathInLine(line);
69
+ out.push({ name, date, time, tags, dir, line: `${date} ${time} ${tags} ${dirOrName}${detail ? ` ${detail}` : ''}` });
70
+ continue;
71
+ }
52
72
  }
53
73
  return out.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
54
74
  }
@@ -48,16 +48,16 @@ async function autoUpdateCheck() {
48
48
  async function addAccountAction() {
49
49
  for (;;) {
50
50
  const provider = await p.select({
51
- message: 'Which provider?',
51
+ message: '☁️ Add cloud account',
52
52
  options: [
53
- { value: 'mega', label: 'MEGA (20 GB free tier)' },
54
- { value: 'gdrive', label: 'Google Drive (15 GB free tier)' },
53
+ { value: 'mega', label: 'MEGA', hint: '20 GB free' },
54
+ { value: 'gdrive', label: 'Google Drive', hint: '15 GB free' },
55
55
  { value: 'back', label: '← Back' },
56
56
  ],
57
57
  });
58
58
  if (p.isCancel(provider) || provider === 'back') return;
59
59
  const res = await guidedRemoteAdd({ provider });
60
- if (res.ok) p.log.success(`✔ ${pc.bold(res.name)} (${res.provider}) added to the pool.`);
60
+ if (res.ok) p.log.success(`✔ ${pc.bold(res.name)} added to pool.`);
61
61
  else if (res.error) p.log.warn(res.error);
62
62
  else if (res.cancelled) { p.log.message(pc.dim('Cancelled.')); return; }
63
63
  const again = await p.confirm({ message: 'Add another account?', initialValue: false });
@@ -68,12 +68,12 @@ async function addAccountAction() {
68
68
  /** Storage pool sub-menu: list / add / remove / quota. */
69
69
  async function accountsMenu() {
70
70
  const sub = await p.select({
71
- message: 'Storage pool — accounts are rclone remotes (MEGA / Google Drive)',
71
+ message: '🗂 Storage Pool',
72
72
  options: [
73
- { value: 'list', label: '🔎 List accounts & quotas' },
74
- { value: 'add', label: '➕ Add account to the pool (existing rclone remote)' },
75
- { value: 'remove', label: '➖ Remove account from the pool' },
76
- { value: 'quota', label: '📐 Set an account quota (GiB)' },
73
+ { value: 'list', label: '📊 Show accounts', hint: 'quotas and usage' },
74
+ { value: 'add', label: '➕ Add account', hint: 'existing rclone remote' },
75
+ { value: 'remove', label: '➖ Remove account', hint: 'from pool only' },
76
+ { value: 'quota', label: '📐 Set quota', hint: 'override account limit' },
77
77
  { value: 'back', label: '← Back' },
78
78
  ],
79
79
  });
@@ -199,13 +199,13 @@ async function listBackupsAction() {
199
199
  /** Restore files or a system snapshot. */
200
200
  async function restoreMenu() {
201
201
  const accs = listAccounts();
202
- if (!accs.length) { p.log.warn('No accounts configured — nothing to restore from cloud yet.'); return; }
202
+ if (!accs.length) { p.log.warn('No cloud accounts configured yet.'); return; }
203
203
  const cfg = loadConfig();
204
204
  const kind = await p.select({
205
- message: 'Restore what?',
205
+ message: '♻️ Restore backup',
206
206
  options: [
207
- { value: 'files', label: '📄 File backup (fonts/images/docs into a folder)' },
208
- { value: 'snapshot', label: '💽 System snapshot (overwrites the running system)', hint: '[sudo]' },
207
+ { value: 'files', label: '📄 Files', hint: 'recover documents, images, etc.' },
208
+ { value: 'snapshot', label: '💽 System snapshot', hint: 'full system restore [sudo]' },
209
209
  { value: 'back', label: '← Back' },
210
210
  ],
211
211
  });
@@ -256,10 +256,10 @@ async function restoreMenu() {
256
256
  /** Always-on service sub-menu. */
257
257
  async function serviceMenu() {
258
258
  const sub = await p.select({
259
- message: 'Always-on background service (systemd user unit / cron fallback)',
259
+ message: ' Schedule Service',
260
260
  options: [
261
- { value: 'install', label: '✅ Install service' },
262
- { value: 'remove', label: '❌ Remove service' },
261
+ { value: 'install', label: '✅ Enable', hint: 'auto-backup on schedule' },
262
+ { value: 'remove', label: '❌ Disable', hint: 'stop auto-backup' },
263
263
  { value: 'back', label: '← Back' },
264
264
  ],
265
265
  });
@@ -275,12 +275,13 @@ async function serviceMenu() {
275
275
 
276
276
  /** Daemon sub-menu. */
277
277
  async function daemonMenu() {
278
+ const running = daemonRunning();
278
279
  const sub = await p.select({
279
- message: `Daemon is currently ${daemonRunning() ? pc.green('running') : pc.yellow('stopped')}`,
280
+ message: `🤖 Daemon ${running ? pc.green('') : pc.yellow('')} ${running ? 'running' : 'stopped'}`,
280
281
  options: [
281
- { value: 'start', label: '▶️ Start daemon' },
282
- { value: 'stop', label: ' Stop daemon' },
283
- { value: 'status', label: '📊 Show status' },
282
+ { value: 'start', label: '▶️ Start' },
283
+ { value: 'stop', label: '⏹️ Stop' },
284
+ { value: 'status', label: '📊 Status' },
284
285
  { value: 'back', label: '← Back' },
285
286
  ],
286
287
  });
@@ -302,7 +303,7 @@ async function daemonMenu() {
302
303
  * prompt just returns you to this menu.
303
304
  */
304
305
  export async function runWizard() {
305
- p.intro(pc.bgYellow(pc.black(` 🦜 parrot-blackbox v${pkg.version} `)));
306
+ p.intro(`🦜 ${pc.bold('parrot-blackbox')} ${pc.dim(`v${pkg.version}`)}`);
306
307
 
307
308
  if (!process.stdin.isTTY) {
308
309
  p.log.warn('No interactive terminal detected — run subcommands directly: `parrot-blackbox help`');
@@ -317,27 +318,27 @@ export async function runWizard() {
317
318
  const action = await p.select({
318
319
  message: 'What would you like to do?',
319
320
  options: [
320
- { value: 'add', label: ' Add cloud account (MEGA / Google Drive)', hint: 'sets up rclone for you' },
321
- { value: 'accounts', label: '🗂 Storage pool', hint: 'list / add / remove / quota' },
322
- { value: 'tools', label: '🛠 Check & install tools', hint: 'rclone, timeshift, git, curl' },
323
- { value: 'snapshot', label: '📸 Snapshot now', hint: 'create + upload a weekly snapshot' },
324
- { value: 'backup', label: '💾 Run backup now', hint: 'every enabled job (default = snapshot)' },
325
- { value: 'list', label: '📋 List backups', hint: 'local + cloud snapshots, file backups' },
326
- { value: 'restore', label: '♻️ Restore', hint: 'files or system snapshot' },
327
- { value: 'service', label: ' Always-on service', hint: 'install / remove' },
328
- { value: 'daemon', label: '🐚 Daemon', hint: 'start / stop / status' },
329
- { value: 'setup', label: '🧭 Guided setup', hint: 'walk every setup step' },
330
- { value: 'status', label: '📊 Status', hint: 'quick overview' },
321
+ { value: 'snapshot', label: '📸 Create snapshot', hint: 'backup your system now' },
322
+ { value: 'backup', label: '💾 Run all backups', hint: 'snapshots + file backups' },
323
+ { value: 'restore', label: '♻️ Restore backup', hint: 'files or system snapshot' },
324
+ { value: 'list', label: '📋 List backups', hint: 'see what\'s saved' },
325
+ { value: 'add', label: '☁️ Add cloud account', hint: 'MEGA or Google Drive' },
326
+ { value: 'accounts', label: '🗂 Manage storage', hint: 'pool, quotas, accounts' },
327
+ { value: 'setup', label: '🚀 Guided setup', hint: 'first-time configuration' },
328
+ { value: 'tools', label: '🔧 Check tools', hint: 'install missing dependencies' },
329
+ { value: 'service', label: '⏱ Schedule service', hint: 'auto-backup setup' },
330
+ { value: 'daemon', label: '🤖 Daemon control', hint: 'start / stop / status' },
331
+ { value: 'status', label: '📊 Status', hint: 'quick health check' },
331
332
  { value: 'doctor', label: '🩺 Doctor', hint: 'full diagnostics' },
332
- { value: 'repair', label: '🛠 Repair broken install', hint: 'check tools, config, service, pool' },
333
- { value: 'update', label: '🔄 Update parrot-blackbox', hint: 'check npm & install latest' },
334
- { value: 'uninstall', label: '🗑 Uninstall', hint: 'remove everything (cloud kept)' },
335
- { value: 'exit', label: '📴 Exit', hint: 'leave the wizard' },
333
+ { value: 'repair', label: '🛠️ Repair', hint: 'fix broken installation' },
334
+ { value: 'update', label: '⬆️ Update', hint: 'check for new version' },
335
+ { value: 'uninstall', label: '🗑️ Uninstall', hint: 'remove parrot-blackbox' },
336
+ { value: 'exit', label: '👋 Exit' },
336
337
  ],
337
338
  });
338
339
 
339
340
  if (p.isCancel(action) || action === 'exit') {
340
- p.outro('Bye! 👋');
341
+ p.outro('👋 See you later!');
341
342
  return;
342
343
  }
343
344
 
@@ -363,6 +364,6 @@ export async function runWizard() {
363
364
  } catch (e) {
364
365
  p.log.warn(`✖ ${e.message}`);
365
366
  }
366
- p.log.message(pc.dim('──────────────────────────────────────────'));
367
+ p.log.message('');
367
368
  }
368
369
  }