brew-tui 0.2.0 → 0.3.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/README.md +5 -4
- package/build/{brewbar-installer-4Z2WE57I.js → brewbar-installer-H5MLNNTD.js} +52 -20
- package/build/brewbar-installer-H5MLNNTD.js.map +1 -0
- package/build/chunk-65YZJX2E.js +103 -0
- package/build/chunk-65YZJX2E.js.map +1 -0
- package/build/{chunk-KXDTKY3E.js → chunk-PTLSNG2N.js} +107 -521
- package/build/chunk-PTLSNG2N.js.map +1 -0
- package/build/{history-logger-65UF2R6F.js → history-logger-2PGYSPFL.js} +2 -2
- package/build/history-logger-2PGYSPFL.js.map +1 -0
- package/build/index.js +1647 -769
- package/build/index.js.map +1 -0
- package/package.json +1 -1
- package/build/chunk-UBHTQL7T.js +0 -76
package/README.md
CHANGED
|
@@ -36,9 +36,6 @@ bun add -g brew-tui
|
|
|
36
36
|
brew tap MoLinesGitHub/tap
|
|
37
37
|
brew install brew-tui
|
|
38
38
|
|
|
39
|
-
# GitHub Packages
|
|
40
|
-
npm install -g @MoLinesGitHub/brew-tui --registry https://npm.pkg.github.com
|
|
41
|
-
|
|
42
39
|
# npx (run without installing)
|
|
43
40
|
npx brew-tui
|
|
44
41
|
```
|
|
@@ -47,8 +44,9 @@ npx brew-tui
|
|
|
47
44
|
|
|
48
45
|
```bash
|
|
49
46
|
brew-tui # Launch the TUI
|
|
50
|
-
brew-tui status # Show license status
|
|
47
|
+
brew-tui status # Show evaluated license status
|
|
51
48
|
brew-tui activate <key> # Activate Pro license
|
|
49
|
+
brew-tui revalidate # Revalidate the current Pro license
|
|
52
50
|
brew-tui deactivate # Deactivate Pro license
|
|
53
51
|
```
|
|
54
52
|
|
|
@@ -62,6 +60,8 @@ brew-tui install-brewbar --force # Reinstall / update
|
|
|
62
60
|
brew-tui uninstall-brewbar # Remove from /Applications
|
|
63
61
|
```
|
|
64
62
|
|
|
63
|
+
If BrewBar or `brew-tui status` reports that your Pro license needs refreshing, run `brew-tui revalidate` before retrying.
|
|
64
|
+
|
|
65
65
|
### Keyboard Navigation
|
|
66
66
|
|
|
67
67
|
| Key | Action |
|
|
@@ -85,6 +85,7 @@ Brew-TUI supports English and Spanish. The language is detected automatically fr
|
|
|
85
85
|
## BrewBar
|
|
86
86
|
|
|
87
87
|
BrewBar is a companion macOS menu bar app (Swift 6 / SwiftUI) that shows outdated package counts, sends notifications, and lets you upgrade packages without opening a terminal.
|
|
88
|
+
Expired Pro licenses fall back to basic mode until they are revalidated or renewed.
|
|
88
89
|
|
|
89
90
|
BrewBar lives in the `menubar/` directory and is built separately with [Tuist](https://tuist.io):
|
|
90
91
|
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
2
|
fetchWithTimeout,
|
|
3
|
-
t
|
|
4
|
-
|
|
5
|
-
verifyPro
|
|
6
|
-
} from "./chunk-KXDTKY3E.js";
|
|
3
|
+
t
|
|
4
|
+
} from "./chunk-PTLSNG2N.js";
|
|
7
5
|
|
|
8
6
|
// src/lib/brewbar-installer.ts
|
|
9
7
|
import { rm, access, readFile } from "fs/promises";
|
|
10
8
|
import { createWriteStream } from "fs";
|
|
11
|
-
import { createHash } from "crypto";
|
|
9
|
+
import { createHash, randomUUID } from "crypto";
|
|
10
|
+
import { tmpdir } from "os";
|
|
11
|
+
import { join } from "path";
|
|
12
12
|
import { pipeline } from "stream/promises";
|
|
13
13
|
import { execFile } from "child_process";
|
|
14
14
|
import { promisify } from "util";
|
|
15
15
|
var execFileAsync = promisify(execFile);
|
|
16
16
|
var BREWBAR_APP_PATH = "/Applications/BrewBar.app";
|
|
17
17
|
var DOWNLOAD_URL = "https://github.com/MoLinesGitHub/Brew-TUI/releases/latest/download/BrewBar.app.zip";
|
|
18
|
-
var
|
|
18
|
+
var MAX_SIZE = 200 * 1024 * 1024;
|
|
19
19
|
async function isBrewBarInstalled() {
|
|
20
20
|
try {
|
|
21
21
|
await access(BREWBAR_APP_PATH);
|
|
@@ -24,42 +24,73 @@ async function isBrewBarInstalled() {
|
|
|
24
24
|
return false;
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
-
async function installBrewBar(force = false) {
|
|
27
|
+
async function installBrewBar(isPro, force = false) {
|
|
28
28
|
if (process.platform !== "darwin") {
|
|
29
29
|
throw new Error(t("cli_brewbarMacOnly"));
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
if (!verifyPro(license, status)) {
|
|
31
|
+
if (!isPro) {
|
|
33
32
|
throw new Error(t("cli_brewbarProRequired"));
|
|
34
33
|
}
|
|
35
34
|
if (!force && await isBrewBarInstalled()) {
|
|
36
35
|
throw new Error(t("cli_brewbarAlreadyInstalled"));
|
|
37
36
|
}
|
|
38
37
|
console.log(t("cli_brewbarInstalling"));
|
|
38
|
+
const TMP_ZIP = join(tmpdir(), "BrewBar-" + randomUUID() + ".zip");
|
|
39
39
|
const res = await fetchWithTimeout(DOWNLOAD_URL, {}, 12e4);
|
|
40
40
|
if (!res.ok || !res.body) {
|
|
41
41
|
throw new Error(t("cli_brewbarDownloadFailed", { error: `HTTP ${res.status}` }));
|
|
42
42
|
}
|
|
43
43
|
const contentLength = Number(res.headers.get("content-length") ?? "0");
|
|
44
|
-
if (contentLength >
|
|
44
|
+
if (contentLength > MAX_SIZE) {
|
|
45
45
|
throw new Error(t("cli_brewbarDownloadFailed", { error: "Download exceeds 200 MB size limit" }));
|
|
46
46
|
}
|
|
47
|
+
let downloadedBytes = 0;
|
|
47
48
|
const fileStream = createWriteStream(TMP_ZIP);
|
|
48
|
-
|
|
49
|
+
const transformedBody = new ReadableStream({
|
|
50
|
+
async start(controller) {
|
|
51
|
+
const bodyReader = res.body.getReader();
|
|
52
|
+
try {
|
|
53
|
+
while (true) {
|
|
54
|
+
const { done, value } = await bodyReader.read();
|
|
55
|
+
if (done) break;
|
|
56
|
+
downloadedBytes += value.length;
|
|
57
|
+
if (downloadedBytes > MAX_SIZE) {
|
|
58
|
+
controller.error(new Error("Download exceeds 200 MB limit"));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
controller.enqueue(value);
|
|
62
|
+
}
|
|
63
|
+
controller.close();
|
|
64
|
+
} catch (err) {
|
|
65
|
+
controller.error(err);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
await pipeline(transformedBody, fileStream);
|
|
70
|
+
let expectedHash = null;
|
|
49
71
|
try {
|
|
50
72
|
const checksumRes = await fetchWithTimeout(`${DOWNLOAD_URL}.sha256`, {}, 15e3);
|
|
51
73
|
if (checksumRes.ok) {
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
await rm(TMP_ZIP, { force: true }).catch(() => {
|
|
57
|
-
});
|
|
58
|
-
throw new Error(t("cli_brewbarDownloadFailed", { error: "SHA-256 checksum mismatch" }));
|
|
74
|
+
const text = await checksumRes.text();
|
|
75
|
+
const hash = text.trim().split(/\s+/)[0];
|
|
76
|
+
if (hash && /^[0-9a-f]{64}$/i.test(hash)) {
|
|
77
|
+
expectedHash = hash.toLowerCase();
|
|
59
78
|
}
|
|
60
79
|
}
|
|
61
|
-
} catch
|
|
62
|
-
|
|
80
|
+
} catch {
|
|
81
|
+
}
|
|
82
|
+
if (expectedHash) {
|
|
83
|
+
const fileBuffer = await readFile(TMP_ZIP);
|
|
84
|
+
const actual = createHash("sha256").update(fileBuffer).digest("hex");
|
|
85
|
+
if (actual !== expectedHash) {
|
|
86
|
+
await rm(TMP_ZIP, { force: true }).catch(() => {
|
|
87
|
+
});
|
|
88
|
+
throw new Error(t("cli_brewbarDownloadFailed", { error: "SHA-256 mismatch: binary may have been tampered with" }));
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
await rm(TMP_ZIP, { force: true }).catch(() => {
|
|
92
|
+
});
|
|
93
|
+
throw new Error(t("cli_brewbarDownloadFailed", { error: "SHA-256 checksum unavailable \u2014 cannot verify download integrity" }));
|
|
63
94
|
}
|
|
64
95
|
if (force && await isBrewBarInstalled()) {
|
|
65
96
|
await rm(BREWBAR_APP_PATH, { recursive: true, force: true });
|
|
@@ -84,3 +115,4 @@ export {
|
|
|
84
115
|
isBrewBarInstalled,
|
|
85
116
|
uninstallBrewBar
|
|
86
117
|
};
|
|
118
|
+
//# sourceMappingURL=brewbar-installer-H5MLNNTD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/brewbar-installer.ts"],"sourcesContent":["import { rm, access, readFile } from 'node:fs/promises';\nimport { createWriteStream } from 'node:fs';\nimport { createHash, randomUUID } from 'node:crypto';\nimport { tmpdir } from 'node:os';\nimport { join } from 'node:path';\nimport { pipeline } from 'node:stream/promises';\nimport { execFile } from 'node:child_process';\nimport { promisify } from 'node:util';\nimport { t } from '../i18n/index.js';\nimport { fetchWithTimeout } from './fetch-timeout.js';\n\nconst execFileAsync = promisify(execFile);\nconst BREWBAR_APP_PATH = '/Applications/BrewBar.app';\nconst DOWNLOAD_URL = 'https://github.com/MoLinesGitHub/Brew-TUI/releases/latest/download/BrewBar.app.zip';\nconst MAX_SIZE = 200 * 1024 * 1024; // 200 MB\n\nexport async function isBrewBarInstalled(): Promise<boolean> {\n try {\n await access(BREWBAR_APP_PATH);\n return true;\n } catch {\n return false;\n }\n}\n\nexport async function installBrewBar(isPro: boolean, force = false): Promise<void> {\n // macOS only\n if (process.platform !== 'darwin') {\n throw new Error(t('cli_brewbarMacOnly'));\n }\n\n // Pro check\n if (!isPro) {\n throw new Error(t('cli_brewbarProRequired'));\n }\n\n // Already installed check\n if (!force && await isBrewBarInstalled()) {\n throw new Error(t('cli_brewbarAlreadyInstalled'));\n }\n\n console.log(t('cli_brewbarInstalling'));\n\n // EP-013: Use unique temp path\n const TMP_ZIP = join(tmpdir(), 'BrewBar-' + randomUUID() + '.zip');\n\n // Download zip (120s timeout for large binary)\n const res = await fetchWithTimeout(DOWNLOAD_URL, {}, 120_000);\n if (!res.ok || !res.body) {\n throw new Error(t('cli_brewbarDownloadFailed', { error: `HTTP ${res.status}` }));\n }\n\n // Reject downloads larger than 200 MB (from Content-Length header)\n const contentLength = Number(res.headers.get('content-length') ?? '0');\n if (contentLength > MAX_SIZE) {\n throw new Error(t('cli_brewbarDownloadFailed', { error: 'Download exceeds 200 MB size limit' }));\n }\n\n // EP-005: Track downloaded bytes during the stream\n let downloadedBytes = 0;\n\n // Write to tmp file with byte counting\n const fileStream = createWriteStream(TMP_ZIP);\n const transformedBody = new ReadableStream({\n async start(controller) {\n const bodyReader = (res.body as ReadableStream<Uint8Array>).getReader();\n try {\n while (true) {\n const { done, value } = await bodyReader.read();\n if (done) break;\n downloadedBytes += value.length;\n if (downloadedBytes > MAX_SIZE) {\n controller.error(new Error('Download exceeds 200 MB limit'));\n return;\n }\n controller.enqueue(value);\n }\n controller.close();\n } catch (err) {\n controller.error(err);\n }\n },\n });\n await pipeline(transformedBody as unknown as NodeJS.ReadableStream, fileStream);\n\n // SEG-001: SHA-256 integrity check with proper error handling\n let expectedHash: string | null = null;\n try {\n const checksumRes = await fetchWithTimeout(`${DOWNLOAD_URL}.sha256`, {}, 15_000);\n if (checksumRes.ok) {\n const text = await checksumRes.text();\n // EP-009: Validate split result is defined\n const hash = text.trim().split(/\\s+/)[0];\n // EP-010: Validate hash format\n if (hash && /^[0-9a-f]{64}$/i.test(hash)) {\n expectedHash = hash.toLowerCase();\n }\n }\n } catch {\n /* checksum file not available */\n }\n\n if (expectedHash) {\n const fileBuffer = await readFile(TMP_ZIP);\n const actual = createHash('sha256').update(fileBuffer).digest('hex');\n if (actual !== expectedHash) {\n await rm(TMP_ZIP, { force: true }).catch(() => {});\n throw new Error(t('cli_brewbarDownloadFailed', { error: 'SHA-256 mismatch: binary may have been tampered with' }));\n }\n } else {\n // NUEVO-003: Treat missing checksum as fatal — don't install unverified binaries\n await rm(TMP_ZIP, { force: true }).catch(() => {});\n throw new Error(t('cli_brewbarDownloadFailed', { error: 'SHA-256 checksum unavailable — cannot verify download integrity' }));\n }\n\n // Remove old app if force reinstall\n if (force && await isBrewBarInstalled()) {\n await rm(BREWBAR_APP_PATH, { recursive: true, force: true });\n }\n\n // Unzip to /Applications\n try {\n await execFileAsync('ditto', ['-xk', TMP_ZIP, '/Applications/']);\n } catch (err) {\n throw new Error(t('cli_brewbarDownloadFailed', { error: err instanceof Error ? err.message : String(err) }));\n } finally {\n // Clean up tmp zip\n await rm(TMP_ZIP, { force: true }).catch(() => {});\n }\n}\n\nexport async function uninstallBrewBar(): Promise<void> {\n if (!await isBrewBarInstalled()) {\n throw new Error(t('cli_brewbarNotInstalled'));\n }\n\n await rm(BREWBAR_APP_PATH, { recursive: true, force: true });\n}\n"],"mappings":";;;;;;AAAA,SAAS,IAAI,QAAQ,gBAAgB;AACrC,SAAS,yBAAyB;AAClC,SAAS,YAAY,kBAAkB;AACvC,SAAS,cAAc;AACvB,SAAS,YAAY;AACrB,SAAS,gBAAgB;AACzB,SAAS,gBAAgB;AACzB,SAAS,iBAAiB;AAI1B,IAAM,gBAAgB,UAAU,QAAQ;AACxC,IAAM,mBAAmB;AACzB,IAAM,eAAe;AACrB,IAAM,WAAW,MAAM,OAAO;AAE9B,eAAsB,qBAAuC;AAC3D,MAAI;AACF,UAAM,OAAO,gBAAgB;AAC7B,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,eAAe,OAAgB,QAAQ,OAAsB;AAEjF,MAAI,QAAQ,aAAa,UAAU;AACjC,UAAM,IAAI,MAAM,EAAE,oBAAoB,CAAC;AAAA,EACzC;AAGA,MAAI,CAAC,OAAO;AACV,UAAM,IAAI,MAAM,EAAE,wBAAwB,CAAC;AAAA,EAC7C;AAGA,MAAI,CAAC,SAAS,MAAM,mBAAmB,GAAG;AACxC,UAAM,IAAI,MAAM,EAAE,6BAA6B,CAAC;AAAA,EAClD;AAEA,UAAQ,IAAI,EAAE,uBAAuB,CAAC;AAGtC,QAAM,UAAU,KAAK,OAAO,GAAG,aAAa,WAAW,IAAI,MAAM;AAGjE,QAAM,MAAM,MAAM,iBAAiB,cAAc,CAAC,GAAG,IAAO;AAC5D,MAAI,CAAC,IAAI,MAAM,CAAC,IAAI,MAAM;AACxB,UAAM,IAAI,MAAM,EAAE,6BAA6B,EAAE,OAAO,QAAQ,IAAI,MAAM,GAAG,CAAC,CAAC;AAAA,EACjF;AAGA,QAAM,gBAAgB,OAAO,IAAI,QAAQ,IAAI,gBAAgB,KAAK,GAAG;AACrE,MAAI,gBAAgB,UAAU;AAC5B,UAAM,IAAI,MAAM,EAAE,6BAA6B,EAAE,OAAO,qCAAqC,CAAC,CAAC;AAAA,EACjG;AAGA,MAAI,kBAAkB;AAGtB,QAAM,aAAa,kBAAkB,OAAO;AAC5C,QAAM,kBAAkB,IAAI,eAAe;AAAA,IACzC,MAAM,MAAM,YAAY;AACtB,YAAM,aAAc,IAAI,KAAoC,UAAU;AACtE,UAAI;AACF,eAAO,MAAM;AACX,gBAAM,EAAE,MAAM,MAAM,IAAI,MAAM,WAAW,KAAK;AAC9C,cAAI,KAAM;AACV,6BAAmB,MAAM;AACzB,cAAI,kBAAkB,UAAU;AAC9B,uBAAW,MAAM,IAAI,MAAM,+BAA+B,CAAC;AAC3D;AAAA,UACF;AACA,qBAAW,QAAQ,KAAK;AAAA,QAC1B;AACA,mBAAW,MAAM;AAAA,MACnB,SAAS,KAAK;AACZ,mBAAW,MAAM,GAAG;AAAA,MACtB;AAAA,IACF;AAAA,EACF,CAAC;AACD,QAAM,SAAS,iBAAqD,UAAU;AAG9E,MAAI,eAA8B;AAClC,MAAI;AACF,UAAM,cAAc,MAAM,iBAAiB,GAAG,YAAY,WAAW,CAAC,GAAG,IAAM;AAC/E,QAAI,YAAY,IAAI;AAClB,YAAM,OAAO,MAAM,YAAY,KAAK;AAEpC,YAAM,OAAO,KAAK,KAAK,EAAE,MAAM,KAAK,EAAE,CAAC;AAEvC,UAAI,QAAQ,kBAAkB,KAAK,IAAI,GAAG;AACxC,uBAAe,KAAK,YAAY;AAAA,MAClC;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AAEA,MAAI,cAAc;AAChB,UAAM,aAAa,MAAM,SAAS,OAAO;AACzC,UAAM,SAAS,WAAW,QAAQ,EAAE,OAAO,UAAU,EAAE,OAAO,KAAK;AACnE,QAAI,WAAW,cAAc;AAC3B,YAAM,GAAG,SAAS,EAAE,OAAO,KAAK,CAAC,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AACjD,YAAM,IAAI,MAAM,EAAE,6BAA6B,EAAE,OAAO,uDAAuD,CAAC,CAAC;AAAA,IACnH;AAAA,EACF,OAAO;AAEL,UAAM,GAAG,SAAS,EAAE,OAAO,KAAK,CAAC,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AACjD,UAAM,IAAI,MAAM,EAAE,6BAA6B,EAAE,OAAO,uEAAkE,CAAC,CAAC;AAAA,EAC9H;AAGA,MAAI,SAAS,MAAM,mBAAmB,GAAG;AACvC,UAAM,GAAG,kBAAkB,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,EAC7D;AAGA,MAAI;AACF,UAAM,cAAc,SAAS,CAAC,OAAO,SAAS,gBAAgB,CAAC;AAAA,EACjE,SAAS,KAAK;AACZ,UAAM,IAAI,MAAM,EAAE,6BAA6B,EAAE,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE,CAAC,CAAC;AAAA,EAC7G,UAAE;AAEA,UAAM,GAAG,SAAS,EAAE,OAAO,KAAK,CAAC,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AAAA,EACnD;AACF;AAEA,eAAsB,mBAAkC;AACtD,MAAI,CAAC,MAAM,mBAAmB,GAAG;AAC/B,UAAM,IAAI,MAAM,EAAE,yBAAyB,CAAC;AAAA,EAC9C;AAEA,QAAM,GAAG,kBAAkB,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAC7D;","names":[]}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// src/lib/history/history-logger.ts
|
|
2
|
+
import { readFile, writeFile, rename, open, unlink } from "fs/promises";
|
|
3
|
+
import { randomUUID } from "crypto";
|
|
4
|
+
|
|
5
|
+
// src/lib/data-dir.ts
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
import { mkdir } from "fs/promises";
|
|
9
|
+
var DATA_DIR = join(homedir(), ".brew-tui");
|
|
10
|
+
var PROFILES_DIR = join(DATA_DIR, "profiles");
|
|
11
|
+
var LICENSE_PATH = join(DATA_DIR, "license.json");
|
|
12
|
+
var HISTORY_PATH = join(DATA_DIR, "history.json");
|
|
13
|
+
async function ensureDataDirs() {
|
|
14
|
+
await mkdir(DATA_DIR, { recursive: true, mode: 448 });
|
|
15
|
+
await mkdir(PROFILES_DIR, { recursive: true, mode: 448 });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// src/lib/history/history-logger.ts
|
|
19
|
+
var MAX_ENTRIES = 1e3;
|
|
20
|
+
function assertPro(isPro) {
|
|
21
|
+
if (!isPro) throw new Error("Pro license required");
|
|
22
|
+
}
|
|
23
|
+
var lockPath = HISTORY_PATH + ".lock";
|
|
24
|
+
async function withLock(fn) {
|
|
25
|
+
const lockFd = await open(lockPath, "wx").catch(() => null);
|
|
26
|
+
if (!lockFd) throw new Error("History file is locked by another process");
|
|
27
|
+
try {
|
|
28
|
+
return await fn();
|
|
29
|
+
} finally {
|
|
30
|
+
await lockFd.close();
|
|
31
|
+
await unlink(lockPath).catch(() => {
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function detectAction(args) {
|
|
36
|
+
const cmd = args[0];
|
|
37
|
+
if (cmd === "install") return { action: "install", packageName: args[1] ?? null };
|
|
38
|
+
if (cmd === "uninstall") {
|
|
39
|
+
const name = args.find((a) => !a.startsWith("-")) === "uninstall" ? args.find((a, i) => i > 0 && !a.startsWith("-")) ?? null : args[1] ?? null;
|
|
40
|
+
return { action: "uninstall", packageName: name };
|
|
41
|
+
}
|
|
42
|
+
if (cmd === "upgrade") {
|
|
43
|
+
if (args.length === 1) return { action: "upgrade-all", packageName: null };
|
|
44
|
+
return { action: "upgrade", packageName: args[1] ?? null };
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
async function loadHistory(isPro) {
|
|
49
|
+
assertPro(isPro);
|
|
50
|
+
try {
|
|
51
|
+
const raw = await readFile(HISTORY_PATH, "utf-8");
|
|
52
|
+
const file = JSON.parse(raw);
|
|
53
|
+
if (file.version !== 1) {
|
|
54
|
+
throw new Error("Unsupported data version");
|
|
55
|
+
}
|
|
56
|
+
const entries = file.entries;
|
|
57
|
+
return Array.isArray(entries) ? entries : [];
|
|
58
|
+
} catch {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function saveHistory(entries) {
|
|
63
|
+
await ensureDataDirs();
|
|
64
|
+
const file = { version: 1, entries };
|
|
65
|
+
const tmp = HISTORY_PATH + ".tmp";
|
|
66
|
+
await writeFile(tmp, JSON.stringify(file, null, 2), { encoding: "utf-8", mode: 384 });
|
|
67
|
+
await rename(tmp, HISTORY_PATH);
|
|
68
|
+
}
|
|
69
|
+
async function appendEntry(isPro, action, packageName, success, error = null) {
|
|
70
|
+
assertPro(isPro);
|
|
71
|
+
await withLock(async () => {
|
|
72
|
+
const entries = await loadHistory(isPro);
|
|
73
|
+
const entry = {
|
|
74
|
+
id: randomUUID(),
|
|
75
|
+
action,
|
|
76
|
+
packageName,
|
|
77
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
78
|
+
success,
|
|
79
|
+
error
|
|
80
|
+
};
|
|
81
|
+
entries.unshift(entry);
|
|
82
|
+
if (entries.length > MAX_ENTRIES) {
|
|
83
|
+
entries.length = MAX_ENTRIES;
|
|
84
|
+
}
|
|
85
|
+
await saveHistory(entries);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async function clearHistory(isPro) {
|
|
89
|
+
assertPro(isPro);
|
|
90
|
+
await saveHistory([]);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export {
|
|
94
|
+
DATA_DIR,
|
|
95
|
+
PROFILES_DIR,
|
|
96
|
+
LICENSE_PATH,
|
|
97
|
+
ensureDataDirs,
|
|
98
|
+
detectAction,
|
|
99
|
+
loadHistory,
|
|
100
|
+
appendEntry,
|
|
101
|
+
clearHistory
|
|
102
|
+
};
|
|
103
|
+
//# sourceMappingURL=chunk-65YZJX2E.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/history/history-logger.ts","../src/lib/data-dir.ts"],"sourcesContent":["import { readFile, writeFile, rename, open, unlink } from 'node:fs/promises';\nimport { randomUUID } from 'node:crypto';\nimport { HISTORY_PATH, ensureDataDirs } from '../data-dir.js';\nimport type { HistoryEntry, HistoryFile, HistoryAction } from './types.js';\n\nconst MAX_ENTRIES = 1000;\n\nfunction assertPro(isPro: boolean): void {\n if (!isPro) throw new Error('Pro license required');\n}\n\n// ── BK-004: Simple file locking ──\nconst lockPath = HISTORY_PATH + '.lock';\n\nasync function withLock<T>(fn: () => Promise<T>): Promise<T> {\n const lockFd = await open(lockPath, 'wx').catch(() => null);\n if (!lockFd) throw new Error('History file is locked by another process');\n try {\n return await fn();\n } finally {\n await lockFd.close();\n await unlink(lockPath).catch(() => {});\n }\n}\n\n/** Map brew subcommand to a history action type */\nexport function detectAction(args: string[]): { action: HistoryAction; packageName: string | null } | null {\n const cmd = args[0];\n if (cmd === 'install') return { action: 'install', packageName: args[1] ?? null };\n if (cmd === 'uninstall') {\n const name = args.find((a) => !a.startsWith('-')) === 'uninstall'\n ? args.find((a, i) => i > 0 && !a.startsWith('-')) ?? null\n : args[1] ?? null;\n return { action: 'uninstall', packageName: name };\n }\n if (cmd === 'upgrade') {\n if (args.length === 1) return { action: 'upgrade-all', packageName: null };\n return { action: 'upgrade', packageName: args[1] ?? null };\n }\n return null;\n}\n\nexport async function loadHistory(isPro: boolean): Promise<HistoryEntry[]> {\n assertPro(isPro);\n\n try {\n const raw = await readFile(HISTORY_PATH, 'utf-8');\n const file = JSON.parse(raw) as HistoryFile;\n if (file.version !== 1) {\n // Future: add migration logic here\n throw new Error('Unsupported data version');\n }\n const entries = file.entries;\n return Array.isArray(entries) ? entries : [];\n } catch {\n return [];\n }\n}\n\nasync function saveHistory(entries: HistoryEntry[]): Promise<void> {\n await ensureDataDirs();\n const file: HistoryFile = { version: 1, entries };\n const tmp = HISTORY_PATH + '.tmp';\n await writeFile(tmp, JSON.stringify(file, null, 2), { encoding: 'utf-8', mode: 0o600 });\n await rename(tmp, HISTORY_PATH);\n}\n\nexport async function appendEntry(\n isPro: boolean,\n action: HistoryAction,\n packageName: string | null,\n success: boolean,\n error: string | null = null,\n): Promise<void> {\n assertPro(isPro);\n\n await withLock(async () => {\n const entries = await loadHistory(isPro);\n\n const entry: HistoryEntry = {\n id: randomUUID(),\n action,\n packageName,\n timestamp: new Date().toISOString(),\n success,\n error,\n };\n\n entries.unshift(entry);\n\n if (entries.length > MAX_ENTRIES) {\n entries.length = MAX_ENTRIES;\n }\n\n await saveHistory(entries);\n });\n}\n\nexport async function clearHistory(isPro: boolean): Promise<void> {\n assertPro(isPro);\n await saveHistory([]);\n}\n","import { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { mkdir } from 'node:fs/promises';\n\nexport const DATA_DIR = join(homedir(), '.brew-tui');\nexport const PROFILES_DIR = join(DATA_DIR, 'profiles');\nexport const LICENSE_PATH = join(DATA_DIR, 'license.json');\nexport const HISTORY_PATH = join(DATA_DIR, 'history.json');\n\nexport async function ensureDataDirs(): Promise<void> {\n await mkdir(DATA_DIR, { recursive: true, mode: 0o700 });\n await mkdir(PROFILES_DIR, { recursive: true, mode: 0o700 });\n}\n"],"mappings":";AAAA,SAAS,UAAU,WAAW,QAAQ,MAAM,cAAc;AAC1D,SAAS,kBAAkB;;;ACD3B,SAAS,eAAe;AACxB,SAAS,YAAY;AACrB,SAAS,aAAa;AAEf,IAAM,WAAW,KAAK,QAAQ,GAAG,WAAW;AAC5C,IAAM,eAAe,KAAK,UAAU,UAAU;AAC9C,IAAM,eAAe,KAAK,UAAU,cAAc;AAClD,IAAM,eAAe,KAAK,UAAU,cAAc;AAEzD,eAAsB,iBAAgC;AACpD,QAAM,MAAM,UAAU,EAAE,WAAW,MAAM,MAAM,IAAM,CAAC;AACtD,QAAM,MAAM,cAAc,EAAE,WAAW,MAAM,MAAM,IAAM,CAAC;AAC5D;;;ADPA,IAAM,cAAc;AAEpB,SAAS,UAAU,OAAsB;AACvC,MAAI,CAAC,MAAO,OAAM,IAAI,MAAM,sBAAsB;AACpD;AAGA,IAAM,WAAW,eAAe;AAEhC,eAAe,SAAY,IAAkC;AAC3D,QAAM,SAAS,MAAM,KAAK,UAAU,IAAI,EAAE,MAAM,MAAM,IAAI;AAC1D,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,2CAA2C;AACxE,MAAI;AACF,WAAO,MAAM,GAAG;AAAA,EAClB,UAAE;AACA,UAAM,OAAO,MAAM;AACnB,UAAM,OAAO,QAAQ,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AAAA,EACvC;AACF;AAGO,SAAS,aAAa,MAA8E;AACzG,QAAM,MAAM,KAAK,CAAC;AAClB,MAAI,QAAQ,UAAW,QAAO,EAAE,QAAQ,WAAW,aAAa,KAAK,CAAC,KAAK,KAAK;AAChF,MAAI,QAAQ,aAAa;AACvB,UAAM,OAAO,KAAK,KAAK,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,CAAC,MAAM,cAClD,KAAK,KAAK,CAAC,GAAG,MAAM,IAAI,KAAK,CAAC,EAAE,WAAW,GAAG,CAAC,KAAK,OACpD,KAAK,CAAC,KAAK;AACf,WAAO,EAAE,QAAQ,aAAa,aAAa,KAAK;AAAA,EAClD;AACA,MAAI,QAAQ,WAAW;AACrB,QAAI,KAAK,WAAW,EAAG,QAAO,EAAE,QAAQ,eAAe,aAAa,KAAK;AACzE,WAAO,EAAE,QAAQ,WAAW,aAAa,KAAK,CAAC,KAAK,KAAK;AAAA,EAC3D;AACA,SAAO;AACT;AAEA,eAAsB,YAAY,OAAyC;AACzE,YAAU,KAAK;AAEf,MAAI;AACF,UAAM,MAAM,MAAM,SAAS,cAAc,OAAO;AAChD,UAAM,OAAO,KAAK,MAAM,GAAG;AAC3B,QAAI,KAAK,YAAY,GAAG;AAEtB,YAAM,IAAI,MAAM,0BAA0B;AAAA,IAC5C;AACA,UAAM,UAAU,KAAK;AACrB,WAAO,MAAM,QAAQ,OAAO,IAAI,UAAU,CAAC;AAAA,EAC7C,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAEA,eAAe,YAAY,SAAwC;AACjE,QAAM,eAAe;AACrB,QAAM,OAAoB,EAAE,SAAS,GAAG,QAAQ;AAChD,QAAM,MAAM,eAAe;AAC3B,QAAM,UAAU,KAAK,KAAK,UAAU,MAAM,MAAM,CAAC,GAAG,EAAE,UAAU,SAAS,MAAM,IAAM,CAAC;AACtF,QAAM,OAAO,KAAK,YAAY;AAChC;AAEA,eAAsB,YACpB,OACA,QACA,aACA,SACA,QAAuB,MACR;AACf,YAAU,KAAK;AAEf,QAAM,SAAS,YAAY;AACzB,UAAM,UAAU,MAAM,YAAY,KAAK;AAEvC,UAAM,QAAsB;AAAA,MAC1B,IAAI,WAAW;AAAA,MACf;AAAA,MACA;AAAA,MACA,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MAClC;AAAA,MACA;AAAA,IACF;AAEA,YAAQ,QAAQ,KAAK;AAErB,QAAI,QAAQ,SAAS,aAAa;AAChC,cAAQ,SAAS;AAAA,IACnB;AAEA,UAAM,YAAY,OAAO;AAAA,EAC3B,CAAC;AACH;AAEA,eAAsB,aAAa,OAA+B;AAChE,YAAU,KAAK;AACf,QAAM,YAAY,CAAC,CAAC;AACtB;","names":[]}
|