@hypit/hypit 0.2.1 → 0.2.2

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 CHANGED
@@ -147,6 +147,28 @@ commands and the repository layout.
147
147
  ## Launch Partner
148
148
 
149
149
  <table>
150
+ <tr>
151
+ <td width="220" align="center">
152
+ <a href="https://watcha.cn">
153
+ <picture>
154
+ <source media="(prefers-color-scheme: dark)" srcset="./docs/public/watcha-dark.png">
155
+ <img alt="Watcha" src="./docs/public/watcha-light.png" width="170">
156
+ </picture>
157
+ </a>
158
+ </td>
159
+ <td>Watcha is an innovative ecosystem and community built for AI builders, connecting great products with high-quality users to turn every Demo into reality. Starting from AI product discovery and genuine feedback, Watcha provides product publishing, distribution, and reputation tracking to support builders from launch to early adoption.</td>
160
+ </tr>
161
+ <tr>
162
+ <td width="220" align="center">
163
+ <a href="https://tokendance.space/?utm_source=hypit">
164
+ <picture>
165
+ <source media="(prefers-color-scheme: dark)" srcset="./docs/public/tokendance-dark.png">
166
+ <img alt="TokenDance" src="./docs/public/tokendance-light.png" width="165">
167
+ </picture>
168
+ </a>
169
+ </td>
170
+ <td>TokenDance offers builders a unified multi-model gateway with optimized inference costs. Through programs like the Billion-Token Subsidy and the Developer Program, TokenDance helps early-stage builders cut costs so they can focus on product validation and growth.</td>
171
+ </tr>
150
172
  <tr>
151
173
  <td width="220" align="center">
152
174
  <a href="https://openagents.org/blog/posts/2026-09-08-pilot-user-program-zh-hypit"><img alt="OpenAgents" src="./docs/public/openagents.png" width="180"></a>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypit/hypit",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "homepage": "https://hypit.ai",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,6 +17,6 @@
17
17
  "@hypit/studio-adapter": "workspace:*",
18
18
  "@hypit/fonts-open": "workspace:*",
19
19
  "@hypit/caption": "workspace:*",
20
- "@hypit/timeline": "workspace:*"
20
+ "@hypit/temporal": "workspace:*"
21
21
  }
22
22
  }
