@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/LICENSE +42 -0
- package/dist/cli.js +1703 -0
- package/package.json +39 -0
- package/readme.md +46 -0
- package/src/cli.ts +67 -0
- package/src/commands/add.ts +23 -0
- package/src/commands/init.ts +18 -0
- package/src/commands/list.ts +19 -0
- package/src/commands/pick.ts +61 -0
- package/src/constants.ts +1 -0
- package/src/lib/ansi.ts +12 -0
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
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const CLI_NAME = 'hanzo-gui-get'
|
package/src/lib/ansi.ts
ADDED
|
@@ -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)
|