@hanzogui/get 7.3.0

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/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@hanzogui/get",
3
+ "version": "7.3.0",
4
+ "description": "CLI installer for Hanzo GUI recipes.",
5
+ "license": "BSD-3-Clause",
6
+ "author": "Hanzo AI <dev@hanzo.ai>",
7
+ "homepage": "https://gui.hanzo.ai",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/hanzoai/gui.git",
11
+ "directory": "pkgs/get"
12
+ },
13
+ "bin": {
14
+ "hanzo-gui-get": "./dist/cli.js"
15
+ },
16
+ "type": "module",
17
+ "main": "./dist/cli.js",
18
+ "source": "src/cli.ts",
19
+ "files": [
20
+ "src/**/*.ts",
21
+ "!src/**/*.test.ts",
22
+ "dist"
23
+ ],
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "scripts": {
28
+ "build": "bun run build.ts",
29
+ "clean": "rm -rf dist",
30
+ "test": "bun test"
31
+ },
32
+ "dependencies": {
33
+ "@hanzogui/recipes": "7.3.0",
34
+ "fuse.js": "7.1.0"
35
+ },
36
+ "engines": {
37
+ "node": ">=20"
38
+ }
39
+ }
package/readme.md ADDED
@@ -0,0 +1,46 @@
1
+ # @hanzogui/get
2
+
3
+ Install Hanzo GUI recipes — composable screens and widgets built on `@hanzogui/*` primitives — into your project with one command.
4
+
5
+ ## Usage
6
+
7
+ ```sh
8
+ npx @hanzogui/get # interactive picker
9
+ npx @hanzogui/get add sign-in-form # install a single recipe
10
+ npx @hanzogui/get list # show all available recipes
11
+ npx @hanzogui/get init # write gui.config.json
12
+ ```
13
+
14
+ After global install (`npm i -g @hanzogui/get`), the command is `hanzo-gui-get`.
15
+
16
+ Recipes are vendored into `src/gui/` by default. Override the destination by adding a `gui.config.json` at the project root:
17
+
18
+ ```json
19
+ { "installDir": "src/components" }
20
+ ```
21
+
22
+ ## How it works
23
+
24
+ `hanzo-gui-get` reads recipes from Hanzo Base at `${HANZO_BASE_URL}/v1/base/collections/gui_recipes/records`. Each recipe is a small, self-contained `.tsx` file (or set of files) that composes `@hanzogui/*` primitives. Recipes can depend on other recipes; transitive deps are pulled in automatically.
25
+
26
+ Override the registry host:
27
+
28
+ ```sh
29
+ HANZO_BASE_URL=https://base.staging.hanzo.ai npx @hanzogui/get
30
+ ```
31
+
32
+ You still need `@hanzogui/*` primitives installed normally.
33
+
34
+ ## Publishing a recipe
35
+
36
+ Recipes live in `hanzoai/gui` at `recipes/<slug>/{recipe.json, files/**}`. To seed Base:
37
+
38
+ ```sh
39
+ HANZO_BASE_ADMIN_TOKEN=… bun run scripts/seed-recipes.ts
40
+ ```
41
+
42
+ The seed script creates the `gui_recipes` collection on first run if it doesn't exist, then upserts each recipe by slug.
43
+
44
+ ## What this is not
45
+
46
+ `@hanzogui/get` is a plain Node CLI — no terminal-UI framework, no React, no `ink`. Just stdout + readline. If you want a Hanzo TUI for building richer CLI apps, use `@hanzo/tui` (separate package).
package/src/cli.ts ADDED
@@ -0,0 +1,67 @@
1
+ import { CLI_NAME } from './constants.js'
2
+ import { red } from './lib/ansi.js'
3
+
4
+ const HELP = `${CLI_NAME} — install Hanzo GUI recipes into your project.
5
+
6
+ Usage:
7
+ ${CLI_NAME} Interactive picker.
8
+ ${CLI_NAME} add <slug> Install a single recipe.
9
+ ${CLI_NAME} list List all available recipes.
10
+ ${CLI_NAME} init Write gui.config.json.
11
+ ${CLI_NAME} --help Show this message.
12
+ ${CLI_NAME} --version Print version.
13
+
14
+ Recipes are fetched from Hanzo Base at $HANZO_BASE_URL (default https://base.hanzo.ai).
15
+ Recipe files vendor into src/gui/ by default; override via gui.config.json.
16
+ `
17
+
18
+ async function main(): Promise<void> {
19
+ const [, , cmd, ...rest] = process.argv
20
+
21
+ if (cmd === '--help' || cmd === '-h' || cmd === 'help') {
22
+ process.stdout.write(HELP)
23
+ return
24
+ }
25
+
26
+ if (cmd === '--version' || cmd === '-v') {
27
+ const pkg = await import('../package.json', { with: { type: 'json' } } as ImportCallOptions)
28
+ process.stdout.write(((pkg as { default?: { version?: string } }).default?.version ?? 'unknown') + '\n')
29
+ return
30
+ }
31
+
32
+ if (!cmd) {
33
+ const { pick } = await import('./commands/pick.js')
34
+ await pick()
35
+ return
36
+ }
37
+
38
+ if (cmd === 'add') {
39
+ if (!rest[0]) {
40
+ process.stderr.write(`Usage: ${CLI_NAME} add <recipe-slug>\n`)
41
+ process.exit(1)
42
+ }
43
+ const { add } = await import('./commands/add.js')
44
+ await add(rest[0])
45
+ return
46
+ }
47
+
48
+ if (cmd === 'list') {
49
+ const { list } = await import('./commands/list.js')
50
+ await list()
51
+ return
52
+ }
53
+
54
+ if (cmd === 'init') {
55
+ const { init } = await import('./commands/init.js')
56
+ await init()
57
+ return
58
+ }
59
+
60
+ process.stderr.write(`Unknown command: ${cmd}\n\n${HELP}`)
61
+ process.exit(1)
62
+ }
63
+
64
+ main().catch((e) => {
65
+ process.stderr.write(red(e instanceof Error ? e.message : String(e)) + '\n')
66
+ process.exit(1)
67
+ })
@@ -0,0 +1,23 @@
1
+ import { resolveRecipe, installPlan } from '@hanzogui/recipes'
2
+ import { green, gray, bold } from '../lib/ansi.js'
3
+
4
+ export async function add(slug: string): Promise<void> {
5
+ const plan = await resolveRecipe(slug)
6
+ const paths = await installPlan(plan)
7
+
8
+ process.stdout.write(green(`Installed "${plan.recipe.name}"`) + '\n')
9
+ if (plan.transitiveRecipes.length > 0) {
10
+ process.stdout.write(
11
+ gray(
12
+ `+ ${plan.transitiveRecipes.length} dependent recipe${
13
+ plan.transitiveRecipes.length === 1 ? '' : 's'
14
+ }: ${plan.transitiveRecipes.map((r) => r.slug).join(', ')}\n`
15
+ )
16
+ )
17
+ }
18
+ for (const p of paths) process.stdout.write(` ${p}\n`)
19
+ if (plan.recipe.npmDeps.length > 0) {
20
+ process.stdout.write('\n' + gray('Install these npm deps:') + '\n')
21
+ process.stdout.write(` ${bold('npm i ' + plan.recipe.npmDeps.join(' '))}\n`)
22
+ }
23
+ }
@@ -0,0 +1,18 @@
1
+ import { existsSync, promises as fs } from 'node:fs'
2
+ import path from 'node:path'
3
+ import { findProjectRoot, DEFAULT_INSTALL_DIR } from '@hanzogui/recipes'
4
+ import { green, yellow } from '../lib/ansi.js'
5
+
6
+ export async function init(): Promise<void> {
7
+ const root = await findProjectRoot()
8
+ const dest = path.join(root, 'gui.config.json')
9
+ if (existsSync(dest)) {
10
+ process.stdout.write(yellow(`gui.config.json already exists at ${dest}\n`))
11
+ return
12
+ }
13
+ await fs.writeFile(
14
+ dest,
15
+ JSON.stringify({ installDir: DEFAULT_INSTALL_DIR }, null, 2) + '\n'
16
+ )
17
+ process.stdout.write(green(`Wrote ${dest}\n`))
18
+ }
@@ -0,0 +1,19 @@
1
+ import { listRecipes } from '@hanzogui/recipes'
2
+ import { bold, cyan, gray, yellow } from '../lib/ansi.js'
3
+
4
+ export async function list(): Promise<void> {
5
+ const recipes = await listRecipes()
6
+ if (recipes.length === 0) {
7
+ process.stdout.write(yellow('No recipes published yet.\n'))
8
+ return
9
+ }
10
+ const byCategory: Record<string, typeof recipes> = {}
11
+ for (const r of recipes) (byCategory[r.category || 'misc'] ||= []).push(r)
12
+ for (const [cat, items] of Object.entries(byCategory)) {
13
+ process.stdout.write(`\n${bold(cat)}\n`)
14
+ for (const r of items) {
15
+ process.stdout.write(` ${cyan(r.slug.padEnd(24))} ${gray(r.description)}\n`)
16
+ }
17
+ }
18
+ process.stdout.write('\n')
19
+ }
@@ -0,0 +1,61 @@
1
+ import Fuse from 'fuse.js'
2
+ import * as readline from 'node:readline/promises'
3
+ import { listRecipes } from '@hanzogui/recipes'
4
+ import { bold, cyan, dim, gray } from '../lib/ansi.js'
5
+ import { add } from './add.js'
6
+
7
+ export async function pick(): Promise<void> {
8
+ const recipes = await listRecipes()
9
+ if (recipes.length === 0) {
10
+ process.stdout.write('No recipes published yet.\n')
11
+ return
12
+ }
13
+
14
+ const fuse = new Fuse(recipes, {
15
+ keys: ['name', 'slug', 'category', 'description'],
16
+ threshold: 0.4,
17
+ })
18
+
19
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
20
+
21
+ try {
22
+ const query = (await rl.question(bold('Search recipes') + ' (empty = show all): ')).trim()
23
+ const hits = query
24
+ ? fuse.search(query).map((h) => h.item)
25
+ : recipes
26
+ if (hits.length === 0) {
27
+ process.stdout.write(dim('No matches.\n'))
28
+ return
29
+ }
30
+
31
+ process.stdout.write('\n')
32
+ hits.slice(0, 20).forEach((r, i) => {
33
+ process.stdout.write(
34
+ ` ${cyan(String(i + 1).padStart(2))} ${bold(r.name)} ${gray(r.slug)}\n`
35
+ )
36
+ process.stdout.write(` ${gray(r.description)}\n`)
37
+ })
38
+ process.stdout.write('\n')
39
+
40
+ const pickRaw = (await rl.question('Pick a number (or paste a slug): ')).trim()
41
+ if (!pickRaw) return
42
+
43
+ let slug: string | undefined
44
+ const asNum = Number.parseInt(pickRaw, 10)
45
+ if (!Number.isNaN(asNum) && asNum >= 1 && asNum <= hits.length) {
46
+ slug = hits[asNum - 1]!.slug
47
+ } else if (recipes.find((r) => r.slug === pickRaw)) {
48
+ slug = pickRaw
49
+ }
50
+
51
+ if (!slug) {
52
+ process.stdout.write(dim(`No recipe matches "${pickRaw}".\n`))
53
+ return
54
+ }
55
+
56
+ process.stdout.write('\n')
57
+ await add(slug)
58
+ } finally {
59
+ rl.close()
60
+ }
61
+ }
@@ -0,0 +1 @@
1
+ export const CLI_NAME = 'hanzo-gui-get'
@@ -0,0 +1,12 @@
1
+ const ESC = '\x1b['
2
+ const isTTY = process.stdout.isTTY === true
3
+ const wrap = (open: number, close: number) => (s: string) =>
4
+ isTTY ? `${ESC}${open}m${s}${ESC}${close}m` : s
5
+
6
+ export const bold = wrap(1, 22)
7
+ export const dim = wrap(2, 22)
8
+ export const red = wrap(31, 39)
9
+ export const green = wrap(32, 39)
10
+ export const yellow = wrap(33, 39)
11
+ export const cyan = wrap(36, 39)
12
+ export const gray = wrap(90, 39)