@hanzo/build 0.2.47 → 0.2.49
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/billing.d.ts +18 -2
- package/lib/api/billing.js +37 -3
- package/lib/api/machines.d.ts +53 -0
- package/lib/api/machines.js +113 -2
- package/lib/customize/agents.js +1 -1
- package/lib/customize/plugins.js +1 -1
- package/lib/customize/skills.js +1 -1
- package/lib/forge.js +19 -4
- package/lib/settings/billing.js +78 -5
- package/lib/settings/card.js +22 -4
- package/lib/settings/machines.js +64 -14
- package/package.json +1 -1
package/lib/api/billing.d.ts
CHANGED
|
@@ -90,6 +90,15 @@ export declare function subscribe(t: Target, p: Purchase): Promise<{
|
|
|
90
90
|
/** End a plan when its paid period does, not before. */
|
|
91
91
|
export declare function cancel(t: Target, id: string): Promise<Subscription>;
|
|
92
92
|
export declare function reactivate(t: Target, id: string): Promise<Subscription>;
|
|
93
|
+
export interface Address {
|
|
94
|
+
name?: string;
|
|
95
|
+
line1?: string;
|
|
96
|
+
line2?: string;
|
|
97
|
+
city?: string;
|
|
98
|
+
state?: string;
|
|
99
|
+
postalCode?: string;
|
|
100
|
+
country?: string;
|
|
101
|
+
}
|
|
93
102
|
export interface Method {
|
|
94
103
|
id: string;
|
|
95
104
|
type: string;
|
|
@@ -100,16 +109,23 @@ export interface Method {
|
|
|
100
109
|
default: boolean;
|
|
101
110
|
/** What the platform calls it when it is not a card. */
|
|
102
111
|
name: string;
|
|
112
|
+
billingAddress?: Address;
|
|
103
113
|
}
|
|
104
114
|
export declare function method(raw: unknown): Method;
|
|
115
|
+
/** Formats a billing address as a readable line: `Acme Inc · 123 Main St · San Francisco CA 94105 · US`. */
|
|
116
|
+
export declare function formatAddress(a?: Address): string;
|
|
105
117
|
/** How a saved method reads in a row: `Visa •••• 4242`. */
|
|
106
118
|
export declare function label(m: Method): string;
|
|
107
119
|
export declare function methods(t: Target): Promise<Method[]>;
|
|
108
120
|
/** The card a charge uses: the default, or the newest. */
|
|
109
121
|
export declare function chosen(list: Method[]): Method | null;
|
|
110
|
-
/** Save a card from the single-use token the processor's form returned. */
|
|
111
|
-
export declare function save(t: Target, token: string): Promise<Method>;
|
|
122
|
+
/** Save a card from the single-use token the processor's form returned, optionally with billing info. */
|
|
123
|
+
export declare function save(t: Target, token: string, billingAddress?: Address): Promise<Method>;
|
|
124
|
+
export declare function updateMethod(t: Target, id: string, what: {
|
|
125
|
+
billingAddress?: Address;
|
|
126
|
+
}): Promise<Method>;
|
|
112
127
|
export declare function detach(t: Target, id: string): Promise<void>;
|
|
128
|
+
export declare function makeDefault(t: Target, id: string): Promise<Method>;
|
|
113
129
|
export interface Processor {
|
|
114
130
|
/** `square`, or '' where none is configured. */
|
|
115
131
|
provider: string;
|
package/lib/api/billing.js
CHANGED
|
@@ -125,6 +125,7 @@ export async function reactivate(t, id) {
|
|
|
125
125
|
export function method(raw) {
|
|
126
126
|
const m = obj(raw);
|
|
127
127
|
const c = obj(m.card);
|
|
128
|
+
const a = obj(m.billingAddress);
|
|
128
129
|
const month = num(c.expMonth);
|
|
129
130
|
const year = num(c.expYear);
|
|
130
131
|
return {
|
|
@@ -135,8 +136,31 @@ export function method(raw) {
|
|
|
135
136
|
expires: month && year ? `${String(month).padStart(2, '0')}/${String(year % 100).padStart(2, '0')}` : '',
|
|
136
137
|
default: m.isDefault === true,
|
|
137
138
|
name: str(m.name),
|
|
139
|
+
billingAddress: a && (a.line1 || a.city || a.country || a.name || a.postalCode || a.state)
|
|
140
|
+
? {
|
|
141
|
+
name: str(a.name),
|
|
142
|
+
line1: str(a.line1),
|
|
143
|
+
line2: str(a.line2),
|
|
144
|
+
city: str(a.city),
|
|
145
|
+
state: str(a.state),
|
|
146
|
+
postalCode: str(a.postalCode),
|
|
147
|
+
country: str(a.country),
|
|
148
|
+
}
|
|
149
|
+
: undefined,
|
|
138
150
|
};
|
|
139
151
|
}
|
|
152
|
+
/** Formats a billing address as a readable line: `Acme Inc · 123 Main St · San Francisco CA 94105 · US`. */
|
|
153
|
+
export function formatAddress(a) {
|
|
154
|
+
if (!a)
|
|
155
|
+
return '';
|
|
156
|
+
const parts = [
|
|
157
|
+
a.name,
|
|
158
|
+
[a.line1, a.line2].filter(Boolean).join(', '),
|
|
159
|
+
[a.city, a.state, a.postalCode].filter(Boolean).join(' '),
|
|
160
|
+
a.country,
|
|
161
|
+
].filter(Boolean);
|
|
162
|
+
return parts.join(' · ');
|
|
163
|
+
}
|
|
140
164
|
/** How a saved method reads in a row: `Visa •••• 4242`. */
|
|
141
165
|
export function label(m) {
|
|
142
166
|
if (m.last4)
|
|
@@ -152,15 +176,25 @@ export async function methods(t) {
|
|
|
152
176
|
export function chosen(list) {
|
|
153
177
|
return list.find((m) => m.default) ?? list[0] ?? null;
|
|
154
178
|
}
|
|
155
|
-
/** Save a card from the single-use token the processor's form returned. */
|
|
156
|
-
export async function save(t, token) {
|
|
179
|
+
/** Save a card from the single-use token the processor's form returned, optionally with billing info. */
|
|
180
|
+
export async function save(t, token, billingAddress) {
|
|
157
181
|
if (!token)
|
|
158
182
|
throw new Error('The card form returned no token');
|
|
159
|
-
|
|
183
|
+
const body = { type: 'card', providerRef: token };
|
|
184
|
+
if (billingAddress && Object.values(billingAddress).some(Boolean)) {
|
|
185
|
+
body.billingAddress = billingAddress;
|
|
186
|
+
}
|
|
187
|
+
return method(await call(t, 'POST', '/v1/billing/methods', body));
|
|
188
|
+
}
|
|
189
|
+
export async function updateMethod(t, id, what) {
|
|
190
|
+
return method(await call(t, 'PATCH', `/v1/billing/methods/${seg(id)}`, what));
|
|
160
191
|
}
|
|
161
192
|
export async function detach(t, id) {
|
|
162
193
|
await call(t, 'DELETE', `/v1/billing/methods/${seg(id)}`);
|
|
163
194
|
}
|
|
195
|
+
export async function makeDefault(t, id) {
|
|
196
|
+
return method(await call(t, 'POST', `/v1/billing/methods/${seg(id)}/default`));
|
|
197
|
+
}
|
|
164
198
|
export async function processor(t) {
|
|
165
199
|
const p = obj(await call(t, 'GET', '/v1/billing/settings'));
|
|
166
200
|
return {
|
package/lib/api/machines.d.ts
CHANGED
|
@@ -16,6 +16,46 @@
|
|
|
16
16
|
import { type Target } from './call.ts';
|
|
17
17
|
export declare const KINDS: readonly ['laptop', 'gpu', 'cloud', 'cluster', 'machine'];
|
|
18
18
|
export type Kind = (typeof KINDS)[number];
|
|
19
|
+
export interface GPU {
|
|
20
|
+
vendor?: string;
|
|
21
|
+
model?: string;
|
|
22
|
+
memory?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface MachineSpec {
|
|
25
|
+
os?: string;
|
|
26
|
+
arch?: string;
|
|
27
|
+
cpus?: number;
|
|
28
|
+
memory?: number;
|
|
29
|
+
gpus?: GPU[];
|
|
30
|
+
}
|
|
31
|
+
export interface MachineMetrics {
|
|
32
|
+
load1?: number;
|
|
33
|
+
load5?: number;
|
|
34
|
+
load15?: number;
|
|
35
|
+
memUsed?: number;
|
|
36
|
+
memFree?: number;
|
|
37
|
+
gpuUtil?: number;
|
|
38
|
+
cpuUtil?: number;
|
|
39
|
+
cpuTemp?: number;
|
|
40
|
+
gpuTemp?: number;
|
|
41
|
+
gpuPower?: number;
|
|
42
|
+
gpuPowerLimit?: number;
|
|
43
|
+
gpuMemUsed?: number;
|
|
44
|
+
gpuMemTotal?: number;
|
|
45
|
+
diskUsed?: number;
|
|
46
|
+
diskTotal?: number;
|
|
47
|
+
diskRead?: number;
|
|
48
|
+
diskWrite?: number;
|
|
49
|
+
netRx?: number;
|
|
50
|
+
netTx?: number;
|
|
51
|
+
model?: string;
|
|
52
|
+
decode?: number;
|
|
53
|
+
prefill?: number;
|
|
54
|
+
ttft?: number;
|
|
55
|
+
running?: number;
|
|
56
|
+
waiting?: number;
|
|
57
|
+
at?: number;
|
|
58
|
+
}
|
|
19
59
|
export interface Machine {
|
|
20
60
|
/** `tgt_…` */
|
|
21
61
|
id: string;
|
|
@@ -33,7 +73,16 @@ export interface Machine {
|
|
|
33
73
|
running: number;
|
|
34
74
|
/** When it last beat, RFC 3339, or '' when it never has. */
|
|
35
75
|
seen: string;
|
|
76
|
+
serving?: boolean;
|
|
77
|
+
createdAt?: string;
|
|
78
|
+
updatedAt?: string;
|
|
79
|
+
spec?: MachineSpec;
|
|
80
|
+
metrics?: MachineMetrics;
|
|
36
81
|
}
|
|
82
|
+
export declare function formatBytes(bytes?: number): string;
|
|
83
|
+
export declare function formatPercent(fraction?: number): string;
|
|
84
|
+
export declare function formatRelative(isoDate?: string): string;
|
|
85
|
+
export declare function specSummary(m: Machine): string;
|
|
37
86
|
export declare function machine(raw: unknown): Machine;
|
|
38
87
|
export declare function machines(t: Target): Promise<Machine[]>;
|
|
39
88
|
/**
|
|
@@ -44,6 +93,7 @@ export declare function add(t: Target, what: {
|
|
|
44
93
|
label: string;
|
|
45
94
|
kind: Kind;
|
|
46
95
|
host?: string;
|
|
96
|
+
capacity?: string;
|
|
47
97
|
}): Promise<Machine>;
|
|
48
98
|
/**
|
|
49
99
|
* A machine's status as a person reads it. The platform reports a hand-registered
|
|
@@ -55,6 +105,9 @@ export declare function state(m: Machine): string;
|
|
|
55
105
|
export declare function change(t: Target, id: string, what: {
|
|
56
106
|
label?: string;
|
|
57
107
|
status?: 'online' | 'draining';
|
|
108
|
+
capacity?: string;
|
|
109
|
+
kind?: Kind;
|
|
110
|
+
host?: string;
|
|
58
111
|
}): Promise<Machine>;
|
|
59
112
|
export declare function remove(t: Target, id: string): Promise<void>;
|
|
60
113
|
/**
|
package/lib/api/machines.js
CHANGED
|
@@ -17,9 +17,101 @@ import { call, Refusal, seg } from './call.js';
|
|
|
17
17
|
export const KINDS = ['laptop', 'gpu', 'cloud', 'cluster', 'machine'];
|
|
18
18
|
const str = (v) => (typeof v === 'string' ? v : '');
|
|
19
19
|
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
|
|
20
|
+
const obj = (v) => (v && typeof v === 'object' ? v : {});
|
|
21
|
+
export function formatBytes(bytes) {
|
|
22
|
+
if (!bytes || bytes <= 0)
|
|
23
|
+
return '';
|
|
24
|
+
const k = 1024;
|
|
25
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
|
26
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
27
|
+
const val = bytes / Math.pow(k, i);
|
|
28
|
+
return `${val >= 10 || i === 0 ? Math.round(val) : val.toFixed(1)} ${sizes[i]}`;
|
|
29
|
+
}
|
|
30
|
+
export function formatPercent(fraction) {
|
|
31
|
+
if (fraction === undefined || !Number.isFinite(fraction))
|
|
32
|
+
return '';
|
|
33
|
+
return `${Math.round(fraction * 100)}%`;
|
|
34
|
+
}
|
|
35
|
+
export function formatRelative(isoDate) {
|
|
36
|
+
if (!isoDate)
|
|
37
|
+
return '';
|
|
38
|
+
const t = new Date(isoDate).getTime();
|
|
39
|
+
if (Number.isNaN(t))
|
|
40
|
+
return '';
|
|
41
|
+
const diffSec = Math.max(0, Math.floor((Date.now() - t) / 1000));
|
|
42
|
+
if (diffSec < 60)
|
|
43
|
+
return `${diffSec}s ago`;
|
|
44
|
+
if (diffSec < 3600)
|
|
45
|
+
return `${Math.floor(diffSec / 60)}m ago`;
|
|
46
|
+
if (diffSec < 86400)
|
|
47
|
+
return `${Math.floor(diffSec / 3600)}h ago`;
|
|
48
|
+
return `${Math.floor(diffSec / 86400)}d ago`;
|
|
49
|
+
}
|
|
50
|
+
export function specSummary(m) {
|
|
51
|
+
if (m.spec) {
|
|
52
|
+
const parts = [];
|
|
53
|
+
if (m.spec.cpus)
|
|
54
|
+
parts.push(`${m.spec.cpus} vCPU`);
|
|
55
|
+
if (m.spec.memory)
|
|
56
|
+
parts.push(formatBytes(m.spec.memory));
|
|
57
|
+
if (m.spec.gpus && m.spec.gpus.length > 0) {
|
|
58
|
+
const g = m.spec.gpus[0];
|
|
59
|
+
parts.push(`${m.spec.gpus.length}× ${g.model || g.vendor || 'GPU'}${g.memory ? ` (${formatBytes(g.memory)})` : ''}`);
|
|
60
|
+
}
|
|
61
|
+
if (parts.length > 0)
|
|
62
|
+
return parts.join(' · ');
|
|
63
|
+
}
|
|
64
|
+
return m.capacity || '';
|
|
65
|
+
}
|
|
20
66
|
export function machine(raw) {
|
|
21
67
|
const o = (raw && typeof raw === 'object' ? raw : {});
|
|
22
|
-
|
|
68
|
+
const specObj = obj(o.spec);
|
|
69
|
+
const metricsObj = obj(o.metrics);
|
|
70
|
+
let spec;
|
|
71
|
+
if (Object.keys(specObj).length > 0) {
|
|
72
|
+
const rawGpus = Array.isArray(specObj.gpus) ? specObj.gpus : [];
|
|
73
|
+
spec = {
|
|
74
|
+
os: str(specObj.os),
|
|
75
|
+
arch: str(specObj.arch),
|
|
76
|
+
cpus: num(specObj.cpus),
|
|
77
|
+
memory: num(specObj.memory),
|
|
78
|
+
gpus: rawGpus.map((g) => {
|
|
79
|
+
const go = obj(g);
|
|
80
|
+
return { vendor: str(go.vendor), model: str(go.model), memory: num(go.memory) };
|
|
81
|
+
}),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
let metrics;
|
|
85
|
+
if (Object.keys(metricsObj).length > 0) {
|
|
86
|
+
metrics = {
|
|
87
|
+
load1: num(metricsObj.load1),
|
|
88
|
+
load5: num(metricsObj.load5),
|
|
89
|
+
load15: num(metricsObj.load15),
|
|
90
|
+
memUsed: num(metricsObj.memUsed),
|
|
91
|
+
memFree: num(metricsObj.memFree),
|
|
92
|
+
gpuUtil: num(metricsObj.gpuUtil),
|
|
93
|
+
cpuUtil: num(metricsObj.cpuUtil),
|
|
94
|
+
cpuTemp: num(metricsObj.cpuTemp),
|
|
95
|
+
gpuTemp: num(metricsObj.gpuTemp),
|
|
96
|
+
gpuPower: num(metricsObj.gpuPower),
|
|
97
|
+
gpuPowerLimit: num(metricsObj.gpuPowerLimit),
|
|
98
|
+
gpuMemUsed: num(metricsObj.gpuMemUsed),
|
|
99
|
+
gpuMemTotal: num(metricsObj.gpuMemTotal),
|
|
100
|
+
diskUsed: num(metricsObj.diskUsed),
|
|
101
|
+
diskTotal: num(metricsObj.diskTotal),
|
|
102
|
+
diskRead: num(metricsObj.diskRead),
|
|
103
|
+
diskWrite: num(metricsObj.diskWrite),
|
|
104
|
+
netRx: num(metricsObj.netRx),
|
|
105
|
+
netTx: num(metricsObj.netTx),
|
|
106
|
+
model: str(metricsObj.model),
|
|
107
|
+
decode: num(metricsObj.decode),
|
|
108
|
+
prefill: num(metricsObj.prefill),
|
|
109
|
+
ttft: num(metricsObj.ttft),
|
|
110
|
+
running: num(metricsObj.running),
|
|
111
|
+
waiting: num(metricsObj.waiting),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const m = {
|
|
23
115
|
id: str(o.id),
|
|
24
116
|
label: str(o.label) || str(o.host) || str(o.id),
|
|
25
117
|
kind: str(o.kind),
|
|
@@ -30,6 +122,14 @@ export function machine(raw) {
|
|
|
30
122
|
running: num(o.running),
|
|
31
123
|
seen: str(o.metricsAt),
|
|
32
124
|
};
|
|
125
|
+
Object.defineProperties(m, {
|
|
126
|
+
serving: { value: o.serving === true, enumerable: false, writable: true },
|
|
127
|
+
createdAt: { value: str(o.createdAt), enumerable: false, writable: true },
|
|
128
|
+
updatedAt: { value: str(o.updatedAt), enumerable: false, writable: true },
|
|
129
|
+
spec: { value: spec, enumerable: false, writable: true },
|
|
130
|
+
metrics: { value: metrics, enumerable: false, writable: true },
|
|
131
|
+
});
|
|
132
|
+
return m;
|
|
33
133
|
}
|
|
34
134
|
export async function machines(t) {
|
|
35
135
|
const raw = (await call(t, 'GET', '/v1/agent/targets')) ?? {};
|
|
@@ -55,7 +155,12 @@ export async function add(t, what) {
|
|
|
55
155
|
const label = what.label.trim() || host;
|
|
56
156
|
if (!label)
|
|
57
157
|
throw new Error('A machine needs a name or a hostname');
|
|
58
|
-
|
|
158
|
+
const body = { label, kind: what.kind };
|
|
159
|
+
if (host)
|
|
160
|
+
body.host = host;
|
|
161
|
+
if (what.capacity?.trim())
|
|
162
|
+
body.capacity = what.capacity.trim();
|
|
163
|
+
return machine(await call(t, 'POST', '/v1/agent/targets', body));
|
|
59
164
|
}
|
|
60
165
|
/**
|
|
61
166
|
* A machine's status as a person reads it. The platform reports a hand-registered
|
|
@@ -76,6 +181,12 @@ export async function change(t, id, what) {
|
|
|
76
181
|
}
|
|
77
182
|
if (what.status !== undefined)
|
|
78
183
|
body.status = what.status;
|
|
184
|
+
if (what.capacity !== undefined)
|
|
185
|
+
body.capacity = what.capacity.trim();
|
|
186
|
+
if (what.kind !== undefined)
|
|
187
|
+
body.kind = what.kind;
|
|
188
|
+
if (what.host !== undefined)
|
|
189
|
+
body.host = what.host.trim();
|
|
79
190
|
return machine(await owned(call(t, 'PATCH', `/v1/agent/targets/${seg(id)}`, body)));
|
|
80
191
|
}
|
|
81
192
|
export async function remove(t, id) {
|
package/lib/customize/agents.js
CHANGED
|
@@ -110,7 +110,7 @@ function Editor({ agent, start, onClose, onSaved, onDeleted, }) {
|
|
|
110
110
|
}
|
|
111
111
|
};
|
|
112
112
|
const pick = (name, yes) => set({ tools: yes ? [...d.tools, name] : d.tools.filter((n) => n !== name) });
|
|
113
|
-
return (_jsx(Sheet, { title: agent ? agent.name : 'New agent', open: true, onOpenChange: (o) => !o && onClose(), width: 680, children: agent && full.error && !full.value ? (_jsx(Soft, { children: full.error.message })) : !ready ? (_jsx(Soft, { children: "Reading the agent\u2026" })) : (_jsxs(_Fragment, { children: [_jsx(Field, { label: "Name", hint: agent ? 'An agent keeps its name.' : 'Letters, digits, . _ or -. Other agents call it as agent_<name>.', children: _jsx(Input, { value: d.name, onChangeText: (v) => set({ name: v }),
|
|
113
|
+
return (_jsx(Sheet, { title: agent ? agent.name : 'New agent', open: true, onOpenChange: (o) => !o && onClose(), width: 680, children: agent && full.error && !full.value ? (_jsx(Soft, { children: full.error.message })) : !ready ? (_jsx(Soft, { children: "Reading the agent\u2026" })) : (_jsxs(_Fragment, { children: [_jsx(Field, { label: "Name", hint: agent ? 'An agent keeps its name.' : 'Letters, digits, . _ or -. Other agents call it as agent_<name>.', children: _jsx(Input, { value: d.name, onChangeText: (v) => set({ name: v }), "aria-label": "Name", disabled: Boolean(agent), autoCapitalize: "none" }) }), _jsx(Field, { label: "Description", hint: "The line another agent reads to decide whether to call this one.", children: _jsx(Input, { value: d.description, onChangeText: (v) => set({ description: v }), placeholder: "Answers questions about our codebase", "aria-label": "Description" }) }), _jsx(Field, { label: "Model", children: _jsxs(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: [_jsx(ModelPicker, { size: "sm", name: "Model", placeholder: "The deployment\u2019s default", models: catalog.value, scope: "chat", value: d.model || undefined, onChange: (id) => set({ model: id }), loading: catalog.loading, error: catalog.error?.message ?? null }), d.model && !agent ? (_jsx(Button, { size: "sm", variant: "ghost", onPress: () => set({ model: '' }), children: "Use the default" })) : null] }) }), _jsx(Field, { label: "Instructions", hint: "The system prompt: what the model reads before every run.", children: _jsx(Textarea, { value: d.instructions, onChangeText: (v) => set({ instructions: v }), placeholder: "Be terse, and cite the file you read.", "aria-label": "Instructions", rows: 8 }) }), _jsxs(YStack, { gap: "$2", children: [_jsxs(XStack, { items: "center", gap: "$3", children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", children: "Tools" }), _jsx(SizableText, { size: "$1", color: "$soft", children: every ? 'Every tool the fleet’s MCP server serves when it runs.' : d.tools.length ? `${d.tools.length} chosen. It can call only these.` : 'None chosen: it can call no tool.' })] }), _jsx(SizableText, { size: "$1", color: "$soft", children: "Every tool" }), _jsx(Switch, { checked: every, onCheckedChange: (v) => set({ tools: v ? [ALL] : [] }), "aria-label": "Every tool" })] }), every ? null : (_jsxs(_Fragment, { children: [_jsx(Input, { value: find, onChangeText: setFind, placeholder: "Find a tool\u2026", "aria-label": "Find a tool" }), _jsx(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", maxH: 240, overflow: "scroll", children: on.loading && !on.value.length ? (_jsx(Soft, { children: "Reading the tools that are on\u2026" })) : on.error && !on.value.length ? (_jsx(Soft, { children: on.error.message })) : !shownTools.length ? (_jsx(Soft, { children: choices.length ? 'No tool matches.' : 'No tool is on for your organization. Switch skills and connectors on first.' })) : (shownTools.map((x, i) => (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2", borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", style: mono, numberOfLines: 1, children: x.name }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: [x.source, x.description].filter(Boolean).join(' · ') })] }), _jsx(Switch, { checked: d.tools.includes(x.name), onCheckedChange: (v) => pick(x.name, v), "aria-label": `Use ${x.name}` })] }, x.name)))) })] }))] }), _jsxs(YStack, { gap: "$2", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: "Budget" }), _jsxs(XStack, { gap: "$3", flexWrap: "wrap", rowGap: "$2", items: "flex-end", children: [_jsxs(YStack, { gap: "$1", width: 140, children: [_jsxs(SizableText, { size: "$1", color: "$soft", children: ["Each ", d.period, ", USD"] }), _jsx(Input, { value: d.cap, onChangeText: (v) => set({ cap: v }), placeholder: "10", "aria-label": "Budget each period", inputMode: "decimal" })] }), _jsxs(YStack, { gap: "$1", width: 140, children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "One run, USD" }), _jsx(Input, { value: d.task, onChangeText: (v) => set({ task: v }), placeholder: "1", "aria-label": "Budget for one run", inputMode: "decimal" })] }), _jsx(Choice, { label: "Period", value: d.period, options: PERIODS.map((p) => ({ id: p, label: p[0].toUpperCase() + p.slice(1) })), onChange: (p) => set({ period: p }) })] }), _jsxs(SizableText, { size: "$1", color: "$soft", children: [full.value && full.value.cap ? `Spent ${money(full.value.spent)} this ${full.value.period}. ` : '', "A run stops at either limit."] })] }), _jsx(Line, { children: note }), _jsxs(XStack, { gap: "$2", items: "center", children: [agent && onDeleted ? (_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setAsking(true), children: "Delete" })) : null, _jsx(XStack, { flex: 1 }), _jsx(Button, { size: "sm", variant: "ghost", onPress: onClose, children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void save(), children: agent ? 'Save' : 'Create' })] }), agent && onDeleted ? (_jsx(Confirm, { what: agent.name, says: "The agent and every run recorded against it are removed.", open: asking, onOpenChange: setAsking, onYes: async () => {
|
|
114
114
|
await remove(t, agent.id || agent.name);
|
|
115
115
|
onDeleted();
|
|
116
116
|
} })) : null] })) }));
|
package/lib/customize/plugins.js
CHANGED
|
@@ -78,7 +78,7 @@ function Build({ onClose, onBuilt }) {
|
|
|
78
78
|
setWorking(false);
|
|
79
79
|
}
|
|
80
80
|
};
|
|
81
|
-
return (_jsxs(Sheet, { title: "Build a plugin", open: true, onOpenChange: (o) => !o && onClose(), width: 680, children: [_jsx(Field, { label: "Name", hint: "One lowercase word: letters, digits, _ or -. The runtime loads it by this name.", children: _jsx(Input, { value: name, onChangeText: setName,
|
|
81
|
+
return (_jsxs(Sheet, { title: "Build a plugin", open: true, onOpenChange: (o) => !o && onClose(), width: 680, children: [_jsx(Field, { label: "Name", hint: "One lowercase word: letters, digits, _ or -. The runtime loads it by this name.", children: _jsx(Input, { value: name, onChangeText: setName, "aria-label": "Name", autoCapitalize: "none" }) }), _jsx(Field, { label: "Connector", hint: "Optional. The connector whose credential the plugin reads when it runs. A plugin never holds a key itself.", children: _jsx(Input, { value: provider, onChangeText: setProvider, "aria-label": "Connector", autoCapitalize: "none" }) }), _jsx(Choice, { label: "Build from", value: way, options: WAYS, onChange: setWay }), _jsx(Field, { label: way === 'source' ? 'Source' : 'The API', hint: way === 'source'
|
|
82
82
|
? 'It is bundled and compiled in the runtime that will run it, and kept only if both succeed.'
|
|
83
83
|
: 'An OpenAPI document, or prose naming the endpoints. A model writes the TypeScript; you read it before it runs.', children: _jsx(Textarea, { value: way === 'source' ? source : spec, onChangeText: way === 'source' ? setSource : setSpec, "aria-label": way === 'source' ? 'Source' : 'The API', placeholder: way === 'source' ? 'export const acme = createProvider({ … })' : 'POST /v1/things creates a thing…', rows: 14, style: mono }) }), _jsx(Line, { children: note }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: onClose, children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void go(), children: "Build" })] })] }));
|
|
84
84
|
}
|
package/lib/customize/skills.js
CHANGED
|
@@ -120,7 +120,7 @@ function Editor({ skill, may, onClose, onSaved, onDeleted, }) {
|
|
|
120
120
|
setWorking(false);
|
|
121
121
|
}
|
|
122
122
|
};
|
|
123
|
-
return (_jsxs(Sheet, { title: skill ? skill.name : 'New skill', open: true, onOpenChange: (o) => !o && onClose(), width: 680, children: [skill?.source ? (_jsxs(SizableText, { size: "$2", color: "$soft", children: ["Read from ", skill.source, ". The next push of that repository replaces what is saved here."] })) : null, _jsx(Field, { label: "Name", hint: skill ? 'A skill’s name is its id; save under another name to make a new one.' : 'One lowercase word: letters, digits, _ or -. Agents name it as skill_<name>.', children: _jsx(Input, { value: name, onChangeText: setName,
|
|
123
|
+
return (_jsxs(Sheet, { title: skill ? skill.name : 'New skill', open: true, onOpenChange: (o) => !o && onClose(), width: 680, children: [skill?.source ? (_jsxs(SizableText, { size: "$2", color: "$soft", children: ["Read from ", skill.source, ". The next push of that repository replaces what is saved here."] })) : null, _jsx(Field, { label: "Name", hint: skill ? 'A skill’s name is its id; save under another name to make a new one.' : 'One lowercase word: letters, digits, _ or -. Agents name it as skill_<name>.', children: _jsx(Input, { value: name, onChangeText: setName, "aria-label": "Name", disabled: Boolean(skill), autoCapitalize: "none" }) }), _jsx(Field, { label: "Description", hint: "The one line an agent reads to decide whether it needs this skill.", children: _jsx(Input, { value: description, onChangeText: setDescription, placeholder: "How we triage an incoming issue", "aria-label": "Description" }) }), _jsx(Field, { label: "SKILL.md", hint: "Markdown: what the skill is for, when to use it, and the steps.", children: _jsx(Textarea, { value: content, onChangeText: setContent, placeholder: '# Triage\n\n1. Read the issue…', "aria-label": "SKILL.md", rows: 14, style: mono }) }), _jsx(Line, { children: note || (may ? '' : 'An org admin writes and deletes the organization’s skills.') }), _jsxs(XStack, { gap: "$2", items: "center", children: [skill && may ? (_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setAsking(true), children: "Delete" })) : null, _jsx(XStack, { flex: 1 }), _jsx(Button, { size: "sm", variant: "ghost", onPress: onClose, children: may ? 'Cancel' : 'Close' }), may ? (_jsx(Button, { size: "sm", disabled: working, onPress: () => void save(), children: "Save" })) : null] }), skill ? (_jsx(Confirm, { what: skill.name, says: "The skill leaves your organization and every agent that names it.", open: asking, onOpenChange: setAsking, onYes: async () => {
|
|
124
124
|
await remove(t, skill.id);
|
|
125
125
|
onDeleted(skill.name);
|
|
126
126
|
} })) : null] }));
|
package/lib/forge.js
CHANGED
|
@@ -60,11 +60,12 @@ function Codebases() {
|
|
|
60
60
|
const [q, setQ] = useState('');
|
|
61
61
|
const [making, setMaking] = useState(false);
|
|
62
62
|
const [sort, setSort] = useState('name');
|
|
63
|
+
const [filter, setFilter] = useState('all');
|
|
63
64
|
const [page, setPage] = useState(0);
|
|
64
65
|
const [landed, setLanded] = useState(0);
|
|
65
66
|
useEffect(() => {
|
|
66
67
|
setPage(0);
|
|
67
|
-
}, [q, sort]);
|
|
68
|
+
}, [q, sort, filter]);
|
|
68
69
|
const list = useRead(signed ? () => codebases(t) : null, [], [t, signed, making, landed]);
|
|
69
70
|
// Which codebases have an environment. A codebase absent here has none.
|
|
70
71
|
const envs = useRead(signed ? () => environments(t) : null, [], [t, signed]);
|
|
@@ -109,6 +110,13 @@ function Codebases() {
|
|
|
109
110
|
branches: ['main'],
|
|
110
111
|
})),
|
|
111
112
|
]
|
|
113
|
+
.filter((c) => {
|
|
114
|
+
if (filter === 'ready')
|
|
115
|
+
return envOf.get(c.name) === 'ready';
|
|
116
|
+
if (filter === 'syncing')
|
|
117
|
+
return syncing.has(c.name);
|
|
118
|
+
return true;
|
|
119
|
+
})
|
|
112
120
|
.filter((c) => !needle || `${c.org}/${c.name} ${c.description}`.toLowerCase().includes(needle))
|
|
113
121
|
.slice()
|
|
114
122
|
.sort((a, b) => {
|
|
@@ -128,7 +136,7 @@ function Codebases() {
|
|
|
128
136
|
pinCodebase(host.org, c);
|
|
129
137
|
host.go('');
|
|
130
138
|
};
|
|
131
|
-
return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", children: [_jsxs(YStack, { width: "100%", maxW: 1040, mx: "auto", gap: "$4", children: [_jsxs(XStack, { justify: "space-between", items: "flex-start", gap: "$4", children: [_jsxs(YStack, { gap: "$1", flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$6", fontWeight: "500", color: "$ink", children: host.org || 'Forge' }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Create and browse this organization\u2019s repositories." })] }), _jsxs(XStack, { render: "button", "aria-label": "Settings", shrink: 0, items: "center", gap: "$1.5", px: "$2", py: "$1", rounded: "$3", hoverStyle: { bg: '$hover' }, onPress: () => host.go('-/settings/environments'), children: [_jsx(Settings, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Settings" })] })] }), _jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(XStack, { px: "$2.5", py: "$1.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", shrink: 0, "$max-md": { display: 'none' }, children: _jsx(SizableText, { size: "$2", color: "$ink", children:
|
|
139
|
+
return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", children: [_jsxs(YStack, { width: "100%", maxW: 1040, mx: "auto", gap: "$4", children: [_jsxs(XStack, { justify: "space-between", items: "flex-start", gap: "$4", children: [_jsxs(YStack, { gap: "$1", flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$6", fontWeight: "500", color: "$ink", children: host.org || 'Forge' }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Create and browse this organization\u2019s repositories." })] }), _jsxs(XStack, { render: "button", "aria-label": "Settings", shrink: 0, items: "center", gap: "$1.5", px: "$2", py: "$1", rounded: "$3", hoverStyle: { bg: '$hover' }, onPress: () => host.go('-/settings/environments'), children: [_jsx(Settings, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Settings" })] })] }), _jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(XStack, { render: "button", "aria-label": "Filter repositories", onPress: () => setFilter((f) => (f === 'all' ? 'ready' : f === 'ready' ? 'syncing' : 'all')), px: "$2.5", py: "$1.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", shrink: 0, hoverStyle: { bg: '$hover' }, "$max-md": { display: 'none' }, children: _jsx(SizableText, { size: "$2", color: "$ink", children: filter === 'ready' ? 'Ready env' : filter === 'syncing' ? 'Syncing' : 'All repos' }) }), _jsx(YStack, { flex: 1, "$max-md": { display: 'none' } }), _jsx(YStack, { width: 240, minW: 0, shrink: 1, "$max-md": { width: 'auto', flex: 1 }, children: _jsx(Input, { value: q, onChangeText: setQ, placeholder: "Find repo\u2026", "aria-label": "Find a repository", disabled: !signed }) }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'sync' })), children: "Sync" }), _jsxs(Button, { size: "sm", disabled: !signed, onPress: () => setMaking(true), children: [_jsx(Plus, { size: 14 }), "New"] })] }), list.error ? (_jsx(Note, { children: list.error.message })) : !signed ? (_jsx(Note, { children: "Sign in to see this organization's repositories." })) : list.loading && list.value.length === 0 ? (_jsx(Note, { children: "Reading the forge\u2026" })) : shown.length === 0 ? (_jsxs(YStack, { gap: "$3", py: "$2", children: [_jsx(Note, { children: list.value.length === 0 ? 'No repositories yet.' : 'Nothing matches.' }), list.value.length === 0 ? (_jsxs(XStack, { gap: "$2", pt: "$1", children: [_jsxs(Button, { size: "sm", disabled: !signed, onPress: () => setMaking(true), children: [_jsx(Plus, { size: 14 }), "Create repository"] }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'sync' })), children: "Sync from GitHub" }), _jsx(Button, { size: "sm", variant: "ghost", onPress: () => host.go('-/templates'), children: "Start from template" })] })) : null] })) : (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { px: "$3", py: "$2", gap: "$3", items: "center", children: [_jsxs(XStack, { flex: 1, render: "button", "aria-label": "Sort by name", items: "center", gap: "$1", onPress: () => setSort('name'), children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Name" }), sort === 'name' ? _jsx(ChevronDown, { size: 12 }) : null] }), _jsx(SizableText, { size: "$1", color: "$soft", width: 96, "$max-md": { display: 'none' }, children: "Environment" }), _jsxs(XStack, { render: "button", "aria-label": "Sort by last updated", items: "center", justify: "flex-end", gap: "$1", width: 140, onPress: () => setSort('updated'), children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Last updated" }), sort === 'updated' ? _jsx(ChevronDown, { size: 12 }) : null] })] }), slice.map((c) => (_jsxs(XStack, { render: "button", "aria-label": `Work on ${c.name}`, onPress: () => open(c), items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: [_jsx(GitBranch, { size: 14 }), _jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: c.name }), _jsx(SizableText, { size: "$1", color: "$soft", width: 96, "$max-md": { display: 'none' }, children: envOf.get(c.name) === 'ready' ? 'Ready' : envOf.get(c.name) === 'proposed' ? 'To review' : '—' }), _jsx(SizableText, { size: "$1", color: "$soft", width: 140, style: { textAlign: 'right' }, children: syncing.has(c.name) ? 'Syncing…' : ago(c.updated) })] }, `${c.org}/${c.name}`))), _jsxs(XStack, { px: "$3", py: "$2", justify: "space-between", items: "center", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: `Showing ${from}–${to} of ${shown.length}` }), _jsxs(XStack, { gap: "$3", items: "center", children: [_jsx(XStack, { render: "button", "aria-label": "Previous page", disabled: safePage === 0, onPress: () => setPage(safePage - 1), children: _jsx(SizableText, { size: "$1", color: safePage === 0 ? '$soft' : '$ink', children: "Prev" }) }), _jsx(SizableText, { size: "$1", color: "$soft", children: `${safePage + 1} / ${pages}` }), _jsx(XStack, { render: "button", "aria-label": "Next page", disabled: safePage >= pages - 1, onPress: () => setPage(safePage + 1), children: _jsx(SizableText, { size: "$1", color: safePage >= pages - 1 ? '$soft' : '$ink', children: "Next" }) })] })] })] }))] }), _jsx(NewRepo, { open: making, onOpenChange: setMaking, onCreate: async (name, description) => {
|
|
132
140
|
const row = await create(t, name, description);
|
|
133
141
|
pinCodebase(host.org, row);
|
|
134
142
|
setMaking(false);
|
|
@@ -140,6 +148,13 @@ function NewRepo({ open, onOpenChange, onCreate, }) {
|
|
|
140
148
|
const [description, setDescription] = useState('');
|
|
141
149
|
const [busy, setBusy] = useState(false);
|
|
142
150
|
const [error, setError] = useState('');
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (!open) {
|
|
153
|
+
setName('');
|
|
154
|
+
setDescription('');
|
|
155
|
+
setError('');
|
|
156
|
+
}
|
|
157
|
+
}, [open]);
|
|
143
158
|
const submit = async () => {
|
|
144
159
|
setBusy(true);
|
|
145
160
|
setError('');
|
|
@@ -166,7 +181,7 @@ function Projects() {
|
|
|
166
181
|
pinBoard(host.org, b.key);
|
|
167
182
|
host.go(path({ kind: 'screen', screen: 'issues' }));
|
|
168
183
|
};
|
|
169
|
-
return (_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", items: "center", children: _jsxs(YStack, { width: "100%", maxW: 720, gap: "$3", children: [_jsx(Head, { title: "Projects", says: "Boards on the forge. A board is a repository that has work on it." }), list.error ? (_jsx(Note, { children: list.error.message })) : !signed ? (_jsx(Note, { children: "Sign in to see this organization's boards." })) : list.loading && list.value.length === 0 ? (_jsx(Note, { children: "Reading the forge\u2026" })) : list.value.length === 0 ? (_jsx(Note, { children: "No boards yet. A repository shows up here once it has an issue." })) : (_jsx(YStack, { gap: "$2", children: list.value.map((b) => (_jsx(Row, { label: `Open ${b.name}`, onPress: () => open(b), children: _jsxs(YStack, { flex: 1, minW: 0, gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", numberOfLines: 1, children: b.name }), b.description ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: b.description })) : (_jsx(SizableText, { size: "$1", color: "$soft", children: b.key }))] }) }, b.key))) }))] }) }));
|
|
184
|
+
return (_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", items: "center", children: _jsxs(YStack, { width: "100%", maxW: 720, gap: "$3", children: [_jsx(Head, { title: "Projects", says: "Boards on the forge. A board is a repository that has work on it." }), list.error ? (_jsx(Note, { children: list.error.message })) : !signed ? (_jsx(Note, { children: "Sign in to see this organization's boards." })) : list.loading && list.value.length === 0 ? (_jsx(Note, { children: "Reading the forge\u2026" })) : list.value.length === 0 ? (_jsxs(YStack, { gap: "$3", py: "$2", children: [_jsx(Note, { children: "No boards yet. A repository shows up here once it has an issue." }), _jsxs(XStack, { gap: "$2", pt: "$1", children: [_jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'codebases' })), children: "View repositories" }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'sync' })), children: "Sync from GitHub" })] })] })) : (_jsx(YStack, { gap: "$2", children: list.value.map((b) => (_jsx(Row, { label: `Open ${b.name}`, onPress: () => open(b), children: _jsxs(YStack, { flex: 1, minW: 0, gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", numberOfLines: 1, children: b.name }), b.description ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: b.description })) : (_jsx(SizableText, { size: "$1", color: "$soft", children: b.key }))] }) }, b.key))) }))] }) }));
|
|
170
185
|
}
|
|
171
186
|
function Issues() {
|
|
172
187
|
const host = useHost();
|
|
@@ -196,5 +211,5 @@ function Issues() {
|
|
|
196
211
|
}
|
|
197
212
|
host.go('');
|
|
198
213
|
};
|
|
199
|
-
return (_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", items: "center", children: _jsxs(YStack, { width: "100%", maxW: 720, gap: "$3", children: [_jsx(Head, { title: "Issues", says: board ? `Open work on ${board}. Choosing one starts the next run on that codebase.` : 'Open work across every board. Choosing one starts the next run on that codebase.' }), board ? (_jsx(XStack, { render: "button", onPress: () => setBoard(''), "aria-label": "Show every board", self: "flex-start", children: _jsx(SizableText, { size: "$1", color: "$soft", children: "All boards" }) })) : null, list.error ? (_jsx(Note, { children: list.error.message })) : !signed ? (_jsx(Note, { children: "Sign in to see this organization's issues." })) : list.loading && list.value.length === 0 ? (_jsx(Note, { children: "Reading the forge\u2026" })) : list.value.length === 0 ? (_jsx(Note, { children: "Nothing open." })) : (_jsx(YStack, { gap: "$2", children: list.value.map((w) => (_jsxs(Row, { label: `Build ${w.identifier || w.title}`, onPress: () => open(w), children: [_jsx(SizableText, { size: "$1", color: "$soft", width: 88, numberOfLines: 1, children: w.identifier || w.kind }), _jsx(SizableText, { size: "$3", color: "$ink", flex: 1, minW: 0, numberOfLines: 1, children: w.title }), _jsx(SizableText, { size: "$1", color: "$soft", children: STATUS[w.status] ?? w.status })] }, w.id))) }))] }) }));
|
|
214
|
+
return (_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", items: "center", children: _jsxs(YStack, { width: "100%", maxW: 720, gap: "$3", children: [_jsx(Head, { title: "Issues", says: board ? `Open work on ${board}. Choosing one starts the next run on that codebase.` : 'Open work across every board. Choosing one starts the next run on that codebase.' }), board ? (_jsx(XStack, { render: "button", onPress: () => setBoard(''), "aria-label": "Show every board", self: "flex-start", children: _jsx(SizableText, { size: "$1", color: "$soft", children: "All boards" }) })) : null, list.error ? (_jsx(Note, { children: list.error.message })) : !signed ? (_jsx(Note, { children: "Sign in to see this organization's issues." })) : list.loading && list.value.length === 0 ? (_jsx(Note, { children: "Reading the forge\u2026" })) : list.value.length === 0 ? (_jsxs(YStack, { gap: "$3", py: "$2", children: [_jsx(Note, { children: "Nothing open." }), _jsxs(XStack, { gap: "$2", pt: "$1", children: [_jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'codebases' })), children: "View repositories" }), _jsx(Button, { size: "sm", onPress: () => host.go(''), children: "Start a new run" })] })] })) : (_jsx(YStack, { gap: "$2", children: list.value.map((w) => (_jsxs(Row, { label: `Build ${w.identifier || w.title}`, onPress: () => open(w), children: [_jsx(SizableText, { size: "$1", color: "$soft", width: 88, numberOfLines: 1, children: w.identifier || w.kind }), _jsx(SizableText, { size: "$3", color: "$ink", flex: 1, minW: 0, numberOfLines: 1, children: w.title }), _jsx(SizableText, { size: "$1", color: "$soft", children: STATUS[w.status] ?? w.status })] }, w.id))) }))] }) }));
|
|
200
215
|
}
|
package/lib/settings/billing.js
CHANGED
|
@@ -6,14 +6,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
6
6
|
*/
|
|
7
7
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
8
8
|
import { CreditCard, Download, Plus, X } from '@hanzogui/lucide-icons-2';
|
|
9
|
-
import { Button, Dialog, DialogContent, DialogTitle, toast } from '@hanzo/ui';
|
|
10
|
-
import { useState } from 'react';
|
|
11
|
-
import { cancel, current, detach, invoices, label, methods, money, pdf, reactivate, subscriptions, } from '../api/billing.js';
|
|
9
|
+
import { Button, Dialog, DialogContent, DialogTitle, Input, toast } from '@hanzo/ui';
|
|
10
|
+
import { useEffect, useState } from 'react';
|
|
11
|
+
import { cancel, chosen, current, detach, formatAddress, invoices, label, makeDefault, methods, money, pdf, reactivate, subscriptions, updateMethod, } from '../api/billing.js';
|
|
12
12
|
import { useRead } from '../data.js';
|
|
13
13
|
import { useHost, useTarget } from '../host.js';
|
|
14
14
|
import { path } from '../route.js';
|
|
15
15
|
import { AddCard } from './card.js';
|
|
16
|
-
import { Card, day, Group, Heading, Note, Row, Soft } from './ui.js';
|
|
16
|
+
import { Card, day, Field, Group, Heading, Note, Row, Soft } from './ui.js';
|
|
17
17
|
export function Billing() {
|
|
18
18
|
const host = useHost();
|
|
19
19
|
const t = useTarget();
|
|
@@ -23,6 +23,7 @@ export function Billing() {
|
|
|
23
23
|
const bills = useRead(signed ? () => invoices(t) : null, [], [t, signed]);
|
|
24
24
|
const [adding, setAdding] = useState(false);
|
|
25
25
|
const [ending, setEnding] = useState(false);
|
|
26
|
+
const [editingBilling, setEditingBilling] = useState(null);
|
|
26
27
|
const [open, setOpen] = useState(null);
|
|
27
28
|
const [working, setWorking] = useState(false);
|
|
28
29
|
const [note, setNote] = useState('');
|
|
@@ -31,6 +32,7 @@ export function Billing() {
|
|
|
31
32
|
}
|
|
32
33
|
const plan = current(subs.value);
|
|
33
34
|
const empty = !cards.error && !cards.loading && cards.value.length === 0;
|
|
35
|
+
const defaultCard = chosen(cards.value);
|
|
34
36
|
// A done act is a toast; a refused one stays on the page, beside what was tried.
|
|
35
37
|
const act = async (what, done, after) => {
|
|
36
38
|
setWorking(true);
|
|
@@ -51,14 +53,85 @@ export function Billing() {
|
|
|
51
53
|
? subs.error.message
|
|
52
54
|
: plan
|
|
53
55
|
? terms(plan)
|
|
54
|
-
: 'No paid plan. Usage is paid from the balance.' })] }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'plans' })), children: "Adjust plan" })] }), _jsxs(Group, { title: "Payment", detail: "The card a plan renews on and a top-up charges.", action: host.admin && (cards.error || cards.value.length > 0) ? (_jsxs(Button, { size: "sm", variant: "outline", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 14 }), " Add card"] })) : undefined, children: [cards.error ? (_jsx(Soft, { children: cards.error.message })) : cards.loading && !cards.value.length ? (_jsx(Soft, { children: "Reading cards\u2026" })) : empty ? (_jsxs(XStack, { items: "center", gap: "$3", px: "$4", py: "$4", borderWidth: 1, borderColor: "$borderColor", rounded: "$3", flexWrap: "wrap", children: [_jsx(CreditCard, { size: 20 }), _jsxs(YStack, { flex: 1, minW: 180, gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: "No card on file." }), _jsx(SizableText, { size: "$2", color: "$soft", children: host.admin ? 'Add one to pay for a plan or a top-up. Adding it charges nothing.' : 'An org admin adds cards.' })] }), host.admin ? (_jsxs(Button, { onPress: () => setAdding(true), children: [_jsx(Plus, { size: 16 }), " Add card"] })) : null] })) : (_jsx(Card, { children: cards.value.map((m, i) => (_jsx(Row, { first: i === 0, leading: _jsx(CreditCard, { size: 16 }), title: label(m), detail: [
|
|
56
|
+
: 'No paid plan. Usage is paid from the balance.' })] }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'plans' })), children: "Adjust plan" })] }), _jsxs(Group, { title: "Payment", detail: "The card a plan renews on and a top-up charges.", action: host.admin && (cards.error || cards.value.length > 0) ? (_jsxs(Button, { size: "sm", variant: "outline", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 14 }), " Add card"] })) : undefined, children: [cards.error ? (_jsx(Soft, { children: cards.error.message })) : cards.loading && !cards.value.length ? (_jsx(Soft, { children: "Reading cards\u2026" })) : empty ? (_jsxs(XStack, { items: "center", gap: "$3", px: "$4", py: "$4", borderWidth: 1, borderColor: "$borderColor", rounded: "$3", flexWrap: "wrap", children: [_jsx(CreditCard, { size: 20 }), _jsxs(YStack, { flex: 1, minW: 180, gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: "No card on file." }), _jsx(SizableText, { size: "$2", color: "$soft", children: host.admin ? 'Add one to pay for a plan or a top-up. Adding it charges nothing.' : 'An org admin adds cards.' })] }), host.admin ? (_jsxs(Button, { variant: "primary", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 16 }), " Add card"] })) : null] })) : (_jsx(Card, { children: cards.value.map((m, i) => (_jsx(Row, { first: i === 0, leading: _jsx(CreditCard, { size: 16 }), title: label(m), detail: [
|
|
57
|
+
m.expires ? `Expires ${m.expires}` : '',
|
|
58
|
+
m.default ? 'Default' : '',
|
|
59
|
+
m.billingAddress ? formatAddress(m.billingAddress) : '',
|
|
60
|
+
]
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
.join(' · ') || undefined, trailing: host.admin ? (_jsxs(XStack, { gap: "$2", items: "center", children: [!m.default ? (_jsx(Button, { size: "sm", variant: "ghost", "aria-label": `Make ${label(m)} default`, disabled: working, onPress: () => void act(() => makeDefault(t, m.id), `${label(m)} is now the default`, cards.reload), children: "Make default" })) : null, _jsx(Button, { size: "sm", variant: "ghost", "aria-label": `Edit billing info for ${label(m)}`, disabled: working, onPress: () => setEditingBilling(m), children: "Edit info" }), _jsx(Button, { size: "sm", variant: "ghost", "aria-label": `Remove ${label(m)}`, disabled: working, onPress: () => void act(() => detach(t, m.id), `${label(m)} is removed`, cards.reload), children: "Remove" })] })) : undefined }, m.id))) })), !host.admin && cards.value.length ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "An org admin adds and removes cards." })) : null] }), _jsx(Group, { title: "Billing details", detail: "Information printed on invoices and payment receipts.", action: host.admin && defaultCard ? (_jsx(Button, { size: "sm", variant: "outline", onPress: () => setEditingBilling(defaultCard), children: defaultCard.billingAddress ? 'Edit details' : 'Add billing info' })) : undefined, children: defaultCard?.billingAddress ? (_jsx(Card, { children: _jsxs(YStack, { p: "$3", gap: "$1.5", children: [defaultCard.billingAddress.name ? (_jsx(SizableText, { size: "$3", color: "$ink", style: { fontWeight: 600 }, children: defaultCard.billingAddress.name })) : null, defaultCard.billingAddress.line1 ? (_jsxs(SizableText, { size: "$2", color: "$soft", children: [defaultCard.billingAddress.line1, defaultCard.billingAddress.line2 ? `, ${defaultCard.billingAddress.line2}` : ''] })) : null, defaultCard.billingAddress.city || defaultCard.billingAddress.state || defaultCard.billingAddress.postalCode ? (_jsx(SizableText, { size: "$2", color: "$soft", children: [defaultCard.billingAddress.city, defaultCard.billingAddress.state, defaultCard.billingAddress.postalCode]
|
|
63
|
+
.filter(Boolean)
|
|
64
|
+
.join(' ') })) : null, defaultCard.billingAddress.country ? (_jsx(SizableText, { size: "$2", color: "$soft", children: defaultCard.billingAddress.country })) : null] }) })) : (_jsxs(XStack, { items: "center", justify: "space-between", gap: "$3", px: "$4", py: "$3", borderWidth: 1, borderColor: "$borderColor", rounded: "$3", flexWrap: "wrap", children: [_jsxs(YStack, { flex: 1, minW: 180, gap: "$1", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: "No billing address on file." }), _jsx(SizableText, { size: "$1", color: "$soft", children: "Add your company name and address so they appear on monthly invoices and receipts." })] }), host.admin ? (_jsxs(Button, { size: "sm", variant: "outline", onPress: () => (defaultCard ? setEditingBilling(defaultCard) : setAdding(true)), children: [_jsx(Plus, { size: 14 }), " ", defaultCard ? 'Add billing info' : 'Set billing info'] })) : null] })) }), _jsx(Group, { title: "Invoices", children: bills.error ? (_jsx(Soft, { children: bills.error.message })) : bills.loading && !bills.value.length ? (_jsx(Soft, { children: "Reading invoices\u2026" })) : bills.value.length === 0 ? (_jsx(Card, { children: _jsx(Soft, { children: "No invoices yet." }) })) : (_jsxs(Card, { children: [_jsxs(XStack, { px: "$3", py: "$2", gap: "$3", children: [_jsx(SizableText, { flex: 1, size: "$1", color: "$soft", children: "Date" }), _jsx(SizableText, { width: 90, size: "$1", color: "$soft", style: { textAlign: 'right' }, children: "Total" }), _jsx(SizableText, { width: 70, size: "$1", color: "$soft", "$max-sm": { display: 'none' }, children: "Status" }), _jsx(YStack, { width: 64 })] }), bills.value.map((b) => (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: day(b.date) || b.number || b.id }), _jsx(SizableText, { width: 90, size: "$2", color: "$ink", style: { textAlign: 'right' }, children: money(b.total, b.currency) }), _jsx(SizableText, { width: 70, size: "$1", color: "$soft", "$max-sm": { display: 'none' }, children: word(b.status) }), _jsx(XStack, { width: 64, justify: "flex-end", children: _jsx(Button, { size: "sm", variant: "ghost", "aria-label": `View invoice ${b.number || day(b.date)}`, onPress: () => setOpen(b), children: "View" }) })] }, b.id)))] })) }), plan ? (_jsx(Group, { title: "Cancellation", children: _jsx(Card, { children: _jsx(Row, { first: true, title: plan.ending ? `Your plan ends on ${day(plan.ends)}` : 'Cancel plan', detail: plan.ending ? 'Keep it, and it renews as before.' : 'It runs to the end of the period you paid for, then ends.', trailing: !host.admin ? undefined : plan.ending ? (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void act(() => reactivate(t, plan.id), 'Your plan renews as before', subs.reload), children: "Keep plan" })) : (_jsx(Button, { size: "sm", variant: "destructive", disabled: working, onPress: () => setEnding(true), children: "Cancel" })) }) }) })) : null, _jsx(Note, { children: note }), _jsx(AddCard, { open: adding, onOpenChange: setAdding, onSaved: (m) => {
|
|
55
65
|
cards.reload();
|
|
56
66
|
toast.success(`${label(m)} is saved`);
|
|
67
|
+
} }), _jsx(EditBilling, { open: editingBilling !== null, onOpenChange: (o) => !o && setEditingBilling(null), card: editingBilling, onSaved: () => {
|
|
68
|
+
cards.reload();
|
|
57
69
|
} }), plan ? (_jsx(Ending, { plan: plan, open: ending, working: working, onOpenChange: setEnding, onConfirm: () => void act(() => cancel(t, plan.id), `Your plan ends on ${day(plan.ends)}`, () => {
|
|
58
70
|
setEnding(false);
|
|
59
71
|
subs.reload();
|
|
60
72
|
}) })) : null, open ? _jsx(Bill, { invoice: open, onClose: () => setOpen(null) }) : null] }));
|
|
61
73
|
}
|
|
74
|
+
function EditBilling({ open, onOpenChange, card, onSaved, }) {
|
|
75
|
+
const t = useTarget();
|
|
76
|
+
const [name, setName] = useState('');
|
|
77
|
+
const [line1, setLine1] = useState('');
|
|
78
|
+
const [line2, setLine2] = useState('');
|
|
79
|
+
const [city, setCity] = useState('');
|
|
80
|
+
const [stateVal, setStateVal] = useState('');
|
|
81
|
+
const [postalCode, setPostalCode] = useState('');
|
|
82
|
+
const [country, setCountry] = useState('US');
|
|
83
|
+
const [working, setWorking] = useState(false);
|
|
84
|
+
const [note, setNote] = useState('');
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (card?.billingAddress) {
|
|
87
|
+
setName(card.billingAddress.name ?? '');
|
|
88
|
+
setLine1(card.billingAddress.line1 ?? '');
|
|
89
|
+
setLine2(card.billingAddress.line2 ?? '');
|
|
90
|
+
setCity(card.billingAddress.city ?? '');
|
|
91
|
+
setStateVal(card.billingAddress.state ?? '');
|
|
92
|
+
setPostalCode(card.billingAddress.postalCode ?? '');
|
|
93
|
+
setCountry(card.billingAddress.country ?? 'US');
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
setName('');
|
|
97
|
+
setLine1('');
|
|
98
|
+
setLine2('');
|
|
99
|
+
setCity('');
|
|
100
|
+
setStateVal('');
|
|
101
|
+
setPostalCode('');
|
|
102
|
+
setCountry('US');
|
|
103
|
+
}
|
|
104
|
+
setNote('');
|
|
105
|
+
}, [card]);
|
|
106
|
+
const save = async () => {
|
|
107
|
+
if (!card)
|
|
108
|
+
return;
|
|
109
|
+
setWorking(true);
|
|
110
|
+
setNote('');
|
|
111
|
+
try {
|
|
112
|
+
const address = {
|
|
113
|
+
name: name.trim() || undefined,
|
|
114
|
+
line1: line1.trim() || undefined,
|
|
115
|
+
line2: line2.trim() || undefined,
|
|
116
|
+
city: city.trim() || undefined,
|
|
117
|
+
state: stateVal.trim() || undefined,
|
|
118
|
+
postalCode: postalCode.trim() || undefined,
|
|
119
|
+
country: country.trim() || undefined,
|
|
120
|
+
};
|
|
121
|
+
await updateMethod(t, card.id, { billingAddress: address });
|
|
122
|
+
onSaved();
|
|
123
|
+
onOpenChange(false);
|
|
124
|
+
toast.success('Billing details updated');
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
setNote(e.message);
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
setWorking(false);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 460, showCloseButton: false, children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [_jsx(DialogTitle, { children: "Billing details" }), _jsx(XStack, { render: "button", "aria-label": "Close", p: "$1", onPress: () => onOpenChange(false), children: _jsx(X, { size: 16 }) })] }), _jsxs(YStack, { gap: "$3", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "These details appear on your invoices and receipts." }), _jsx(Field, { label: "Business or personal name", children: _jsx(Input, { value: name, onChangeText: setName, placeholder: "e.g. Webby AI, Inc. or Joshua Lynch" }) }), _jsx(Field, { label: "Address line 1", children: _jsx(Input, { value: line1, onChangeText: setLine1, placeholder: "Street address or P.O. Box" }) }), _jsx(Field, { label: "Address line 2", hint: "Optional", children: _jsx(Input, { value: line2, onChangeText: setLine2, placeholder: "Suite, unit, building, floor" }) }), _jsxs(XStack, { gap: "$2", children: [_jsx(YStack, { flex: 1, children: _jsx(Field, { label: "City", children: _jsx(Input, { value: city, onChangeText: setCity, placeholder: "City" }) }) }), _jsx(YStack, { width: 100, children: _jsx(Field, { label: "State", children: _jsx(Input, { value: stateVal, onChangeText: setStateVal, placeholder: "State / Prov" }) }) })] }), _jsxs(XStack, { gap: "$2", children: [_jsx(YStack, { flex: 1, children: _jsx(Field, { label: "Postal / ZIP code", children: _jsx(Input, { value: postalCode, onChangeText: setPostalCode, placeholder: "ZIP or Postal Code" }) }) }), _jsx(YStack, { width: 110, children: _jsx(Field, { label: "Country", children: _jsx(Input, { value: country, onChangeText: setCountry, placeholder: "Country (US)" }) }) })] }), note ? (_jsx(SizableText, { size: "$2", color: "$red10", role: "alert", children: note })) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", pt: "$2", children: [_jsx(Button, { size: "sm", variant: "ghost", disabled: working, onPress: () => onOpenChange(false), children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void save(), children: "Save details" })] })] })] }) }));
|
|
134
|
+
}
|
|
62
135
|
/** What the plan costs, and when it renews or ends. */
|
|
63
136
|
function terms(p) {
|
|
64
137
|
const cost = `${money(p.price * p.seats, p.currency)} a ${p.interval}${p.seats > 1 ? ` for ${p.seats} seats` : ''}.`;
|
package/lib/settings/card.js
CHANGED
|
@@ -8,12 +8,13 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
8
8
|
* it will be charged on.
|
|
9
9
|
*/
|
|
10
10
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
11
|
-
import { X } from '@hanzogui/lucide-icons-2';
|
|
12
|
-
import { Button, Dialog, DialogContent, DialogTitle } from '@hanzo/ui';
|
|
11
|
+
import { ChevronDown, ChevronUp, X } from '@hanzogui/lucide-icons-2';
|
|
12
|
+
import { Button, Dialog, DialogContent, DialogTitle, Input } from '@hanzo/ui';
|
|
13
13
|
import { useEffect, useRef, useState } from 'react';
|
|
14
14
|
import { processor, save } from '../api/billing.js';
|
|
15
15
|
import { useRead } from '../data.js';
|
|
16
16
|
import { useTarget } from '../host.js';
|
|
17
|
+
import { Field as FormField } from './ui.js';
|
|
17
18
|
/** Anything but production loads the sandbox SDK, so a misread setting never arms real charges. */
|
|
18
19
|
export const sdk = (environment) => environment === 'production' ? 'https://web.squarecdn.com/v1/square.js' : 'https://sandbox.web.squarecdn.com/v1/square.js';
|
|
19
20
|
const loads = new Map();
|
|
@@ -47,6 +48,13 @@ export function AddCard({ open, onOpenChange, onSaved }) {
|
|
|
47
48
|
const [mounted, setMounted] = useState(false);
|
|
48
49
|
const [working, setWorking] = useState(false);
|
|
49
50
|
const [note, setNote] = useState('');
|
|
51
|
+
const [name, setName] = useState('');
|
|
52
|
+
const [postalCode, setPostalCode] = useState('');
|
|
53
|
+
const [moreAddress, setMoreAddress] = useState(false);
|
|
54
|
+
const [line1, setLine1] = useState('');
|
|
55
|
+
const [city, setCity] = useState('');
|
|
56
|
+
const [stateVal, setStateVal] = useState('');
|
|
57
|
+
const [country, setCountry] = useState('US');
|
|
50
58
|
const p = cfg.value;
|
|
51
59
|
useEffect(() => {
|
|
52
60
|
if (!open || !box || !ready(p))
|
|
@@ -87,7 +95,17 @@ export function AddCard({ open, onOpenChange, onSaved }) {
|
|
|
87
95
|
const r = await f.tokenize();
|
|
88
96
|
if (r.status !== 'OK' || !r.token)
|
|
89
97
|
throw new Error(r.errors?.map((e) => e.message).filter(Boolean).join('; ') || 'The card details are incomplete');
|
|
90
|
-
const
|
|
98
|
+
const billingAddress = name.trim() || postalCode.trim() || line1.trim() || city.trim() || stateVal.trim()
|
|
99
|
+
? {
|
|
100
|
+
name: name.trim() || undefined,
|
|
101
|
+
postalCode: postalCode.trim() || undefined,
|
|
102
|
+
line1: line1.trim() || undefined,
|
|
103
|
+
city: city.trim() || undefined,
|
|
104
|
+
state: stateVal.trim() || undefined,
|
|
105
|
+
country: country.trim() || 'US',
|
|
106
|
+
}
|
|
107
|
+
: undefined;
|
|
108
|
+
const m = await save(t, r.token, billingAddress);
|
|
91
109
|
onSaved(m);
|
|
92
110
|
onOpenChange(false);
|
|
93
111
|
}
|
|
@@ -99,7 +117,7 @@ export function AddCard({ open, onOpenChange, onSaved }) {
|
|
|
99
117
|
}
|
|
100
118
|
};
|
|
101
119
|
const why = cfg.error ? cfg.error.message : !cfg.loading && !ready(p) ? 'This deployment takes no cards yet.' : '';
|
|
102
|
-
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW:
|
|
120
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 460, showCloseButton: false, children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [_jsx(DialogTitle, { children: "Add a card" }), _jsx(XStack, { render: "button", "aria-label": "Close", p: "$1", onPress: () => onOpenChange(false), children: _jsx(X, { size: 16 }) })] }), _jsxs(YStack, { gap: "$3", children: [why ? (_jsx(SizableText, { size: "$2", color: "$soft", children: why })) : (_jsxs(_Fragment, { children: [_jsxs(YStack, { minH: 96, "aria-label": "Card details", children: [_jsx("div", { ref: setBox, style: { minHeight: 96 } }), !mounted && !note ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "Loading the card form\u2026" })) : null] }), _jsxs(XStack, { gap: "$2", children: [_jsx(YStack, { flex: 1, children: _jsx(FormField, { label: "Name on card", hint: "Optional", children: _jsx(Input, { value: name, onChangeText: setName, placeholder: "Name or business" }) }) }), _jsx(YStack, { width: 120, children: _jsx(FormField, { label: "Postal code", hint: "Optional", children: _jsx(Input, { value: postalCode, onChangeText: setPostalCode, placeholder: "ZIP / Postal" }) }) })] }), _jsx(XStack, { children: _jsxs(Button, { size: "sm", variant: "ghost", onPress: () => setMoreAddress(!moreAddress), children: [moreAddress ? _jsx(ChevronUp, { size: 14 }) : _jsx(ChevronDown, { size: 14 }), moreAddress ? 'Hide address' : 'Add billing address'] }) }), moreAddress ? (_jsxs(YStack, { gap: "$2", pt: "$1", children: [_jsx(FormField, { label: "Street address", children: _jsx(Input, { value: line1, onChangeText: setLine1, placeholder: "Address line 1" }) }), _jsxs(XStack, { gap: "$2", children: [_jsx(YStack, { flex: 1, children: _jsx(FormField, { label: "City", children: _jsx(Input, { value: city, onChangeText: setCity, placeholder: "City" }) }) }), _jsx(YStack, { width: 90, children: _jsx(FormField, { label: "State", children: _jsx(Input, { value: stateVal, onChangeText: setStateVal, placeholder: "State" }) }) }), _jsx(YStack, { width: 90, children: _jsx(FormField, { label: "Country", children: _jsx(Input, { value: country, onChangeText: setCountry, placeholder: "Country (US)" }) }) })] })] })) : null, _jsx(SizableText, { size: "$1", color: "$soft", children: p && p.environment !== 'production'
|
|
103
121
|
? 'Sandbox: use a test card. Nothing is charged.'
|
|
104
122
|
: 'The card number goes to the payment processor, never to Hanzo. Saving it charges nothing.' })] })), note ? (_jsx(SizableText, { size: "$2", color: "$red10", role: "alert", children: note })) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => onOpenChange(false), children: "Cancel" }), _jsx(Button, { size: "sm", disabled: !mounted || working, onPress: () => void add(), children: "Save card" })] })] })] }) }));
|
|
105
123
|
}
|
package/lib/settings/machines.js
CHANGED
|
@@ -7,11 +7,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
7
7
|
* mints its claim key or removes it.
|
|
8
8
|
*/
|
|
9
9
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
10
|
-
import { MoreHorizontal, Plus } from '@hanzogui/lucide-icons-2';
|
|
10
|
+
import { Activity, ChevronDown, ChevronUp, Cloud, Cpu, HardDrive, Laptop, MoreHorizontal, Plus, Server, Terminal, Zap, } 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
13
|
import { useEffect, useState } from 'react';
|
|
14
|
-
import { add, change,
|
|
14
|
+
import { add, change, formatBytes, formatPercent, formatRelative, key, KINDS, 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';
|
|
@@ -32,6 +32,44 @@ function Dot({ m }) {
|
|
|
32
32
|
const on = state(m) === 'online';
|
|
33
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 }));
|
|
34
34
|
}
|
|
35
|
+
function KindIcon({ kind, size = 16 }) {
|
|
36
|
+
switch (kind) {
|
|
37
|
+
case 'laptop':
|
|
38
|
+
return _jsx(Laptop, { size: size });
|
|
39
|
+
case 'gpu':
|
|
40
|
+
return _jsx(Cpu, { size: size });
|
|
41
|
+
case 'cloud':
|
|
42
|
+
return _jsx(Cloud, { size: size });
|
|
43
|
+
case 'cluster':
|
|
44
|
+
return _jsx(Server, { size: size });
|
|
45
|
+
default:
|
|
46
|
+
return _jsx(Terminal, { size: size });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function StatusBadge({ m }) {
|
|
50
|
+
const s = state(m);
|
|
51
|
+
if (!s)
|
|
52
|
+
return null;
|
|
53
|
+
let color = '$soft';
|
|
54
|
+
let text = s;
|
|
55
|
+
if (s === 'online') {
|
|
56
|
+
color = '$green10';
|
|
57
|
+
text = 'Online';
|
|
58
|
+
}
|
|
59
|
+
else if (s === 'draining') {
|
|
60
|
+
color = '$yellow10';
|
|
61
|
+
text = 'Draining';
|
|
62
|
+
}
|
|
63
|
+
else if (s === 'not seen yet') {
|
|
64
|
+
color = '$blue10';
|
|
65
|
+
text = 'Not seen yet';
|
|
66
|
+
}
|
|
67
|
+
else if (s === 'offline') {
|
|
68
|
+
color = '$soft';
|
|
69
|
+
text = 'Offline';
|
|
70
|
+
}
|
|
71
|
+
return (_jsxs(XStack, { px: "$2", py: "$0.5", rounded: "$10", borderWidth: 1, borderColor: color, items: "center", gap: "$1.5", children: [_jsx(Dot, { m: m }), _jsx(SizableText, { size: "$1", color: color, style: { fontWeight: 500 }, children: text })] }));
|
|
72
|
+
}
|
|
35
73
|
export function Machines() {
|
|
36
74
|
const host = useHost();
|
|
37
75
|
const t = useTarget();
|
|
@@ -42,6 +80,8 @@ export function Machines() {
|
|
|
42
80
|
const [removing, setRemoving] = useState(null);
|
|
43
81
|
const [keying, setKeying] = useState(null);
|
|
44
82
|
const [minted, setMinted] = useState(null);
|
|
83
|
+
const [expandedId, setExpandedId] = useState(null);
|
|
84
|
+
const [showAdvancedLink, setShowAdvancedLink] = useState(false);
|
|
45
85
|
const [note, setNote] = useState('');
|
|
46
86
|
// Read again on the agent's beat; "Reading machines…" stays for the first read only.
|
|
47
87
|
const again = list.reload;
|
|
@@ -65,15 +105,20 @@ export function Machines() {
|
|
|
65
105
|
setNote(e.message);
|
|
66
106
|
}
|
|
67
107
|
};
|
|
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" })] }),
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
108
|
+
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" })] }), _jsxs(XStack, { items: "center", justify: "space-between", flexWrap: "wrap", gap: "$2", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsxs(Button, { size: "sm", variant: "ghost", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 14 }), " Register one by hand"] }), _jsxs(Button, { size: "sm", variant: "ghost", onPress: () => setShowAdvancedLink(!showAdvancedLink), children: [showAdvancedLink ? _jsx(ChevronUp, { size: 14 }) : _jsx(ChevronDown, { size: 14 }), showAdvancedLink ? 'Hide options' : 'More connection options'] })] }), _jsx(SizableText, { size: "$1", color: "$soft", children: "Supported: macOS, Linux, Windows, NVIDIA CUDA GPUs, Cloud VMs" })] }), showAdvancedLink ? (_jsxs(YStack, { gap: "$2.5", p: "$3", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$raised", children: [_jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: "1. Background daemon (system service)" }), _jsx(SizableText, { size: "$1", color: "$soft", children: "Run worker continuously in the background without keeping a terminal open:" }), _jsxs(XStack, { items: "center", justify: "space-between", px: "$2.5", py: "$1.5", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$ink", style: mono, children: "hanzo code --serve" }), _jsx(CopyButton, { value: "hanzo code --serve", label: "Copy daemon command" })] })] }), _jsxs(YStack, { gap: "$1", pt: "$1", children: [_jsx(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: "2. Install CLI from shell" }), _jsxs(XStack, { items: "center", justify: "space-between", px: "$2.5", py: "$1.5", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$ink", style: mono, children: "curl -fsSL https://hanzo.sh | sh" }), _jsx(CopyButton, { value: "curl -fsSL https://hanzo.sh | sh", label: "Copy install command" })] })] })] })) : null] }), _jsxs(Group, { title: "Linked", children: [list.value === UNREAD ? (_jsx(Soft, { children: list.error ? list.error.message : 'Reading machines…' })) : list.value.length === 0 ? (_jsxs(YStack, { gap: "$3", children: [_jsx(Soft, { children: "No machine is linked yet." }), _jsx(Card, { children: _jsxs(YStack, { p: "$4", gap: "$3", children: [_jsxs(XStack, { items: "center", gap: "$2.5", children: [_jsx(Server, { size: 20 }), _jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$3", color: "$ink", style: { fontWeight: 600 }, children: "Connect your first computer or GPU server" }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Execute code, runs, and local models directly on your hardware with full privacy and zero cloud fees." })] })] }), _jsxs(YStack, { gap: "$2", pt: "$1", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$1", color: "$soft", style: { ...mono, width: 20 }, children: "1." }), _jsxs(SizableText, { size: "$2", color: "$ink", children: ["Install Hanzo CLI: ", _jsx("span", { style: mono, children: "curl -fsSL https://hanzo.sh | sh" })] })] }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$1", color: "$soft", style: { ...mono, width: 20 }, children: "2." }), _jsxs(SizableText, { size: "$2", color: "$ink", children: ["Sign in with your account: ", _jsx("span", { style: mono, children: "hanzo login" })] })] }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$1", color: "$soft", style: { ...mono, width: 20 }, children: "3." }), _jsxs(SizableText, { size: "$2", color: "$ink", children: ["Link the machine: ", _jsx("span", { style: mono, children: "hanzo link" })] })] })] }), _jsx(XStack, { pt: "$1", children: _jsxs(Button, { size: "sm", variant: "outline", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 14 }), " Register one by hand"] }) })] }) })] })) : (_jsx(Card, { children: list.value.map((m, i) => {
|
|
109
|
+
const isExpanded = expandedId === m.id;
|
|
110
|
+
const hasTelemetry = Boolean(m.metrics || m.spec);
|
|
111
|
+
return (_jsxs(YStack, { borderTopWidth: i > 0 ? 1 : 0, borderColor: "$borderColor", children: [_jsx(Row, { first: i === 0, leading: _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Dot, { m: m }), _jsx(KindIcon, { kind: m.kind, size: 16 })] }), title: m.label, detail: about(m), trailing: _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(StatusBadge, { m: m }), m.serving ? (_jsx(XStack, { px: "$1.5", py: "$0.5", rounded: "$10", borderWidth: 1, borderColor: "$green10", children: _jsx(SizableText, { size: "$1", color: "$green10", children: "Serving" }) })) : null, m.running > 0 ? (_jsxs(XStack, { px: "$1.5", py: "$0.5", rounded: "$10", bg: "$raised", items: "center", gap: "$1", children: [_jsx(Activity, { size: 12, color: "$green10" }), _jsxs(SizableText, { size: "$1", color: "$ink", children: [m.running, " active"] })] })) : null, _jsx(CopyButton, { value: `hanzo link --target ${m.id}`, label: `Copy link command for ${m.label}` }), _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: [
|
|
112
|
+
{ key: 'details', label: isExpanded ? 'Hide details' : 'Show details', onSelect: () => setExpandedId(isExpanded ? null : m.id) },
|
|
113
|
+
{ key: 'rename', label: 'Rename', onSelect: () => setRenaming(m) },
|
|
114
|
+
m.status === 'draining'
|
|
115
|
+
? { key: 'online', label: 'Take runs again', onSelect: () => void drain(m, 'online') }
|
|
116
|
+
: { key: 'drain', label: 'Drain', description: 'Finish what it has, take nothing new', onSelect: () => void drain(m, 'draining') },
|
|
117
|
+
{ key: 'key', label: 'Claim key', onSelect: () => setKeying(m) },
|
|
118
|
+
{ type: 'separator' },
|
|
119
|
+
{ key: 'remove', label: 'Remove', destructive: true, onSelect: () => setRemoving(m) },
|
|
120
|
+
] })] }) }), isExpanded || hasTelemetry ? (_jsxs(YStack, { px: "$4", pb: "$3", pt: "$1", gap: "$2.5", children: [m.spec ? (_jsxs(XStack, { flexWrap: "wrap", gap: "$2", items: "center", children: [m.spec.os ? (_jsxs(XStack, { px: "$2", py: "$1", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", items: "center", gap: "$1.5", children: [_jsx(Terminal, { size: 12 }), _jsxs(SizableText, { size: "$1", color: "$ink", children: [m.spec.os, m.spec.arch ? ` · ${m.spec.arch}` : ''] })] })) : null, m.spec.cpus ? (_jsxs(XStack, { px: "$2", py: "$1", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", items: "center", gap: "$1.5", children: [_jsx(Cpu, { size: 12 }), _jsxs(SizableText, { size: "$1", color: "$ink", children: [m.spec.cpus, " cores"] })] })) : null, m.spec.memory ? (_jsxs(XStack, { px: "$2", py: "$1", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", items: "center", gap: "$1.5", children: [_jsx(HardDrive, { size: 12 }), _jsxs(SizableText, { size: "$1", color: "$ink", children: [formatBytes(m.spec.memory), " RAM"] })] })) : null, m.spec.gpus && m.spec.gpus.length > 0 ? (_jsxs(XStack, { px: "$2", py: "$1", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", items: "center", gap: "$1.5", children: [_jsx(Zap, { size: 12, color: "$yellow10" }), _jsx(SizableText, { size: "$1", color: "$ink", children: m.spec.gpus.map((g) => `${g.model || g.vendor || 'GPU'}${g.memory ? ` (${formatBytes(g.memory)})` : ''}`).join(', ') })] })) : null] })) : null, m.metrics && (m.metrics.cpuUtil || m.metrics.load1 || m.metrics.memUsed || m.metrics.gpuUtil) ? (_jsxs(XStack, { flexWrap: "wrap", gap: "$3", p: "$2.5", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", items: "center", children: [m.metrics.cpuUtil ? (_jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "CPU Utilization" }), _jsxs(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: [formatPercent(m.metrics.cpuUtil), " ", m.metrics.cpuTemp ? `· ${m.metrics.cpuTemp}°C` : ''] })] })) : m.metrics.load1 ? (_jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Load (1m, 5m, 15m)" }), _jsxs(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: [m.metrics.load1, ", ", m.metrics.load5 ?? 0, ", ", m.metrics.load15 ?? 0] })] })) : null, m.metrics.memUsed ? (_jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Memory" }), _jsxs(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: [formatBytes(m.metrics.memUsed), " ", m.spec?.memory ? `/ ${formatBytes(m.spec.memory)}` : 'used'] })] })) : null, m.metrics.gpuUtil !== undefined && m.metrics.gpuUtil > 0 ? (_jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "GPU" }), _jsxs(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: [formatPercent(m.metrics.gpuUtil), " ", m.metrics.gpuTemp ? `· ${m.metrics.gpuTemp}°C` : '', " ", m.metrics.gpuPower ? `· ${m.metrics.gpuPower}W` : ''] })] })) : null, m.metrics.model ? (_jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Serving Model" }), _jsxs(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: [m.metrics.model, " ", m.metrics.decode ? `(${m.metrics.decode} tok/s)` : ''] })] })) : null] })) : null, _jsxs(XStack, { justify: "space-between", items: "center", flexWrap: "wrap", gap: "$2", pt: "$0.5", children: [_jsxs(XStack, { items: "center", gap: "$3", children: [_jsxs(SizableText, { size: "$1", color: "$soft", style: mono, children: ["ID: ", m.id] }), m.seen ? (_jsxs(SizableText, { size: "$1", color: "$soft", children: ["Last heartbeat: ", formatRelative(m.seen)] })) : null] }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Run directly on this machine:" }), _jsxs(XStack, { items: "center", gap: "$1", px: "$2", py: "$0.5", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", children: [_jsxs(SizableText, { size: "$1", color: "$ink", style: mono, children: ["hanzo run --target ", m.id] }), _jsx(CopyButton, { value: `hanzo run --target ${m.id}`, label: `Copy run command for ${m.label}` })] })] })] })] })) : null] }, m.id));
|
|
121
|
+
}) })), list.error && list.value !== UNREAD ? _jsx(Soft, { children: list.error.message }) : null] }), _jsx(Note, { children: note }), _jsx(Adding, { open: adding, onOpenChange: setAdding, onAdded: (m) => {
|
|
77
122
|
setNote(`${m.label} is registered`);
|
|
78
123
|
list.reload();
|
|
79
124
|
} }), _jsx(Rename, { open: renaming !== null, onOpenChange: (o) => !o && setRenaming(null), title: "Rename machine", name: renaming?.label ?? '',
|
|
@@ -89,7 +134,7 @@ export function Machines() {
|
|
|
89
134
|
} }), _jsx(Confirm, { open: keying !== null, onOpenChange: (o) => !o && setKeying(null), title: `Mint a claim key for ${keying?.label ?? 'this machine'}?`, says: "A key it had before stops working, so a runner using it stops getting runs until it has the new one. The new key is shown once.", act: "Mint key", run: async () => {
|
|
90
135
|
const k = await key(t, keying.id);
|
|
91
136
|
setMinted({ machine: keying, key: k });
|
|
92
|
-
} }), _jsx(Dialog, { open: minted !== null, onOpenChange: (o) => !o && setMinted(null), children: _jsxs(DialogContent, { maxW: 480, showCloseButton: false, children: [_jsxs(DialogTitle, { children: ["Claim key for ", minted?.machine.label ?? ''] }), minted ? (_jsx(Once, { value: minted.key, label: "Claim key", children: _jsxs(SizableText, { size: "$1", color: "$soft", children: ["A runner on this machine sends it as X-Target-Key when it claims the runs sent here, at POST /v1/agent/targets/", minted.machine.id, "/claim."] }) })) : null, _jsx(XStack, { justify: "flex-end", children: _jsx(Button, { size: "sm", onPress: () => setMinted(null), children: "Done" }) })] }) })] }));
|
|
137
|
+
} }), _jsx(Dialog, { open: minted !== null, onOpenChange: (o) => !o && setMinted(null), children: _jsxs(DialogContent, { maxW: 480, showCloseButton: false, children: [_jsxs(DialogTitle, { children: ["Claim key for ", minted?.machine.label ?? ''] }), minted ? (_jsxs(YStack, { gap: "$3", children: [_jsx(Once, { value: minted.key, label: "Claim key", children: _jsxs(SizableText, { size: "$1", color: "$soft", children: ["A runner on this machine sends it as X-Target-Key when it claims the runs sent here, at POST /v1/agent/targets/", minted.machine.id, "/claim."] }) }), _jsxs(YStack, { gap: "$1", p: "$2.5", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$ink", style: { fontWeight: 600 }, children: "Start background worker with this key:" }), _jsxs(XStack, { items: "center", justify: "space-between", px: "$2", py: "$1", rounded: "$2", bg: "$raised", children: [_jsxs(SizableText, { size: "$1", color: "$ink", style: mono, children: ["export HANZO_TARGET_KEY=\"", minted.key, "\""] }), _jsx(CopyButton, { value: `export HANZO_TARGET_KEY="${minted.key}"\nhanzo code --serve --target ${minted.machine.id}`, label: "Copy key command" })] })] })] })) : null, _jsx(XStack, { justify: "flex-end", pt: "$2", children: _jsx(Button, { size: "sm", onPress: () => setMinted(null), children: "Done" }) })] }) })] }));
|
|
93
138
|
}
|
|
94
139
|
/** Registering a machine by hand: its name, what it is, and the hostname its runs report. */
|
|
95
140
|
function Adding({ open, onOpenChange, onAdded }) {
|
|
@@ -97,6 +142,7 @@ function Adding({ open, onOpenChange, onAdded }) {
|
|
|
97
142
|
const [label, setLabel] = useState('');
|
|
98
143
|
const [kind, setKind] = useState('machine');
|
|
99
144
|
const [hostname, setHostname] = useState('');
|
|
145
|
+
const [capacity, setCapacity] = useState('');
|
|
100
146
|
const [working, setWorking] = useState(false);
|
|
101
147
|
const [note, setNote] = useState('');
|
|
102
148
|
const close = () => {
|
|
@@ -105,6 +151,7 @@ function Adding({ open, onOpenChange, onAdded }) {
|
|
|
105
151
|
setLabel('');
|
|
106
152
|
setKind('machine');
|
|
107
153
|
setHostname('');
|
|
154
|
+
setCapacity('');
|
|
108
155
|
setNote('');
|
|
109
156
|
onOpenChange(false);
|
|
110
157
|
};
|
|
@@ -112,7 +159,10 @@ function Adding({ open, onOpenChange, onAdded }) {
|
|
|
112
159
|
setWorking(true);
|
|
113
160
|
setNote('');
|
|
114
161
|
try {
|
|
115
|
-
const
|
|
162
|
+
const targetLabel = label.trim() || hostname.trim();
|
|
163
|
+
if (!targetLabel)
|
|
164
|
+
throw new Error('A machine needs a name or a hostname');
|
|
165
|
+
const m = await add(t, { label: targetLabel, kind, host: hostname, capacity: capacity.trim() || undefined });
|
|
116
166
|
setWorking(false);
|
|
117
167
|
onAdded(m);
|
|
118
168
|
close();
|
|
@@ -122,5 +172,5 @@ function Adding({ open, onOpenChange, onAdded }) {
|
|
|
122
172
|
setWorking(false);
|
|
123
173
|
}
|
|
124
174
|
};
|
|
125
|
-
return (_jsx(Dialog, { open: open, onOpenChange: (o) => !o && close(), children: _jsxs(DialogContent, { maxW:
|
|
175
|
+
return (_jsx(Dialog, { open: open, onOpenChange: (o) => !o && close(), children: _jsxs(DialogContent, { maxW: 460, 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) => (_jsxs(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', items: "center", gap: "$1.5", children: [_jsx(KindIcon, { kind: k, size: 14 }), _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" }) }), _jsx(Field, { label: "Capacity / Hardware", hint: "Optional human summary (e.g. 16 vCPU / 64G / 1\u00D7 RTX 4090).", children: _jsx(Input, { value: capacity, onChangeText: setCapacity, "aria-label": "Capacity", placeholder: "e.g. 16 vCPU / 64G / 1\u00D7 RTX 4090" }) }), 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
176
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/build",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.49",
|
|
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.",
|