@hanzo/build 0.2.5 → 0.2.6

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 CHANGED
@@ -5,7 +5,9 @@ the database, the sign-in and the storage already there.
5
5
 
6
6
  The builder is one component, `<Builder host={…} />` from `@hanzo/build`, with
7
7
  three hosts: this page (hanzo.build), and the Dev section of the Hanzo app at
8
- hanzo.ai/dev and hanzo.app/dev. None of them forks it.
8
+ hanzo.ai/dev and hanzo.app/dev. None of them forks it. hanzo.build is this
9
+ frontend. A click stays on this origin — sign-in returns to `/auth/callback`
10
+ here — and github.com is opened only for the repository grant.
9
11
 
10
12
  ```
11
13
  pnpm install
package/lib/account.d.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  import type { RailSession } from '@hanzo/ui/chat';
2
+ /** The builder listens, so a settings control on this page opens the account dialog. */
3
+ export declare function onAccount(fn: () => void): () => void;
4
+ export declare function revealAccount(): void;
2
5
  export declare function Account({ open, onOpenChange }: {
3
6
  open: boolean;
4
7
  onOpenChange: (o: boolean) => void;
package/lib/account.js CHANGED
@@ -8,10 +8,33 @@ import { LogOut, Settings } from '@hanzogui/lucide-icons-2';
8
8
  import { Button, Dialog, DialogContent, DialogTitle, Input } from '@hanzo/ui';
9
9
  import { useMemo, useState } from 'react';
10
10
  import { useHost } from './host.js';
11
+ let reveal = null;
12
+ /** The builder listens, so a settings control on this page opens the account dialog. */
13
+ export function onAccount(fn) {
14
+ reveal = fn;
15
+ return () => {
16
+ if (reveal === fn)
17
+ reveal = null;
18
+ };
19
+ }
20
+ export function revealAccount() {
21
+ reveal?.();
22
+ }
23
+ /** A settings address on another host. This page's own account is the dialog itself. */
24
+ function accountSettings(href) {
25
+ if (typeof window === 'undefined')
26
+ return false;
27
+ try {
28
+ return new URL(href, window.location.origin).origin !== window.location.origin;
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
11
34
  export function Account({ open, onOpenChange }) {
12
35
  const host = useHost();
13
36
  const who = host.person;
14
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 360, children: [_jsx(DialogTitle, { children: "Account" }), _jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", numberOfLines: 1, children: who?.name || who?.email || 'Signed out' }), who?.email && who.name ? (_jsx(SizableText, { size: "$2", color: "$soft", numberOfLines: 1, children: who.email })) : null, host.org ? (_jsxs(SizableText, { size: "$1", color: "$soft", children: ["Organization: ", host.org] })) : null] }), _jsxs(XStack, { gap: "$2", justify: "flex-end", flexWrap: "wrap", children: [_jsxs(Button, { variant: "outline", size: "sm", onPress: () => host.open(host.links.settings), children: [_jsx(Settings, { size: 14 }), " Settings"] }), host.signOut ? (_jsxs(Button, { variant: "outline", size: "sm", onPress: () => host.signOut?.(), children: [_jsx(LogOut, { size: 14 }), " Sign out"] })) : null] })] }) }));
37
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 360, children: [_jsx(DialogTitle, { children: "Account" }), _jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", numberOfLines: 1, children: who?.name || who?.email || 'Signed out' }), who?.email && who.name ? (_jsx(SizableText, { size: "$2", color: "$soft", numberOfLines: 1, children: who.email })) : null, host.org ? (_jsxs(SizableText, { size: "$1", color: "$soft", children: ["Organization: ", host.org] })) : null] }), _jsxs(XStack, { gap: "$2", justify: "flex-end", flexWrap: "wrap", children: [accountSettings(host.links.settings) ? (_jsxs(Button, { variant: "outline", size: "sm", onPress: () => host.open(host.links.settings), children: [_jsx(Settings, { size: 14 }), " Settings"] })) : null, host.signOut ? (_jsxs(Button, { variant: "outline", size: "sm", onPress: () => host.signOut?.(), children: [_jsx(LogOut, { size: 14 }), " Sign out"] })) : null] })] }) }));
15
38
  }
16
39
  export function Find({ open, onOpenChange, recents, onOpen, }) {
17
40
  const [q, setQ] = useState('');
package/lib/builder.js CHANGED
@@ -29,7 +29,7 @@ import { Blocks, BookOpen, CircleDot, Cpu, FolderGit2, Kanban, LayoutTemplate, M
29
29
  import { Button } from '@hanzo/ui';
30
30
  import { SessionRail } from '@hanzo/ui/chat';
31
31
  import { HanzoMark } from '@hanzo/ui/product';
32
- import { useMemo, useState } from 'react';
32
+ import { useEffect, useMemo, useState } from 'react';
33
33
  import { pinBoard } from './choice.js';
34
34
  import { useKept, useRecents } from './data.js';
35
35
  import { Forge } from './forge.js';
@@ -40,7 +40,7 @@ import { path, route } from './route.js';
40
40
  import { Run } from './run.js';
41
41
  import { DOTS } from './section.js';
42
42
  import { Artifacts, Templates } from './shelf.js';
43
- import { Account, Find } from './account.js';
43
+ import { Account, Find, onAccount } from './account.js';
44
44
  function Shell() {
45
45
  const host = useHost();
46
46
  const t = useTarget();
@@ -51,6 +51,7 @@ function Shell() {
51
51
  const [order, setOrder] = useState('newest');
52
52
  const [account, setAccount] = useState(false);
53
53
  const [finding, setFinding] = useState(false);
54
+ useEffect(() => onAccount(() => setAccount(true)), []);
54
55
  const go = (p) => {
55
56
  setDrawer(false);
56
57
  host.go(p);
@@ -81,12 +82,41 @@ function Shell() {
81
82
  ];
82
83
  const more = [
83
84
  { id: 'templates', label: 'Templates', icon: _jsx(LayoutTemplate, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'templates' })), active: screen === 'templates' },
84
- { id: 'machines', label: 'Machines', icon: _jsx(Cpu, { size: 16 }), onPress: () => host.open(`${host.links.home.replace(/\/+$/, '')}/platform/computers`) },
85
+ {
86
+ id: 'machines',
87
+ label: 'Machines',
88
+ icon: _jsx(Cpu, { size: 16 }),
89
+ onPress: () => {
90
+ const home = host.links.home.replace(/\/+$/, '');
91
+ const computers = `${home}/platform/computers`;
92
+ try {
93
+ if (new URL(computers, window.location.origin).origin === window.location.origin) {
94
+ host.go('');
95
+ return;
96
+ }
97
+ }
98
+ catch {
99
+ return;
100
+ }
101
+ host.open(computers);
102
+ },
103
+ },
85
104
  { id: 'docs', label: 'Docs', icon: _jsx(BookOpen, { size: 16 }), onPress: () => window.open('https://docs.hanzo.ai/docs/dev', '_blank', 'noopener,noreferrer') },
86
105
  ];
87
106
  return (_jsxs(XStack, { flex: 1, minH: 0, minW: 0, bg: "$background", children: [_jsxs(YStack, { position: "relative", shrink: 0, children: [_jsx(SessionRail, { onNew: () => go(''), fresh: r.kind === 'new', links: links, more: more, recents: rows, active: r.kind === 'run' ? r.id : null, onOpen: (id) => go(id), onSort: () => setOrder(order === 'newest' ? 'running' : 'newest'), sortLabel: order === 'newest' ? 'Show running first' : 'Show newest first', empty: _jsx(SizableText, { size: "$1", color: "$soft", px: "$2", children: !host.person ? 'Sign in to see your runs.' : recents.error ? recents.error.message : recents.loading ? 'Reading…' : 'No runs yet.' }), account: host.person
88
107
  ? { name: host.person.email || host.person.name, onPress: () => setAccount(true) }
89
- : { name: 'Sign in', onPress: () => host.signIn?.() }, onSettings: () => host.open(host.links.settings), onSearch: () => setFinding(true), collapsed: collapsed, onCollapse: setCollapsed, mark: _jsx(Brand, { org: host.org }), pt: collapsed ? undefined : 44, open: drawer, onOpenChange: setDrawer, label: "Runs" }), collapsed ? null : (_jsxs(XStack, { position: "absolute", t: 8, l: 8, r: 8, z: 2, items: "center", gap: "$2", render: "button", "aria-label": host.org ? `Organization ${host.org}` : 'Hanzo', onPress: () => go(''), children: [_jsx(Brand, { org: host.org }), _jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: host.org || 'Hanzo' })] }))] }), _jsxs(YStack, { flex: 1, minW: 0, minH: 0, position: "relative", children: [_jsx(XStack, { height: 44, px: "$2", items: "center", shrink: 0, $md: { display: 'none' }, children: _jsx(Button, { variant: "ghost", size: "icon-sm", onPress: () => setDrawer(true), "aria-label": "Open runs", children: _jsx(Menu, { size: 18 }) }) }), _jsx(Pane, { onStarted: () => recents.reload() })] }), _jsx(Account, { open: account, onOpenChange: setAccount }), _jsx(Find, { open: finding, onOpenChange: setFinding, recents: rows, onOpen: (id) => { setFinding(false); go(id); } })] }));
108
+ : { name: 'Sign in', onPress: () => host.signIn?.() }, onSettings: () => {
109
+ try {
110
+ if (new URL(host.links.settings, window.location.origin).origin === window.location.origin) {
111
+ setAccount(true);
112
+ return;
113
+ }
114
+ }
115
+ catch {
116
+ return;
117
+ }
118
+ host.open(host.links.settings);
119
+ }, onSearch: () => setFinding(true), collapsed: collapsed, onCollapse: setCollapsed, mark: _jsx(Brand, { org: host.org }), pt: collapsed ? undefined : 44, open: drawer, onOpenChange: setDrawer, label: "Runs" }), collapsed ? null : (_jsxs(XStack, { position: "absolute", t: 8, l: 8, r: 8, z: 2, items: "center", gap: "$2", render: "button", "aria-label": host.org ? `Organization ${host.org}` : 'Hanzo', onPress: () => go(''), children: [_jsx(Brand, { org: host.org }), _jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: host.org || 'Hanzo' })] }))] }), _jsxs(YStack, { flex: 1, minW: 0, minH: 0, position: "relative", children: [_jsx(XStack, { height: 44, px: "$2", items: "center", shrink: 0, $md: { display: 'none' }, children: _jsx(Button, { variant: "ghost", size: "icon-sm", onPress: () => setDrawer(true), "aria-label": "Open runs", children: _jsx(Menu, { size: 18 }) }) }), _jsx(Pane, { onStarted: () => recents.reload() })] }), _jsx(Account, { open: account, onOpenChange: setAccount }), _jsx(Find, { open: finding, onOpenChange: setFinding, recents: rows, onOpen: (id) => { setFinding(false); go(id); } })] }));
90
120
  }
91
121
  /** Whichever pane the address names, beside a rail — the builder's or the host's. */
92
122
  function Pane({ onStarted }) {
package/lib/forge.js CHANGED
@@ -10,6 +10,7 @@ import { SizableText, XStack, YStack } from '@hanzo/gui';
10
10
  import { ChevronDown, GitBranch, Plus, Settings, Workflow } from '@hanzogui/lucide-icons-2';
11
11
  import { Button, Dialog, DialogContent, DialogTitle, Input } from '@hanzo/ui';
12
12
  import { useEffect, useState } from 'react';
13
+ import { revealAccount } from './account.js';
13
14
  import { add, arm, flows } from './api/auto.js';
14
15
  import { codebases, create } from './api/codebases.js';
15
16
  import { boards, issues } from './api/work.js';
@@ -169,7 +170,18 @@ function Codebases() {
169
170
  pinCodebase(host.org, c);
170
171
  host.go('');
171
172
  };
172
- 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", 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", items: "center", gap: "$1.5", px: "$2", py: "$1", rounded: "$3", hoverStyle: { bg: '$hover' }, onPress: () => host.open(host.links.settings), 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", children: _jsx(SizableText, { size: "$2", color: "$ink", children: "All repos" }) }), _jsx(YStack, { flex: 1 }), _jsx(YStack, { width: 240, minW: 0, 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 ? (_jsx(Note, { children: list.value.length === 0 ? 'No repositories yet.' : 'Nothing matches.' })) : (_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] }), _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: 140, style: { textAlign: 'right' }, children: syncing.has(c.name) ? 'Syncing…' : when(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) => {
173
+ 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", 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", items: "center", gap: "$1.5", px: "$2", py: "$1", rounded: "$3", hoverStyle: { bg: '$hover' }, onPress: () => {
174
+ try {
175
+ if (new URL(host.links.settings, window.location.origin).origin === window.location.origin) {
176
+ revealAccount();
177
+ return;
178
+ }
179
+ }
180
+ catch {
181
+ return;
182
+ }
183
+ host.open(host.links.settings);
184
+ }, 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", children: _jsx(SizableText, { size: "$2", color: "$ink", children: "All repos" }) }), _jsx(YStack, { flex: 1 }), _jsx(YStack, { width: 240, minW: 0, 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 ? (_jsx(Note, { children: list.value.length === 0 ? 'No repositories yet.' : 'Nothing matches.' })) : (_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] }), _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: 140, style: { textAlign: 'right' }, children: syncing.has(c.name) ? 'Syncing…' : when(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) => {
173
185
  const row = await create(t, name, description);
174
186
  pinCodebase(host.org, row);
175
187
  setMaking(false);
package/lib/host.d.ts CHANGED
@@ -32,18 +32,18 @@ export interface Host {
32
32
  go: (path: string, how?: {
33
33
  replace?: boolean;
34
34
  }) => void;
35
- /** Where the builder links out to. Same-window addresses on the host, or https. */
35
+ /** Where the builder links out. On hanzo.build these are addresses of this page. */
36
36
  links: {
37
- /** Troubleshoot GitHub connection: the GitHub pane of the platform's plugins. */
37
+ /** GitHub for this page: bringing repositories onto the forge. */
38
38
  github: string;
39
- /** Customize: the platform's plugins (skills, MCP, connectors). */
39
+ /** Customize, an address of this host. */
40
40
  customize: string;
41
- /** Account settings. */
41
+ /** Account settings, an address of this host. */
42
42
  settings: string;
43
- /** The platform itself, where the mark leads. */
43
+ /** This host's home. On hanzo.build it is the page itself. */
44
44
  home: string;
45
45
  };
46
- /** Leave the builder for a host address, same window. */
46
+ /** Open a host address in this window. hanzo.build keeps the window on its own origin. */
47
47
  open: (href: string) => void;
48
48
  signIn?: () => void;
49
49
  signOut?: () => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/build",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
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 page (hanzo.build) and as a component (hanzo.ai/dev).",