@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.
Files changed (44) 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 +27 -0
  8. package/packages/cli/src/command-hint.ts +20 -0
  9. package/packages/cli/src/commands/environment.ts +39 -18
  10. package/packages/cli/src/commands/execution.ts +27 -17
  11. package/packages/cli/src/commands/results.ts +2 -1
  12. package/packages/cli/src/machine-view.ts +4 -2
  13. package/packages/cli/src/main.ts +23 -22
  14. package/packages/cli/src/observation.ts +7 -2
  15. package/packages/cli/src/output.ts +3 -0
  16. package/packages/cli/src/view.ts +4 -1
  17. package/packages/driver-node/README.md +5 -0
  18. package/packages/driver-node/src/driver.ts +20 -15
  19. package/packages/endpoint-kit/README.md +11 -2
  20. package/packages/endpoint-kit/src/index.ts +1 -1
  21. package/packages/generation/README.md +10 -0
  22. package/packages/provider-hypihub/README.md +41 -6
  23. package/packages/provider-hypihub/src/errors.ts +61 -0
  24. package/packages/provider-hypihub/src/mapping.ts +10 -25
  25. package/packages/provider-hypihub/src/oauth.ts +4 -1
  26. package/packages/provider-hypihub/src/provider.ts +79 -74
  27. package/packages/provider-hypihub/src/routes.ts +24 -3
  28. package/packages/provider-hypihub/src/upload.ts +8 -18
  29. package/packages/provider-whisperx-local/README.md +7 -1
  30. package/packages/provider-whisperx-local/src/program.ts +2 -0
  31. package/packages/runtime-host-node/src/index.ts +4 -0
  32. package/packages/runtime-kit/README.md +3 -0
  33. package/packages/runtime-kit/src/index.ts +2 -0
  34. package/packages/runtime-local/README.md +17 -0
  35. package/packages/runtime-local/package.json +2 -1
  36. package/packages/runtime-local/src/program-lock.ts +40 -0
  37. package/packages/runtime-local/src/programs.ts +156 -79
  38. package/packages/video-cli/README.md +33 -6
  39. package/packages/video-cli/src/cli.ts +4 -1
  40. package/packages/video-cli/src/creation.ts +7 -2
  41. package/packages/video-cli/src/index.ts +2 -0
  42. package/packages/video-cli/src/version.ts +90 -0
  43. package/services/whisperx/README.md +10 -3
  44. 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.8",
3
+ "version": "0.1.9",
4
4
  "homepage": "https://hypit.ai",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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 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";
@@ -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: `hypit result finish ${args.build}`,
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 hypit logs ${build.id}`]),
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 }