parrot-blackbox 1.0.3 → 1.0.5

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 CHANGED
@@ -16,7 +16,7 @@ Parrot install died" — a single CLI + background daemon that:
16
16
  missed backups are run **in order, oldest first, the moment WiFi is back**.
17
17
  Every job is journalled and retried; a lock prevents collisions; state is
18
18
  written atomically. Nothing is silently lost.
19
- 4. **Manages ~150 GB of free cloud storage for you** — 5 MEGA + 5 Google Drive
19
+ 4. **Manages ~175 GB of free cloud storage for you** — 5 MEGA + 5 Google Drive
20
20
  accounts are connected as one pool. Files are placed whole on whichever
21
21
  account has the most relative headroom; anything bigger than a single
22
22
  account's free space is split into byte-range chunks across accounts and a
@@ -436,13 +436,14 @@ y/e> y ⬅ y confirms permanent deletion
436
436
  ## How the smart storage pool works
437
437
 
438
438
  ```
439
- 5 MEGA (20 GiB each)
440
- 5 Drive (10 GiB each)
439
+ 5 MEGA (20 GiB each) = 100 GiB
440
+ 5 Drive (15 GiB each) = 75 GiB ← Google Drive free tier is 15 GiB
441
441
  ──────────────────────────────
442
- 150 GiB managed automatically
442
+ 175 GiB managed automatically
443
443
  ```
444
444
 
445
- - Every account is exactly one rclone remote (`mega-1:`, `drive-2:` ).
445
+ (Your live `account list` proves it: 10 accounts 175 GiB total.)
446
+ Every account is exactly one rclone remote (`mega-1:`, `gdrive-6:` …).
446
447
  - Each backup has **one manifest** that records where every file (or chunk) is.
447
448
  - Placement picks the account that would end up with the **lowest used
448
449
  percentage** (water-filling), then most free.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "parrot-blackbox",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
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",
@@ -17,7 +17,7 @@ import { refreshAccounts } from '../storage/accounts.js';
17
17
  import { planAndPlace } from '../storage/allocator.js';
18
18
  import { listArtifacts, removeArtifact } from '../storage/archive.js';
19
19
  import { planPrune } from './retention.js';
20
- import { sudoInteractive, sudoNonInteractive } from '../util/sudo.js';
20
+ import { sudoInteractive, sudoNonInteractive, sudoInteractiveCapture } from '../util/sudo.js';
21
21
 
