@expitc/termad 0.1.2 → 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/assets/pty-overlay.mjs +51 -2
- package/assets/wrapper.sh.overlay +2 -2
- package/dist/assets/pty-overlay.mjs +51 -2
- package/dist/assets/wrapper.sh.overlay +2 -2
- package/dist/core/paths.d.ts +9 -0
- package/dist/core/paths.js +11 -0
- package/dist/install.js +38 -1
- package/dist/surfaces/inline.js +2 -2
- package/dist/surfaces/pty-inject.js +95 -26
- package/dist/surfaces/wrap.js +5 -2
- package/package.json +9 -1
- package/scripts/ensure-pty-helper.mjs +44 -0
package/assets/pty-overlay.mjs
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Launcher for the PTY overlay.
|
|
4
|
+
*
|
|
5
|
+
* Must work from any install layout:
|
|
6
|
+
* <pkg>/dist/assets/pty-overlay.mjs (published package, global install)
|
|
7
|
+
* <pkg>/src/assets/pty-overlay.mjs (running from source)
|
|
8
|
+
* ~/.termad/pty-overlay.mjs (written by `termad install`)
|
|
9
|
+
*
|
|
10
|
+
* Resolves pty-inject.js relative to itself first, then by package name.
|
|
11
|
+
* If nothing resolves, exit non-zero WITHOUT touching the CLI — the
|
|
12
|
+
* wrapper's own pre-banner fallback takes over and the CLI still runs.
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync } from "node:fs";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
17
|
+
|
|
18
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
|
|
20
|
+
const candidates = [
|
|
21
|
+
join(here, "..", "surfaces", "pty-inject.js"), // dist/assets → dist/surfaces
|
|
22
|
+
join(here, "surfaces", "pty-inject.js"), // src/assets → src/surfaces
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
let runWithAdOverlay = null;
|
|
26
|
+
|
|
27
|
+
for (const c of candidates) {
|
|
28
|
+
if (!existsSync(c)) continue;
|
|
29
|
+
try {
|
|
30
|
+
({ runWithAdOverlay } = await import(pathToFileURL(c).href));
|
|
31
|
+
break;
|
|
32
|
+
} catch {}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!runWithAdOverlay) {
|
|
36
|
+
try {
|
|
37
|
+
({ runWithAdOverlay } = await import("@expitc/termad/dist/surfaces/pty-inject.js"));
|
|
38
|
+
} catch {}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (typeof runWithAdOverlay !== "function") {
|
|
42
|
+
console.error("termad: pty-inject.js not found — falling back to banner mode");
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
3
46
|
const binary = process.argv[2];
|
|
4
47
|
const args = process.argv.slice(3);
|
|
5
|
-
|
|
48
|
+
|
|
49
|
+
if (!binary) {
|
|
50
|
+
console.error("termad: usage: pty-overlay.mjs <binary> [args...]");
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
await runWithAdOverlay(binary, args);
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
TERMAD_AD_FILE="__TERMAD_AD__"
|
|
10
10
|
TERMAD_BACKUP="__TERMAD_BACKUP__"
|
|
11
11
|
TERMAD_COUNT="__TERMAD_COUNT__"
|
|
12
|
-
TERMAD_HOME_DIR="$(dirname "$
|
|
12
|
+
TERMAD_HOME_DIR="$(dirname "$TERMAD_AD_FILE")"
|
|
13
13
|
|
|
14
14
|
# Try PTY overlay via node script bundled with termad
|
|
15
15
|
OVERLAY_SCRIPT="$TERMAD_HOME_DIR/pty-overlay.mjs"
|
|
16
16
|
if [ ! -f "$OVERLAY_SCRIPT" ]; then
|
|
17
17
|
# Try to find in node_modules or global install
|
|
18
|
-
OVERLAY_SCRIPT="$(npm root -g 2>/dev/null)/@expitc/termad/dist/
|
|
18
|
+
OVERLAY_SCRIPT="$(npm root -g 2>/dev/null)/@expitc/termad/dist/assets/pty-overlay.mjs"
|
|
19
19
|
fi
|
|
20
20
|
|
|
21
21
|
if [ -f "$OVERLAY_SCRIPT" ] && command -v node >/dev/null 2>&1; then
|
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Launcher for the PTY overlay.
|
|
4
|
+
*
|
|
5
|
+
* Must work from any install layout:
|
|
6
|
+
* <pkg>/dist/assets/pty-overlay.mjs (published package, global install)
|
|
7
|
+
* <pkg>/src/assets/pty-overlay.mjs (running from source)
|
|
8
|
+
* ~/.termad/pty-overlay.mjs (written by `termad install`)
|
|
9
|
+
*
|
|
10
|
+
* Resolves pty-inject.js relative to itself first, then by package name.
|
|
11
|
+
* If nothing resolves, exit non-zero WITHOUT touching the CLI — the
|
|
12
|
+
* wrapper's own pre-banner fallback takes over and the CLI still runs.
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync } from "node:fs";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
17
|
+
|
|
18
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
|
|
20
|
+
const candidates = [
|
|
21
|
+
join(here, "..", "surfaces", "pty-inject.js"), // dist/assets → dist/surfaces
|
|
22
|
+
join(here, "surfaces", "pty-inject.js"), // src/assets → src/surfaces
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
let runWithAdOverlay = null;
|
|
26
|
+
|
|
27
|
+
for (const c of candidates) {
|
|
28
|
+
if (!existsSync(c)) continue;
|
|
29
|
+
try {
|
|
30
|
+
({ runWithAdOverlay } = await import(pathToFileURL(c).href));
|
|
31
|
+
break;
|
|
32
|
+
} catch {}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!runWithAdOverlay) {
|
|
36
|
+
try {
|
|
37
|
+
({ runWithAdOverlay } = await import("@expitc/termad/dist/surfaces/pty-inject.js"));
|
|
38
|
+
} catch {}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (typeof runWithAdOverlay !== "function") {
|
|
42
|
+
console.error("termad: pty-inject.js not found — falling back to banner mode");
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
3
46
|
const binary = process.argv[2];
|
|
4
47
|
const args = process.argv.slice(3);
|
|
5
|
-
|
|
48
|
+
|
|
49
|
+
if (!binary) {
|
|
50
|
+
console.error("termad: usage: pty-overlay.mjs <binary> [args...]");
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
await runWithAdOverlay(binary, args);
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
TERMAD_AD_FILE="__TERMAD_AD__"
|
|
10
10
|
TERMAD_BACKUP="__TERMAD_BACKUP__"
|
|
11
11
|
TERMAD_COUNT="__TERMAD_COUNT__"
|
|
12
|
-
TERMAD_HOME_DIR="$(dirname "$
|
|
12
|
+
TERMAD_HOME_DIR="$(dirname "$TERMAD_AD_FILE")"
|
|
13
13
|
|
|
14
14
|
# Try PTY overlay via node script bundled with termad
|
|
15
15
|
OVERLAY_SCRIPT="$TERMAD_HOME_DIR/pty-overlay.mjs"
|
|
16
16
|
if [ ! -f "$OVERLAY_SCRIPT" ]; then
|
|
17
17
|
# Try to find in node_modules or global install
|
|
18
|
-
OVERLAY_SCRIPT="$(npm root -g 2>/dev/null)/@expitc/termad/dist/
|
|
18
|
+
OVERLAY_SCRIPT="$(npm root -g 2>/dev/null)/@expitc/termad/dist/assets/pty-overlay.mjs"
|
|
19
19
|
fi
|
|
20
20
|
|
|
21
21
|
if [ -f "$OVERLAY_SCRIPT" ] && command -v node >/dev/null 2>&1; then
|
package/dist/core/paths.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export declare const paths: {
|
|
|
6
6
|
adCache: () => string;
|
|
7
7
|
/** Claude statusline launcher installed into ~/.claude/settings.json. */
|
|
8
8
|
statuslineScript: () => string;
|
|
9
|
+
/** PTY overlay launcher the shell wrappers exec. Written by `termad install`. */
|
|
10
|
+
ptyOverlay: () => string;
|
|
9
11
|
/** Captured pre-install statusLine (chain capture, like claude does natively). */
|
|
10
12
|
prevStatusline: () => string;
|
|
11
13
|
log: () => string;
|
|
@@ -22,3 +24,10 @@ export declare const paths: {
|
|
|
22
24
|
export declare function backupPath(target: string): string;
|
|
23
25
|
export declare const WRAPPER_MARKER = "===== TERMAD-CLI =====";
|
|
24
26
|
export declare const OVERLAY_MARKER = "===== TERMAD-OVERLAY =====";
|
|
27
|
+
/** Every marker termad can write into a wrapped target.
|
|
28
|
+
* A target is termad's own if it carries ANY of them — checking only one
|
|
29
|
+
* makes overlay-wrapped binaries look untouched, which breaks uninstall
|
|
30
|
+
* (the original backup gets discarded) and misreports audit status. */
|
|
31
|
+
export declare const WRAPPER_MARKERS: readonly ["===== TERMAD-CLI =====", "===== TERMAD-OVERLAY ====="];
|
|
32
|
+
/** True if the content carries any termad wrapper marker. */
|
|
33
|
+
export declare function hasWrapperMarker(content: string): boolean;
|
package/dist/core/paths.js
CHANGED
|
@@ -10,6 +10,8 @@ export const paths = {
|
|
|
10
10
|
adCache: () => join(termadHome(), "ad.txt"),
|
|
11
11
|
/** Claude statusline launcher installed into ~/.claude/settings.json. */
|
|
12
12
|
statuslineScript: () => join(termadHome(), "statusline.mjs"),
|
|
13
|
+
/** PTY overlay launcher the shell wrappers exec. Written by `termad install`. */
|
|
14
|
+
ptyOverlay: () => join(termadHome(), "pty-overlay.mjs"),
|
|
13
15
|
/** Captured pre-install statusLine (chain capture, like claude does natively). */
|
|
14
16
|
prevStatusline: () => join(termadHome(), "prev-statusline.json"),
|
|
15
17
|
log: () => join(termadHome(), "termad.log"),
|
|
@@ -28,3 +30,12 @@ export function backupPath(target) {
|
|
|
28
30
|
}
|
|
29
31
|
export const WRAPPER_MARKER = "===== TERMAD-CLI =====";
|
|
30
32
|
export const OVERLAY_MARKER = "===== TERMAD-OVERLAY =====";
|
|
33
|
+
/** Every marker termad can write into a wrapped target.
|
|
34
|
+
* A target is termad's own if it carries ANY of them — checking only one
|
|
35
|
+
* makes overlay-wrapped binaries look untouched, which breaks uninstall
|
|
36
|
+
* (the original backup gets discarded) and misreports audit status. */
|
|
37
|
+
export const WRAPPER_MARKERS = [WRAPPER_MARKER, OVERLAY_MARKER];
|
|
38
|
+
/** True if the content carries any termad wrapper marker. */
|
|
39
|
+
export function hasWrapperMarker(content) {
|
|
40
|
+
return WRAPPER_MARKERS.some((m) => content.includes(m));
|
|
41
|
+
}
|
package/dist/install.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
import { loadConfig, saveConfig, writeFileAtomic } from "./core/config.js";
|
|
8
8
|
import { paths, termadHome, backupPath } from "./core/paths.js";
|
|
9
9
|
import { fetchAd, writeAdCache } from "./core/source.js";
|
|
10
|
-
import { existsSync, mkdirSync } from "node:fs";
|
|
10
|
+
import { existsSync, mkdirSync, writeFileSync, chmodSync } from "node:fs";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
11
12
|
import { getSurface } from "./surfaces/surfaces.js";
|
|
12
13
|
import { installWrapper, uninstallWrapper, isWrapped } from "./surfaces/wrap.js";
|
|
13
14
|
import { installStatusline, uninstallStatusline, settingsPath, isStatuslineInstalled } from "./surfaces/statusline.js";
|
|
@@ -46,11 +47,47 @@ export function planInstall(surfaces, force = false) {
|
|
|
46
47
|
}
|
|
47
48
|
return items;
|
|
48
49
|
}
|
|
50
|
+
/** Absolute path to the overlay implementation shipped with THIS install.
|
|
51
|
+
* dist/surfaces/pty-inject.js in a published package, src/… when run from
|
|
52
|
+
* source. */
|
|
53
|
+
function ptyInjectPath() {
|
|
54
|
+
const here = dirname(new URL(import.meta.url).pathname);
|
|
55
|
+
const js = join(here, "surfaces", "pty-inject.js");
|
|
56
|
+
const ts = join(here, "surfaces", "pty-inject.ts");
|
|
57
|
+
return existsSync(js) ? js : ts;
|
|
58
|
+
}
|
|
59
|
+
/** Write the overlay launcher into termad home so the wrapper can always
|
|
60
|
+
* find it. The absolute path to pty-inject is baked in, which keeps the
|
|
61
|
+
* launcher working regardless of how termad itself was installed. */
|
|
62
|
+
function installOverlayLauncher() {
|
|
63
|
+
const target = paths.ptyOverlay();
|
|
64
|
+
try {
|
|
65
|
+
const mod = ptyInjectPath();
|
|
66
|
+
const body = `#!/usr/bin/env node
|
|
67
|
+
// Written by \`termad install\`. Launches the PTY ad overlay for a CLI binary.
|
|
68
|
+
// Regenerated on every install — safe to edit/delete; termad rewrites it.
|
|
69
|
+
import { pathToFileURL } from "node:url";
|
|
70
|
+
const mod = await import(pathToFileURL(${JSON.stringify(mod)}).href);
|
|
71
|
+
if (typeof mod.runWithAdOverlay !== "function") {
|
|
72
|
+
console.error("termad: overlay unavailable");
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
await mod.runWithAdOverlay(process.argv[2], process.argv.slice(3));
|
|
76
|
+
`;
|
|
77
|
+
writeFileSync(target, body);
|
|
78
|
+
chmodSync(target, 0o755);
|
|
79
|
+
return target;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
49
85
|
/** Fetch the ad, write the cache, then install the opted-in surfaces. */
|
|
50
86
|
export async function install(surfaces, opts = {}) {
|
|
51
87
|
const force = opts.force ?? false;
|
|
52
88
|
const cfg = loadConfig();
|
|
53
89
|
mkdirSync(termadHome(), { recursive: true });
|
|
90
|
+
installOverlayLauncher();
|
|
54
91
|
const ad = await fetchAd(surfaces?.[0] ?? "claude");
|
|
55
92
|
writeAdCache(ad);
|
|
56
93
|
const wanted = surfaces ?? Object.entries(cfg.surfaces).filter(([, v]) => v).map(([k]) => k);
|
package/dist/surfaces/inline.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { createHash } from "node:crypto";
|
|
15
15
|
import { copyFileSync, existsSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
16
|
-
import {
|
|
16
|
+
import { hasWrapperMarker } from "../core/paths.js";
|
|
17
17
|
export const INLINE_MARKER = "===== TERMAD-INLINE =====";
|
|
18
18
|
export const INLINE_BACKUP_EXT = ".termad-bundle-orig";
|
|
19
19
|
export function inlineBackupPath(target) {
|
|
@@ -141,7 +141,7 @@ export function installInline(target) {
|
|
|
141
141
|
function isWrappedCheck(target) {
|
|
142
142
|
try {
|
|
143
143
|
const head = readFileSync(target, "utf8").slice(0, 256);
|
|
144
|
-
return head
|
|
144
|
+
return hasWrapperMarker(head);
|
|
145
145
|
}
|
|
146
146
|
catch {
|
|
147
147
|
return false;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { readFileSync, appendFileSync } from "node:fs";
|
|
1
|
+
import { readFileSync, appendFileSync, existsSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
const TERMAD_HOME = process.env.TERMAD_HOME || join(homedir(), ".termad");
|
|
5
5
|
const AD_FILE = join(TERMAD_HOME, "ad.txt");
|
|
6
6
|
const COUNT_FILE = join(TERMAD_HOME, "display.log");
|
|
7
|
+
const CONFIG_FILE = join(TERMAD_HOME, "config.json");
|
|
7
8
|
function readAd() {
|
|
8
9
|
try {
|
|
9
10
|
const raw = readFileSync(AD_FILE, "utf8").replace(/\r/g, "").split("\n");
|
|
@@ -18,6 +19,17 @@ function readAd() {
|
|
|
18
19
|
return null;
|
|
19
20
|
}
|
|
20
21
|
}
|
|
22
|
+
function readStyle() {
|
|
23
|
+
try {
|
|
24
|
+
if (!existsSync(CONFIG_FILE))
|
|
25
|
+
return "line";
|
|
26
|
+
const cfg = JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
|
|
27
|
+
return cfg.style === "banner" ? "banner" : "line";
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return "line";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
21
33
|
function count(id) {
|
|
22
34
|
if (!id)
|
|
23
35
|
return;
|
|
@@ -26,20 +38,65 @@ function count(id) {
|
|
|
26
38
|
}
|
|
27
39
|
catch { }
|
|
28
40
|
}
|
|
29
|
-
function
|
|
41
|
+
function osc8(url, text) {
|
|
42
|
+
if (!url)
|
|
43
|
+
return text;
|
|
44
|
+
return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`;
|
|
45
|
+
}
|
|
46
|
+
function getDomain(url) {
|
|
47
|
+
if (!url)
|
|
48
|
+
return null;
|
|
49
|
+
try {
|
|
50
|
+
const u = new URL(url);
|
|
51
|
+
return u.hostname.replace(/^www\./, "");
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function truncate(s, n) {
|
|
58
|
+
if (s.length <= n)
|
|
59
|
+
return s;
|
|
60
|
+
return s.slice(0, Math.max(0, n - 3)) + "...";
|
|
61
|
+
}
|
|
62
|
+
function buildFooter(ad, style, width, offset = 0) {
|
|
30
63
|
const label = "\x1b[2m\x1b[33m[ad]\x1b[0m";
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
64
|
+
const domain = getDomain(ad.url);
|
|
65
|
+
const w = Math.max(40, width - 4);
|
|
66
|
+
if (style === "banner") {
|
|
67
|
+
// Boxed style like Kickbacks
|
|
68
|
+
const innerW = Math.max(30, w - 2);
|
|
69
|
+
let displayText = ad.text;
|
|
70
|
+
// Marquee for long text
|
|
71
|
+
if (displayText.length > innerW - 20) {
|
|
72
|
+
const padded = displayText + " • " + displayText;
|
|
73
|
+
const start = offset % displayText.length;
|
|
74
|
+
displayText = padded.slice(start, start + innerW - 25);
|
|
75
|
+
}
|
|
76
|
+
const linkText = domain || ad.url;
|
|
77
|
+
const linked = ad.url ? osc8(ad.url, `\x1b[36m${truncate(linkText, 30)}\x1b[0m`) : "";
|
|
78
|
+
const content = ` ${label} ${truncate(displayText, innerW - 15)}${linked ? " " + linked : ""}`;
|
|
79
|
+
const padded = content + " ".repeat(Math.max(0, innerW - content.replace(/\x1b\[[0-9;]*m/g, "").length));
|
|
80
|
+
return `\x1b[90m┌${"─".repeat(innerW)}┐\x1b[0m\n\x1b[90m│\x1b[0m${padded}\x1b[90m│\x1b[0m\n\x1b[90m└${"─".repeat(innerW)}┘\x1b[0m`;
|
|
81
|
+
}
|
|
82
|
+
// Line style (minimal, clean)
|
|
83
|
+
let displayText = ad.text;
|
|
84
|
+
if (displayText.length > w - 40) {
|
|
85
|
+
const padded = displayText + " ";
|
|
86
|
+
const start = offset % (displayText.length + 5);
|
|
87
|
+
displayText = padded.slice(start, start + w - 45);
|
|
88
|
+
}
|
|
89
|
+
const linkText = domain ? `\x1b[2m•\x1b[0m \x1b[36m${domain}\x1b[0m` : (ad.url ? `\x1b[2m•\x1b[0m \x1b[36m${truncate(ad.url, 40)}\x1b[0m` : "");
|
|
90
|
+
const linked = ad.url ? osc8(ad.url, linkText) : linkText;
|
|
91
|
+
let line = ` ${label} ${truncate(displayText, w - 35)}`;
|
|
92
|
+
if (linked)
|
|
93
|
+
line += ` ${linked}`;
|
|
36
94
|
return line;
|
|
37
95
|
}
|
|
38
96
|
export function runWithAdOverlay(binary, args = []) {
|
|
39
97
|
return runWithAdOverlayAsync(binary, args);
|
|
40
98
|
}
|
|
41
99
|
async function runWithAdOverlayAsync(binary, args = []) {
|
|
42
|
-
// Try to use node-pty if available
|
|
43
100
|
let pty;
|
|
44
101
|
try {
|
|
45
102
|
// @ts-ignore
|
|
@@ -53,12 +110,11 @@ async function runWithAdOverlayAsync(binary, args = []) {
|
|
|
53
110
|
catch { }
|
|
54
111
|
}
|
|
55
112
|
if (!pty) {
|
|
56
|
-
// No PTY available - fall back to printing banner then execing
|
|
57
113
|
const ad = readAd();
|
|
58
114
|
if (ad) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
115
|
+
const style = readStyle();
|
|
116
|
+
const width = process.stdout.columns || 120;
|
|
117
|
+
console.log(buildFooter(ad, style, width).replace(/\n/g, "\n"));
|
|
62
118
|
count(ad.id);
|
|
63
119
|
}
|
|
64
120
|
const { spawn } = await import("node:child_process");
|
|
@@ -71,6 +127,7 @@ async function runWithAdOverlayAsync(binary, args = []) {
|
|
|
71
127
|
count(ad.id);
|
|
72
128
|
const cols = process.stdout.columns || 120;
|
|
73
129
|
const rows = process.stdout.rows || 40;
|
|
130
|
+
const style = readStyle();
|
|
74
131
|
const ptyProcess = pty.spawn(binary, args, {
|
|
75
132
|
name: "xterm-256color",
|
|
76
133
|
cols,
|
|
@@ -80,32 +137,46 @@ async function runWithAdOverlayAsync(binary, args = []) {
|
|
|
80
137
|
});
|
|
81
138
|
let footerVisible = !!ad;
|
|
82
139
|
let lastAd = ad;
|
|
140
|
+
let marqueeOffset = 0;
|
|
83
141
|
function drawFooter() {
|
|
84
142
|
if (!footerVisible || !lastAd)
|
|
85
143
|
return;
|
|
86
|
-
const
|
|
87
|
-
// Move to bottom row, clear line, print footer
|
|
144
|
+
const width = process.stdout.columns || cols;
|
|
88
145
|
const termRows = ptyProcess.rows || rows;
|
|
89
|
-
|
|
146
|
+
const footer = buildFooter(lastAd, style, width, marqueeOffset);
|
|
147
|
+
marqueeOffset++;
|
|
148
|
+
if (style === "banner") {
|
|
149
|
+
// Draw 3 lines for boxed
|
|
150
|
+
process.stdout.write(`\x1b[s\x1b[${termRows - 2};1H\x1b[2K${footer.split('\n')[0]}\x1b[u`);
|
|
151
|
+
process.stdout.write(`\x1b[s\x1b[${termRows - 1};1H\x1b[2K${footer.split('\n')[1]}\x1b[u`);
|
|
152
|
+
process.stdout.write(`\x1b[s\x1b[${termRows};1H\x1b[2K${footer.split('\n')[2]}\x1b[u`);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
process.stdout.write(`\x1b[s\x1b[${termRows};1H\x1b[2K${footer}\x1b[u`);
|
|
156
|
+
}
|
|
90
157
|
}
|
|
91
158
|
function hideFooter() {
|
|
92
159
|
const termRows = ptyProcess.rows || rows;
|
|
93
|
-
|
|
160
|
+
if (style === "banner") {
|
|
161
|
+
process.stdout.write(`\x1b[s\x1b[${termRows - 2};1H\x1b[2K\x1b[${termRows - 1};1H\x1b[2K\x1b[${termRows};1H\x1b[2K\x1b[u`);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
process.stdout.write(`\x1b[s\x1b[${termRows};1H\x1b[2K\x1b[u`);
|
|
165
|
+
}
|
|
94
166
|
}
|
|
95
|
-
// Pipe PTY output to stdout with footer redrawn after each chunk
|
|
96
167
|
ptyProcess.onData((data) => {
|
|
97
168
|
process.stdout.write(data);
|
|
98
|
-
|
|
99
|
-
setTimeout(drawFooter, 10);
|
|
169
|
+
setTimeout(drawFooter, 15);
|
|
100
170
|
});
|
|
101
|
-
// Pipe stdin to PTY
|
|
102
171
|
if (process.stdin.isTTY) {
|
|
103
|
-
|
|
172
|
+
try {
|
|
173
|
+
process.stdin.setRawMode(true);
|
|
174
|
+
}
|
|
175
|
+
catch { }
|
|
104
176
|
}
|
|
105
177
|
process.stdin.on("data", (data) => {
|
|
106
178
|
ptyProcess.write(data);
|
|
107
179
|
});
|
|
108
|
-
// Handle resize
|
|
109
180
|
process.stdout.on("resize", () => {
|
|
110
181
|
const c = process.stdout.columns || cols;
|
|
111
182
|
const r = process.stdout.rows || rows;
|
|
@@ -115,7 +186,6 @@ async function runWithAdOverlayAsync(binary, args = []) {
|
|
|
115
186
|
catch { }
|
|
116
187
|
setTimeout(drawFooter, 50);
|
|
117
188
|
});
|
|
118
|
-
// Handle exit
|
|
119
189
|
ptyProcess.onExit(({ exitCode }) => {
|
|
120
190
|
if (process.stdin.isTTY) {
|
|
121
191
|
try {
|
|
@@ -127,7 +197,6 @@ async function runWithAdOverlayAsync(binary, args = []) {
|
|
|
127
197
|
hideFooter();
|
|
128
198
|
process.exit(exitCode ?? 0);
|
|
129
199
|
});
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
setInterval(drawFooter, 500); // Keep footer visible on redraws
|
|
200
|
+
setTimeout(drawFooter, 150);
|
|
201
|
+
setInterval(drawFooter, style === "banner" ? 800 : 1000);
|
|
133
202
|
}
|
package/dist/surfaces/wrap.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { chmodSync, copyFileSync, existsSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { delimiter, dirname, join } from "node:path";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
|
-
import {
|
|
11
|
+
import { hasWrapperMarker, backupPath, paths } from "../core/paths.js";
|
|
12
12
|
const SH_TEMPLATE_OVERLAY = "wrapper.sh.overlay";
|
|
13
13
|
const SH_TEMPLATE = "wrapper.sh.asset";
|
|
14
14
|
const CMD_TEMPLATE = "wrapper.cmd.asset";
|
|
@@ -21,7 +21,10 @@ function templatePath(isWin, useOverlay = true) {
|
|
|
21
21
|
}
|
|
22
22
|
export function isWrapped(target) {
|
|
23
23
|
try {
|
|
24
|
-
|
|
24
|
+
// Any termad marker counts — the overlay variant is just as much
|
|
25
|
+
// "ours" as the plain wrapper. Missing it makes uninstall discard the
|
|
26
|
+
// pristine backup and leaves the shim behind.
|
|
27
|
+
return hasWrapperMarker(readFileSync(target, "utf8"));
|
|
25
28
|
}
|
|
26
29
|
catch {
|
|
27
30
|
return false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expitc/termad",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Opt-in, fully reversible text ads for terminal AI CLIs (claude, codex, opencode, gemini). Consent-first, tamper-evident audit, 50% runner revenue share.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"files": [
|
|
32
32
|
"dist",
|
|
33
33
|
"assets",
|
|
34
|
+
"scripts/ensure-pty-helper.mjs",
|
|
34
35
|
"README.md",
|
|
35
36
|
"POLICY.md",
|
|
36
37
|
"LICENSE"
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
"build": "tsc && node scripts/copy-assets.mjs",
|
|
40
41
|
"dev": "tsc --watch",
|
|
41
42
|
"server": "node server/server.mjs",
|
|
43
|
+
"postinstall": "node scripts/ensure-pty-helper.mjs || exit 0",
|
|
42
44
|
"prepublishOnly": "npm run build"
|
|
43
45
|
},
|
|
44
46
|
"engines": {
|
|
@@ -47,5 +49,11 @@
|
|
|
47
49
|
"devDependencies": {
|
|
48
50
|
"@types/node": "^22.10.0",
|
|
49
51
|
"typescript": "^5.7.0"
|
|
52
|
+
},
|
|
53
|
+
"optionalDependencies": {
|
|
54
|
+
"node-pty": "^1.1.0"
|
|
55
|
+
},
|
|
56
|
+
"allowScripts": {
|
|
57
|
+
"node-pty@1.1.0": true
|
|
50
58
|
}
|
|
51
59
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* node-pty ships its prebuilt `spawn-helper` (the tiny helper that opens the
|
|
4
|
+
* pty) inside the npm tarball WITHOUT the executable bit, and npm >= 11
|
|
5
|
+
* blocks dependency install scripts by default — so the helper is never
|
|
6
|
+
* rebuilt and never made executable. The result is a confusing
|
|
7
|
+
* "posix_spawnp failed" error and a silent downgrade to banner mode.
|
|
8
|
+
*
|
|
9
|
+
* This postinstall just fixes the mode. It is a no-op if the helper is
|
|
10
|
+
* already executable, and a no-op if node-pty is not installed at all
|
|
11
|
+
* (it is an optional dependency).
|
|
12
|
+
*/
|
|
13
|
+
import { chmodSync, existsSync, statSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { createRequire } from "node:module";
|
|
16
|
+
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
|
|
19
|
+
function findHelper() {
|
|
20
|
+
let root;
|
|
21
|
+
try {
|
|
22
|
+
root = join(require.resolve("node-pty/package.json"), "..");
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const candidates = [
|
|
27
|
+
join(root, "build", "Release", "spawn-helper"),
|
|
28
|
+
join(root, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper"),
|
|
29
|
+
];
|
|
30
|
+
return candidates.find((c) => existsSync(c)) ?? null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const helper = findHelper();
|
|
34
|
+
if (!helper) process.exit(0);
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const mode = statSync(helper).mode & 0o777;
|
|
38
|
+
if (mode & 0o111) process.exit(0); // already executable
|
|
39
|
+
chmodSync(helper, 0o755);
|
|
40
|
+
console.log("termad: made node-pty spawn-helper executable");
|
|
41
|
+
} catch (e) {
|
|
42
|
+
// Non-fatal — the overlay degrades to banner mode.
|
|
43
|
+
console.log(`termad: could not make node-pty spawn-helper executable: ${e.message}`);
|
|
44
|
+
}
|