@hanzo/build 0.2.38 → 0.2.39

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,20 @@ 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
+ * Whether a machine is up. The platform reports a hand-registered machine at its
50
+ * registered status until a heartbeat arrives, so `online` counts only once one has.
51
+ */
52
+ export declare function live(m: Machine): boolean;
45
53
  /** Rename a machine, or drain it (`draining`) and bring it back (`online`). */
46
54
  export declare function change(t: Target, id: string, what: {
47
55
  label?: string;
@@ -46,14 +46,24 @@ 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
+ * Whether a machine is up. The platform reports a hand-registered machine at its
62
+ * registered status until a heartbeat arrives, so `online` counts only once one has.
63
+ */
64
+ export function live(m) {
65
+ return m.status === 'online' && m.seen !== '';
66
+ }
57
67
  /** Rename a machine, or drain it (`draining`) and bring it back (`online`). */
58
68
  export async function change(t, id, what) {
59
69
  const body = {};
@@ -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, live, machines, remove } 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,39 @@ 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
+ const state = m.status === 'online' && !live(m) ? 'not seen yet' : m.status || 'unknown';
30
+ return [state, m.kind, m.host && m.host !== m.label ? m.host : '', m.capacity, load].filter(Boolean).join(' · ');
26
31
  }
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 }));
32
+ function Dot({ m }) {
33
+ const on = live(m);
34
+ 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
35
  }
31
36
  export function Machines() {
32
37
  const host = useHost();
33
38
  const t = useTarget();
34
39
  const signed = Boolean(host.person);
35
- const list = useRead(signed ? () => machines(t) : null, [], [t, signed]);
40
+ const list = useRead(signed ? () => machines(t) : null, UNREAD, [t, signed]);
36
41
  const [adding, setAdding] = useState(false);
37
42
  const [renaming, setRenaming] = useState(null);
38
43
  const [removing, setRemoving] = useState(null);
39
44
  const [keying, setKeying] = useState(null);
40
45
  const [minted, setMinted] = useState(null);
41
46
  const [note, setNote] = useState('');
47
+ // Read again on the agent's beat; "Reading machines…" stays for the first read only.
48
+ const again = list.reload;
49
+ useEffect(() => {
50
+ if (!signed)
51
+ return;
52
+ const id = setInterval(again, BEAT);
53
+ return () => clearInterval(id);
54
+ }, [again, signed]);
42
55
  if (!signed) {
43
56
  return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: "Machines" }), _jsx(Soft, { children: "Sign in to see your organization\u2019s machines." })] }));
44
57
  }
@@ -53,7 +66,7 @@ export function Machines() {
53
66
  setNote(e.message);
54
67
  }
55
68
  };
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: [
69
+ 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.value === UNREAD ? (_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, { 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: [
57
70
  { key: 'rename', label: 'Rename', onSelect: () => setRenaming(m) },
58
71
  m.status === 'draining'
59
72
  ? { key: 'online', label: 'Take runs again', onSelect: () => void drain(m, 'online') }
@@ -110,5 +123,5 @@ function Adding({ open, onOpenChange, onAdded }) {
110
123
  setWorking(false);
111
124
  }
112
125
  };
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" })] })] })] }) }));
126
+ 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
127
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/build",
3
- "version": "0.2.38",
3
+ "version": "0.2.39",
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.",