@hanzo/build 0.2.0 → 0.2.1

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
@@ -3,9 +3,9 @@
3
3
  Say what you want. Hanzo writes it, runs it, and puts it on a live URL — with
4
4
  the database, the sign-in and the storage already there.
5
5
 
6
- The builder is one component with two hosts: this page (hanzo.build), and the
7
- platform console at platform.hanzo.ai/dev. Both mount `<Builder host={…} />`
8
- from `@hanzo/build`; neither forks it.
6
+ The builder is one component, `<Builder host={…} />` from `@hanzo/build`, with
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.
9
9
 
10
10
  ```
11
11
  pnpm install
@@ -42,6 +42,25 @@ under the mount (`path`) and how to move (`go`), and where to link out
42
42
  (`links`, `open`). Peers: `@hanzo/ui`, `@hanzo/gui`, `@hanzogui/lucide-icons-2`,
43
43
  `react`.
44
44
 
45
+ ### In a host that has its own rail
46
+
47
+ The Hanzo app already has a left column, so it mounts `<Builder host={host}
48
+ rail={false} />` in its pane and lists the builder in its own rail:
49
+
50
+ ```tsx
51
+ import { Builder, DevSection } from '@hanzo/build'
52
+
53
+ <Sidebar> {/* the host's rail, @hanzo/ui/chat */}
54
+ <DevSection host={host} /> {/* New run, Artifacts, Templates, the runs */}
55
+ </Sidebar>
56
+ <Builder host={host} rail={false} />
57
+ ```
58
+
59
+ `DevSection` is rows of `@hanzo/ui/chat`'s Sidebar; `useSessions(host)` is its
60
+ data — the org's coding runs, kept live by the org's feed — for a host that
61
+ draws the rows itself. `administers(token, org)` reads the org-admin bit off a
62
+ hanzo.id token for `Host.admin`. One left column, never two.
63
+
45
64
  ## Addresses
46
65
 
47
66
  | path | screen |
@@ -57,10 +76,10 @@ collide.
57
76
 
58
77
  ## What is on the screen
59
78
 
60
- **The rail.** New, Artifacts, Customize (the platform's plugins), More
61
- (Templates, Machines, Docs), then the org's coding runs newest first with a
62
- live status dot, and the account. Collapse is an explicit toggle kept in this
63
- browser.
79
+ **The rail** (hanzo.build). New, Artifacts, Customize (the platform's
80
+ plugins), More (Templates, Machines, Docs), then the org's coding runs newest
81
+ first with a live status dot, and the account. Collapse is an explicit toggle
82
+ kept in this browser. A host with its own rail draws `DevSection` there instead.
64
83
 
65
84
  **New.** "What's up next?", and at the foot the composer: where the run runs
66
85
  (Default is the platform's sandbox; the org's machines follow), the repository
package/lib/builder.d.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  import { type Host } from './host.tsx';
2
- /** The builder. Mount it under a gui root (`<Hanzo>`), in a box with a height. */
3
- export declare function Builder({ host }: {
2
+ /**
3
+ * The builder. Mount it under a gui root (`<Hanzo>`), in a box with a height.
4
+ *
5
+ * `rail={false}` leaves the left column to the host, which lists the builder's
6
+ * places and runs in its own rail with `DevSection`.
7
+ */
8
+ export declare function Builder({ host, rail }: {
4
9
  host: Host;
10
+ rail?: boolean;
5
11
  }): import("react").JSX.Element;
package/lib/builder.js CHANGED
@@ -14,6 +14,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
14
14
  *
15
15
  * A project's workspace takes the whole window — it has its own chat column,
16
16
  * bar and panes — and its mark leads back here.
17
+ *
18
+ * A host with a rail of its own mounts `<Builder rail={false}>` and draws the
19
+ * builder's places and runs in that rail with `DevSection` (section.tsx): one
20
+ * left column, never two.
17
21
  */
18
22
  import { SizableText, XStack, YStack } from '@hanzo/gui';
19
23
  import { Blocks, BookOpen, Cpu, LayoutTemplate, Menu, Puzzle } from '@hanzogui/lucide-icons-2';
@@ -27,15 +31,9 @@ import { Landing } from './landing.js';
27
31
  import { Project } from './project.js';
28
32
  import { path, route } from './route.js';
29
33
  import { Run } from './run.js';
34
+ import { DOTS } from './section.js';
30
35
  import { Artifacts, Templates } from './shelf.js';
31
36
  import { Account, Find } from './account.js';
32
- const DOTS = {
33
- running: 'running',
34
- paused: 'paused',
35
- done: 'done',
36
- stopped: 'stopped',
37
- error: 'error',
38
- };
39
37
  function Shell() {
40
38
  const host = useHost();
41
39
  const t = useTarget();
@@ -68,14 +66,34 @@ function Shell() {
68
66
  ];
69
67
  return (_jsxs(XStack, { flex: 1, minH: 0, minW: 0, bg: "$background", 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
70
68
  ? { name: host.person.email || host.person.name, onPress: () => setAccount(true) }
71
- : { name: 'Sign in', onPress: () => host.signIn?.() }, onSettings: () => host.open(host.links.settings), onSearch: () => setFinding(true), collapsed: collapsed, onCollapse: setCollapsed, mark: _jsx(HanzoMark, { size: 18 }), open: drawer, onOpenChange: setDrawer, label: "Runs" }), _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 }) }) }), r.kind === 'run' ? (_jsx(Run, { id: r.id }, r.id)) : r.kind === 'screen' ? (r.screen === 'artifacts' ? _jsx(Artifacts, {}) : _jsx(Templates, {})) : (_jsx(Landing, { onStarted: (id) => { recents.reload(); go(id); } }))] }), _jsx(Account, { open: account, onOpenChange: setAccount }), _jsx(Find, { open: finding, onOpenChange: setFinding, recents: rows, onOpen: (id) => { setFinding(false); go(id); } })] }));
69
+ : { name: 'Sign in', onPress: () => host.signIn?.() }, onSettings: () => host.open(host.links.settings), onSearch: () => setFinding(true), collapsed: collapsed, onCollapse: setCollapsed, mark: _jsx(HanzoMark, { size: 18 }), open: drawer, onOpenChange: setDrawer, label: "Runs" }), _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); } })] }));
70
+ }
71
+ /** Whichever pane the address names, beside a rail — the builder's or the host's. */
72
+ function Pane({ onStarted }) {
73
+ const host = useHost();
74
+ const r = route(host.path);
75
+ if (r.kind === 'run')
76
+ return _jsx(Run, { id: r.id }, r.id);
77
+ if (r.kind === 'screen')
78
+ return r.screen === 'artifacts' ? _jsx(Artifacts, {}) : _jsx(Templates, {});
79
+ return (_jsx(Landing, { onStarted: (id) => {
80
+ onStarted?.(id);
81
+ host.go(id);
82
+ } }));
72
83
  }
73
- function Screens() {
84
+ function Screens({ rail }) {
74
85
  const host = useHost();
75
86
  const r = route(host.path);
76
- return r.kind === 'project' ? _jsx(Project, { slug: r.slug }, r.slug) : _jsx(Shell, {});
87
+ if (r.kind === 'project')
88
+ return _jsx(Project, { slug: r.slug }, r.slug);
89
+ return rail ? _jsx(Shell, {}) : _jsx(Pane, {});
77
90
  }
78
- /** The builder. Mount it under a gui root (`<Hanzo>`), in a box with a height. */
79
- export function Builder({ host }) {
80
- return (_jsx(HostProvider, { host: host, children: _jsx(YStack, { flex: 1, minH: 0, minW: 0, height: "100%", children: _jsx(Screens, {}) }) }));
91
+ /**
92
+ * The builder. Mount it under a gui root (`<Hanzo>`), in a box with a height.
93
+ *
94
+ * `rail={false}` leaves the left column to the host, which lists the builder's
95
+ * places and runs in its own rail with `DevSection`.
96
+ */
97
+ export function Builder({ host, rail = true }) {
98
+ return (_jsx(HostProvider, { host: host, children: _jsx(YStack, { flex: 1, minH: 0, minW: 0, height: "100%", bg: "$background", children: _jsx(Screens, { rail: rail }) }) }));
81
99
  }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * What a hanzo.id access token says, read without trusting it for anything the
3
+ * platform does not check again.
4
+ */
5
+ /** The token's claims, or null when it is not a JWT. */
6
+ export declare function claims(token: string | null): Record<string, unknown> | null;
7
+ /**
8
+ * Whether the token says its person administers `org`. Org admin, read off the
9
+ * token's own `orgs` claim — never the reserved `admin` org, which is platform
10
+ * authority and not what a builder decides anything by. The platform enforces
11
+ * it again; this only decides which publish path is offered.
12
+ */
13
+ export declare function administers(token: string | null, org: string | null): boolean;
package/lib/claims.js ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * What a hanzo.id access token says, read without trusting it for anything the
3
+ * platform does not check again.
4
+ */
5
+ /** The token's claims, or null when it is not a JWT. */
6
+ export function claims(token) {
7
+ if (!token)
8
+ return null;
9
+ try {
10
+ const part = token.split('.')[1];
11
+ if (!part)
12
+ return null;
13
+ return JSON.parse(atob(part.replace(/-/g, '+').replace(/_/g, '/')));
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ /**
20
+ * Whether the token says its person administers `org`. Org admin, read off the
21
+ * token's own `orgs` claim — never the reserved `admin` org, which is platform
22
+ * authority and not what a builder decides anything by. The platform enforces
23
+ * it again; this only decides which publish path is offered.
24
+ */
25
+ export function administers(token, org) {
26
+ if (!org)
27
+ return false;
28
+ const set = claims(token)?.orgs;
29
+ return (Array.isArray(set) ? set : []).some((r) => r?.org === org && r.role === 'admin');
30
+ }
package/lib/host.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * What the builder needs from the page that mounts it.
3
3
  *
4
- * The builder is ONE component with two hosts: its own page (hanzo.build, which
5
- * signs in through Hanzo IAM and routes with react-router) and the platform
6
- * console (platform.hanzo.ai/dev, which already has a session and a router).
7
- * Everything that differs between them is here, as values, so neither host
8
- * forks the builder and the builder imports neither host.
4
+ * The builder is ONE component with several hosts: its own page (hanzo.build,
5
+ * which signs in through Hanzo IAM and routes with react-router) and the Hanzo
6
+ * app's Dev section (hanzo.ai/dev and hanzo.app/dev, which already have a
7
+ * session, a router and a rail). Everything that differs between them is here,
8
+ * as values, so no host forks the builder and the builder imports no host.
9
9
  */
10
10
  import { type ReactNode } from 'react';
11
11
  import type { Target } from './api/call.ts';
package/lib/host.js CHANGED
@@ -2,11 +2,11 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  /**
3
3
  * What the builder needs from the page that mounts it.
4
4
  *
5
- * The builder is ONE component with two hosts: its own page (hanzo.build, which
6
- * signs in through Hanzo IAM and routes with react-router) and the platform
7
- * console (platform.hanzo.ai/dev, which already has a session and a router).
8
- * Everything that differs between them is here, as values, so neither host
9
- * forks the builder and the builder imports neither host.
5
+ * The builder is ONE component with several hosts: its own page (hanzo.build,
6
+ * which signs in through Hanzo IAM and routes with react-router) and the Hanzo
7
+ * app's Dev section (hanzo.ai/dev and hanzo.app/dev, which already have a
8
+ * session, a router and a rail). Everything that differs between them is here,
9
+ * as values, so no host forks the builder and the builder imports no host.
10
10
  */
11
11
  import { createContext, useContext, useMemo } from 'react';
12
12
  const Context = createContext(null);
package/lib/index.d.ts CHANGED
@@ -3,9 +3,14 @@
3
3
  *
4
4
  * Mount `<Builder host={…} />` under a gui root in a box with a height. The
5
5
  * host says where the platform is, who is signed in, and how to move; the
6
- * builder does everything else. hanzo.build and platform.hanzo.ai/dev are its
7
- * two hosts.
6
+ * builder does everything else. hanzo.build hosts it whole; hanzo.ai and the
7
+ * Hanzo App mount it at /dev with `rail={false}` and list its places and runs in
8
+ * their own rail with `DevSection`.
8
9
  */
9
10
  export { Builder } from './builder.tsx';
11
+ export { DevSection, DOTS, useSessions } from './section.tsx';
12
+ export { administers } from './claims.ts';
10
13
  export type { Host, Person } from './host.tsx';
14
+ export type { Session, Status } from './api/sessions.ts';
15
+ export type { Read } from './data.ts';
11
16
  export { path, route, SESSION, SLUG, type Route, type Screen } from './route.ts';
package/lib/index.js CHANGED
@@ -3,8 +3,11 @@
3
3
  *
4
4
  * Mount `<Builder host={…} />` under a gui root in a box with a height. The
5
5
  * host says where the platform is, who is signed in, and how to move; the
6
- * builder does everything else. hanzo.build and platform.hanzo.ai/dev are its
7
- * two hosts.
6
+ * builder does everything else. hanzo.build hosts it whole; hanzo.ai and the
7
+ * Hanzo App mount it at /dev with `rail={false}` and list its places and runs in
8
+ * their own rail with `DevSection`.
8
9
  */
9
10
  export { Builder } from './builder.js';
11
+ export { DevSection, DOTS, useSessions } from './section.js';
12
+ export { administers } from './claims.js';
10
13
  export { path, route, SESSION, SLUG } from './route.js';
@@ -0,0 +1,20 @@
1
+ import { type SessionStatus } from '@hanzo/ui/chat';
2
+ import { type ReactNode } from 'react';
3
+ import type { Session } from './api/sessions.ts';
4
+ import { type Read } from './data.ts';
5
+ import type { Host } from './host.tsx';
6
+ /** A run's status, as the rail's dot draws it. Anything else is idle. */
7
+ export declare const DOTS: Record<string, SessionStatus>;
8
+ /** The org's coding runs, newest first, kept live by the org's feed. */
9
+ export declare function useSessions(host: Host): Read<Session[]>;
10
+ /**
11
+ * New, Artifacts, Templates, then the runs. `children` sit between the places
12
+ * and the runs — where a host lists its projects. `onPick` fires after a move,
13
+ * so a host that draws its rail as a drawer on a phone can close it.
14
+ */
15
+ export declare function DevSection({ host, onPick, label, children, }: {
16
+ host: Host;
17
+ onPick?: () => void;
18
+ label?: string;
19
+ children?: ReactNode;
20
+ }): import("react").JSX.Element;
package/lib/section.js ADDED
@@ -0,0 +1,47 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The builder's places and runs, for a host that draws its own rail.
4
+ *
5
+ * A host that mounts `<Builder rail={false}>` keeps ONE left column — its own —
6
+ * and lists the builder in it with these. `useSessions` is the same read and
7
+ * the same org-wide feed the built-in rail uses, so a run started in the pane
8
+ * appears here with its live status. `DevSection` draws the builder's places
9
+ * and runs as rows of the host's sidebar (the `@hanzo/ui/chat` Sidebar the
10
+ * built-in rail is made of), and every row moves the builder through the
11
+ * host's own `go`.
12
+ */
13
+ import { SizableText } from '@hanzo/gui';
14
+ import { Blocks, LayoutTemplate, SquarePen } from '@hanzogui/lucide-icons-2';
15
+ import { SidebarItem, SidebarSection, StatusDot } from '@hanzo/ui/chat';
16
+ import { useMemo } from 'react';
17
+ import { useRecents } from './data.js';
18
+ import { path, route } from './route.js';
19
+ /** A run's status, as the rail's dot draws it. Anything else is idle. */
20
+ export const DOTS = {
21
+ running: 'running',
22
+ paused: 'paused',
23
+ done: 'done',
24
+ stopped: 'stopped',
25
+ error: 'error',
26
+ };
27
+ /** The org's coding runs, newest first, kept live by the org's feed. */
28
+ export function useSessions(host) {
29
+ const t = useMemo(() => ({ api: host.api, token: host.token, org: host.org }), [host.api, host.token, host.org]);
30
+ return useRecents(t, Boolean(host.person));
31
+ }
32
+ const Note = ({ children }) => (_jsx(SizableText, { size: "$1", color: "$soft", px: "$2", py: "$1", children: children }));
33
+ /**
34
+ * New, Artifacts, Templates, then the runs. `children` sit between the places
35
+ * and the runs — where a host lists its projects. `onPick` fires after a move,
36
+ * so a host that draws its rail as a drawer on a phone can close it.
37
+ */
38
+ export function DevSection({ host, onPick, label = 'Runs', children, }) {
39
+ const sessions = useSessions(host);
40
+ const r = route(host.path);
41
+ const go = (p) => {
42
+ host.go(p);
43
+ onPick?.();
44
+ };
45
+ const screen = r.kind === 'screen' ? r.screen : '';
46
+ 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(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.' })) })] }));
47
+ }
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@hanzo/build",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0 OR MIT",
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 (platform.hanzo.ai/dev).",
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).",
7
7
  "engines": {
8
8
  "node": ">=22.18"
9
9
  },