@@ -172,7 +172,9 @@ export async function runEnvironmentCommand(input: {
172
172
  }
173
173
 
174
174
  if (args.command === "programs") {
175
- if (runtimeProfile === undefined) throw new Error("programs requires a Runtime Profile");
175
+ if (runtimeProfile === undefined) {
176
+ throw new Error("programs requires a Runtime; run hypit runtime init, select one with runtime use, or pass --runtime <profile>");
177
+ }
176
178
  const profile = resolve(runtimeProfile);
177
179
  const host = await runtimeHost(profile);
178
180
  if (args.action === "up" || args.action === "prepare") {
@@ -195,11 +197,13 @@ export async function runEnvironmentCommand(input: {
195
197
  : await controller.programs.report(args.endpoints === undefined ? {} : { endpoints: args.endpoints });
196
198
  const ready = result.programs.every((item) => item.state.state === "ready");
197
199
  const needsAttention = (item: typeof result.programs[number]) => args.action === "down"
198
- ? item.state.state !== "down" || (item.action !== "stopped" && item.action !== "nothing-to-stop")
200
+ ? item.action !== "nothing-to-stop"
201
+ && (item.state.state !== "down" || item.action !== "stopped")
199
202
  : item.state.state !== "ready";
200
203
  // Readiness describes the service, not whether a stop was performed. An owned process can
201
204
  // still be loading, and another command may have declined a concurrent stop during preparation.
202
205
  const lifecycleOk = args.action === "status" || !result.programs.some(needsAttention);
206
+ const stoppedAny = result.programs.some((item) => item.action === "stopped");
203
207
  const relevant = result.programs.filter((item) => args.presentation.verbose || args.action === "status" || needsAttention(item));
204
208
  const urgent = relevant.filter(needsAttention);
205
209
  const shownPrograms = [...urgent, ...relevant.filter((item) => !needsAttention(item)).slice(0, Math.max(0, args.limit - urgent.length))];
@@ -209,7 +213,8 @@ export async function runEnvironmentCommand(input: {
209
213
  : args.action === "up"
210
214
  ? lifecycleOk ? "External programs ready" : "External programs need attention"
211
215
  : args.action === "down"
212
- ? lifecycleOk ? "External programs stopped" : "External program stop needs attention"
216
+ ? lifecycleOk ? stoppedAny ? "External programs stopped" : "No external programs to stop"
217
+ : "External program stop needs attention"
213
218
  : "External program status";
214
219
  write({
215
220
  format: "hypit.cli-programs@1",
@@ -232,7 +237,9 @@ export async function runEnvironmentCommand(input: {
232
237
  }
233
238
 
234
239
  if (args.command === "runtime") {
235
- if (runtimeProfile === undefined) throw new Error("runtime requires a Runtime Profile");
240
+ if (runtimeProfile === undefined) {
241
+ throw new Error("runtime requires a Runtime; run hypit runtime init, select one with runtime use, or pass --runtime <profile>");
242
+ }
236
243
  const profile = resolve(runtimeProfile);
237
244
  const controller = await runtimeController(profile);
238
245
  if (args.action === "up") {
@@ -298,7 +305,7 @@ export async function runEnvironmentCommand(input: {
298
305
  write({ format: "hypit.cli-runtime-down@1", worker: worker.state },
299
306
  stopped ? "Runtime Worker is down" : "Runtime Worker is still running",
300
307
  stopped ? "success" : "warning", [["Worker", worker.state]],
301
- [`Managed programs were left running. To stop them: ${commandHint(["programs", "down"], { projectRoot, runtimeProfile: resolve(profile) })}`]);
308
+ [`Managed Programs are unchanged. To stop processes started by Hypit: ${commandHint(["programs", "down"], { projectRoot, runtimeProfile: resolve(profile) })}`]);
302
309
  if (!stopped) io.setExitCode?.(1);
303
310
  return;
304
311
  }
@@ -1015,7 +1015,7 @@ export function writeCliHelp(io: CliIo, topic?: string): void {
1015
1015
  row("doctor [profile]", "diagnose selected external setup"),
1016
1016
  row("runtime init|use|unset", "create or select this project's Runtime Profile"),
1017
1017
  row("runtime up|status|logs|down", "prepare and manage the local Build Runtime"),
1018
- row("programs up|status|down", "manage declared external programs only"),
1018
+ row("programs prepare|up|status|down", "manage declared external programs only"),
1019
1019
  row("packages install|status", "manage pinned upstream packages in the machine home"),
1020
1020
  row("activity [--watch]", "show active Builds and their current phases"),
1021
1021
  row("cancel <build-id>", "withdraw one active Build"),
@@ -1,10 +1,10 @@
1
- import { join, resolve } from "node:path";
2
1
  import {
3
2
  createRuntimeCredentialStoreAdapterFacet,
4
3
  runtimeConfigExact,
5
4
  runtimeConfigObject,
6
5
  runtimeConfigString,
7
6
  } from "@hypit/runtime-kit";
7
+ import { resolveCredentialDirectory } from "./paths.js";
8
8
  import { FileCredentialStore } from "./store.js";
9
9
 
10
10
  const fileCredentialStoreAdapter = createRuntimeCredentialStoreAdapterFacet({
@@ -17,8 +17,9 @@ const fileCredentialStoreAdapter = createRuntimeCredentialStoreAdapterFacet({
17
17
  open(context) {
18
18
  const config = runtimeConfigObject(context.config, "file CredentialStore");
19
19
  const path = runtimeConfigString(config.path, "file credential path");
20
- return { value: new FileCredentialStore(path === undefined
21
- ? join(context.hostStateRoot, "credentials") : resolve(context.hostStateRoot, path)) };
20
+ return { value: new FileCredentialStore(
21
+ resolveCredentialDirectory(context.hostStateRoot, path, "file CredentialStore"),
22
+ ) };
22
23
  },
23
24
  });
24
25
 
@@ -1 +1,2 @@
1
1
  export { FileCredentialStore } from "./store.js";
2
+ export { resolveCredentialDirectory } from "./paths.js";
@@ -0,0 +1,16 @@
1
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
2
+
3
+ /** Profile `path` is Host-state-relative; it must not write secrets outside that root. */
4
+ export function resolveCredentialDirectory(
5
+ hostStateRoot: string,
6
+ path: string | undefined,
7
+ label: string,
8
+ ): string {
9
+ const root = resolve(hostStateRoot);
10
+ const directory = path === undefined ? join(root, "credentials") : resolve(root, path);
11
+ const relation = relative(root, directory);
12
+ if (relation === ".." || relation.startsWith(`..${sep}`) || isAbsolute(relation)) {
13
+ throw new Error(`${label} path must stay inside the Host state root`);
14
+ }
15
+ return directory;
16
+ }
@@ -8,6 +8,26 @@ function missing(error: unknown): boolean {
8
8
  return error instanceof Error && "code" in error && error.code === "ENOENT";
9
9
  }
10
10
 
11
+ /**
12
+ * One process reads a credential for one Endpoint action while another action's refresh replaces
13
+ * it, so a read handle and a replacement can reach the same document at the same instant. Windows
14
+ * refuses to replace a file any handle still holds. Each document's work takes its turn instead,
15
+ * keyed by path so separate Store instances over one directory share the same order. Keys are
16
+ * independent: a turn on one document never delays another.
17
+ */
18
+ const documentTurns = new Map<string, Promise<unknown>>();
19
+
20
+ function onDocument<T>(path: string, work: () => Promise<T>): Promise<T> {
21
+ const turn = (documentTurns.get(path) ?? Promise.resolve()).then(work, work);
22
+ // The successor waits for this turn to settle, not to succeed, and the last turn clears the path.
23
+ const settled = turn.then(() => undefined, () => undefined);
24
+ documentTurns.set(path, settled);
25
+ void settled.then(() => {
26
+ if (documentTurns.get(path) === settled) documentTurns.delete(path);
27
+ });
28
+ return turn;
29
+ }
30
+
11
31
  function credentialValue(value: unknown): CredentialValue {
12
32
  if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("invalid credential value");
13
33
  const record = value as Record<string, unknown>;
@@ -44,6 +64,10 @@ export class FileCredentialStore implements WritableCredentialStore {
44
64
  async resolve(ref: CredentialRef): Promise<CredentialValue | undefined> {
45
65
  if (!this.owns(ref)) return undefined;
46
66
  const path = this.#path(ref);
67
+ return await onDocument(path, async () => await this.#read(path));
68
+ }
69
+
70
+ async #read(path: string): Promise<CredentialValue | undefined> {
47
71
  try { await this.#privateDirectory(false); } catch (error) {
48
72
  if (missing(error)) return undefined;
49
73
  throw error;
@@ -69,21 +93,25 @@ export class FileCredentialStore implements WritableCredentialStore {
69
93
  async put(ref: CredentialRef, value: CredentialValue): Promise<void> {
70
94
  const path = this.#path(ref);
71
95
  const contents = JSON.stringify(credentialValue(value));
72
- await this.#privateDirectory(true);
73
- const temporary = join(this.directory, `.write-${randomUUID()}.tmp`);
74
- const file = await open(temporary, "wx", 0o600);
75
- try {
76
- try { await file.writeFile(`${contents}\n`, "utf8"); } finally { await file.close(); }
77
- await rename(temporary, path);
78
- } finally { await rm(temporary, { force: true }); }
96
+ await onDocument(path, async () => {
97
+ await this.#privateDirectory(true);
98
+ const temporary = join(this.directory, `.write-${randomUUID()}.tmp`);
99
+ const file = await open(temporary, "wx", 0o600);
100
+ try {
101
+ try { await file.writeFile(`${contents}\n`, "utf8"); } finally { await file.close(); }
102
+ await rename(temporary, path);
103
+ } finally { await rm(temporary, { force: true }); }
104
+ });
79
105
  }
80
106
 
81
107
  async delete(ref: CredentialRef): Promise<boolean> {
82
108
  const path = this.#path(ref);
83
109
  // Deletion never opens or decodes the old document.
84
- try { await unlink(path); return true; } catch (error) {
85
- if (missing(error)) return false;
86
- throw error;
87
- }
110
+ return await onDocument(path, async () => {
111
+ try { await unlink(path); return true; } catch (error) {
112
+ if (missing(error)) return false;
113
+ throw error;
114
+ }
115
+ });
88
116
  }
89
117
  }
@@ -67,6 +67,8 @@ type WindowsCredentialResult = {
67
67
 
68
68
  const windowsScript = fileURLToPath(new URL("../runtime/windows-credential.ps1", import.meta.url));
69
69
 
70
+ const windowsCredentialTimeoutMs = 10_000;
71
+
70
72
  function windowsCredential(
71
73
  operation: "read" | "write" | "delete",
72
74
  service: string,
@@ -84,8 +86,13 @@ function windowsCredential(
84
86
  const fail = (error: Error): void => {
85
87
  if (settled) return;
86
88
  settled = true;
89
+ clearTimeout(timer);
87
90
  reject(error);
88
91
  };
92
+ const timer = setTimeout(() => {
93
+ fail(new Error(`Windows credential ${operation} for ${account} timed out`));
94
+ child.kill("SIGKILL");
95
+ }, windowsCredentialTimeoutMs);
89
96
  child.on("error", fail);
90
97
  child.stdout.on("data", (chunk: Buffer) => {
91
98
  stdout += chunk.toString("utf8");
@@ -97,6 +104,7 @@ function windowsCredential(
97
104
  });
98
105
  child.on("close", (code) => {
99
106
  if (settled) return;
107
+ clearTimeout(timer);
100
108
  if (code !== 0) {
101
109
  fail(new Error(`Windows credential ${operation} for ${account} failed${stderr.trim().length === 0 ? "" : `: ${stderr.trim()}`}`));
102
110
  return;
@@ -142,8 +150,9 @@ function platformBackend(service: string) {
142
150
  }
143
151
  if (process.platform === "win32") return windowsBackend();
144
152
  // A Profile that selects this Store cannot be repaired by anything the user does here, and the
145
- // other two Stores are the answer, so name them where the failure is read.
153
+ // other Stores are the answer, so name them where the failure is read.
146
154
  throw new Error("OS CredentialStore supports macOS and Windows only; select "
155
+ + "@hypit/credential-store-platform (platform locker, owner-private file on Linux), "
147
156
  + "@hypit/credential-store-file (owner-private local file) or @hypit/credential-store-env "
148
157
  + "(externally supplied value) in this Profile's credentials instead");
149
158
  }
@@ -1,10 +1,10 @@
1
- import { join, resolve } from "node:path";
2
1
  import {
3
2
  createRuntimeCredentialStoreAdapterFacet,
4
3
  runtimeConfigExact,
5
4
  runtimeConfigObject,
6
5
  runtimeConfigString,
7
6
  } from "@hypit/runtime-kit";
7
+ import { resolveCredentialDirectory } from "@hypit/credential-store-file";
8
8
  import { PlatformCredentialStore } from "./store.js";
9
9
 
10
10
  const platformCredentialStoreAdapter = createRuntimeCredentialStoreAdapterFacet({
@@ -23,9 +23,7 @@ const platformCredentialStoreAdapter = createRuntimeCredentialStoreAdapterFacet(
23
23
  value: new PlatformCredentialStore({
24
24
  // Linux uses the same directory the file Store uses by default, so a Profile
25
25
  // that switches between them on Linux finds the credential it stored.
26
- directory: path === undefined
27
- ? join(context.hostStateRoot, "credentials")
28
- : resolve(context.hostStateRoot, path),
26
+ directory: resolveCredentialDirectory(context.hostStateRoot, path, "platform CredentialStore"),
29
27
  ...(service === undefined ? {} : { service }),
30
28
  }),
31
29
  };
@@ -1,4 +1,4 @@
1
- const windowsProcessCreation = ["PATHEXT", "SYSTEMROOT", "WINDIR", "ComSpec"] as const;
1
+ const windowsProcessCreation = ["PATHEXT", "SYSTEMROOT", "WINDIR", "ComSpec", "TEMP", "TMP"] as const;
2
2
 
3
3
  /**
4
4
  * ffmpeg and ffprobe are started as PATH executables. The Host environment is
@@ -7,9 +7,10 @@ const windowsProcessCreation = ["PATHEXT", "SYSTEMROOT", "WINDIR", "ComSpec"] as
7
7
  */
8
8
  export function mediaProcessEnv(
9
9
  extra?: Readonly<Record<string, string>>,
10
+ platform: NodeJS.Platform = process.platform,
10
11
  ): NodeJS.ProcessEnv {
11
12
  const env: NodeJS.ProcessEnv = { PATH: process.env.PATH ?? "" };
12
- if (process.platform === "win32") {
13
+ if (platform === "win32") {
13
14
  for (const name of windowsProcessCreation) {
14
15
  const value = process.env[name];
15
16
  if (value !== undefined && value.length > 0) env[name] = value;
@@ -25,6 +25,7 @@
25
25
  "@hypit/markup": "workspace:*",
26
26
  "@hypit/temporal": "workspace:*",
27
27
  "@hypit/temporal-markup": "workspace:*",
28
- "@hypit/media-track": "workspace:*"
28
+ "@hypit/media-track": "workspace:*",
29
+ "@hypit/visual-ir": "workspace:*"
29
30
  }
30
31
  }
@@ -35,8 +35,12 @@ export async function mediaExecutablePath(value: string): Promise<string> {
35
35
  throw new Error(`HyperFrames media executable ${value} is unavailable; correct the Provider's ffmpegPath or ffprobePath.`);
36
36
  }
37
37
 
38
- function processEnvironment(): NodeJS.ProcessEnv {
39
- const names = ["PATH", "HOME", "TMPDIR", "LANG", "LC_ALL"] as const;
38
+ export function processEnvironment(platform: NodeJS.Platform = process.platform): NodeJS.ProcessEnv {
39
+ // POSIX temp is TMPDIR; Windows is TEMP/TMP. HyperFrames writes extracted
40
+ // frames under %TEMP%\hf-render-… and ffmpeg creates temporary files the same way.
41
+ const names = platform === "win32"
42
+ ? ["PATH", "PATHEXT", "SYSTEMROOT", "WINDIR", "ComSpec", "TEMP", "TMP", "USERPROFILE", "LANG", "LC_ALL"] as const
43
+ : ["PATH", "HOME", "TMPDIR", "LANG", "LC_ALL"] as const;
40
44
  return Object.fromEntries(names.flatMap((name) => process.env[name] === undefined
41
45
  ? []
42
46
  : [[name, process.env[name]]])) as NodeJS.ProcessEnv;
@@ -504,11 +504,14 @@ export async function takeManagedProgramsDown(
504
504
  if (pid === undefined || !processAlive(pid)) {
505
505
  if (pid !== undefined) await rm(join(directory(dataRoot, program), "process.pid"), { force: true });
506
506
  const state = await program.probe();
507
- return state.state === "down"
508
- ? { ...base, action: "nothing-to-stop", state }
509
- // Someone else's process, or one started by hand. Killing it is not this
510
- // command's business; saying so is.
511
- : { ...base, action: "not-ours", state, detail: `${program.id} is running without a Hypit process record` };
507
+ if (state.state === "down" || program.start === undefined) {
508
+ // Probe-only Programs describe installed tools or resources. Readiness means they are
509
+ // usable, not that a process exists for Hypit to stop.
510
+ return { ...base, action: "nothing-to-stop", state };
511
+ }
512
+ // Someone else's process, or one started by hand. Killing it is not this
513
+ // command's business; saying so is.
514
+ return { ...base, action: "not-ours", state, detail: `${program.id} is running without a Hypit process record` };
512
515
  }
513
516
  const term = await stopProcessTree(pid);
514
517
  if (term === "denied") {
@@ -35,10 +35,12 @@ Keep an existing chosen service, or configure the chosen local or hosted Provide
35
35
  bindings. HypiHub is the recommended integrated hosted route in the official Distribution; other
36
36
  services use project Provider packages. If the user chooses HypiHub,
37
37
  `hypit auth login hypihub.default` connects that account after choosing its CredentialStore.
38
- The starter selects the OS store for macOS Keychain / Windows Credential Locker. On Linux, or when
39
- explicitly choosing file storage, edit the Profile's `credentials` and Endpoint reference as shown
40
- in [file CredentialStore](../credential-store-file/README.md#select-it-before-login) before `auth` or
41
- `runtime up`. This selection is configuration; execution never switches stores automatically.
38
+ The starter selects the [platform CredentialStore](../credential-store-platform/README.md#select-it):
39
+ macOS Keychain or Windows Credential Locker on those two platforms, and an owner-private file on
40
+ Linux, so the Profile it writes needs no edit on any of them. Name another Store in `credentials` and
41
+ in the Endpoint's credential reference as the
42
+ [file CredentialStore](../credential-store-file/README.md#select-it-before-login) shows — to choose it
43
+ explicitly. This selection is configuration; execution never switches stores automatically.
42
44
  `hypit doctor --endpoint <name>` checks a selected Endpoint;
43
45
  `hypit runtime up --endpoint <name>` prepares that Endpoint and starts the Worker. Repeat the flag
44
46
  for several chosen Endpoints; omitting it prepares the whole Profile. `hypit programs up --endpoint
@@ -25,6 +25,7 @@
25
25
  "@hypit/protocol": "workspace:*",
26
26
  "@hypit/provider-hypihub": "workspace:*",
27
27
  "@hypit/runtime": "workspace:*",
28
+ "@hypit/runtime-kit": "workspace:*",
28
29
  "@hypit/runtime-host-node": "workspace:*",
29
30
  "@hypit/runtime-local": "workspace:*",
30
31
  "@hypit/speech": "workspace:*",
@@ -25,15 +25,18 @@ export const videoCliDistribution: CliDistribution = {
25
25
  initialRuntimeProfile: {
26
26
  format: "hypit.runtime-local@1",
27
27
  dataRoot: ".hypit/runtimes/local",
28
+ // The starter selects the portable Store, so the Profile it writes is openable and writable on
29
+ // Linux as well: macOS and Windows keep the platform locker, and a Linux host uses an
30
+ // owner-private file. `os` and `file` stay selectable by name for one explicit backend.
28
31
  credentials: {
29
- os: { use: "@hypit/credential-store-os" },
32
+ platform: { use: "@hypit/credential-store-platform" },
30
33
  },
31
34
  endpoints: {
32
35
  "hypihub.default": {
33
36
  use: "@hypit/provider-hypihub",
34
37
  config: {
35
38
  baseUrl: "https://hypit.ai",
36
- apiKey: { store: "os", key: "hypihub.oauth" },
39
+ apiKey: { store: "platform", key: "hypihub.oauth" },
37
40
  },
38
41
  },
39
42
  "media.local": {