@hypit/hypit 0.1.13 → 0.1.14
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/README.md +36 -2
- package/bin/hypit.mjs +0 -2
- package/package.json +2 -1
- package/packages/cli/package.json +1 -0
- package/packages/cli/src/commands/environment.ts +12 -7
- package/packages/cli/src/main.ts +2 -1
- package/packages/cli/src/oauth.ts +50 -7
- package/packages/credential-store-file/README.md +60 -0
- package/packages/credential-store-file/package.json +21 -0
- package/packages/credential-store-file/src/activation.ts +29 -0
- package/packages/credential-store-file/src/index.ts +1 -0
- package/packages/credential-store-file/src/store.ts +89 -0
- package/packages/fonts-open/src/surface.ts +8 -2
- package/packages/hyperframes/README.md +16 -2
- package/packages/hyperframes/src/browser-program.ts +6 -1
- package/packages/hyperframes/src/document.ts +31 -21
- package/packages/hyperframes/src/project.ts +11 -20
- package/packages/media-execution/README.md +7 -0
- package/packages/media-execution/src/execute.ts +5 -5
- package/packages/media-execution/src/index.ts +1 -1
- package/packages/media-execution/src/process-env.ts +20 -0
- package/packages/media-execution/src/surface.ts +83 -76
- package/packages/package-loader-node/README.md +10 -0
- package/packages/package-loader-node/src/index.ts +1 -0
- package/packages/package-loader-node/src/loader.ts +25 -5
- package/packages/package-loader-node/src/location.ts +11 -2
- package/packages/provider-hyperframes-local/README.md +81 -8
- package/packages/provider-hyperframes-local/package.json +13 -3
- package/packages/provider-hyperframes-local/src/activation.ts +20 -6
- package/packages/provider-hyperframes-local/src/browser-install.ts +5 -0
- package/packages/provider-hyperframes-local/src/browser.ts +118 -0
- package/packages/provider-hyperframes-local/src/capture-bootstrap.ts +4 -6
- package/packages/provider-hyperframes-local/src/capture-exit.ts +24 -0
- package/packages/provider-hyperframes-local/src/capture-process.ts +29 -52
- package/packages/provider-hyperframes-local/src/capture-worker.ts +2 -0
- package/packages/provider-hyperframes-local/src/capture.ts +12 -5
- package/packages/provider-hyperframes-local/src/opaque-capture.ts +46 -18
- package/packages/provider-hyperframes-local/src/options.ts +2 -2
- package/packages/provider-hyperframes-local/src/process-tree.ts +82 -0
- package/packages/provider-hyperframes-local/src/process.ts +22 -0
- package/packages/provider-hyperframes-local/src/program.ts +25 -33
- package/packages/provider-hyperframes-local/src/provider.ts +4 -7
- package/packages/provider-hyperframes-local/src/render.ts +9 -4
- package/packages/runtime-host-node/README.md +5 -0
- package/packages/runtime-host-node/src/index.ts +7 -2
- package/packages/runtime-host-node/src/packages.ts +13 -7
- package/packages/runtime-local/README.md +10 -0
- package/packages/runtime-local/src/config.ts +1 -1
- package/packages/runtime-local/src/credentials.ts +10 -5
- package/packages/studio/src/preview/runtime-shim.ts +17 -3
- package/packages/video-cli/README.md +5 -1
- package/packages/video-cli/package.json +1 -0
- package/packages/video-cli/src/distribution.ts +3 -10
- package/packages/video-cli/src/version.ts +1 -1
- package/packages/yt-dlp/README.md +3 -2
- package/packages/yt-dlp/package.json +4 -0
- package/packages/yt-dlp/src/download.ts +10 -14
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { access, stat } from "node:fs/promises";
|
|
3
|
+
import { constants } from "node:fs";
|
|
4
|
+
import { delimiter, isAbsolute, resolve } from "node:path";
|
|
2
5
|
|
|
3
6
|
type ProcessResult = {
|
|
4
7
|
readonly stdout: Uint8Array;
|
|
@@ -13,6 +16,25 @@ export function positiveInteger(value: number, subject: string): number {
|
|
|
13
16
|
return value;
|
|
14
17
|
}
|
|
15
18
|
|
|
19
|
+
/** The engine's binary override requires a path; resolve our selected command without its fallback search. */
|
|
20
|
+
export async function mediaExecutablePath(value: string): Promise<string> {
|
|
21
|
+
const pathLike = isAbsolute(value) || value.includes("/") || value.includes("\\");
|
|
22
|
+
const bases = pathLike ? [resolve(value)]
|
|
23
|
+
: (process.env.PATH ?? "").split(delimiter).filter(Boolean).map(directory => resolve(directory, value));
|
|
24
|
+
const extensions = process.platform === "win32" && !/\.[^\\/]+$/u.test(value) ? [".exe", ".com", ""] : [""];
|
|
25
|
+
for (const base of bases) for (const extension of extensions) {
|
|
26
|
+
const candidate = `${base}${extension}`;
|
|
27
|
+
try {
|
|
28
|
+
if (!(await stat(candidate)).isFile()) continue;
|
|
29
|
+
await access(candidate, process.platform === "win32" ? constants.F_OK : constants.X_OK);
|
|
30
|
+
return candidate;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (!["ENOENT", "ENOTDIR", "EACCES"].includes((error as NodeJS.ErrnoException).code ?? "")) throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
throw new Error(`HyperFrames media executable ${value} is unavailable; correct the Provider's ffmpegPath or ffprobePath.`);
|
|
36
|
+
}
|
|
37
|
+
|
|
16
38
|
function processEnvironment(): NodeJS.ProcessEnv {
|
|
17
39
|
const names = ["PATH", "HOME", "TMPDIR", "LANG", "LC_ALL"] as const;
|
|
18
40
|
return Object.fromEntries(names.flatMap((name) => process.env[name] === undefined
|
|
@@ -1,54 +1,46 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { join } from "node:path";
|
|
2
3
|
|
|
3
4
|
import { probeMediaToolchain } from "@hypit/media-execution";
|
|
4
5
|
import type { ManagedProgram, ManagedProgramState } from "@hypit/runtime-kit";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
return new Promise((resolve) => {
|
|
8
|
-
execFile(executable, [...args], { timeout: 15_000, shell: false, windowsHide: true }, (error, stdout, stderr) => {
|
|
9
|
-
resolve(error === null
|
|
10
|
-
? { ok: true, output: stdout.trim() }
|
|
11
|
-
: { ok: false, output: (stderr.trim() || error.message).split("\n").at(-1) ?? "" });
|
|
12
|
-
});
|
|
13
|
-
});
|
|
14
|
-
}
|
|
6
|
+
import { browserCacheDirectory, browserDownloadBaseUrl, browserDownloadUrl, browserExecutablePath, configuredBrowserPath, requireBrowserExecutable, selectedBrowserVersion } from "./browser.js";
|
|
7
|
+
import type { BrowserOptions } from "./browser.js";
|
|
15
8
|
|
|
16
9
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* when this Provider is selected.
|
|
10
|
+
* This Provider owns browser selection and preparation. Probes never install;
|
|
11
|
+
* rendering receives the same selected executable instead of invoking engine discovery.
|
|
20
12
|
*/
|
|
21
13
|
export function localHyperframesBrowserProgram(
|
|
22
|
-
input: {
|
|
14
|
+
input: BrowserOptions & {
|
|
23
15
|
readonly id: string;
|
|
24
16
|
readonly nodePath: string;
|
|
25
|
-
readonly hyperframesCliPath: string | (() => string);
|
|
26
17
|
readonly ffprobePath: string;
|
|
27
18
|
readonly ffmpegPath?: string;
|
|
28
19
|
},
|
|
29
20
|
): ManagedProgram {
|
|
30
|
-
const
|
|
21
|
+
const version = selectedBrowserVersion(input);
|
|
22
|
+
const selectedPath = browserExecutablePath(input);
|
|
23
|
+
const baseUrl = browserDownloadBaseUrl(input);
|
|
31
24
|
const probeBrowser = async (): Promise<ManagedProgramState> => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const located = await run(input.nodePath, [executable, "browser", "path"]);
|
|
35
|
-
if (!located.ok) return { state: "down", detail: `HyperFrames browser is unavailable: ${located.output}` };
|
|
36
|
-
const path = located.output.trim();
|
|
37
|
-
if (path.length === 0 || path.includes("\n") || path.includes("\r")) {
|
|
38
|
-
return { state: "mismatch", detail: "HyperFrames returned an invalid browser path" };
|
|
39
|
-
}
|
|
40
|
-
const version = await run(path, ["--version"]);
|
|
41
|
-
if (!version.ok || version.output.length === 0) {
|
|
42
|
-
return { state: "mismatch", detail: `HyperFrames browser cannot start: ${version.output}` };
|
|
43
|
-
}
|
|
25
|
+
try { await requireBrowserExecutable(selectedPath, version); }
|
|
26
|
+
catch (error) { return { state: "down", detail: error instanceof Error ? error.message : String(error) }; }
|
|
44
27
|
return { state: "ready" };
|
|
45
28
|
};
|
|
46
29
|
return {
|
|
47
30
|
id: input.id,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
31
|
+
...(configuredBrowserPath(input) === undefined ? {
|
|
32
|
+
// Projects sharing this installation also share the existing Program lifecycle lock/logs.
|
|
33
|
+
stateRoot: join(browserCacheDirectory(input), ".hypit-render-program"),
|
|
34
|
+
installation: {
|
|
35
|
+
probe: probeBrowser,
|
|
36
|
+
commands: [{ label: `Install Chrome Headless Shell ${version} at ${selectedPath} from ${browserDownloadUrl(input)}`, command: input.nodePath, args: [
|
|
37
|
+
"--import", import.meta.resolve("tsx"),
|
|
38
|
+
"--import", new URL("./capture-bootstrap.ts", import.meta.url).href,
|
|
39
|
+
fileURLToPath(new URL("./browser-install.ts", import.meta.url)), browserCacheDirectory(input), version!,
|
|
40
|
+
...(baseUrl === undefined ? [] : [baseUrl]),
|
|
41
|
+
] }],
|
|
42
|
+
},
|
|
43
|
+
} : {}),
|
|
52
44
|
async probe(): Promise<ManagedProgramState> {
|
|
53
45
|
const browser = await probeBrowser();
|
|
54
46
|
if (browser.state !== "ready") return browser;
|
|
@@ -2,7 +2,6 @@ import { defineEndpointPackage } from "@hypit/endpoint-kit";
|
|
|
2
2
|
import { mediaTypes } from "@hypit/media";
|
|
3
3
|
import { renderHyperframesCapabilities, verifyHyperframesVisualRequest } from "@hypit/render-hyperframes";
|
|
4
4
|
import { canonicalize } from "@hypit/protocol";
|
|
5
|
-
import { resolveNodePackageExecutable } from "@hypit/package-loader-node";
|
|
6
5
|
import { renderHyperframesVisual, resolveExecutionOptions, renderWorkerLimit } from "./render.js";
|
|
7
6
|
import type { HyperframesExecutionOptions } from "./options.js";
|
|
8
7
|
import { renderProgressReporter } from "./progress.js";
|
|
@@ -14,17 +13,12 @@ export type CreateLocalHyperframesProviderOptions = HyperframesExecutionOptions
|
|
|
14
13
|
readonly pool?: string;
|
|
15
14
|
/** Used by managed browser installation, not frame capture. */
|
|
16
15
|
readonly nodePath?: string;
|
|
17
|
-
readonly hyperframesCliPath?: string;
|
|
18
16
|
/** Whole render requests admitted concurrently; independent of frame workers. */
|
|
19
17
|
readonly defaultConcurrency?: number;
|
|
20
18
|
/** Shared Chrome slots across Need executions using this pool. */
|
|
21
19
|
readonly browserCapacity?: number;
|
|
22
20
|
};
|
|
23
21
|
|
|
24
|
-
export function defaultHyperframesCliPath(): string {
|
|
25
|
-
return resolveNodePackageExecutable("hyperframes", "hyperframes", { from: import.meta.url });
|
|
26
|
-
}
|
|
27
|
-
|
|
28
22
|
export function createLocalHyperframesProvider(config: CreateLocalHyperframesProviderOptions) {
|
|
29
23
|
const execution = resolveExecutionOptions(config);
|
|
30
24
|
const pool = config.pool ?? config.instance ?? "hyperframes.local";
|
|
@@ -62,7 +56,10 @@ export function createLocalHyperframesProvider(config: CreateLocalHyperframesPro
|
|
|
62
56
|
: request.range.endFrameExclusive - request.range.startFrame;
|
|
63
57
|
const progress = renderProgressReporter(context.reportProgress, frameCount);
|
|
64
58
|
try {
|
|
65
|
-
const visual = await renderHyperframesVisual(request, { ...config,
|
|
59
|
+
const visual = await renderHyperframesVisual(request, { ...config,
|
|
60
|
+
...(execution.chromePath === undefined ? { browserVersion: execution.browserVersion! } : { chromePath: execution.chromePath }),
|
|
61
|
+
browserCacheDirectory: execution.browserCacheDirectory,
|
|
62
|
+
workers: execution.workers, maxWorkers,
|
|
66
63
|
resources: context.resources, onProgress: progress.onProgress,
|
|
67
64
|
...(context.reportDiagnostic === undefined ? {} : { onDiagnostic: context.reportDiagnostic }) });
|
|
68
65
|
return { value: { kind: "inline", value: canonicalize(visual) } };
|
|
@@ -6,7 +6,7 @@ import type { EndpointInvocationContext } from "@hypit/endpoint-kit";
|
|
|
6
6
|
import { stageHyperframesProject } from "@hypit/hyperframes/project";
|
|
7
7
|
import { sealRenderedVisual } from "@hypit/media";
|
|
8
8
|
import type { MediaFrameRange, RenderedVisual } from "@hypit/media";
|
|
9
|
-
import {
|
|
9
|
+
import { verifyCompositableSurfaceFile } from "@hypit/media-execution";
|
|
10
10
|
import { verifyHyperframesVisualRequest } from "@hypit/render-hyperframes";
|
|
11
11
|
import type { HyperframesVisualRequest } from "@hypit/render-hyperframes";
|
|
12
12
|
import { isStreamingResourceStore } from "@hypit/runtime";
|
|
@@ -15,6 +15,7 @@ import { assert, positiveInteger } from "./process.js";
|
|
|
15
15
|
import { runCaptureProcess } from "./capture-process.js";
|
|
16
16
|
import { finished } from "node:stream/promises";
|
|
17
17
|
import { autoWorkerLimit } from "./concurrency.js";
|
|
18
|
+
import { browserCacheDirectory, browserExecutablePath, configuredBrowserPath, requireBrowserExecutable, selectedBrowserVersion } from "./browser.js";
|
|
18
19
|
|
|
19
20
|
export type HyperframesRenderProgress =
|
|
20
21
|
| { readonly phase: "staging" | "encoding" | "storing"; readonly elapsedMs: number }
|
|
@@ -43,6 +44,9 @@ export function resolveExecutionOptions(options: HyperframesExecutionOptions) {
|
|
|
43
44
|
const browserGpu = options.browserGpu ?? "hardware";
|
|
44
45
|
assert(["auto", "software", "hardware"].includes(browserGpu), "HyperFrames browserGpu is invalid");
|
|
45
46
|
return {
|
|
47
|
+
chromePath: configuredBrowserPath(options),
|
|
48
|
+
browserVersion: selectedBrowserVersion(options),
|
|
49
|
+
browserCacheDirectory: browserCacheDirectory(options),
|
|
46
50
|
workers,
|
|
47
51
|
maxWorkers: workers === "auto" ? positiveInteger(options.maxWorkers ?? autoWorkerLimit(), "maxWorkers") : workers,
|
|
48
52
|
quality, browserGpu,
|
|
@@ -71,7 +75,7 @@ export async function renderHyperframesVisual(
|
|
|
71
75
|
options: RenderHyperframesVisualOptions,
|
|
72
76
|
): Promise<RenderedVisual> {
|
|
73
77
|
verifyHyperframesVisualRequest(request);
|
|
74
|
-
const config = resolveExecutionOptions(options);
|
|
78
|
+
const config = { ...resolveExecutionOptions(options), chromePath: browserExecutablePath(options) };
|
|
75
79
|
const { document } = request;
|
|
76
80
|
const range = request.range ?? { startFrame: 0, endFrameExclusive: document.frameCount };
|
|
77
81
|
const frameCount = range.endFrameExclusive - range.startFrame;
|
|
@@ -97,8 +101,9 @@ export async function renderHyperframesVisual(
|
|
|
97
101
|
};
|
|
98
102
|
try {
|
|
99
103
|
signal.throwIfAborted();
|
|
104
|
+
await requireBrowserExecutable(config.chromePath, config.browserVersion);
|
|
100
105
|
await options.onDiagnostic?.({ level: "info", message:
|
|
101
|
-
`Render ${frameCount} frames; workers ${config.workers} (limit ${renderWorkerLimit(config, frameCount, document.frameRate.numerator / document.frameRate.denominator)}); opaque fast PNG; quality ${config.quality}; GPU ${config.browserGpu}; encoder ${config.ffmpegPath}` });
|
|
106
|
+
`Render ${frameCount} frames; workers ${config.workers} (limit ${renderWorkerLimit(config, frameCount, document.frameRate.numerator / document.frameRate.denominator)}); opaque fast PNG; quality ${config.quality}; GPU ${config.browserGpu}; browser ${config.chromePath}; encoder ${config.ffmpegPath}` });
|
|
102
107
|
options.onProgress?.({ phase: "staging", elapsedMs: 0 });
|
|
103
108
|
work = await mkdtemp(join(tmpdir(), "hypit-hyperframes-local-"));
|
|
104
109
|
await stageHyperframesProject({ document, directory: work, signal,
|
|
@@ -109,7 +114,7 @@ export async function renderHyperframesVisual(
|
|
|
109
114
|
assert(bytes !== undefined, `HyperFrames Artifact ${artifact.resource} is unavailable`);
|
|
110
115
|
return bytes;
|
|
111
116
|
},
|
|
112
|
-
validateSurface: (surface,
|
|
117
|
+
validateSurface: (surface, path, probeSignal) => verifyCompositableSurfaceFile({ surface, path,
|
|
113
118
|
ffprobePath: config.ffprobePath, processTimeoutMs: config.processTimeoutMs,
|
|
114
119
|
maxProbeOutputBytes: config.maxProcessOutputBytes, signal: probeSignal! }),
|
|
115
120
|
});
|
|
@@ -53,3 +53,8 @@ to start execution, then `const runtime = await host.createRuntime(); await runt
|
|
|
53
53
|
to submit work. `RuntimeHostExecution` does not run a second embedded Worker loop. A local carrier
|
|
54
54
|
may stop accepting new Builds and drain; its assigned Builds stay in place and all carriers continue
|
|
55
55
|
using the same resource accounting. Process policy belongs to the local Runtime, not this Host ABI.
|
|
56
|
+
|
|
57
|
+
`prepareHostPackages` accepts exact specifier strings or `{ specifier, env }` installation inputs.
|
|
58
|
+
The optional environment is applied only to that npm child process, merged over its inherited
|
|
59
|
+
environment. It is neither persisted as installation status nor returned in package reports.
|
|
60
|
+
The shared installer does not interpret SDK-specific variables; their owning packages supply them.
|
|
@@ -70,17 +70,20 @@ export type BuildView = {
|
|
|
70
70
|
}[];
|
|
71
71
|
};
|
|
72
72
|
|
|
73
|
-
export type
|
|
73
|
+
export type RuntimeHostCredentialDescription = {
|
|
74
74
|
readonly endpoint: string;
|
|
75
75
|
readonly slot: string;
|
|
76
76
|
readonly label: string;
|
|
77
77
|
readonly kind: "secret" | "json";
|
|
78
78
|
readonly ref: CredentialRef;
|
|
79
79
|
readonly acquisition?: CredentialAcquisition;
|
|
80
|
-
readonly configured: boolean;
|
|
81
80
|
readonly writable: boolean;
|
|
82
81
|
};
|
|
83
82
|
|
|
83
|
+
export type RuntimeHostCredentialStatus = RuntimeHostCredentialDescription & {
|
|
84
|
+
readonly configured: boolean;
|
|
85
|
+
};
|
|
86
|
+
|
|
84
87
|
export type RuntimeHostControl = {
|
|
85
88
|
/** Build evidence while active. Finished evidence is read through its Result Repository. */
|
|
86
89
|
logs?(build: string, lines: number): Promise<import("@hypit/runtime").ExecutionLogView | undefined>;
|
|
@@ -105,6 +108,8 @@ export type RuntimeHostResultControl = {
|
|
|
105
108
|
};
|
|
106
109
|
|
|
107
110
|
export type RuntimeHostCredentialControl = {
|
|
111
|
+
/** Endpoint declarations and Store write capability; never reads an existing secret. */
|
|
112
|
+
describeCredentials(endpoint?: string): Promise<readonly RuntimeHostCredentialDescription[]>;
|
|
108
113
|
credentials(endpoint?: string): Promise<readonly RuntimeHostCredentialStatus[]>;
|
|
109
114
|
putCredential(endpoint: string, slot: string, secret: string): Promise<RuntimeHostCredentialStatus>;
|
|
110
115
|
deleteCredential(endpoint: string, slot: string): Promise<{
|
|
@@ -73,7 +73,7 @@ export async function inspectHostPackage(
|
|
|
73
73
|
: undefined;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
async function runNpm(root: string, specifiers: readonly string[], logPath: string): Promise<void> {
|
|
76
|
+
async function runNpm(root: string, specifiers: readonly string[], logPath: string, env?: Readonly<Record<string, string>>): Promise<void> {
|
|
77
77
|
// npm's prefix and cwd must denote the same physical project (not /tmp vs /private/tmp).
|
|
78
78
|
const cwd = await realpath(root);
|
|
79
79
|
const npmArgs = [
|
|
@@ -96,6 +96,7 @@ async function runNpm(root: string, specifiers: readonly string[], logPath: stri
|
|
|
96
96
|
cwd,
|
|
97
97
|
shell: false,
|
|
98
98
|
windowsHide: true,
|
|
99
|
+
env: { ...process.env, ...env },
|
|
99
100
|
stdio: ["ignore", log.fd, log.fd],
|
|
100
101
|
});
|
|
101
102
|
child.on("error", (error) => reject(new Error(`Cannot start npm: ${error.message}. Log: ${logPath}`, { cause: error })));
|
|
@@ -114,21 +115,26 @@ async function runNpm(root: string, specifiers: readonly string[], logPath: stri
|
|
|
114
115
|
* package.json, lockfile and dependencies; Hypit keeps no parallel inventory.
|
|
115
116
|
*/
|
|
116
117
|
export async function prepareHostPackages(
|
|
117
|
-
specifiers: readonly string[],
|
|
118
|
+
specifiers: readonly (string | { readonly specifier: string; readonly env?: Readonly<Record<string, string>> })[],
|
|
118
119
|
options: {
|
|
119
120
|
readonly root: string;
|
|
120
121
|
readonly onProgress?: (event: HostPackageProgress) => void;
|
|
121
122
|
},
|
|
122
123
|
): Promise<readonly HostPackageReport[]> {
|
|
123
124
|
const root = resolve(options.root);
|
|
124
|
-
const bySpecifier = new Map<string, RegistryPackageSpec>();
|
|
125
|
+
const bySpecifier = new Map<string, RegistryPackageSpec & { readonly env?: Readonly<Record<string, string>> }>();
|
|
125
126
|
for (const specifier of specifiers) {
|
|
126
|
-
const parsed = parseRegistryPackageSpec(specifier);
|
|
127
|
-
bySpecifier.
|
|
127
|
+
const parsed = parseRegistryPackageSpec(typeof specifier === "string" ? specifier : specifier.specifier);
|
|
128
|
+
const env = { ...bySpecifier.get(parsed.specifier)?.env };
|
|
129
|
+
for (const [key, value] of Object.entries(typeof specifier === "string" ? {} : specifier.env ?? {})) {
|
|
130
|
+
if (env[key] !== undefined && env[key] !== value) throw new Error(`Conflicting installation environment ${key} for ${parsed.specifier}`);
|
|
131
|
+
env[key] = value;
|
|
132
|
+
}
|
|
133
|
+
bySpecifier.set(parsed.specifier, { ...parsed, ...(Object.keys(env).length === 0 ? {} : { env }) });
|
|
128
134
|
}
|
|
129
135
|
const required = [...bySpecifier.values()].sort((left, right) => left.specifier.localeCompare(right.specifier));
|
|
130
136
|
const reports: HostPackageReport[] = [];
|
|
131
|
-
for (const item of required) {
|
|
137
|
+
for (const { env, ...item } of required) {
|
|
132
138
|
const installation = externalPackageInstallRoot(root, item.name, item.version);
|
|
133
139
|
options.onProgress?.({ ...item, phase: "checking" });
|
|
134
140
|
const missing = await installedVersion(installation, item.name) !== item.version;
|
|
@@ -142,7 +148,7 @@ export async function prepareHostPackages(
|
|
|
142
148
|
dependencies: { [item.name]: item.version },
|
|
143
149
|
}, null, 2)}\n`, "utf8");
|
|
144
150
|
options.onProgress?.({ ...item, phase: "installing", logPath });
|
|
145
|
-
await runNpm(installation, [item.specifier], logPath);
|
|
151
|
+
await runNpm(installation, [item.specifier], logPath, env);
|
|
146
152
|
}
|
|
147
153
|
const version = await installedVersion(installation, item.name);
|
|
148
154
|
if (version !== item.version) {
|
|
@@ -258,3 +258,13 @@ capacity release and every completed Result. They use the production execution p
|
|
|
258
258
|
This is a load experiment rather than part of every package regression. It reports fixture preparation,
|
|
259
259
|
local progress, completion, executor RSS and cleanup separately; total test time also includes creating
|
|
260
260
|
and removing the temporary Result repositories. Neither suite uses a completion-time performance target.
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
### Credential management without reading the old value
|
|
264
|
+
|
|
265
|
+
`openCredentials(endpoint)` opens only the selected Endpoint's credential control.
|
|
266
|
+
`describeCredentials(endpoint)` returns its declared slots and each Store's write capability without
|
|
267
|
+
resolving secrets. `credentials(endpoint)` also reads current values to report status and propagates
|
|
268
|
+
read failures. Login and logout use the former: a damaged old credential cannot prevent replacement
|
|
269
|
+
or deletion. Successful writes and deletions report their operation's result without rereading the
|
|
270
|
+
secret. Execution still resolves credentials normally and reports Store errors.
|
|
@@ -462,7 +462,7 @@ export async function prepareRuntimeConfigPackages(
|
|
|
462
462
|
...document.endpoints.map((item) => item.use),
|
|
463
463
|
...credentials.credentials.map((item) => item.use),
|
|
464
464
|
], options.distributionPackageRoot);
|
|
465
|
-
return await prepareHostPackages(requirements
|
|
465
|
+
return await prepareHostPackages(requirements, {
|
|
466
466
|
root: hypitHostPackageRoot(options.hostStateRoot),
|
|
467
467
|
...(options.onProgress === undefined ? {} : { onProgress: options.onProgress }),
|
|
468
468
|
});
|
|
@@ -28,16 +28,21 @@ export function createLocalCredentialControl(
|
|
|
28
28
|
: `Endpoint ${endpoint} repeats credential slot ${slot}`);
|
|
29
29
|
return matches[0]!;
|
|
30
30
|
};
|
|
31
|
-
const
|
|
31
|
+
const describe = async (item: typeof descriptions[number]) => ({
|
|
32
32
|
...structuredClone(item),
|
|
33
|
-
configured: await options.credentialStore.resolve(item.ref) !== undefined,
|
|
34
33
|
writable: await writableCredentialStore(options.credentialStore, item.ref) !== undefined,
|
|
35
34
|
});
|
|
36
35
|
|
|
37
36
|
return {
|
|
37
|
+
async describeCredentials(endpoint) {
|
|
38
|
+
return await Promise.all(descriptions.filter((item) => endpoint === undefined || item.endpoint === endpoint).map(describe));
|
|
39
|
+
},
|
|
38
40
|
async credentials(endpoint) {
|
|
39
41
|
const selected = descriptions.filter((item) => endpoint === undefined || item.endpoint === endpoint);
|
|
40
|
-
return await Promise.all(selected.map(
|
|
42
|
+
return await Promise.all(selected.map(async (item) => ({
|
|
43
|
+
...await describe(item),
|
|
44
|
+
configured: await options.credentialStore.resolve(item.ref) !== undefined,
|
|
45
|
+
})));
|
|
41
46
|
},
|
|
42
47
|
async putCredential(endpoint, slot, secret) {
|
|
43
48
|
assert(secret.length > 0, "credential secret is empty");
|
|
@@ -45,14 +50,14 @@ export function createLocalCredentialControl(
|
|
|
45
50
|
const store = await writableCredentialStore(options.credentialStore, item.ref);
|
|
46
51
|
assert(store !== undefined, `CredentialStore ${item.ref.store} is not writable`);
|
|
47
52
|
await store.put(item.ref, { secret });
|
|
48
|
-
return
|
|
53
|
+
return { ...structuredClone(item), writable: true, configured: true };
|
|
49
54
|
},
|
|
50
55
|
async deleteCredential(endpoint, slot) {
|
|
51
56
|
const item = credential(endpoint, slot);
|
|
52
57
|
const store = await writableCredentialStore(options.credentialStore, item.ref);
|
|
53
58
|
assert(store !== undefined, `CredentialStore ${item.ref.store} is not writable`);
|
|
54
59
|
const deleted = await store.delete(item.ref);
|
|
55
|
-
return { deleted, credential:
|
|
60
|
+
return { deleted, credential: { ...structuredClone(item), writable: true, configured: false } };
|
|
56
61
|
},
|
|
57
62
|
close() {
|
|
58
63
|
return options.close?.();
|
|
@@ -47,7 +47,12 @@ function shim(): string {
|
|
|
47
47
|
start: parseFloat(ownStart || (present && present.getAttribute('data-start')) || '0') || 0,
|
|
48
48
|
duration: parseFloat(ownDuration || (present && present.getAttribute('data-duration')) || '0') || 0,
|
|
49
49
|
mediaStart: parseFloat(element.getAttribute('data-media-start') || '0') || 0,
|
|
50
|
-
rate: parseFloat(element.getAttribute('data-playback-rate') || '1') || 1
|
|
50
|
+
rate: parseFloat(element.getAttribute('data-playback-rate') || '1') || 1,
|
|
51
|
+
startFrame: Number(element.getAttribute('data-hypit-start-frame')),
|
|
52
|
+
endFrame: Number(element.getAttribute('data-hypit-end-frame')),
|
|
53
|
+
sourceFrame: element.getAttribute('data-hypit-source-frame').split('/').map(BigInt),
|
|
54
|
+
sourceRate: element.getAttribute('data-hypit-source-rate').split('/').map(BigInt),
|
|
55
|
+
sourceFps: element.getAttribute('data-hypit-source-fps').split('/').map(Number)
|
|
51
56
|
});
|
|
52
57
|
}
|
|
53
58
|
var audioContext;
|
|
@@ -149,7 +154,8 @@ function shim(): string {
|
|
|
149
154
|
}
|
|
150
155
|
for (var record of media) {
|
|
151
156
|
var local = currentSeconds - record.start;
|
|
152
|
-
var
|
|
157
|
+
var programFrame = Math.round(currentSeconds * fps);
|
|
158
|
+
var inside = programFrame >= record.startFrame && programFrame < record.endFrame;
|
|
153
159
|
var element = record.element;
|
|
154
160
|
element.style.visibility = inside ? 'visible' : 'hidden';
|
|
155
161
|
// Normalized picture Artifacts are deliberately silent. Programme sound
|
|
@@ -157,10 +163,18 @@ function shim(): string {
|
|
|
157
163
|
element.muted = true;
|
|
158
164
|
if (!inside) { element.pause(); continue; }
|
|
159
165
|
var target = record.mediaStart + (local + frameSeconds / 2) * record.rate;
|
|
166
|
+
var discrete = scrubbing || programFrame === record.endFrame - 1;
|
|
167
|
+
if (discrete) {
|
|
168
|
+
// The renderer floors exact source-frame coordinates. The midpoint of a
|
|
169
|
+
// programme frame can cross that source boundary at fractional speeds.
|
|
170
|
+
var a = record.sourceFrame, r = record.sourceRate;
|
|
171
|
+
var sourceFrame = Number((a[0] * r[1] + BigInt(programFrame - record.startFrame) * r[0] * a[1]) / (a[1] * r[1]));
|
|
172
|
+
target = (sourceFrame + 0.5) * record.sourceFps[1] / record.sourceFps[0];
|
|
173
|
+
}
|
|
160
174
|
if (Number.isFinite(element.duration) && element.duration > 0) {
|
|
161
175
|
target = Math.min(target, Math.max(0, element.duration - 0.001));
|
|
162
176
|
}
|
|
163
|
-
if (
|
|
177
|
+
if (discrete) {
|
|
164
178
|
element.pause();
|
|
165
179
|
waits.push(seekDecoded(element, target));
|
|
166
180
|
} else {
|
|
@@ -34,7 +34,11 @@ an editable starter; its Endpoint entries describe available routes, not choices
|
|
|
34
34
|
Keep an existing chosen service, or configure the chosen local or hosted Provider and its capability
|
|
35
35
|
bindings. HypiHub is the recommended integrated hosted route in the official Distribution; other
|
|
36
36
|
services use project Provider packages. If the user chooses HypiHub,
|
|
37
|
-
`hypit auth login hypihub.default` connects that account.
|
|
37
|
+
`hypit auth login hypihub.default` connects that account after choosing its CredentialStore.
|
|
38
|
+
The starter selects the OS store for macOS Keychain / Windows Credential Locker. On Linux, or when
|
|
39
|
+
explicitly choosing file storage, edit the Profile's `credentials` and Endpoint reference as shown
|
|
40
|
+
in [file CredentialStore](../credential-store-file/README.md#select-it-before-login) before `auth` or
|
|
41
|
+
`runtime up`. This selection is configuration; execution never switches stores automatically.
|
|
38
42
|
`hypit doctor --endpoint <name>` checks a selected Endpoint;
|
|
39
43
|
`hypit runtime up --endpoint <name>` prepares that Endpoint and starts the Worker. Repeat the flag
|
|
40
44
|
for several chosen Endpoints; omitting it prepares the whole Profile. `hypit programs up --endpoint
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"@hypit/cli": "workspace:*",
|
|
14
14
|
"@hypit/compiler-markup-node": "workspace:*",
|
|
15
15
|
"@hypit/composition": "workspace:*",
|
|
16
|
+
"@hypit/credential-store-file": "workspace:*",
|
|
16
17
|
"@hypit/credential-store-os": "workspace:*",
|
|
17
18
|
"@hypit/driver-node": "workspace:*",
|
|
18
19
|
"@hypit/estimate": "workspace:*",
|
|
@@ -12,8 +12,7 @@ import {
|
|
|
12
12
|
|
|
13
13
|
// The Distribution root is the replaceable Hypit tool checkout, not this
|
|
14
14
|
// package's source directory and never the author's project.
|
|
15
|
-
const packageRoot = resolve(
|
|
16
|
-
const installedLauncher = process.env.HYPIT_CLI_LAUNCHER;
|
|
15
|
+
const packageRoot = resolve(import.meta.dirname, "../../..");
|
|
17
16
|
const defaultBuildResultRepository = {
|
|
18
17
|
use: "@hypit/build-result-fs",
|
|
19
18
|
config: { path: ".hypit/results" },
|
|
@@ -57,14 +56,8 @@ export const videoCliDistribution: CliDistribution = {
|
|
|
57
56
|
: { distributionPackageRoot: options.distributionPackageRoot }),
|
|
58
57
|
workerLaunch: {
|
|
59
58
|
command: process.execPath,
|
|
60
|
-
// The
|
|
61
|
-
|
|
62
|
-
args: installedLauncher === undefined
|
|
63
|
-
? [
|
|
64
|
-
...process.execArgv.filter((item) => !item.startsWith("--test")),
|
|
65
|
-
fileURLToPath(new URL("./cli.ts", import.meta.url)),
|
|
66
|
-
]
|
|
67
|
-
: [installedLauncher],
|
|
59
|
+
// The loaded Distribution owns its Worker entry and TypeScript/package resolution.
|
|
60
|
+
args: [fileURLToPath(new URL("../../../bin/hypit.mjs", import.meta.url))],
|
|
68
61
|
},
|
|
69
62
|
}),
|
|
70
63
|
openProjectResults: async (projectRoot, options) => {
|
|
@@ -20,7 +20,7 @@ export function writeVersionHelp(io: CliIo): void {
|
|
|
20
20
|
/** Installation discovery belongs to the executable, independent of video execution and Skill installers. */
|
|
21
21
|
export async function runVersionCli(argv: readonly string[], io: CliIo, environment: VersionEnvironment = {
|
|
22
22
|
packageRoot: resolve(import.meta.dirname, "../../.."),
|
|
23
|
-
|
|
23
|
+
launcher: resolve(import.meta.dirname, "../../../bin/hypit.mjs"),
|
|
24
24
|
fetch: globalThis.fetch,
|
|
25
25
|
}): Promise<void> {
|
|
26
26
|
let check = false;
|
|
@@ -17,8 +17,9 @@ Runtime Profile and creates no Build.
|
|
|
17
17
|
- `downloadVideo(url, target)` downloads one video to the given path. Its caller owns destination
|
|
18
18
|
preparation and overwrite policy. The target extension must be `.mp4`, `.mkv`, `.webm` or `.mov`.
|
|
19
19
|
|
|
20
|
-
The downloader
|
|
21
|
-
|
|
20
|
+
The downloader locates its declared `@hypit/yt-dlp-service-runtime` package through the active
|
|
21
|
+
Distribution/package resolver and invokes that package's locked Python project through `uv`.
|
|
22
|
+
Ship its `pyproject.toml` and `uv.lock`; the caller's location and a `services/` ancestor are irrelevant.
|
|
22
23
|
`uv` and `ffmpeg` must be available on PATH; the CLI also uses `ffprobe` to report the saved media.
|
|
23
24
|
|
|
24
25
|
The request selects `bv*+ba/b`, with `res:1080,vcodec:h264` format preferences and `--no-playlist`.
|
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
* the same shape WhisperX and OpenCV already use for their Python programs.
|
|
12
12
|
*/
|
|
13
13
|
import { spawnSync } from "node:child_process";
|
|
14
|
-
import { existsSync } from "node:fs";
|
|
15
14
|
import { copyFile, mkdtemp, readdir, rename, rm } from "node:fs/promises";
|
|
16
15
|
import { tmpdir } from "node:os";
|
|
17
16
|
import { dirname, extname, join } from "node:path";
|
|
18
|
-
|
|
17
|
+
|
|
18
|
+
import { resolveNodePackageResource } from "@hypit/package-loader-node";
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Whether this is a link to fetch rather than a path to open.
|
|
@@ -34,20 +34,16 @@ export function isVideoUrl(value: string): boolean {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* The uv project
|
|
38
|
-
*
|
|
37
|
+
* The locked uv project is a Distribution package asset, the same way WhisperX and
|
|
38
|
+
* OpenCV locate their Python programs. Walking parents of this file only works
|
|
39
|
+
* while the module still sits above `services/` in a contributor checkout.
|
|
39
40
|
*/
|
|
40
41
|
function serviceProject(): string {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (parent === directory) {
|
|
47
|
-
throw new Error("no services/yt-dlp project above this module; the Distribution is incomplete");
|
|
48
|
-
}
|
|
49
|
-
directory = parent;
|
|
50
|
-
}
|
|
42
|
+
return dirname(resolveNodePackageResource(
|
|
43
|
+
"@hypit/yt-dlp-service-runtime",
|
|
44
|
+
"pyproject.toml",
|
|
45
|
+
{ from: import.meta.url },
|
|
46
|
+
));
|
|
51
47
|
}
|
|
52
48
|
|
|
53
49
|
/**
|