blyuecode 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/deps.js +99 -0
- package/dist/core/image.js +69 -0
- package/dist/core/media.js +210 -0
- package/dist/core/player.js +208 -0
- package/dist/core/videoPlayer.js +44 -0
- package/dist/core/ytdlp.js +153 -0
- package/dist/index.js +36 -5
- package/dist/ui/Banner.js +10 -3
- package/dist/ui/Chat.js +438 -1
- package/dist/ui/InputBar.js +8 -2
- package/dist/ui/MascotPrompt.js +27 -6
- package/dist/ui/SplashBanner.js +119 -0
- package/dist/ui/splash.js +121 -0
- package/package.json +1 -1
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
const execAsync = promisify(exec);
|
|
7
|
+
const LOG_DIR = path.join(os.homedir(), '.blyue', 'log');
|
|
8
|
+
const LOG_FILE = path.join(LOG_DIR, 'ytdlp.log');
|
|
9
|
+
function log(line) {
|
|
10
|
+
try {
|
|
11
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
12
|
+
fs.appendFileSync(LOG_FILE, `${new Date().toISOString()} ${line}\n`, 'utf8');
|
|
13
|
+
}
|
|
14
|
+
catch { }
|
|
15
|
+
}
|
|
16
|
+
export function ytdlpLogPath() {
|
|
17
|
+
return LOG_FILE;
|
|
18
|
+
}
|
|
19
|
+
export function readYtdlpLog(limit = 100) {
|
|
20
|
+
if (!fs.existsSync(LOG_FILE))
|
|
21
|
+
return [];
|
|
22
|
+
try {
|
|
23
|
+
const lines = fs.readFileSync(LOG_FILE, 'utf8').split('\n').filter(Boolean);
|
|
24
|
+
return lines.slice(-limit);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function classify(stderr, exitCode) {
|
|
31
|
+
const s = stderr.toLowerCase();
|
|
32
|
+
const tail = stderr.split('\n').slice(-4).join(' | ').slice(0, 300);
|
|
33
|
+
if (s.includes('temporary failure in name resolution') ||
|
|
34
|
+
s.includes('network is unreachable') ||
|
|
35
|
+
s.includes('connection refused') ||
|
|
36
|
+
s.includes('failed to resolve') ||
|
|
37
|
+
s.includes('no address associated') ||
|
|
38
|
+
s.includes('connection timed out')) {
|
|
39
|
+
return {
|
|
40
|
+
kind: 'network',
|
|
41
|
+
userMessage: 'no network — check your connection and try again',
|
|
42
|
+
detail: tail,
|
|
43
|
+
autoFixable: false,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (s.includes('403') || s.includes('forbidden')) {
|
|
47
|
+
return {
|
|
48
|
+
kind: 'forbidden',
|
|
49
|
+
userMessage: 'yt-dlp blocked (HTTP 403) — usually a stale yt-dlp version',
|
|
50
|
+
detail: tail,
|
|
51
|
+
autoFixable: true,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (s.includes('429') || s.includes('too many requests')) {
|
|
55
|
+
return {
|
|
56
|
+
kind: 'ratelimit',
|
|
57
|
+
userMessage: 'rate limited by youtube — wait a minute and retry',
|
|
58
|
+
detail: tail,
|
|
59
|
+
autoFixable: false,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (s.includes('video unavailable') || s.includes('private video') || s.includes('removed')) {
|
|
63
|
+
return {
|
|
64
|
+
kind: 'notfound',
|
|
65
|
+
userMessage: 'video is unavailable, private, or removed',
|
|
66
|
+
detail: tail,
|
|
67
|
+
autoFixable: false,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (s.includes('not available in your country') || s.includes('geo')) {
|
|
71
|
+
return {
|
|
72
|
+
kind: 'geo',
|
|
73
|
+
userMessage: 'video is geo-blocked in your region',
|
|
74
|
+
detail: tail,
|
|
75
|
+
autoFixable: false,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (s.includes('no space left')) {
|
|
79
|
+
return {
|
|
80
|
+
kind: 'disk',
|
|
81
|
+
userMessage: 'out of disk space',
|
|
82
|
+
detail: tail,
|
|
83
|
+
autoFixable: false,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
kind: 'unknown',
|
|
88
|
+
userMessage: `yt-dlp failed (exit ${exitCode}): ${tail || 'no output'}`,
|
|
89
|
+
detail: tail,
|
|
90
|
+
autoFixable: false,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
export async function runYtdlp(args, opts = {}) {
|
|
94
|
+
const cmd = 'yt-dlp ' + args.map((a) => `"${a.replace(/"/g, '\\"')}"`).join(' ');
|
|
95
|
+
const started = Date.now();
|
|
96
|
+
try {
|
|
97
|
+
const { stdout, stderr } = await execAsync(cmd, {
|
|
98
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
99
|
+
timeout: opts.timeoutMs ?? 15 * 60 * 1000,
|
|
100
|
+
});
|
|
101
|
+
log(`OK ${Date.now() - started}ms ${cmd.slice(0, 200)}`);
|
|
102
|
+
return { ok: true, stdout, stderr };
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
const stderr = (err.stderr || '') + (err.stdout ? '\n' + err.stdout : '');
|
|
106
|
+
const code = err.code ?? 1;
|
|
107
|
+
const classified = classify(stderr || String(err), code);
|
|
108
|
+
log(`ERR ${Date.now() - started}ms kind=${classified.kind} code=${code} ` +
|
|
109
|
+
`cmd=${cmd.slice(0, 200)} detail=${classified.detail}`);
|
|
110
|
+
return { ok: false, stdout: err.stdout || '', stderr, error: classified };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export function updateCmd() {
|
|
114
|
+
const isTermux = !!process.env.TERMUX_VERSION || !!process.env.PREFIX?.includes('com.termux');
|
|
115
|
+
if (isTermux)
|
|
116
|
+
return 'pkg upgrade -y yt-dlp';
|
|
117
|
+
const p = os.platform();
|
|
118
|
+
if (p === 'linux')
|
|
119
|
+
return 'sudo apt install --only-upgrade -y yt-dlp || pip install --upgrade yt-dlp';
|
|
120
|
+
if (p === 'darwin')
|
|
121
|
+
return 'brew upgrade yt-dlp';
|
|
122
|
+
if (p === 'win32')
|
|
123
|
+
return 'winget upgrade yt-dlp.yt-dlp';
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
export async function updateYtdlp() {
|
|
127
|
+
const cmd = updateCmd();
|
|
128
|
+
if (!cmd)
|
|
129
|
+
return { ok: false, output: 'no update command for this platform' };
|
|
130
|
+
log(`UPDATE ${cmd}`);
|
|
131
|
+
try {
|
|
132
|
+
const { stdout, stderr } = await execAsync(cmd, {
|
|
133
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
134
|
+
timeout: 5 * 60 * 1000,
|
|
135
|
+
});
|
|
136
|
+
log(`UPDATE OK ${(stdout + stderr).slice(-300)}`);
|
|
137
|
+
return { ok: true, output: (stdout + stderr).slice(-1000) };
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
const out = (err.stderr || '') + (err.stdout || '') + (err.message || '');
|
|
141
|
+
log(`UPDATE ERR ${out.slice(-300)}`);
|
|
142
|
+
return { ok: false, output: out.slice(-1000) };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
export async function ytdlpVersion() {
|
|
146
|
+
try {
|
|
147
|
+
const { stdout } = await execAsync('yt-dlp --version');
|
|
148
|
+
return stdout.trim();
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return 'not installed';
|
|
152
|
+
}
|
|
153
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useEffect } from 'react';
|
|
4
4
|
import { render, Box, Static } from 'ink';
|
|
5
|
+
import { readFileSync } from 'fs';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { dirname, join } from 'path';
|
|
5
8
|
import { Setup } from './ui/Setup.js';
|
|
6
9
|
import { Chat } from './ui/Chat.js';
|
|
7
10
|
import { Banner } from './ui/Banner.js';
|
|
@@ -9,6 +12,24 @@ import { Consent, hasConsented } from './ui/Consent.js';
|
|
|
9
12
|
import { loadConfig, applyConfig } from './core/config.js';
|
|
10
13
|
import { loadSession } from './core/session.js';
|
|
11
14
|
import { initMcpRegistry } from './core/mcpRegistry.js';
|
|
15
|
+
import { playSplash } from './ui/splash.js';
|
|
16
|
+
function getVersion() {
|
|
17
|
+
try {
|
|
18
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
return JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8'))
|
|
20
|
+
.version || '0.0.0';
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return '0.0.0';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function splashWanted() {
|
|
27
|
+
if (process.env.BLYUE_NO_SPLASH === '1')
|
|
28
|
+
return false;
|
|
29
|
+
if (process.env.BLYUE_STATIC === '1')
|
|
30
|
+
return false;
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
12
33
|
const App = () => {
|
|
13
34
|
const [ready, setReady] = useState(false);
|
|
14
35
|
const [resumedCount, setResumedCount] = useState(0);
|
|
@@ -19,15 +40,25 @@ const App = () => {
|
|
|
19
40
|
const existing = loadConfig();
|
|
20
41
|
if (existing) {
|
|
21
42
|
applyConfig(existing);
|
|
22
|
-
void initMcpRegistry();
|
|
23
43
|
const hist = loadSession();
|
|
24
44
|
setResumedCount(hist ? hist.length : 0);
|
|
25
45
|
setReady(true);
|
|
26
46
|
}
|
|
27
47
|
}, [consented]);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!consented)
|
|
50
|
+
return;
|
|
51
|
+
void initMcpRegistry();
|
|
52
|
+
}, [consented]);
|
|
28
53
|
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Static, { items: ['banner'], children: () => _jsx(Banner, {}, "banner") }), !consented ? (_jsx(Consent, { onAccept: () => setConsented(true) })) : ready ? (_jsx(Chat, { resumedCount: resumedCount })) : (_jsx(Setup, { onDone: () => setReady(true) }))] }));
|
|
29
54
|
};
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
55
|
+
async function main() {
|
|
56
|
+
if (splashWanted()) {
|
|
57
|
+
await playSplash(getVersion());
|
|
58
|
+
}
|
|
59
|
+
render(_jsx(App, {}), {
|
|
60
|
+
patchConsole: false,
|
|
61
|
+
exitOnCtrlC: true,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
void main();
|
package/dist/ui/Banner.js
CHANGED
|
@@ -63,7 +63,9 @@ const CODE_SMALL = [
|
|
|
63
63
|
];
|
|
64
64
|
const BLYUE_WIDTH = 44;
|
|
65
65
|
const BLYUE_SMALL_WIDTH = 27;
|
|
66
|
-
const
|
|
66
|
+
const LUMEN_COL_BIG = 35;
|
|
67
|
+
const LUMEN_COL_SMALL = 9;
|
|
68
|
+
const LUMEN_TEXT = 'L U M E N';
|
|
67
69
|
export const Banner = () => {
|
|
68
70
|
const { stdout } = useStdout();
|
|
69
71
|
const width = stdout?.columns ?? 80;
|
|
@@ -71,6 +73,7 @@ export const Banner = () => {
|
|
|
71
73
|
const blyue = wide ? BLYUE_BIG : BLYUE_SMALL;
|
|
72
74
|
const code = wide ? CODE_BIG : CODE_SMALL;
|
|
73
75
|
const blockWidth = wide ? BLYUE_WIDTH : BLYUE_SMALL_WIDTH;
|
|
76
|
+
const lumenCol = wide ? LUMEN_COL_BIG : LUMEN_COL_SMALL;
|
|
74
77
|
const leftPad = Math.max(0, Math.floor((width - blockWidth) / 2));
|
|
75
78
|
const pad = ' '.repeat(leftPad);
|
|
76
79
|
const [animate] = useState(() => shouldAnimate());
|
|
@@ -83,6 +86,10 @@ export const Banner = () => {
|
|
|
83
86
|
}, [animate]);
|
|
84
87
|
const baseHue = animate ? t : 0.08;
|
|
85
88
|
const brand = pastelAt(baseHue);
|
|
86
|
-
const
|
|
87
|
-
|
|
89
|
+
const versionText = 'v' + getVersion();
|
|
90
|
+
const lumenStart = leftPad + lumenCol;
|
|
91
|
+
const lumenEnd = lumenStart + LUMEN_TEXT.length;
|
|
92
|
+
const versionStart = Math.max(lumenEnd + 2, width - versionText.length - 1);
|
|
93
|
+
const gap = Math.max(1, versionStart - lumenEnd);
|
|
94
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [' '.repeat(lumenStart), _jsx(Text, { color: brand, bold: true, children: LUMEN_TEXT }), ' '.repeat(gap), _jsx(Text, { color: "gray", children: versionText })] }), _jsx(Text, { children: " " }), blyue.map((line, i) => (_jsx(Text, { color: pastelAt((baseHue + i * 0.035) % 1), bold: true, children: pad + line }, 'b' + i))), code.map((line, i) => (_jsx(Text, { color: pastelAt((baseHue + i * 0.035 + 0.18) % 1), bold: true, children: pad + line }, 'c' + i)))] }));
|
|
88
95
|
};
|