@expitc/termad 0.0.0-stage → 0.1.1

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.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/POLICY.md +91 -0
  3. package/README.md +94 -2
  4. package/assets/pty-overlay.mjs +5 -0
  5. package/assets/wrapper.cmd.asset +31 -0
  6. package/assets/wrapper.sh.asset +29 -0
  7. package/assets/wrapper.sh.overlay +45 -0
  8. package/dist/assets/pty-overlay.mjs +5 -0
  9. package/dist/assets/wrapper.cmd.asset +31 -0
  10. package/dist/assets/wrapper.sh.asset +29 -0
  11. package/dist/assets/wrapper.sh.overlay +45 -0
  12. package/dist/audit.d.ts +9 -0
  13. package/dist/audit.js +54 -0
  14. package/dist/cli.d.ts +2 -0
  15. package/dist/cli.js +193 -0
  16. package/dist/core/ad.d.ts +19 -0
  17. package/dist/core/ad.js +12 -0
  18. package/dist/core/config.d.ts +18 -0
  19. package/dist/core/config.js +33 -0
  20. package/dist/core/manifest.d.ts +20 -0
  21. package/dist/core/manifest.js +42 -0
  22. package/dist/core/paths.d.ts +24 -0
  23. package/dist/core/paths.js +30 -0
  24. package/dist/core/source.d.ts +18 -0
  25. package/dist/core/source.js +78 -0
  26. package/dist/index.d.ts +9 -0
  27. package/dist/index.js +9 -0
  28. package/dist/install.d.ts +26 -0
  29. package/dist/install.js +182 -0
  30. package/dist/render/render.d.ts +20 -0
  31. package/dist/render/render.js +47 -0
  32. package/dist/report.d.ts +7 -0
  33. package/dist/report.js +91 -0
  34. package/dist/surfaces/inline.d.ts +17 -0
  35. package/dist/surfaces/inline.js +171 -0
  36. package/dist/surfaces/pty-inject.d.ts +1 -0
  37. package/dist/surfaces/pty-inject.js +133 -0
  38. package/dist/surfaces/statusline.d.ts +9 -0
  39. package/dist/surfaces/statusline.js +115 -0
  40. package/dist/surfaces/surfaces.d.ts +10 -0
  41. package/dist/surfaces/surfaces.js +47 -0
  42. package/dist/surfaces/wrap.d.ts +16 -0
  43. package/dist/surfaces/wrap.js +111 -0
  44. package/package.json +49 -4
