@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 CHANGED
@@ -99,18 +99,18 @@ hanzo.id token for `Host.admin`. One left column, never two.
99
99
  | `-/automations` | repeating work, read from `/v1/auto/flows` |
100
100
  | `-/customize` · `-/customize/connectors` · `/plugins` · `/agents` | Customize: skills, connectors, plugins, agents |
101
101
  | `-/mcp` | Connectors → Discover, where the fleet's native MCP servers now live |
102
- | `-/codebases` | the forge's repositories |
103
- | `-/sync` | bring granted repositories onto the forge |
104
- | `-/projects` | the forge's boards |
102
+ | `-/projects` | the org's projects: its repositories on the forge |
103
+ | `-/sync` | link repositories from GitHub onto the forge |
105
104
  | `-/issues` | the forge's issues |
106
105
  | `-/artifacts` | what the org has built |
107
106
  | `-/templates` | the public starters |
108
107
  | `-/plans` | the plans this brand sells, and the move to another |
109
108
  | `-/settings/<section>` | Billing, Usage, Members, Integrations, Notifications, … |
110
- | `<slug>` | a project's workspace |
109
+ | `<org>/<repo>` | a project's workspace: the repository, and its site when it has one |
110
+ | `<slug>` | a deployed site's workspace |
111
111
 
112
- A project slug holds no `_` and never starts with `-`, so the forms cannot
113
- collide.
112
+ A project slug holds no `_` and never starts with `-`, and a repository is the
113
+ one address of two segments, so the forms cannot collide.
114
114
 
115
115
  ## What is on the screen
116
116
 
@@ -214,7 +214,7 @@ picker and open-in-tab; Share and Publish; the console dock under it.
214
214
  | `PATCH /v1/agent/sessions/{id}` `{title}` · `{published}` | rename a run, share its story at `GET /v1/agent/builds/{org}/{project}` |
215
215
  | `GET /v1/agent/sessions?kind=coding&status=&after=` | Find, by status, paged |
216
216
  | `GET`/`POST /v1/agent/targets` · `PATCH`/`DELETE …/{id}` · `POST …/{id}/key` | the org's machines, and a machine's claim key |
217
- | `GET`/`POST`/`DELETE /v1/account/keys` | your API keys |
217
+ | `GET`/`POST /v1/account/keys` · `DELETE …/{id}` | your API keys, as many as you need; one revoked at a time |
218
218
  | `GET /v1/auto/flows` · `POST /v1/auto/flows` · `POST /v1/auto/flows/{id}/enable` | automations |
219
219
  | `GET /v1/environment` · `GET`/`PUT`/`DELETE /v1/environment/{repo}` · `PUT`/`DELETE …/secrets/{name}` | a codebase's environment |
220
220
  | `GET /v1/agent/coding/{session}/changes` · `/tree` · `/blob` | what a run pushed, and its branch's files, from the forge |
@@ -8,6 +8,7 @@
8
8
  * POST /v1/provider/github/user/complete {grant} → the connection
9
9
  * POST /v1/provider/github/user/disconnect
10
10
  * GET /v1/provider/github/installations the accounts this org has bound the App on
11
+ * POST /v1/provider/github/issues/backfill {state, repo?} → mirror GitHub issues onto the board
11
12
  *
12
13
  * Paged by an opaque `after` cursor; `next` is the cursor for the page after
13
14
  * this one, or empty on the last. Rows are read defensively: a field the
@@ -128,3 +129,20 @@ export declare function accounts(g: Grants): Account[];
128
129
  export declare function grants(t: Target, signal?: AbortSignal): Promise<Grants>;
129
130
  /** Queue a mirror of the named repositories onto the forge. The names are `owner/name`. */
130
131
  export declare function bring(t: Target, repos: string[]): Promise<number>;
132
+ /** What one issue sync did. */
133
+ export interface Synced {
134
+ repos: number;
135
+ issues: number;
136
+ created: number;
137
+ updated: number;
138
+ failed: number;
139
+ /** The pass stopped at its budget; syncing again continues where it stopped. */
140
+ truncated: boolean;
141
+ }
142
+ /**
143
+ * Mirror GitHub issues onto the board — open and closed, so a closed one reads as
144
+ * closed here too. `repo` (`name` or `owner/name`) syncs one project; empty syncs
145
+ * every repository the connection grants. Idempotent: a second sync updates and
146
+ * never duplicates, and the App's webhook keeps them live afterwards.
147
+ */
148
+ export declare function syncIssues(t: Target, repo?: string): Promise<Synced>;
package/lib/api/github.js CHANGED
@@ -8,6 +8,7 @@
8
8
  * POST /v1/provider/github/user/complete {grant} → the connection
