@hanzo/build 0.2.38 → 0.2.40

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.
@@ -36,12 +36,21 @@ export interface Machine {
36
36
  }
37
37
  export declare function machine(raw: unknown): Machine;
38
38
  export declare function machines(t: Target): Promise<Machine[]>;
39
- /** Register a machine by hand. `hanzo link` on a machine with this hostname takes the row over. */
39
+ /**
40
+ * Register a machine by hand. One left unnamed takes its hostname, the name `hanzo
41
+ * link` gives it; `hanzo link` on a machine with this hostname takes the row over.
42
+ */
40
43
  export declare function add(t: Target, what: {
41
44
  label: string;
42
45
  kind: Kind;
43
46
  host?: string;
44
47
  }): Promise<Machine>;
48
+ /**
49
+ * A machine's status as a person reads it. The platform reports a hand-registered
50
+ * machine at its registered status until a heartbeat arrives, so `online` holds only
51
+ * once one has; before that it is `not seen yet`, and nothing is there to take a run.
52
+ */
53
+ export declare function state(m: Machine): string;
45
54
  /** Rename a machine, or drain it (`draining`) and bring it back (`online`). */
46
55
  export declare function change(t: Target, id: string, what: {
47
56
  label?: string;
@@ -46,14 +46,25 @@ async function owned(p) {
46
46
  throw e;
47
47
  }
48
48
  }
49
- /** Register a machine by hand. `hanzo link` on a machine with this hostname takes the row over. */
49
+ /**
50
+ * Register a machine by hand. One left unnamed takes its hostname, the name `hanzo
51
+ * link` gives it; `hanzo link` on a machine with this hostname takes the row over.
52
+ */
50
53
  export async function add(t, what) {
51
- const label = what.label.trim();
52
- if (!label)
53
- throw new Error('A machine needs a name');
54
54
  const host = (what.host ?? '').trim();
55
+ const label = what.label.trim() || host;
56
+ if (!label)
57
+ throw new Error('A machine needs a name or a hostname');
55
58
  return machine(await call(t, 'POST', '/v1/agent/targets', { label, kind: what.kind, ...(host ? { host } : {}) }));
56
59
  }
60
+ /**
61
+ * A machine's status as a person reads it. The platform reports a hand-registered
62
+ * machine at its registered status until a heartbeat arrives, so `online` holds only
63
+ * once one has; before that it is `not seen yet`, and nothing is there to take a run.
64
+ */
65
+ export function state(m) {
66
+ return m.status === 'online' && !m.seen ? 'not seen yet' : m.status;
67
+ }
57
68
  /** Rename a machine, or drain it (`draining`) and bring it back (`online`). */
58
69
  export async function change(t, id, what) {
59
70
  const body = {};
@@ -14,7 +14,7 @@ export interface Place {
14
14
  label: string;
15
15
  /** laptop | gpu | cloud | … as the platform names it; 'sandbox' for the sandbox. */
16
16
  kind: string;
17
- /** online | offline | draining; the sandbox is always online. */
17
+ /** online | offline | draining | not seen yet, as machines.ts `state` reads it; the sandbox is always online. */
18
18
  status: string;
19
19
  /** "10 vCPU / 122G / 1× GB10", when the machine reported one. */
20
20
  capacity: string;
package/lib/api/places.js CHANGED
@@ -1,8 +1,8 @@
1
- import { machines } from './machines.js';
1
+ import { machines, state } from './machines.js';
2
2
  export const SANDBOX = { id: '', label: 'Hanzo sandbox', kind: 'sandbox', status: 'online', capacity: '' };
3
3
  export async function places(t) {
4
4
  const list = await machines(t);
5
- return [SANDBOX, ...list.map(({ id, label, kind, status, capacity }) => ({ id, label, kind, status, capacity }))];
5
+ return [SANDBOX, ...list.map((m) => ({ id: m.id, label: m.label, kind: m.kind, status: state(m), capacity: m.capacity }))];
6
6
  }
7
7
  /** Whether a run sent here now would be taken. */
8
8
  export const ready = (p) => p.status === 'online';
@@ -10,8 +10,8 @@ import { SizableText, XStack, YStack } from '@hanzo/gui';
10
10
  import { MoreHorizontal, Plus } from '@hanzogui/lucide-icons-2';
11
11
  import { Button, Dialog, DialogContent, DialogTitle, DropdownMenu, Input } from '@hanzo/ui';
12
12
  import { CopyButton } from '@hanzo/ui/product';
13
- import { useState } from 'react';
14
- import { add, change, KINDS, key, machines, remove } from '../api/machines.js';
13
+ import { useEffect, useState } from 'react';
14
+ import { add, change, KINDS, key, machines, remove, state } from '../api/machines.js';
15
15
  import { Confirm, Rename } from '../ask.js';
16
16
  import { useRead } from '../data.js';
17
17
  import { useHost, useTarget } from '../host.js';
@@ -19,26 +19,38 @@ import { Card, Field, Group, Heading, Note, Once, Row, Soft } from './ui.js';
19
19
  const mono = { fontFamily: 'var(--f-mono, ui-monospace, monospace)' };
20
20
  /** What `hanzo link` needs, in the order it is typed on the machine. */
21
21
  const LINK = ['hanzo login', 'hanzo link'].join('\n');
22
+ /** How often the list is read again, so a machine that comes up or goes quiet shows it: the agent's beat. */
23
+ const BEAT = 30_000;
24
+ /** The list before its first answer; every answer is a new array, even an empty one. */
25
+ const UNREAD = [];
22
26
  /** The line under a machine: what it is, where, how big, and what it is doing. */
23
27
  function about(m) {
24
28
  const load = m.running ? `${m.running} running` : m.sessions ? `${m.sessions} runs` : '';
25
- return [m.status || 'unknown', m.kind, m.host && m.host !== m.label ? m.host : '', m.capacity, load].filter(Boolean).join(' · ');
29
+ return [state(m) || 'unknown', m.kind, m.host && m.host !== m.label ? m.host : '', m.capacity, load].filter(Boolean).join(' · ');
26
30
  }
27
- function Dot({ status }) {
28
- const on = status === 'online';
29
- return (_jsx(YStack, { width: 8, height: 8, rounded: 999, shrink: 0, bg: on ? '$green10' : 'transparent', borderWidth: on ? 0 : 1, borderColor: status === 'draining' ? '$yellow10' : '$soft', "aria-hidden": true }));
31
+ function Dot({ m }) {
32
+ const on = state(m) === 'online';
33
+ return (_jsx(YStack, { width: 8, height: 8, rounded: 999, shrink: 0, bg: on ? '$green10' : 'transparent', borderWidth: on ? 0 : 1, borderColor: m.status === 'draining' ? '$yellow10' : '$soft', "aria-hidden": true }));
30
34
  }
31
35
  export function Machines() {
32
36
  const host = useHost();
33
37
  const t = useTarget();
34
38
  const signed = Boolean(host.person);
35
- const list = useRead(signed ? () => machines(t) : null, [], [t, signed]);
39
+ const list = useRead(signed ? () => machines(t) : null, UNREAD, [t, signed]);
36
40
  const [adding, setAdding] = useState(false);
37
41
  const [renaming, setRenaming] = useState(null);
38
42
  const [removing, setRemoving] = useState(null);
39
43
  const [keying, setKeying] = useState(null);
40
44
  const [minted, setMinted] = useState(null);
41
45
  const [note, setNote] = useState('');
46
+ // Read again on the agent's beat; "Reading machines…" stays for the first read only.
47
+ const again = list.reload;
48
+ useEffect(() => {
49
+ if (!signed)
50
+ return;
51
+ const id = setInterval(again, BEAT);
52
+ return () => clearInterval(id);
53
+ }, [again, signed]);
42
54
  if (!signed) {
43
55
  return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: "Machines" }), _jsx(Soft, { children: "Sign in to see your organization\u2019s machines." })] }));