package/dist/cli.js ADDED
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * termad — opt-in, reversible text ads for terminal AI CLIs.
4
+ *
5
+ * termad install [claude|codex|opencode|gemini ...] install (default: all opted-in)
6
+ * termad uninstall [surface ...] full revert
7
+ * termad audit re-verify every recorded change
8
+ * termad status what's installed where
9
+ * termad sync fetch ad now, write cache
10
+ * termad watch poll endpoint, refresh cache
11
+ * termad set-endpoint <url> point at your ad server
12
+ * termad style line|banner render style
13
+ */
14
+ import { ALL_SURFACES, loadConfig, saveConfig } from "./core/config.js";
15
+ import { readSync } from "node:fs";
16
+ import { paths, termadHome } from "./core/paths.js";
17
+ import { readAdCache, fetchAd } from "./core/source.js";
18
+ import { install, uninstall, sync, planInstall } from "./install.js";
19
+ import { surfaces } from "./surfaces/surfaces.js";
20
+ import { isWrapped } from "./surfaces/wrap.js";
21
+ import { isStatuslineInstalled } from "./surfaces/statusline.js";
22
+ import { render } from "./render/render.js";
23
+ import { audit, auditFailed } from "./audit.js";
24
+ const [cmd, ...args] = process.argv.slice(2);
25
+ import { reportDisplays } from "./report.js";
26
+ function confirmPlan(surfaces, force = false) {
27
+ if (args.includes("--yes"))
28
+ return;
29
+ const plan = planInstall(surfaces, force);
30
+ console.log("termad will make these changes (nothing happens without your consent):\n");
31
+ for (const p of plan) {
32
+ console.log(` ${p.surface.padEnd(9)} ${p.action}`);
33
+ console.log(` ${"".padEnd(9)} → ${p.target}`);
34
+ }
35
+ console.log(`
36
+ • every change is sha256-recorded in ${paths.manifest()}
37
+ • \`termad uninstall\` restores everything byte-for-byte
38
+ • \`termad audit\` re-verifies all of it at any time`);
39
+ if (!process.stdin.isTTY) {
40
+ console.error("\nnon-interactive session: re-run with --yes to confirm");
41
+ process.exit(1);
42
+ }
43
+ process.stdout.write("\nProceed? [y/N] ");
44
+ const answer = readlineSync();
45
+ if (!/^y(es)?$/i.test(answer.trim())) {
46
+ console.log("aborted — nothing changed");
47
+ process.exit(1);
48
+ }
49
+ }
50
+ function readlineSync() {
51
+ // Minimal blocking read for the consent prompt (no deps).
52
+ try {
53
+ const buf = Buffer.alloc(1024);
54
+ const n = readSync(0, buf, 0, 1024, null);
55
+ return buf.subarray(0, n).toString("utf8");
56
+ }
57
+ catch {
58
+ return "";
59
+ }
60
+ }
61
+ function parseSurfaces(argv) {
62
+ const picked = argv.filter((a) => ALL_SURFACES.includes(a));
63
+ return picked.length ? picked : undefined;
64
+ }
65
+ function printOutcomes(out) {
66
+ for (const o of out)
67
+ console.log(` ${o.ok ? "✓" : "✗"} ${o.surface.padEnd(9)} ${o.detail}`);
68
+ if (out.some((o) => !o.ok))
69
+ process.exitCode = 1;
70
+ }
71
+ switch (cmd) {
72
+ case "install": {
73
+ const force = args.includes("--force");
74
+ confirmPlan(parseSurfaces(args), force);
75
+ const out = await install(parseSurfaces(args), { force });
76
+ printOutcomes(out);
77
+ const ad = readAdCache();
78
+ if (ad)
79
+ console.log(`\n ad cache: ${paths.adCache()}\n ${render(ad, loadConfig().style)}`);
80
+ const rows = audit();
81
+ if (rows.length)
82
+ console.log(` audit: ${rows.length} change(s) recorded → \`termad audit\` to re-verify`);
83
+ break;
84
+ }
85
+ case "uninstall": {
86
+ const out = uninstall(parseSurfaces(args));
87
+ printOutcomes(out);
88
+ break;
89
+ }
90
+ case "status": {
91
+ const cfg = loadConfig();
92
+ console.log(`termad home : ${termadHome()}`);
93
+ console.log(`endpoint : ${cfg.endpoint ?? "(none — local/house ads only)"}`);
94
+ console.log(`style : ${cfg.style}`);
95
+ console.log("");
96
+ for (const s of surfaces) {
97
+ const bin = s.locate();
98
+ const state = s.id === "claude"
99
+ ? isStatuslineInstalled()
100
+ ? "installed (statusline)"
101
+ : "—"
102
+ : bin && isWrapped(bin)
103
+ ? `installed (wrapper)`
104
+ : "—";
105
+ console.log(` ${s.id.padEnd(9)} ${String(bin ?? "not found").padEnd(42)} ${state}`);
106
+ }
107
+ const ad = readAdCache();
108
+ if (ad)
109
+ console.log(`\ncached ad:\n${render(ad, cfg.style)}`);
110
+ break;
111
+ }
112
+ case "audit": {
113
+ const rows = audit();
114
+ if (!rows.length) {
115
+ console.log(` no recorded changes (nothing installed by this CLI yet)`);
116
+ break;
117
+ }
118
+ console.log(` ${"SURFACE".padEnd(9)} ${"STATUS".padEnd(9)} DETAIL`);
119
+ for (const r of rows) {
120
+ const icon = r.status === "ok" ? "✓" : r.status === "tampered" || r.status === "missing" ? "✗" : "•";
121
+ console.log(` ${icon} ${r.surface.padEnd(9)} ${r.status.padEnd(9)} ${r.detail}`);
122
+ console.log(` ${"".padEnd(9)} ${"".padEnd(9)} ${r.target}`);
123
+ }
124
+ if (auditFailed(rows)) {
125
+ console.log(`\n integrity problems found — see entries above`);
126
+ process.exit(1);
127
+ }
128
+ console.log(`\n all recorded changes verified (sha256)`);
129
+ break;
130
+ }
131
+ case "sync": {
132
+ const text = await sync();
133
+ const rep = await reportDisplays();
134
+ console.log(` ad cache refreshed: ${text}`);
135
+ if (rep)
136
+ console.log(` display report: ${rep}`);
137
+ else
138
+ console.log(` display report: nothing pending (or endpoint unreachable — counts retained)`);
139
+ break;
140
+ }
141
+ case "watch": {
142
+ const cfg = loadConfig();
143
+ if (!cfg.endpoint) {
144
+ console.error(" no endpoint configured — run: termad set-endpoint <url>");
145
+ process.exit(1);
146
+ }
147
+ console.log(` watching ${cfg.endpoint} every ${cfg.pollSeconds}s (ctrl-c to stop)`);
148
+ for (;;) {
149
+ const text = await sync().catch(() => null);
150
+ if (text)
151
+ process.stdout.write(` ${new Date().toISOString()} ${text}\n`);
152
+ await new Promise((r) => setTimeout(r, cfg.pollSeconds * 1000));
153
+ }
154
+ break;
155
+ }
156
+ case "set-endpoint": {
157
+ if (!args[0]) {
158
+ console.error(" usage: termad set-endpoint <url>");
159
+ process.exit(1);
160
+ }
161
+ const cfg = loadConfig();
162
+ cfg.endpoint = args[0].replace(/\/+$/, "");
163
+ saveConfig(cfg);
164
+ console.log(` endpoint set: ${cfg.endpoint}`);
165
+ // Pull the first ad immediately so the cache isn't empty.
166
+ const ad = await fetchAd("claude").catch(() => null);
167
+ if (ad)
168
+ console.log(` first ad: ${ad.text}`);
169
+ break;
170
+ }
171
+ case "style": {
172
+ if (args[0] !== "line" && args[0] !== "banner") {
173
+ console.error(" usage: termad style line|banner");
174
+ process.exit(1);
175
+ }
176
+ const cfg = loadConfig();
177
+ cfg.style = args[0];
178
+ saveConfig(cfg);
179
+ console.log(` style set: ${args[0]}`);
180
+ break;
181
+ }
182
+ default:
183
+ console.log(`termad — opt-in, reversible text ads for terminal AI CLIs
184
+
185
+ termad install [surface ...] install (claude | codex | opencode | gemini)
186
+ termad uninstall [surface ...] remove everything termad added
187
+ termad audit re-verify every recorded change (sha256)
188
+ termad status show installed surfaces + cached ad
189
+ termad set-endpoint <url> point at your ad server
190
+ termad sync fetch ad now + report pending displays
191
+ termad watch poll the endpoint continuously
192
+ termad style line|banner render style`);
193
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The smallest possible ad: one line of wording, optionally a URL.
3
+ * No images, no beacons, no HTML — the terminal renders it.
4
+ */
5
+ export interface Ad {
6
+ /** Unique id (server-assigned or slug of the text). */
7
+ id: string;
8
+ /** The wording shown to the user. Kept short — terminal line. */
9
+ text: string;
10
+ /** Click target. Rendered as a bare URL so the terminal auto-links it. */
11
+ url?: string;
12
+ /** Optional second line (e.g. "sponsored by …"). */
13
+ sub?: string;
14
+ }
15
+ /** Shape the HTTP endpoint returns. */
16
+ export interface AdResponse {
17
+ ad: Ad;
18
+ }
19
+ export declare function isValidAd(v: unknown): v is Ad;
@@ -0,0 +1,12 @@
1
+ export function isValidAd(v) {
2
+ if (typeof v !== "object" || v === null)
3
+ return false;
4
+ const a = v;
5
+ return (typeof a.id === "string" &&
6
+ a.id.length > 0 &&
7
+ typeof a.text === "string" &&
8
+ a.text.length > 0 &&
9
+ a.text.length <= 300 &&
10
+ (a.url === undefined ||
11
+ (typeof a.url === "string" && /^https?:\/\//.test(a.url) && a.url.length <= 500)));
12
+ }
@@ -0,0 +1,18 @@
1
+ export type SurfaceId = "claude" | "codex" | "opencode" | "gemini";
2
+ export declare const ALL_SURFACES: SurfaceId[];
3
+ export interface TermadConfig {
4
+ /** Your ad endpoint. GET <endpoint>/v1/ad?surface=<s>&uid=<u> → {ad:{...}} */
5
+ endpoint: string | null;
6
+ /** Stable anonymous install id (rotation/frequency capping server-side). */
7
+ uid: string;
8
+ /** How often `termad watch` re-fetches, seconds. */
9
+ pollSeconds: number;
10
+ /** Which surfaces are opted in. */
11
+ surfaces: Record<SurfaceId, boolean>;
12
+ /** "line" | "banner" — render style in the terminal. */
13
+ style: "line" | "banner";
14
+ }
15
+ export declare function loadConfig(): TermadConfig;
16
+ export declare function saveConfig(cfg: TermadConfig): void;
17
+ /** Atomic-ish write so wrappers never read a half-written cache. */
18
+ export declare function writeFileAtomic(path: string, data: string): void;
@@ -0,0 +1,33 @@
1
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { paths, termadHome } from "./paths.js";
4
+ export const ALL_SURFACES = ["claude", "codex", "opencode", "gemini"];
5
+ function defaultConfig() {
6
+ return {
7
+ endpoint: null,
8
+ uid: crypto.randomUUID(),
9
+ pollSeconds: 900,
10
+ surfaces: { claude: false, codex: false, opencode: false, gemini: false },
11
+ style: "line",
12
+ };
13
+ }
14
+ export function loadConfig() {
15
+ try {
16
+ const raw = JSON.parse(readFileSync(paths.config(), "utf8"));
17
+ return { ...defaultConfig(), ...raw, surfaces: { ...defaultConfig().surfaces, ...raw.surfaces } };
18
+ }
19
+ catch {
20
+ return defaultConfig();
21
+ }
22
+ }
23
+ export function saveConfig(cfg) {
24
+ mkdirSync(termadHome(), { recursive: true });
25
+ writeFileSync(paths.config(), JSON.stringify(cfg, null, 2) + "\n");
26
+ }
27
+ /** Atomic-ish write so wrappers never read a half-written cache. */
28
+ export function writeFileAtomic(path, data) {
29
+ mkdirSync(dirname(path), { recursive: true });
30
+ const tmp = path + ".tmp";
31
+ writeFileSync(tmp, data);
32
+ renameSync(tmp, path);
33
+ }
@@ -0,0 +1,20 @@
1
+ export interface ManifestEntry {
2
+ surface: string;
3
+ target: string;
4
+ mechanism: "wrapper" | "statusline";
5
+ /** Backup holding the pristine original (null if none was needed). */
6
+ backup: string | null;
7
+ sha256Original: string | null;
8
+ sha256Installed: string | null;
9
+ at: string;
10
+ }
11
+ export interface Manifest {
12
+ version: 1;
13
+ entries: ManifestEntry[];
14
+ }
15
+ export declare function sha256File(p: string): string | null;
16
+ export declare function loadManifest(): Manifest;
17
+ export declare function saveManifest(m: Manifest): void;
18
+ /** Record (or replace) the entry for one surface+target pair. */
19
+ export declare function recordEntry(e: Omit<ManifestEntry, "at">): void;
20
+ export declare function removeEntries(surface: string): void;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Tamper-evident audit manifest: every change termad makes is recorded with
3
+ * sha256 fingerprints of the original and the installed file. `termad audit`
4
+ * re-verifies the chain, so any modification — by termad, the user, the CLI
5
+ * itself, or a third party — is attributable.
6
+ */
7
+ import { createHash } from "node:crypto";
8
+ import { readFileSync } from "node:fs";
9
+ import { paths } from "./paths.js";
10
+ import { writeFileAtomic } from "./config.js";
11
+ export function sha256File(p) {
12
+ try {
13
+ return createHash("sha256").update(readFileSync(p)).digest("hex");
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ export function loadManifest() {
20
+ try {
21
+ const m = JSON.parse(readFileSync(paths.manifest(), "utf8"));
22
+ if (m?.version === 1 && Array.isArray(m.entries))
23
+ return m;
24
+ }
25
+ catch { }
26
+ return { version: 1, entries: [] };
27
+ }
28
+ export function saveManifest(m) {
29
+ writeFileAtomic(paths.manifest(), JSON.stringify(m, null, 2) + "\n");
30
+ }
31
+ /** Record (or replace) the entry for one surface+target pair. */
32
+ export function recordEntry(e) {
33
+ const m = loadManifest();
34
+ m.entries = m.entries.filter((x) => !(x.surface === e.surface && x.target === e.target));
35
+ m.entries.push({ ...e, at: new Date().toISOString() });
36
+ saveManifest(m);
37
+ }
38
+ export function removeEntries(surface) {
39
+ const m = loadManifest();
40
+ m.entries = m.entries.filter((x) => x.surface !== surface);
41
+ saveManifest(m);
42
+ }
@@ -0,0 +1,24 @@
1
+ /** All termad state lives under ~/.termad (override with TERMAD_HOME). */
2
+ export declare function termadHome(): string;
3
+ export declare const paths: {
4
+ config: () => string;
5
+ /** Cache file the wrappers read: line 1 = text, line 2 = url, line 3 = sub. */
6
+ adCache: () => string;
7
+ /** Claude statusline launcher installed into ~/.claude/settings.json. */
8
+ statuslineScript: () => string;
9
+ /** Captured pre-install statusLine (chain capture, like claude does natively). */
10
+ prevStatusline: () => string;
11
+ log: () => string;
12
+ /** Tamper-evident record of every change (`termad audit` re-verifies it). */
13
+ manifest: () => string;
14
+ /**
15
+ * Display counter: one line of ad id per render, appended by the wrapper,
16
+ * reported in batches by `termad sync` (never per-render — no network on
17
+ * the CLI hot path, no per-view beacon).
18
+ */
19
+ displayLog: () => string;
20
+ };
21
+ /** Wrapper backup for a binary/shim lives next to it: <bin>.termad-orig. */
22
+ export declare function backupPath(target: string): string;
23
+ export declare const WRAPPER_MARKER = "===== TERMAD-CLI =====";
24
+ export declare const OVERLAY_MARKER = "===== TERMAD-OVERLAY =====";
@@ -0,0 +1,30 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ /** All termad state lives under ~/.termad (override with TERMAD_HOME). */
4
+ export function termadHome() {
5
+ return process.env.TERMAD_HOME || join(homedir(), ".termad");
6
+ }
7
+ export const paths = {
8
+ config: () => join(termadHome(), "config.json"),
9
+ /** Cache file the wrappers read: line 1 = text, line 2 = url, line 3 = sub. */
10
+ adCache: () => join(termadHome(), "ad.txt"),
11
+ /** Claude statusline launcher installed into ~/.claude/settings.json. */
12
+ statuslineScript: () => join(termadHome(), "statusline.mjs"),
13
+ /** Captured pre-install statusLine (chain capture, like claude does natively). */
14
+ prevStatusline: () => join(termadHome(), "prev-statusline.json"),
15
+ log: () => join(termadHome(), "termad.log"),
16
+ /** Tamper-evident record of every change (`termad audit` re-verifies it). */
17
+ manifest: () => join(termadHome(), "manifest.json"),
18
+ /**
19
+ * Display counter: one line of ad id per render, appended by the wrapper,
20
+ * reported in batches by `termad sync` (never per-render — no network on
21
+ * the CLI hot path, no per-view beacon).
22
+ */
23
+ displayLog: () => join(termadHome(), "display.log"),
24
+ };
25
+ /** Wrapper backup for a binary/shim lives next to it: <bin>.termad-orig. */
26
+ export function backupPath(target) {
27
+ return target + ".termad-orig";
28
+ }
29
+ export const WRAPPER_MARKER = "===== TERMAD-CLI =====";
30
+ export const OVERLAY_MARKER = "===== TERMAD-OVERLAY =====";
@@ -0,0 +1,18 @@
1
+ import { Ad } from "./ad.js";
2
+ import { type SurfaceId } from "./config.js";
3
+ /** Last-good ad survives endpoint outages (write-through cache). */
4
+ export declare function fetchAd(surface: SurfaceId): Promise<Ad>;
5
+ /**
6
+ * Write the ad cache the wrappers read:
7
+ * line 1 = wording, line 2 = click URL, line 3 = sub, line 4 = ad id.
8
+ * Lines 2/3 are padded with "-" when absent so line-numbered readers
9
+ * (including cmd's `for /f`, which skips blank lines) stay positional.
10
+ * Wrappers only ever read this file — zero network in the hot path.
11
+ * Line 4 (ad id) is what the wrapper appends to the display counter.
12
+ */
13
+ export declare function writeAdCache(ad: Ad): void;
14
+ export declare function readAdCache(): {
15
+ text: string;
16
+ url?: string;
17
+ sub?: string;
18
+ } | null;
@@ -0,0 +1,78 @@
1
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
2
+ import { isValidAd } from "./ad.js";
3
+ import { loadConfig, writeFileAtomic } from "./config.js";
4
+ import { paths, termadHome } from "./paths.js";
5
+ const FETCH_TIMEOUT_MS = 4000;
6
+ /** Fallback ad if nothing is configured or the endpoint is down. */
7
+ const HOUSE_AD = { id: "house:default", text: "termad — ads for your terminal" };
8
+ /** Last-good ad survives endpoint outages (write-through cache). */
9
+ export async function fetchAd(surface) {
10
+ const cfg = loadConfig();
11
+ if (cfg.endpoint) {
12
+ try {
13
+ const url = `${cfg.endpoint.replace(/\/+$/, "")}/v1/ad?surface=${surface}&uid=${cfg.uid}`;
14
+ const res = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
15
+ if (res.ok) {
16
+ const body = (await res.json());
17
+ if (isValidAd(body.ad)) {
18
+ writeLastGood(body.ad);
19
+ return body.ad;
20
+ }
21
+ }
22
+ }
23
+ catch {
24
+ // fall through to cache / house ad — never break the CLI over an ad
25
+ }
26
+ }
27
+ return lastGood() ?? HOUSE_AD;
28
+ }
29
+ function lastGoodPath() {
30
+ return paths.adCache() + ".json";
31
+ }
32
+ function writeLastGood(ad) {
33
+ try {
34
+ mkdirSync(termadHome(), { recursive: true });
35
+ writeFileSync(lastGoodPath(), JSON.stringify(ad));
36
+ }
37
+ catch { }
38
+ }
39
+ function lastGood() {
40
+ try {
41
+ const ad = JSON.parse(readFileSync(lastGoodPath(), "utf8"));
42
+ return isValidAd(ad) ? ad : null;
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ /**
49
+ * Write the ad cache the wrappers read:
50
+ * line 1 = wording, line 2 = click URL, line 3 = sub, line 4 = ad id.
51
+ * Lines 2/3 are padded with "-" when absent so line-numbered readers
52
+ * (including cmd's `for /f`, which skips blank lines) stay positional.
53
+ * Wrappers only ever read this file — zero network in the hot path.
54
+ * Line 4 (ad id) is what the wrapper appends to the display counter.
55
+ */
56
+ export function writeAdCache(ad) {
57
+ const lines = [
58
+ ad.text.replace(/[\r\n]+/g, " ").trim(),
59
+ ad.url ?? "-",
60
+ ad.sub ? ad.sub.replace(/[\r\n]+/g, " ").trim() : "-",
61
+ ad.id || "-",
62
+ ];
63
+ writeFileAtomic(paths.adCache(), lines.join("\n") + "\n");
64
+ }
65
+ export function readAdCache() {
66
+ try {
67
+ const raw = readFileSync(paths.adCache(), "utf8").split("\n");
68
+ const text = (raw[0] ?? "").trim();
69
+ if (!text)
70
+ return null;
71
+ const url = (raw[1] ?? "").trim();
72
+ const sub = (raw[2] ?? "").trim();
73
+ return { text, url: url && url !== "-" ? url : undefined, sub: sub && sub !== "-" ? sub : undefined };
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ }
@@ -0,0 +1,9 @@
1
+ export { type Ad, isValidAd } from "./core/ad.js";
2
+ export { fetchAd, writeAdCache, readAdCache } from "./core/source.js";
3
+ export { loadConfig, saveConfig, type SurfaceId, type TermadConfig } from "./core/config.js";
4
+ export { install, uninstall, sync } from "./install.js";
5
+ export { render, renderLine, renderBanner } from "./render/render.js";
6
+ export { installWrapper, uninstallWrapper, isWrapped, locateCli } from "./surfaces/wrap.js";
7
+ export { installStatusline, uninstallStatusline, isStatuslineInstalled } from "./surfaces/statusline.js";
8
+ export { surfaces, getSurface, type Surface } from "./surfaces/surfaces.js";
9
+ export { paths, termadHome, backupPath } from "./core/paths.js";
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export { isValidAd } from "./core/ad.js";
2
+ export { fetchAd, writeAdCache, readAdCache } from "./core/source.js";
3
+ export { loadConfig, saveConfig } from "./core/config.js";
4
+ export { install, uninstall, sync } from "./install.js";
5
+ export { render, renderLine, renderBanner } from "./render/render.js";
6
+ export { installWrapper, uninstallWrapper, isWrapped, locateCli } from "./surfaces/wrap.js";
7
+ export { installStatusline, uninstallStatusline, isStatuslineInstalled } from "./surfaces/statusline.js";
8
+ export { surfaces, getSurface } from "./surfaces/surfaces.js";
9
+ export { paths, termadHome, backupPath } from "./core/paths.js";
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Install / uninstall orchestration across all opted-in surfaces.
3
+ *
4
+ * Every change is recorded in the sha256 audit manifest BEFORE the user
5
+ * consents (the plan is shown) and re-verified by `termad audit`.
6
+ */
7
+ import { type SurfaceId } from "./core/config.js";
8
+ export interface SurfaceOutcome {
9
+ surface: SurfaceId;
10
+ ok: boolean;
11
+ detail: string;
12
+ }
13
+ export interface PlanItem {
14
+ surface: SurfaceId;
15
+ target: string;
16
+ action: string;
17
+ }
18
+ /** What `termad install` would change — shown to the user before consent. */
19
+ export declare function planInstall(surfaces?: SurfaceId[], force?: boolean): PlanItem[];
20
+ /** Fetch the ad, write the cache, then install the opted-in surfaces. */
21
+ export declare function install(surfaces?: SurfaceId[], opts?: {
22
+ force?: boolean;
23
+ }): Promise<SurfaceOutcome[]>;
24
+ export declare function uninstall(surfaces?: SurfaceId[]): SurfaceOutcome[];
25
+ /** Refresh the ad cache (used by `termad watch` and cron). */
26
+ export declare function sync(): Promise<string>;