@crievo/omni 0.1.6 → 0.1.8
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/omnivideo.config.example.json +1 -0
- package/package.json +1 -1
- package/reference/cloud.md +15 -1
- package/reference/learnings.md +7 -0
- package/reference/studio.md +1 -0
- package/studio/api/app.ts +4 -0
- package/studio/api/routes/cost.ts +13 -0
- package/studio/api/routes/me.ts +8 -0
- package/studio/api/services/agent-tools.ts +2 -2
- package/studio/api/services/cost.ts +22 -0
- package/studio/api/services/me.ts +67 -0
- package/studio/api/services/sessions.ts +8 -2
- package/studio/runner/runner.ts +57 -10
- package/studio/shared/omni-screens.ts +1 -1
- package/studio/shared/schemas/cost.ts +10 -0
- package/studio/shared/types/api.ts +74 -0
- package/studio/studio.ts +27 -2
- package/studio/web/components/AppShell.tsx +11 -3
- package/studio/web/components/FirstConnection.tsx +222 -0
- package/studio/web/components/OfflineNotice.tsx +59 -0
- package/studio/web/components/SetupSteps.tsx +63 -0
- package/studio/web/components/Sidebar.tsx +10 -25
- package/studio/web/components/StudioOffline.tsx +48 -50
- package/studio/web/components/UserCard.tsx +79 -0
- package/studio/web/components/icons.tsx +2 -0
- package/studio/web/hooks/useComputers.ts +55 -0
- package/studio/web/hooks/useCost.ts +11 -0
- package/studio/web/hooks/useMe.ts +32 -0
- package/studio/web/hooks/useSetupProgress.ts +54 -0
- package/studio/web/lib/me.ts +17 -0
- package/studio/web/lib/query-client.ts +6 -0
- package/studio/web/lib/settings-categories.ts +2 -2
- package/studio/web/lib/setup-retry.ts +29 -0
- package/studio/web/lib/studio-offline.ts +105 -59
- package/studio/web/pages/ConnectPreviewPage/index.tsx +215 -0
- package/studio/web/pages/SettingsPage/components/AgentCategory.tsx +18 -9
- package/studio/web/pages/SettingsPage/components/AgentCostPanel.tsx +253 -0
- package/studio/web/pages/SettingsPage/components/ComputersPanel.tsx +145 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/OmnichatSettingsPanel.tsx +2 -2
- package/studio/web/pages/SettingsPage/components/PublishingCategory/SchedulePanel.tsx +3 -3
- package/studio/web/pages/SettingsPage/components/UsageCategory.tsx +7 -2
- package/studio/web/pages/SettingsPage/index.tsx +15 -1
- package/studio/web/pages/VideoPage/components/PublishForm.tsx +5 -2
- package/studio/web/router.tsx +8 -0
- package/studio/web/styles/components/setup-steps.css +163 -0
- package/studio/web/styles/components/user-card.css +121 -0
- package/studio/web/styles/index.css +2 -0
- package/studio/web/styles/pages/settings/shell.css +79 -0
- package/studio/web/styles/pages/settings/usage.css +47 -0
- package/studio/web/styles/pages/studio-offline.css +224 -38
- package/tools/cost.mjs +114 -0
- package/tools/lib/agents.mjs +9 -3
- package/tools/lib/channel.mjs +1 -1
- package/tools/lib/cloud-user.mjs +58 -0
- package/tools/lib/cost.mjs +357 -0
- package/tools/lib/deps.mjs +27 -12
- package/tools/lib/oauth-client.mjs +28 -14
- package/tools/lib/pages-style.mjs +67 -0
- package/tools/lib/pairing.mjs +1 -1
- package/tools/lib/schedule/schedule.mjs +1 -1
- package/tools/lib/self-update.mjs +122 -0
- package/tools/lib/setup-progress.mjs +315 -0
- package/tools/lib/up.mjs +176 -61
- package/tools/metrics.mjs +6 -1
- package/tools/omni.mjs +41 -16
- package/tools/omnichat.mjs +45 -9
- package/tools/platforms.mjs +75 -14
- package/tools/runner.mjs +6 -6
|
@@ -20,6 +20,7 @@ import { mkdirSync, rmSync, statSync } from "node:fs";
|
|
|
20
20
|
import { createServer } from "node:http";
|
|
21
21
|
import { homedir } from "node:os";
|
|
22
22
|
import { join } from "node:path";
|
|
23
|
+
import { confirmation, document_ } from "./pages-style.mjs";
|
|
23
24
|
import { openUrl } from "./platform.mjs";
|
|
24
25
|
import { defaultVault } from "./vault.mjs";
|
|
25
26
|
|
|
@@ -155,25 +156,38 @@ export async function login(studioUrl, o) {
|
|
|
155
156
|
res.writeHead(404).end();
|
|
156
157
|
return;
|
|
157
158
|
}
|
|
158
|
-
const
|
|
159
|
-
`<!doctype html><meta charset="utf-8"><title>OmniVideo</title><body style="font:16px system-ui;padding:48px;max-width:32em">${msg}</body>`;
|
|
159
|
+
const html = (/** @type {Parameters<typeof confirmation>[0]} */ c) => document_(c.title, confirmation(c));
|
|
160
160
|
if (u.searchParams.get("state") !== state) {
|
|
161
|
-
res
|
|
162
|
-
|
|
163
|
-
|
|
161
|
+
res.writeHead(400, { "content-type": "text/html; charset=utf-8" }).end(
|
|
162
|
+
html({
|
|
163
|
+
kind: "error",
|
|
164
|
+
title: "Esse retorno não é deste login",
|
|
165
|
+
text: "Esta página veio de outra tentativa de login. Nada foi conectado.",
|
|
166
|
+
hint: "Abra uma nova sessão do Claude Code para tentar de novo.",
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
164
169
|
return;
|
|
165
170
|
}
|
|
166
171
|
const code = u.searchParams.get("code");
|
|
167
172
|
const error = u.searchParams.get("error");
|
|
168
|
-
res
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
173
|
+
res.writeHead(code ? 200 : 400, { "content-type": "text/html; charset=utf-8" }).end(
|
|
174
|
+
html(
|
|
175
|
+
code
|
|
176
|
+
? {
|
|
177
|
+
kind: "ok",
|
|
178
|
+
title: "Computador conectado",
|
|
179
|
+
text: "Pode fechar esta aba. Estamos preparando este computador em segundo plano: acompanhe cada etapa no Omni.",
|
|
180
|
+
action: { href: studioUrl, label: "Abrir o Omni" },
|
|
181
|
+
hint: "Este login vale só para este computador. Você pode desconectá-lo em Configurações → Agente de IA.",
|
|
182
|
+
}
|
|
183
|
+
: {
|
|
184
|
+
kind: "error",
|
|
185
|
+
title: "Login recusado",
|
|
186
|
+
text: "Este computador não foi conectado ao Omni.",
|
|
187
|
+
hint: "Para tentar de novo, abra uma nova sessão do Claude Code.",
|
|
188
|
+
},
|
|
189
|
+
),
|
|
190
|
+
);
|
|
177
191
|
settle(code ? { code } : { error: error ?? "recusado" });
|
|
178
192
|
});
|
|
179
193
|
await new Promise((resolve, reject) => {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/* The look of the few pages that are not the studio's web app: the hosted sign-in and the computer's approval
|
|
2
|
+
(cloud/studio/src/pages.mjs, account-pages.mjs) and the page the CLI's loopback shows when the login is done
|
|
3
|
+
(tools/lib/oauth-client.mjs). They come before any session or any app, so they can't reuse the bundle; this
|
|
4
|
+
file carries the same tokens (studio/web/styles/tokens.css: the ElevenLabs layer, Schibsted Grotesk, 12 px
|
|
5
|
+
controls, 7.5 % borders, light and dark) so they read as the same product. Pure strings: no Worker or Node API,
|
|
6
|
+
so both sides import it. Text in Portuguese: it reaches the user as it is. */
|
|
7
|
+
|
|
8
|
+
export const esc = (/** @type {unknown} */ s) => String(s ?? "").replace(/[&<>"']/g, c => `&#${c.charCodeAt(0)};`);
|
|
9
|
+
|
|
10
|
+
/* the brand ring, black on transparent (studio/web/assets/logos/omni-black.png); the dark theme inverts it */
|
|
11
|
+
const RING =
|
|
12
|
+
"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";
|
|
13
|
+
|
|
14
|
+
export const brandMark = (/** @type {number} */ size = 28) =>
|
|
15
|
+
`<img class="ring" src="data:image/png;base64,${RING}" width="${size}" height="${size}" alt="">`;
|
|
16
|
+
|
|
17
|
+
export const brand = () => `<span class="brand">${brandMark(28)}<span>Omni</span></span>`;
|
|
18
|
+
|
|
19
|
+
/** Google's mark, in its own colors (the outlined "Entrar com Google" button) */
|
|
20
|
+
export const GOOGLE_LOGO =
|
|
21
|
+
'<svg class="g" viewBox="0 0 48 48" width="18" height="18" aria-hidden="true"><path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/><path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/><path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/><path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/></svg>';
|
|
22
|
+
|
|
23
|
+
/** the round badge on the confirmation pages: a check in emerald, a cross in the error color, or a mark in amber
|
|
24
|
+
* @param {"ok" | "error" | "warning"} kind */
|
|
25
|
+
export const badge = kind =>
|
|
26
|
+
kind === "warning"
|
|
27
|
+
? '<span class="badge warning" aria-hidden="true"><svg viewBox="0 0 24 24" width="30" height="30" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M12 7v6M12 17h.01"/></svg></span>'
|
|
28
|
+
: kind === "ok"
|
|
29
|
+
? '<span class="badge ok" aria-hidden="true"><svg viewBox="0 0 24 24" width="30" height="30" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></span>'
|
|
30
|
+
: '<span class="badge error" aria-hidden="true"><svg viewBox="0 0 24 24" width="30" height="30" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M6.5 6.5l11 11M17.5 6.5l-11 11"/></svg></span>';
|
|
31
|
+
|
|
32
|
+
export const STYLE = `:root{color-scheme:light dark;--bg:#fff;--surface-2:#f4f4f4;--text:#0f0f10;--text-2:#00000087;--text-3:#00000070;--border:#00000013;--border-strong:#0000001a;--muted:#0000000b;--action:#0f0f10;--action-text:#fff;--action-hover:#2e2e2e;--emerald:#059669;--ok-bg:rgb(0 204 68/.1);--error:#ef4444;--error-bg:rgb(239 68 68/.1);--warning:#c2610c;--warning-bg:rgb(245 158 11/.12)}
|
|
33
|
+
@media (prefers-color-scheme:dark){:root{--bg:#0f0f10;--surface-2:#18181b;--text:#fff;--text-2:#ffffffa4;--text-3:#ffffff87;--border:#ffffff13;--border-strong:#ffffff1a;--muted:#ffffff0b;--action:#fff;--action-text:#0f0f10;--action-hover:#e5e5e5;--emerald:#34d399;--ok-bg:rgb(0 204 68/.16);--error:#ff6b6b;--error-bg:rgb(255 107 107/.14);--warning:#fda05e;--warning-bg:rgb(252 122 55/.15)}.ring{filter:invert(1)}}
|
|
34
|
+
*{box-sizing:border-box}html,body{height:100%}body{margin:0;background:var(--bg);color:var(--text);font:400 14px/20px "Schibsted Grotesk","Helvetica Neue",Arial,sans-serif;-webkit-font-smoothing:antialiased}
|
|
35
|
+
a{color:inherit}code{font:400 13px/20px "Geist Mono",ui-monospace,Menlo,monospace;padding:1px 6px;border:1px solid var(--border-strong);border-radius:6px;overflow-wrap:anywhere}
|
|
36
|
+
.brand{display:inline-flex;align-items:center;gap:8px;font-weight:700;font-size:18px;letter-spacing:-.2px}.ring{display:block}
|
|
37
|
+
h1{margin:0;font:400 28px/36px "Schibsted Grotesk","Helvetica Neue",Arial,sans-serif;letter-spacing:-.21px}p{margin:0;color:var(--text-2)}
|
|
38
|
+
.button{display:inline-flex;align-items:center;justify-content:center;gap:10px;width:100%;height:40px;padding:0 16px;border-radius:12px;border:1px solid transparent;background:var(--action);color:var(--action-text);font-family:inherit;font-weight:500;font-size:14px;line-height:20px;cursor:pointer;text-decoration:none}
|
|
39
|
+
.button:hover{background:var(--action-hover)}.button.outline{background:transparent;color:var(--text);border-color:var(--border-strong)}.button.outline:hover{background:var(--muted)}
|
|
40
|
+
.button:focus-visible{outline:2px solid var(--text);outline-offset:2px}.button .g{flex:none}.row{display:grid;gap:8px}
|
|
41
|
+
.notice{padding:10px 12px;border-radius:12px;font-size:13px;line-height:18px}.notice.error{background:var(--error-bg);color:var(--error)}.notice.warning{background:var(--warning-bg);color:var(--warning)}
|
|
42
|
+
.badge{display:grid;place-items:center;width:72px;height:72px;border-radius:50%}.badge.ok{background:var(--ok-bg);color:var(--emerald)}.badge.error{background:var(--error-bg);color:var(--error)}.badge.warning{background:var(--warning-bg);color:var(--warning)}
|
|
43
|
+
/* the confirmation card: one centered column */
|
|
44
|
+
.center{min-height:100%;display:grid;place-items:center;padding:24px 16px}.card{width:100%;max-width:420px;display:grid;justify-items:center;gap:12px;text-align:center}
|
|
45
|
+
.card .brand{margin-bottom:12px}.card .two{grid-template-columns:1fr 1fr}.code-big{font:600 24px/32px "Geist Mono",ui-monospace,Menlo,monospace;letter-spacing:2px;padding:10px 18px;border:1px solid var(--border-strong);border-radius:12px;background:var(--surface-2)}.card p{max-width:46ch}.card .row{width:100%;margin-top:12px}.card .hint{font-size:13px;color:var(--text-3)}
|
|
46
|
+
/* the sign-in: two columns, the form on the right */
|
|
47
|
+
.split{min-height:100%;display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr)}
|
|
48
|
+
.split .intro{display:flex;flex-direction:column;justify-content:space-between;gap:32px;padding:40px 48px;background:var(--surface-2);border-right:1px solid var(--border)}
|
|
49
|
+
.split .intro h2{margin:0 0 12px;font:400 28px/36px "Schibsted Grotesk","Helvetica Neue",Arial,sans-serif;letter-spacing:-.21px;max-width:18ch}.split .intro ul{list-style:none;margin:0;padding:0;display:grid;gap:14px;max-width:44ch}
|
|
50
|
+
.split .intro li{display:grid;grid-template-columns:20px 1fr;gap:10px;color:var(--text-2)}.split .intro li svg{margin-top:2px;color:var(--emerald)}.split .intro li b{display:block;color:var(--text);font-weight:500}
|
|
51
|
+
.split .intro small{color:var(--text-3);font-size:12px}
|
|
52
|
+
.split .side{display:grid;place-items:center;padding:40px 24px}.split form,.split .box{width:100%;max-width:360px;display:grid;gap:16px}.split .box .head{display:grid;gap:6px}
|
|
53
|
+
.split .box .brand-top{display:none}
|
|
54
|
+
@media (max-width:860px){.split{grid-template-columns:1fr}.split .intro{display:none}.split .box .brand-top{display:inline-flex;margin-bottom:8px}}
|
|
55
|
+
@media (prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}`;
|
|
56
|
+
|
|
57
|
+
/** The whole document.
|
|
58
|
+
* @param {string} title @param {string} body @param {{ head?: string }} [o] */
|
|
59
|
+
export function document_(title, body, { head = "" } = {}) {
|
|
60
|
+
return `<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${esc(title)} · Omni</title><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Schibsted+Grotesk:wght@400;500;600;700&family=Geist+Mono:wght@400;500&display=swap">${head}<style>${STYLE}</style></head><body>${body}</body></html>`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** a centered confirmation: the badge, a title and a disclaimer, with an optional button
|
|
64
|
+
* @param {{ kind: "ok" | "error" | "warning", title: string, text: string, hint?: string, action?: { href: string, label: string }, extra?: string }} o
|
|
65
|
+
* `text`, `hint` and `extra` are HTML: whoever calls escapes what came from outside */
|
|
66
|
+
export const confirmation = ({ kind, title, text, hint, action, extra = "" }) =>
|
|
67
|
+
`<main class="center"><div class="card">${brand()}${badge(kind)}<h1>${esc(title)}</h1><p>${text}</p>${extra}${action ? `<div class="row"><a class="button" href="${esc(action.href)}">${esc(action.label)}</a></div>` : ""}${hint ? `<p class="hint">${hint}</p>` : ""}</div></main>`;
|
package/tools/lib/pairing.mjs
CHANGED
|
@@ -77,7 +77,7 @@ export async function finishPairing(url, code, name, d = {}) {
|
|
|
77
77
|
if (!session) throw new Error("o código do pareamento expirou");
|
|
78
78
|
const reg = await call(url, "/runner/register", { body: { name }, token: session }, f);
|
|
79
79
|
if (!reg.ok || !reg.body?.key)
|
|
80
|
-
throw new Error(`o studio não entregou a chave
|
|
80
|
+
throw new Error(`o studio não entregou a chave da máquina (${reg.status}: ${reg.body?.error ?? "?"})`);
|
|
81
81
|
return { key: reg.body.key, name: reg.body.name };
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -121,7 +121,7 @@ export function dueAction(sch, { now, alive, settings }) {
|
|
|
121
121
|
if (!sch.attempts && delay > settings.max_delay_h)
|
|
122
122
|
return {
|
|
123
123
|
action: "missed",
|
|
124
|
-
message: `
|
|
124
|
+
message: `a máquina estava dormindo ou desligada na hora: ${delay.toFixed(1)} h de atraso passa do limite de ${settings.max_delay_h} h (publish.schedule.max_delay_h)`,
|
|
125
125
|
};
|
|
126
126
|
return { action: "run" };
|
|
127
127
|
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/* The studio service updating itself. When the hosted studio and this machine speak different protocols the hub
|
|
2
|
+
closes the runner's socket with UPGRADE (tools/lib/channel.mjs), and which side is old is not in a form the runner
|
|
3
|
+
can read. It doesn't need to: it asks the npm registry whether a version of the CLI newer than the one it runs
|
|
4
|
+
exists. If so, this machine is the one behind, and `npx -y @crievo/omni@<latest> up` does what a session's hook
|
|
5
|
+
would: installs the new copy under ~/.omni/app, swaps the studio service onto it and restarts it
|
|
6
|
+
(tools/lib/up.mjs). If not, the hosted studio is the old side (or npm hasn't published yet) and there is nothing
|
|
7
|
+
to do here but wait: the runner looks again in a minute.
|
|
8
|
+
|
|
9
|
+
Never on a development checkout (`.git` in the code root: `omni up` never rewires one), never without the
|
|
10
|
+
plugin's login (a legacy key machine has no `up` to run), never while the setup is running, and at most once an
|
|
11
|
+
hour. Everything the outside world does (the registry, the process) is injected for the tests. Messages in
|
|
12
|
+
Portuguese: they reach the log as they are. */
|
|
13
|
+
|
|
14
|
+
import { spawn } from "node:child_process";
|
|
15
|
+
import { existsSync, mkdirSync, openSync, readFileSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { PACKAGE } from "./cli.mjs";
|
|
18
|
+
import { OMNI_DIR, depsEnv } from "./deps.mjs";
|
|
19
|
+
import { CODE_ROOT, DATA_ROOT } from "./paths.mjs";
|
|
20
|
+
|
|
21
|
+
const REGISTRY = "https://registry.npmjs.org";
|
|
22
|
+
export const UPDATE_EVERY_MS = 60 * 60_000;
|
|
23
|
+
|
|
24
|
+
/** the version a code root runs, or null (a checkout has no package.json of the CLI) @param {string} codeRoot */
|
|
25
|
+
export function versionOf(codeRoot) {
|
|
26
|
+
try {
|
|
27
|
+
return String(JSON.parse(readFileSync(join(codeRoot, "package.json"), "utf8")).version ?? "") || null;
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** whether `a` is a later release than `b` (major.minor.patch; a pre-release tag counts as the release it precedes)
|
|
34
|
+
* @param {string} a @param {string} b */
|
|
35
|
+
export function isNewer(a, b) {
|
|
36
|
+
const n = (/** @type {string} */ v) =>
|
|
37
|
+
v
|
|
38
|
+
.split("-")[0]
|
|
39
|
+
.split(".")
|
|
40
|
+
.map(x => Number.parseInt(x, 10) || 0);
|
|
41
|
+
const [x, y] = [n(a), n(b)];
|
|
42
|
+
for (let i = 0; i < 3; i++) if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) > (y[i] ?? 0);
|
|
43
|
+
/* same numbers: "1.2.3" is later than "1.2.3-beta" */
|
|
44
|
+
return !a.includes("-") && b.includes("-");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** the newest published version of the CLI @param {{ fetch?: typeof fetch }} [o] */
|
|
48
|
+
export async function latestVersion({ fetch: doFetch = fetch } = {}) {
|
|
49
|
+
const r = await doFetch(`${REGISTRY}/${PACKAGE.replace("/", "%2F")}/latest`, {
|
|
50
|
+
headers: { accept: "application/json" },
|
|
51
|
+
signal: AbortSignal.timeout(8_000),
|
|
52
|
+
});
|
|
53
|
+
if (!r.ok) throw new Error(`o npm respondeu ${r.status}`);
|
|
54
|
+
const v = String(/** @type {any} */ (await r.json()).version ?? "");
|
|
55
|
+
if (!/^\d+\.\d+\.\d+/.test(v)) throw new Error("o npm não disse a versão");
|
|
56
|
+
return v;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** runs `npx -y <spec> up` in a process of its own, so the service swapping itself doesn't take it down with it;
|
|
60
|
+
* its output goes to ~/.omni/update.log @param {{ spec: string, dataRoot: string }} o */
|
|
61
|
+
export function startUpdate({ spec, dataRoot }) {
|
|
62
|
+
mkdirSync(OMNI_DIR, { recursive: true });
|
|
63
|
+
const out = openSync(join(OMNI_DIR, "update.log"), "a");
|
|
64
|
+
const win = process.platform === "win32";
|
|
65
|
+
spawn(win ? "npx.cmd" : "npx", ["-y", spec, "up"], {
|
|
66
|
+
detached: true,
|
|
67
|
+
windowsHide: true,
|
|
68
|
+
/* npx is a .cmd on Windows, which Node only spawns through a shell */
|
|
69
|
+
shell: win,
|
|
70
|
+
stdio: ["ignore", out, out],
|
|
71
|
+
env: { ...process.env, ...depsEnv(), OMNI_HOME: dataRoot },
|
|
72
|
+
}).unref();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {{
|
|
77
|
+
* codeRoot?: string,
|
|
78
|
+
* dataRoot?: string,
|
|
79
|
+
* fetch?: typeof fetch,
|
|
80
|
+
* run?: (o: { spec: string, dataRoot: string }) => void,
|
|
81
|
+
* log?: (line: string) => void,
|
|
82
|
+
* now?: () => number,
|
|
83
|
+
* everyMs?: number,
|
|
84
|
+
* busy?: () => boolean,
|
|
85
|
+
* loggedIn?: () => boolean,
|
|
86
|
+
* }} [o]
|
|
87
|
+
* @returns {() => Promise<"dev" | "login" | "busy" | "wait" | "offline" | "current" | "started">} one look: what it did
|
|
88
|
+
*/
|
|
89
|
+
export function createSelfUpdater({
|
|
90
|
+
codeRoot = CODE_ROOT,
|
|
91
|
+
dataRoot = DATA_ROOT,
|
|
92
|
+
fetch: doFetch = fetch,
|
|
93
|
+
run = startUpdate,
|
|
94
|
+
log = () => {},
|
|
95
|
+
now = Date.now,
|
|
96
|
+
everyMs = UPDATE_EVERY_MS,
|
|
97
|
+
busy = () => false,
|
|
98
|
+
loggedIn = () => true,
|
|
99
|
+
} = {}) {
|
|
100
|
+
let lastStart = 0;
|
|
101
|
+
return async function update() {
|
|
102
|
+
if (existsSync(join(codeRoot, ".git"))) return "dev";
|
|
103
|
+
if (!loggedIn()) return "login";
|
|
104
|
+
if (busy()) return "busy";
|
|
105
|
+
if (lastStart && now() - lastStart < everyMs) return "wait";
|
|
106
|
+
const current = versionOf(codeRoot);
|
|
107
|
+
let latest;
|
|
108
|
+
try {
|
|
109
|
+
latest = await latestVersion({ fetch: doFetch });
|
|
110
|
+
} catch (e) {
|
|
111
|
+
log(`aviso: não consegui ver se há versão nova do Omni (${/** @type {Error} */ (e).message})`);
|
|
112
|
+
return "offline";
|
|
113
|
+
}
|
|
114
|
+
/* no newer version: this machine is already the latest, so the hosted studio is the old side, or npm is yet to
|
|
115
|
+
publish what it was deployed with */
|
|
116
|
+
if (!current || !isNewer(latest, current)) return "current";
|
|
117
|
+
lastStart = now();
|
|
118
|
+
log(`aviso: atualizando o Omni ${current} → ${latest}: esta máquina está atrás do studio hospedado`);
|
|
119
|
+
run({ spec: `${PACKAGE}@${latest}`, dataRoot });
|
|
120
|
+
return "started";
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/* The first-time setup, as the user watches it. After the plugin's login the CLI prepares the computer (the
|
|
2
|
+
workspace, a stable copy of the app, the programs the pipeline runs, the studio service), and the hosted
|
|
3
|
+
page shows each piece as a vertical stepper with a loading circle that turns into a check
|
|
4
|
+
(studio/web/components/SetupSteps.tsx). The page can't ask this computer while the studio isn't connected
|
|
5
|
+
to it, so the CLI writes the progress to the user's own cloud, which the page reads
|
|
6
|
+
(`setup` in the config of GET /api/cloud/config; cloud/studio/src/cloud.mjs). The same record is kept in
|
|
7
|
+
~/.omni/setup.json.
|
|
8
|
+
|
|
9
|
+
The steps are named for what they are for, never for the program that does it: the user reads "Configuração
|
|
10
|
+
para extrair o estilo visual", not "yt-dlp". Their ids are the ones tools/lib/deps.mjs reports (video,
|
|
11
|
+
render, voice, style). Labels reach the user as they are: Portuguese.
|
|
12
|
+
|
|
13
|
+
The reporter never throws and never waits on the network for long: a failed update is dropped, the next
|
|
14
|
+
one carries the whole record anyway. The clock, fetch, the token and the file are injected for the tests. */
|
|
15
|
+
|
|
16
|
+
import { mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { OMNI_DIR } from "./deps.mjs";
|
|
19
|
+
|
|
20
|
+
export const SETUP_FILE = join(OMNI_DIR, "setup.json");
|
|
21
|
+
const LOCK_FILE = join(OMNI_DIR, "setup.lock");
|
|
22
|
+
/* a setup that holds the lock for longer than this is a dead one */
|
|
23
|
+
const LOCK_STALE_MS = 60 * 60_000;
|
|
24
|
+
const PUT_TIMEOUT_MS = 8_000;
|
|
25
|
+
|
|
26
|
+
/** @typedef {"pending" | "running" | "done" | "error"} StepState */
|
|
27
|
+
/** @typedef {{ id: string, label: string, detail: string, state: StepState, error?: string }} SetupStep */
|
|
28
|
+
/** @typedef {{ v: 1, state: "running" | "done" | "error", device: string, started_at: number, updated_at: number, steps: SetupStep[], watching_at?: number, handled?: string }} SetupRecord
|
|
29
|
+
* `watching_at`: the last time the process that is waiting for a retry looked (the page offers "Tentar de novo" only
|
|
30
|
+
* while it is recent); `handled`: the id of the last retry request it dealt with */
|
|
31
|
+
|
|
32
|
+
/** every piece of the setup, in the order it runs; what each is for, in the user's words */
|
|
33
|
+
export const SETUP_STEPS = [
|
|
34
|
+
{
|
|
35
|
+
id: "workspace",
|
|
36
|
+
label: "Criando a sua pasta de trabalho",
|
|
37
|
+
detail: "Onde ficam os seus vídeos e as suas configurações, no seu computador.",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "app",
|
|
41
|
+
label: "Instalando o Omni neste computador",
|
|
42
|
+
detail: "Uma cópia que se atualiza sozinha e fica pronta para o Claude usar.",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "video",
|
|
46
|
+
label: "Configuração para montar e exportar o vídeo",
|
|
47
|
+
detail: "Junta as cenas, a voz e as legendas no arquivo final.",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "render",
|
|
51
|
+
label: "Configuração para renderizar as animações",
|
|
52
|
+
detail: "Transforma o motion de cada cena em vídeo.",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "voice",
|
|
56
|
+
label: "Configuração para a voz, o tempo e as legendas",
|
|
57
|
+
detail: "Mede a fala e deixa cada cena e cada legenda no tempo certo.",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "style",
|
|
61
|
+
label: "Configuração para extrair o estilo visual",
|
|
62
|
+
detail: "Lê um vídeo ou um link de referência e aprende o estilo dele.",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: "studio",
|
|
66
|
+
label: "Conectando este computador ao studio",
|
|
67
|
+
detail: "Para o site conversar com o Claude e com os seus arquivos daqui.",
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
/** a fresh record: every step waiting @param {{ device: string, now?: () => number }} o @returns {SetupRecord} */
|
|
72
|
+
export function newSetup({ device, now = Date.now }) {
|
|
73
|
+
const t = now();
|
|
74
|
+
return {
|
|
75
|
+
v: 1,
|
|
76
|
+
state: "running",
|
|
77
|
+
device,
|
|
78
|
+
started_at: t,
|
|
79
|
+
updated_at: t,
|
|
80
|
+
steps: SETUP_STEPS.map(s => ({ ...s, state: "pending" })),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The record after a step changed (pure). The whole setup is done when every step is, and it fails when one
|
|
85
|
+
* did and none is left running. @param {SetupRecord} rec @param {string} id @param {StepState} state
|
|
86
|
+
* @param {string} [error] @param {() => number} [now] @returns {SetupRecord} */
|
|
87
|
+
export function withStep(rec, id, state, error, now = Date.now) {
|
|
88
|
+
const steps = rec.steps.map(s => {
|
|
89
|
+
if (s.id !== id) return s;
|
|
90
|
+
const { error: _old, ...rest } = s;
|
|
91
|
+
return state === "error" ? { ...rest, state, error: String(error ?? "").slice(0, 300) } : { ...rest, state };
|
|
92
|
+
});
|
|
93
|
+
const all = steps.every(s => s.state === "done");
|
|
94
|
+
const failed = steps.some(s => s.state === "error") && !steps.some(s => s.state === "running");
|
|
95
|
+
return { ...rec, steps, state: all ? "done" : failed ? "error" : "running", updated_at: now() };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Keeps a record and tells the cloud (and the file) every time it changes.
|
|
100
|
+
* @param {{
|
|
101
|
+
* studioUrl: string,
|
|
102
|
+
* token: () => Promise<string | null>,
|
|
103
|
+
* device: string,
|
|
104
|
+
* fetch?: typeof fetch,
|
|
105
|
+
* now?: () => number,
|
|
106
|
+
* file?: string,
|
|
107
|
+
* }} o
|
|
108
|
+
*/
|
|
109
|
+
export function createReporter({
|
|
110
|
+
studioUrl,
|
|
111
|
+
token,
|
|
112
|
+
device,
|
|
113
|
+
fetch: doFetch = fetch,
|
|
114
|
+
now = Date.now,
|
|
115
|
+
file = SETUP_FILE,
|
|
116
|
+
}) {
|
|
117
|
+
let record = newSetup({ device, now });
|
|
118
|
+
/* updates go one after the other, so the last one written is the last one the cloud keeps */
|
|
119
|
+
let chain = Promise.resolve();
|
|
120
|
+
|
|
121
|
+
const push = () => {
|
|
122
|
+
const body = JSON.stringify({ setup: record });
|
|
123
|
+
try {
|
|
124
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
125
|
+
writeFileSync(file, `${JSON.stringify(record, null, 2)}\n`);
|
|
126
|
+
} catch {
|
|
127
|
+
/* the cloud copy is the one the page reads */
|
|
128
|
+
}
|
|
129
|
+
chain = chain.then(async () => {
|
|
130
|
+
try {
|
|
131
|
+
const t = await token();
|
|
132
|
+
if (!t) return;
|
|
133
|
+
await doFetch(`${studioUrl}/api/cloud/config`, {
|
|
134
|
+
method: "PUT",
|
|
135
|
+
headers: { authorization: `Bearer ${t}`, "content-type": "application/json" },
|
|
136
|
+
body,
|
|
137
|
+
signal: AbortSignal.timeout(PUT_TIMEOUT_MS),
|
|
138
|
+
});
|
|
139
|
+
} catch {
|
|
140
|
+
/* offline or the studio is down: the next update carries everything */
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
/** the record as it is now */
|
|
147
|
+
get record() {
|
|
148
|
+
return record;
|
|
149
|
+
},
|
|
150
|
+
/** begins again with every step waiting (also tells the cloud) */
|
|
151
|
+
start() {
|
|
152
|
+
record = newSetup({ device, now });
|
|
153
|
+
push();
|
|
154
|
+
},
|
|
155
|
+
/** @param {string} id @param {StepState} state @param {string} [error] */
|
|
156
|
+
step(id, state, error) {
|
|
157
|
+
record = withStep(record, id, state, error, now);
|
|
158
|
+
push();
|
|
159
|
+
},
|
|
160
|
+
/** the process is waiting for a retry request: says so, so the page knows someone is there */
|
|
161
|
+
beat() {
|
|
162
|
+
record = { ...record, watching_at: now(), updated_at: now() };
|
|
163
|
+
push();
|
|
164
|
+
},
|
|
165
|
+
/** the retry request with this id has been dealt with @param {string} id */
|
|
166
|
+
handled(id) {
|
|
167
|
+
record = { ...record, handled: id, updated_at: now() };
|
|
168
|
+
push();
|
|
169
|
+
},
|
|
170
|
+
/** nobody is waiting any more */
|
|
171
|
+
stopWatching() {
|
|
172
|
+
const { watching_at: _gone, ...rest } = record;
|
|
173
|
+
record = { ...rest, updated_at: now() };
|
|
174
|
+
push();
|
|
175
|
+
},
|
|
176
|
+
/** waits for the last update to leave */
|
|
177
|
+
async flush() {
|
|
178
|
+
await chain;
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** @typedef {ReturnType<typeof createReporter>} Reporter */
|
|
184
|
+
|
|
185
|
+
/** a reporter that tells no one (the hook, the tests) @type {Reporter} */
|
|
186
|
+
export const silentReporter = {
|
|
187
|
+
get record() {
|
|
188
|
+
return /** @type {SetupRecord} */ (newSetup({ device: "" }));
|
|
189
|
+
},
|
|
190
|
+
start() {},
|
|
191
|
+
step() {},
|
|
192
|
+
beat() {},
|
|
193
|
+
handled() {},
|
|
194
|
+
stopWatching() {},
|
|
195
|
+
async flush() {},
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/* --- retrying a step from the page ------------------------------------------------------------- */
|
|
199
|
+
|
|
200
|
+
/* the page asks for a retry by writing this key of the user's cloud config (the browser's session may write
|
|
201
|
+
there); the process that ran the setup, still alive after a failure, reads it */
|
|
202
|
+
export const RETRY_KEY = "setup_retry";
|
|
203
|
+
const RETRY_STEPS = new Set([...SETUP_STEPS.map(s => s.id), "all"]);
|
|
204
|
+
|
|
205
|
+
/** A retry request, if it is well formed: an id and a step this setup knows (or "all"). Nothing else the page
|
|
206
|
+
* could write is ever run. @param {unknown} v @returns {{ id: string, step: string } | null} */
|
|
207
|
+
export function parseRetry(v) {
|
|
208
|
+
if (!v || typeof v !== "object") return null;
|
|
209
|
+
const { id, step } = /** @type {Record<string, unknown>} */ (v);
|
|
210
|
+
return typeof id === "string" && id.length > 0 && id.length <= 64 && typeof step === "string" && RETRY_STEPS.has(step)
|
|
211
|
+
? { id, step }
|
|
212
|
+
: null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* After a failed setup, stays alive and does what the page asks: "Tentar de novo" on a step (or on all of them).
|
|
217
|
+
* It looks every few seconds, tells the page it is there (`watching_at`), and gives up after a quiet half hour or
|
|
218
|
+
* once the setup is done. Never throws: a studio that doesn't answer is just a look that found nothing.
|
|
219
|
+
* @param {{
|
|
220
|
+
* studioUrl: string,
|
|
221
|
+
* token: () => Promise<string | null>,
|
|
222
|
+
* reporter: ReturnType<typeof createReporter>,
|
|
223
|
+
* run: (step: string) => Promise<void>,
|
|
224
|
+
* fetch?: typeof fetch,
|
|
225
|
+
* now?: () => number,
|
|
226
|
+
* sleep?: (ms: number) => Promise<void>,
|
|
227
|
+
* pollMs?: number, beatMs?: number, idleMs?: number,
|
|
228
|
+
* }} o
|
|
229
|
+
*/
|
|
230
|
+
export async function watchRetries({
|
|
231
|
+
studioUrl,
|
|
232
|
+
token,
|
|
233
|
+
reporter,
|
|
234
|
+
run,
|
|
235
|
+
fetch: doFetch = fetch,
|
|
236
|
+
now = Date.now,
|
|
237
|
+
sleep = ms => new Promise(r => setTimeout(r, ms)),
|
|
238
|
+
pollMs = 5_000,
|
|
239
|
+
beatMs = 15_000,
|
|
240
|
+
idleMs = 30 * 60_000,
|
|
241
|
+
}) {
|
|
242
|
+
const look = async () => {
|
|
243
|
+
try {
|
|
244
|
+
const t = await token();
|
|
245
|
+
if (!t) return null;
|
|
246
|
+
const r = await doFetch(`${studioUrl}/api/cloud/config`, {
|
|
247
|
+
headers: { authorization: `Bearer ${t}` },
|
|
248
|
+
signal: AbortSignal.timeout(PUT_TIMEOUT_MS),
|
|
249
|
+
});
|
|
250
|
+
return r.ok ? parseRetry(/** @type {any} */ (await r.json()).config?.[RETRY_KEY]) : null;
|
|
251
|
+
} catch {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
let seen = reporter.record.handled ?? "";
|
|
256
|
+
let lastBeat = now();
|
|
257
|
+
let lastWork = now();
|
|
258
|
+
reporter.beat();
|
|
259
|
+
while (reporter.record.state === "error" && now() - lastWork < idleMs) {
|
|
260
|
+
await sleep(pollMs);
|
|
261
|
+
const req = await look();
|
|
262
|
+
if (req && req.id !== seen) {
|
|
263
|
+
seen = req.id;
|
|
264
|
+
try {
|
|
265
|
+
await run(req.step);
|
|
266
|
+
} catch {
|
|
267
|
+
/* the step reports its own failure */
|
|
268
|
+
}
|
|
269
|
+
reporter.handled(req.id);
|
|
270
|
+
lastWork = lastBeat = now();
|
|
271
|
+
} else if (now() - lastBeat >= beatMs) {
|
|
272
|
+
reporter.beat();
|
|
273
|
+
lastBeat = now();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
reporter.stopWatching();
|
|
277
|
+
await reporter.flush();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/* --- the lock: one setup at a time --------------------------------------------------------- */
|
|
281
|
+
|
|
282
|
+
const alive = (/** @type {number} */ pid) => {
|
|
283
|
+
try {
|
|
284
|
+
process.kill(pid, 0);
|
|
285
|
+
return true;
|
|
286
|
+
} catch (e) {
|
|
287
|
+
/* EPERM: it exists and isn't ours */
|
|
288
|
+
return /** @type {NodeJS.ErrnoException} */ (e).code === "EPERM";
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
/** whether a setup is running now: the lock names a live process and is not too old @param {string} [lock] */
|
|
293
|
+
export function setupRunning(lock = LOCK_FILE) {
|
|
294
|
+
try {
|
|
295
|
+
const pid = Number(readFileSync(lock, "utf8"));
|
|
296
|
+
return Number.isInteger(pid) && pid > 0 && alive(pid) && Date.now() - statSync(lock).mtimeMs < LOCK_STALE_MS;
|
|
297
|
+
} catch {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Takes the lock for this process, or returns null when another setup holds it.
|
|
303
|
+
* @param {string} [lock] @returns {(() => void) | null} the release */
|
|
304
|
+
export function acquireSetupLock(lock = LOCK_FILE) {
|
|
305
|
+
if (setupRunning(lock)) return null;
|
|
306
|
+
mkdirSync(dirname(lock), { recursive: true });
|
|
307
|
+
writeFileSync(lock, String(process.pid));
|
|
308
|
+
return () => {
|
|
309
|
+
try {
|
|
310
|
+
if (Number(readFileSync(lock, "utf8")) === process.pid) rmSync(lock, { force: true });
|
|
311
|
+
} catch {
|
|
312
|
+
/* already gone */
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
}
|