@datatruck/restic 0.0.1 → 0.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.
@@ -1,129 +1,119 @@
1
+ import { createRunner } from "../utils/async.js";
1
2
  import { checkDiskSpace } from "../utils/fs.js";
2
- import { Ntfy } from "../utils/ntfy.js";
3
+ import { Action } from "./base.js";
4
+ import { Prune } from "./prune.js";
3
5
  import { SnapshotTagEnum } from "@datatruck/cli/repositories/RepositoryAbstract.js";
4
6
  import { ResticRepository } from "@datatruck/cli/repositories/ResticRepository.js";
5
7
  import { formatBytes } from "@datatruck/cli/utils/bytes.js";
6
- import { duration } from "@datatruck/cli/utils/date.js";
7
8
  import { isLocalDir } from "@datatruck/cli/utils/fs.js";
8
9
  import { Restic } from "@datatruck/cli/utils/restic.js";
9
- export class Copy {
10
- config;
11
- global;
12
- ntfy;
13
- verbose;
14
- constructor(config, global) {
15
- this.config = config;
16
- this.global = global;
17
- this.verbose = this.global?.verbose ?? this.config.verbose;
18
- this.ntfy = new Ntfy({
19
- token: this.config.ntfyToken,
20
- titlePrefix: `[${this.config.hostname}] `,
10
+ export class Copy extends Action {
11
+ initializedRepos = new Set();
12
+ async findSnapshots(name, packages) {
13
+ const repo = this.cm.findRepository(name);
14
+ const restic = new Restic({
15
+ log: this.verbose,
16
+ env: {
17
+ RESTIC_REPOSITORY: repo.uri,
18
+ RESTIC_PASSWORD: repo.password,
19
+ },
21
20
  });
21
+ const snapshots = (await restic.snapshots({
22
+ latest: 1,
23
+ group: ["path"],
24
+ tags: packages
25
+ ? packages.map((name) => ResticRepository.createSnapshotTag(SnapshotTagEnum.PACKAGE, name))
26
+ : undefined,
27
+ }));
28
+ return snapshots.flatMap((s) => s.snapshots);
22
29
  }
23
- findRepo(name) {
24
- const repo = this.config.repositories.find((repo) => repo.name === name);
25
- if (!repo)
26
- throw new Error(`Repository '${name}' not found`);
27
- return repo;
30
+ findPackageTag(inTags) {
31
+ const tags = inTags
32
+ .map((t) => ResticRepository.parseSnapshotTag(t))
33
+ .filter((t) => !!t);
34
+ return tags.find((t) => t.name === SnapshotTagEnum.PACKAGE);
35
+ }
36
+ async runSingle(options) {
37
+ const { snapshot } = options;
38
+ const targetRepo = this.cm.findRepository(options.target);
39
+ const sourceRepo = this.cm.findRepository(options.source);
40
+ const pkgTag = this.findPackageTag(snapshot.tags);
41
+ const packageName = pkgTag?.value;
42
+ const targetPath = isLocalDir(targetRepo.uri) ? targetRepo.uri : undefined;
43
+ const target = new Restic({
44
+ log: this.verbose,
45
+ env: {
46
+ RESTIC_REPOSITORY: targetRepo.uri,
47
+ RESTIC_PASSWORD: targetRepo.password,
48
+ ["GODEBUG"]: "http2client=0",
49
+ },
50
+ });
51
+ let space;
52
+ await createRunner(async () => {
53
+ if (!this.initializedRepos.has(targetRepo.name)) {
54
+ await target.tryInit();
55
+ this.initializedRepos.add(targetRepo.name);
56
+ }
57
+ space = await checkDiskSpace({
58
+ minFreeSpace: this.config.minFreeSpace,
59
+ targetPath,
60
+ rutine: () => target.copy({
61
+ ids: [snapshot.id],
62
+ fromRepo: sourceRepo.uri,
63
+ fromRepoPassword: sourceRepo.password,
64
+ }),
65
+ });
66
+ }).start(async (data) => {
67
+ await this.ntfy.send(`Copy`, {
68
+ Id: snapshot.short_id,
69
+ Source: sourceRepo.name,
70
+ Target: targetRepo.name,
71
+ Package: packageName,
72
+ ...(space !== undefined && {
73
+ Size: `${formatBytes(space.size)} (${formatBytes(space.diff, true)})`,
74
+ }),
75
+ Duration: data.duration,
76
+ Error: data.error?.message,
77
+ }, data.error);
78
+ });
79
+ return space?.diff ?? 0;
28
80
  }
29
81
  async run(options) {
30
- const now = Date.now();
31
82
  let globalDiffSize;
32
- let error;
33
- try {
83
+ await createRunner(async () => {
84
+ const [source] = this.cm.filterRepositories([options.source]);
85
+ const targets = this.cm.filterRepositories(options.targets);
34
86
  await this.ntfy.send(`Copy start`, {
35
- "- Source": options.source,
36
- "- Targets": options.targets.join(", "),
37
- });
38
- const sourceRepo = this.findRepo(options.source);
39
- const source = new Restic({
40
- log: this.verbose,
41
- env: {
42
- RESTIC_REPOSITORY: sourceRepo.uri,
43
- RESTIC_PASSWORD: sourceRepo.password,
44
- },
87
+ Source: source.name,
88
+ Targets: targets.map((t) => t.name).join(", "),
45
89
  });
46
- const targetRepos = options.targets.map((target) => this.findRepo(target));
47
- if (!targetRepos.length)
48
- throw new Error(`No target repositories specified`);
49
- const inSnapshots = (await source.snapshots({
50
- latest: 1,
51
- group: ["path"],
52
- tags: options.packages
53
- ? options.packages.map((name) => ResticRepository.createSnapshotTag(SnapshotTagEnum.PACKAGE, name))
54
- : undefined,
55
- }));
56
- const snapshots = inSnapshots.flatMap((s) => s.snapshots);
57
- for (const targetRepo of targetRepos) {
58
- const target = new Restic({
59
- log: this.verbose,
60
- env: {
61
- RESTIC_REPOSITORY: targetRepo.uri,
62
- RESTIC_PASSWORD: targetRepo.password,
63
- ["GODEBUG"]: "http2client=0",
64
- },
65
- });
66
- const exists = await target.checkRepository();
67
- if (!exists && isLocalDir(targetRepo.uri))
68
- await target.exec(["init"]);
90
+ const snapshots = await this.findSnapshots(source.name, options.packages);
91
+ for (const target of targets) {
69
92
  for (const snapshot of snapshots) {
70
- const tags = snapshot.tags
71
- .map((t) => ResticRepository.parseSnapshotTag(t))
72
- .filter((t) => !!t);
73
- const pkgTag = tags.find((t) => t.name === SnapshotTagEnum.PACKAGE);
74
- const packageName = pkgTag?.value;
75
- const now = Date.now();
76
- let copyError;
77
- let diffSize;
78
- const targetPath = isLocalDir(targetRepo.uri)
79
- ? targetRepo.uri
80
- : undefined;
81
- try {
82
- diffSize = await checkDiskSpace({
83
- minFreeSpace: this.config.minFreeSpace,
84
- targetPath,
85
- rutine: () => target.copy({
86
- ids: [snapshot.id],
87
- fromRepo: sourceRepo.uri,
88
- fromRepoPassword: sourceRepo.password,
89
- }),
90
- });
91
- if (diffSize !== undefined) {
92
- globalDiffSize = (globalDiffSize ?? 0) + (diffSize ?? 0);
93
- }
94
- }
95
- catch (inError) {
96
- copyError = inError;
97
- }
98
- await this.ntfy.send(`Copy`, {
99
- "- Id": snapshot.short_id,
100
- "- Source": sourceRepo.name,
101
- "- Target": targetRepo.name,
102
- "- Package": packageName,
103
- ...(diffSize !== undefined && {
104
- "- Diff size": (diffSize > 0 ? "+" : "") + formatBytes(diffSize),
105
- }),
106
- "- Duration": duration(Date.now() - now),
107
- "- Error": copyError?.message,
108
- }, {
109
- priority: copyError ? "high" : "default",
110
- tags: [copyError ? "red_circle" : "green_circle"],
93
+ const diffSize = await this.runSingle({
94
+ snapshot,
95
+ source: source.name,
96
+ target: target.name,
111
97
  });
98
+ globalDiffSize = (globalDiffSize ?? 0) + diffSize;
112
99
  }
113
100
  }
114
- }
115
- catch (inError) {
116
- error = inError;
117
- }
118
- await this.ntfy.send(`Copy end`, {
119
- ...(globalDiffSize !== undefined && {
120
- "- Diff size": (globalDiffSize > 0 ? "+" : "") + formatBytes(globalDiffSize),
121
- }),
122
- "- Duration": duration(Date.now() - now),
123
- "- Error": error?.message,
124
- }, {
125
- priority: error ? "high" : "default",
126
- tags: [error ? "red_circle" : "green_circle"],
101
+ if (!options.targets.length)
102
+ throw new Error(`No target repositories specified`);
103
+ }).start(async (data) => {
104
+ await this.ntfy.send(`Copy end`, {
105
+ ...(globalDiffSize !== undefined && {
106
+ "Diff size": formatBytes(globalDiffSize, true),
107
+ }),
108
+ Duration: data.duration,
109
+ Error: data.error?.message,
110
+ }, data.error);
111
+ if (options.prune) {
112
+ await new Prune(this.config, this.global).run({
113
+ packages: options.packages,
114
+ repositories: options.targets,
115
+ });
116
+ }
127
117
  });
128
118
  }
129
119
  }
@@ -1,13 +1,8 @@
1
- import { Config, GlobalConfig } from "../config.js";
2
- import { Ntfy } from "../utils/ntfy.js";
1
+ import { Action } from "./base.js";
3
2
  export type InitOptions = {
4
3
  repositories?: string[];
5
4
  };
6
- export declare class Init {
7
- readonly config: Config;
8
- readonly global?: GlobalConfig | undefined;
9
- readonly ntfy: Ntfy;
10
- protected verbose: boolean | undefined;
11
- constructor(config: Config, global?: GlobalConfig | undefined);
5
+ export declare class Init extends Action {
6
+ protected runSingle(name: string): Promise<void>;
12
7
  run(options: InitOptions): Promise<void>;
13
8
  }
@@ -1,56 +1,41 @@
1
- import { Ntfy } from "../utils/ntfy.js";
2
- import { duration } from "@datatruck/cli/utils/date.js";
3
- import { isLocalDir } from "@datatruck/cli/utils/fs.js";
1
+ import { createRunner } from "../utils/async.js";
2
+ import { Action } from "./base.js";
4
3
  import { Restic } from "@datatruck/cli/utils/restic.js";
5
- export class Init {
6
- config;
7
- global;
8
- ntfy;
9
- verbose;
10
- constructor(config, global) {
11
- this.config = config;
12
- this.global = global;
13
- this.verbose = this.global?.verbose ?? this.config.verbose;
14
- this.ntfy = new Ntfy({
15
- token: this.config.ntfyToken,
16
- titlePrefix: `[${this.config.hostname}] `,
4
+ export class Init extends Action {
5
+ async runSingle(name) {
6
+ let exists;
7
+ await createRunner(async () => {
8
+ const repo = this.cm.findRepository(name);
9
+ const restic = new Restic({
10
+ log: this.verbose,
11
+ env: {
12
+ RESTIC_REPOSITORY: repo.uri,
13
+ RESTIC_PASSWORD: repo.password,
14
+ },
15
+ });
16
+ exists = await restic.tryInit();
17
+ }).start(async (data) => {
18
+ await this.ntfy.send(`Init`, {
19
+ Repository: name,
20
+ Exists: exists ? "yes" : "no",
21
+ Duration: data.duration,
22
+ Error: data.error?.message,
23
+ }, data.error);
17
24
  });
18
25
  }
19
26
  async run(options) {
20
- const now = Date.now();
21
- await this.ntfy.send(`Init start`, {});
22
- const repositories = options.repositories ?? this.config.repositories.map((r) => r.name);
23
- for (const name of repositories) {
24
- let error;
25
- let exists;
26
- try {
27
- const repo = this.config.repositories.find((repo) => repo.name === name);
28
- if (!repo)
29
- throw new Error(`Repository not found`);
30
- const source = new Restic({
31
- log: this.verbose,
32
- env: {
33
- RESTIC_REPOSITORY: repo.uri,
34
- RESTIC_PASSWORD: repo.password,
35
- },
36
- });
37
- exists = await source.checkRepository();
38
- if (!exists && isLocalDir(repo.uri))
39
- await source.exec(["init"]);
40
- }
41
- catch (inError) {
42
- error = inError;
43
- }
44
- await this.ntfy.send(`Init`, {
45
- "- Repository": name,
46
- "- Exists": exists ? "yes" : "no",
47
- "- Duration": duration(Date.now() - now),
48
- "- Error": error?.message,
49
- }, {
50
- priority: error ? "high" : "default",
51
- tags: [error ? "red_circle" : "green_circle"],
27
+ await createRunner(async () => {
28
+ const repositories = this.cm.filterRepositories(options.repositories);
29
+ await this.ntfy.send(`Init start`, {
30
+ Repositories: repositories.length,
52
31
  });
53
- }
54
- await this.ntfy.send(`Init end`, {});
32
+ for (const repo of repositories)
33
+ await this.runSingle(repo.name);
34
+ }).start(async (data) => {
35
+ await this.ntfy.send(`Init end`, {
36
+ Duration: data.duration,
37
+ Error: data.error?.message,
38
+ });
39
+ });
55
40
  }
56
41
  }
@@ -0,0 +1,10 @@
1
+ import { PrunePolicy } from "../config.js";
2
+ import { Action } from "./base.js";
3
+ export type PruneOptions = {
4
+ packages?: string[];
5
+ repositories?: string[];
6
+ };
7
+ export declare class Prune extends Action {
8
+ protected runSingle(repoName: string, pkgName: string, policy: PrunePolicy): Promise<number>;
9
+ run(options: PruneOptions): Promise<void>;
10
+ }
@@ -0,0 +1,105 @@
1
+ import { createRunner, safeRun } from "../utils/async.js";
2
+ import { checkDiskSpace, fetchMultipleDiskStats } from "../utils/fs.js";
3
+ import { Action } from "./base.js";
4
+ import { SnapshotTagEnum } from "@datatruck/cli/repositories/RepositoryAbstract.js";
5
+ import { ResticRepository } from "@datatruck/cli/repositories/ResticRepository.js";
6
+ import { formatBytes } from "@datatruck/cli/utils/bytes.js";
7
+ import { isLocalDir } from "@datatruck/cli/utils/fs.js";
8
+ import { progressPercent } from "@datatruck/cli/utils/math.js";
9
+ import { Restic } from "@datatruck/cli/utils/restic.js";
10
+ export class Prune extends Action {
11
+ async runSingle(repoName, pkgName, policy) {
12
+ let stats;
13
+ let space;
14
+ await createRunner(async () => {
15
+ const repo = this.cm.findRepository(repoName);
16
+ const pkg = this.cm.findPackage(pkgName);
17
+ const restic = new Restic({
18
+ log: this.verbose,
19
+ env: {
20
+ RESTIC_REPOSITORY: repo.uri,
21
+ RESTIC_PASSWORD: repo.password,
22
+ },
23
+ });
24
+ const targetPath = isLocalDir(repo.uri) ? repo.uri : undefined;
25
+ space = await checkDiskSpace({
26
+ minFreeSpace: this.config.minFreeSpace,
27
+ targetPath,
28
+ rutine: async () => {
29
+ const results = await restic.forget({
30
+ ...policy,
31
+ json: true,
32
+ prune: true,
33
+ args: ["--group-by", ""],
34
+ tag: [
35
+ ResticRepository.createSnapshotTag(SnapshotTagEnum.PACKAGE, pkg.name),
36
+ ],
37
+ });
38
+ stats = {
39
+ keep: results.at(0)?.keep?.length ?? 0,
40
+ remove: results.at(0)?.remove?.length ?? 0,
41
+ };
42
+ },
43
+ });
44
+ }).start(async (data) => {
45
+ await this.ntfy.send(`Prune`, {
46
+ Repository: repoName,
47
+ Package: pkgName,
48
+ ...(stats && {
49
+ Snapshots: `${stats.keep}`,
50
+ Removed: `${stats.remove}`,
51
+ }),
52
+ ...(space !== undefined && {
53
+ Size: `${formatBytes(space.size)} (${formatBytes(space.diff, true)})`,
54
+ }),
55
+ Duration: data.duration,
56
+ Error: data.error?.message,
57
+ }, data.error);
58
+ });
59
+ return space?.diff ?? 0;
60
+ }
61
+ async run(options) {
62
+ let globalDiffSize;
63
+ let localRepositoryPaths = [];
64
+ await createRunner(async () => {
65
+ const repositories = this.cm.filterRepositories(options.repositories);
66
+ const packages = this.cm.filterPackages(options.packages);
67
+ await this.ntfy.send(`Prune start`, {
68
+ Repositories: repositories.length,
69
+ Packages: packages.length,
70
+ });
71
+ localRepositoryPaths = repositories
72
+ .filter((repo) => isLocalDir(repo.uri))
73
+ .map((repo) => repo.uri);
74
+ for (const repo of repositories) {
75
+ for (const pkg of packages) {
76
+ const policy = pkg.prunePolicy ?? repo.prunePolicy ?? this.config.prunePolicy;
77
+ if (policy) {
78
+ const diffSize = await this.runSingle(repo.name, pkg.name, policy);
79
+ if (diffSize !== undefined)
80
+ globalDiffSize = (globalDiffSize ?? 0) + diffSize;
81
+ }
82
+ }
83
+ }
84
+ }).start(async (data) => {
85
+ const diskStats = await safeRun(() => fetchMultipleDiskStats(localRepositoryPaths));
86
+ if (diskStats.error)
87
+ console.error(diskStats.error);
88
+ await this.ntfy.send(`Prune end`, {
89
+ Duration: data.duration,
90
+ ...(globalDiffSize !== undefined && {
91
+ "Diff size": (globalDiffSize > 0 ? "+" : "") + formatBytes(globalDiffSize),
92
+ }),
93
+ Error: data.error?.message,
94
+ "": [
95
+ !!diskStats.result?.length && { key: "Disk stats", value: "" },
96
+ ...(diskStats.result?.map((p) => ({
97
+ key: p.name,
98
+ value: `${formatBytes(p.free)}/${formatBytes(p.total)} (${progressPercent(p.total, p.free)}%)`,
99
+ level: 1,
100
+ })) || []),
101
+ ],
102
+ });
103
+ });
104
+ }
105
+ }
package/lib/bin.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Backup } from "./actions/backup.js";
2
2
  import { Copy } from "./actions/copy.js";
3
3
  import { Init } from "./actions/init.js";
4
+ import { Prune } from "./actions/prune.js";
4
5
  import { parseConfigFile } from "./config.js";
5
6
  import { parseStringList } from "@datatruck/cli/utils/string.js";
6
7
  import { program } from "commander";
@@ -37,6 +38,7 @@ program
37
38
  .description("Run backup action")
38
39
  .option("-r, --repositories <names>", "Repository names", (v) => parseStringList(v))
39
40
  .option("-p, --packages <packages>", "Package names", (v) => parseStringList(v))
41
+ .option("--prune", "Prune after backup")
40
42
  .action(async (options) => {
41
43
  const { config, globalOptions } = await load();
42
44
  const backup = new Backup(config, globalOptions);
@@ -49,9 +51,22 @@ program
49
51
  .option("-p, --packages <packages>", "Package names", (v) => parseStringList(v))
50
52
  .requiredOption("-s, --source <name>", "Source repository name")
51
53
  .requiredOption("-t, --targets <names>", "Target repository names", (v) => parseStringList(v))
54
+ .option("--prune", "Prune after copy")
52
55
  .action(async (options) => {
53
56
  const { config, globalOptions } = await load();
54
57
  const copy = new Copy(config, globalOptions);
55
58
  await copy.run(options);
56
59
  });
60
+ program
61
+ .command("prune")
62
+ .alias("p")
63
+ .description("Run prune action")
64
+ .option("-r, --repositories <names>", "Repository names", (v) => parseStringList(v))
65
+ .option("-p, --packages <packages>", "Package names", (v) => parseStringList(v))
66
+ .option("--prune", "Prune after copy")
67
+ .action(async (options) => {
68
+ const { config, globalOptions } = await load();
69
+ const prune = new Prune(config, globalOptions);
70
+ await prune.run(options);
71
+ });
57
72
  program.parse();
package/lib/config.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { MySQLDumpOptions } from "./utils/mysql.js";
1
2
  export type GlobalConfig = {
2
3
  config?: string;
3
4
  verbose?: boolean;
@@ -8,6 +9,7 @@ export type Config = {
8
9
  ntfyToken?: string;
9
10
  minFreeSpace?: string;
10
11
  verbose?: boolean;
12
+ prunePolicy?: PrunePolicy;
11
13
  tasks?: {
12
14
  type: "mysql-dump";
13
15
  packages: string[];
@@ -20,26 +22,60 @@ export type Config = {
20
22
  path: string | false;
21
23
  }[] | string;
22
24
  concurrency?: number;
23
- connection: {
24
- hostname: string;
25
- username: string;
26
- password: string;
27
- database?: string;
28
- };
25
+ connection: MySQLDumpOptions["connection"];
29
26
  };
30
27
  }[];
31
28
  packages: {
32
29
  name: string;
33
30
  path: string;
34
31
  exclude?: string[];
32
+ prunePolicy?: PrunePolicy;
35
33
  }[];
36
34
  repositories: {
37
35
  name: string;
38
36
  password: string;
39
37
  uri: string;
38
+ prunePolicy?: PrunePolicy;
40
39
  }[];
41
40
  };
41
+ export type PrunePolicy = {
42
+ keepMinutely?: number;
43
+ keepDaily?: number;
44
+ keepHourly?: number;
45
+ keepLast?: number;
46
+ keepMonthly?: number;
47
+ keepWeekly?: number;
48
+ keepYearly?: number;
49
+ };
42
50
  export declare function defineConfig(config: Config): Config;
43
51
  export declare function validateConfig(config: unknown): Promise<void>;
44
52
  export declare function readConfigSchemaFile(): Promise<unknown>;
45
53
  export declare function parseConfigFile(path?: string): Promise<Config>;
54
+ export declare class ConfigManager {
55
+ readonly config: Config;
56
+ constructor(config: Config);
57
+ filterRepositories(filter: string[] | undefined): {
58
+ name: string;
59
+ password: string;
60
+ uri: string;
61
+ prunePolicy?: PrunePolicy;
62
+ }[];
63
+ filterPackages(filter: string[] | undefined): {
64
+ name: string;
65
+ path: string;
66
+ exclude?: string[];
67
+ prunePolicy?: PrunePolicy;
68
+ }[];
69
+ findRepository(name: string): {
70
+ name: string;
71
+ password: string;
72
+ uri: string;
73
+ prunePolicy?: PrunePolicy;
74
+ };
75
+ findPackage(name: string): {
76
+ name: string;
77
+ path: string;
78
+ exclude?: string[];
79
+ prunePolicy?: PrunePolicy;
80
+ };
81
+ }
package/lib/config.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { parseJSONFile } from "./utils/fs.js";
2
+ import { match } from "@datatruck/cli/utils/string.js";
2
3
  import { Ajv } from "ajv";
3
4
  export function defineConfig(config) {
4
5
  return config;
@@ -20,3 +21,33 @@ export async function parseConfigFile(path = "datatruck.restic.json") {
20
21
  await validateConfig(config);
21
22
  return config;
22
23
  }
24
+ export class ConfigManager {
25
+ config;
26
+ constructor(config) {
27
+ this.config = config;
28
+ }
29
+ filterRepositories(filter) {
30
+ const repositories = this.config.repositories.filter((v) => filter ? match(v.name, filter) : true);
31
+ if (!repositories.length)
32
+ throw new Error(`No repositories found for filter: ${filter?.join(", ")}`);
33
+ return repositories;
34
+ }
35
+ filterPackages(filter) {
36
+ const packages = this.config.packages.filter((v) => filter ? match(v.name, filter) : true);
37
+ if (!packages.length)
38
+ throw new Error(`No packages found for filter: ${filter?.join(", ")}`);
39
+ return packages;
40
+ }
41
+ findRepository(name) {
42
+ const repo = this.config.repositories.find((repo) => repo.name === name);
43
+ if (!repo)
44
+ throw new Error(`Repository '${name}' not found`);
45
+ return repo;
46
+ }
47
+ findPackage(name) {
48
+ const pkg = this.config.packages.find((pkg) => pkg.name === name);
49
+ if (!pkg)
50
+ throw new Error(`Package '${name}' not found`);
51
+ return pkg;
52
+ }
53
+ }
package/lib/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- export { Backup, type BackupRunOptions } from "./actions/backup.js";
2
- export { Copy, type CopyRunOptions } from "./actions/copy.js";
1
+ export { Backup, type BackupOptions } from "./actions/backup.js";
2
+ export { Copy, type CopyOptions } from "./actions/copy.js";
3
3
  export { Init, type InitOptions } from "./actions/init.js";
4
+ export { Prune, type PruneOptions } from "./actions/prune.js";
4
5
  export { type Config, type GlobalConfig, parseConfigFile, defineConfig, validateConfig, } from "./config.js";
package/lib/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { Backup } from "./actions/backup.js";
2
2
  export { Copy } from "./actions/copy.js";
3
3
  export { Init } from "./actions/init.js";
4
+ export { Prune } from "./actions/prune.js";
4
5
  export { parseConfigFile, defineConfig, validateConfig, } from "./config.js";
@@ -0,0 +1,10 @@
1
+ export declare function createRunner(rutine: () => any): {
2
+ start: <R = any>(cb: (data: {
3
+ error: Error | undefined;
4
+ duration: string;
5
+ }) => Promise<R>) => Promise<R>;
6
+ };
7
+ export declare function safeRun<T>(cb: () => Promise<T>): Promise<{
8
+ error: Error | undefined;
9
+ result: T | undefined;
10
+ }>;