@hanzo/build 0.2.37 → 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.
- package/lib/api/machines.d.ts +9 -1
- package/lib/api/machines.js +14 -4
- package/lib/settings/machines.js +22 -9
- package/lib/shelf.js +8 -3
- package/package.json +1 -1
package/lib/api/machines.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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;
|
package/lib/api/machines.js
CHANGED
|
@@ -46,14 +46,24 @@ async function owned(p) {
|
|
|
46
46
|
throw e;
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
-
/**
|
|
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 = {};
|
package/lib/settings/machines.js
CHANGED
|
@@ -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
|
-
|
|
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({
|
|
28
|
-
const on =
|
|
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,
|
|
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.
|
|
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,
|
|
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/lib/shelf.js
CHANGED
|
@@ -97,7 +97,7 @@ export function Artifacts() {
|
|
|
97
97
|
}
|
|
98
98
|
};
|
|
99
99
|
const said = failed || note;
|
|
100
|
-
return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", items: "center", "$max-md": { px: '$4' }, children: [_jsxs(YStack, { width: "100%", maxW: 1080, children: [_jsx(Head, { title: "Artifacts", says: "What this organization has built. Open one to keep building it." }), said ? (_jsx(SizableText, { size: "$2", color: "$soft", role: "status", pb: "$3", children: said })) : null, starters.value.length ? (_jsxs(YStack, { gap: "$3", pb: "$6", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: "Make something new" }), _jsx(XStack, { flexWrap: "wrap", gap: "$3", children: starters.value.slice(0, 4).map((x) => (_jsxs(Card, { small: true, label: `Start from ${x.title}`, onPress: () => void take(x.slug), children: [_jsx(Shot, { slug: x.slug
|
|
100
|
+
return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", items: "center", "$max-md": { px: '$4' }, children: [_jsxs(YStack, { width: "100%", maxW: 1080, children: [_jsx(Head, { title: "Artifacts", says: "What this organization has built. Open one to keep building it." }), said ? (_jsx(SizableText, { size: "$2", color: "$soft", role: "status", pb: "$3", children: said })) : null, starters.value.length ? (_jsxs(YStack, { gap: "$3", pb: "$6", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: "Make something new" }), _jsx(XStack, { flexWrap: "wrap", gap: "$3", children: starters.value.slice(0, 4).map((x) => (_jsxs(Card, { small: true, label: `Start from ${x.title}`, onPress: () => void take(x.slug), children: [_jsx(Shot, { slug: x.slug }), _jsx(YStack, { px: "$3", py: "$2.5", children: _jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: busy === x.slug ? 'Copying…' : x.title }) })] }, x.slug))) })] })) : null, list.error ? (_jsx(SizableText, { size: "$2", color: "$soft", children: list.error.message })) : !host.person ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "Sign in to see what your organization has built." })) : list.loading && list.value.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "Reading your projects\u2026" })) : list.value.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "Nothing built yet. Describe something on the New page and it lands here." })) : (_jsx(YStack, { gap: "$5", children: months(list.value).map((g) => (_jsxs(YStack, { gap: "$2", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: g.label }), _jsx(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: g.rows.map((p, i) => (_jsxs(XStack, { items: "center", pr: "$2", borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: [_jsxs(XStack, { flex: 1, minW: 0, render: "button", "aria-label": `Open ${p.name}`, onPress: () => host.go(p.slug), items: "center", gap: "$3", px: "$3", py: "$2.5", cursor: "pointer", style: { textAlign: 'left' }, children: [p.visibility === 'private' ? _jsx(Lock, { size: 16, opacity: 0.7 }) : _jsx(Globe, { size: 16, opacity: 0.7 }), _jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$3", color: "$ink", numberOfLines: 1, children: p.name }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: [p.visibility === 'private' ? 'Private' : 'Public', edited(when(p)), p.live ? new URL(p.live).host : 'Not deployed yet']
|
|
101
101
|
.filter(Boolean)
|
|
102
102
|
.join(' · ') })] })] }), _jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label": `Actions for ${p.name}`, px: "$1.5", py: "$1", rounded: "$2", hoverStyle: { bg: '$raised' }, children: _jsx(MoreHorizontal, { size: 16 }) }), items: [
|
|
103
103
|
{ key: 'rename', label: 'Rename', onSelect: () => setRenaming(p) },
|
|
@@ -114,9 +114,14 @@ export function Artifacts() {
|
|
|
114
114
|
list.reload();
|
|
115
115
|
} })) : null] }));
|
|
116
116
|
}
|
|
117
|
-
|
|
117
|
+
/**
|
|
118
|
+
* A starter's picture: a capture of its page from the top, 16:10. The frame has
|
|
119
|
+
* the capture's own shape at every width the grid gives a card, and the image is
|
|
120
|
+
* anchored at its top edge, so nothing of the page's header is cropped away.
|
|
121
|
+
*/
|
|
122
|
+
function Shot({ slug }) {
|
|
118
123
|
const [gone, setGone] = useState(false);
|
|
119
|
-
return (_jsxs(YStack, {
|
|
124
|
+
return (_jsxs(YStack, { width: "100%", aspectRatio: 16 / 10, bg: "$hover", items: "center", justify: "center", position: "relative", overflow: "hidden", children: [_jsxs(YStack, { items: "center", gap: "$2", opacity: 0.6, children: [_jsx(Boxes, { size: 24 }), _jsx(SizableText, { size: "$1", color: "$soft", textTransform: "uppercase", letterSpacing: 1, children: slug })] }), gone ? null : (_jsx("img", { src: `${SHOTS}/${encodeURIComponent(slug)}.webp`, alt: "", loading: "lazy", onError: () => setGone(true), style: { position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'top' } }))] }));
|
|
120
125
|
}
|
|
121
126
|
export function Templates() {
|
|
122
127
|
const t = useTarget();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/build",
|
|
3
|
-
"version": "0.2.
|
|
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.",
|