@hypit/hypit 0.1.7 → 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.
Files changed (46) hide show
  1. package/README.md +15 -4
  2. package/dist/public/endpoint-kit.d.ts +1 -1
  3. package/dist/public/runtime-kit.d.ts +3 -1
  4. package/examples/provider-package/README.md +12 -0
  5. package/examples/provider-package/packages/provider-images/src/provider.ts +37 -8
  6. package/package.json +1 -1
  7. package/packages/cli/README.md +35 -0
  8. package/packages/cli/src/arguments.ts +12 -1
  9. package/packages/cli/src/command-hint.ts +20 -0
  10. package/packages/cli/src/commands/environment.ts +39 -18
  11. package/packages/cli/src/commands/execution.ts +34 -18
  12. package/packages/cli/src/commands/results.ts +8 -3
  13. package/packages/cli/src/machine-view.ts +4 -2
  14. package/packages/cli/src/main.ts +25 -23
  15. package/packages/cli/src/observation.ts +7 -2
  16. package/packages/cli/src/output.ts +8 -0
  17. package/packages/cli/src/usage-error.ts +8 -0
  18. package/packages/cli/src/view.ts +4 -1
  19. package/packages/driver-node/README.md +5 -0
  20. package/packages/driver-node/src/driver.ts +20 -15
  21. package/packages/endpoint-kit/README.md +11 -2
  22. package/packages/endpoint-kit/src/index.ts +1 -1
  23. package/packages/generation/README.md +10 -0
  24. package/packages/provider-hypihub/README.md +41 -6
  25. package/packages/provider-hypihub/src/errors.ts +61 -0
  26. package/packages/provider-hypihub/src/mapping.ts +10 -25
  27. package/packages/provider-hypihub/src/oauth.ts +4 -1
  28. package/packages/provider-hypihub/src/provider.ts +79 -74
  29. package/packages/provider-hypihub/src/routes.ts +24 -3
  30. package/packages/provider-hypihub/src/upload.ts +8 -18
  31. package/packages/provider-whisperx-local/README.md +7 -1
  32. package/packages/provider-whisperx-local/src/program.ts +2 -0
  33. package/packages/runtime-host-node/src/index.ts +4 -0
  34. package/packages/runtime-kit/README.md +3 -0
  35. package/packages/runtime-kit/src/index.ts +2 -0
  36. package/packages/runtime-local/README.md +17 -0
  37. package/packages/runtime-local/package.json +2 -1
  38. package/packages/runtime-local/src/program-lock.ts +40 -0
  39. package/packages/runtime-local/src/programs.ts +156 -79
  40. package/packages/video-cli/README.md +33 -6
  41. package/packages/video-cli/src/cli.ts +4 -1
  42. package/packages/video-cli/src/creation.ts +7 -2
  43. package/packages/video-cli/src/index.ts +2 -0
  44. package/packages/video-cli/src/version.ts +90 -0
  45. package/services/whisperx/README.md +10 -3
  46. 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 single model, so a video can cost exactly $0.
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
  ![SVML source and live video preview](https://storage.googleapis.com/hypit-public-assets/showcase/npm/2026-09-10/demo-compact.gif)
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 second video costs almost nothing; the hundredth is a loop.
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, $0:** no seat pricing, no per-render fees, no watermark. Arcads charges $220/mo, Creatify $39, before you render anything — but Hypit is $0.
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. Compiled from source, zero API calls, 0 dollars a video.
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
+ [![Star History Chart](https://api.star-history.com/svg?repos=hypit-ai/hypit&type=Date)](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 of a long local call without creating a remote Operation. */
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 of a long local call without creating a remote Operation. */
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) throw new Error(`Image service ${path} returned HTTP ${response.status}`);
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 request = await compileWireRequest(mapping, context.need.constraints as unknown as GenerationRequest,
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") return {
91
- status: "failed", failure: { code: "IMAGE_SERVICE_FAILED", message: "Image service task failed" },
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 source = `${base}/rates?model=gpt-image-2`;
117
- const rates = await json("/rates?model=gpt-image-2", key(await context.credentials()));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypit/hypit",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "homepage": "https://hypit.ai",
5
5
  "repository": {
6
6
  "type": "git",
@@ -14,6 +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
+
24
+ Argument errors use `CLI_USAGE` and point to the relevant `hypit help <command>`; JSON retains that
25
+ command in `error.help`. Unknown help topics fail explicitly. Runtime and execution failures retain
26
+ their own diagnostics and optional `--debug` trace. Result pagination changes only the `--before`
27
+ cursor on the current query, preserving its project, Source filter and other options.
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
+
17
39
  | Command | Default scope | Explicit detail |
18
40
  | --- | --- | --- |
19
41
  | `check` | Validation, targets and counts | `--verbose`: exported names/types and historical references |
@@ -56,6 +78,12 @@ not select it. The pointer is a file, separate from the Profile's `dataRoot`; a
56
78
  path is reported explicitly and preserved. Project selection is also available on `paths`, `doctor`, execution status/control,
57
79
  Runtime operations, `programs` and `auth`. Machine-wide `packages` operations have no project selector.
58
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
+
59
87
  `paths` shows the effective locations and whether the Profile came from a command argument, a project
60
88
  selection or neither. Its JSON fields `profileSource` and `selectionFile` expose that distinction; the
61
89
  selection-file location is shown even when no selection exists. `doctor` states whether it checked
@@ -113,6 +141,9 @@ flow.
113
141
  execution phases and Provider diagnostics. It reads a finished Result directly, without opening the
114
142
  Runtime; an active Build is read through Runtime control. The selected Repository handles file access.
115
143
  `--lines` limits the tail and the report states the omitted count; JSON carries records plus that count.
144
+ An unavailable log reports `source: "unavailable"` and exits unsuccessfully; a readable log with zero
145
+ records is a successful empty result. The human report distinguishes a finished Result with no log
146
+ from a lookup that still needs the Build's Runtime or correct project selection.
116
147
  `inspect` exposes an available log separately from authored Outputs. `hypit runtime logs` reads the
117
148
  Worker process log instead, for Runtime startup or process-level failures.
118
149
 
@@ -123,6 +154,10 @@ CLI, Studio and creation tools call that same package; the CLI is not another en
123
154
  The same Endpoint scope reaches package preparation and Program operations. Omission means the whole
124
155
  Profile. Build preflight instead uses the Endpoints resolved for that Build's concrete requests.
125
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.
126
161
 
127
162
  Upstream package installation reports an `install.log` path at preparation time. Exact package
128
163
  releases coexist below the machine home, each with npm's own package.json and lockfile. `paths` shows
@@ -13,6 +13,7 @@ import type {
13
13
  RuntimeSelectionCommand,
14
14
  } from "./command.js";
15
15
  import type { CliColorMode, CliOutputOptions } from "./output.js";
16
+ import { CliUsageError } from "./usage-error.js";
16
17
 
17
18
  type RawOptions = {
18
19
  readonly presentation: CliOutputOptions;
@@ -48,6 +49,15 @@ type RawOptions = {
48
49
  const commonOptions = ["--json", "--color", "--no-color", "--verbose", "--debug"] as const;
49
50
 
50
51
  export function parseCommand(argv: readonly string[]): CliCommand {
52
+ try { return parseArguments(argv); }
53
+ catch (error) {
54
+ if (error instanceof CliUsageError) throw error;
55
+ throw new CliUsageError(error instanceof Error ? error.message : String(error),
56
+ argv[0] === undefined ? "hypit help" : `hypit help ${argv[0]}`, { cause: error });
57
+ }
58
+ }
59
+
60
+ function parseArguments(argv: readonly string[]): CliCommand {
51
61
  const [command, ...tail] = argv;
52
62
  switch (command) {
53
63
  case "check": {
@@ -246,7 +256,8 @@ export function parseCommand(argv: readonly string[]): CliCommand {
246
256
  ...optionalPackageRoot(options),
247
257
  };
248
258
  }
249
- default: throw new Error(usage());
259
+ default: throw new CliUsageError(command === undefined ? "A command is required"
260
+ : `Unknown command ${JSON.stringify(command)}`, "hypit help");
250
261
  }
251
262
  }
252
263
 
@@ -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 reportProgramProgress = args.presentation.json
66
+ const progressWriter = args.presentation.json ? io.writeProgress : io.writeProgress ?? io.write;
67
+ const reportProgramProgress = progressWriter === undefined
62
68
  ? undefined
63
- : (event: { readonly id: string; readonly phase: "checking" | "installing" | "starting" | "waiting" | "ready"; readonly logPath?: string }): void => {
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
- io.write(` · ${verb} ${event.id}${event.logPath === undefined ? "" : ` · log ${event.logPath}`}\n`);
78
+ progressWriter(` · ${verb} ${event.id}${event.detail === undefined ? "" : ` — ${event.detail}`}${event.logPath === undefined ? "" : ` · log ${event.logPath}`}\n`);
73
79
  };
74
- const reportPackageProgress = args.presentation.json
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") io.write(` · Installing ${event.specifier}${event.logPath === undefined ? "" : ` · log ${event.logPath}`}\n`);
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 === "ready" : item.state.state !== "ready";
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
- ? desiredState ? "External programs ready" : "External programs need attention"
203
+ ? lifecycleOk ? "External programs ready" : "External programs need attention"
196
204
  : args.action === "down"
197
- ? desiredState ? "External programs stopped" : "Some external programs are still running"
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", "success", [["Worker", worker.state]],
283
- ["Managed programs were left running. Stop them explicitly with hypit programs down."]);
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" : "Runtime Worker stopped",
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
- `${item.slot}: ${item.configured ? "configured" : "missing"} · ${item.writable ? "writable" : "read-only"}`));
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";
@@ -83,13 +91,19 @@ export async function runExecutionCommand(input: {
83
91
  const records = view?.records ?? [];
84
92
  const omitted = (view?.total ?? 0) - records.length;
85
93
  write({ format: "hypit.cli-logs@1", build: args.build, source, records, omittedRecords: omitted },
86
- view === undefined ? "No execution log recorded" : "Build execution log", "info",
94
+ view === undefined ? "Execution log unavailable" : "Build execution log", view === undefined ? "warning" : "info",
87
95
  [["Build", args.build], ["Source", source]], [
96
+ ...(view !== undefined ? [] : [finished
97
+ ? "This Result has no saved execution log."
98
+ : activeProfile === undefined
99
+ ? "No saved log found in this project. Select the Build's Runtime with --runtime <profile> to check active execution."
100
+ : "No log found in this project's Results or the selected Runtime. Check the Build id and project selection."]),
88
101
  ...(omitted > 0 ? [`Showing last ${records.length} records; ${omitted} earlier records omitted. Use --lines to read more.`] : []),
89
102
  ...records.map((record) => `${new Date(record.time).toISOString()} ${record.endpoint} ${record.command} ${
90
103
  record.kind === "phase" ? record.phase : record.kind === "diagnostic" || record.kind === "failed"
91
104
  ? `${record.kind}: ${record.message}` : record.kind}`),
92
105
  ]);
106
+ if (view === undefined) io.setExitCode?.(1);
93
107
  return;
94
108
  }
95
109
 
@@ -158,7 +172,7 @@ export async function runExecutionCommand(input: {
158
172
  controller.worker.status(),
159
173
  ]);
160
174
  const builds = activity.builds.slice(0, args.limit).map((item) => {
161
- const status = buildStatusView({ id: item.id, runtime: item });
175
+ const status = buildStatusView({ id: item.id, runtime: item, commandScope });
162
176
  return {
163
177
  id: item.id,
164
178
  work: status.work,
@@ -213,23 +227,23 @@ export async function runExecutionCommand(input: {
213
227
  let result: BuildResultManifest | undefined;
214
228
  let resultReadError: string | undefined;
215
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
+ }
216
245
  try {
217
246
  openedResults = await openProjectResults();
218
- if (args.watch && view !== undefined && view.issue === undefined) {
219
- const controller = await runtimeController(runtimeProfile);
220
- view = await observeBuildView(runtime, args.build, view, {
221
- ...(args.maxWaitMs === undefined ? {} : { maxWaitMs: args.maxWaitMs }),
222
- controller,
223
- ...(args.presentation.json ? {} : {
224
- onProgress: (progress) => {
225
- for (const line of buildProgressLines(progress, {
226
- verbose: args.presentation.verbose,
227
- limit: args.limit,
228
- })) io.write(`${line}\n`);
229
- },
230
- }),
231
- });
232
- }
233
247
  result = await openedResults.repository.read(args.build);
234
248
  } catch (error) {
235
249
  resultReadError = error instanceof Error ? error.message : String(error);
@@ -244,6 +258,7 @@ export async function runExecutionCommand(input: {
244
258
  const issue = view?.issue;
245
259
  const build = !found ? null : buildStatusView({
246
260
  id: view?.id ?? result!.id,
261
+ commandScope,
247
262
  ...(view === undefined ? {} : { runtime: view }),
248
263
  ...(result === undefined ? {} : { result }),
249
264
  ...(resultReadError === undefined ? {} : { resultReadError }),
@@ -332,7 +347,7 @@ export async function runExecutionCommand(input: {
332
347
  outcome: finished.outcome,
333
348
  ...(finished.issue === undefined ? {} : { attention: {
334
349
  message: finished.issue.message,
335
- action: `hypit result finish ${args.build}`,
350
+ action: commandHint(["result", "finish", args.build], commandScope),
336
351
  } }),
337
352
  }, finished.issue === undefined ? "Result finished" : "Result still needs attention",
338
353
  finished.issue === undefined ? "success" : "error", [
@@ -350,6 +365,7 @@ export async function runExecutionCommand(input: {
350
365
  : await openedResults.repository.read(args.build).finally(async () => await openedResults.close());
351
366
  const build = active === undefined && finished === undefined ? null : buildStatusView({
352
367
  id: args.build,
368
+ commandScope,
353
369
  ...(active === undefined ? {} : { runtime: active }),
354
370
  ...(finished === undefined ? {} : { result: finished }),
355
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"
@@ -54,7 +55,9 @@ export async function runProjectResultCommand(input: {
54
55
  const label = item.title === undefined ? item.id : `${item.title} · ${item.id}`;
55
56
  const run = item.run === undefined ? "" : ` · ${item.run}`;
56
57
  return `${label}: ${item.outcome} · ${new Date(item.createdAt).toLocaleString()}${run} · ${item.targetCount} target${item.targetCount === 1 ? "" : "s"}`;
57
- }).concat(page.next === undefined ? [] : [`Older hypit builds --before ${page.next}`]));
58
+ }).concat(page.next === undefined ? [] : [
59
+ `Older Repeat this command with --before ${page.next}, keeping the other options.`,
60
+ ]));
58
61
  return;
59
62
  }
60
63
 
@@ -89,7 +92,9 @@ export async function runProjectResultCommand(input: {
89
92
  const label = item.title === undefined ? item.build : `${item.title} · ${item.build}`;
90
93
  return `${label}: ${item.outcome} · ${new Date(item.createdAt).toLocaleString()}`
91
94
  + (args.presentation.verbose ? ` · ${item.output.kind} · ${item.output.type}` : "");
92
- }).concat(page.next === undefined ? [] : [`Older hypit history ${args.outputName} --before ${page.next}`]));
95
+ }).concat(page.next === undefined ? [] : [
96
+ `Older Repeat this command with --before ${page.next}, keeping the other options.`,
97
+ ]));
93
98
  return;
94
99
  }
95
100
 
@@ -116,7 +121,7 @@ export async function runProjectResultCommand(input: {
116
121
  ] : []),
117
122
  ], [
118
123
  ...(build.failure === undefined ? [] : [`Reason ${build.failure}`]),
119
- ...(build.executionLog === undefined ? [] : [`Execution log hypit logs ${build.id}`]),
124
+ ...(build.executionLog === undefined ? [] : [`Execution log ${commandHint(["logs", build.id], { projectRoot })}`]),
120
125
  ...(build.operations ?? []).map((operation) =>
121
126
  `${operation.endpoint}: ${operation.status}${operation.receipt === undefined ? "" : ` · task ${operation.receipt.id}`}`
122
127
  + (operation.failure === undefined ? "" : ` · ${operation.failure.code}: ${operation.failure.message}`)),