@intactfile/cli 0.1.0
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/dist/index.js +273 -0
- package/package.json +27 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* IntactFile CLI. Repairs run entirely on this machine — files never leave it.
|
|
4
|
+
* Diagnosis is free; repair/batch require an active Business subscription.
|
|
5
|
+
*/
|
|
6
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { basename, dirname, extname, join, parse } from "node:path";
|
|
8
|
+
import { createInterface } from "node:readline";
|
|
9
|
+
import { Command } from "commander";
|
|
10
|
+
import { ApiError, balance, clearAuth, consume, EntitlementError, getToken, login as apiLogin, magic as apiMagic, recover as apiRecover, repairPath, diagnosePath, requireBusiness, setToken, UnsupportedError, } from "@intactfile/node-core";
|
|
11
|
+
const program = new Command();
|
|
12
|
+
program
|
|
13
|
+
.name("intactfile")
|
|
14
|
+
.description("Repair corrupted files locally — your files never leave your machine.")
|
|
15
|
+
.version("0.1.0");
|
|
16
|
+
/* ------------------------------- helpers ------------------------------- */
|
|
17
|
+
function ask(query) {
|
|
18
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
19
|
+
return new Promise((resolve) => rl.question(query, (a) => (rl.close(), resolve(a.trim()))));
|
|
20
|
+
}
|
|
21
|
+
/** Password prompt with echo muted. */
|
|
22
|
+
function askHidden(query) {
|
|
23
|
+
process.stdout.write(query);
|
|
24
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
25
|
+
rl._writeToOutput = () => { };
|
|
26
|
+
return new Promise((resolve) => rl.question("", (a) => {
|
|
27
|
+
rl.close();
|
|
28
|
+
process.stdout.write("\n");
|
|
29
|
+
resolve(a);
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
const die = (msg) => {
|
|
33
|
+
console.error(`✗ ${msg}`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
};
|
|
36
|
+
/** Extension to give the repaired output, preferring the input's own. */
|
|
37
|
+
const OUT_EXT = {
|
|
38
|
+
zip: ".zip",
|
|
39
|
+
pdf: ".pdf",
|
|
40
|
+
video: ".mp4",
|
|
41
|
+
jpeg: ".jpg",
|
|
42
|
+
png: ".png",
|
|
43
|
+
sqlite: ".sqlite",
|
|
44
|
+
archive: "",
|
|
45
|
+
};
|
|
46
|
+
function defaultOut(input, family) {
|
|
47
|
+
const p = parse(input);
|
|
48
|
+
const ext = p.ext || OUT_EXT[family];
|
|
49
|
+
return join(p.dir, `${p.name}.repaired${ext}`);
|
|
50
|
+
}
|
|
51
|
+
function printReport(family, report) {
|
|
52
|
+
const outcome = report.outcome ?? (family === "zip" ? "(see counts)" : "unknown");
|
|
53
|
+
console.log(` format: ${family}`);
|
|
54
|
+
console.log(` outcome: ${String(outcome)}`);
|
|
55
|
+
if (report.damageClass)
|
|
56
|
+
console.log(` damage: ${String(report.damageClass)}`);
|
|
57
|
+
if (report.bytesIn != null)
|
|
58
|
+
console.log(` bytes in: ${Number(report.bytesIn).toLocaleString()}`);
|
|
59
|
+
if (report.bytesOut != null)
|
|
60
|
+
console.log(` bytes out:${Number(report.bytesOut).toLocaleString()}`);
|
|
61
|
+
const notes = report.notes;
|
|
62
|
+
if (Array.isArray(notes) && notes.length) {
|
|
63
|
+
for (const n of notes.slice(0, 4))
|
|
64
|
+
console.log(` · ${String(n)}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Write a repair result to disk; returns the paths written. */
|
|
68
|
+
function writeResult(input, out, r) {
|
|
69
|
+
if (r.kind === "archive") {
|
|
70
|
+
const dir = out ?? join(parse(input).dir, `${parse(input).name}.recovered`);
|
|
71
|
+
mkdirSync(dir, { recursive: true });
|
|
72
|
+
const written = [];
|
|
73
|
+
for (const f of r.files) {
|
|
74
|
+
if (f.status !== "ok")
|
|
75
|
+
continue;
|
|
76
|
+
const safe = basename(f.name.replace(/[/\\]+/g, "_")) || "file";
|
|
77
|
+
const dest = join(dir, safe);
|
|
78
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
79
|
+
writeFileSync(dest, f.data);
|
|
80
|
+
written.push(dest);
|
|
81
|
+
}
|
|
82
|
+
return written;
|
|
83
|
+
}
|
|
84
|
+
const dest = out ?? defaultOut(input, r.family);
|
|
85
|
+
writeFileSync(dest, r.bytes);
|
|
86
|
+
return [dest];
|
|
87
|
+
}
|
|
88
|
+
/* -------------------------------- login -------------------------------- */
|
|
89
|
+
program
|
|
90
|
+
.command("login")
|
|
91
|
+
.description("Sign in (email + password). Use --link to get a one-time email link instead.")
|
|
92
|
+
.option("--link <email>", "email yourself a one-time sign-in link")
|
|
93
|
+
.option("--magic <token>", "complete sign-in with the token from that email link")
|
|
94
|
+
.action(async (opts) => {
|
|
95
|
+
try {
|
|
96
|
+
if (opts.magic) {
|
|
97
|
+
const res = await apiMagic(opts.magic.trim());
|
|
98
|
+
if (res.token)
|
|
99
|
+
setToken(res.token);
|
|
100
|
+
console.log("✓ Signed in.");
|
|
101
|
+
return summarize(res.subscription);
|
|
102
|
+
}
|
|
103
|
+
if (opts.link) {
|
|
104
|
+
const res = await apiRecover(opts.link.trim());
|
|
105
|
+
if (res.reason === "recovery-not-available") {
|
|
106
|
+
return die("Email sign-in isn't available right now — write to info@intactfile.com.");
|
|
107
|
+
}
|
|
108
|
+
console.log("✓ If that email has a purchase, a one-time link is on its way.\n" +
|
|
109
|
+
" Open it, copy the token after `magic=`, then run:\n" +
|
|
110
|
+
" intactfile login --magic <token>");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const email = await ask("Email: ");
|
|
114
|
+
const password = await askHidden("Password: ");
|
|
115
|
+
const res = await apiLogin(email, password);
|
|
116
|
+
if (res.token)
|
|
117
|
+
setToken(res.token);
|
|
118
|
+
console.log("✓ Signed in.");
|
|
119
|
+
summarize(res.subscription);
|
|
120
|
+
}
|
|
121
|
+
catch (e) {
|
|
122
|
+
handle(e);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
program
|
|
126
|
+
.command("logout")
|
|
127
|
+
.description("Forget the stored token on this machine.")
|
|
128
|
+
.action(() => {
|
|
129
|
+
clearAuth();
|
|
130
|
+
console.log("✓ Signed out.");
|
|
131
|
+
});
|
|
132
|
+
program
|
|
133
|
+
.command("status")
|
|
134
|
+
.description("Show the signed-in plan and remaining monthly repairs.")
|
|
135
|
+
.action(async () => {
|
|
136
|
+
const token = getToken();
|
|
137
|
+
if (!token)
|
|
138
|
+
return console.log("Not signed in. Run `intactfile login`.");
|
|
139
|
+
try {
|
|
140
|
+
const res = await balance(token);
|
|
141
|
+
if (res.token)
|
|
142
|
+
setToken(res.token);
|
|
143
|
+
console.log(`Credits balance: ${res.balance}`);
|
|
144
|
+
summarize(res.subscription);
|
|
145
|
+
}
|
|
146
|
+
catch (e) {
|
|
147
|
+
handle(e);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
function summarize(sub) {
|
|
151
|
+
if (!sub)
|
|
152
|
+
return console.log("Plan: none (no active subscription).");
|
|
153
|
+
console.log(`Plan: ${sub.tier} · ${sub.monthlyRemaining} repairs left this month · active until ${sub.activeUntil.slice(0, 10)}`);
|
|
154
|
+
}
|
|
155
|
+
/* ------------------------------- diagnose ------------------------------ */
|
|
156
|
+
program
|
|
157
|
+
.command("diagnose <file>")
|
|
158
|
+
.description("Inspect a file and report what's wrong. Free, fully local, no sign-in.")
|
|
159
|
+
.action(async (file) => {
|
|
160
|
+
try {
|
|
161
|
+
const d = await diagnosePath(file);
|
|
162
|
+
console.log(`File: ${basename(file)}`);
|
|
163
|
+
console.log(`Format: ${d.format}${d.family !== "unknown" ? ` (${d.family})` : ""}`);
|
|
164
|
+
console.log(`Confidence: ${d.confidence}`);
|
|
165
|
+
if (d.family === "unknown") {
|
|
166
|
+
console.log("Verdict: Not a format IntactFile can repair.");
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
console.log("Verdict: Repairable format — run `intactfile repair` (Business plan).");
|
|
170
|
+
}
|
|
171
|
+
for (const n of d.notes.slice(0, 6))
|
|
172
|
+
console.log(` · ${n}`);
|
|
173
|
+
}
|
|
174
|
+
catch (e) {
|
|
175
|
+
handle(e);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
/* -------------------------------- repair ------------------------------- */
|
|
179
|
+
program
|
|
180
|
+
.command("repair <file>")
|
|
181
|
+
.description("Repair a corrupted file locally (Business plan).")
|
|
182
|
+
.option("-o, --out <path>", "output path (or directory for archives)")
|
|
183
|
+
.option("-r, --reference <clip>", "healthy reference clip/photo (missing-moov video, JPEG donor)")
|
|
184
|
+
.option("-p, --password <pw>", "archive password (RAR/7z)")
|
|
185
|
+
.action(async (file, opts) => {
|
|
186
|
+
try {
|
|
187
|
+
const sub = await requireBusiness();
|
|
188
|
+
if (sub.monthlyRemaining <= 0) {
|
|
189
|
+
return die("Monthly repair allowance exhausted — it resets on your billing cycle.");
|
|
190
|
+
}
|
|
191
|
+
const r = await repairPath(file, { referencePath: opts.reference, password: opts.password });
|
|
192
|
+
printReport(r.family, r.report);
|
|
193
|
+
if (!r.usable) {
|
|
194
|
+
return die("Couldn't produce a usable result — nothing was charged.");
|
|
195
|
+
}
|
|
196
|
+
const written = writeResult(file, opts.out, r);
|
|
197
|
+
await meter();
|
|
198
|
+
console.log(`✓ Wrote:\n${written.map((w) => ` ${w}`).join("\n")}`);
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
handle(e);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
/* -------------------------------- batch -------------------------------- */
|
|
205
|
+
program
|
|
206
|
+
.command("batch <files...>")
|
|
207
|
+
.description("Repair up to 100 files (Business plan). Each is metered separately.")
|
|
208
|
+
.option("-o, --outdir <dir>", "directory to write results into")
|
|
209
|
+
.action(async (files, opts) => {
|
|
210
|
+
try {
|
|
211
|
+
await requireBusiness();
|
|
212
|
+
if (files.length > 100)
|
|
213
|
+
return die(`Batch is capped at 100 files (got ${files.length}).`);
|
|
214
|
+
let ok = 0;
|
|
215
|
+
let failed = 0;
|
|
216
|
+
for (const file of files) {
|
|
217
|
+
try {
|
|
218
|
+
const sub = await requireBusiness();
|
|
219
|
+
if (sub.monthlyRemaining <= 0) {
|
|
220
|
+
console.error("✗ Monthly allowance exhausted — stopping.");
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
const r = await repairPath(file);
|
|
224
|
+
if (!r.usable) {
|
|
225
|
+
console.log(`- ${basename(file)}: no usable result (skipped)`);
|
|
226
|
+
failed++;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const out = opts.outdir
|
|
230
|
+
? join(opts.outdir, basename(defaultOut(file, r.family)))
|
|
231
|
+
: undefined;
|
|
232
|
+
if (opts.outdir)
|
|
233
|
+
mkdirSync(opts.outdir, { recursive: true });
|
|
234
|
+
const written = writeResult(file, out, r);
|
|
235
|
+
await meter();
|
|
236
|
+
console.log(`✓ ${basename(file)} → ${written[0] ?? "(extracted)"}`);
|
|
237
|
+
ok++;
|
|
238
|
+
}
|
|
239
|
+
catch (e) {
|
|
240
|
+
failed++;
|
|
241
|
+
console.log(`✗ ${basename(file)}: ${e instanceof Error ? e.message : String(e)}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
console.log(`\nDone: ${ok} repaired, ${failed} skipped/failed.`);
|
|
245
|
+
}
|
|
246
|
+
catch (e) {
|
|
247
|
+
handle(e);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
/** Best-effort meter against the ledger (never blocks a delivered result). */
|
|
251
|
+
async function meter() {
|
|
252
|
+
const token = getToken();
|
|
253
|
+
if (!token)
|
|
254
|
+
return;
|
|
255
|
+
try {
|
|
256
|
+
const res = await consume(token);
|
|
257
|
+
if (res.token)
|
|
258
|
+
setToken(res.token);
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
/* offline/again-later: the file is already the user's; don't fail on metering */
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function handle(e) {
|
|
265
|
+
if (e instanceof EntitlementError)
|
|
266
|
+
return die(e.message);
|
|
267
|
+
if (e instanceof UnsupportedError)
|
|
268
|
+
return die(e.message);
|
|
269
|
+
if (e instanceof ApiError)
|
|
270
|
+
return die(e.message);
|
|
271
|
+
return die(e instanceof Error ? e.message : String(e));
|
|
272
|
+
}
|
|
273
|
+
program.parseAsync(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@intactfile/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "IntactFile CLI — repair corrupted files locally (video, ZIP/Office, PDF, JPEG, PNG, SQLite, RAR/7z). Your files never leave your machine. Business plan for repairs; diagnosis is free.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"bin": {
|
|
8
|
+
"intactfile": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=22"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"commander": "^12.1.0",
|
|
18
|
+
"@intactfile/node-core": "0.1.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^22.15.0",
|
|
22
|
+
"typescript": "^5.9.3"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc -p tsconfig.json"
|
|
26
|
+
}
|
|
27
|
+
}
|