@crievo/omni 0.1.1 → 0.1.2
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/package.json +3 -2
- package/tools/lib/deps.mjs +236 -0
- package/tools/lib/service.mjs +4 -1
- package/tools/lib/up.mjs +34 -23
- package/tools/omni.mjs +43 -9
- package/tools/package.mjs +3 -3
- package/tools/render.mjs +11 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crievo/omni",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "OmniVideo no seu computador: o CLI omni, o studio local e as ferramentas do pipeline.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"recharts": "3.10.1",
|
|
38
38
|
"voice-glow": "0.3.0",
|
|
39
39
|
"zod": "4.6.5",
|
|
40
|
-
"bun": "1.4.3"
|
|
40
|
+
"bun": "1.4.3",
|
|
41
|
+
"@puppeteer/browsers": "3.2.3"
|
|
41
42
|
}
|
|
42
43
|
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/* The programs the pipeline runs, brought by the CLI itself into ~/.omni (docs/specs/phase-2-plugin-cli.md,
|
|
2
|
+
step 4): no Homebrew, no admin password, the same on macOS, Windows and Linux (x64 and arm64).
|
|
3
|
+
|
|
4
|
+
ffmpeg, ffprobe static builds from the ffmpeg-static project (FFMPEG_RELEASE), in ~/.omni/bin
|
|
5
|
+
chrome a Chrome, Chromium or Brave already installed, else Chrome Headless Shell (Google's
|
|
6
|
+
build for automation) through @puppeteer/browsers, in ~/.omni/browsers; the render
|
|
7
|
+
takes it from CHROME_PATH
|
|
8
|
+
python uv (UV_VERSION, in ~/.omni/bin) makes the app's .venv with a Python of its own and
|
|
9
|
+
installs requirements.txt into it
|
|
10
|
+
|
|
11
|
+
Each piece is checked by running it, and only fetched when it doesn't answer, so `omni up` can call
|
|
12
|
+
ensureDeps on every session. What was installed goes in ~/.omni/deps.json; depsEnv() is the PATH and
|
|
13
|
+
CHROME_PATH the service and `omni run` hand to every tool, so the 60-odd places that spawn "ffmpeg" by
|
|
14
|
+
name need no change. OMNI_DIR moves ~/.omni (tests). */
|
|
15
|
+
|
|
16
|
+
import { spawnSync } from "node:child_process";
|
|
17
|
+
import {
|
|
18
|
+
chmodSync,
|
|
19
|
+
existsSync,
|
|
20
|
+
mkdirSync,
|
|
21
|
+
mkdtempSync,
|
|
22
|
+
readFileSync,
|
|
23
|
+
readdirSync,
|
|
24
|
+
renameSync,
|
|
25
|
+
rmSync,
|
|
26
|
+
writeFileSync,
|
|
27
|
+
} from "node:fs";
|
|
28
|
+
import { homedir, tmpdir } from "node:os";
|
|
29
|
+
import { delimiter, join } from "node:path";
|
|
30
|
+
import { gunzipSync } from "node:zlib";
|
|
31
|
+
|
|
32
|
+
export const OMNI_DIR = process.env.OMNI_DIR || join(homedir(), ".omni");
|
|
33
|
+
export const BIN_DIR = join(OMNI_DIR, "bin");
|
|
34
|
+
const STATE = join(OMNI_DIR, "deps.json");
|
|
35
|
+
export const FFMPEG_RELEASE = "b6.1.1";
|
|
36
|
+
export const UV_VERSION = "0.12.21";
|
|
37
|
+
const PYTHON_VERSION = "3.12";
|
|
38
|
+
|
|
39
|
+
const win = process.platform === "win32";
|
|
40
|
+
const exe = (/** @type {string} */ name) => (win ? `${name}.exe` : name);
|
|
41
|
+
/* ffmpeg-static has no Windows arm64 build: the x64 one runs under emulation */
|
|
42
|
+
const arch = () => (process.arch === "arm64" && !win ? "arm64" : "x64");
|
|
43
|
+
|
|
44
|
+
/** the app's venv Python, as each OS lays a venv out @param {string} codeRoot */
|
|
45
|
+
export const venvPython = codeRoot =>
|
|
46
|
+
win ? join(codeRoot, ".venv", "Scripts", "python.exe") : join(codeRoot, ".venv", "bin", "python");
|
|
47
|
+
|
|
48
|
+
/** @returns {{ chrome?: string }} */
|
|
49
|
+
const readState = () => {
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(readFileSync(STATE, "utf8"));
|
|
52
|
+
} catch {
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const writeState = (/** @type {object} */ patch) => {
|
|
57
|
+
mkdirSync(OMNI_DIR, { recursive: true });
|
|
58
|
+
writeFileSync(STATE, `${JSON.stringify({ ...readState(), ...patch }, null, 2)}\n`);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** the first line a program prints for these args, or null when it doesn't run @param {string} bin @param {string[]} args */
|
|
62
|
+
const answers = (bin, args) => {
|
|
63
|
+
const r = spawnSync(bin, args, { encoding: "utf8", timeout: 20_000 });
|
|
64
|
+
return r.status === 0 ? (r.stdout || r.stderr).trim().split("\n")[0] : null;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** @param {string} url */
|
|
68
|
+
async function fetchBytes(url) {
|
|
69
|
+
const r = await fetch(url);
|
|
70
|
+
if (!r.ok) throw new Error(`download falhou (${r.status}): ${url}`);
|
|
71
|
+
return Buffer.from(await r.arrayBuffer());
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** writes a binary atomically and makes it executable @param {string} file @param {Buffer} bytes */
|
|
75
|
+
function install(file, bytes) {
|
|
76
|
+
mkdirSync(BIN_DIR, { recursive: true });
|
|
77
|
+
const tmp = `${file}.download`;
|
|
78
|
+
writeFileSync(tmp, bytes);
|
|
79
|
+
chmodSync(tmp, 0o755);
|
|
80
|
+
renameSync(tmp, file);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* --- ffmpeg and ffprobe ------------------------------------------------------------------ */
|
|
84
|
+
|
|
85
|
+
export async function ensureFfmpeg() {
|
|
86
|
+
for (const name of ["ffmpeg", "ffprobe"]) {
|
|
87
|
+
const file = join(BIN_DIR, exe(name));
|
|
88
|
+
if (existsSync(file) && answers(file, ["-version"])) continue;
|
|
89
|
+
const url = `https://github.com/eugeneware/ffmpeg-static/releases/download/${FFMPEG_RELEASE}/${name}-${win ? "win32" : process.platform}-${arch()}.gz`;
|
|
90
|
+
install(file, gunzipSync(await fetchBytes(url)));
|
|
91
|
+
if (!answers(file, ["-version"])) throw new Error(`o ${name} baixado não roda neste computador`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* --- Chrome Headless Shell --------------------------------------------------------------- */
|
|
96
|
+
|
|
97
|
+
/** a Chromium browser already on this machine, which the render can drive just as well */
|
|
98
|
+
export function systemChrome() {
|
|
99
|
+
const pf = [process.env.PROGRAMFILES, process.env["PROGRAMFILES(X86)"], process.env.LOCALAPPDATA].filter(Boolean);
|
|
100
|
+
return [
|
|
101
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
102
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
103
|
+
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
|
|
104
|
+
"/usr/bin/google-chrome",
|
|
105
|
+
"/usr/bin/chromium",
|
|
106
|
+
"/usr/bin/chromium-browser",
|
|
107
|
+
...pf.map(d => join(/** @type {string} */ (d), "Google", "Chrome", "Application", "chrome.exe")),
|
|
108
|
+
].find(p => existsSync(p));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** the Chrome the render will use: CHROME_PATH, the one fetched into ~/.omni, else one installed */
|
|
112
|
+
export function chromePath() {
|
|
113
|
+
const fetched = readState().chrome;
|
|
114
|
+
return [process.env.CHROME_PATH, fetched, systemChrome()].find(p => p && existsSync(p)) ?? null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function ensureChrome() {
|
|
118
|
+
const known = chromePath();
|
|
119
|
+
if (known) return known;
|
|
120
|
+
/* the CLI package depends on it; a development checkout uses the Chrome it has */
|
|
121
|
+
const browsers = await import("@puppeteer/browsers");
|
|
122
|
+
const platform = browsers.detectBrowserPlatform();
|
|
123
|
+
if (!platform) throw new Error(`não há Chrome Headless Shell para ${process.platform}/${process.arch}`);
|
|
124
|
+
const browser = browsers.Browser.CHROMEHEADLESSSHELL;
|
|
125
|
+
const buildId = await browsers.resolveBuildId(browser, platform, "stable");
|
|
126
|
+
const got = await browsers.install({ browser, buildId, platform, cacheDir: join(OMNI_DIR, "browsers") });
|
|
127
|
+
writeState({ chrome: got.executablePath });
|
|
128
|
+
return got.executablePath;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* --- uv and the Python venv -------------------------------------------------------------- */
|
|
132
|
+
|
|
133
|
+
async function ensureUv() {
|
|
134
|
+
const file = join(BIN_DIR, exe("uv"));
|
|
135
|
+
if (existsSync(file) && answers(file, ["--version"])) return file;
|
|
136
|
+
const triple = `${process.arch === "arm64" ? "aarch64" : "x86_64"}-${win ? "pc-windows-msvc" : process.platform === "darwin" ? "apple-darwin" : "unknown-linux-gnu"}`;
|
|
137
|
+
const archive = `uv-${triple}.${win ? "zip" : "tar.gz"}`;
|
|
138
|
+
const tmp = mkdtempSync(join(tmpdir(), "omni-uv-"));
|
|
139
|
+
try {
|
|
140
|
+
writeFileSync(
|
|
141
|
+
join(tmp, archive),
|
|
142
|
+
await fetchBytes(`https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/${archive}`),
|
|
143
|
+
);
|
|
144
|
+
/* tar reads both .tar.gz and .zip on macOS and Windows 10+, and .tar.gz on Linux */
|
|
145
|
+
const x = spawnSync("tar", ["-xf", archive], { cwd: tmp, encoding: "utf8" });
|
|
146
|
+
if (x.status !== 0) throw new Error(`não consegui abrir o ${archive}: ${x.stderr.trim()}`);
|
|
147
|
+
const found = readdirSync(tmp, { recursive: true, withFileTypes: true }).find(
|
|
148
|
+
e => e.isFile() && e.name === exe("uv"),
|
|
149
|
+
);
|
|
150
|
+
if (!found) throw new Error(`o ${archive} não tem o uv`);
|
|
151
|
+
install(file, readFileSync(join(found.parentPath, found.name)));
|
|
152
|
+
} finally {
|
|
153
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
154
|
+
}
|
|
155
|
+
return file;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** the venv with requirements.txt installed @param {string} codeRoot */
|
|
159
|
+
export async function ensurePython(codeRoot) {
|
|
160
|
+
const python = venvPython(codeRoot);
|
|
161
|
+
if (existsSync(python) && answers(python, ["-c", "import numpy, soundfile, requests; print('ok')"])) return python;
|
|
162
|
+
const uv = await ensureUv();
|
|
163
|
+
const env = {
|
|
164
|
+
...process.env,
|
|
165
|
+
UV_PYTHON_INSTALL_DIR: join(OMNI_DIR, "python"),
|
|
166
|
+
UV_CACHE_DIR: join(OMNI_DIR, "uv-cache"),
|
|
167
|
+
};
|
|
168
|
+
const venv = spawnSync(uv, ["venv", "--python", PYTHON_VERSION, "--allow-existing", join(codeRoot, ".venv")], {
|
|
169
|
+
encoding: "utf8",
|
|
170
|
+
env,
|
|
171
|
+
});
|
|
172
|
+
if (venv.status !== 0) throw new Error(`uv venv falhou: ${venv.stderr.trim().split("\n").at(-1)}`);
|
|
173
|
+
const pip = spawnSync(uv, ["pip", "install", "--python", python, "-r", join(codeRoot, "requirements.txt")], {
|
|
174
|
+
encoding: "utf8",
|
|
175
|
+
env,
|
|
176
|
+
});
|
|
177
|
+
if (pip.status !== 0) throw new Error(`uv pip install falhou: ${pip.stderr.trim().split("\n").at(-1)}`);
|
|
178
|
+
return python;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* --- together ---------------------------------------------------------------------------- */
|
|
182
|
+
|
|
183
|
+
/** the environment every tool runs with: ~/.omni/bin first on PATH, and the Chrome the render uses */
|
|
184
|
+
export function depsEnv(env = process.env) {
|
|
185
|
+
const chrome = chromePath();
|
|
186
|
+
return {
|
|
187
|
+
PATH: [BIN_DIR, env.PATH ?? ""].filter(Boolean).join(delimiter),
|
|
188
|
+
...(chrome && !env.CHROME_PATH ? { CHROME_PATH: chrome } : {}),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** what answers now: each program's version line, or null @param {string} codeRoot */
|
|
193
|
+
export function checkDeps(codeRoot) {
|
|
194
|
+
const env = depsEnv();
|
|
195
|
+
const on = (/** @type {string} */ name, /** @type {string[]} */ args) => {
|
|
196
|
+
const r = spawnSync(name, args, { encoding: "utf8", timeout: 20_000, env: { ...process.env, ...env } });
|
|
197
|
+
return r.status === 0 ? (r.stdout || r.stderr).trim().split("\n")[0] : null;
|
|
198
|
+
};
|
|
199
|
+
const python = venvPython(codeRoot);
|
|
200
|
+
return {
|
|
201
|
+
ffmpeg: on("ffmpeg", ["-version"]),
|
|
202
|
+
ffprobe: on("ffprobe", ["-version"]),
|
|
203
|
+
chrome: chromePath(),
|
|
204
|
+
python: existsSync(python)
|
|
205
|
+
? answers(python, ["-c", "import numpy, soundfile, requests; print('ok')"]) && python
|
|
206
|
+
: null,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Fetches whatever doesn't answer, one piece at a time, and says how it went.
|
|
212
|
+
* @param {string} codeRoot @param {{ log?: (line: string) => void }} [o]
|
|
213
|
+
* @returns {Promise<{ ok: boolean, missing: string[], errors: string[] }>}
|
|
214
|
+
*/
|
|
215
|
+
export async function ensureDeps(codeRoot, { log = () => {} } = {}) {
|
|
216
|
+
/** @type {string[]} */
|
|
217
|
+
const errors = [];
|
|
218
|
+
const steps = /** @type {const} */ ([
|
|
219
|
+
["ffmpeg", () => ensureFfmpeg()],
|
|
220
|
+
["Chrome sem janela", () => ensureChrome()],
|
|
221
|
+
["Python", () => ensurePython(codeRoot)],
|
|
222
|
+
]);
|
|
223
|
+
for (const [name, run] of steps) {
|
|
224
|
+
try {
|
|
225
|
+
log(`preparando ${name}…`);
|
|
226
|
+
await run();
|
|
227
|
+
} catch (e) {
|
|
228
|
+
errors.push(`${name}: ${/** @type {Error} */ (e).message}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const c = checkDeps(codeRoot);
|
|
232
|
+
const missing = Object.entries(c)
|
|
233
|
+
.filter(([, v]) => !v)
|
|
234
|
+
.map(([k]) => k);
|
|
235
|
+
return { ok: missing.length === 0, missing, errors };
|
|
236
|
+
}
|
package/tools/lib/service.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/* The studio kept on by launchd (macOS): `bun studio.ts` in <code root>/studio, started at login and again
|
|
2
2
|
whenever it stops (30 s apart, so a studio already open in a terminal holding the port just waits its
|
|
3
3
|
turn). PATH goes along because launchd starts with a minimal one, without claude, ffmpeg or the venv's
|
|
4
|
-
python; OMNI_HOME goes along when the workspace is not the
|
|
4
|
+
python, with ~/.omni/bin (tools/lib/deps.mjs) first; OMNI_HOME goes along when the workspace is not the
|
|
5
|
+
code root (tools/lib/paths.mjs).
|
|
5
6
|
|
|
6
7
|
Used by `tools/runner.mjs service` (this repo) and `omni up` (the installed CLI). Throws Errors in
|
|
7
8
|
Portuguese; the callers print them. */
|
|
@@ -10,6 +11,7 @@ import { spawnSync } from "node:child_process";
|
|
|
10
11
|
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
11
12
|
import { homedir } from "node:os";
|
|
12
13
|
import { dirname, join } from "node:path";
|
|
14
|
+
import { BIN_DIR } from "./deps.mjs";
|
|
13
15
|
import { CODE_ROOT, DATA_ROOT, homeEnvPlist } from "./paths.mjs";
|
|
14
16
|
|
|
15
17
|
export const AGENT = "com.omnivideo.studio";
|
|
@@ -44,6 +46,7 @@ function plist(/** @type {string} */ bun, /** @type {string} */ codeRoot) {
|
|
|
44
46
|
const x = (/** @type {string} */ v) => v.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
45
47
|
const path = [
|
|
46
48
|
...new Set([
|
|
49
|
+
BIN_DIR,
|
|
47
50
|
dirname(bun),
|
|
48
51
|
dirname(process.execPath),
|
|
49
52
|
"/opt/homebrew/bin",
|
package/tools/lib/up.mjs
CHANGED
|
@@ -5,24 +5,24 @@
|
|
|
5
5
|
workspace ~/OmniVideo (OMNI_HOME): videos/, omnivideo.config.json pointing at the hosted studio, .env
|
|
6
6
|
app a stable copy of the package in ~/.omni/app/<version>/ (npx's cache can be wiped), and
|
|
7
7
|
~/.omni/bin/omni
|
|
8
|
-
|
|
8
|
+
tools ffmpeg, the Chrome without a window and Python in ~/.omni (tools/lib/deps.mjs; in the
|
|
9
|
+
background on a hook)
|
|
9
10
|
login none yet: the browser opens on the approval page (at most once an hour)
|
|
10
11
|
service the studio as a launchd agent, from the app, so the hosted studio reaches this Mac
|
|
11
12
|
|
|
12
|
-
In this repo (a git checkout) the app, the
|
|
13
|
+
In this repo (a git checkout) the app, the tools and the service are left to `tools/setup.mjs` and
|
|
13
14
|
`tools/runner.mjs service`: `up` never rewires a development machine. */
|
|
14
15
|
|
|
15
16
|
import { spawn, spawnSync } from "node:child_process";
|
|
16
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
17
|
-
import { homedir } from "node:os";
|
|
17
|
+
import { copyFileSync, existsSync, mkdirSync, openSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
18
18
|
import { join } from "node:path";
|
|
19
19
|
import { missingRules } from "./claude-settings.mjs";
|
|
20
20
|
import { CLI_COMMAND, PACKAGE, STUDIO_URL } from "./cli.mjs";
|
|
21
|
+
import { OMNI_DIR, checkDeps, ensureDeps } from "./deps.mjs";
|
|
21
22
|
import { readTokens } from "./oauth-client.mjs";
|
|
22
23
|
import { CODE_ROOT, DATA_ROOT } from "./paths.mjs";
|
|
23
24
|
import { PLIST, service, serviceInstall } from "./service.mjs";
|
|
24
25
|
|
|
25
|
-
export const OMNI_DIR = join(homedir(), ".omni");
|
|
26
26
|
const LOGIN_EVERY_MS = 60 * 60_000;
|
|
27
27
|
|
|
28
28
|
const isDev = (/** @type {string} */ codeRoot) => existsSync(join(codeRoot, ".git"));
|
|
@@ -82,26 +82,31 @@ export function ensureApp(log = () => {}, codeRoot = CODE_ROOT) {
|
|
|
82
82
|
return installed;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
/** the
|
|
86
|
-
*
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
85
|
+
/** the programs the pipeline runs (tools/lib/deps.mjs), fetched by a process of its own on a hook so the
|
|
86
|
+
* session never waits; logs in ~/.omni/deps.log
|
|
87
|
+
* @returns {"ok" | "started" | "running" | string} "ok", or what is still missing */
|
|
88
|
+
export function ensureTools(codeRoot = CODE_ROOT, { background = false } = {}) {
|
|
89
|
+
const c = checkDeps(codeRoot);
|
|
90
|
+
const missing = Object.entries(c)
|
|
91
|
+
.filter(([, v]) => !v)
|
|
92
|
+
.map(([k]) => k);
|
|
93
|
+
if (!missing.length) return "ok";
|
|
90
94
|
mkdirSync(OMNI_DIR, { recursive: true });
|
|
95
|
+
const lock = join(OMNI_DIR, "deps.lock");
|
|
91
96
|
try {
|
|
92
|
-
if (Date.now() - statSync(lock).mtimeMs < 15 * 60_000) return "
|
|
97
|
+
if (Date.now() - statSync(lock).mtimeMs < 15 * 60_000) return "running";
|
|
93
98
|
} catch {
|
|
94
|
-
/* no one is
|
|
99
|
+
/* no one is fetching them */
|
|
95
100
|
}
|
|
101
|
+
if (!background) return missing.join(", ");
|
|
96
102
|
writeFileSync(lock, String(process.pid));
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return r.status === 0 ? "ok" : "failed";
|
|
103
|
+
const out = openSync(join(OMNI_DIR, "deps.log"), "a");
|
|
104
|
+
spawn(process.execPath, [join(codeRoot, "tools", "omni.mjs"), "deps", "install", "--unlock"], {
|
|
105
|
+
detached: true,
|
|
106
|
+
stdio: ["ignore", out, out],
|
|
107
|
+
env: { ...process.env, OMNI_HOME: DATA_ROOT },
|
|
108
|
+
}).unref();
|
|
109
|
+
return "started";
|
|
105
110
|
}
|
|
106
111
|
|
|
107
112
|
/** opens the browser on the approval page in a process of its own, at most once an hour
|
|
@@ -145,10 +150,16 @@ export async function up({ hook = false, log = () => {} } = {}) {
|
|
|
145
150
|
const dev = isDev(codeRoot);
|
|
146
151
|
const url = studioUrlOf();
|
|
147
152
|
|
|
153
|
+
/* ffmpeg, the Chrome without a window and Python, in ~/.omni: in the background on a hook, here otherwise */
|
|
148
154
|
if (!dev) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
155
|
+
if (hook) {
|
|
156
|
+
const tools = ensureTools(codeRoot, { background: true });
|
|
157
|
+
if (tools === "started" || tools === "running")
|
|
158
|
+
notes.push("baixando o ffmpeg, o Chrome sem janela e o Python (alguns minutos)");
|
|
159
|
+
} else {
|
|
160
|
+
const r = await ensureDeps(codeRoot, { log });
|
|
161
|
+
if (!r.ok) notes.push(`faltou preparar ${r.missing.join(", ")}: ${r.errors.join("; ")}`);
|
|
162
|
+
}
|
|
152
163
|
}
|
|
153
164
|
|
|
154
165
|
const loggedIn = !!readTokens(url);
|
package/tools/omni.mjs
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
omni logout signs this computer out in the cloud and here
|
|
13
13
|
omni allow [--yes] the Claude Code permission rules the skills need (asks first)
|
|
14
14
|
omni setup login, the rules and up, for whoever starts from a terminal
|
|
15
|
-
omni doctor
|
|
15
|
+
omni doctor [--json] whether ffmpeg, ffprobe, the Chrome without a window and Python answer
|
|
16
|
+
omni deps install fetches whichever of them doesn't answer into ~/.omni (tools/lib/deps.mjs)
|
|
16
17
|
omni skill <nome> [--v <versão>] [--part i/n]
|
|
17
18
|
one part of a skill's body from the registry (tools/lib/skill-client.mjs)
|
|
18
19
|
omni run <ferramenta> [args…]
|
|
@@ -21,12 +22,13 @@
|
|
|
21
22
|
============================================================================ */
|
|
22
23
|
|
|
23
24
|
import { spawnSync } from "node:child_process";
|
|
24
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
25
|
+
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
25
26
|
import { hostname } from "node:os";
|
|
26
27
|
import { join } from "node:path";
|
|
27
28
|
import { createInterface } from "node:readline/promises";
|
|
28
29
|
import { allowSkillRules, missingRules, settingsFile } from "./lib/claude-settings.mjs";
|
|
29
30
|
import { CLI_COMMAND, STUDIO_URL } from "./lib/cli.mjs";
|
|
31
|
+
import { OMNI_DIR, checkDeps, depsEnv, ensureDeps } from "./lib/deps.mjs";
|
|
30
32
|
import { serveMcp } from "./lib/mcp-server.mjs";
|
|
31
33
|
import { accessToken, login, logout, readTokens } from "./lib/oauth-client.mjs";
|
|
32
34
|
import { CODE_ROOT, DATA_ROOT, isMainModule } from "./lib/paths.mjs";
|
|
@@ -87,6 +89,12 @@ async function statusText(url) {
|
|
|
87
89
|
? "O studio deste computador está ligado e atende o studio hospedado."
|
|
88
90
|
: "O studio deste computador está desligado.",
|
|
89
91
|
);
|
|
92
|
+
const missing = depsMissing();
|
|
93
|
+
lines.push(
|
|
94
|
+
missing.length
|
|
95
|
+
? `Ainda preparando os programas do pipeline (faltam: ${missing.join(", ")}); o omni up baixa sozinho, ou chame-o de novo se demorar mais de 10 minutos.`
|
|
96
|
+
: "O computador está pronto: ffmpeg, ffprobe, o Chrome sem janela e o Python respondem.",
|
|
97
|
+
);
|
|
90
98
|
const rules = missingRules();
|
|
91
99
|
lines.push(
|
|
92
100
|
rules.length
|
|
@@ -97,6 +105,12 @@ async function statusText(url) {
|
|
|
97
105
|
return lines.join("\n");
|
|
98
106
|
}
|
|
99
107
|
|
|
108
|
+
/** which of the pipeline's programs don't answer (tools/lib/deps.mjs) */
|
|
109
|
+
const depsMissing = () =>
|
|
110
|
+
Object.entries(checkDeps(CODE_ROOT))
|
|
111
|
+
.filter(([, v]) => !v)
|
|
112
|
+
.map(([k]) => k);
|
|
113
|
+
|
|
100
114
|
/** the plugin's MCP tools @param {string} url */
|
|
101
115
|
const mcpTools = url => [
|
|
102
116
|
{
|
|
@@ -194,17 +208,37 @@ export async function main(args) {
|
|
|
194
208
|
if (!bin) fail(`não existe a ferramenta ${tool}`);
|
|
195
209
|
const r = spawnSync(/** @type {string} */ (bin), [/** @type {string} */ (script), ...rest], {
|
|
196
210
|
stdio: "inherit",
|
|
197
|
-
env: { ...process.env, OMNI_HOME: DATA_ROOT },
|
|
211
|
+
env: { ...process.env, ...depsEnv(), OMNI_HOME: DATA_ROOT },
|
|
198
212
|
});
|
|
199
213
|
process.exit(r.status ?? 1);
|
|
200
214
|
}
|
|
201
215
|
|
|
202
216
|
if (cmd === "doctor") {
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
217
|
+
const c = checkDeps(CODE_ROOT);
|
|
218
|
+
if (args.includes("--json")) return console.log(JSON.stringify(c));
|
|
219
|
+
const names = {
|
|
220
|
+
ffmpeg: "ffmpeg",
|
|
221
|
+
ffprobe: "ffprobe",
|
|
222
|
+
chrome: "Chrome sem janela",
|
|
223
|
+
python: "Python das ferramentas",
|
|
224
|
+
};
|
|
225
|
+
for (const [k, v] of Object.entries(c))
|
|
226
|
+
console.log(`${v ? "ok " : "falta"} ${names[/** @type {keyof typeof names} */ (k)]}${v ? `: ${v}` : ""}`);
|
|
227
|
+
if (Object.values(c).some(v => !v)) console.log(`Para baixar o que falta: ${CLI_COMMAND} deps install`);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (cmd === "deps" && args[1] === "install") {
|
|
232
|
+
const r = await ensureDeps(CODE_ROOT, { log: line => console.log(line) });
|
|
233
|
+
/* the background run of `omni up --hook` holds this lock while it fetches */
|
|
234
|
+
if (args.includes("--unlock")) rmSync(join(OMNI_DIR, "deps.lock"), { force: true });
|
|
235
|
+
console.log(
|
|
236
|
+
r.ok
|
|
237
|
+
? "Tudo pronto: ffmpeg, ffprobe, o Chrome sem janela e o Python respondem."
|
|
238
|
+
: `Faltou: ${r.errors.join("; ")}`,
|
|
239
|
+
);
|
|
240
|
+
if (!r.ok) process.exit(1);
|
|
241
|
+
return;
|
|
208
242
|
}
|
|
209
243
|
|
|
210
244
|
const url = studioUrl();
|
|
@@ -291,7 +325,7 @@ export async function main(args) {
|
|
|
291
325
|
}
|
|
292
326
|
|
|
293
327
|
fail(
|
|
294
|
-
"uso: omni up [--hook] | login | status [--json] | logout | allow [--yes] | setup | doctor | skill <nome> | run <ferramenta> … | mcp",
|
|
328
|
+
"uso: omni up [--hook] | login | status [--json] | logout | allow [--yes] | setup | doctor | deps install | skill <nome> | run <ferramenta> … | mcp",
|
|
295
329
|
);
|
|
296
330
|
}
|
|
297
331
|
|
package/tools/package.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
this repo into .release/npm/ and packed as a .tgz. It carries the code a user's machine runs (the CLI,
|
|
5
5
|
the pipeline tools, the templates and guides, the studio's API, runner and web) and nothing of the
|
|
6
6
|
skills, which come from the registry. The studio runs on the `bun` dependency, so a machine needs Node,
|
|
7
|
-
not Bun; Python
|
|
7
|
+
not Bun; ffmpeg, the Chrome without a window and Python are fetched by `omni up` (tools/lib/deps.mjs).
|
|
8
8
|
|
|
9
9
|
node tools/package.mjs [--version <x.y.z>] builds and packs; prints the .tgz path
|
|
10
10
|
node tools/package.mjs publish [--version …] builds, packs and publishes to npm with the account in
|
|
@@ -93,8 +93,8 @@ function build() {
|
|
|
93
93
|
os: ["darwin"],
|
|
94
94
|
license: "UNLICENSED",
|
|
95
95
|
homepage: "https://omni.crievo.com",
|
|
96
|
-
/* the studio's runtime libraries,
|
|
97
|
-
dependencies: { ...studio.dependencies, bun: "1.4.3" },
|
|
96
|
+
/* the studio's runtime libraries, the bun that runs it, and what fetches the Chrome without a window */
|
|
97
|
+
dependencies: { ...studio.dependencies, bun: "1.4.3", "@puppeteer/browsers": "3.2.3" },
|
|
98
98
|
};
|
|
99
99
|
writeFileSync(join(PKG, "package.json"), `${JSON.stringify(pkg, null, 2)}\n`);
|
|
100
100
|
writeFileSync(
|
package/tools/render.mjs
CHANGED
|
@@ -79,9 +79,20 @@ const TMP = join(tmpdir(), `render-${process.pid}`);
|
|
|
79
79
|
const GL = has("gl") || /webgl|VoiceAvatar/i.test(readFileSync(SRC, "utf8"));
|
|
80
80
|
|
|
81
81
|
/* --- Chrome ---------------------------------------------------------------- */
|
|
82
|
+
/* the Chrome Headless Shell `omni up` fetched into ~/.omni (tools/lib/deps.mjs) */
|
|
83
|
+
function omniChrome() {
|
|
84
|
+
try {
|
|
85
|
+
const dir = process.env.OMNI_DIR || join(process.env.HOME ?? "", ".omni");
|
|
86
|
+
return JSON.parse(readFileSync(join(dir, "deps.json"), "utf8")).chrome;
|
|
87
|
+
} catch {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
82
92
|
async function findChrome() {
|
|
83
93
|
const fixed = [
|
|
84
94
|
process.env.CHROME_PATH,
|
|
95
|
+
omniChrome(),
|
|
85
96
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
86
97
|
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
87
98
|
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
|