@khemsok/tunl 0.1.1 → 0.1.3
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 +0 -31
- package/dist/cli.js +301 -57
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -21,16 +21,6 @@ Then just run `tunl` from anywhere.
|
|
|
21
21
|
3. Progressively reveals animated ASCII art as you stay focused
|
|
22
22
|
4. Unblocks everything when the session ends
|
|
23
23
|
|
|
24
|
-
## Art Themes
|
|
25
|
-
|
|
26
|
-
Three animated themes that evolve in real-time:
|
|
27
|
-
|
|
28
|
-
- **City Skyline** — buildings rise, windows flicker on, moon glows, stars twinkle, shooting stars streak across, clouds drift
|
|
29
|
-
- **Forest** — trees grow from trunks to full canopy, sun rises with rays, birds fly, butterflies flutter, flowers bloom
|
|
30
|
-
- **Space** — stars fill the void, planet forms with rings, nebula swirls, rocket builds and launches with animated fire, comets streak past
|
|
31
|
-
|
|
32
|
-
The art is procedurally generated and animated every 800ms — stars twinkle, windows flicker, neon signs pulse. Nothing is static.
|
|
33
|
-
|
|
34
24
|
## Controls
|
|
35
25
|
|
|
36
26
|
| Key | Action |
|
|
@@ -58,27 +48,6 @@ tunl --reset # reset config, re-run onboarding
|
|
|
58
48
|
tunl --help # show usage help
|
|
59
49
|
```
|
|
60
50
|
|
|
61
|
-
## How blocking works
|
|
62
|
-
|
|
63
|
-
tunl appends entries to `/etc/hosts` mapping blocked domains to `0.0.0.0`. This requires sudo access — you'll be prompted before the timer starts.
|
|
64
|
-
|
|
65
|
-
Sites are unblocked when:
|
|
66
|
-
- The session completes
|
|
67
|
-
- You stop the session with `r`
|
|
68
|
-
- You quit with `q` or `Ctrl+C`
|
|
69
|
-
- If the process crashes, the next run detects stale entries and cleans up
|
|
70
|
-
|
|
71
|
-
## Config
|
|
72
|
-
|
|
73
|
-
Preferences are saved to `~/.tunl.json` after first run. Edit with `tunl --config` to view, `tunl --reset` to start fresh, or press `s`/`t` in the app.
|
|
74
|
-
|
|
75
|
-
## Tech Stack
|
|
76
|
-
|
|
77
|
-
- **Runtime:** Bun
|
|
78
|
-
- **Language:** TypeScript
|
|
79
|
-
- **TUI Framework:** @opentui/react
|
|
80
|
-
- **Art:** Procedurally generated, animated every 800ms
|
|
81
|
-
|
|
82
51
|
## License
|
|
83
52
|
|
|
84
53
|
MIT
|
package/dist/cli.js
CHANGED
|
@@ -27633,6 +27633,11 @@ var init_chunk_bdqvmfwv = __esm(async () => {
|
|
|
27633
27633
|
import_react_devtools_core.default.connectToDevTools();
|
|
27634
27634
|
});
|
|
27635
27635
|
|
|
27636
|
+
// src/cli.tsx
|
|
27637
|
+
import { existsSync as existsSync9, unlinkSync as unlinkSync4 } from "fs";
|
|
27638
|
+
import { homedir as homedir5 } from "os";
|
|
27639
|
+
import { join as join6 } from "path";
|
|
27640
|
+
|
|
27636
27641
|
// node_modules/@opentui/core/index-qr7b6cvh.js
|
|
27637
27642
|
import { Buffer as Buffer2 } from "buffer";
|
|
27638
27643
|
import { EventEmitter } from "events";
|
|
@@ -55095,9 +55100,6 @@ var useTerminalDimensions = () => {
|
|
|
55095
55100
|
return dimensions;
|
|
55096
55101
|
};
|
|
55097
55102
|
|
|
55098
|
-
// src/cli.tsx
|
|
55099
|
-
import { execSync as execSync3 } from "child_process";
|
|
55100
|
-
|
|
55101
55103
|
// src/app.tsx
|
|
55102
55104
|
var import_react21 = __toESM(require_react(), 1);
|
|
55103
55105
|
|
|
@@ -56692,14 +56694,136 @@ function recordSession(durationMinutes) {
|
|
|
56692
56694
|
}
|
|
56693
56695
|
|
|
56694
56696
|
// src/lib/blocker.ts
|
|
56697
|
+
import { existsSync as existsSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2 } from "fs";
|
|
56698
|
+
import { execSync as execSync2 } from "child_process";
|
|
56699
|
+
import { homedir as homedir3 } from "os";
|
|
56700
|
+
import { join as join4 } from "path";
|
|
56701
|
+
|
|
56702
|
+
// src/lib/pf.ts
|
|
56695
56703
|
import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync } from "fs";
|
|
56696
56704
|
import { execSync } from "child_process";
|
|
56697
56705
|
import { homedir as homedir2 } from "os";
|
|
56698
56706
|
import { join as join3 } from "path";
|
|
56707
|
+
var PF_ANCHOR_PATH = "/etc/pf.anchors/tunl";
|
|
56708
|
+
var PF_CONF_PATH = "/etc/pf.conf";
|
|
56709
|
+
var PF_TOKEN_PATH = join3(homedir2(), ".tunl-pf-token");
|
|
56710
|
+
var ANCHOR_NAME = "tunl";
|
|
56711
|
+
function isValidDomain(domain) {
|
|
56712
|
+
return /^[a-zA-Z0-9.-]+$/.test(domain);
|
|
56713
|
+
}
|
|
56714
|
+
function digRecords(domain, recordType) {
|
|
56715
|
+
try {
|
|
56716
|
+
return execSync(`dig +short ${recordType} ${domain} 2>/dev/null`, {
|
|
56717
|
+
encoding: "utf-8",
|
|
56718
|
+
timeout: 5000
|
|
56719
|
+
}).trim().split(`
|
|
56720
|
+
`);
|
|
56721
|
+
} catch {
|
|
56722
|
+
return [];
|
|
56723
|
+
}
|
|
56724
|
+
}
|
|
56725
|
+
function resolveIPs(domain) {
|
|
56726
|
+
if (!isValidDomain(domain))
|
|
56727
|
+
return [];
|
|
56728
|
+
const v4 = digRecords(domain, "A").filter((line) => /^\d+\.\d+\.\d+\.\d+$/.test(line));
|
|
56729
|
+
const v6 = digRecords(domain, "AAAA").filter((line) => line.includes(":"));
|
|
56730
|
+
return [...v4, ...v6];
|
|
56731
|
+
}
|
|
56732
|
+
function generateRules(sites) {
|
|
56733
|
+
const lines = [
|
|
56734
|
+
"# tunl packet filter rules",
|
|
56735
|
+
"set block-policy drop",
|
|
56736
|
+
'set fingerprints "/etc/pf.os"',
|
|
56737
|
+
"set skip on lo0",
|
|
56738
|
+
""
|
|
56739
|
+
];
|
|
56740
|
+
const allDomains = sites.flatMap((s) => [s, `www.${s}`]);
|
|
56741
|
+
const seenIPs = new Set;
|
|
56742
|
+
for (const domain of allDomains) {
|
|
56743
|
+
const ips = resolveIPs(domain);
|
|
56744
|
+
for (const ip of ips) {
|
|
56745
|
+
if (seenIPs.has(ip))
|
|
56746
|
+
continue;
|
|
56747
|
+
seenIPs.add(ip);
|
|
56748
|
+
lines.push(`block return out proto tcp from any to ${ip}`);
|
|
56749
|
+
lines.push(`block return out proto udp from any to ${ip}`);
|
|
56750
|
+
}
|
|
56751
|
+
}
|
|
56752
|
+
return lines.join(`
|
|
56753
|
+
`) + `
|
|
56754
|
+
`;
|
|
56755
|
+
}
|
|
56756
|
+
function addAnchorToConf() {
|
|
56757
|
+
const conf = readFileSync2(PF_CONF_PATH, "utf-8");
|
|
56758
|
+
if (conf.includes(`/etc/pf.anchors/${ANCHOR_NAME}`))
|
|
56759
|
+
return;
|
|
56760
|
+
const addition = `
|
|
56761
|
+
anchor "${ANCHOR_NAME}"
|
|
56762
|
+
load anchor "${ANCHOR_NAME}" from "/etc/pf.anchors/${ANCHOR_NAME}"
|
|
56763
|
+
`;
|
|
56764
|
+
const newConf = conf.trimEnd() + addition;
|
|
56765
|
+
const tmpConf = join3(homedir2(), ".tunl-pf-conf.tmp");
|
|
56766
|
+
writeFileSync2(tmpConf, newConf);
|
|
56767
|
+
execSync(`sudo cp "${tmpConf}" ${PF_CONF_PATH}`);
|
|
56768
|
+
unlinkSync(tmpConf);
|
|
56769
|
+
}
|
|
56770
|
+
function removeAnchorFromConf() {
|
|
56771
|
+
const conf = readFileSync2(PF_CONF_PATH, "utf-8");
|
|
56772
|
+
if (!conf.includes(ANCHOR_NAME))
|
|
56773
|
+
return;
|
|
56774
|
+
const cleaned = conf.split(`
|
|
56775
|
+
`).filter((line) => !line.includes(ANCHOR_NAME)).join(`
|
|
56776
|
+
`).replace(/\n{3,}/g, `
|
|
56777
|
+
|
|
56778
|
+
`).trimEnd() + `
|
|
56779
|
+
`;
|
|
56780
|
+
const tmpConf = join3(homedir2(), ".tunl-pf-conf.tmp");
|
|
56781
|
+
writeFileSync2(tmpConf, cleaned);
|
|
56782
|
+
execSync(`sudo cp "${tmpConf}" ${PF_CONF_PATH}`);
|
|
56783
|
+
unlinkSync(tmpConf);
|
|
56784
|
+
}
|
|
56785
|
+
function blockPF(sites) {
|
|
56786
|
+
const rules = generateRules(sites);
|
|
56787
|
+
const tmpAnchor = join3(homedir2(), ".tunl-pf-anchor.tmp");
|
|
56788
|
+
writeFileSync2(tmpAnchor, rules);
|
|
56789
|
+
execSync(`sudo cp "${tmpAnchor}" ${PF_ANCHOR_PATH}`);
|
|
56790
|
+
unlinkSync(tmpAnchor);
|
|
56791
|
+
addAnchorToConf();
|
|
56792
|
+
const output = execSync(`sudo pfctl -E -f ${PF_CONF_PATH} -F states 2>&1`, { encoding: "utf-8" });
|
|
56793
|
+
const tokenMatch = output.match(/Token\s*:\s*(\S+)/);
|
|
56794
|
+
if (tokenMatch) {
|
|
56795
|
+
writeFileSync2(PF_TOKEN_PATH, tokenMatch[1]);
|
|
56796
|
+
}
|
|
56797
|
+
}
|
|
56798
|
+
function unblockPF() {
|
|
56799
|
+
try {
|
|
56800
|
+
const tmpEmpty = join3(homedir2(), ".tunl-pf-empty.tmp");
|
|
56801
|
+
writeFileSync2(tmpEmpty, "");
|
|
56802
|
+
execSync(`sudo cp "${tmpEmpty}" ${PF_ANCHOR_PATH}`);
|
|
56803
|
+
unlinkSync(tmpEmpty);
|
|
56804
|
+
} catch {}
|
|
56805
|
+
try {
|
|
56806
|
+
removeAnchorFromConf();
|
|
56807
|
+
} catch {}
|
|
56808
|
+
try {
|
|
56809
|
+
if (existsSync5(PF_TOKEN_PATH)) {
|
|
56810
|
+
const token = readFileSync2(PF_TOKEN_PATH, "utf-8").trim();
|
|
56811
|
+
if (token) {
|
|
56812
|
+
execSync(`sudo pfctl -X ${token} -f ${PF_CONF_PATH} 2>/dev/null`);
|
|
56813
|
+
}
|
|
56814
|
+
unlinkSync(PF_TOKEN_PATH);
|
|
56815
|
+
}
|
|
56816
|
+
} catch {}
|
|
56817
|
+
}
|
|
56818
|
+
function hasStalePFBlock() {
|
|
56819
|
+
return existsSync5(PF_TOKEN_PATH);
|
|
56820
|
+
}
|
|
56821
|
+
|
|
56822
|
+
// src/lib/blocker.ts
|
|
56699
56823
|
var HOSTS_PATH = "/etc/hosts";
|
|
56700
56824
|
var START_MARKER = "# --- tunl start ---";
|
|
56701
56825
|
var END_MARKER = "# --- tunl end ---";
|
|
56702
|
-
var PID_PATH =
|
|
56826
|
+
var PID_PATH = join4(homedir3(), ".tunl.pid");
|
|
56703
56827
|
function validateSite(site) {
|
|
56704
56828
|
return /^[a-zA-Z0-9.-]+$/.test(site);
|
|
56705
56829
|
}
|
|
@@ -56712,41 +56836,41 @@ function blockHosts(sites) {
|
|
|
56712
56836
|
`:: www.${s}`
|
|
56713
56837
|
]).join(`
|
|
56714
56838
|
`);
|
|
56715
|
-
const currentHosts =
|
|
56839
|
+
const currentHosts = readFileSync3(HOSTS_PATH, "utf-8").trimEnd();
|
|
56716
56840
|
const block = `${currentHosts}
|
|
56717
56841
|
${START_MARKER}
|
|
56718
56842
|
${entries}
|
|
56719
56843
|
${END_MARKER}
|
|
56720
56844
|
`;
|
|
56721
|
-
const tmpPath =
|
|
56722
|
-
|
|
56723
|
-
|
|
56724
|
-
|
|
56845
|
+
const tmpPath = join4(homedir3(), ".tunl-block.tmp");
|
|
56846
|
+
writeFileSync3(tmpPath, block);
|
|
56847
|
+
execSync2(`sudo cp "${tmpPath}" ${HOSTS_PATH}`);
|
|
56848
|
+
unlinkSync2(tmpPath);
|
|
56725
56849
|
}
|
|
56726
56850
|
function unblockHosts() {
|
|
56727
56851
|
try {
|
|
56728
|
-
const hosts =
|
|
56852
|
+
const hosts = readFileSync3(HOSTS_PATH, "utf-8");
|
|
56729
56853
|
const startIdx = hosts.indexOf(START_MARKER);
|
|
56730
56854
|
const endIdx = hosts.indexOf(END_MARKER);
|
|
56731
56855
|
if (startIdx === -1 || endIdx === -1)
|
|
56732
56856
|
return;
|
|
56733
56857
|
const cleaned = (hosts.slice(0, startIdx) + hosts.slice(endIdx + END_MARKER.length)).trimEnd() + `
|
|
56734
56858
|
`;
|
|
56735
|
-
const tmpPath =
|
|
56736
|
-
|
|
56737
|
-
|
|
56738
|
-
|
|
56859
|
+
const tmpPath = join4(homedir3(), ".tunl-hosts.tmp");
|
|
56860
|
+
writeFileSync3(tmpPath, cleaned);
|
|
56861
|
+
execSync2(`sudo cp "${tmpPath}" ${HOSTS_PATH}`);
|
|
56862
|
+
unlinkSync2(tmpPath);
|
|
56739
56863
|
} catch {}
|
|
56740
56864
|
}
|
|
56741
56865
|
function flushDNS() {
|
|
56742
56866
|
try {
|
|
56743
|
-
|
|
56867
|
+
execSync2("sudo dscacheutil -flushcache 2>/dev/null");
|
|
56744
56868
|
} catch {}
|
|
56745
56869
|
try {
|
|
56746
|
-
|
|
56870
|
+
execSync2("sudo killall -HUP mDNSResponder 2>/dev/null");
|
|
56747
56871
|
} catch {}
|
|
56748
56872
|
try {
|
|
56749
|
-
|
|
56873
|
+
execSync2("sudo killall mDNSResponderHelper 2>/dev/null");
|
|
56750
56874
|
} catch {}
|
|
56751
56875
|
}
|
|
56752
56876
|
function blockSites(sites) {
|
|
@@ -56754,22 +56878,33 @@ function blockSites(sites) {
|
|
|
56754
56878
|
const validSites = sites.filter(validateSite);
|
|
56755
56879
|
if (validSites.length === 0)
|
|
56756
56880
|
return;
|
|
56757
|
-
|
|
56881
|
+
writeFileSync3(PID_PATH, String(process.pid));
|
|
56882
|
+
try {
|
|
56883
|
+
blockPF(validSites);
|
|
56884
|
+
} catch {}
|
|
56758
56885
|
blockHosts(validSites);
|
|
56759
56886
|
flushDNS();
|
|
56760
56887
|
}
|
|
56761
56888
|
function unblockSites() {
|
|
56762
56889
|
unblockHosts();
|
|
56890
|
+
try {
|
|
56891
|
+
unblockPF();
|
|
56892
|
+
} catch {}
|
|
56763
56893
|
flushDNS();
|
|
56764
56894
|
try {
|
|
56765
|
-
if (
|
|
56766
|
-
|
|
56895
|
+
if (existsSync6(PID_PATH))
|
|
56896
|
+
unlinkSync2(PID_PATH);
|
|
56767
56897
|
} catch {}
|
|
56768
56898
|
}
|
|
56769
56899
|
function cleanupStaleBlocks() {
|
|
56770
|
-
if (
|
|
56900
|
+
if (hasStalePFBlock()) {
|
|
56901
|
+
try {
|
|
56902
|
+
unblockPF();
|
|
56903
|
+
} catch {}
|
|
56904
|
+
}
|
|
56905
|
+
if (!existsSync6(PID_PATH))
|
|
56771
56906
|
return;
|
|
56772
|
-
const pid = parseInt(
|
|
56907
|
+
const pid = parseInt(readFileSync3(PID_PATH, "utf-8").trim());
|
|
56773
56908
|
try {
|
|
56774
56909
|
process.kill(pid, 0);
|
|
56775
56910
|
} catch {
|
|
@@ -56918,7 +57053,7 @@ function useBlocker() {
|
|
|
56918
57053
|
// src/app.tsx
|
|
56919
57054
|
var ALL_THEMES = [cityTheme, forestTheme, spaceTheme];
|
|
56920
57055
|
function App({ initialDuration, noblock, extraBlocks }) {
|
|
56921
|
-
const config = loadConfig();
|
|
57056
|
+
const [config, setConfig] = import_react21.useState(() => loadConfig());
|
|
56922
57057
|
const [screen, setScreen] = import_react21.useState(config.isFirstRun ? "onboarding" : "timer");
|
|
56923
57058
|
const [blockedSites, setBlockedSites] = import_react21.useState(config.blockedSites);
|
|
56924
57059
|
const [currentTheme, setCurrentTheme] = import_react21.useState(ALL_THEMES.find((t2) => t2.name === config.theme) || cityTheme);
|
|
@@ -56927,6 +57062,7 @@ function App({ initialDuration, noblock, extraBlocks }) {
|
|
|
56927
57062
|
const timer = useTimer(initialSeconds, () => {
|
|
56928
57063
|
setScreen("completed");
|
|
56929
57064
|
recordSession(Math.round(timer.totalSeconds / 60));
|
|
57065
|
+
setConfig(loadConfig());
|
|
56930
57066
|
stopBlocking();
|
|
56931
57067
|
});
|
|
56932
57068
|
const { animTick, resetAnimation } = useAnimation(timer.timerStatus);
|
|
@@ -57122,10 +57258,10 @@ function TimerStatusMessage({
|
|
|
57122
57258
|
/* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("span", {
|
|
57123
57259
|
fg: COLORS.textMuted,
|
|
57124
57260
|
children: [
|
|
57125
|
-
config.totalSessions
|
|
57261
|
+
config.totalSessions,
|
|
57126
57262
|
" sessions \xB7",
|
|
57127
57263
|
" ",
|
|
57128
|
-
config.totalMinutesFocused
|
|
57264
|
+
config.totalMinutesFocused,
|
|
57129
57265
|
" min total",
|
|
57130
57266
|
config.currentStreak > 0 ? ` \xB7 ${config.currentStreak} day streak` : ""
|
|
57131
57267
|
]
|
|
@@ -57166,10 +57302,10 @@ function TimerStatusMessage({
|
|
|
57166
57302
|
}
|
|
57167
57303
|
|
|
57168
57304
|
// src/lib/browser-dns.ts
|
|
57169
|
-
import { existsSync as
|
|
57170
|
-
import { execSync as
|
|
57171
|
-
import { homedir as
|
|
57172
|
-
import { join as
|
|
57305
|
+
import { existsSync as existsSync7, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3 } from "fs";
|
|
57306
|
+
import { execSync as execSync3 } from "child_process";
|
|
57307
|
+
import { homedir as homedir4 } from "os";
|
|
57308
|
+
import { join as join5 } from "path";
|
|
57173
57309
|
var BROWSERS = [
|
|
57174
57310
|
"com.google.Chrome",
|
|
57175
57311
|
"com.google.Chrome.canary",
|
|
@@ -57179,19 +57315,82 @@ var BROWSERS = [
|
|
|
57179
57315
|
"com.vivaldi.Vivaldi",
|
|
57180
57316
|
"com.operasoftware.Opera"
|
|
57181
57317
|
];
|
|
57182
|
-
var DNS_SETUP_FLAG =
|
|
57318
|
+
var DNS_SETUP_FLAG = join5(homedir4(), ".tunl-dns-setup");
|
|
57183
57319
|
function setupBrowserDNS() {
|
|
57184
|
-
if (
|
|
57320
|
+
if (existsSync7(DNS_SETUP_FLAG))
|
|
57185
57321
|
return false;
|
|
57186
57322
|
for (const bundle of BROWSERS) {
|
|
57187
57323
|
try {
|
|
57188
|
-
|
|
57189
|
-
|
|
57324
|
+
execSync3(`defaults write ${bundle} BuiltInDnsClientEnabled -bool false 2>/dev/null`);
|
|
57325
|
+
execSync3(`defaults write ${bundle} DnsOverHttpsMode -string "off" 2>/dev/null`);
|
|
57190
57326
|
} catch {}
|
|
57191
57327
|
}
|
|
57192
|
-
|
|
57328
|
+
writeFileSync4(DNS_SETUP_FLAG, new Date().toISOString());
|
|
57193
57329
|
return true;
|
|
57194
57330
|
}
|
|
57331
|
+
function restoreBrowserDNS() {
|
|
57332
|
+
for (const bundle of BROWSERS) {
|
|
57333
|
+
try {
|
|
57334
|
+
execSync3(`defaults delete ${bundle} BuiltInDnsClientEnabled 2>/dev/null`);
|
|
57335
|
+
execSync3(`defaults delete ${bundle} DnsOverHttpsMode 2>/dev/null`);
|
|
57336
|
+
} catch {}
|
|
57337
|
+
}
|
|
57338
|
+
try {
|
|
57339
|
+
unlinkSync3(DNS_SETUP_FLAG);
|
|
57340
|
+
} catch {}
|
|
57341
|
+
}
|
|
57342
|
+
|
|
57343
|
+
// src/lib/sudo-setup.ts
|
|
57344
|
+
import { existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
57345
|
+
import { execSync as execSync4 } from "child_process";
|
|
57346
|
+
import { userInfo } from "os";
|
|
57347
|
+
var SUDOERS_PATH = "/etc/sudoers.d/tunl";
|
|
57348
|
+
var SUDOERS_VERSION = "2";
|
|
57349
|
+
function buildRules() {
|
|
57350
|
+
const username = userInfo().username;
|
|
57351
|
+
return [
|
|
57352
|
+
`${username} ALL=(root) NOPASSWD: /bin/cp */.tunl-block.tmp /etc/hosts`,
|
|
57353
|
+
`${username} ALL=(root) NOPASSWD: /bin/cp */.tunl-hosts.tmp /etc/hosts`,
|
|
57354
|
+
`${username} ALL=(root) NOPASSWD: /usr/sbin/dscacheutil -flushcache`,
|
|
57355
|
+
`${username} ALL=(root) NOPASSWD: /usr/bin/killall -HUP mDNSResponder`,
|
|
57356
|
+
`${username} ALL=(root) NOPASSWD: /usr/bin/killall mDNSResponderHelper`,
|
|
57357
|
+
`${username} ALL=(root) NOPASSWD: /sbin/pfctl *`,
|
|
57358
|
+
`${username} ALL=(root) NOPASSWD: /bin/cp */.tunl-pf-anchor.tmp /etc/pf.anchors/tunl`,
|
|
57359
|
+
`${username} ALL=(root) NOPASSWD: /bin/cp */.tunl-pf-empty.tmp /etc/pf.anchors/tunl`,
|
|
57360
|
+
`${username} ALL=(root) NOPASSWD: /bin/cp */.tunl-pf-conf.tmp /etc/pf.conf`
|
|
57361
|
+
].join(`
|
|
57362
|
+
`);
|
|
57363
|
+
}
|
|
57364
|
+
function sudoersInstalled() {
|
|
57365
|
+
if (!existsSync8(SUDOERS_PATH))
|
|
57366
|
+
return false;
|
|
57367
|
+
try {
|
|
57368
|
+
const content = readFileSync4(SUDOERS_PATH, "utf-8");
|
|
57369
|
+
return content.includes(`# tunl-version: ${SUDOERS_VERSION}`);
|
|
57370
|
+
} catch {
|
|
57371
|
+
return false;
|
|
57372
|
+
}
|
|
57373
|
+
}
|
|
57374
|
+
function installSudoers() {
|
|
57375
|
+
const rules = buildRules();
|
|
57376
|
+
const content = `# tunl - terminal focus timer
|
|
57377
|
+
# tunl-version: ${SUDOERS_VERSION}
|
|
57378
|
+
# allows site blocking without password prompts
|
|
57379
|
+
${rules}
|
|
57380
|
+
`;
|
|
57381
|
+
const tmpPath = "/tmp/tunl-sudoers";
|
|
57382
|
+
writeFileSync5(tmpPath, content, { mode: 288 });
|
|
57383
|
+
execSync4(`sudo visudo -cf ${tmpPath}`);
|
|
57384
|
+
execSync4(`sudo install -m 0440 ${tmpPath} ${SUDOERS_PATH}`);
|
|
57385
|
+
execSync4(`rm ${tmpPath}`);
|
|
57386
|
+
}
|
|
57387
|
+
function removeSudoers() {
|
|
57388
|
+
if (existsSync8(SUDOERS_PATH)) {
|
|
57389
|
+
try {
|
|
57390
|
+
execSync4(`sudo rm ${SUDOERS_PATH}`);
|
|
57391
|
+
} catch {}
|
|
57392
|
+
}
|
|
57393
|
+
}
|
|
57195
57394
|
|
|
57196
57395
|
// src/utils/args.ts
|
|
57197
57396
|
function parseArgs(argv) {
|
|
@@ -57202,7 +57401,8 @@ function parseArgs(argv) {
|
|
|
57202
57401
|
sites: undefined,
|
|
57203
57402
|
showConfig: false,
|
|
57204
57403
|
showStats: false,
|
|
57205
|
-
resetConfig: false
|
|
57404
|
+
resetConfig: false,
|
|
57405
|
+
uninstall: false
|
|
57206
57406
|
};
|
|
57207
57407
|
for (let i = 0;i < argv.length; i++) {
|
|
57208
57408
|
const arg = argv[i];
|
|
@@ -57223,6 +57423,8 @@ function parseArgs(argv) {
|
|
|
57223
57423
|
opts.showStats = true;
|
|
57224
57424
|
} else if (arg === "--reset") {
|
|
57225
57425
|
opts.resetConfig = true;
|
|
57426
|
+
} else if (arg === "--uninstall") {
|
|
57427
|
+
opts.uninstall = true;
|
|
57226
57428
|
} else if (arg === "--help" || arg === "-h") {
|
|
57227
57429
|
printHelp();
|
|
57228
57430
|
process.exit(0);
|
|
@@ -57243,6 +57445,7 @@ function printHelp() {
|
|
|
57243
57445
|
tunl --stats Show focus stats and streaks
|
|
57244
57446
|
tunl --config Show current saved config
|
|
57245
57447
|
tunl --reset Reset config (re-run onboarding)
|
|
57448
|
+
tunl --uninstall Remove all tunl system files (sudoers, config, DNS settings)
|
|
57246
57449
|
tunl --help Show this help
|
|
57247
57450
|
|
|
57248
57451
|
Controls:
|
|
@@ -57264,7 +57467,11 @@ async function main2() {
|
|
|
57264
57467
|
process.exit(0);
|
|
57265
57468
|
}
|
|
57266
57469
|
if (args.resetConfig) {
|
|
57267
|
-
|
|
57470
|
+
resetConfig();
|
|
57471
|
+
process.exit(0);
|
|
57472
|
+
}
|
|
57473
|
+
if (args.uninstall) {
|
|
57474
|
+
uninstall();
|
|
57268
57475
|
process.exit(0);
|
|
57269
57476
|
}
|
|
57270
57477
|
if (args.sites) {
|
|
@@ -57284,24 +57491,23 @@ async function main2() {
|
|
|
57284
57491
|
console.log(` This only needs to happen once.
|
|
57285
57492
|
`);
|
|
57286
57493
|
}
|
|
57287
|
-
|
|
57288
|
-
|
|
57289
|
-
|
|
57290
|
-
\u25C9 tunl \u2014 Terminal Focus Timer
|
|
57494
|
+
if (!sudoersInstalled()) {
|
|
57495
|
+
console.log(`
|
|
57496
|
+
\u25C9 tunl \u2014 setup
|
|
57291
57497
|
`);
|
|
57292
|
-
|
|
57293
|
-
|
|
57498
|
+
console.log(" tunl needs to install a passwordless sudo rule so it can");
|
|
57499
|
+
console.log(" block sites without prompting you every time.");
|
|
57500
|
+
console.log(` You'll enter your password once \u2014 never again after this.
|
|
57294
57501
|
`);
|
|
57295
|
-
|
|
57296
|
-
|
|
57297
|
-
|
|
57298
|
-
\u2713 Ready! Entering focus mode...
|
|
57502
|
+
try {
|
|
57503
|
+
installSudoers();
|
|
57504
|
+
console.log(` \u2713 sudo rule installed. No more password prompts!
|
|
57299
57505
|
`);
|
|
57300
|
-
|
|
57301
|
-
|
|
57302
|
-
\u2715 Could not get sudo access. Running without site blocking.
|
|
57506
|
+
} catch {
|
|
57507
|
+
console.log(` \u2715 Could not install sudo rule. Running without site blocking.
|
|
57303
57508
|
`);
|
|
57304
|
-
|
|
57509
|
+
args.noblock = true;
|
|
57510
|
+
}
|
|
57305
57511
|
}
|
|
57306
57512
|
}
|
|
57307
57513
|
const renderer = await createCliRenderer({
|
|
@@ -57344,13 +57550,10 @@ function printConfig() {
|
|
|
57344
57550
|
console.log(` First run: ${config.isFirstRun}
|
|
57345
57551
|
`);
|
|
57346
57552
|
}
|
|
57347
|
-
|
|
57348
|
-
const
|
|
57349
|
-
|
|
57350
|
-
|
|
57351
|
-
const configPath = join5(homedir4(), ".tunl.json");
|
|
57352
|
-
if (existsSync4(configPath)) {
|
|
57353
|
-
unlinkSync3(configPath);
|
|
57553
|
+
function resetConfig() {
|
|
57554
|
+
const configPath = join6(homedir5(), ".tunl.json");
|
|
57555
|
+
if (existsSync9(configPath)) {
|
|
57556
|
+
unlinkSync4(configPath);
|
|
57354
57557
|
console.log(`
|
|
57355
57558
|
\u2713 Config reset. Next run will show onboarding.
|
|
57356
57559
|
`);
|
|
@@ -57360,6 +57563,47 @@ async function resetConfig() {
|
|
|
57360
57563
|
`);
|
|
57361
57564
|
}
|
|
57362
57565
|
}
|
|
57566
|
+
function uninstall() {
|
|
57567
|
+
console.log(`
|
|
57568
|
+
\u25C9 tunl \u2014 uninstall
|
|
57569
|
+
`);
|
|
57570
|
+
if (sudoersInstalled()) {
|
|
57571
|
+
try {
|
|
57572
|
+
removeSudoers();
|
|
57573
|
+
console.log(" \u2713 Removed sudo rule (/etc/sudoers.d/tunl)");
|
|
57574
|
+
} catch {
|
|
57575
|
+
console.log(" \u2715 Could not remove sudo rule (may need manual sudo)");
|
|
57576
|
+
}
|
|
57577
|
+
} else {
|
|
57578
|
+
console.log(" - No sudo rule found");
|
|
57579
|
+
}
|
|
57580
|
+
const configPath = join6(homedir5(), ".tunl.json");
|
|
57581
|
+
if (existsSync9(configPath)) {
|
|
57582
|
+
unlinkSync4(configPath);
|
|
57583
|
+
console.log(" \u2713 Removed config (~/.tunl.json)");
|
|
57584
|
+
} else {
|
|
57585
|
+
console.log(" - No config found");
|
|
57586
|
+
}
|
|
57587
|
+
const dnsFlag = join6(homedir5(), ".tunl-dns-setup");
|
|
57588
|
+
if (existsSync9(dnsFlag)) {
|
|
57589
|
+
unlinkSync4(dnsFlag);
|
|
57590
|
+
console.log(" \u2713 Removed DNS setup flag");
|
|
57591
|
+
}
|
|
57592
|
+
const pidPath = join6(homedir5(), ".tunl.pid");
|
|
57593
|
+
if (existsSync9(pidPath)) {
|
|
57594
|
+
unlinkSync4(pidPath);
|
|
57595
|
+
console.log(" \u2713 Removed PID file");
|
|
57596
|
+
}
|
|
57597
|
+
try {
|
|
57598
|
+
unblockPF();
|
|
57599
|
+
console.log(" \u2713 Cleaned up firewall rules");
|
|
57600
|
+
} catch {
|
|
57601
|
+
console.log(" - No firewall rules to clean");
|
|
57602
|
+
}
|
|
57603
|
+
restoreBrowserDNS();
|
|
57604
|
+
console.log(" \u2713 Restored browser DNS settings");
|
|
57605
|
+
console.log("\n All clean. Run `bun remove -g @khemsok/tunl` to finish.\n");
|
|
57606
|
+
}
|
|
57363
57607
|
main2().catch((err) => {
|
|
57364
57608
|
const renderer = globalThis.__tunl_renderer;
|
|
57365
57609
|
if (renderer) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khemsok/tunl",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Terminal focus timer that blocks distracting sites and progressively reveals animated ASCII art",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
14
|
"dev": "bun run src/cli.tsx",
|
|
15
|
+
"dev:noblock": "bun run src/cli.tsx --noblock",
|
|
16
|
+
"dev:reset": "bun run src/cli.tsx --reset",
|
|
17
|
+
"dev:stats": "bun run src/cli.tsx --stats",
|
|
18
|
+
"dev:config": "bun run src/cli.tsx --config",
|
|
19
|
+
"dev:uninstall": "bun run src/cli.tsx --uninstall",
|
|
15
20
|
"build": "bun build src/cli.tsx --outdir dist --target bun",
|
|
16
21
|
"postinstall": "bun run build"
|
|
17
22
|
},
|