parrot-blackbox 1.0.4 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "parrot-blackbox",
3
- "version": "1.0.4",
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
 
@@ -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}`);
@@ -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';