@hanzo/build 0.2.50 → 0.2.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/lib/api/github.d.ts +18 -0
- package/lib/api/github.js +19 -0
- package/lib/api/keys.d.ts +21 -13
- package/lib/api/keys.js +35 -19
- package/lib/api/work.d.ts +14 -0
- package/lib/api/work.js +17 -0
- package/lib/builder.js +17 -10
- package/lib/forge.js +175 -80
- package/lib/host.d.ts +1 -1
- package/lib/landing.js +9 -9
- package/lib/project.d.ts +16 -1
- package/lib/project.js +86 -28
- package/lib/route.d.ts +12 -4
- package/lib/route.js +8 -1
- package/lib/section.d.ts +1 -1
- package/lib/section.js +3 -3
- package/lib/settings/keys.js +22 -27
- package/lib/sync.js +2 -2
- package/package.json +1 -1
package/lib/project.js
CHANGED
|
@@ -3,6 +3,11 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
3
3
|
* A project's workspace, in the same window: the chat on the left, what it
|
|
4
4
|
* built on the right.
|
|
5
5
|
*
|
|
6
|
+
* A project is a repository on the forge, addressed `<org>/<repo>`; the site it
|
|
7
|
+
* publishes to, when it has one, is the org's project row on that repository
|
|
8
|
+
* (or of the same name). A deployed site with no repository on the forge is
|
|
9
|
+
* opened by its slug (Artifacts), and works the same way.
|
|
10
|
+
*
|
|
6
11
|
* bar mark · project · history · chat toggle | Preview Files Code Layers |
|
|
7
12
|
* Share · Publish
|
|
8
13
|
* left the project's runs as a conversation, suggestions, and the ask
|
|
@@ -41,10 +46,11 @@ import { Button, Dialog, DialogContent, DialogTitle } from '@hanzo/ui';
|
|
|
41
46
|
import { HanzoMark } from '@hanzo/ui/product';
|
|
42
47
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
43
48
|
import { read as pushed } from './api/changes.js';
|
|
49
|
+
import { codebases, one } from './api/codebases.js';
|
|
44
50
|
import { start } from './api/coding.js';
|
|
45
51
|
import { blob, tree } from './api/git.js';
|
|
46
52
|
import { declare } from './api/platform.js';
|
|
47
|
-
import { deployments, name as repoName, ours, templates } from './api/projects.js';
|
|
53
|
+
import { address, deployments, name as repoName, ours, templates } from './api/projects.js';
|
|
48
54
|
import { list, message, stop } from './api/sessions.js';
|
|
49
55
|
import { decode, outcome, pull, said, who } from './api/turn.js';
|
|
50
56
|
import { verdict as record } from './api/verdict.js';
|
|
@@ -54,8 +60,8 @@ import { Grip } from './grip.js';
|
|
|
54
60
|
import { useHost, useTarget } from './host.js';
|
|
55
61
|
import { MODES } from './landing.js';
|
|
56
62
|
import { Out } from './out.js';
|
|
57
|
-
import { pane as cut } from './pane.js';
|
|
58
63
|
import { Publish } from './publish.js';
|
|
64
|
+
import { path } from './route.js';
|
|
59
65
|
import { Attach, compose, Dictate, Files } from './tools.js';
|
|
60
66
|
const LIVE = new Set(['running', 'paused']);
|
|
61
67
|
/** How often a project being published is read again, ms. */
|
|
@@ -70,6 +76,20 @@ const CHAT_CEIL = 640;
|
|
|
70
76
|
const GUTTER = 12;
|
|
71
77
|
/** What the work keeps beside the chat however wide the chat was asked to be, px. */
|
|
72
78
|
const WORK = 400 + 2 * GUTTER;
|
|
79
|
+
/** Whether the site `p` is the repository `r`'s: built from it, or named as it is. */
|
|
80
|
+
export function siteOf(p, r) {
|
|
81
|
+
const want = `${r.org}/${r.name}`.toLowerCase();
|
|
82
|
+
return (p.repo !== '' && address(p.repo).toLowerCase() === want) || p.slug === r.name.toLowerCase();
|
|
83
|
+
}
|
|
84
|
+
/** A site's slug for a repository's name: lowercase letters, digits and hyphens, at most 40. */
|
|
85
|
+
export function slugOf(name) {
|
|
86
|
+
return name
|
|
87
|
+
.toLowerCase()
|
|
88
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
89
|
+
.replace(/^-+|-+$/g, '')
|
|
90
|
+
.slice(0, 40)
|
|
91
|
+
.replace(/-+$/, '');
|
|
92
|
+
}
|
|
73
93
|
/** One run in the conversation: what was asked, and what it came to. */
|
|
74
94
|
function Turn({ run, open, onOpen, project }) {
|
|
75
95
|
const t = useTarget();
|
|
@@ -83,12 +103,18 @@ function Turn({ run, open, onOpen, project }) {
|
|
|
83
103
|
});
|
|
84
104
|
} }), note ? (_jsx(SizableText, { size: "$1", color: "$soft", children: note })) : null, !open ? (_jsx(XStack, { render: "button", onPress: onOpen, hitSlop: 8, children: _jsx(SizableText, { size: "$1", color: "$soft", textDecorationLine: "underline", children: "Show steps" }) })) : null] }), children: _jsx(SizableText, { size: "$2", color: "$ink", children: run.status === 'done' ? 'Done.' : run.status === 'error' ? 'This run hit an error.' : run.status === 'stopped' ? 'Stopped.' : 'Working…' }) })] }));
|
|
85
105
|
}
|
|
86
|
-
export function Project({
|
|
106
|
+
export function Project({ at }) {
|
|
87
107
|
const host = useHost();
|
|
88
108
|
const t = useTarget();
|
|
89
109
|
const signed = Boolean(host.person);
|
|
90
110
|
const all = useProjects(t, signed);
|
|
91
|
-
|
|
111
|
+
// The repository the address names, and the forge's record of it.
|
|
112
|
+
const named = 'slug' in at ? null : at;
|
|
113
|
+
const forge = useRead(named && signed ? () => one(t, named.name) : null, null, [t, signed, named?.name]);
|
|
114
|
+
const project = all.value.find((p) => (named ? siteOf(p, named) : 'slug' in at && p.slug === at.slug)) ?? null;
|
|
115
|
+
// The site this workspace publishes to: the project's, or the repository's name as one.
|
|
116
|
+
const slug = project?.slug ?? ('slug' in at ? at.slug : slugOf(named?.name ?? ''));
|
|
117
|
+
const title = project?.name ?? named?.name ?? slug;
|
|
92
118
|
// A project that has never served reads `building` while it is published, and
|
|
93
119
|
// is read again until it is live or its build failed.
|
|
94
120
|
const building = project?.status === 'building';
|
|
@@ -113,12 +139,15 @@ export function Project({ slug }) {
|
|
|
113
139
|
const stalled = building && ran > STALL;
|
|
114
140
|
// Publish again: the project's repository, built at its branch, the way Publish builds it.
|
|
115
141
|
const [redo, setRedo] = useState({ busy: false, error: '' });
|
|
142
|
+
// The repository's clone address: the forge's for a repository, the site's own otherwise.
|
|
143
|
+
const clone = named ? (forge.value?.clone ?? '') : (project?.repo ?? '');
|
|
144
|
+
const home = named ? (forge.value?.branch ?? 'main') : project?.branch || 'main';
|
|
116
145
|
const retry = async () => {
|
|
117
|
-
if (!
|
|
146
|
+
if (!clone || redo.busy)
|
|
118
147
|
return;
|
|
119
148
|
setRedo({ busy: true, error: '' });
|
|
120
149
|
try {
|
|
121
|
-
await declare(t, { repo:
|
|
150
|
+
await declare(t, { repo: clone, ref: home, name: slug, project: slug, mode: 'branch' });
|
|
122
151
|
setNow(Date.now());
|
|
123
152
|
again();
|
|
124
153
|
setRedo({ busy: false, error: '' });
|
|
@@ -127,11 +156,14 @@ export function Project({ slug }) {
|
|
|
127
156
|
setRedo({ busy: false, error: e.message });
|
|
128
157
|
}
|
|
129
158
|
};
|
|
130
|
-
|
|
159
|
+
// The site's runs, or the org's when the repository has no site yet.
|
|
160
|
+
const sited = !named || Boolean(project);
|
|
161
|
+
const listed = useRead(signed ? () => list(t, sited ? { kind: 'coding', project: slug, limit: 50 } : { kind: 'coding', limit: 50 }) : null, [], [t, signed, slug, sited]);
|
|
131
162
|
// Only the runs on this project's own repository: a record can be moved into
|
|
132
163
|
// any project, and a run's branch is what Files, Code and Publish read.
|
|
133
|
-
const
|
|
134
|
-
const
|
|
164
|
+
const mine = (r) => (named ? ours(r.repo, `${named.org}/${named.name}`) && Boolean(r.repo) : ours(r.repo, project?.repo ?? ''));
|
|
165
|
+
const runs = { ...listed, value: listed.value.filter(mine) };
|
|
166
|
+
const ordered = useMemo(() => [...runs.value].reverse(), [listed.value, project?.repo, named?.org, named?.name]);
|
|
135
167
|
const [chosen, setChosen] = useState(null);
|
|
136
168
|
const current = chosen ?? runs.value[0]?.id ?? null;
|
|
137
169
|
const run = useRun(t, current);
|
|
@@ -149,6 +181,12 @@ export function Project({ slug }) {
|
|
|
149
181
|
// Below md the chat and the work take turns, and Chat is one more view to switch back to.
|
|
150
182
|
const [pane, setPane] = useState('chat');
|
|
151
183
|
const wide = useMedia().md;
|
|
184
|
+
// A workspace too narrow for the chat at its floor and the work at its room
|
|
185
|
+
// (a tablet beside the host's column) shows the work, and the chat when asked:
|
|
186
|
+
// never both cut. Asked here is for this visit; the kept choice is the wide one's.
|
|
187
|
+
const cramped = wide && room > 0 && room < CHAT_FLOOR + WORK;
|
|
188
|
+
const [asked, setAsked] = useState(false);
|
|
189
|
+
const shut = cramped ? !asked : collapsed;
|
|
152
190
|
const [dock, setDock] = useKept('hanzo.build.dock', 36);
|
|
153
191
|
const [dismissed, setDismissed] = useKept(`hanzo.build.hints.${slug}`, false);
|
|
154
192
|
const [mode, setMode] = useKept('hanzo.build.mode', 'build');
|
|
@@ -164,6 +202,7 @@ export function Project({ slug }) {
|
|
|
164
202
|
const [history, setHistory] = useState(false);
|
|
165
203
|
const [publish, setPublish] = useState(null);
|
|
166
204
|
const [switcher, setSwitcher] = useState(false);
|
|
205
|
+
const repos = useRead(switcher && signed ? () => codebases(t) : null, [], [t, switcher, signed]);
|
|
167
206
|
const frame = useRef(null);
|
|
168
207
|
// What was published may be new files at the same address: the project is read
|
|
169
208
|
// for its address, and the frame loads again.
|
|
@@ -196,8 +235,8 @@ export function Project({ slug }) {
|
|
|
196
235
|
// Files is drawn again after a merge, from what the forge holds now.
|
|
197
236
|
const [round, setRound] = useState(0);
|
|
198
237
|
// Files and Code: native git, at the branch the open run pushed (or the project's own).
|
|
199
|
-
const repo = project?.repo ? repoName(project.repo) : '';
|
|
200
|
-
const ref = run.record?.branch ||
|
|
238
|
+
const repo = named ? named.name : project?.repo ? repoName(project.repo) : '';
|
|
239
|
+
const ref = run.record?.branch || home;
|
|
201
240
|
const [files, setFiles] = useState([]);
|
|
202
241
|
const [file, setFile] = useState(null);
|
|
203
242
|
useEffect(() => {
|
|
@@ -294,9 +333,10 @@ export function Project({ slug }) {
|
|
|
294
333
|
// A project with no repository yet sends neither, and its first run makes one.
|
|
295
334
|
const next = await start(t, {
|
|
296
335
|
prompt: compose(`${ask}${context}`, attached),
|
|
297
|
-
|
|
336
|
+
// A repository with no site yet runs on the repository alone.
|
|
337
|
+
project: project?.slug,
|
|
298
338
|
repo,
|
|
299
|
-
base: project?.branch,
|
|
339
|
+
base: named ? forge.value?.branch : project?.branch,
|
|
300
340
|
after: current ?? undefined,
|
|
301
341
|
mode,
|
|
302
342
|
});
|
|
@@ -337,10 +377,21 @@ export function Project({ slug }) {
|
|
|
337
377
|
if (!signed) {
|
|
338
378
|
return (_jsxs(YStack, { flex: 1, items: "center", justify: "center", gap: "$3", children: [_jsx(SizableText, { size: "$4", color: "$ink", children: "Sign in to open this project." }), _jsx(Button, { onPress: () => host.signIn?.(), children: "Sign in" })] }));
|
|
339
379
|
}
|
|
340
|
-
|
|
341
|
-
|
|
380
|
+
// A repository of another organization is opened in that one.
|
|
381
|
+
if (named && host.org && named.org.toLowerCase() !== host.org.toLowerCase()) {
|
|
382
|
+
const member = host.memberships?.some((m) => m.toLowerCase() === named.org.toLowerCase()) && host.chooseOrg;
|
|
383
|
+
return (_jsxs(YStack, { flex: 1, items: "center", justify: "center", gap: "$3", px: "$4", children: [_jsx(SizableText, { size: "$4", color: "$ink", text: "center", children: `${named.org}/${named.name} is in ${named.org}, and you are working in ${host.org}.` }), member ? (_jsx(Button, { variant: "outline", onPress: () => host.chooseOrg?.(named.org), children: `Switch to ${named.org}` })) : (_jsx(Button, { variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'projects' })), children: "See this organization\u2019s projects" }))] }));
|
|
384
|
+
}
|
|
385
|
+
if (named ? !forge.loading && !forge.value : !all.loading && !project && !all.error) {
|
|
386
|
+
return (_jsxs(YStack, { flex: 1, items: "center", justify: "center", gap: "$3", px: "$4", children: [_jsx(SizableText, { size: "$4", color: "$ink", text: "center", children: named
|
|
387
|
+
? forge.error && !/not found|404/i.test(forge.error.message)
|
|
388
|
+
? forge.error.message
|
|
389
|
+
: `${named.org} has no repository named ${named.name}.`
|
|
390
|
+
: host.org
|
|
391
|
+
? `${host.org} has no project named ${slug}.`
|
|
392
|
+
: `There is no project named ${slug}.` }), _jsx(Button, { variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: named ? 'projects' : 'artifacts' })), children: named ? 'See this organization’s projects' : 'See what this organization has built' })] }));
|
|
342
393
|
}
|
|
343
|
-
const chat = (_jsxs(YStack, { flex: 1, minH: 0, position: "relative", children: [_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$3",
|
|
394
|
+
const chat = (_jsxs(YStack, { "data-slot": "project-chat", flex: 1, minH: 0, position: "relative", children: [_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$3", pt: "$2", pb: "$4", gap: "$4", $md: { pr: 0 }, children: ordered.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$ink", children: runs.loading
|
|
344
395
|
? 'Reading this project’s runs…'
|
|
345
396
|
: project?.live
|
|
346
397
|
? `${project.name} is loaded — it is in the preview, and every run on it is under the clock above. Say what to change and it gets built.`
|
|
@@ -354,12 +405,14 @@ export function Project({ slug }) {
|
|
|
354
405
|
? `${project?.name} is loaded — the preview shows the ${starter.title} starter until your copy is published. Say what to change and it gets built.`
|
|
355
406
|
: project
|
|
356
407
|
? `${project.name} is loaded and nothing is published yet. Say what to change and it gets built.`
|
|
357
|
-
:
|
|
408
|
+
: named
|
|
409
|
+
? `${named.org}/${named.name} is open on ${home}. Say what to change and it gets built there.`
|
|
410
|
+
: `${slug} is loaded. Say what to change and it gets built.` })) : (ordered.map((r) => (_jsxs(YStack, { gap: "$2", children: [_jsx(Turn, { run: r, open: r.id === current, onOpen: () => setChosen(r.id), project: slug }), r.id === current ? (_jsxs(YStack, { gap: "$1", pl: "$2", borderLeftWidth: 1, borderColor: "$borderColor", children: [fold(run.events.map((e) => ({ kind: e.kind, actor: who(e.actor), seq: e.seq, id: e.id, text: said(e, run.record?.mode) })).filter((b) => b.text)).map((b) => (_jsx(SizableText, { size: "$1", color: "$soft", children: b.text }, b.key))), pr.href ? (_jsxs(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: [_jsxs(Out, { href: pr.href, label: `Open pull request ${pr.label}`, children: [_jsxs(SizableText, { size: "$1", color: "$ink", textDecorationLine: "underline", children: ["Pull request ", pr.label] }), _jsx(ExternalLink, { size: 11 })] }), _jsx(Merge, { session: r.id, open: proposing, onMerged: merged })] })) : null] })) : null] }, r.id)))) }), _jsxs(YStack, { "data-slot": "project-ask", px: "$3", pb: "$3", gap: "$2", minW: 0, $md: { pr: 0 }, children: [!dismissed && !running ? _jsx(Suggestions, { items: SUGGESTIONS, onPick: (s) => void send(s), onDismiss: () => setDismissed(true) }) : null, note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note })) : null, picked.length ? _jsx(Attachments, { items: picked, onRemove: () => setPicked([]) }) : null, attached.length ? (_jsx(XStack, { gap: 6, flexWrap: "wrap", children: _jsx(Files, { files: attached, onFiles: setAttached }) })) : null, _jsxs(Composer, { value: draft, onChange: setDraft, onSend: () => void (current && running ? steer(current) : send()), onStop: current
|
|
358
411
|
? () => void stop(t, current).then(() => setNote('Stop requested — the run keeps its work on its branch'), (e) => setNote(e.message))
|
|
359
|
-
: undefined, busy: busy || (running && !draft.trim()), rows: 3, placeholder: running ? 'Steer this run' : 'Ask Hanzo for edits', label: "Ask Hanzo for edits", children: [_jsx(Attach, { files: attached, onFiles: setAttached, onNote: setNote }), _jsx(XStack, { flex: 1 }), _jsx(ModeSelect, { modes: MODES, value: mode, onChange: (m) => setMode(m), self: "center" }), _jsx(Dictate, { onText: (said) => setDraft((d) => (d ? `${d} ${said}` : said)), onNote: setNote })] })] }), wide && !
|
|
412
|
+
: undefined, busy: busy || (running && !draft.trim()), rows: 3, placeholder: running ? 'Steer this run' : 'Ask Hanzo for edits', label: "Ask Hanzo for edits", children: [_jsx(Attach, { files: attached, onFiles: setAttached, onNote: setNote }), _jsx(XStack, { flex: 1 }), _jsx(ModeSelect, { modes: MODES, value: mode, onChange: (m) => setMode(m), self: "center" }), _jsx(Dictate, { onText: (said) => setDraft((d) => (d ? `${d} ${said}` : said)), onNote: setNote })] })] }), wide && !shut ? (_jsx(Grip, { side: "left", span: across, floor: CHAT_FLOOR, ceil: most, reset: CHAT_WIDTH, onSpan: setSplit, label: "Resize the chat", r: -GUTTER, width: GUTTER })) : null] }));
|
|
360
413
|
// The preview stays loaded while another view is open, so coming back to it
|
|
361
414
|
// is the page as it was left.
|
|
362
|
-
const preview = (_jsxs(YStack, { "data-slot": "preview", flex: 1, minH: 0, minW: 0, gap: "$2", display: view === 'preview' ? 'flex' : 'none', children: [_jsxs(XStack, { "data-slot": "preview-bar", items: "center", gap: "$1.5", minW: 0, shrink: 0, "$max-md": { px: '$2' }, children: [_jsx(XStack, { render: "button", "aria-label": "Reload the preview", onPress: () => frame.current?.reload(), p: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: _jsx(RefreshCcw, { size: 16 }) }), _jsx(PageSelect, { pages: pages, value: page, onChange: setPage, shrink: 1 }), _jsx(XStack, { flex: 1 }), _jsx(Views, { views: DEVICES, value: device, onChange: (d) => setDevice(d), label: "Device", labels: "none" }), src ? (_jsx(Out, { href: src, label: "Open in a new tab", children: _jsx(XStack, { p: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: _jsx(ExternalLink, { size: 16 }) }) })) : null] }), _jsx(PreviewFrame, { ref: frame, src: src, title: `${
|
|
415
|
+
const preview = (_jsxs(YStack, { "data-slot": "preview", flex: 1, minH: 0, minW: 0, gap: "$2", display: view === 'preview' ? 'flex' : 'none', children: [_jsxs(XStack, { "data-slot": "preview-bar", items: "center", gap: "$1.5", minW: 0, shrink: 0, "$max-md": { px: '$2' }, children: [_jsx(XStack, { render: "button", "aria-label": "Reload the preview", onPress: () => frame.current?.reload(), p: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: _jsx(RefreshCcw, { size: 16 }) }), _jsx(PageSelect, { pages: pages, value: page, onChange: setPage, shrink: 1 }), _jsx(XStack, { flex: 1 }), _jsx(Views, { views: DEVICES, value: device, onChange: (d) => setDevice(d), label: "Device", labels: "none" }), src ? (_jsx(Out, { href: src, label: "Open in a new tab", children: _jsx(XStack, { p: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: _jsx(ExternalLink, { size: 16 }) }) })) : null] }), _jsx(PreviewFrame, { ref: frame, src: src, title: `${title} preview`, device: device, onBridge: onBridge, empty: _jsxs(YStack, { items: "center", gap: "$2", p: "$6", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: stalled
|
|
363
416
|
? 'Publishing is taking too long'
|
|
364
417
|
: building
|
|
365
418
|
? project?.forked
|
|
@@ -373,29 +426,34 @@ export function Project({ slug }) {
|
|
|
373
426
|
? 'Its page appears here when the build finishes.'
|
|
374
427
|
: failed
|
|
375
428
|
? why
|
|
376
|
-
: 'Publish this project and its page appears here.' }), (stalled || failed) &&
|
|
429
|
+
: 'Publish this project and its page appears here.' }), (stalled || failed) && clone ? (_jsx(Button, { variant: "outline", disabled: redo.busy, onPress: () => void retry(), children: "Publish again" })) : null, redo.error ? (_jsx(SizableText, { size: "$2", color: "$soft", text: "center", role: "alert", children: redo.error })) : null] }), toolbar: bridge ? (_jsx(XStack, { render: "button", "aria-pressed": editing, "aria-label": editing ? 'Stop picking elements' : 'Pick an element to edit', onPress: () => {
|
|
377
430
|
const next = !editing;
|
|
378
431
|
setEditing(next);
|
|
379
432
|
frame.current?.post({ type: 'preview:editable', active: next });
|
|
380
433
|
}, p: "$2", rounded: "$10", children: _jsx(MousePointerClick, { size: 16 }) })) : undefined })] }));
|
|
381
|
-
const body = view === 'preview' ? null : view === 'files' ? (repo ? (_jsx(FileTree, { load: async (dir) => (await tree(t, repo, ref, dir)).map((e) => ({ path: e.path, kind: e.dir ? 'dir' : 'file' })), value: file, onSelect: (p) => void openFile(p), label: `${repo} at ${ref}` }, round)) : (_jsx(SizableText, { size: "$2", color: "$soft", p: "$4", children: "This project has no repository yet. Its first run creates one." }))) : view === 'code' ? (_jsx(FileTabs, { files: files, value: file, onSelect: setFile, onClose: (p) => {
|
|
434
|
+
const body = view === 'preview' ? null : view === 'files' ? (repo && root.error ? (_jsx(SizableText, { size: "$2", color: "$soft", p: "$4", role: "status", children: `Nothing to show for ${repo} at ${ref}: the forge answered “${root.error.message}”.` })) : repo ? (_jsx(FileTree, { load: async (dir) => (await tree(t, repo, ref, dir)).map((e) => ({ path: e.path, kind: e.dir ? 'dir' : 'file' })), value: file, onSelect: (p) => void openFile(p), label: `${repo} at ${ref}` }, round)) : (_jsx(SizableText, { size: "$2", color: "$soft", p: "$4", children: "This project has no repository yet. Its first run creates one." }))) : view === 'code' ? (_jsx(FileTabs, { files: files, value: file, onSelect: setFile, onClose: (p) => {
|
|
382
435
|
setFiles((was) => was.filter((f) => f.path !== p));
|
|
383
436
|
if (file === p)
|
|
384
437
|
setFile(null);
|
|
385
438
|
}, empty: _jsxs(SizableText, { size: "$2", color: "$soft", p: "$4", children: ["Open a file from Files. Code here is read at ", ref, "; ask in the chat to change it."] }) })) : (_jsx(YStack, { p: "$4", gap: "$2", children: picked.length ? (picked.map((p) => (_jsxs(SizableText, { size: "$2", color: "$ink", children: [p.label, " \u2014 ", p.id] }, p.id)))) : (_jsx(SizableText, { size: "$2", color: "$soft", children: bridge
|
|
386
439
|
? 'Pick an element in the preview to see it here and attach it to your next ask.'
|
|
387
440
|
: 'Layers are read through the preview bridge, and this page does not run it. Its elements cannot be listed from here.' })) }));
|
|
388
|
-
return (_jsxs(_Fragment, { children: [_jsxs(Workspace, { start: _jsxs(XStack, { items: "center", gap: "$2", minW: 0, shrink: 1, children: [_jsx(XStack, { render: "button", "aria-label": "All runs", onPress: () => host.go(''), p: "$1", shrink: 0, children: _jsx(HanzoMark, { size: 20 }) }), _jsx(ProjectChip, { name:
|
|
441
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Workspace, { start: _jsxs(XStack, { items: "center", gap: "$2", minW: 0, shrink: 1, children: [_jsx(XStack, { render: "button", "aria-label": "All runs", onPress: () => host.go(''), p: "$1", shrink: 0, children: _jsx(HanzoMark, { size: 20 }) }), _jsx(ProjectChip, { name: title, onPress: () => setSwitcher(true) }), _jsx(XStack, { render: "button", "aria-label": "History", onPress: () => setHistory(true), p: "$1.5", rounded: "$3", shrink: 0, hoverStyle: { bg: '$hover' }, children: _jsx(Clock, { size: 16 }) }), _jsx(XStack, { render: "button", "aria-label": shut ? 'Show the chat' : 'Hide the chat', "aria-pressed": !shut, onPress: () => (cramped ? setAsked(!asked) : setCollapsed(!collapsed)), p: "$1.5", rounded: "$3", shrink: 0, hoverStyle: { bg: '$hover' }, "$max-md": { display: 'none' }, children: _jsx(PanelLeft, { size: 16 }) })] }), middle: _jsx(Views, { views: [CHAT, ...VIEWS], value: pane === 'chat' && !wide ? 'chat' : view, onChange: (v) => {
|
|
389
442
|
if (v === 'chat')
|
|
390
443
|
return setPane('chat');
|
|
391
444
|
setView(v);
|
|
392
445
|
setPane('view');
|
|
393
|
-
}, label: "View" }), end: _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(XStack, { display: "none", $md: { display: 'flex' }, children: _jsxs(Button, { variant: "outline", size: "sm", onPress: () => void share(), children: [_jsx(Share2, { size: 14 }), " Share"] }) }), _jsxs(Button, { size: "sm", disabled: !
|
|
394
|
-
setPublish({ repo: project.repo, title: project.name, ref, name: project.slug, project: project.slug }), children: [_jsx(Upload, { size: 14 }), " Publish"] })] }), chat: chat, dock: _jsx(Console, { lines: lines, height: dock, onHeight: setDock, onClear: () => setPageLines([]), status: run.status || end.status || undefined, label: "Console" }), collapsed: collapsed, pane: pane, width: across, onLayout: (e) => setRoom(e.nativeEvent.layout.width), children: [preview, body] }), _jsx(Dialog, { open: history, onOpenChange: setHistory, children: _jsxs(DialogContent, { maxW: 480, children: [_jsxs(DialogTitle, { children: ["Runs on ", project?.name ?? slug] }), _jsx(YStack, { gap: "$1", children: runs.value.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "No runs yet." })) : (runs.value.map((r) => (_jsxs(XStack, { render: "button", onPress: () => {
|
|
446
|
+
}, label: "View" }), end: _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(XStack, { display: "none", $md: { display: 'flex' }, children: _jsxs(Button, { variant: "outline", size: "sm", onPress: () => void share(), children: [_jsx(Share2, { size: 14 }), " Share"] }) }), _jsxs(Button, { size: "sm", "aria-label": "Publish", disabled: !clone || !slug, onPress: () => setPublish({ repo: clone, title, ref, name: slug, project: slug }), children: [_jsx(Upload, { size: 14 }), _jsx(SizableText, { size: "$2", color: "inherit", "$max-md": { display: 'none' }, children: "Publish" })] })] }), chat: chat, dock: _jsx(Console, { lines: lines, height: dock, onHeight: setDock, onClear: () => setPageLines([]), status: run.status || end.status || undefined, label: "Console" }), collapsed: shut, pane: pane, width: across, onLayout: (e) => setRoom(e.nativeEvent.layout.width), children: [preview, body] }), _jsx(Dialog, { open: history, onOpenChange: setHistory, children: _jsxs(DialogContent, { maxW: 480, children: [_jsxs(DialogTitle, { children: ["Runs on ", title] }), _jsx(YStack, { gap: "$1", children: runs.value.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "No runs yet." })) : (runs.value.map((r) => (_jsxs(XStack, { render: "button", onPress: () => {
|
|
395
447
|
setChosen(r.id);
|
|
396
448
|
setHistory(false);
|
|
397
|
-
}, justify: "space-between", px: "$2", py: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, flex: 1, children: r.title || 'Untitled run' }), _jsx(SizableText, { size: "$1", color: "$soft", children: r.status })] }, r.id)))) })] }) }), _jsx(Dialog, { open: switcher, onOpenChange: setSwitcher, children: _jsxs(DialogContent, { maxW: 420, children: [_jsx(DialogTitle, { children: "Projects" }), _jsx(YStack, { gap: "$1", maxH: 420, overflow: "scroll", children:
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
449
|
+
}, justify: "space-between", px: "$2", py: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, flex: 1, children: r.title || 'Untitled run' }), _jsx(SizableText, { size: "$1", color: "$soft", children: r.status })] }, r.id)))) })] }) }), _jsx(Dialog, { open: switcher, onOpenChange: setSwitcher, children: _jsxs(DialogContent, { maxW: 420, children: [_jsx(DialogTitle, { children: "Projects" }), _jsx(YStack, { gap: "$1", maxH: 420, overflow: "scroll", children: repos.error ? (_jsx(SizableText, { size: "$2", color: "$soft", children: repos.error.message })) : repos.loading && repos.value.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "Reading the forge\u2026" })) : (repos.value.map((c) => {
|
|
450
|
+
const here = Boolean(named) && c.name === named?.name;
|
|
451
|
+
return (_jsx(XStack, { render: "button", "aria-label": `Open ${c.org}/${c.name}`, "aria-current": here ? 'page' : undefined, onPress: () => {
|
|
452
|
+
setSwitcher(false);
|
|
453
|
+
host.go(path({ kind: 'repo', org: c.org || host.org || '', name: c.name }));
|
|
454
|
+
}, px: "$2", py: "$1.5", rounded: "$3", bg: here ? '$hover' : undefined, hoverStyle: { bg: '$hover' }, children: _jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: c.name }) }, `${c.org}/${c.name}`));
|
|
455
|
+
})) }), _jsx(Button, { variant: "outline", size: "sm", onPress: () => {
|
|
456
|
+
setSwitcher(false);
|
|
457
|
+
host.go(path({ kind: 'screen', screen: 'projects' }));
|
|
458
|
+
}, children: "All projects" })] }) }), _jsx(Publish, { source: publish, onClose: () => setPublish(null), onLive: fresh })] }));
|
|
401
459
|
}
|
package/lib/route.d.ts
CHANGED
|
@@ -8,13 +8,15 @@
|
|
|
8
8
|
* -/settings[/<section>] a section of Settings
|
|
9
9
|
* -/customize[/<tab>] skills, connectors and plugins: what the agent brings to a run
|
|
10
10
|
* -/plans the plans an organization can be on
|
|
11
|
-
* <
|
|
11
|
+
* <org>/<repo> a repository's workspace: a project is a repository on the forge
|
|
12
|
+
* <slug> a deployed site's workspace (Artifacts)
|
|
12
13
|
*
|
|
13
14
|
* Unambiguous by construction: a project slug is `^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$`,
|
|
14
|
-
* so it can hold neither the `_` a session id carries nor start with
|
|
15
|
-
*
|
|
15
|
+
* so it can hold neither the `_` a session id carries nor start with `-`, and a
|
|
16
|
+
* repository is the one address with two segments, each starting with a letter
|
|
17
|
+
* or a digit. Anything else is not an address here and reads as the empty state.
|
|
16
18
|
*/
|
|
17
|
-
export type Screen = 'artifacts' | 'templates' | '
|
|
19
|
+
export type Screen = 'artifacts' | 'templates' | 'projects' | 'issues' | 'automations' | 'sync' | 'mcp' | 'plans';
|
|
18
20
|
/** Settings, one section each. Every setting the builder has lives on this page, none elsewhere. */
|
|
19
21
|
export declare const SECTIONS: readonly ['general', 'account', 'privacy', 'billing', 'usage', 'capabilities', 'memory', 'code', 'environments', 'machines', 'keys', 'members', 'integrations', 'notifications'];
|
|
20
22
|
export type Section = (typeof SECTIONS)[number];
|
|
@@ -38,12 +40,18 @@ export type Route = {
|
|
|
38
40
|
} | {
|
|
39
41
|
kind: 'project';
|
|
40
42
|
slug: string;
|
|
43
|
+
} | {
|
|
44
|
+
kind: 'repo';
|
|
45
|
+
org: string;
|
|
46
|
+
name: string;
|
|
41
47
|
} | {
|
|
42
48
|
kind: 'automation';
|
|
43
49
|
id: string;
|
|
44
50
|
};
|
|
45
51
|
export declare const SESSION: RegExp;
|
|
46
52
|
export declare const SLUG: RegExp;
|
|
53
|
+
/** An organization or a repository name on the forge (api/codebases.ts `NAME`). */
|
|
54
|
+
export declare const NAME: RegExp;
|
|
47
55
|
/** An automation's id, or `new` for one not yet written. */
|
|
48
56
|
export declare const AUTOMATION: RegExp;
|
|
49
57
|
export declare function route(path: string): Route;
|
package/lib/route.js
CHANGED
|
@@ -19,9 +19,11 @@ export const SECTIONS = [
|
|
|
19
19
|
export const TABS = ['skills', 'connectors', 'plugins', 'agents'];
|
|
20
20
|
export const SESSION = /^sess_[0-9a-f]{32}$/;
|
|
21
21
|
export const SLUG = /^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$/;
|
|
22
|
+
/** An organization or a repository name on the forge (api/codebases.ts `NAME`). */
|
|
23
|
+
export const NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
22
24
|
/** An automation's id, or `new` for one not yet written. */
|
|
23
25
|
export const AUTOMATION = /^(flow_[0-9a-f]{32}|new)$/;
|
|
24
|
-
const SCREENS = ['artifacts', 'templates', '
|
|
26
|
+
const SCREENS = ['artifacts', 'templates', 'projects', 'issues', 'automations', 'sync', 'mcp', 'plans'];
|
|
25
27
|
export function route(path) {
|
|
26
28
|
const p = path.replace(/^\/+|\/+$/g, '');
|
|
27
29
|
if (SESSION.test(p))
|
|
@@ -44,6 +46,9 @@ export function route(path) {
|
|
|
44
46
|
}
|
|
45
47
|
if (SLUG.test(p))
|
|
46
48
|
return { kind: 'project', slug: p };
|
|
49
|
+
const [org, name, ...rest] = p.split('/');
|
|
50
|
+
if (!rest.length && org && name && NAME.test(org) && NAME.test(name))
|
|
51
|
+
return { kind: 'repo', org, name };
|
|
47
52
|
return { kind: 'new' };
|
|
48
53
|
}
|
|
49
54
|
/** The path for a route — the inverse of `route`. */
|
|
@@ -61,6 +66,8 @@ export function path(r) {
|
|
|
61
66
|
return r.tab === 'skills' ? '-/customize' : `-/customize/${r.tab}`;
|
|
62
67
|
case 'project':
|
|
63
68
|
return r.slug;
|
|
69
|
+
case 'repo':
|
|
70
|
+
return `${r.org}/${r.name}`;
|
|
64
71
|
case 'automation':
|
|
65
72
|
return `-/automations/${r.id}`;
|
|
66
73
|
}
|
package/lib/section.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export declare const DOTS: Record<string, SessionStatus>;
|
|
|
8
8
|
/** The org's coding runs, newest first, kept live by the org's feed. */
|
|
9
9
|
export declare function useSessions(host: Host): Read<Session[]>;
|
|
10
10
|
/**
|
|
11
|
-
* New, Automations, the forge (
|
|
11
|
+
* New, Automations, the forge (Projects, Issues), Artifacts, Templates, then the
|
|
12
12
|
* runs. `children` sit between the places and the runs — where a host lists its
|
|
13
13
|
* own. `onPick` fires after a move, so a host that draws its rail as a drawer
|
|
14
14
|
* on a phone can close it.
|
package/lib/section.js
CHANGED
|
@@ -11,7 +11,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
11
11
|
* host's own `go`.
|
|
12
12
|
*/
|
|
13
13
|
import { SizableText } from '@hanzo/gui';
|
|
14
|
-
import { Blocks, CircleDot, FolderGit2,
|
|
14
|
+
import { Blocks, CircleDot, FolderGit2, LayoutTemplate, SlidersHorizontal, SquarePen, Workflow } from '@hanzogui/lucide-icons-2';
|
|
15
15
|
import { SidebarItem, SidebarSection, StatusDot } from '@hanzo/ui/chat';
|
|
16
16
|
import { useMemo } from 'react';
|
|
17
17
|
import { pinBoard } from './choice.js';
|
|
@@ -32,7 +32,7 @@ export function useSessions(host) {
|
|
|
32
32
|
}
|
|
33
33
|
const Note = ({ children }) => (_jsx(SizableText, { size: "$1", color: "$soft", px: "$2", py: "$1", children: children }));
|
|
34
34
|
/**
|
|
35
|
-
* New, Automations, the forge (
|
|
35
|
+
* New, Automations, the forge (Projects, Issues), Artifacts, Templates, then the
|
|
36
36
|
* runs. `children` sit between the places and the runs — where a host lists its
|
|
37
37
|
* own. `onPick` fires after a move, so a host that draws its rail as a drawer
|
|
38
38
|
* on a phone can close it.
|
|
@@ -45,7 +45,7 @@ export function DevSection({ host, onPick, label = 'Runs', children, }) {
|
|
|
45
45
|
onPick?.();
|
|
46
46
|
};
|
|
47
47
|
const screen = r.kind === 'screen' ? r.screen : '';
|
|
48
|
-
return (_jsxs(_Fragment, { children: [_jsx(SidebarItem, { icon: _jsx(SquarePen, { size: 16, "aria-hidden": true }), active: r.kind === 'new', onPress: () => go(''), children: "New run" }), _jsx(SidebarItem, { icon: _jsx(Workflow, { size: 16, "aria-hidden": true }), active: screen === 'automations' || r.kind === 'automation', onPress: () => go(path({ kind: 'screen', screen: 'automations' })), children: "Automations" }), _jsx(SidebarItem, { icon: _jsx(FolderGit2, { size: 16, "aria-hidden": true }), active: screen === '
|
|
48
|
+
return (_jsxs(_Fragment, { children: [_jsx(SidebarItem, { icon: _jsx(SquarePen, { size: 16, "aria-hidden": true }), active: r.kind === 'new', onPress: () => go(''), children: "New run" }), _jsx(SidebarItem, { icon: _jsx(Workflow, { size: 16, "aria-hidden": true }), active: screen === 'automations' || r.kind === 'automation', onPress: () => go(path({ kind: 'screen', screen: 'automations' })), children: "Automations" }), _jsx(SidebarItem, { icon: _jsx(FolderGit2, { size: 16, "aria-hidden": true }), active: screen === 'projects' || screen === 'sync' || r.kind === 'repo', onPress: () => go(path({ kind: 'screen', screen: 'projects' })), children: "Projects" }), _jsx(SidebarItem, { icon: _jsx(CircleDot, { size: 16, "aria-hidden": true }), active: screen === 'issues', onPress: () => {
|
|
49
49
|
pinBoard(host.org, '');
|
|
50
50
|
go(path({ kind: 'screen', screen: 'issues' }));
|
|
51
51
|
}, children: "Issues" }), _jsx(SidebarItem, { icon: _jsx(SlidersHorizontal, { size: 16, "aria-hidden": true }), active: r.kind === 'customize', onPress: () => go(path({ kind: 'customize', tab: 'connectors' })), children: "Customize" }), _jsx(SidebarItem, { icon: _jsx(Blocks, { size: 16, "aria-hidden": true }), active: screen === 'artifacts', onPress: () => go(path({ kind: 'screen', screen: 'artifacts' })), children: "Artifacts" }), _jsx(SidebarItem, { icon: _jsx(LayoutTemplate, { size: 16, "aria-hidden": true }), active: screen === 'templates', onPress: () => go(path({ kind: 'screen', screen: 'templates' })), children: "Templates" }), children, _jsx(SidebarSection, { label: label, children: !host.person ? (_jsx(Note, { children: "Sign in to see your runs." })) : sessions.value.length ? (sessions.value.map((s) => (_jsx(SidebarItem, { icon: _jsx(StatusDot, { status: DOTS[s.status] ?? 'idle' }), active: r.kind === 'run' && r.id === s.id, onPress: () => go(s.id), children: s.title || 'Untitled run' }, s.id)))) : (_jsx(Note, { children: sessions.error ? sessions.error.message : sessions.loading ? 'Reading…' : 'No runs yet. Say what to build and it lands here.' })) })] }));
|
package/lib/settings/keys.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
* API keys: your
|
|
4
|
-
* for a server,
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* API keys: your keys for calling the Hanzo API from code, as many as you need —
|
|
4
|
+
* a secret key for a server, a publishable key for a page's source. Each is its
|
|
5
|
+
* own credential: creating one never touches another, and revoking one stops only
|
|
6
|
+
* that one. A secret key is shown once, when it is made. A limit narrows what a
|
|
7
|
+
* key reaches; it never reaches further than you do.
|
|
7
8
|
*/
|
|
8
9
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
9
10
|
import { Button, Dialog, DialogContent, DialogTitle, Input } from '@hanzo/ui';
|
|
@@ -15,19 +16,15 @@ import { useRead } from '../data.js';
|
|
|
15
16
|
import { useHost, useTarget } from '../host.js';
|
|
16
17
|
import { Card, Field, Heading, Note, Once, Row, Soft } from './ui.js';
|
|
17
18
|
const NAME = { secret: 'Secret key', publishable: 'Publishable key' };
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
publishable: 'Safe in a page’s source.',
|
|
21
|
-
};
|
|
22
|
-
/** The line under a key: what it is for, how it starts, what it reaches, when it was made. */
|
|
23
|
-
function about(type, k) {
|
|
24
|
-
if (!k)
|
|
25
|
-
return `${FOR[type]} None yet.`;
|
|
19
|
+
/** The line under a key: its head, what it reaches, when it was made, and where it stands. */
|
|
20
|
+
function about(k) {
|
|
26
21
|
const when = k.created ? new Date(k.created) : null;
|
|
27
22
|
return [
|
|
28
|
-
type === 'secret' && k.prefix ? `${k.prefix}…` : '',
|
|
23
|
+
k.type === 'secret' && k.prefix ? `${k.prefix}…` : '',
|
|
24
|
+
k.type === 'publishable' ? 'Publishable' : '',
|
|
29
25
|
k.limit.length ? `Reaches ${k.limit.join(', ')}` : 'Unrestricted',
|
|
30
26
|
when && !Number.isNaN(when.getTime()) ? `Created ${when.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })}` : '',
|
|
27
|
+
k.status !== 'active' ? k.status[0].toUpperCase() + k.status.slice(1) : '',
|
|
31
28
|
]
|
|
32
29
|
.filter(Boolean)
|
|
33
30
|
.join(' · ');
|
|
@@ -43,24 +40,21 @@ export function Keys() {
|
|
|
43
40
|
if (!signed) {
|
|
44
41
|
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: "API keys" }), _jsx(Soft, { children: "Sign in to see your API keys." })] }));
|
|
45
42
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const k = held(type);
|
|
49
|
-
return (_jsxs(YStack, { borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", children: [_jsx(Row, { first: true, title: NAME[type], detail: about(type, k), trailing: _jsxs(XStack, { gap: "$1.5", shrink: 0, children: [k ? (_jsx(Button, { size: "sm", variant: "ghost", "aria-label": `Revoke the ${type} key`, onPress: () => setRevoking(type), children: "Revoke" })) : null, _jsx(Button, { size: "sm", variant: "outline", "aria-label": `${k ? 'Rotate' : 'Create'} the ${type} key`, onPress: () => setMaking(type), children: k ? 'Rotate' : 'Create' })] }) }), type === 'publishable' && k?.key ? (_jsxs(XStack, { items: "center", gap: "$2", px: "$3", pb: "$2.5", children: [_jsx(SizableText, { flex: 1, minW: 0, size: "$1", color: "$soft", numberOfLines: 1, "aria-label": "Publishable key", style: { fontFamily: 'var(--f-mono, ui-monospace, monospace)' }, children: k.key }), _jsx(CopyButton, { value: k.key, label: "Copy publishable key" })] })) : null] }, type));
|
|
50
|
-
}) })), _jsx(Note, { children: note }), _jsx(Making, { type: making, rotating: making ? Boolean(held(making)) : false, onClose: () => setMaking(null), onMade: (m) => {
|
|
51
|
-
setNote(`${NAME[m.type]} created`);
|
|
43
|
+
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: "API keys", detail: "Your keys for calling api.hanzo.ai from code. Make one for each app; revoking one stops only that one." }), _jsx(XStack, { gap: "$2", children: KINDS.map((type) => (_jsx(Button, { size: "sm", variant: "outline", "aria-label": `Create a ${type} key`, onPress: () => setMaking(type), children: `Create ${NAME[type].toLowerCase()}` }, type))) }), list.error ? (_jsx(Soft, { children: list.error.message })) : list.loading && list.value.length === 0 ? (_jsx(Soft, { children: "Reading your keys\u2026" })) : list.value.length === 0 ? (_jsx(Soft, { children: "No keys yet." })) : (_jsx(Card, { children: list.value.map((k, i) => (_jsxs(YStack, { borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", opacity: k.status === 'active' ? 1 : 0.55, children: [_jsx(Row, { first: true, title: k.name || NAME[k.type], detail: about(k), trailing: k.status === 'revoked' ? null : (_jsx(Button, { size: "sm", variant: "ghost", "aria-label": `Revoke ${k.name || k.id}`, onPress: () => setRevoking(k), children: "Revoke" })) }), k.type === 'publishable' && k.key ? (_jsxs(XStack, { items: "center", gap: "$2", px: "$3", pb: "$2.5", children: [_jsx(SizableText, { flex: 1, minW: 0, size: "$1", color: "$soft", numberOfLines: 1, "aria-label": "Publishable key", style: { fontFamily: 'var(--f-mono, ui-monospace, monospace)' }, children: k.key }), _jsx(CopyButton, { value: k.key, label: "Copy publishable key" })] })) : null] }, k.id))) })), _jsx(Note, { children: note }), _jsx(Making, { type: making, onClose: () => setMaking(null), onMade: (m) => {
|
|
44
|
+
setNote(`${m.name || NAME[m.type]} created`);
|
|
52
45
|
list.reload();
|
|
53
|
-
} }), _jsx(Confirm, { open: revoking !== null, onOpenChange: (o) => !o && setRevoking(null), title: `Revoke
|
|
54
|
-
// It acts only while it is open, which is while a
|
|
46
|
+
} }), _jsx(Confirm, { open: revoking !== null, onOpenChange: (o) => !o && setRevoking(null), title: `Revoke ${revoking?.name || 'this key'}?`, says: "It stops working within a minute, and anything using it is refused. Your other keys keep working.", act: "Revoke",
|
|
47
|
+
// It acts only while it is open, which is while a key is chosen.
|
|
55
48
|
run: async () => {
|
|
56
|
-
await revoke(t, revoking);
|
|
57
|
-
setNote(`${NAME[revoking]} revoked`);
|
|
49
|
+
await revoke(t, revoking.id);
|
|
50
|
+
setNote(`${revoking.name || NAME[revoking.type]} revoked`);
|
|
58
51
|
list.reload();
|
|
59
52
|
} })] }));
|
|
60
53
|
}
|
|
61
|
-
/** Creating
|
|
62
|
-
function Making({ type,
|
|
54
|
+
/** Creating one key: a name and an optional limit, then the key itself, once. */
|
|
55
|
+
function Making({ type, onClose, onMade }) {
|
|
63
56
|
const t = useTarget();
|
|
57
|
+
const [name, setName] = useState('');
|
|
64
58
|
const [limit, setLimit] = useState('');
|
|
65
59
|
const [made, setMade] = useState(null);
|
|
66
60
|
const [working, setWorking] = useState(false);
|
|
@@ -68,6 +62,7 @@ function Making({ type, rotating, onClose, onMade }) {
|
|
|
68
62
|
const close = () => {
|
|
69
63
|
if (working)
|
|
70
64
|
return;
|
|
65
|
+
setName('');
|
|
71
66
|
setLimit('');
|
|
72
67
|
setMade(null);
|
|
73
68
|
setNote('');
|
|
@@ -86,7 +81,7 @@ function Making({ type, rotating, onClose, onMade }) {
|
|
|
86
81
|
}
|
|
87
82
|
setWorking(true);
|
|
88
83
|
try {
|
|
89
|
-
const m = await mint(t, type, list);
|
|
84
|
+
const m = await mint(t, type, name, list);
|
|
90
85
|
setMade(m);
|
|
91
86
|
onMade(m);
|
|
92
87
|
}
|
|
@@ -97,5 +92,5 @@ function Making({ type, rotating, onClose, onMade }) {
|
|
|
97
92
|
setWorking(false);
|
|
98
93
|
}
|
|
99
94
|
};
|
|
100
|
-
return (_jsx(Dialog, { open: type !== null, onOpenChange: (o) => !o && close(), children: _jsxs(DialogContent, { maxW: 480, showCloseButton: false, children: [_jsx(DialogTitle, { children: made ? `Your new ${made.type} key` :
|
|
95
|
+
return (_jsx(Dialog, { open: type !== null, onOpenChange: (o) => !o && close(), children: _jsxs(DialogContent, { maxW: 480, showCloseButton: false, children: [_jsx(DialogTitle, { children: made ? `Your new ${made.type} key` : `Create a ${type ?? ''} key` }), made ? (_jsxs(YStack, { gap: "$3", children: [made.type === 'secret' ? (_jsx(Once, { value: made.key, label: "Secret key" })) : (_jsxs(XStack, { items: "center", gap: "$2", px: "$3", py: "$3", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$raised", children: [_jsx(SizableText, { flex: 1, minW: 0, size: "$2", color: "$ink", style: { fontFamily: 'var(--f-mono, ui-monospace, monospace)', wordBreak: 'break-all' }, children: made.key }), _jsx(CopyButton, { value: made.key, label: "Copy publishable key" })] })), made.limit.length ? (_jsxs(SizableText, { size: "$1", color: "$soft", children: ["It reaches ", made.limit.join(', '), " and nothing else."] })) : null, _jsx(XStack, { justify: "flex-end", children: _jsx(Button, { size: "sm", onPress: close, children: "Done" }) })] })) : (_jsxs(YStack, { gap: "$3", children: [_jsx(Field, { label: "Name", hint: "What it is for, so you can tell it from your other keys.", children: _jsx(Input, { value: name, onChangeText: setName, placeholder: "production", "aria-label": "Name" }) }), _jsx(Field, { label: "Limit", hint: "Optional. What it may reach, as kind:name \u2014 model:zen5, project:acme, product:train, read:* for read-only. Empty reaches whatever you do.", children: _jsx(Input, { value: limit, onChangeText: setLimit, placeholder: "model:zen5, project:acme", "aria-label": "Limit", 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 go(type), children: "Create" })] })] }))] }) }));
|
|
101
96
|
}
|
package/lib/sync.js
CHANGED
|
@@ -77,7 +77,7 @@ export function Sync() {
|
|
|
77
77
|
}
|
|
78
78
|
};
|
|
79
79
|
const back = () => {
|
|
80
|
-
host.go(path({ kind: 'screen', screen: '
|
|
80
|
+
host.go(path({ kind: 'screen', screen: 'projects' }));
|
|
81
81
|
};
|
|
82
82
|
useEffect(() => {
|
|
83
83
|
if (!syncing)
|
|
@@ -102,7 +102,7 @@ export function Sync() {
|
|
|
102
102
|
window.clearInterval(id);
|
|
103
103
|
};
|
|
104
104
|
}, [syncing, t, host.org, host.go, picked]);
|
|
105
|
-
return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", children: [_jsxs(XStack, { px: "$6", pt: "$5", pb: "$2", gap: "$2", items: "center", children: [_jsx(XStack, { render: "button", "aria-label": "Back to
|
|
105
|
+
return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", children: [_jsxs(XStack, { px: "$6", pt: "$5", pb: "$2", gap: "$2", items: "center", children: [_jsx(XStack, { render: "button", "aria-label": "Back to projects", onPress: back, children: _jsx(SizableText, { size: "$2", color: "$soft", children: "Projects" }) }), _jsx(SizableText, { size: "$2", color: "$soft", children: "/" }), _jsx(SizableText, { size: "$2", color: "$ink", children: "Sync" })] }), _jsxs(YStack, { flex: 1, items: "center", justify: "center", px: "$6", pb: "$10", gap: "$5", children: [_jsxs(XStack, { justify: "center", gap: "$6", children: [_jsx(Step, { label: "Select repositories", done: step === 'access', current: step === 'pick' }), _jsx(Step, { label: "Repository access", done: false, current: step === 'access' })] }), _jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$5", bg: "$panel", px: "$6", py: "$6", gap: "$4", width: "100%", maxW: 460, children: [step === 'pick' && !owner ? (_jsxs(_Fragment, { children: [_jsxs(YStack, { gap: "$1", items: "center", children: [_jsx(SizableText, { size: "$5", color: "$ink", children: "Select repositories" }), _jsxs(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: ["Choose repositories to bring into ", dest] })] }), _jsxs(Panel, { children: [_jsx(Input, { value: q, onChangeText: setQ, placeholder: "Search organizations", "aria-label": "Search organizations", disabled: !signed }), signed ? (_jsx(SizableText, { size: "$1", color: "$soft", children: `${orgs.length} ${orgs.length === 1 ? 'organization' : 'organizations'}` })) : null, list.error ? _jsx(Soft, { children: list.error.message }) : null, !signed ? (_jsx(Soft, { children: "Sign in to choose repositories the GitHub connection can read." })) : list.loading && rows.length === 0 && orgs.length === 0 ? (_jsx(Soft, { children: "Reading the connection\u2026" })) : shownOrgs.length === 0 ? (_jsx(Soft, { children: orgs.length === 0 ? 'No organizations on this connection yet.' : 'Nothing matches.' })) : (shownOrgs.map((a) => (_jsxs(XStack, { render: "button", "aria-label": a.blocked ? `${a.name} needs an organization admin` : `Open ${a.name}`, disabled: a.blocked, items: "center", gap: "$2", py: "$2", borderTopWidth: 1, borderColor: "$borderColor", onPress: () => {
|
|
106
106
|
setOwner(a.name);
|
|
107
107
|
setQ('');
|
|
108
108
|
}, children: [_jsx(SizableText, { flex: 1, size: "$2", color: a.blocked ? '$soft' : '$ink', numberOfLines: 1, children: a.name }), a.blocked ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "Needs organization admin" })) : (_jsx(ChevronRight, { size: 14 }))] }, a.name)))), _jsxs(XStack, { render: "button", "aria-label": "Add organization", items: "center", gap: "$2", py: "$2", onPress: () => void addOrg(), disabled: !signed, children: [_jsx(Plus, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$ink", children: "Add organization" })] })] })] })) : null, step === 'pick' && owner ? (_jsxs(_Fragment, { children: [_jsxs(YStack, { gap: "$1", items: "center", children: [_jsx(SizableText, { size: "$5", color: "$ink", children: "Select repositories" }), _jsxs(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: ["Choose repositories to bring into ", dest] })] }), _jsxs(Panel, { children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(XStack, { render: "button", "aria-label": "All organizations", onPress: () => setOwner(''), children: _jsx(ChevronLeft, { size: 16 }) }), _jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: owner })] }), _jsx(Input, { value: q, onChangeText: setQ, placeholder: "Search repositories", "aria-label": "Search repositories" }), _jsxs(XStack, { justify: "space-between", items: "center", children: [_jsxs(SizableText, { size: "$1", color: "$soft", children: [ofOwner.length, " ", ofOwner.length === 1 ? 'repository' : 'repositories'] }), _jsx(XStack, { render: "button", "aria-label": "Select all", onPress: selectShown, disabled: shownRepos.length === 0, children: _jsx(SizableText, { size: "$1", color: "$ink", children: "Select all" }) })] }), shownRepos.length === 0 ? (_jsx(Soft, { children: "Nothing matches." })) : (shownRepos.map((r) => (_jsx(RepoRow, { repo: r, on: picked.includes(r.fullName), onPress: () => toggle(r.fullName) }, r.fullName)))), _jsxs(XStack, { render: "button", "aria-label": "Grant more repositories", items: "center", gap: "$2", py: "$2", onPress: () => void addOrg(), children: [_jsx(Plus, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$ink", children: "Add repositories" })] })] })] })) : null, step === 'access' ? (_jsxs(_Fragment, { children: [_jsxs(YStack, { gap: "$1", items: "center", children: [_jsx(SizableText, { size: "$5", color: "$ink", children: "Repository access" }), _jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: "Review who can read these repositories" })] }), _jsxs(XStack, { gap: "$2", items: "center", children: [_jsxs(XStack, { flex: 1, items: "center", gap: "$2", px: "$3", py: "$2", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(XStack, { render: "button", "aria-label": "Clear the selection", onPress: () => { setPicked([]); setStep('pick'); }, children: _jsx(X, { size: 12 }) }), _jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: more ? `${lead}, +${more} more` : lead })] }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => setStep('pick'), children: "Edit" })] }), _jsxs(XStack, { justify: "space-between", gap: "$4", children: [_jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Who gets access" }), _jsxs(SizableText, { size: "$2", color: "$ink", children: [host.person?.name || host.person?.email || 'You', " \u00B7 You"] })] }), _jsxs(YStack, { gap: "$1", items: "flex-end", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Role" }), _jsx(SizableText, { size: "$2", color: "$ink", children: host.admin ? 'Repository admin' : 'Member' })] })] }), _jsxs(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: ["Anyone with access to ", dest] })] })) : null, error ? _jsx(Soft, { children: error }) : null, step === 'pick' ? (_jsx(Button, { width: "100%", disabled: !owner || picked.length === 0, onPress: () => setStep('access'), children: "Continue" })) : (_jsxs(YStack, { gap: "$3", items: "center", width: "100%", children: [_jsx(Button, { width: "100%", disabled: busy || syncing || picked.length === 0, onPress: () => void send(), children: syncing || busy ? 'Syncing…' : `Sync ${picked.length} ${picked.length === 1 ? 'repository' : 'repositories'} to ${dest}` }), syncing ? null : (_jsx(XStack, { render: "button", "aria-label": "Sync later", onPress: back, children: _jsx(SizableText, { size: "$2", color: "$soft", children: "Sync later" }) }))] })), !signed ? (_jsx(Button, { width: "100%", onPress: () => host.signIn?.(), children: "Sign in" })) : null] })] })] }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/build",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.52",
|
|
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.",
|