9
9
  * POST /v1/provider/github/user/disconnect
10
10
  * GET /v1/provider/github/installations the accounts this org has bound the App on
11
+ * POST /v1/provider/github/issues/backfill {state, repo?} → mirror GitHub issues onto the board
11
12
  *
12
13
  * Paged by an opaque `after` cursor; `next` is the cursor for the page after
13
14
  * this one, or empty on the last. Rows are read defensively: a field the
@@ -143,3 +144,21 @@ export async function bring(t, repos) {
143
144
  const raw = obj(await call(t, 'POST', '/v1/provider/github/repos/import', { repos: names }));
144
145
  return num(raw.queued);
145
146
  }
147
+ /**
148
+ * Mirror GitHub issues onto the board — open and closed, so a closed one reads as
149
+ * closed here too. `repo` (`name` or `owner/name`) syncs one project; empty syncs
150
+ * every repository the connection grants. Idempotent: a second sync updates and
151
+ * never duplicates, and the App's webhook keeps them live afterwards.
152
+ */
153
+ export async function syncIssues(t, repo = '') {
154
+ const name = repo.trim();
155
+ const raw = obj(await call(t, 'POST', '/v1/provider/github/issues/backfill', name ? { state: 'all', repo: name } : { state: 'all' }));
156
+ return {
157
+ repos: num(raw.repos),
158
+ issues: num(raw.issues),
159
+ created: num(raw.created),
160
+ updated: num(raw.updated),
161
+ failed: num(raw.failed),
162
+ truncated: raw.truncated === true,
163
+ };
164
+ }
package/lib/api/keys.d.ts CHANGED
@@ -1,31 +1,38 @@
1
1
  /**
2
- * A person's own API keys: one secret key (sk-, it belongs on a server) and one
3
- * publishable key (pk-, safe in a page's source), at most one of each.
2
+ * A person's API keys, each its own credential: as many as they like, of either
3
+ * type — secret (sk-, it belongs on a server) or publishable (pk-, safe in a
4
+ * page's source).
4
5
  *
5
- * GET /v1/account/keys {keys: [{type, prefix, key?, limit?, createdAt}]}
6
- * POST /v1/account/keys {type, limit?} → {key, type, limit}, answered once
7
- * DELETE /v1/account/keys?type=… revoke that one
6
+ * GET /v1/account/keys {keys: [{id, name, type, prefix, key?, status, limit?, created, …}]}
7
+ * POST /v1/account/keys {name, type, limit?} → the key, its secret answered once
8
+ * DELETE /v1/account/keys/{id} revoke exactly that one; it stays listed as revoked
8
9
  *
9
- * Creating is rotating: a new key of a type ends the one before it. A secret key
10
- * is answered once and by its prefix after; a publishable key is public by
11
- * construction and always comes back whole. A limit only narrows what a key may
12
- * reach — `model:zen5`, `project:acme`, `product:commerce`, `model:*`.
10
+ * Creating a key never touches another. A secret key is answered once and by its
11
+ * prefix after; a publishable key is public by construction and always comes back
12
+ * whole. A limit only narrows what a key may reach — `model:zen5`,
13
+ * `project:acme`, `product:train`, `read:*`.
13
14
  */
14
15
  import { type Target } from './call.ts';
15
16
  export declare const KINDS: readonly ['secret', 'publishable'];
16
17
  export type Kind = (typeof KINDS)[number];
17
18
  export interface Key {
19
+ id: string;
20
+ name: string;
18
21
  type: Kind;
19
22
  /** The recognizable head, never enough to use the key. */
20
23
  prefix: string;
21
24
  /** The whole key: a publishable key's only. */
22
25
  key: string;
26
+ /** active, expired, revoked or disabled. */
27
+ status: string;
23
28
  /** `kind:name` entries; none reaches whatever its holder does. */
24
29
  limit: string[];
25
- /** When it last changed, as IAM records it. */
30
+ /** When it was made, as IAM records it. */
26
31
  created: string;
27
32
  }