44
56
  }
@@ -53,15 +65,15 @@ export function Machines() {
53
65
  setNote(e.message);
54
66
  }
55
67
  };
56
- return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Machines", detail: "Your organization\u2019s own computers. A run sent to one runs there instead of in the Hanzo sandbox." }), _jsxs(Group, { title: "Link a machine", detail: "Run these on it. It registers under its hostname, and stays online while hanzo link runs.", children: [_jsxs(XStack, { items: "flex-start", gap: "$2", px: "$3", py: "$2.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$raised", children: [_jsx(SizableText, { flex: 1, minW: 0, size: "$2", color: "$ink", "aria-label": "Link commands", style: { ...mono, whiteSpace: 'pre-wrap' }, children: LINK }), _jsx(CopyButton, { value: LINK, label: "Copy the commands" })] }), _jsx(XStack, { children: _jsxs(Button, { size: "sm", variant: "ghost", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 14 }), " Register one by hand"] }) })] }), _jsx(Group, { title: "Linked", children: list.error ? (_jsx(Soft, { children: list.error.message })) : list.loading && list.value.length === 0 ? (_jsx(Soft, { children: "Reading machines\u2026" })) : list.value.length === 0 ? (_jsx(Soft, { children: "No machine is linked yet." })) : (_jsx(Card, { children: list.value.map((m, i) => (_jsx(Row, { first: i === 0, leading: _jsx(Dot, { status: m.status }), title: m.label, detail: about(m), trailing: _jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label": `Actions for ${m.label}`, px: "$1.5", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(MoreHorizontal, { size: 16 }) }), items: [
57
- { key: 'rename', label: 'Rename', onSelect: () => setRenaming(m) },
58
- m.status === 'draining'
59
- ? { key: 'online', label: 'Take runs again', onSelect: () => void drain(m, 'online') }
60
- : { key: 'drain', label: 'Drain', description: 'Finish what it has, take nothing new', onSelect: () => void drain(m, 'draining') },
61
- { key: 'key', label: 'Claim key', onSelect: () => setKeying(m) },
62
- { type: 'separator' },
63
- { key: 'remove', label: 'Remove', destructive: true, onSelect: () => setRemoving(m) },
64
- ] }) }, m.id))) })) }), _jsx(Note, { children: note }), _jsx(Adding, { open: adding, onOpenChange: setAdding, onAdded: (m) => {
68
+ return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Machines", detail: "Your organization\u2019s own computers. A run sent to one runs there instead of in the Hanzo sandbox." }), _jsxs(Group, { title: "Link a machine", detail: "Run these on it. It registers under its hostname, and stays online while hanzo link runs.", children: [_jsxs(XStack, { items: "flex-start", gap: "$2", px: "$3", py: "$2.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$raised", children: [_jsx(SizableText, { flex: 1, minW: 0, size: "$2", color: "$ink", "aria-label": "Link commands", style: { ...mono, whiteSpace: 'pre-wrap' }, children: LINK }), _jsx(CopyButton, { value: LINK, label: "Copy the commands" })] }), _jsx(XStack, { children: _jsxs(Button, { size: "sm", variant: "ghost", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 14 }), " Register one by hand"] }) })] }), _jsxs(Group, { title: "Linked", children: [list.value === UNREAD ? (_jsx(Soft, { children: list.error ? list.error.message : 'Reading machines…' })) : list.value.length === 0 ? (_jsx(Soft, { children: "No machine is linked yet." })) : (_jsx(Card, { children: list.value.map((m, i) => (_jsx(Row, { first: i === 0, leading: _jsx(Dot, { m: m }), title: m.label, detail: about(m), trailing: _jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label": `Actions for ${m.label}`, px: "$1.5", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(MoreHorizontal, { size: 16 }) }), items: [
69
+ { key: 'rename', label: 'Rename', onSelect: () => setRenaming(m) },
70
+ m.status === 'draining'
71
+ ? { key: 'online', label: 'Take runs again', onSelect: () => void drain(m, 'online') }
72
+ : { key: 'drain', label: 'Drain', description: 'Finish what it has, take nothing new', onSelect: () => void drain(m, 'draining') },
73
+ { key: 'key', label: 'Claim key', onSelect: () => setKeying(m) },
74
+ { type: 'separator' },
75
+ { key: 'remove', label: 'Remove', destructive: true, onSelect: () => setRemoving(m) },
76
+ ] }) }, m.id))) })), list.error && list.value !== UNREAD ? _jsx(Soft, { children: list.error.message }) : null] }), _jsx(Note, { children: note }), _jsx(Adding, { open: adding, onOpenChange: setAdding, onAdded: (m) => {
65
77
  setNote(`${m.label} is registered`);
66
78
  list.reload();
67
79
  } }), _jsx(Rename, { open: renaming !== null, onOpenChange: (o) => !o && setRenaming(null), title: "Rename machine", name: renaming?.label ?? '',
@@ -110,5 +122,5 @@ function Adding({ open, onOpenChange, onAdded }) {
110
122
  setWorking(false);
111
123
  }
112
124
  };
113
- return (_jsx(Dialog, { open: open, onOpenChange: (o) => !o && close(), children: _jsxs(DialogContent, { maxW: 440, showCloseButton: false, children: [_jsx(DialogTitle, { children: "Register a machine" }), _jsxs(YStack, { gap: "$3", children: [_jsx(Field, { label: "Name", children: _jsx(Input, { value: label, onChangeText: setLabel, placeholder: "workshop", "aria-label": "Machine name", autoFocus: true }) }), _jsx(Field, { label: "Kind", children: _jsx(XStack, { flexWrap: "wrap", gap: "$1.5", role: "radiogroup", "aria-label": "Kind", children: KINDS.map((k) => (_jsx(XStack, { render: "button", role: "radio", "aria-checked": k === kind, "aria-label": k, onPress: () => setKind(k), px: "$2.5", py: "$1", rounded: "$10", borderWidth: 1, borderColor: k === kind ? '$ink' : '$borderColor', children: _jsx(SizableText, { size: "$1", color: k === kind ? '$ink' : '$soft', children: k }) }, k))) }) }), _jsx(Field, { label: "Hostname", hint: "Optional. What its runs report. hanzo link, signed in as you on a machine with this hostname, takes this row over.", children: _jsx(Input, { value: hostname, onChangeText: setHostname, placeholder: "gpu-01", "aria-label": "Hostname", autoCapitalize: "none" }) }), note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note })) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", disabled: working, onPress: close, children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void save(), children: "Register" })] })] })] }) }));
125
+ return (_jsx(Dialog, { open: open, onOpenChange: (o) => !o && close(), children: _jsxs(DialogContent, { maxW: 440, showCloseButton: false, children: [_jsx(DialogTitle, { children: "Register a machine" }), _jsxs(YStack, { gap: "$3", children: [_jsx(Field, { label: "Name", hint: "Left empty, it takes the hostname.", children: _jsx(Input, { value: label, onChangeText: setLabel, "aria-label": "Machine name", autoFocus: true }) }), _jsx(Field, { label: "Kind", children: _jsx(XStack, { flexWrap: "wrap", gap: "$1.5", role: "radiogroup", "aria-label": "Kind", children: KINDS.map((k) => (_jsx(XStack, { render: "button", role: "radio", "aria-checked": k === kind, "aria-label": k, onPress: () => setKind(k), px: "$2.5", py: "$1", rounded: "$10", borderWidth: 1, borderColor: k === kind ? '$ink' : '$borderColor', children: _jsx(SizableText, { size: "$1", color: k === kind ? '$ink' : '$soft', children: k }) }, k))) }) }), _jsx(Field, { label: "Hostname", hint: "Optional. What its runs report. hanzo link, signed in as you on a machine with this hostname, takes this row over.", children: _jsx(Input, { value: hostname, onChangeText: setHostname, "aria-label": "Hostname", autoCapitalize: "none" }) }), note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note })) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", disabled: working, onPress: close, children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void save(), children: "Register" })] })] })] }) }));
114
126
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/build",
3
- "version": "0.2.38",
3
+ "version": "0.2.40",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0 OR MIT",
6
6
  "description": "Hanzo Build — say what you want, watch it built, deployed and running. The builder as a component, mounted by the Hanzo app at hanzo.ai.",