@hypit/hypit 0.1.8 → 0.1.10
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 +15 -4
- package/dist/public/endpoint-kit.d.ts +1 -1
- package/dist/public/runtime-kit.d.ts +3 -1
- package/examples/provider-package/README.md +12 -0
- package/examples/provider-package/packages/provider-images/src/provider.ts +37 -8
- package/package.json +1 -1
- package/packages/cli/README.md +27 -0
- package/packages/cli/src/command-hint.ts +20 -0
- package/packages/cli/src/commands/environment.ts +39 -18
- package/packages/cli/src/commands/execution.ts +27 -17
- package/packages/cli/src/commands/results.ts +2 -1
- package/packages/cli/src/machine-view.ts +4 -2
- package/packages/cli/src/main.ts +23 -22
- package/packages/cli/src/observation.ts +7 -2
- package/packages/cli/src/output.ts +3 -0
- package/packages/cli/src/view.ts +4 -1
- package/packages/driver-node/README.md +5 -0
- package/packages/driver-node/src/driver.ts +20 -15
- package/packages/endpoint-kit/README.md +11 -2
- package/packages/endpoint-kit/src/index.ts +1 -1
- package/packages/generation/README.md +10 -0
- package/packages/provider-hypihub/README.md +41 -6
- package/packages/provider-hypihub/src/errors.ts +61 -0
- package/packages/provider-hypihub/src/mapping.ts +10 -25
- package/packages/provider-hypihub/src/oauth.ts +4 -1
- package/packages/provider-hypihub/src/provider.ts +104 -75
- package/packages/provider-hypihub/src/routes.ts +24 -3
- package/packages/provider-hypihub/src/upload.ts +8 -18
- package/packages/provider-whisperx-local/README.md +7 -1
- package/packages/provider-whisperx-local/src/program.ts +2 -0
- package/packages/runtime-host-node/src/index.ts +4 -0
- package/packages/runtime-kit/README.md +3 -0
- package/packages/runtime-kit/src/index.ts +2 -0
- package/packages/runtime-local/README.md +17 -0
- package/packages/runtime-local/package.json +2 -1
- package/packages/runtime-local/src/program-lock.ts +40 -0
- package/packages/runtime-local/src/programs.ts +156 -79
- package/packages/video-cli/README.md +33 -6
- package/packages/video-cli/src/cli.ts +4 -1
- package/packages/video-cli/src/creation.ts +7 -2
- package/packages/video-cli/src/index.ts +2 -0
- package/packages/video-cli/src/version.ts +90 -0
- package/services/whisperx/README.md +10 -3
- package/services/whisperx/src/hypit_whisperx_service/engine.py +19 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { basename, join } from "node:path";
|
|
4
4
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
5
5
|
|
|
6
6
|
import type { CapabilityRef } from "@hypit/protocol";
|
|
@@ -30,6 +30,8 @@ export type ManagedProgramReport = {
|
|
|
30
30
|
/** Why the action fell short. Never a copy of what `state` already says. */
|
|
31
31
|
readonly detail?: string;
|
|
32
32
|
readonly logPath?: string;
|
|
33
|
+
readonly installationLogPath?: string;
|
|
34
|
+
readonly errorLogPath?: string;
|
|
33
35
|
readonly pid?: number;
|
|
34
36
|
};
|
|
35
37
|
|
|
@@ -37,6 +39,7 @@ export type ManagedProgramProgress = {
|
|
|
37
39
|
readonly id: string;
|
|
38
40
|
readonly phase: "checking" | "installing" | "starting" | "waiting" | "ready";
|
|
39
41
|
readonly logPath?: string;
|
|
42
|
+
readonly detail?: string;
|
|
40
43
|
};
|
|
41
44
|
|
|
42
45
|
export type ManagedProgramOptions = LoadRuntimeConfigOptions & {
|
|
@@ -99,9 +102,40 @@ async function readPid(root: string, program: ManagedProgram): Promise<number |
|
|
|
99
102
|
}
|
|
100
103
|
}
|
|
101
104
|
|
|
105
|
+
async function existingLogs(root: string, program: ManagedProgram) {
|
|
106
|
+
const home = directory(root, program);
|
|
107
|
+
const paths = {
|
|
108
|
+
logPath: join(home, "program.log"),
|
|
109
|
+
installationLogPath: join(home, "install.log"),
|
|
110
|
+
...(process.platform === "win32" ? { errorLogPath: processErrorLogPath(join(home, "program.log")) } : {}),
|
|
111
|
+
};
|
|
112
|
+
const found: { logPath?: string; installationLogPath?: string; errorLogPath?: string } = {};
|
|
113
|
+
for (const [key, path] of Object.entries(paths)) {
|
|
114
|
+
try { await stat(path); found[key as keyof typeof found] = path; }
|
|
115
|
+
catch (error) { if (!nodeError(error, "ENOENT")) throw error; }
|
|
116
|
+
}
|
|
117
|
+
return found;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function programReport(root: string, program: ManagedProgram, endpoint: string): Promise<ManagedProgramReport> {
|
|
121
|
+
const state = await program.probe();
|
|
122
|
+
const pid = await readPid(root, program);
|
|
123
|
+
return { id: program.id, endpoint, state, ...await existingLogs(root, program),
|
|
124
|
+
...(pid !== undefined && processAlive(pid) ? { pid } : {}),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function commandLabel(command: ManagedProgramCommand): string {
|
|
129
|
+
// Labels describe purpose. Arguments and environment values can contain credentials.
|
|
130
|
+
return (command.label ?? basename(command.command)).replace(/\s+/gu, " ").trim();
|
|
131
|
+
}
|
|
132
|
+
|
|
102
133
|
async function run(root: string, command: ManagedProgramCommand, logPath: string): Promise<{ ok: boolean; detail: string }> {
|
|
103
134
|
const log = await open(logPath, "a+");
|
|
104
135
|
try {
|
|
136
|
+
const started = Date.now();
|
|
137
|
+
const label = commandLabel(command);
|
|
138
|
+
await log.appendFile(`\n${new Date().toISOString()} Starting: ${label}\n`);
|
|
105
139
|
const offset = (await log.stat()).size;
|
|
106
140
|
const exit = await new Promise<{ code: number | null; error?: string }>((resolve) => {
|
|
107
141
|
const child = spawn(command.command, [...command.args], {
|
|
@@ -114,25 +148,24 @@ async function run(root: string, command: ManagedProgramCommand, logPath: string
|
|
|
114
148
|
child.on("error", (error) => resolve({ code: null, error: error.message }));
|
|
115
149
|
child.on("close", (code) => resolve({ code }));
|
|
116
150
|
});
|
|
117
|
-
if (exit.code === 0)
|
|
151
|
+
if (exit.code === 0) {
|
|
152
|
+
await log.appendFile(`${new Date().toISOString()} Finished: ${label} (${((Date.now() - started) / 1000).toFixed(1)}s)\n`);
|
|
153
|
+
return { ok: true, detail: "" };
|
|
154
|
+
}
|
|
118
155
|
const end = (await log.stat()).size;
|
|
119
156
|
const start = Math.max(offset, end - 8192);
|
|
120
157
|
const tail = Buffer.alloc(end - start);
|
|
121
158
|
const { bytesRead } = await log.read(tail, 0, tail.length, start);
|
|
122
159
|
const line = tail.subarray(0, bytesRead).toString().trim().split(/\r?\n/u).at(-1);
|
|
160
|
+
await log.appendFile(`${new Date().toISOString()} Failed: ${label} (${((Date.now() - started) / 1000).toFixed(1)}s)\n`);
|
|
123
161
|
return { ok: false, detail: exit.error ?? (line || `exited ${exit.code}`) };
|
|
124
162
|
} finally {
|
|
125
163
|
await log.close();
|
|
126
164
|
}
|
|
127
165
|
}
|
|
128
166
|
|
|
129
|
-
/**
|
|
130
|
-
* Poll until the program answers as itself. `down` is expected while it loads
|
|
131
|
-
* weights; `mismatch` is not, and stops the wait — a program that is answering
|
|
132
|
-
* with another identity will not become the right one by waiting.
|
|
133
|
-
*/
|
|
134
167
|
/** POSIX: its own session, and stdio already pointed at the log. */
|
|
135
|
-
async function startDetached(start: ManagedProgramCommand, root: string, logFd: number): Promise<number
|
|
168
|
+
async function startDetached(start: ManagedProgramCommand, root: string, logFd: number): Promise<{ pid?: number; detail?: string }> {
|
|
136
169
|
const child = spawn(start.command, [...start.args], {
|
|
137
170
|
cwd: start.cwd ?? root,
|
|
138
171
|
env: { ...process.env, ...start.env },
|
|
@@ -141,8 +174,13 @@ async function startDetached(start: ManagedProgramCommand, root: string, logFd:
|
|
|
141
174
|
windowsHide: true,
|
|
142
175
|
stdio: ["ignore", logFd, logFd],
|
|
143
176
|
});
|
|
144
|
-
|
|
145
|
-
|
|
177
|
+
return await new Promise((resolve) => {
|
|
178
|
+
child.once("error", (error) => resolve({ detail: error.message }));
|
|
179
|
+
child.once("spawn", () => {
|
|
180
|
+
child.unref();
|
|
181
|
+
resolve(child.pid === undefined ? { detail: "spawn reported no process id" } : { pid: child.pid });
|
|
182
|
+
});
|
|
183
|
+
});
|
|
146
184
|
}
|
|
147
185
|
|
|
148
186
|
/** A PowerShell single-quoted literal, which escapes by doubling the quote and nothing else. */
|
|
@@ -243,11 +281,12 @@ async function waitForReady(program: ManagedProgram, pid: number, maxWaitMs: num
|
|
|
243
281
|
return state;
|
|
244
282
|
}
|
|
245
283
|
|
|
246
|
-
async function
|
|
284
|
+
async function bringUpOwned(
|
|
247
285
|
root: string,
|
|
248
286
|
program: ManagedProgram,
|
|
249
287
|
endpoint: string,
|
|
250
288
|
maxWaitMs: number,
|
|
289
|
+
release: () => void,
|
|
251
290
|
onProgress?: (event: ManagedProgramProgress) => void,
|
|
252
291
|
): Promise<ManagedProgramReport> {
|
|
253
292
|
const base = { id: program.id, endpoint };
|
|
@@ -265,13 +304,28 @@ async function bringUp(
|
|
|
265
304
|
let installed = false;
|
|
266
305
|
const logPath = join(directory(root, program), "program.log");
|
|
267
306
|
const installLogPath = join(directory(root, program), "install.log");
|
|
307
|
+
const existingPid = await readPid(root, program);
|
|
308
|
+
if (existingPid !== undefined && processAlive(existingPid)) {
|
|
309
|
+
release();
|
|
310
|
+
onProgress?.({ id: program.id, phase: "waiting", logPath,
|
|
311
|
+
detail: `observing existing process ${existingPid}` });
|
|
312
|
+
const state = await waitForReady(program, existingPid, maxWaitMs);
|
|
313
|
+
const alive = processAlive(existingPid);
|
|
314
|
+
return { ...base, action: state.state === "ready" ? "already-running" : "unchanged", state, logPath,
|
|
315
|
+
...(alive ? { pid: existingPid } : {}),
|
|
316
|
+
...(state.state === "ready" ? {} : { detail: alive
|
|
317
|
+
? `process ${existingPid} is still running; readiness has not been established; see ${logPath}`
|
|
318
|
+
: `process exited; see ${logPath}` }),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
if (existingPid !== undefined) await rm(join(directory(root, program), "process.pid"), { force: true });
|
|
268
322
|
if (program.installation !== undefined) {
|
|
269
323
|
const installation = await program.installation.probe();
|
|
270
324
|
if (installation.state !== "ready" || program.installation.prepareBeforeStart === true) {
|
|
271
325
|
await mkdir(directory(root, program), { recursive: true });
|
|
272
326
|
await rotateLog(installLogPath);
|
|
273
|
-
onProgress?.({ id: program.id, phase: "installing", logPath: installLogPath });
|
|
274
327
|
for (const command of program.installation.commands) {
|
|
328
|
+
onProgress?.({ id: program.id, phase: "installing", logPath: installLogPath, detail: commandLabel(command) });
|
|
275
329
|
const result = await run(root, command, installLogPath);
|
|
276
330
|
if (!result.ok) {
|
|
277
331
|
return { ...base, action: "unchanged", state: initial, logPath: installLogPath, detail: `${command.command} failed: ${result.detail}` };
|
|
@@ -317,7 +371,7 @@ async function bringUp(
|
|
|
317
371
|
// creates a new console and hides it, which is the combination neither spawn option reaches.
|
|
318
372
|
const started = process.platform === "win32"
|
|
319
373
|
? await startWithOwnConsole(program.start, root, logPath)
|
|
320
|
-
:
|
|
374
|
+
: await startDetached(program.start, root, log.fd);
|
|
321
375
|
if (started.pid === undefined) {
|
|
322
376
|
const refusal = "detail" in started && started.detail !== undefined ? `: ${started.detail}` : "";
|
|
323
377
|
return {
|
|
@@ -329,11 +383,27 @@ async function bringUp(
|
|
|
329
383
|
};
|
|
330
384
|
}
|
|
331
385
|
const child = { pid: started.pid };
|
|
386
|
+
// Publish ownership before observing readiness. Another up can now reuse this process,
|
|
387
|
+
// and down can stop it even if loading is slow. The observation owns no startup lock.
|
|
388
|
+
const pidPath = join(directory(root, program), "process.pid");
|
|
389
|
+
// Under the startup lock the old record is already absent. Rename publishes a complete
|
|
390
|
+
// record to read-only observers without replacing a file they might have open on Windows.
|
|
391
|
+
await writeFile(`${pidPath}.tmp`, `${child.pid}\n`);
|
|
392
|
+
await rename(`${pidPath}.tmp`, pidPath);
|
|
393
|
+
release();
|
|
332
394
|
onProgress?.({ id: program.id, phase: "waiting" });
|
|
333
395
|
const state = await waitForReady(program, child.pid, maxWaitMs);
|
|
334
396
|
if (state.state === "ready") onProgress?.({ id: program.id, phase: "ready" });
|
|
335
397
|
const alive = processAlive(child.pid);
|
|
336
|
-
if (alive)
|
|
398
|
+
if (!alive) {
|
|
399
|
+
const { tryProgramLock } = await import("./program-lock.js");
|
|
400
|
+
const releaseCleanup = tryProgramLock(directory(root, program));
|
|
401
|
+
if (releaseCleanup !== undefined) {
|
|
402
|
+
try {
|
|
403
|
+
if (await readPid(root, program) === child.pid) await rm(pidPath, { force: true });
|
|
404
|
+
} finally { releaseCleanup(); }
|
|
405
|
+
}
|
|
406
|
+
}
|
|
337
407
|
return {
|
|
338
408
|
...base,
|
|
339
409
|
action: state.state === "ready" ? (alive ? "started" : "already-running") : "unchanged",
|
|
@@ -341,7 +411,7 @@ async function bringUp(
|
|
|
341
411
|
...(alive ? { pid: child.pid } : {}),
|
|
342
412
|
logPath,
|
|
343
413
|
...(state.state === "ready" ? {} : {
|
|
344
|
-
detail: alive ? `see ${logPath}` : `process exited; see ${logPath}`,
|
|
414
|
+
detail: alive ? `process ${child.pid} is still running; readiness has not been established; see ${logPath}` : `process exited; see ${logPath}`,
|
|
345
415
|
}),
|
|
346
416
|
};
|
|
347
417
|
} finally {
|
|
@@ -349,6 +419,22 @@ async function bringUp(
|
|
|
349
419
|
}
|
|
350
420
|
}
|
|
351
421
|
|
|
422
|
+
async function bringUp(
|
|
423
|
+
root: string, program: ManagedProgram, endpoint: string, maxWaitMs: number,
|
|
424
|
+
onProgress?: (event: ManagedProgramProgress) => void,
|
|
425
|
+
): Promise<ManagedProgramReport> {
|
|
426
|
+
const home = directory(root, program);
|
|
427
|
+
await mkdir(home, { recursive: true });
|
|
428
|
+
const { tryProgramLock } = await import("./program-lock.js");
|
|
429
|
+
const release = tryProgramLock(home);
|
|
430
|
+
if (release === undefined) return { ...await programReport(root, program, endpoint), action: "unchanged",
|
|
431
|
+
detail: "another command owns this Program's preparation or lifecycle change; inspect its logs", };
|
|
432
|
+
try {
|
|
433
|
+
const report = await bringUpOwned(root, program, endpoint, maxWaitMs, release, onProgress);
|
|
434
|
+
return { ...await existingLogs(root, program), ...report };
|
|
435
|
+
} finally { release(); }
|
|
436
|
+
}
|
|
437
|
+
|
|
352
438
|
/** Install when needed and start every external program the Runtime Profile implies. */
|
|
353
439
|
export async function bringManagedProgramsUp(
|
|
354
440
|
path: string,
|
|
@@ -371,60 +457,68 @@ export async function takeManagedProgramsDown(
|
|
|
371
457
|
const { dataRoot, programs } = await declaredManagedPrograms(path, options);
|
|
372
458
|
const reports = await Promise.all(independentPrograms(programs).map(async ({ instance, program }): Promise<ManagedProgramReport> => {
|
|
373
459
|
const base = { id: program.id, endpoint: instance };
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
}
|
|
394
|
-
if (term === "gone") {
|
|
395
|
-
await rm(join(directory(dataRoot, program), "process.pid"), { force: true });
|
|
396
|
-
const state = await program.probe();
|
|
397
|
-
return state.state === "down"
|
|
398
|
-
? { ...base, action: "nothing-to-stop", state }
|
|
399
|
-
: { ...base, action: "not-ours", state, detail: `${program.id} is now served by another process` };
|
|
400
|
-
}
|
|
401
|
-
const deadline = Date.now() + 15_000;
|
|
402
|
-
while (processAlive(pid) && Date.now() < deadline) await sleep(200);
|
|
403
|
-
if (processAlive(pid)) {
|
|
404
|
-
if (await stopProcessTree(pid, true) === "denied") {
|
|
460
|
+
const home = directory(dataRoot, program);
|
|
461
|
+
await mkdir(home, { recursive: true });
|
|
462
|
+
const { tryProgramLock } = await import("./program-lock.js");
|
|
463
|
+
const release = tryProgramLock(home);
|
|
464
|
+
if (release === undefined) return { ...await programReport(dataRoot, program, instance), action: "unchanged",
|
|
465
|
+
detail: "another command owns this Program's preparation or lifecycle change; inspect its logs", };
|
|
466
|
+
try {
|
|
467
|
+
const pid = await readPid(dataRoot, program);
|
|
468
|
+
if (pid === undefined || !processAlive(pid)) {
|
|
469
|
+
if (pid !== undefined) await rm(join(directory(dataRoot, program), "process.pid"), { force: true });
|
|
470
|
+
const state = await program.probe();
|
|
471
|
+
return state.state === "down"
|
|
472
|
+
? { ...base, action: "nothing-to-stop", state }
|
|
473
|
+
// Someone else's process, or one started by hand. Killing it is not this
|
|
474
|
+
// command's business; saying so is.
|
|
475
|
+
: { ...base, action: "not-ours", state, detail: `${program.id} is running without a Hypit process record` };
|
|
476
|
+
}
|
|
477
|
+
const term = await stopProcessTree(pid);
|
|
478
|
+
if (term === "denied") {
|
|
405
479
|
return {
|
|
406
480
|
...base,
|
|
407
481
|
action: "unchanged",
|
|
408
482
|
state: await program.probe(),
|
|
409
483
|
pid,
|
|
410
|
-
detail: `process ${pid}
|
|
484
|
+
detail: `process ${pid} is running but this environment cannot stop it`,
|
|
411
485
|
};
|
|
412
486
|
}
|
|
413
|
-
|
|
414
|
-
|
|
487
|
+
if (term === "gone") {
|
|
488
|
+
await rm(join(directory(dataRoot, program), "process.pid"), { force: true });
|
|
489
|
+
const state = await program.probe();
|
|
490
|
+
return state.state === "down"
|
|
491
|
+
? { ...base, action: "nothing-to-stop", state }
|
|
492
|
+
: { ...base, action: "not-ours", state, detail: `${program.id} is now served by another process` };
|
|
493
|
+
}
|
|
494
|
+
const deadline = Date.now() + 15_000;
|
|
495
|
+
while (processAlive(pid) && Date.now() < deadline) await sleep(200);
|
|
415
496
|
if (processAlive(pid)) {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
497
|
+
if (await stopProcessTree(pid, true) === "denied") {
|
|
498
|
+
return {
|
|
499
|
+
...base,
|
|
500
|
+
action: "unchanged",
|
|
501
|
+
state: await program.probe(),
|
|
502
|
+
pid,
|
|
503
|
+
detail: `process ${pid} ignored graceful termination and cannot be force-stopped`,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
const forceDeadline = Date.now() + 2_000;
|
|
507
|
+
while (processAlive(pid) && Date.now() < forceDeadline) await sleep(50);
|
|
508
|
+
if (processAlive(pid)) {
|
|
509
|
+
return {
|
|
510
|
+
...base,
|
|
511
|
+
action: "unchanged",
|
|
512
|
+
state: await program.probe(),
|
|
513
|
+
pid,
|
|
514
|
+
detail: `process ${pid} remained alive after forced termination`,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
423
517
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
518
|
+
await rm(join(directory(dataRoot, program), "process.pid"), { force: true });
|
|
519
|
+
return { ...base, action: "stopped", state: await program.probe(), pid };
|
|
520
|
+
} finally { release(); }
|
|
521
|
+
}));
|
|
428
522
|
return { dataRoot, programs: reports };
|
|
429
523
|
}
|
|
430
524
|
|
|
@@ -434,24 +528,7 @@ export async function reportManagedPrograms(
|
|
|
434
528
|
options: ManagedProgramOptions = {},
|
|
435
529
|
): Promise<{ readonly dataRoot: string; readonly programs: readonly ManagedProgramReport[] }> {
|
|
436
530
|
const { dataRoot, programs } = await declaredManagedPrograms(path, options);
|
|
437
|
-
const reports = await Promise.all(independentPrograms(programs).map(async ({ instance, program })
|
|
438
|
-
|
|
439
|
-
const pid = await readPid(dataRoot, program);
|
|
440
|
-
const logPath = join(directory(dataRoot, program), "program.log");
|
|
441
|
-
let hasLog = false;
|
|
442
|
-
try {
|
|
443
|
-
await stat(logPath);
|
|
444
|
-
hasLog = true;
|
|
445
|
-
} catch (error) {
|
|
446
|
-
if (!nodeError(error, "ENOENT")) throw error;
|
|
447
|
-
}
|
|
448
|
-
return {
|
|
449
|
-
id: program.id,
|
|
450
|
-
endpoint: instance,
|
|
451
|
-
state,
|
|
452
|
-
...(pid !== undefined && processAlive(pid) ? { pid } : {}),
|
|
453
|
-
...(hasLog ? { logPath } : {}),
|
|
454
|
-
};
|
|
455
|
-
}));
|
|
531
|
+
const reports = await Promise.all(independentPrograms(programs).map(async ({ instance, program }) =>
|
|
532
|
+
await programReport(dataRoot, program, instance)));
|
|
456
533
|
return { dataRoot, programs: reports };
|
|
457
534
|
}
|
|
@@ -11,15 +11,38 @@ reusable Source directly, for example
|
|
|
11
11
|
`<import as="ugc" source="@hypit/gpt-image-kits/phone-ugc-v1"/>`; resolving that Source does not
|
|
12
12
|
activate package code.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
`hypit version` reports the Distribution version, physical root and launcher independently of a
|
|
15
|
+
project or Runtime. `hypit version --check` also reads the package's `latest` tag at
|
|
16
|
+
`https://registry.npmjs.org/`; `--registry <url>` explicitly selects another registry. `--json`
|
|
17
|
+
returns `hypit.cli-version@1`. A failed check retains local facts, leaves the remote version unknown
|
|
18
|
+
and exits nonzero. A different version is not automatically newer: the launcher may be a newer
|
|
19
|
+
checkout or the mirror may lag. The command links release notes and never installs or updates.
|
|
20
|
+
Skill installers own their separate installed copies; this command does not scan Agent directories.
|
|
21
|
+
`hypit --version` remains a local version-only query.
|
|
22
|
+
|
|
23
|
+
Commands below serve independent authoring decisions. Start with the current project's material;
|
|
24
|
+
local inspection and estimation need no generation account:
|
|
15
25
|
|
|
16
26
|
```bash
|
|
17
27
|
cd path/to/project
|
|
18
|
-
hypit runtime init
|
|
19
|
-
hypit auth login hypihub.default
|
|
20
|
-
hypit doctor
|
|
21
|
-
hypit runtime up
|
|
22
28
|
hypit check main.svml
|
|
29
|
+
hypit measure main.svml --segment hook --language en
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
When the work needs execution, the project selects a Runtime Profile. `hypit runtime init` creates
|
|
33
|
+
an editable starter; its Endpoint entries describe available routes, not choices made by the user.
|
|
34
|
+
Keep an existing chosen service, or configure the chosen local or hosted Provider and its capability
|
|
35
|
+
bindings. HypiHub is the recommended integrated hosted route in the official Distribution; other
|
|
36
|
+
services use project Provider packages. If the user chooses HypiHub,
|
|
37
|
+
`hypit auth login hypihub.default` connects that account.
|
|
38
|
+
`hypit doctor --endpoint <name>` checks a selected Endpoint;
|
|
39
|
+
`hypit runtime up --endpoint <name>` prepares that Endpoint and starts the Worker. Repeat the flag
|
|
40
|
+
for several chosen Endpoints; omitting it prepares the whole Profile. `hypit programs up --endpoint
|
|
41
|
+
<name>` prepares a local helper independently of the Worker.
|
|
42
|
+
|
|
43
|
+
With the selected execution environment:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
23
46
|
hypit plan build.svrun
|
|
24
47
|
hypit build build.svrun --follow
|
|
25
48
|
hypit status <build-id> --watch
|
|
@@ -29,7 +52,6 @@ hypit history <source-output-name> [--source ./main.svml]
|
|
|
29
52
|
hypit inspect <build-id> [--output <source-output-name>]
|
|
30
53
|
hypit get <build-id> --output final.video --to ./final.mp4
|
|
31
54
|
hypit cancel <build-id>
|
|
32
|
-
hypit doctor
|
|
33
55
|
```
|
|
34
56
|
|
|
35
57
|
`transcribe` runs one immediate request through the selected Runtime Profile, with no Build,
|
|
@@ -63,6 +85,11 @@ that produced them, so they are declared in the Source and go through `plan` and
|
|
|
63
85
|
voice or learn a passage's real length before authoring the rest, build a Run whose target is that
|
|
64
86
|
speech output and reuse it as a Candidate.
|
|
65
87
|
|
|
88
|
+
Provider selection is checked before `transcribe` invokes the service. Multiple matching Endpoints
|
|
89
|
+
can remain in the Profile: `bindings` chooses one for the capability. An unsupported request reports
|
|
90
|
+
the selected binding and Provider-owned rejection reasons so the author can adjust the request or
|
|
91
|
+
choose a compatible Endpoint. It does not imply that an account needs payment or login.
|
|
92
|
+
|
|
66
93
|
Two more families are local, stateless and spend nothing. `hypit media` exposes the source at chosen
|
|
67
94
|
times and scales, and `hypit vocabulary`
|
|
68
95
|
prints what a Source may write:
|
|
@@ -5,6 +5,7 @@ import { creationCommands, isCreationCommand, writeCreationHelp } from "./creati
|
|
|
5
5
|
import { isMediaCommand, mediaCommands, writeMediaHelp } from "./media.js";
|
|
6
6
|
import { writeVocabularyHelp } from "./vocabulary.js";
|
|
7
7
|
import { writeCaptureHelp } from "./capture.js";
|
|
8
|
+
import { runVersionCli, writeVersionHelp } from "./version.js";
|
|
8
9
|
|
|
9
10
|
const argv = process.argv.slice(2);
|
|
10
11
|
const json = argv.includes("--json");
|
|
@@ -67,6 +68,7 @@ async function main(): Promise<void> {
|
|
|
67
68
|
const topic = argv[0] === "help" ? argv[1]
|
|
68
69
|
: argv[0] === "--help" ? undefined
|
|
69
70
|
: argv.includes("--help") ? argv[0] : undefined;
|
|
71
|
+
if (topic === "version") { writeVersionHelp(io); return; }
|
|
70
72
|
if (isCreationCommand(topic)) {
|
|
71
73
|
writeCreationHelp(io, topic);
|
|
72
74
|
return;
|
|
@@ -86,12 +88,13 @@ async function main(): Promise<void> {
|
|
|
86
88
|
}
|
|
87
89
|
writeCliHelp(io, topic);
|
|
88
90
|
if (topic === undefined) {
|
|
89
|
-
io.write(`\nCreation tools (one request through the selected Runtime Profile, no Build)\n${
|
|
91
|
+
io.write(`\nInstallation\n version [--check] [--registry <url>] [--json]\n\nCreation tools (one request through the selected Runtime Profile, no Build)\n${
|
|
90
92
|
creationCommands.map((item) => ` ${item}`).join("\n")}\n hypit help <tool> for each\n`
|
|
91
93
|
+ `\nStudio\n studio --run <build.svrun>\n hypit studio --help for session options\n\nPreparation (local tools and project files)\n media ${mediaCommands.join(" | ")}\n capture screenshot | run | install-browser\n vocabulary\n hypit help media, hypit help capture, hypit help vocabulary\n`);
|
|
92
94
|
}
|
|
93
95
|
return;
|
|
94
96
|
}
|
|
97
|
+
if (argv[0] === "version") { await runVersionCli(argv, io); return; }
|
|
95
98
|
const { runVideoCli } = await import("./index.js");
|
|
96
99
|
await runVideoCli(argv, io);
|
|
97
100
|
}
|
|
@@ -159,9 +159,14 @@ async function selectedProvider(host: CreationHost, need: Need, profile: string)
|
|
|
159
159
|
}]);
|
|
160
160
|
const subject = capabilityName(need.capability);
|
|
161
161
|
assert(provider !== undefined && provider.status !== "unresolved",
|
|
162
|
-
`No Endpoint in ${profile} serves ${subject};
|
|
162
|
+
`No Endpoint in ${profile} serves ${subject}; configure an Endpoint that supports this capability and check its binding in that Profile`);
|
|
163
163
|
assert(provider.status !== "ambiguous",
|
|
164
|
-
`Several Endpoints in ${profile} serve ${subject}: ${(provider.endpoints ?? []).join(", ")};
|
|
164
|
+
`Several Endpoints in ${profile} serve ${subject}: ${(provider.endpoints ?? []).join(", ")}; select one with bindings[${JSON.stringify(subject)}] in that Profile`);
|
|
165
|
+
assert(provider.status !== "unsupported",
|
|
166
|
+
`The configured Endpoints in ${profile} do not support this request for ${subject}`
|
|
167
|
+
+ (provider.binding === undefined ? "" : ` (binding: ${provider.binding})`)
|
|
168
|
+
+ `: ${(provider.rejections ?? []).map((item) => `${item.endpoint}: ${item.message}`).join("; ") || "no support reason supplied"}`
|
|
169
|
+
+ "; adjust the request or select a compatible Endpoint in that Profile");
|
|
165
170
|
return provider;
|
|
166
171
|
}
|
|
167
172
|
|
|
@@ -12,6 +12,7 @@ import { videoCliDistribution } from "./distribution.js";
|
|
|
12
12
|
import { runMediaCli } from "./media.js";
|
|
13
13
|
import { runVocabularyCli } from "./vocabulary.js";
|
|
14
14
|
import { runCaptureCli } from "./capture.js";
|
|
15
|
+
import { runVersionCli } from "./version.js";
|
|
15
16
|
|
|
16
17
|
export {
|
|
17
18
|
createVideoCompiler,
|
|
@@ -35,6 +36,7 @@ export function runVideoCli(
|
|
|
35
36
|
io: CliIo,
|
|
36
37
|
packages: readonly LoadedPackage[] = [],
|
|
37
38
|
): Promise<void> {
|
|
39
|
+
if (argv[0] === "version") return runVersionCli(argv, io);
|
|
38
40
|
installDistributionPackageResolution(videoCliDistribution.packageRoot === undefined
|
|
39
41
|
? []
|
|
40
42
|
: [videoCliDistribution.packageRoot]);
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import type { CliIo } from "@hypit/cli";
|
|
4
|
+
import { requestDeadline } from "@hypit/runtime-kit";
|
|
5
|
+
|
|
6
|
+
type VersionEnvironment = {
|
|
7
|
+
readonly packageRoot: string;
|
|
8
|
+
readonly launcher?: string;
|
|
9
|
+
readonly fetch: typeof globalThis.fetch;
|
|
10
|
+
readonly timeoutMs?: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function writeVersionHelp(io: CliIo): void {
|
|
14
|
+
io.write("Usage: hypit version [--check] [--registry <url>] [--json]\n\n"
|
|
15
|
+
+ "Show the executing Distribution and launcher without opening a project or Runtime.\n"
|
|
16
|
+
+ "--check queries npm latest (registry.npmjs.org by default); --registry selects a mirror.\n"
|
|
17
|
+
+ "No packages or Skills are updated. hypit --version remains a local version-only query.\n");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Installation discovery belongs to the executable, independent of video execution and Skill installers. */
|
|
21
|
+
export async function runVersionCli(argv: readonly string[], io: CliIo, environment: VersionEnvironment = {
|
|
22
|
+
packageRoot: resolve(import.meta.dirname, "../../.."),
|
|
23
|
+
...(process.env.HYPIT_CLI_LAUNCHER === undefined ? {} : { launcher: process.env.HYPIT_CLI_LAUNCHER }),
|
|
24
|
+
fetch: globalThis.fetch,
|
|
25
|
+
}): Promise<void> {
|
|
26
|
+
let check = false;
|
|
27
|
+
let json = false;
|
|
28
|
+
let registry = "https://registry.npmjs.org/";
|
|
29
|
+
let selectedRegistry = false;
|
|
30
|
+
for (let i = 1; i < argv.length; i++) {
|
|
31
|
+
const arg = argv[i];
|
|
32
|
+
if (arg === "--check") check = true;
|
|
33
|
+
else if (arg === "--json") json = true;
|
|
34
|
+
else if (arg === "--registry") {
|
|
35
|
+
selectedRegistry = true;
|
|
36
|
+
const value = argv[++i];
|
|
37
|
+
if (value === undefined || value.startsWith("--")) throw new Error("--registry requires a URL");
|
|
38
|
+
const url = new URL(value);
|
|
39
|
+
if (!["https:", "http:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
|
|
40
|
+
throw new Error("--registry requires an HTTP(S) registry URL without credentials, query or fragment");
|
|
41
|
+
}
|
|
42
|
+
registry = `${url.href.replace(/\/+$/u, "")}/`;
|
|
43
|
+
} else if (arg === "--help") { writeVersionHelp(io); return; }
|
|
44
|
+
else if (arg !== "--no-color" && arg !== "--debug") throw new Error(`Unknown version option: ${arg}`);
|
|
45
|
+
}
|
|
46
|
+
if (selectedRegistry && !check) throw new Error("--registry selects the source for --check; add --check to query it");
|
|
47
|
+
const manifest = JSON.parse(await readFile(resolve(environment.packageRoot, "package.json"), "utf8")) as {
|
|
48
|
+
name: string; version: string; repository?: { url?: string };
|
|
49
|
+
};
|
|
50
|
+
const repository = manifest.repository?.url?.replace(/^git\+/u, "").replace(/\.git$/u, "");
|
|
51
|
+
const releases = repository === undefined ? undefined : `${repository}/releases`;
|
|
52
|
+
let latest: { registry: string; version?: string; matchesInstalled?: boolean; error?: string } | undefined;
|
|
53
|
+
if (check) {
|
|
54
|
+
latest = { registry };
|
|
55
|
+
const deadline = requestDeadline(environment.timeoutMs ?? 10_000,
|
|
56
|
+
() => new Error("Registry version query timed out; latest version is unknown"));
|
|
57
|
+
try {
|
|
58
|
+
const response = await deadline.wait(environment.fetch(new URL(`${encodeURIComponent(manifest.name)}/latest`, registry), {
|
|
59
|
+
signal: deadline.signal, headers: { accept: "application/json" },
|
|
60
|
+
}));
|
|
61
|
+
if (!response.ok) throw new Error(`Registry returned HTTP ${response.status}; latest version is unknown`);
|
|
62
|
+
const body = await deadline.wait(response.json()) as { name?: unknown; version?: unknown };
|
|
63
|
+
if (body.name !== manifest.name || typeof body.version !== "string" || body.version.length === 0) {
|
|
64
|
+
throw new Error("Registry returned no matching package version; latest version is unknown");
|
|
65
|
+
}
|
|
66
|
+
latest.version = body.version;
|
|
67
|
+
latest.matchesInstalled = body.version === manifest.version;
|
|
68
|
+
} catch (error) {
|
|
69
|
+
latest.error = error instanceof Error ? error.message : String(error);
|
|
70
|
+
io.setExitCode?.(1);
|
|
71
|
+
} finally { deadline.finish(); }
|
|
72
|
+
}
|
|
73
|
+
const report = {
|
|
74
|
+
format: "hypit.cli-version@1", package: manifest.name, version: manifest.version,
|
|
75
|
+
distribution: resolve(environment.packageRoot),
|
|
76
|
+
...(environment.launcher === undefined ? {} : { launcher: resolve(environment.launcher) }),
|
|
77
|
+
...(releases === undefined ? {} : { releases }),
|
|
78
|
+
...(latest === undefined ? {} : { latest }),
|
|
79
|
+
};
|
|
80
|
+
if (json) { io.write(`${JSON.stringify(report, null, 2)}\n`); return; }
|
|
81
|
+
io.write(`${manifest.name} ${manifest.version}\nDistribution: ${report.distribution}\n`
|
|
82
|
+
+ (report.launcher === undefined ? "" : `Launcher: ${report.launcher}\n`));
|
|
83
|
+
if (latest !== undefined) {
|
|
84
|
+
io.write(`npm latest: ${latest.version === undefined ? "unknown" : `${latest.version} (${latest.matchesInstalled ? "matches installed" : "differs from installed"})`}\nRegistry: ${registry}\n`);
|
|
85
|
+
if (latest.error !== undefined) io.write(`${latest.error}\n`);
|
|
86
|
+
if (releases !== undefined) io.write(`Release notes: ${releases}\n`);
|
|
87
|
+
io.write("Update through this installation's package manager or checkout; preserve the project's chosen version.\n"
|
|
88
|
+
+ "Skill updates belong to its installer and are separate from this executable.\n");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -19,8 +19,8 @@ semantic projection combines this evidence with one explicit Script Segment late
|
|
|
19
19
|
## Install
|
|
20
20
|
|
|
21
21
|
For an ordinary installed Distribution, select the local WhisperX Endpoint and run
|
|
22
|
-
`hypit
|
|
23
|
-
|
|
22
|
+
`hypit programs up --endpoint <instance>`. The Runtime creates or reconciles the cold environment in
|
|
23
|
+
the machine Program Home and reuses a running service across projects and sessions. The commands below are contributor/operator
|
|
24
24
|
diagnostics for a deliberately managed deployment:
|
|
25
25
|
|
|
26
26
|
WhisperX 3.8.6 supports Python 3.10 through 3.13. The checked-in lock selects Python 3.13:
|
|
@@ -32,7 +32,8 @@ uv run --project services/whisperx --frozen hypit-whisperx-prepare
|
|
|
32
32
|
uv run --project services/whisperx --frozen hypit-whisperx-check
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
The first model start may download ASR
|
|
35
|
+
The first model start may download ASR weights; first use of a language can download its alignment
|
|
36
|
+
model during the request. Production should put the relevant
|
|
36
37
|
Hugging Face cache on persistent storage. `hypit-whisperx-prepare` separately installs NLTK's
|
|
37
38
|
`punkt_tab` sentence data through NLTK's own downloader. This resource is required by WhisperX
|
|
38
39
|
alignment and is prepared explicitly before the warm service starts, never inside an inference request.
|
|
@@ -71,6 +72,12 @@ Configuration is deployment state:
|
|
|
71
72
|
The Node Provider must configure the same model, device, compute, batch size, service version and
|
|
72
73
|
WhisperX version. A mismatch fails before transcription results are accepted.
|
|
73
74
|
|
|
75
|
+
The service logs ASR loading, transcription, language-model loading and word alignment where those
|
|
76
|
+
operations run. Completion entries include elapsed times. A loading entry means the library call
|
|
77
|
+
has begun and may include a weight download; the download client supplies any transfer progress.
|
|
78
|
+
Transcripts and audio content are not included in these service progress entries. `/health` answers
|
|
79
|
+
after ASR loading, so a healthy service may still prepare a language model on its first request.
|
|
80
|
+
|
|
74
81
|
## Package preparation
|
|
75
82
|
|
|
76
83
|
Local package preparation includes `src/**/*.py` in uv's package cache inputs. Updating service code
|