22
22
  export class SudoDeferredError extends Error {
23
23
  constructor() {
@@ -50,18 +50,33 @@ function findPathInLine(line) {
50
50
  null;
51
51
  }
52
52
 
53
- /** `timeshift --list` works WITHOUT root — run it directly (no sudo hang risk). */
54
- async function runTimeshiftList() {
53
+ /**
54
+ * `timeshift --list` needs admin on most installs — run it with sudo.
55
+ * Interactive: capture stdout while keeping stdin (so the sudo password
56
+ * prompt works). Non-interactive: `sudo -n`, falling back to a direct call
57
+ * for the few builds that allow unprivileged listing.
58
+ */
59
+ async function runTimeshiftList({ privileged = 'noninteractive' } = {}) {
60
+ if (privileged === 'interactive') {
61
+ try {
62
+ const res = await sudoInteractiveCapture(['timeshift', '--list']);
63
+ return (res.exitCode === 0 ? res.stdout : res.stdout || res.stderr) || '';
64
+ } catch {
65
+ return '';
66
+ }
67
+ }
55
68
  try {
56
- const res = await execa('timeshift', ['--list'], { reject: false });
57
- return (res.exitCode === 0 ? res.stdout : res.stdout || res.stderr) || '';
69
+ const res = await sudoNonInteractive(['timeshift', '--list']);
70
+ if (res.exitCode === 0) return res.stdout || '';
71
+ const direct = await execa('timeshift', ['--list'], { reject: false });
72
+ return (direct.exitCode === 0 ? direct.stdout : direct.stdout || direct.stderr) || '';
58
73
  } catch {
59
74
  return '';
60
75
  }
61
76
  }
62
77
 
63
78
  export async function listLocalSnapshots({ privileged = 'noninteractive' } = {}) {
64
- const text = await runTimeshiftList();
79
+ const text = await runTimeshiftList({ privileged });
65
80
  return parseTimeshiftList(text);
66
81
  }
67
82
 
package/src/cli.js CHANGED
@@ -293,7 +293,7 @@ async function remoteCommands(rest) {
293
293
  message: 'Provider?',
294
294
  options: [
295
295
  { value: 'mega', label: 'MEGA (20 GB free tier)' },
296
- { value: 'gdrive', label: 'Google Drive (10 GB free tier)' },
296
+ { value: 'gdrive', label: 'Google Drive (15 GB free tier)' },
297
297
  ],
298
298
  });
299
299
  if (p.isCancel(choice)) return;
@@ -185,14 +185,29 @@ export async function runRepair({ auto = false } = {}) {
185
185
  // 3. Service
186
186
  try {
187
187
  const { serviceFile, daemonLogFile } = await import('../core/paths.js');
188
- const { serviceBackend } = await import('./service.js');
188
+ const { serviceBackend, installService } = await import('./service.js');
189
189
  const fsMod = await import('node:fs');
190
- if (serviceBackend() === 'systemd' && !fsMod.existsSync(serviceFile())) {
191
- const backend = await installService();
192
- p.log.success(`Always-on service re-installed via ${backend}.`);
193
- fixed.push('service');
190
+ if (serviceBackend() === 'systemd') {
191
+ // The ExecStart line must reference a REAL absolute path; the v1.0.4 unit
192
+ // wrote `node parrot-blackbox daemon foreground` (bare name) which crashed
193
+ // with "Cannot find module '/home/artkins/parrot-blackbox'".
194
+ let unitOk = false;
195
+ try {
196
+ const unit = fsMod.readFileSync(serviceFile(), 'utf8');
197
+ const execLine = unit.split('\n').find((l) => l.startsWith('ExecStart=')) || '';
198
+ unitOk = fsMod.existsSync(serviceFile()) && /ExecStart=\S+/.test(execLine) && /\//.test(execLine);
199
+ } catch {
200
+ unitOk = false;
201
+ }
202
+ if (!unitOk) {
203
+ const backend = await installService();
204
+ p.log.success(`Always-on service re-written via ${backend}.`);
205
+ fixed.push('service');
206
+ } else {
207
+ p.log.success('Service OK.');
208
+ }
194
209
  } else {
195
- p.log.success('Service OK.');
210
+ p.log.success('Service backend OK.');
196
211
  }
197
212
  } catch (e) {
198
213
  p.log.warn(`Service check failed: ${e.message}`);
@@ -115,7 +115,7 @@ export async function registerRemotesAsAccounts() {
115
115
  message: `Provider for "${remote}"?`,
116
116
  options: [
117
117
  { value: 'mega', label: 'MEGA (20 GB free tier)' },
118
- { value: 'gdrive', label: 'Google Drive (10 GB free tier)' },
118
+ { value: 'gdrive', label: 'Google Drive (15 GB free tier)' },
119
119
  ],
120
120
  });
121
121
  if (p.isCancel(provider)) continue;
@@ -7,6 +7,7 @@
7
7
 
8
8
  import fs from 'node:fs';
9
9
  import path from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
10
11
  import { execa } from 'execa';
11
12
  import { serviceFile, daemonLogFile } from '../core/paths.js';
12
13
  import { loadConfig, journal, hasCommandSync } from '../core/store.js';
@@ -15,6 +16,8 @@ function findCliBin() {
15
16
  // npm-installed global binary (preferred) or our own bin entry.
16
17
  const candidates = [
17
18
  process.env.PBB_BIN,
19
+ // This module lives in src/commands/ → ../../bin/parrot-blackbox.js
20
+ path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'bin', 'parrot-blackbox.js'),
18
21
  path.join(path.dirname(process.argv[1] || ''), 'parrot-blackbox.js'),
19
22
  ];
20
23
  return candidates.find((c) => c && fs.existsSync(c)) || 'parrot-blackbox';
@@ -99,7 +99,7 @@ async function storagePoolStep() {
99
99
  message: 'Which provider?',
100
100
  options: [
101
101
  { value: 'mega', label: 'MEGA (20 GB free tier)' },
102
- { value: 'gdrive', label: 'Google Drive (10 GB free tier)' },
102
+ { value: 'gdrive', label: 'Google Drive (15 GB free tier)' },
103
103
  ],
104
104
  });
105
105
  if (p.isCancel(provider)) break;
@@ -51,7 +51,7 @@ async function addAccountAction() {
51
51
  message: 'Which provider?',
52
52
  options: [
53
53
  { value: 'mega', label: 'MEGA (20 GB free tier)' },
54
- { value: 'gdrive', label: 'Google Drive (10 GB free tier)' },
54
+ { value: 'gdrive', label: 'Google Drive (15 GB free tier)' },
55
55
  { value: 'back', label: '← Back' },
56
56
  ],
57
57
  });
package/src/core/store.js CHANGED
@@ -52,7 +52,7 @@ export function defaultConfig() {
52
52
  chunkSize: 2 * GiB,
53
53
  providers: {
54
54
  mega: { defaultQuotaGiB: 20 },
55
- gdrive: { defaultQuotaGiB: 10 },
55
+ gdrive: { defaultQuotaGiB: 15 },
56
56
  },
57
57
  accounts: [], // {id, provider, label, remote, quotaGiB?}
58
58
  },