@hypit/hypit 0.1.8 → 0.1.9
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 +79 -74
- 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
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
|
|
39
39
|
Hypit gives AI agents (Claude Code, Codex...) a language and system to create video. Drop in a video, and your agent clones it as a complete workflow: footage, captions, B-roll and effects, all anchored to words instead of seconds.
|
|
40
40
|
|
|
41
|
-
**To be clear:** cloning a video is the fastest way in, not the only one. You can start from our templates, or just describe the video you want and your agent writes the workflow from scratch. Generation models are optional too: a workflow can compile captions, motion graphics and code-rendered visuals into a finished video without calling a
|
|
41
|
+
**To be clear:** cloning a video is the fastest way in, not the only one. You can start from our templates, or just describe the video you want and your agent writes the workflow from scratch. Generation models are optional too: a workflow can compile captions, motion graphics and code-rendered visuals into a finished video without calling a generation model or incurring its service charges.
|
|
42
42
|
|
|
43
43
|

|
|
44
44
|
|
|
@@ -61,6 +61,13 @@ npx skills add hypit-ai/hypit -g
|
|
|
61
61
|
This installs the Skill. On first use, your agent checks for the Hypit executable and helps prepare
|
|
62
62
|
it if needed. Your video project can live anywhere.
|
|
63
63
|
|
|
64
|
+
Hypit is free to use; your Coding Agent and model services have their own accounts and charges.
|
|
65
|
+
HypiHub is our recommended hosted model service. You can also use your own API or local models;
|
|
66
|
+
tell your agent the service name and API documentation so it can set up the appropriate connection.
|
|
67
|
+
|
|
68
|
+
[Agent environments and entry partners](https://github.com/hypit-ai/hypit/blob/main/docs/guide/agents.md) ·
|
|
69
|
+
[Model and deployment services](https://github.com/hypit-ai/hypit/blob/main/docs/guide/service-partners.md)
|
|
70
|
+
|
|
64
71
|
[Watch the complete video examples on GitHub](https://github.com/hypit-ai/hypit#examples).
|
|
65
72
|
|
|
66
73
|
## Use the Hypit skill
|
|
@@ -85,9 +92,9 @@ Your agent can check the environment, request the credentials the video needs, g
|
|
|
85
92
|
## Why Hypit
|
|
86
93
|
|
|
87
94
|
- **Clone any video:** drop in a video, get the whole workflow — footage, captions, B-roll, effects. Not a script breakdown.
|
|
88
|
-
- **One workflow, 100 variants:** the
|
|
95
|
+
- **One workflow, 100 variants:** reuse the composition and existing material; generate the parts that change.
|
|
89
96
|
- **Pluggable components:** swap the host without touching the captions. Use the library, fork it, or write your own.
|
|
90
|
-
- **Open source
|
|
97
|
+
- **Open source:** no Hypit seat pricing, per-render fees or added watermark. Model-service usage is billed by the service you choose.
|
|
91
98
|
|
|
92
99
|
## What Hypit Can Build
|
|
93
100
|
|
|
@@ -98,7 +105,7 @@ Drop in a video and your agent clones the whole workflow — or describe what yo
|
|
|
98
105
|
- **TikTok Shop and affiliate videos** — one format that converts, a new SKU every day. Swap the product, the price, the CTA; the structure that worked stays untouched.
|
|
99
106
|
- **AI UGC and talking heads** — narration, word-level captions, B-roll, comment stickers, beat-synced cuts, all wired automatically.
|
|
100
107
|
- **Podcast and interview clips** — split-screen layouts, speaker-aware captions, reaction overlays.
|
|
101
|
-
- **Code-rendered videos** — visuals driven entirely by front-end code
|
|
108
|
+
- **Code-rendered videos** — visuals driven entirely by front-end code, rendered locally without generation API calls.
|
|
102
109
|
- **Localized versions** — the same video in ten languages. Rewrite a line and the timing re-flows itself.
|
|
103
110
|
|
|
104
111
|
## Stay Tuned with Us
|
|
@@ -137,6 +144,10 @@ commands and the repository layout.
|
|
|
137
144
|
</tr>
|
|
138
145
|
</table>
|
|
139
146
|
|
|
147
|
+
## ⭐ Star History
|
|
148
|
+
|
|
149
|
+
[](https://www.star-history.com/#hypit-ai/hypit&Date)
|
|
150
|
+
|
|
140
151
|
## License
|
|
141
152
|
|
|
142
153
|
Hypit is released under the [Hypit Open Source License](https://github.com/hypit-ai/hypit/blob/main/LICENSE). The videos and other outputs you create belong to you; third-party models and services may have their own terms.
|
|
@@ -187,7 +187,7 @@ type EndpointInvocationContext = {
|
|
|
187
187
|
readonly credentials: Readonly<Record<string, EndpointCredential>>;
|
|
188
188
|
/** Explicit non-secret diagnostic output. The Provider owns its content, never its storage. */
|
|
189
189
|
readonly reportDiagnostic?: (diagnostic: ExecutionDiagnostic) => Promise<void>;
|
|
190
|
-
/** Report non-secret activity
|
|
190
|
+
/** Report non-secret activity while an immediate call or asynchronous action is still running. */
|
|
191
191
|
readonly reportProgress?: (progress: OperationProgress) => Promise<void>;
|
|
192
192
|
};
|
|
193
193
|
type ImmediateEndpointHandler = (context: EndpointInvocationContext) => Awaitable<EndpointFulfillment>;
|
|
@@ -185,7 +185,7 @@ type EndpointInvocationContext = {
|
|
|
185
185
|
readonly credentials: Readonly<Record<string, EndpointCredential>>;
|
|
186
186
|
/** Explicit non-secret diagnostic output. The Provider owns its content, never its storage. */
|
|
187
187
|
readonly reportDiagnostic?: (diagnostic: ExecutionDiagnostic) => Promise<void>;
|
|
188
|
-
/** Report non-secret activity
|
|
188
|
+
/** Report non-secret activity while an immediate call or asynchronous action is still running. */
|
|
189
189
|
readonly reportProgress?: (progress: OperationProgress) => Promise<void>;
|
|
190
190
|
};
|
|
191
191
|
type ImmediateEndpointHandler = (context: EndpointInvocationContext) => Awaitable<EndpointFulfillment>;
|
|
@@ -350,6 +350,8 @@ type RuntimeDoctorDiagnostic = {
|
|
|
350
350
|
readonly subject?: string;
|
|
351
351
|
};
|
|
352
352
|
type ManagedProgramCommand = {
|
|
353
|
+
/** Optional human-readable purpose, supplied by the program owner. Not a lifecycle state. */
|
|
354
|
+
readonly label?: string;
|
|
353
355
|
readonly command: string;
|
|
354
356
|
readonly args: readonly string[];
|
|
355
357
|
readonly cwd?: string;
|
|
@@ -23,6 +23,18 @@ The example service uses Bearer authentication for these API operations:
|
|
|
23
23
|
| Read rates | `GET /rates?model=gpt-image-2` → service-owned fields and a `description` stating rates, units and conditions |
|
|
24
24
|
| Collect result | GET the signed image URL without the account key |
|
|
25
25
|
|
|
26
|
+
HTTP failures expose a public `{ "error": { "code": "…", "message": "…" } }` and optional
|
|
27
|
+
`X-Request-Id`; failed tasks carry the same `error` object. The Provider preserves these fields,
|
|
28
|
+
the failed API operation or task ID, and omits unrelated response fields and signed URLs.
|
|
29
|
+
This example schema is not a universal service-error format: adapt the interpretation to the chosen API.
|
|
30
|
+
|
|
31
|
+
Known request limits and model identity are resolved before the media URL resolver can upload.
|
|
32
|
+
This service exposes no model-catalogue endpoint, so the example invents none. Services with a
|
|
33
|
+
documented query can use it before transferring references. Request preparation, submission and
|
|
34
|
+
collection report live activity through `reportProgress`; queued/running task responses supply
|
|
35
|
+
pending progress between calls. The Provider follows the selected mapping without substituting
|
|
36
|
+
another model or account after a failure.
|
|
37
|
+
|
|
26
38
|
`start` records the received task ID through `checkpoint` before returning it. Runtime owns polling,
|
|
27
39
|
capacity and durable operation state. `poll` returns `ready` for completed remote work; `collect`
|
|
28
40
|
stores the image through `context.resources` and returns the Model's declared value. The Provider
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { canonicalize, defineEndpointPackage, wakeAfter } from "@hypit/hypit/endpoint-kit";
|
|
2
2
|
import type { AsyncEndpoint, CredentialRef, EndpointRequest } from "@hypit/hypit/endpoint-kit";
|
|
3
|
-
import { compileWireRequest, generationTypes, sealGeneratedImageSet } from "@hypit/hypit/generation";
|
|
3
|
+
import { compileWireRequest, generationTypes, sealGeneratedImageSet, selectWireModelForRequest } from "@hypit/hypit/generation";
|
|
4
4
|
import type { GenerationRequest, GenerationWireMapping } from "@hypit/hypit/generation";
|
|
5
5
|
|
|
6
6
|
export const providerModule = { name: "@example/provider-images", version: "1" } as const;
|
|
@@ -25,6 +25,13 @@ function text(value: unknown): string {
|
|
|
25
25
|
if (typeof value !== "string" || value.length === 0) throw new Error("Expected nonempty service text");
|
|
26
26
|
return value;
|
|
27
27
|
}
|
|
28
|
+
// This illustrative service documents these fields as its public failure evidence.
|
|
29
|
+
function publicFailure(value: unknown): { code: string; message: string } | undefined {
|
|
30
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
31
|
+
const error = value as Record<string, unknown>;
|
|
32
|
+
if (typeof error.code !== "string" || typeof error.message !== "string") return undefined;
|
|
33
|
+
return { code: error.code, message: error.message.replace(/https?:\/\/\S+/giu, "[redacted-url]") };
|
|
34
|
+
}
|
|
28
35
|
function address(value: string): string {
|
|
29
36
|
const url = new URL(value);
|
|
30
37
|
if (url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1"].includes(url.hostname))) {
|
|
@@ -57,7 +64,16 @@ export function createImageProvider(options: {
|
|
|
57
64
|
...init, headers: { ...init.headers, authorization: `Bearer ${secret}` },
|
|
58
65
|
signal: AbortSignal.timeout(30_000),
|
|
59
66
|
});
|
|
60
|
-
if (!response.ok)
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
let error: ReturnType<typeof publicFailure>;
|
|
69
|
+
try { error = publicFailure(object(await response.json()).error); }
|
|
70
|
+
catch { /* A missing public error body leaves the HTTP evidence intact. */ }
|
|
71
|
+
const requestId = response.headers.get("x-request-id");
|
|
72
|
+
throw Object.assign(new Error(`Image service ${init.method ?? "GET"} ${path} returned HTTP ${response.status}`
|
|
73
|
+
+ (requestId === null ? "" : `; request=${requestId}`)
|
|
74
|
+
+ (error === undefined ? "" : `; ${error.code}: ${error.message}`)),
|
|
75
|
+
error === undefined ? {} : { code: error.code });
|
|
76
|
+
}
|
|
61
77
|
return object(await response.json());
|
|
62
78
|
}
|
|
63
79
|
const endpoint: AsyncEndpoint = {
|
|
@@ -65,7 +81,11 @@ export function createImageProvider(options: {
|
|
|
65
81
|
const supported = support(context.need);
|
|
66
82
|
if (supported.status === "unsupported") throw new Error(supported.reason);
|
|
67
83
|
const secret = key(context.credentials);
|
|
68
|
-
const
|
|
84
|
+
const authored = context.need.constraints as unknown as GenerationRequest;
|
|
85
|
+
const model = selectWireModelForRequest(mapping, authored);
|
|
86
|
+
// This service has no catalogue query; its known request limits were checked above.
|
|
87
|
+
await context.reportProgress?.({ phase: `Preparing image request: ${model}` });
|
|
88
|
+
const request = await compileWireRequest(mapping, authored,
|
|
69
89
|
async (artifact) => {
|
|
70
90
|
const bytes = await context.resources.get(artifact.resource);
|
|
71
91
|
if (bytes === undefined) throw new Error("Reference image is unavailable");
|
|
@@ -74,6 +94,7 @@ export function createImageProvider(options: {
|
|
|
74
94
|
});
|
|
75
95
|
return address(text(upload.url));
|
|
76
96
|
});
|
|
97
|
+
await context.reportProgress?.({ phase: `Submitting image request: ${model}` });
|
|
77
98
|
const task = await json("/tasks", secret, {
|
|
78
99
|
method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(request),
|
|
79
100
|
});
|
|
@@ -87,15 +108,20 @@ export function createImageProvider(options: {
|
|
|
87
108
|
if (task.state === "queued" || task.state === "running") {
|
|
88
109
|
return wakeAfter({ id }, interval, Date.now(), { phase: task.state });
|
|
89
110
|
}
|
|
90
|
-
if (task.state === "failed")
|
|
91
|
-
|
|
92
|
-
|
|
111
|
+
if (task.state === "failed") {
|
|
112
|
+
const error = publicFailure(task.error);
|
|
113
|
+
return { status: "failed", receipt: { id }, failure: {
|
|
114
|
+
code: error?.code ?? "IMAGE_SERVICE_FAILED",
|
|
115
|
+
message: `Image service task ${id} failed${error === undefined ? "" : `: ${error.message}`}`,
|
|
116
|
+
} };
|
|
117
|
+
}
|
|
93
118
|
if (task.state !== "succeeded") throw new Error("Image service returned an unknown task state");
|
|
94
119
|
return { status: "ready", handle: { id, url: address(text(task.url)) } };
|
|
95
120
|
},
|
|
96
121
|
async collect(context) {
|
|
97
122
|
// The service returns a signed asset URL; account credentials go only to its API.
|
|
98
123
|
const url = address(text(object(context.handle).url));
|
|
124
|
+
await context.reportProgress?.({ phase: "Receiving generated image" });
|
|
99
125
|
const response = await fetcher(url, { signal: AbortSignal.timeout(60_000) });
|
|
100
126
|
if (!response.ok) throw new Error(`Image download returned HTTP ${response.status}`);
|
|
101
127
|
const mediaType = response.headers.get("content-type")?.split(";")[0]?.trim();
|
|
@@ -113,8 +139,11 @@ export function createImageProvider(options: {
|
|
|
113
139
|
actionLimits: { submit: { concurrency: 1 }, poll: { concurrency: 4 }, collect: { concurrency: 1 } },
|
|
114
140
|
pricing: { kind: "page", url: `${base}/pricing` },
|
|
115
141
|
async readPricing(context) {
|
|
116
|
-
const
|
|
117
|
-
|
|
142
|
+
const model = selectWireModelForRequest(mapping, context.request.constraints as unknown as GenerationRequest,
|
|
143
|
+
context.request.pendingInputs?.map((input) => input.input));
|
|
144
|
+
const path = `/rates?model=${encodeURIComponent(model)}`;
|
|
145
|
+
const source = `${base}${path}`;
|
|
146
|
+
const rates = await json(path, key(await context.credentials()));
|
|
118
147
|
return [{ source, data: canonicalize(rates), summary: text(rates.description) }];
|
|
119
148
|
},
|
|
120
149
|
capabilities: [{ capability, returns: generationTypes.imageSet, lifecycle: "asynchronous", supports: support, endpoint }],
|
package/package.json
CHANGED
package/packages/cli/README.md
CHANGED
|
@@ -14,11 +14,28 @@ invent targets, candidates or Provider choices.
|
|
|
14
14
|
Human and JSON output answer the same command-specific question. `--json` changes encoding;
|
|
15
15
|
`--verbose` expands scope. Compiler, Runtime and Repository objects are not default reports.
|
|
16
16
|
|
|
17
|
+
Design reports for the decision they support: retain relevant facts, uncertainty and usable object
|
|
18
|
+
selectors, with direct access to omitted detail. Shorter output is useful when the next action
|
|
19
|
+
remains well-founded. Observe the command result separately from service availability; titles,
|
|
20
|
+
exit codes and JSON must preserve that distinction. A lifecycle refusal cannot become success just
|
|
21
|
+
because a health probe is down. These rules concern presentation and composition of existing
|
|
22
|
+
operations, not a prescribed production workflow.
|
|
23
|
+
|
|
17
24
|
Argument errors use `CLI_USAGE` and point to the relevant `hypit help <command>`; JSON retains that
|
|
18
25
|
command in `error.help`. Unknown help topics fail explicitly. Runtime and execution failures retain
|
|
19
26
|
their own diagnostics and optional `--debug` trace. Result pagination changes only the `--before`
|
|
20
27
|
cursor on the current query, preserving its project, Source filter and other options.
|
|
21
28
|
|
|
29
|
+
Concrete follow-up commands keep the selected project and, for execution operations, Runtime Profile.
|
|
30
|
+
Result-only commands need no Runtime. Arguments in displayed commands are quoted for POSIX shells
|
|
31
|
+
or PowerShell on Windows, including paths with spaces. The CLI formats these existing commands;
|
|
32
|
+
it does not choose which one the Agent must execute next.
|
|
33
|
+
|
|
34
|
+
`build --follow` and `status --watch` report coalesced progress through `CliIo.writeProgress` when
|
|
35
|
+
provided, including in JSON mode. The executable writes that channel to stderr; stdout remains the
|
|
36
|
+
final machine result. A stopped Worker ends observation with evidence-reading commands, without
|
|
37
|
+
promising to restart failed execution or describing the interruption as a Result storage failure.
|
|
38
|
+
|
|
22
39
|
| Command | Default scope | Explicit detail |
|
|
23
40
|
| --- | --- | --- |
|
|
24
41
|
| `check` | Validation, targets and counts | `--verbose`: exported names/types and historical references |
|
|
@@ -61,6 +78,12 @@ not select it. The pointer is a file, separate from the Profile's `dataRoot`; a
|
|
|
61
78
|
path is reported explicitly and preserved. Project selection is also available on `paths`, `doctor`, execution status/control,
|
|
62
79
|
Runtime operations, `programs` and `auth`. Machine-wide `packages` operations have no project selector.
|
|
63
80
|
|
|
81
|
+
`auth status <endpoint>` reports credential presence, write access, and the Provider's declared
|
|
82
|
+
OAuth authorization endpoint when present. This describes how a subsequent `auth login` acquires a
|
|
83
|
+
credential; it does not classify the secret already stored or verify remote account access.
|
|
84
|
+
Without browser acquisition, login uses secure input. `--from <file>` explicitly imports a secret
|
|
85
|
+
instead. Credential entry operates on an already declared Endpoint and changes no Provider or binding.
|
|
86
|
+
|
|
64
87
|
`paths` shows the effective locations and whether the Profile came from a command argument, a project
|
|
65
88
|
selection or neither. Its JSON fields `profileSource` and `selectionFile` expose that distinction; the
|
|
66
89
|
selection-file location is shown even when no selection exists. `doctor` states whether it checked
|
|
@@ -131,6 +154,10 @@ CLI, Studio and creation tools call that same package; the CLI is not another en
|
|
|
131
154
|
The same Endpoint scope reaches package preparation and Program operations. Omission means the whole
|
|
132
155
|
Profile. Build preflight instead uses the Endpoints resolved for that Build's concrete requests.
|
|
133
156
|
An unrelated offered capability does not add another credential or Program requirement.
|
|
157
|
+
Program rows show the configured Endpoint selector alongside an internal Program ID when they differ.
|
|
158
|
+
The `programs` JSON `ok` field reports whether this command succeeded; `ready` reports service
|
|
159
|
+
readiness. Successful stopping can therefore report `ok: true` with `ready: false`. A declined stop remains visible even
|
|
160
|
+
while the service is still preparing and cannot yet answer its health probe.
|
|
134
161
|
|
|
135
162
|
Upstream package installation reports an `install.log` path at preparation time. Exact package
|
|
136
163
|
releases coexist below the machine home, each with npm's own package.json and lockfile. `paths` shows
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Scope belongs to the invocation; a suggested command must keep the same target. */
|
|
2
|
+
export type CommandScope = {
|
|
3
|
+
readonly projectRoot?: string;
|
|
4
|
+
readonly runtimeProfile?: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
/** Display copyable arguments for POSIX shells or PowerShell, without expanding their contents. */
|
|
8
|
+
export function commandHint(
|
|
9
|
+
args: readonly string[],
|
|
10
|
+
scope: CommandScope = {},
|
|
11
|
+
shell: "posix" | "powershell" = process.platform === "win32" ? "powershell" : "posix",
|
|
12
|
+
): string {
|
|
13
|
+
const quote = (value: string): string => /^[a-zA-Z0-9_./:@=+-]+$/u.test(value)
|
|
14
|
+
? value
|
|
15
|
+
: `'${value.replaceAll("'", shell === "powershell" ? "''" : "'\"'\"'")}'`;
|
|
16
|
+
return ["hypit", ...args,
|
|
17
|
+
...(scope.projectRoot === undefined ? [] : ["--workspace", scope.projectRoot]),
|
|
18
|
+
...(scope.runtimeProfile === undefined ? [] : ["--runtime", scope.runtimeProfile]),
|
|
19
|
+
].map(quote).join(" ");
|
|
20
|
+
}
|
|
@@ -5,12 +5,13 @@ import type { NodeRuntimeHost } from "@hypit/runtime-host-node";
|
|
|
5
5
|
import { hypitHostPackageRoot, inspectHostPackage, prepareHostPackages } from "@hypit/runtime-host-node";
|
|
6
6
|
|
|
7
7
|
import type { CliCommand, EnvironmentCommand } from "../command.js";
|
|
8
|
+
import { commandHint } from "../command-hint.js";
|
|
8
9
|
import type { CliDistribution } from "../distribution.js";
|
|
9
10
|
import { acquireOAuthCredential } from "../oauth.js";
|
|
10
11
|
import { writeCliOutput } from "../output.js";
|
|
11
12
|
import type { CliIo } from "../output.js";
|
|
12
13
|
import { hypitHostStateRoot, hypitProjectStateRoot } from "../paths.js";
|
|
13
|
-
import type { CliManagedProgramReport, CliRuntimeController } from "../runtime-port.js";
|
|
14
|
+
import type { CliManagedProgramProgress, CliManagedProgramReport, CliRuntimeController } from "../runtime-port.js";
|
|
14
15
|
import type { OperationalWriter } from "./types.js";
|
|
15
16
|
|
|
16
17
|
function programRecord(item: CliManagedProgramReport) {
|
|
@@ -27,8 +28,12 @@ function programDescription(item: CliManagedProgramReport): string {
|
|
|
27
28
|
...(item.detail === undefined ? [] : [item.detail]),
|
|
28
29
|
])];
|
|
29
30
|
return `${item.id}: ${item.state.state}${details.length === 0 ? "" : ` — ${details.join("; ")}`}`
|
|
31
|
+
+ (item.endpoint === item.id ? "" : ` · endpoint ${item.endpoint}`)
|
|
30
32
|
+ (item.pid === undefined ? "" : ` · PID ${item.pid}`)
|
|
31
|
-
+ (item.logPath === undefined ? "" : ` · log ${item.logPath}`)
|
|
33
|
+
+ (item.logPath === undefined ? "" : ` · log ${item.logPath}`)
|
|
34
|
+
+ (item.installationLogPath === undefined || item.installationLogPath === item.logPath
|
|
35
|
+
? "" : ` · installation log ${item.installationLogPath}`)
|
|
36
|
+
+ (item.errorLogPath === undefined ? "" : ` · stderr ${item.errorLogPath}`);
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
export function isEnvironmentCommand(args: CliCommand): args is EnvironmentCommand {
|
|
@@ -58,10 +63,11 @@ export async function runEnvironmentCommand(input: {
|
|
|
58
63
|
const selectionDescription = profileSource === "argument"
|
|
59
64
|
? "command argument (this invocation only)"
|
|
60
65
|
: runtimeSelectionFile ?? "none";
|
|
61
|
-
const
|
|
66
|
+
const progressWriter = args.presentation.json ? io.writeProgress : io.writeProgress ?? io.write;
|
|
67
|
+
const reportProgramProgress = progressWriter === undefined
|
|
62
68
|
? undefined
|
|
63
|
-
: (event:
|
|
64
|
-
if (!args.presentation.verbose && event.phase !== "installing" && event.phase !== "starting") return;
|
|
69
|
+
: (event: CliManagedProgramProgress): void => {
|
|
70
|
+
if (!args.presentation.verbose && event.detail === undefined && event.phase !== "installing" && event.phase !== "starting") return;
|
|
65
71
|
const verb = {
|
|
66
72
|
checking: "Checking",
|
|
67
73
|
installing: "Installing",
|
|
@@ -69,12 +75,12 @@ export async function runEnvironmentCommand(input: {
|
|
|
69
75
|
waiting: "Waiting for",
|
|
70
76
|
ready: "Ready",
|
|
71
77
|
}[event.phase];
|
|
72
|
-
|
|
78
|
+
progressWriter(` · ${verb} ${event.id}${event.detail === undefined ? "" : ` — ${event.detail}`}${event.logPath === undefined ? "" : ` · log ${event.logPath}`}\n`);
|
|
73
79
|
};
|
|
74
|
-
const reportPackageProgress =
|
|
80
|
+
const reportPackageProgress = progressWriter === undefined
|
|
75
81
|
? undefined
|
|
76
82
|
: (event: { readonly specifier: string; readonly phase: "checking" | "installing" | "ready"; readonly logPath?: string }): void => {
|
|
77
|
-
if (event.phase === "installing")
|
|
83
|
+
if (event.phase === "installing") progressWriter(` · Installing ${event.specifier}${event.logPath === undefined ? "" : ` · log ${event.logPath}`}\n`);
|
|
78
84
|
};
|
|
79
85
|
const reportCredentialProgress = args.presentation.json
|
|
80
86
|
? io.writeProgress
|
|
@@ -183,22 +189,25 @@ export async function runEnvironmentCommand(input: {
|
|
|
183
189
|
? await controller.programs.down(args.endpoints === undefined ? {} : { endpoints: args.endpoints })
|
|
184
190
|
: await controller.programs.report(args.endpoints === undefined ? {} : { endpoints: args.endpoints });
|
|
185
191
|
const ready = result.programs.every((item) => item.state.state === "ready");
|
|
186
|
-
const desiredState = args.action === "down" ? !result.programs.some((item) => item.state.state === "ready") : ready;
|
|
187
|
-
const lifecycleOk = args.action === "status" || desiredState;
|
|
188
192
|
const needsAttention = (item: typeof result.programs[number]) => args.action === "down"
|
|
189
|
-
? item.state.state
|
|
193
|
+
? item.state.state !== "down" || (item.action !== "stopped" && item.action !== "nothing-to-stop")
|
|
194
|
+
: item.state.state !== "ready";
|
|
195
|
+
// Readiness describes the service, not whether a stop was performed. An owned process can
|
|
196
|
+
// still be loading, and another command may have declined a concurrent stop during preparation.
|
|
197
|
+
const lifecycleOk = args.action === "status" || !result.programs.some(needsAttention);
|
|
190
198
|
const relevant = result.programs.filter((item) => args.presentation.verbose || args.action === "status" || needsAttention(item));
|
|
191
199
|
const urgent = relevant.filter(needsAttention);
|
|
192
200
|
const shownPrograms = [...urgent, ...relevant.filter((item) => !needsAttention(item)).slice(0, Math.max(0, args.limit - urgent.length))];
|
|
193
201
|
const omittedPrograms = relevant.length - shownPrograms.length;
|
|
194
202
|
const title = args.action === "up"
|
|
195
|
-
?
|
|
203
|
+
? lifecycleOk ? "External programs ready" : "External programs need attention"
|
|
196
204
|
: args.action === "down"
|
|
197
|
-
?
|
|
205
|
+
? lifecycleOk ? "External programs stopped" : "External program stop needs attention"
|
|
198
206
|
: "External program status";
|
|
199
207
|
write({
|
|
200
208
|
format: "hypit.cli-programs@1",
|
|
201
209
|
action: args.action,
|
|
210
|
+
ok: lifecycleOk,
|
|
202
211
|
ready,
|
|
203
212
|
programCount: result.programs.length,
|
|
204
213
|
readyCount: result.programs.filter((item) => item.state.state === "ready").length,
|
|
@@ -278,9 +287,12 @@ export async function runEnvironmentCommand(input: {
|
|
|
278
287
|
const worker = await controller.worker.down({
|
|
279
288
|
...(args.maxWaitMs === undefined ? {} : { maxWaitMs: args.maxWaitMs }),
|
|
280
289
|
});
|
|
290
|
+
const stopped = worker.state === "stopped";
|
|
281
291
|
write({ format: "hypit.cli-runtime-down@1", worker: worker.state },
|
|
282
|
-
"Runtime Worker is down"
|
|
283
|
-
|
|
292
|
+
stopped ? "Runtime Worker is down" : "Runtime Worker is still running",
|
|
293
|
+
stopped ? "success" : "warning", [["Worker", worker.state]],
|
|
294
|
+
[`Managed programs were left running. To stop them: ${commandHint(["programs", "down"], { projectRoot, runtimeProfile: resolve(profile) })}`]);
|
|
295
|
+
if (!stopped) io.setExitCode?.(1);
|
|
284
296
|
return;
|
|
285
297
|
}
|
|
286
298
|
|
|
@@ -324,7 +336,8 @@ export async function runEnvironmentCommand(input: {
|
|
|
324
336
|
};
|
|
325
337
|
write(machine, attention
|
|
326
338
|
? "Local Runtime needs attention"
|
|
327
|
-
: ready ? "Local Runtime ready" :
|
|
339
|
+
: ready ? "Local Runtime ready" : worker.state === "running"
|
|
340
|
+
? "Runtime Worker running; Programs not ready" : "Runtime Worker stopped",
|
|
328
341
|
attention ? "warning" : ready ? "success" : "info", [
|
|
329
342
|
["Worker", worker.state],
|
|
330
343
|
["Active Builds", String(activity.builds.length)],
|
|
@@ -365,6 +378,10 @@ export async function runEnvironmentCommand(input: {
|
|
|
365
378
|
kind: item.kind,
|
|
366
379
|
configured: item.configured,
|
|
367
380
|
writable: item.writable,
|
|
381
|
+
...(item.acquisition === undefined ? {} : { acquisition: {
|
|
382
|
+
kind: item.acquisition.kind,
|
|
383
|
+
authorizationEndpoint: item.acquisition.authorizationEndpoint,
|
|
384
|
+
} }),
|
|
368
385
|
}));
|
|
369
386
|
write({
|
|
370
387
|
format: "hypit.cli-auth-status@1",
|
|
@@ -374,8 +391,12 @@ export async function runEnvironmentCommand(input: {
|
|
|
374
391
|
}, "Credential status", "info", [
|
|
375
392
|
["Endpoint", args.endpoint],
|
|
376
393
|
["Configured", `${credentials.filter((item) => item.configured).length}/${credentials.length}`],
|
|
377
|
-
], credentials.slice(0, args.limit).map((item) =>
|
|
378
|
-
|
|
394
|
+
], credentials.slice(0, args.limit).map((item) => {
|
|
395
|
+
const entry = !item.writable ? "managed by its external credential source"
|
|
396
|
+
: item.acquisition === undefined ? "login uses secure secret input"
|
|
397
|
+
: `login opens OAuth: ${item.acquisition.authorizationEndpoint}`;
|
|
398
|
+
return `${item.slot}: ${item.configured ? "configured" : "missing"} · ${item.writable ? "writable" : "read-only"} · ${entry}`;
|
|
399
|
+
}));
|
|
379
400
|
} else if (args.action === "login") {
|
|
380
401
|
const [item] = credentials;
|
|
381
402
|
if (item === undefined) throw new Error(`Endpoint ${args.endpoint} has no matching credential`);
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
|
|
1
3
|
import { readExecutionLog } from "@hypit/runtime";
|
|
2
4
|
import type { BuildResultManifest, BuildResultRepository } from "@hypit/build-result";
|
|
3
5
|
import type { NodeRuntimeHost } from "@hypit/runtime-host-node";
|
|
4
6
|
|
|
5
7
|
import type { CliCommand, ExecutionCommand } from "../command.js";
|
|
8
|
+
import { commandHint } from "../command-hint.js";
|
|
6
9
|
import { activityObservationKey, buildProgressLines, buildProgressView, observeBuildView } from "../observation.js";
|
|
7
10
|
import type { CliIo } from "../output.js";
|
|
8
11
|
import type { CliRuntimeController } from "../runtime-port.js";
|
|
@@ -37,6 +40,7 @@ export function isExecutionCommand(args: CliCommand): args is ExecutionCommand {
|
|
|
37
40
|
/** Inspect or control accepted Build work. Observation never owns execution. */
|
|
38
41
|
export async function runExecutionCommand(input: {
|
|
39
42
|
readonly args: ExecutionCommand;
|
|
43
|
+
readonly projectRoot: string;
|
|
40
44
|
readonly runtimeProfile: string | undefined;
|
|
41
45
|
readonly resolveProjectRuntime?: () => Promise<string | undefined>;
|
|
42
46
|
readonly io: CliIo;
|
|
@@ -46,6 +50,10 @@ export async function runExecutionCommand(input: {
|
|
|
46
50
|
readonly write: OperationalWriter;
|
|
47
51
|
}): Promise<void> {
|
|
48
52
|
const { args, runtimeProfile, io, runtimeHost, runtimeController, openProjectResults, write } = input;
|
|
53
|
+
const commandScope = {
|
|
54
|
+
projectRoot: input.projectRoot,
|
|
55
|
+
...(runtimeProfile === undefined ? {} : { runtimeProfile: resolve(runtimeProfile) }),
|
|
56
|
+
};
|
|
49
57
|
if (args.command === "logs") {
|
|
50
58
|
const opened = await openProjectResults();
|
|
51
59
|
let source: "runtime" | "result" | "unavailable" = "unavailable";
|
|
@@ -164,7 +172,7 @@ export async function runExecutionCommand(input: {
|
|
|
164
172
|
controller.worker.status(),
|
|
165
173
|
]);
|
|
166
174
|
const builds = activity.builds.slice(0, args.limit).map((item) => {
|
|
167
|
-
const status = buildStatusView({ id: item.id, runtime: item });
|
|
175
|
+
const status = buildStatusView({ id: item.id, runtime: item, commandScope });
|
|
168
176
|
return {
|
|
169
177
|
id: item.id,
|
|
170
178
|
work: status.work,
|
|
@@ -219,23 +227,23 @@ export async function runExecutionCommand(input: {
|
|
|
219
227
|
let result: BuildResultManifest | undefined;
|
|
220
228
|
let resultReadError: string | undefined;
|
|
221
229
|
let openedResults: Awaited<ReturnType<OpenProjectResults>> | undefined;
|
|
230
|
+
if (args.watch && view !== undefined && view.issue === undefined) {
|
|
231
|
+
const controller = await runtimeController(runtimeProfile);
|
|
232
|
+
view = await observeBuildView(runtime, args.build, view, {
|
|
233
|
+
...(args.maxWaitMs === undefined ? {} : { maxWaitMs: args.maxWaitMs }),
|
|
234
|
+
controller,
|
|
235
|
+
commandScope,
|
|
236
|
+
onProgress: (progress) => {
|
|
237
|
+
const report = io.writeProgress ?? (args.presentation.json ? undefined : io.write);
|
|
238
|
+
for (const line of buildProgressLines(progress, {
|
|
239
|
+
verbose: args.presentation.verbose,
|
|
240
|
+
limit: args.limit,
|
|
241
|
+
})) report?.(`${line}\n`);
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
}
|
|
222
245
|
try {
|
|
223
246
|
openedResults = await openProjectResults();
|
|
224
|
-
if (args.watch && view !== undefined && view.issue === undefined) {
|
|
225
|
-
const controller = await runtimeController(runtimeProfile);
|
|
226
|
-
view = await observeBuildView(runtime, args.build, view, {
|
|
227
|
-
...(args.maxWaitMs === undefined ? {} : { maxWaitMs: args.maxWaitMs }),
|
|
228
|
-
controller,
|
|
229
|
-
...(args.presentation.json ? {} : {
|
|
230
|
-
onProgress: (progress) => {
|
|
231
|
-
for (const line of buildProgressLines(progress, {
|
|
232
|
-
verbose: args.presentation.verbose,
|
|
233
|
-
limit: args.limit,
|
|
234
|
-
})) io.write(`${line}\n`);
|
|
235
|
-
},
|
|
236
|
-
}),
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
247
|
result = await openedResults.repository.read(args.build);
|
|
240
248
|
} catch (error) {
|
|
241
249
|
resultReadError = error instanceof Error ? error.message : String(error);
|
|
@@ -250,6 +258,7 @@ export async function runExecutionCommand(input: {
|
|
|
250
258
|
const issue = view?.issue;
|
|
251
259
|
const build = !found ? null : buildStatusView({
|
|
252
260
|
id: view?.id ?? result!.id,
|
|
261
|
+
commandScope,
|
|
253
262
|
...(view === undefined ? {} : { runtime: view }),
|
|
254
263
|
...(result === undefined ? {} : { result }),
|
|
255
264
|
...(resultReadError === undefined ? {} : { resultReadError }),
|
|
@@ -338,7 +347,7 @@ export async function runExecutionCommand(input: {
|
|
|
338
347
|
outcome: finished.outcome,
|
|
339
348
|
...(finished.issue === undefined ? {} : { attention: {
|
|
340
349
|
message: finished.issue.message,
|
|
341
|
-
action:
|
|
350
|
+
action: commandHint(["result", "finish", args.build], commandScope),
|
|
342
351
|
} }),
|
|
343
352
|
}, finished.issue === undefined ? "Result finished" : "Result still needs attention",
|
|
344
353
|
finished.issue === undefined ? "success" : "error", [
|
|
@@ -356,6 +365,7 @@ export async function runExecutionCommand(input: {
|
|
|
356
365
|
: await openedResults.repository.read(args.build).finally(async () => await openedResults.close());
|
|
357
366
|
const build = active === undefined && finished === undefined ? null : buildStatusView({
|
|
358
367
|
id: args.build,
|
|
368
|
+
commandScope,
|
|
359
369
|
...(active === undefined ? {} : { runtime: active }),
|
|
360
370
|
...(finished === undefined ? {} : { result: finished }),
|
|
361
371
|
});
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
projectPath,
|
|
14
14
|
} from "../view.js";
|
|
15
15
|
import type { OperationalWriter } from "./types.js";
|
|
16
|
+
import { commandHint } from "../command-hint.js";
|
|
16
17
|
|
|
17
18
|
export function isProjectResultCommand(args: CliCommand): args is ProjectResultCommand {
|
|
18
19
|
return args.command === "builds" || args.command === "history" || args.command === "inspect"
|
|
@@ -120,7 +121,7 @@ export async function runProjectResultCommand(input: {
|
|
|
120
121
|
] : []),
|
|
121
122
|
], [
|
|
122
123
|
...(build.failure === undefined ? [] : [`Reason ${build.failure}`]),
|
|
123
|
-
...(build.executionLog === undefined ? [] : [`Execution log
|
|
124
|
+
...(build.executionLog === undefined ? [] : [`Execution log ${commandHint(["logs", build.id], { projectRoot })}`]),
|
|
124
125
|
...(build.operations ?? []).map((operation) =>
|
|
125
126
|
`${operation.endpoint}: ${operation.status}${operation.receipt === undefined ? "" : ` · task ${operation.receipt.id}`}`
|
|
126
127
|
+ (operation.failure === undefined ? "" : ` · ${operation.failure.code}: ${operation.failure.message}`)),
|
|
@@ -9,6 +9,8 @@ type ProgramStateView = {
|
|
|
9
9
|
readonly detail?: string;
|
|
10
10
|
readonly pid?: number;
|
|
11
11
|
readonly logPath?: string;
|
|
12
|
+
readonly installationLogPath?: string;
|
|
13
|
+
readonly errorLogPath?: string;
|
|
12
14
|
};
|
|
13
15
|
type AttentionView = { readonly message: string; readonly action?: string };
|
|
14
16
|
|
|
@@ -18,12 +20,12 @@ export type OperationalMachineView =
|
|
|
18
20
|
| { readonly format: "hypit.cli-runtime-selection@1"; readonly selected: boolean; readonly removed?: boolean; readonly profile?: string; readonly project?: string }
|
|
19
21
|
| { readonly format: "hypit.cli-paths@1"; readonly project: string; readonly projectState: string; readonly profileSource: "argument" | "project" | "none"; readonly selectionFile: string; readonly profile?: string; readonly runtimeData?: string; readonly hostState: string; readonly machinePackages: string; readonly distribution?: string }
|
|
20
22
|
| { readonly format: "hypit.cli-package@1"; readonly action: "install" | "status"; readonly package: string; readonly ready: boolean }
|
|
21
|
-
| { readonly format: "hypit.cli-programs@1"; readonly action: "up" | "down" | "status"; readonly ready: boolean; readonly programCount: number; readonly readyCount: number; readonly programs: readonly ProgramStateView[]; readonly omittedPrograms?: number }
|
|
23
|
+
| { readonly format: "hypit.cli-programs@1"; readonly action: "up" | "down" | "status"; readonly ok: boolean; readonly ready: boolean; readonly programCount: number; readonly readyCount: number; readonly programs: readonly ProgramStateView[]; readonly omittedPrograms?: number }
|
|
22
24
|
| { readonly format: "hypit.cli-runtime-up@1"; readonly ready: boolean; readonly worker: string; readonly preparedPackages: number; readonly programs: { readonly total: number; readonly ready: number; readonly items: readonly ProgramStateView[] } }
|
|
23
25
|
| { readonly format: "hypit.cli-runtime-logs@1"; readonly lines: readonly string[]; readonly totalLines: number; readonly omittedLines: number; readonly path?: string }
|
|
24
26
|
| { readonly format: "hypit.cli-runtime-down@1"; readonly worker: string }
|
|
25
27
|
| { readonly format: "hypit.cli-runtime-status@1"; readonly ready: boolean; readonly attention: boolean; readonly worker: { readonly state: string; readonly configuration?: string }; readonly builds: Readonly<Record<string, number>>; readonly programs: { readonly total: number; readonly ready: number; readonly unavailable: readonly ProgramStateView[] }; readonly capacity: { readonly active: number } }
|
|
26
|
-
| { readonly format: "hypit.cli-auth-status@1"; readonly endpoint: string; readonly credentials: readonly { readonly endpoint: string; readonly slot: string; readonly label: string; readonly kind: string; readonly configured: boolean; readonly writable: boolean }[]; readonly omittedCredentials?: number }
|
|
28
|
+
| { readonly format: "hypit.cli-auth-status@1"; readonly endpoint: string; readonly credentials: readonly { readonly endpoint: string; readonly slot: string; readonly label: string; readonly kind: string; readonly configured: boolean; readonly writable: boolean; readonly acquisition?: { readonly kind: string; readonly authorizationEndpoint: string } }[]; readonly omittedCredentials?: number }
|
|
27
29
|
| { readonly format: "hypit.cli-auth-change@1"; readonly endpoint: string; readonly slot: string; readonly configured: boolean; readonly changed?: boolean }
|
|
28
30
|
| { readonly format: "hypit.cli-builds@1"; readonly builds: readonly { readonly id: string; readonly createdAt: string; readonly title?: string; readonly outcome: string; readonly run?: string; readonly targetCount: number; readonly targets?: readonly string[]; readonly omittedTargets?: number; readonly outputCount: number }[]; readonly next?: string }
|
|
29
31
|
| { readonly format: "hypit.cli-history@1"; readonly output: string; readonly source?: string; readonly entries: readonly { readonly build: string; readonly title?: string; readonly createdAt: string; readonly outcome: string; readonly output: CliOutputView }[]; readonly next?: string }
|