28
33
  export interface Minted {
34
+ id: string;
35
+ name: string;
29
36
  type: Kind;
30
37
  /** The key itself. A secret one is never readable again. */
31
38
  key: string;
@@ -36,6 +43,7 @@ export declare function keys(t: Target): Promise<Key[]>;
36
43
  export declare const LIMIT: RegExp;
37
44
  /** The limits a person typed, split on commas and spaces; throws on the first that is not one. */
38
45
  export declare function limits(raw: string): string[];
39
- /** Create a key of this type, ending the one before it. The answer is the only time a secret key is shown. */
40
- export declare function mint(t: Target, type: Kind, limit?: string[]): Promise<Minted>;
41
- export declare function revoke(t: Target, type: Kind): Promise<void>;
46
+ /** Create a new key beside the ones held. The answer is the only time a secret key is shown. */
47
+ export declare function mint(t: Target, type: Kind, name?: string, limit?: string[]): Promise<Minted>;
48
+ /** Revoke exactly one key, by id. Every other key keeps working. */
49
+ export declare function revoke(t: Target, id: string): Promise<void>;
package/lib/api/keys.js CHANGED
@@ -1,17 +1,18 @@
1
1
  /**
2
- * A person's own API keys: one secret key (sk-, it belongs on a server) and one
3
- * publishable key (pk-, safe in a page's source), at most one of each.
2
+ * A person's API keys, each its own credential: as many as they like, of either
3
+ * type — secret (sk-, it belongs on a server) or publishable (pk-, safe in a
4
+ * page's source).
4
5
  *
5
- * GET /v1/account/keys {keys: [{type, prefix, key?, limit?, createdAt}]}
6
- * POST /v1/account/keys {type, limit?} → {key, type, limit}, answered once
7
- * DELETE /v1/account/keys?type=… revoke that one
6
+ * GET /v1/account/keys {keys: [{id, name, type, prefix, key?, status, limit?, created, …}]}
7
+ * POST /v1/account/keys {name, type, limit?} → the key, its secret answered once
8
+ * DELETE /v1/account/keys/{id} revoke exactly that one; it stays listed as revoked
8
9
  *
9
- * Creating is rotating: a new key of a type ends the one before it. A secret key
10
- * is answered once and by its prefix after; a publishable key is public by
11
- * construction and always comes back whole. A limit only narrows what a key may
12
- * reach — `model:zen5`, `project:acme`, `product:commerce`, `model:*`.
10
+ * Creating a key never touches another. A secret key is answered once and by its
11
+ * prefix after; a publishable key is public by construction and always comes back
12
+ * whole. A limit only narrows what a key may reach — `model:zen5`,
13
+ * `project:acme`, `product:train`, `read:*`.
13
14
  */
14
- import { call, query } from './call.js';
15
+ import { call } from './call.js';
15
16
  export const KINDS = ['secret', 'publishable'];
16
17
  const str = (v) => (typeof v === 'string' ? v : '');
17
18
  const list = (v) => (Array.isArray(v) ? v.filter((s) => typeof s === 'string' && s !== '') : []);
@@ -23,9 +24,18 @@ export async function keys(t) {
23
24
  for (const raw of rows) {
24
25
  const o = (raw && typeof raw === 'object' ? raw : {});
25
26
  const type = kind(o.type);
26
- if (!type)
27
+ if (!type || !str(o.id))
27
28
  continue;
28
- out.push({ type, prefix: str(o.prefix), key: str(o.key), limit: list(o.limit), created: str(o.createdAt) });
29
+ out.push({
30
+ id: str(o.id),
31
+ name: str(o.name),
32
+ type,
33
+ prefix: str(o.prefix),
34
+ key: str(o.key),
35
+ status: str(o.status) || 'active',
36
+ limit: list(o.limit),
37
+ created: str(o.created),
38
+ });
29
39
  }
30
40
  return out;
31
41
  }
@@ -42,14 +52,20 @@ export function limits(raw) {
42
52
  throw new Error(`${bad} is not a limit — write it as kind:name, like model:zen5`);
43
53
  return out;
44
54
  }
45
- /** Create a key of this type, ending the one before it. The answer is the only time a secret key is shown. */
46
- export async function mint(t, type, limit = []) {
47
- const r = await call(t, 'POST', '/v1/account/keys', limit.length ? { type, limit } : { type });
48
- const key = str(r?.key) || str(r?.accessKey);
55
+ /** Create a new key beside the ones held. The answer is the only time a secret key is shown. */
56
+ export async function mint(t, type, name = '', limit = []) {
57
+ const body = { type };
58
+ if (name.trim())
59
+ body.name = name.trim();
60
+ if (limit.length)
61
+ body.limit = limit;
62
+ const r = await call(t, 'POST', '/v1/account/keys', body);
63
+ const key = str(r?.key);
49
64
  if (!key)
50
65
  throw new Error('The platform answered no key');
51
- return { type: kind(r?.type) ?? type, key, limit: list(r?.limit) };
66
+ return { id: str(r?.id), name: str(r?.name), type: kind(r?.type) ?? type, key, limit: list(r?.limit) };
52
67
  }
53
- export async function revoke(t, type) {
54
- await call(t, 'DELETE', `/v1/account/keys${query({ type })}`);
68
+ /** Revoke exactly one key, by id. Every other key keeps working. */
69
+ export async function revoke(t, id) {
70
+ await call(t, 'DELETE', `/v1/account/keys/${encodeURIComponent(id)}`);
55
71
  }
package/lib/api/work.d.ts CHANGED
@@ -31,7 +31,21 @@ export interface Work {
31
31
  assignee: string;
32
32
  /** The repository the item is bound to, or ''. */
33
33
  repo: string;
34
+ /** Where it was opened: `git` (the forge, or mirrored from GitHub), `team`, `agent`, … */
35
+ source: string;
36
+ labels: string[];
37
+ /** Its page upstream (a GitHub issue for a mirrored one), https only, or ''. */
38
+ url: string;
39
+ /** Unix milliseconds, or 0 when the platform did not say. */
40
+ created: number;
41
+ updated: number;
34
42
  }
43
+ /** The project an item is filed under: its repository, else its board. */
44
+ export declare const projectOf: (w: Work) => string;
45
+ /** Done and canceled are closed; every other column is open work. */
46
+ export declare const closed: (w: Work) => boolean;
47
+ /** Whether `w` belongs to the project `key` — by repository or by board, either case. */
48
+ export declare function inProject(w: Work, key: string): boolean;
35
49
  export declare function board(raw: unknown): Board | null;
36
50
  export declare function work(raw: unknown): Work | null;
37
51
  export declare function boards(t: Target): Promise<Board[]>;
package/lib/api/work.js CHANGED
@@ -12,7 +12,19 @@
12
12
  * reads the work and opens a run against it.
13
13
  */
14
14
  import { call, seg } from './call.js';
15
+ /** The project an item is filed under: its repository, else its board. */
16
+ export const projectOf = (w) => w.repo || w.project;
17
+ /** Done and canceled are closed; every other column is open work. */
18
+ export const closed = (w) => w.status === 'done' || w.status === 'canceled';
19
+ /** Whether `w` belongs to the project `key` — by repository or by board, either case. */
20
+ export function inProject(w, key) {
21
+ const k = key.toLowerCase();
22
+ return w.repo.toLowerCase() === k || w.project.toLowerCase() === k;
23
+ }
15
24
  const str = (v) => (typeof v === 'string' ? v : '');
25
+ /** The platform states seconds; a value already in milliseconds is kept. */
26
+ const when = (v) => (typeof v === 'number' && Number.isFinite(v) && v > 0 ? (v < 1e12 ? v * 1000 : v) : 0);
27
+ const https = (v) => (typeof v === 'string' && /^https:\/\//.test(v) ? v : '');
16
28
  const obj = (v) => v && typeof v === 'object' ? v : {};
17
29
  function rows(raw) {
18
30
  if (Array.isArray(raw))
@@ -49,6 +61,11 @@ export function work(raw) {
49
61
  priority: str(o.priority) || 'none',
50
62
  assignee: str(o.assignee),
51
63
  repo: str(o.repo),
64
+ source: str(o.source),
65
+ labels: Array.isArray(o.labels) ? o.labels.filter((l) => typeof l === 'string' && l !== '') : [],
66
+ url: https(o.url) || https(o.extRef),
67
+ created: when(o.createdAt),
68
+ updated: when(o.updatedAt),
52
69
  };
53
70
  }
54
71
  export async function boards(t) {
package/lib/builder.js CHANGED
@@ -5,14 +5,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
5
5
  * / New — the empty state, a composer over the codebase it works on
6
6
  * /sess_… one run, live
7
7
  * /-/automations work that runs itself; /-/automations/<id> one, to edit and run
8
- /-/codebases the forge's repositories
9
- * /-/sync bring granted repositories onto the forge
10
- * /-/projects the forge's boards
8
+ * /-/projects the org's repositories on the forge: a project is a repository
9
+ * /-/sync link repositories from GitHub onto the forge
11
10
  * /-/issues the forge's issues
12
11
  * /-/artifacts what this org has built
13
12
  * /-/templates the public starters
14
13
  * /-/customize skills, connectors, plugins and agents
15
- * /<slug> a deployed project's workspace, in this same window
14
+ * /<org>/<repo> a repository's workspace, in this same window
15
+ * /<slug> a deployed site's workspace
16
16
  *
17
17
  * The rail is the sessions rail every Hanzo surface shares: New, the builder's
18
18
  * places with the rest under More, the org's coding runs newest first with a
@@ -20,15 +20,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
20
20
  * whose menu opens Settings, Usage, the plans, help and logging out (foot.tsx).
21
21
  * Its collapse is an explicit toggle kept per browser.
22
22
  *
23
- * A project's workspace takes the whole window — it has its own chat column,
24
- * bar and panes — and its mark leads back here.
23
+ * A workspace takes the whole window — it has its own chat column, bar and
24
+ * panes — and its mark leads back here.
25
25
  *
26
26
  * A host with a rail of its own mounts `<Builder rail={false}>` and draws the
27
27
  * builder's places (`nav`) and runs in that rail (`useSessions`, or `DevSection`,
28
28
  * section.tsx): one left column, never two.
29
29
  */
30
30
  import { SizableText, XStack, YStack } from '@hanzo/gui';
31
- import { Blocks, BookOpen, CircleDot, Cpu, FolderGit2, Kanban, LayoutTemplate, Menu, Search, SlidersHorizontal, Workflow } from '@hanzogui/lucide-icons-2';
31
+ import { Blocks, BookOpen, CircleDot, Cpu, FolderGit2, LayoutTemplate, Menu, Search, SlidersHorizontal, Workflow } from '@hanzogui/lucide-icons-2';
32
32
  import { Button } from '@hanzo/ui';
33
33
  import { SessionRail } from '@hanzo/ui/chat';
34
34
  import { HanzoMark } from '@hanzo/ui/product';
@@ -98,13 +98,18 @@ export function nav(host, go) {
98
98
  const r = route(host.path);
99
99
  const screen = r.kind === 'screen' ? r.screen : '';
100
100
  const links = [
101
- { id: 'projects', label: 'Projects', icon: _jsx(Kanban, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'projects' })), active: screen === 'projects' },
101
+ {
102
+ id: 'projects',
103
+ label: 'Projects',
104
+ icon: _jsx(FolderGit2, { size: 16 }),
105
+ onPress: () => go(path({ kind: 'screen', screen: 'projects' })),
106
+ active: screen === 'projects' || screen === 'sync' || r.kind === 'repo',
107
+ },
102
108
  { id: 'artifacts', label: 'Artifacts', icon: _jsx(Blocks, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'artifacts' })), active: screen === 'artifacts' },
103
109
  { id: 'customize', label: 'Customize', icon: _jsx(SlidersHorizontal, { size: 16 }), onPress: () => go(path({ kind: 'customize', tab: 'skills' })), active: r.kind === 'customize' || screen === 'mcp' },
104
110
  { id: 'automations', label: 'Automations', icon: _jsx(Workflow, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'automations' })), active: screen === 'automations' || r.kind === 'automation' },
105
111
  ];
106
112
  const more = [
107
- { id: 'codebases', label: 'Codebase', icon: _jsx(FolderGit2, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'codebases' })), active: screen === 'codebases' },
108
113
  {
109
114
  id: 'issues',
110
115
  label: 'Issues',
@@ -172,7 +177,9 @@ function Screens({ rail }) {
172
177
  const host = useHost();
173
178
  const r = route(host.path);
174
179
  if (r.kind === 'project')
175
- return _jsx(Project, { slug: r.slug }, r.slug);
180
+ return _jsx(Project, { at: { slug: r.slug } }, r.slug);
181
+ if (r.kind === 'repo')
182
+ return _jsx(Project, { at: { org: r.org, name: r.name } }, `${r.org}/${r.name}`);
176
183
  if (!rail)
177
184
  return _jsx(Pane, {});
178
185
  return _jsx(Shell, {});