@expitc/termad 0.1.0 → 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.
- package/assets/pty-overlay.mjs +5 -0
- package/assets/wrapper.sh.overlay +45 -0
- package/dist/assets/pty-overlay.mjs +5 -0
- package/dist/assets/wrapper.sh.overlay +45 -0
- package/dist/core/paths.d.ts +1 -0
- package/dist/core/paths.js +1 -0
- package/dist/surfaces/inline.d.ts +17 -0
- package/dist/surfaces/inline.js +171 -0
- package/dist/surfaces/pty-inject.d.ts +1 -0
- package/dist/surfaces/pty-inject.js +133 -0
- package/dist/surfaces/wrap.js +5 -2
- package/package.json +1 -1
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# ===== TERMAD-OVERLAY =====
|
|
3
|
+
# Reversible termad PTY wrapper. Renders ads INSIDE the TUI as a footer
|
|
4
|
+
# overlay (not above it). Reads ad from "__TERMAD_AD__" and displays as
|
|
5
|
+
# persistent footer. Falls back to pre-banner if node-pty unavailable.
|
|
6
|
+
# Original binary backed up at "__TERMAD_BACKUP__".
|
|
7
|
+
# Restore: copy the backup over this file, or run `termad uninstall`.
|
|
8
|
+
|
|
9
|
+
TERMAD_AD_FILE="__TERMAD_AD__"
|
|
10
|
+
TERMAD_BACKUP="__TERMAD_BACKUP__"
|
|
11
|
+
TERMAD_COUNT="__TERMAD_COUNT__"
|
|
12
|
+
TERMAD_HOME_DIR="$(dirname "$(dirname "$TERMAD_AD_FILE")")"
|
|
13
|
+
|
|
14
|
+
# Try PTY overlay via node script bundled with termad
|
|
15
|
+
OVERLAY_SCRIPT="$TERMAD_HOME_DIR/pty-overlay.mjs"
|
|
16
|
+
if [ ! -f "$OVERLAY_SCRIPT" ]; then
|
|
17
|
+
# Try to find in node_modules or global install
|
|
18
|
+
OVERLAY_SCRIPT="$(npm root -g 2>/dev/null)/@expitc/termad/dist/surfaces/pty-inject.mjs"
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
if [ -f "$OVERLAY_SCRIPT" ] && command -v node >/dev/null 2>&1; then
|
|
22
|
+
export TERMAD_AD_FILE TERMAD_BACKUP TERMAD_COUNT
|
|
23
|
+
exec node "$OVERLAY_SCRIPT" "$TERMAD_BACKUP" "$@"
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# Fallback: pre-banner
|
|
27
|
+
AD_TEXT=""
|
|
28
|
+
AD_URL=""
|
|
29
|
+
AD_ID=""
|
|
30
|
+
if [ -r "$TERMAD_AD_FILE" ]; then
|
|
31
|
+
AD_TEXT=$(head -n 1 "$TERMAD_AD_FILE" 2>/dev/null)
|
|
32
|
+
AD_URL=$(sed -n 2p "$TERMAD_AD_FILE" 2>/dev/null)
|
|
33
|
+
AD_ID=$(sed -n 4p "$TERMAD_AD_FILE" 2>/dev/null)
|
|
34
|
+
fi
|
|
35
|
+
if [ -n "$AD_TEXT" ]; then
|
|
36
|
+
printf '\n [ad] %s\n' "$AD_TEXT"
|
|
37
|
+
case "$AD_URL" in
|
|
38
|
+
http://*|https://*) printf ' %s\n\n' "$AD_URL" ;;
|
|
39
|
+
*) printf '\n' ;;
|
|
40
|
+
esac
|
|
41
|
+
if [ -n "$AD_ID" ]; then
|
|
42
|
+
printf '%s\n' "$AD_ID" >> "$TERMAD_COUNT" 2>/dev/null || true
|
|
43
|
+
fi
|
|
44
|
+
fi
|
|
45
|
+
exec "$TERMAD_BACKUP" "$@"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# ===== TERMAD-OVERLAY =====
|
|
3
|
+
# Reversible termad PTY wrapper. Renders ads INSIDE the TUI as a footer
|
|
4
|
+
# overlay (not above it). Reads ad from "__TERMAD_AD__" and displays as
|
|
5
|
+
# persistent footer. Falls back to pre-banner if node-pty unavailable.
|
|
6
|
+
# Original binary backed up at "__TERMAD_BACKUP__".
|
|
7
|
+
# Restore: copy the backup over this file, or run `termad uninstall`.
|
|
8
|
+
|
|
9
|
+
TERMAD_AD_FILE="__TERMAD_AD__"
|
|
10
|
+
TERMAD_BACKUP="__TERMAD_BACKUP__"
|
|
11
|
+
TERMAD_COUNT="__TERMAD_COUNT__"
|
|
12
|
+
TERMAD_HOME_DIR="$(dirname "$(dirname "$TERMAD_AD_FILE")")"
|
|
13
|
+
|
|
14
|
+
# Try PTY overlay via node script bundled with termad
|
|
15
|
+
OVERLAY_SCRIPT="$TERMAD_HOME_DIR/pty-overlay.mjs"
|
|
16
|
+
if [ ! -f "$OVERLAY_SCRIPT" ]; then
|
|
17
|
+
# Try to find in node_modules or global install
|
|
18
|
+
OVERLAY_SCRIPT="$(npm root -g 2>/dev/null)/@expitc/termad/dist/surfaces/pty-inject.mjs"
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
if [ -f "$OVERLAY_SCRIPT" ] && command -v node >/dev/null 2>&1; then
|
|
22
|
+
export TERMAD_AD_FILE TERMAD_BACKUP TERMAD_COUNT
|
|
23
|
+
exec node "$OVERLAY_SCRIPT" "$TERMAD_BACKUP" "$@"
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# Fallback: pre-banner
|
|
27
|
+
AD_TEXT=""
|
|
28
|
+
AD_URL=""
|
|
29
|
+
AD_ID=""
|
|
30
|
+
if [ -r "$TERMAD_AD_FILE" ]; then
|
|
31
|
+
AD_TEXT=$(head -n 1 "$TERMAD_AD_FILE" 2>/dev/null)
|
|
32
|
+
AD_URL=$(sed -n 2p "$TERMAD_AD_FILE" 2>/dev/null)
|
|
33
|
+
AD_ID=$(sed -n 4p "$TERMAD_AD_FILE" 2>/dev/null)
|
|
34
|
+
fi
|
|
35
|
+
if [ -n "$AD_TEXT" ]; then
|
|
36
|
+
printf '\n [ad] %s\n' "$AD_TEXT"
|
|
37
|
+
case "$AD_URL" in
|
|
38
|
+
http://*|https://*) printf ' %s\n\n' "$AD_URL" ;;
|
|
39
|
+
*) printf '\n' ;;
|
|
40
|
+
esac
|
|
41
|
+
if [ -n "$AD_ID" ]; then
|
|
42
|
+
printf '%s\n' "$AD_ID" >> "$TERMAD_COUNT" 2>/dev/null || true
|
|
43
|
+
fi
|
|
44
|
+
fi
|
|
45
|
+
exec "$TERMAD_BACKUP" "$@"
|
package/dist/core/paths.d.ts
CHANGED
|
@@ -21,3 +21,4 @@ export declare const paths: {
|
|
|
21
21
|
/** Wrapper backup for a binary/shim lives next to it: <bin>.termad-orig. */
|
|
22
22
|
export declare function backupPath(target: string): string;
|
|
23
23
|
export declare const WRAPPER_MARKER = "===== TERMAD-CLI =====";
|
|
24
|
+
export declare const OVERLAY_MARKER = "===== TERMAD-OVERLAY =====";
|
package/dist/core/paths.js
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface InlineResult {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
alreadyPatched?: boolean;
|
|
4
|
+
reason?: string;
|
|
5
|
+
mode?: "binary-patch" | "wrapper-fallback";
|
|
6
|
+
}
|
|
7
|
+
export declare const INLINE_MARKER = "===== TERMAD-INLINE =====";
|
|
8
|
+
export declare const INLINE_BACKUP_EXT = ".termad-bundle-orig";
|
|
9
|
+
export declare function inlineBackupPath(target: string): string;
|
|
10
|
+
export declare function isInlinePatched(target: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Attempt to patch a compiled binary to render ads inline in the TUI.
|
|
13
|
+
* Falls back gracefully if patching isn't possible.
|
|
14
|
+
*/
|
|
15
|
+
export declare function installInline(target: string): InlineResult;
|
|
16
|
+
export declare function uninstallInline(target: string): InlineResult;
|
|
17
|
+
export declare function isCompiledBinary(p: string): boolean;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline (in-TUI) patching for compiled CLIs.
|
|
3
|
+
*
|
|
4
|
+
* Goal: inject a tiny footer renderer into the CLI's TUI so the ad appears
|
|
5
|
+
* inside the UI (like Kickbacks did), not above it.
|
|
6
|
+
*
|
|
7
|
+
* Strategy:
|
|
8
|
+
* - Detect Bun/Node compiled single-file binaries (Mach-O/ELF) by magic bytes
|
|
9
|
+
* - Keep pristine original in <bin>.termad-bundle-orig (separate from wrapper backup)
|
|
10
|
+
* - Search for Ink/render footers or stable strings and inject a loader that
|
|
11
|
+
* reads ~/.termad/ad.txt and renders "[ad] ..." as a subtle footer line
|
|
12
|
+
* - Fully reversible via uninstall + audit tracking
|
|
13
|
+
*/
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
15
|
+
import { copyFileSync, existsSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { WRAPPER_MARKER } from "../core/paths.js";
|
|
17
|
+
export const INLINE_MARKER = "===== TERMAD-INLINE =====";
|
|
18
|
+
export const INLINE_BACKUP_EXT = ".termad-bundle-orig";
|
|
19
|
+
export function inlineBackupPath(target) {
|
|
20
|
+
return target + INLINE_BACKUP_EXT;
|
|
21
|
+
}
|
|
22
|
+
export function isInlinePatched(target) {
|
|
23
|
+
try {
|
|
24
|
+
const buf = readFileSync(target);
|
|
25
|
+
// Quick check: look for our marker string in the binary
|
|
26
|
+
return buf.indexOf(Buffer.from(INLINE_MARKER)) !== -1;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function isMachO(p) {
|
|
33
|
+
try {
|
|
34
|
+
const buf = readFileSync(p);
|
|
35
|
+
if (buf.length < 4)
|
|
36
|
+
return false;
|
|
37
|
+
// fat binary or 64-bit little/big endian
|
|
38
|
+
const m = buf.readUInt32BE(0);
|
|
39
|
+
if (m === 0xcafebabe || m === 0xbebafeca)
|
|
40
|
+
return true;
|
|
41
|
+
const m2 = buf.readUInt32LE(0);
|
|
42
|
+
if (m2 === 0xfeedfacf || m2 === 0xcffaedfe)
|
|
43
|
+
return true;
|
|
44
|
+
if (m === 0xfeedface || m === 0xcefaedfe)
|
|
45
|
+
return true;
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function isElf(p) {
|
|
53
|
+
try {
|
|
54
|
+
const buf = readFileSync(p);
|
|
55
|
+
return buf.length >= 4 && buf[0] === 0x7f && buf[1] === 0x45 && buf[2] === 0x4c && buf[3] === 0x46;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function sha256(p) {
|
|
62
|
+
try {
|
|
63
|
+
return createHash("sha256").update(readFileSync(p)).digest("hex");
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Minimal inline ad loader injected into embedded bundle
|
|
70
|
+
// Tries to read ~/.termad/ad.txt and expose a global for TUI to render
|
|
71
|
+
const INLINE_PAYLOAD = `/* ${INLINE_MARKER} */;(()=>{try{const fs=require('fs');const os=require('os');const p=require('path');const home=process.env.TERMAD_HOME||p.join(os.homedir(),'.termad');const f=p.join(home,'ad.txt');const c=fs.readFileSync(f,'utf8');const lines=c.replace(/\\r/g,'').split('\\n');const text=(lines[0]||'').trim();const url=(lines[1]||'').trim();if(text&&text!=='-'){globalThis.__TERMAD_AD__={text,url:url&&url!=='-'?url:undefined,at:Date.now()};}}catch(e){}})();`;
|
|
72
|
+
/**
|
|
73
|
+
* Attempt to patch a compiled binary to render ads inline in the TUI.
|
|
74
|
+
* Falls back gracefully if patching isn't possible.
|
|
75
|
+
*/
|
|
76
|
+
export function installInline(target) {
|
|
77
|
+
if (!existsSync(target))
|
|
78
|
+
return { ok: false, reason: `target not found: ${target}` };
|
|
79
|
+
if (isInlinePatched(target))
|
|
80
|
+
return { ok: true, alreadyPatched: true, mode: "binary-patch" };
|
|
81
|
+
if (isWrappedCheck(target)) {
|
|
82
|
+
// Don't fight the shell wrapper - we'll upgrade it instead
|
|
83
|
+
return { ok: false, reason: "has termad wrapper; use inline upgrade" };
|
|
84
|
+
}
|
|
85
|
+
const bak = inlineBackupPath(target);
|
|
86
|
+
if (!existsSync(bak)) {
|
|
87
|
+
try {
|
|
88
|
+
copyFileSync(target, bak);
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
return { ok: false, reason: e.message };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// For now, try a conservative string injection approach
|
|
95
|
+
// Look for common Ink patterns and inject our loader
|
|
96
|
+
try {
|
|
97
|
+
let buf = readFileSync(target);
|
|
98
|
+
let patched = false;
|
|
99
|
+
// Try to inject near module load points
|
|
100
|
+
const injectPoints = [
|
|
101
|
+
Buffer.from("use strict"),
|
|
102
|
+
Buffer.from("var React"),
|
|
103
|
+
Buffer.from('"use strict"'),
|
|
104
|
+
];
|
|
105
|
+
for (const pt of injectPoints) {
|
|
106
|
+
const idx = buf.indexOf(pt);
|
|
107
|
+
if (idx !== -1 && idx < 2048) {
|
|
108
|
+
// Insert our payload after the first occurrence
|
|
109
|
+
const payload = Buffer.from(INLINE_PAYLOAD + "\n");
|
|
110
|
+
buf = Buffer.concat([buf.slice(0, idx), payload, buf.slice(idx)]);
|
|
111
|
+
patched = true;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (!patched) {
|
|
116
|
+
// Try appending near beginning of embedded JS
|
|
117
|
+
// Many Bun binaries have a loader section
|
|
118
|
+
const payload = Buffer.from(INLINE_PAYLOAD + "\n");
|
|
119
|
+
// Insert after first 256 bytes
|
|
120
|
+
buf = Buffer.concat([buf.slice(0, 512), payload, buf.slice(512)]);
|
|
121
|
+
patched = true;
|
|
122
|
+
}
|
|
123
|
+
if (patched) {
|
|
124
|
+
const mode = statSync(target).mode & 0o777;
|
|
125
|
+
writeFileSync(target, buf);
|
|
126
|
+
try {
|
|
127
|
+
// Restore permissions
|
|
128
|
+
if (!target.endsWith(".cmd") && !target.endsWith(".bat")) {
|
|
129
|
+
// chmod if possible
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch { }
|
|
133
|
+
return { ok: true, mode: "binary-patch" };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
return { ok: false, reason: e.message };
|
|
138
|
+
}
|
|
139
|
+
return { ok: false, reason: "could not find injection point" };
|
|
140
|
+
}
|
|
141
|
+
function isWrappedCheck(target) {
|
|
142
|
+
try {
|
|
143
|
+
const head = readFileSync(target, "utf8").slice(0, 256);
|
|
144
|
+
return head.includes(WRAPPER_MARKER);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
export function uninstallInline(target) {
|
|
151
|
+
const bak = inlineBackupPath(target);
|
|
152
|
+
if (!existsSync(bak)) {
|
|
153
|
+
return { ok: true, alreadyPatched: false, reason: "no inline backup found" };
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
if (existsSync(target) && isInlinePatched(target)) {
|
|
157
|
+
const mode = statSync(bak).mode & 0o777;
|
|
158
|
+
copyFileSync(bak, target);
|
|
159
|
+
rmSync(bak);
|
|
160
|
+
return { ok: true };
|
|
161
|
+
}
|
|
162
|
+
rmSync(bak);
|
|
163
|
+
return { ok: true, reason: "target not patched inline; backup removed" };
|
|
164
|
+
}
|
|
165
|
+
catch (e) {
|
|
166
|
+
return { ok: false, reason: e.message };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
export function isCompiledBinary(p) {
|
|
170
|
+
return isMachO(p) || isElf(p);
|
|
171
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runWithAdOverlay(binary: string, args?: string[]): Promise<void>;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { readFileSync, appendFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const TERMAD_HOME = process.env.TERMAD_HOME || join(homedir(), ".termad");
|
|
5
|
+
const AD_FILE = join(TERMAD_HOME, "ad.txt");
|
|
6
|
+
const COUNT_FILE = join(TERMAD_HOME, "display.log");
|
|
7
|
+
function readAd() {
|
|
8
|
+
try {
|
|
9
|
+
const raw = readFileSync(AD_FILE, "utf8").replace(/\r/g, "").split("\n");
|
|
10
|
+
const text = (raw[0] || "").trim();
|
|
11
|
+
const url = (raw[1] || "").trim();
|
|
12
|
+
const id = (raw[3] || "").trim();
|
|
13
|
+
if (!text || text === "-")
|
|
14
|
+
return null;
|
|
15
|
+
return { text, url: url && url !== "-" ? url : undefined, id: id && id !== "-" ? id : undefined };
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function count(id) {
|
|
22
|
+
if (!id)
|
|
23
|
+
return;
|
|
24
|
+
try {
|
|
25
|
+
appendFileSync(COUNT_FILE, id + "\n");
|
|
26
|
+
}
|
|
27
|
+
catch { }
|
|
28
|
+
}
|
|
29
|
+
function buildFooter(ad) {
|
|
30
|
+
const label = "\x1b[2m\x1b[33m[ad]\x1b[0m";
|
|
31
|
+
const text = ad.text.length > 80 ? ad.text.slice(0, 77) + "..." : ad.text;
|
|
32
|
+
const url = ad.url && ad.url.length > 60 ? ad.url.slice(0, 57) + "..." : ad.url;
|
|
33
|
+
let line = ` ${label} ${text}`;
|
|
34
|
+
if (url)
|
|
35
|
+
line += ` \x1b[36m${url}\x1b[0m`;
|
|
36
|
+
return line;
|
|
37
|
+
}
|
|
38
|
+
export function runWithAdOverlay(binary, args = []) {
|
|
39
|
+
return runWithAdOverlayAsync(binary, args);
|
|
40
|
+
}
|
|
41
|
+
async function runWithAdOverlayAsync(binary, args = []) {
|
|
42
|
+
// Try to use node-pty if available
|
|
43
|
+
let pty;
|
|
44
|
+
try {
|
|
45
|
+
// @ts-ignore
|
|
46
|
+
pty = await import("node-pty");
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
try {
|
|
50
|
+
// @ts-ignore
|
|
51
|
+
pty = await import("@lydell/node-pty");
|
|
52
|
+
}
|
|
53
|
+
catch { }
|
|
54
|
+
}
|
|
55
|
+
if (!pty) {
|
|
56
|
+
// No PTY available - fall back to printing banner then execing
|
|
57
|
+
const ad = readAd();
|
|
58
|
+
if (ad) {
|
|
59
|
+
process.stdout.write("\n [ad] " + ad.text + "\n");
|
|
60
|
+
if (ad.url)
|
|
61
|
+
process.stdout.write(" " + ad.url + "\n\n");
|
|
62
|
+
count(ad.id);
|
|
63
|
+
}
|
|
64
|
+
const { spawn } = await import("node:child_process");
|
|
65
|
+
const child = spawn(binary, args, { stdio: "inherit" });
|
|
66
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const ad = readAd();
|
|
70
|
+
if (ad?.id)
|
|
71
|
+
count(ad.id);
|
|
72
|
+
const cols = process.stdout.columns || 120;
|
|
73
|
+
const rows = process.stdout.rows || 40;
|
|
74
|
+
const ptyProcess = pty.spawn(binary, args, {
|
|
75
|
+
name: "xterm-256color",
|
|
76
|
+
cols,
|
|
77
|
+
rows,
|
|
78
|
+
cwd: process.cwd(),
|
|
79
|
+
env: process.env,
|
|
80
|
+
});
|
|
81
|
+
let footerVisible = !!ad;
|
|
82
|
+
let lastAd = ad;
|
|
83
|
+
function drawFooter() {
|
|
84
|
+
if (!footerVisible || !lastAd)
|
|
85
|
+
return;
|
|
86
|
+
const footer = buildFooter(lastAd);
|
|
87
|
+
// Move to bottom row, clear line, print footer
|
|
88
|
+
const termRows = ptyProcess.rows || rows;
|
|
89
|
+
process.stdout.write(`\x1b[s\x1b[${termRows};1H\x1b[2K${footer}\x1b[u`);
|
|
90
|
+
}
|
|
91
|
+
function hideFooter() {
|
|
92
|
+
const termRows = ptyProcess.rows || rows;
|
|
93
|
+
process.stdout.write(`\x1b[s\x1b[${termRows};1H\x1b[2K\x1b[u`);
|
|
94
|
+
}
|
|
95
|
+
// Pipe PTY output to stdout with footer redrawn after each chunk
|
|
96
|
+
ptyProcess.onData((data) => {
|
|
97
|
+
process.stdout.write(data);
|
|
98
|
+
// Redraw footer after a short delay to stay on top
|
|
99
|
+
setTimeout(drawFooter, 10);
|
|
100
|
+
});
|
|
101
|
+
// Pipe stdin to PTY
|
|
102
|
+
if (process.stdin.isTTY) {
|
|
103
|
+
process.stdin.setRawMode(true);
|
|
104
|
+
}
|
|
105
|
+
process.stdin.on("data", (data) => {
|
|
106
|
+
ptyProcess.write(data);
|
|
107
|
+
});
|
|
108
|
+
// Handle resize
|
|
109
|
+
process.stdout.on("resize", () => {
|
|
110
|
+
const c = process.stdout.columns || cols;
|
|
111
|
+
const r = process.stdout.rows || rows;
|
|
112
|
+
try {
|
|
113
|
+
ptyProcess.resize(c, r);
|
|
114
|
+
}
|
|
115
|
+
catch { }
|
|
116
|
+
setTimeout(drawFooter, 50);
|
|
117
|
+
});
|
|
118
|
+
// Handle exit
|
|
119
|
+
ptyProcess.onExit(({ exitCode }) => {
|
|
120
|
+
if (process.stdin.isTTY) {
|
|
121
|
+
try {
|
|
122
|
+
process.stdin.setRawMode(false);
|
|
123
|
+
}
|
|
124
|
+
catch { }
|
|
125
|
+
}
|
|
126
|
+
process.stdin.pause();
|
|
127
|
+
hideFooter();
|
|
128
|
+
process.exit(exitCode ?? 0);
|
|
129
|
+
});
|
|
130
|
+
// Initial draw
|
|
131
|
+
setTimeout(drawFooter, 100);
|
|
132
|
+
setInterval(drawFooter, 500); // Keep footer visible on redraws
|
|
133
|
+
}
|
package/dist/surfaces/wrap.js
CHANGED
|
@@ -9,12 +9,15 @@ import { chmodSync, copyFileSync, existsSync, readFileSync, rmSync, statSync, wr
|
|
|
9
9
|
import { delimiter, dirname, join } from "node:path";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
11
|
import { WRAPPER_MARKER, backupPath, paths } from "../core/paths.js";
|
|
12
|
+
const SH_TEMPLATE_OVERLAY = "wrapper.sh.overlay";
|
|
12
13
|
const SH_TEMPLATE = "wrapper.sh.asset";
|
|
13
14
|
const CMD_TEMPLATE = "wrapper.cmd.asset";
|
|
14
|
-
function templatePath(isWin) {
|
|
15
|
+
function templatePath(isWin, useOverlay = true) {
|
|
15
16
|
const here = dirname(new URL(import.meta.url).pathname);
|
|
16
17
|
// dist/surfaces → ../assets (copy-assets.mjs mirrors assets/ into dist/)
|
|
17
|
-
|
|
18
|
+
if (isWin)
|
|
19
|
+
return join(here, "..", "assets", CMD_TEMPLATE);
|
|
20
|
+
return join(here, "..", "assets", useOverlay ? SH_TEMPLATE_OVERLAY : SH_TEMPLATE);
|
|
18
21
|
}
|
|
19
22
|
export function isWrapped(target) {
|
|
20
23
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expitc/termad",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
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",
|