@hanzogui/recipes 8.0.0 → 8.1.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/dist/admin.js +11 -3
- package/dist/index.js +42 -34
- package/package.json +2 -2
- package/src/admin.ts +13 -4
- package/src/project.ts +2 -1
- package/src/read.ts +10 -4
- package/src/types.ts +4 -1
- package/src/write.ts +8 -2
package/dist/admin.js
CHANGED
|
@@ -11,7 +11,11 @@ function resolveBaseUrl(override) {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
// src/admin.ts
|
|
14
|
-
async function authAdmin({
|
|
14
|
+
async function authAdmin({
|
|
15
|
+
baseUrl,
|
|
16
|
+
email,
|
|
17
|
+
password
|
|
18
|
+
}) {
|
|
15
19
|
const url = `${resolveBaseUrl(baseUrl)}/v1/base/admins/auth-with-password`;
|
|
16
20
|
const res = await fetch(url, {
|
|
17
21
|
method: "POST",
|
|
@@ -27,7 +31,9 @@ async function authAdmin({ baseUrl, email, password }) {
|
|
|
27
31
|
async function ensureCollection(auth) {
|
|
28
32
|
const base = resolveBaseUrl(auth.baseUrl);
|
|
29
33
|
const headers = { Authorization: auth.token, "Content-Type": "application/json" };
|
|
30
|
-
const probe = await fetch(`${base}/v1/base/collections/${RECIPES_COLLECTION}`, {
|
|
34
|
+
const probe = await fetch(`${base}/v1/base/collections/${RECIPES_COLLECTION}`, {
|
|
35
|
+
headers
|
|
36
|
+
});
|
|
31
37
|
if (probe.ok)
|
|
32
38
|
return "existed";
|
|
33
39
|
const schema = {
|
|
@@ -42,7 +48,9 @@ async function ensureCollection(auth) {
|
|
|
42
48
|
{ name: "recipeDeps", type: "json", required: false, options: {} },
|
|
43
49
|
{ name: "files", type: "json", required: true, options: {} }
|
|
44
50
|
],
|
|
45
|
-
indexes: [
|
|
51
|
+
indexes: [
|
|
52
|
+
`CREATE UNIQUE INDEX idx_${RECIPES_COLLECTION}_slug ON ${RECIPES_COLLECTION} (slug)`
|
|
53
|
+
],
|
|
46
54
|
listRule: "",
|
|
47
55
|
viewRule: "",
|
|
48
56
|
createRule: null,
|
package/dist/index.js
CHANGED
|
@@ -45,6 +45,35 @@ function resolveInstallDir(root, config) {
|
|
|
45
45
|
return path.join(root, config.installDir ?? DEFAULT_INSTALL_DIR);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// src/write.ts
|
|
49
|
+
import { existsSync as existsSync2, promises as fs2 } from "node:fs";
|
|
50
|
+
import path2 from "node:path";
|
|
51
|
+
async function getInstallDir(opts) {
|
|
52
|
+
if (opts.installDir)
|
|
53
|
+
return opts.installDir;
|
|
54
|
+
const root = await findProjectRoot(opts.cwd);
|
|
55
|
+
return resolveInstallDir(root, await readConfig(root));
|
|
56
|
+
}
|
|
57
|
+
async function installRecipe(recipe, opts = {}) {
|
|
58
|
+
const installDir = await getInstallDir(opts);
|
|
59
|
+
const written = [];
|
|
60
|
+
for (const file of recipe.files) {
|
|
61
|
+
const dest = path2.join(installDir, file.path);
|
|
62
|
+
const dir = path2.dirname(dest);
|
|
63
|
+
if (!existsSync2(dir))
|
|
64
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
65
|
+
await fs2.writeFile(dest, file.source);
|
|
66
|
+
written.push(dest);
|
|
67
|
+
}
|
|
68
|
+
return written;
|
|
69
|
+
}
|
|
70
|
+
async function installPlan(plan, opts = {}) {
|
|
71
|
+
const written = [];
|
|
72
|
+
for (const r of [plan.recipe, ...plan.transitiveRecipes]) {
|
|
73
|
+
written.push(...await installRecipe(r, opts));
|
|
74
|
+
}
|
|
75
|
+
return written;
|
|
76
|
+
}
|
|
48
77
|
// src/read.ts
|
|
49
78
|
async function getJSON(url) {
|
|
50
79
|
const res = await fetch(url, { headers: { Accept: "application/json" } });
|
|
@@ -93,13 +122,17 @@ async function resolveRecipe(slug, opts = {}) {
|
|
|
93
122
|
})();
|
|
94
123
|
const recipe = seen.get(slug);
|
|
95
124
|
const transitiveRecipes = Array.from(seen.values()).filter((r) => r.slug !== slug);
|
|
96
|
-
const
|
|
97
|
-
const targetPaths = [recipe, ...transitiveRecipes].flatMap((r) => r.files.map((f) =>
|
|
125
|
+
const path3 = await import("node:path");
|
|
126
|
+
const targetPaths = [recipe, ...transitiveRecipes].flatMap((r) => r.files.map((f) => path3.join(installDir, f.path)));
|
|
98
127
|
return { recipe, transitiveRecipes, targetPaths };
|
|
99
128
|
}
|
|
100
129
|
|
|
101
130
|
// src/admin.ts
|
|
102
|
-
async function authAdmin({
|
|
131
|
+
async function authAdmin({
|
|
132
|
+
baseUrl,
|
|
133
|
+
email,
|
|
134
|
+
password
|
|
135
|
+
}) {
|
|
103
136
|
const url = `${resolveBaseUrl(baseUrl)}/v1/base/admins/auth-with-password`;
|
|
104
137
|
const res = await fetch(url, {
|
|
105
138
|
method: "POST",
|
|
@@ -115,7 +148,9 @@ async function authAdmin({ baseUrl, email, password }) {
|
|
|
115
148
|
async function ensureCollection(auth) {
|
|
116
149
|
const base = resolveBaseUrl(auth.baseUrl);
|
|
117
150
|
const headers = { Authorization: auth.token, "Content-Type": "application/json" };
|
|
118
|
-
const probe = await fetch(`${base}/v1/base/collections/${RECIPES_COLLECTION}`, {
|
|
151
|
+
const probe = await fetch(`${base}/v1/base/collections/${RECIPES_COLLECTION}`, {
|
|
152
|
+
headers
|
|
153
|
+
});
|
|
119
154
|
if (probe.ok)
|
|
120
155
|
return "existed";
|
|
121
156
|
const schema = {
|
|
@@ -130,7 +165,9 @@ async function ensureCollection(auth) {
|
|
|
130
165
|
{ name: "recipeDeps", type: "json", required: false, options: {} },
|
|
131
166
|
{ name: "files", type: "json", required: true, options: {} }
|
|
132
167
|
],
|
|
133
|
-
indexes: [
|
|
168
|
+
indexes: [
|
|
169
|
+
`CREATE UNIQUE INDEX idx_${RECIPES_COLLECTION}_slug ON ${RECIPES_COLLECTION} (slug)`
|
|
170
|
+
],
|
|
134
171
|
listRule: "",
|
|
135
172
|
viewRule: "",
|
|
136
173
|
createRule: null,
|
|
@@ -176,35 +213,6 @@ async function upsertRecipe(auth, recipe) {
|
|
|
176
213
|
throw new Error(`POST ${recipe.slug}: ${res.status} ${await res.text()}`);
|
|
177
214
|
return "created";
|
|
178
215
|
}
|
|
179
|
-
// src/write.ts
|
|
180
|
-
import { existsSync as existsSync2, promises as fs2 } from "node:fs";
|
|
181
|
-
import path2 from "node:path";
|
|
182
|
-
async function getInstallDir(opts) {
|
|
183
|
-
if (opts.installDir)
|
|
184
|
-
return opts.installDir;
|
|
185
|
-
const root = await findProjectRoot(opts.cwd);
|
|
186
|
-
return resolveInstallDir(root, await readConfig(root));
|
|
187
|
-
}
|
|
188
|
-
async function installRecipe(recipe, opts = {}) {
|
|
189
|
-
const installDir = await getInstallDir(opts);
|
|
190
|
-
const written = [];
|
|
191
|
-
for (const file of recipe.files) {
|
|
192
|
-
const dest = path2.join(installDir, file.path);
|
|
193
|
-
const dir = path2.dirname(dest);
|
|
194
|
-
if (!existsSync2(dir))
|
|
195
|
-
await fs2.mkdir(dir, { recursive: true });
|
|
196
|
-
await fs2.writeFile(dest, file.source);
|
|
197
|
-
written.push(dest);
|
|
198
|
-
}
|
|
199
|
-
return written;
|
|
200
|
-
}
|
|
201
|
-
async function installPlan(plan, opts = {}) {
|
|
202
|
-
const written = [];
|
|
203
|
-
for (const r of [plan.recipe, ...plan.transitiveRecipes]) {
|
|
204
|
-
written.push(...await installRecipe(r, opts));
|
|
205
|
-
}
|
|
206
|
-
return written;
|
|
207
|
-
}
|
|
208
216
|
export {
|
|
209
217
|
upsertRecipe,
|
|
210
218
|
resolveRecipe,
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzogui/recipes",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.1.1",
|
|
4
4
|
"description": "Hanzo GUI recipe library: read from Base, resolve transitive deps, install into a project.",
|
|
5
5
|
"license": "BSD-3-Clause",
|
|
6
6
|
"author": "Hanzo AI <dev@hanzo.ai>",
|
|
7
7
|
"homepage": "https://gui.hanzo.ai",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "https://github.com/hanzoai/gui.git",
|
|
10
|
+
"url": "git+https://github.com/hanzoai/gui.git",
|
|
11
11
|
"directory": "pkgs/recipes"
|
|
12
12
|
},
|
|
13
13
|
"type": "module",
|
package/src/admin.ts
CHANGED
|
@@ -13,7 +13,11 @@ export type AdminPasswordAuth = {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/** Exchanges admin email+password for a Base session token. */
|
|
16
|
-
export async function authAdmin({
|
|
16
|
+
export async function authAdmin({
|
|
17
|
+
baseUrl,
|
|
18
|
+
email,
|
|
19
|
+
password,
|
|
20
|
+
}: AdminPasswordAuth): Promise<string> {
|
|
17
21
|
const url = `${resolveBaseUrl(baseUrl)}/v1/base/admins/auth-with-password`
|
|
18
22
|
const res = await fetch(url, {
|
|
19
23
|
method: 'POST',
|
|
@@ -31,7 +35,9 @@ export async function authAdmin({ baseUrl, email, password }: AdminPasswordAuth)
|
|
|
31
35
|
export async function ensureCollection(auth: AdminAuth): Promise<'created' | 'existed'> {
|
|
32
36
|
const base = resolveBaseUrl(auth.baseUrl)
|
|
33
37
|
const headers = { Authorization: auth.token, 'Content-Type': 'application/json' }
|
|
34
|
-
const probe = await fetch(`${base}/v1/base/collections/${RECIPES_COLLECTION}`, {
|
|
38
|
+
const probe = await fetch(`${base}/v1/base/collections/${RECIPES_COLLECTION}`, {
|
|
39
|
+
headers,
|
|
40
|
+
})
|
|
35
41
|
if (probe.ok) return 'existed'
|
|
36
42
|
|
|
37
43
|
const schema = {
|
|
@@ -46,7 +52,9 @@ export async function ensureCollection(auth: AdminAuth): Promise<'created' | 'ex
|
|
|
46
52
|
{ name: 'recipeDeps', type: 'json', required: false, options: {} },
|
|
47
53
|
{ name: 'files', type: 'json', required: true, options: {} },
|
|
48
54
|
],
|
|
49
|
-
indexes: [
|
|
55
|
+
indexes: [
|
|
56
|
+
`CREATE UNIQUE INDEX idx_${RECIPES_COLLECTION}_slug ON ${RECIPES_COLLECTION} (slug)`,
|
|
57
|
+
],
|
|
50
58
|
listRule: '',
|
|
51
59
|
viewRule: '',
|
|
52
60
|
createRule: null,
|
|
@@ -90,7 +98,8 @@ export async function upsertRecipe(
|
|
|
90
98
|
`${base}/v1/base/collections/${RECIPES_COLLECTION}/records/${existing}`,
|
|
91
99
|
{ method: 'PATCH', headers, body }
|
|
92
100
|
)
|
|
93
|
-
if (!res.ok)
|
|
101
|
+
if (!res.ok)
|
|
102
|
+
throw new Error(`PATCH ${recipe.slug}: ${res.status} ${await res.text()}`)
|
|
94
103
|
return 'updated'
|
|
95
104
|
}
|
|
96
105
|
const res = await fetch(`${base}/v1/base/collections/${RECIPES_COLLECTION}/records`, {
|
package/src/project.ts
CHANGED
|
@@ -11,7 +11,8 @@ export async function findProjectRoot(start: string = process.cwd()): Promise<st
|
|
|
11
11
|
if (existsSync(pkgPath)) {
|
|
12
12
|
try {
|
|
13
13
|
const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8'))
|
|
14
|
-
if (pkg.workspaces || existsSync(path.join(dir, 'pnpm-workspace.yaml')))
|
|
14
|
+
if (pkg.workspaces || existsSync(path.join(dir, 'pnpm-workspace.yaml')))
|
|
15
|
+
return dir
|
|
15
16
|
} catch {}
|
|
16
17
|
}
|
|
17
18
|
if (existsSync(path.join(dir, '.git'))) return dir
|
package/src/read.ts
CHANGED
|
@@ -17,9 +17,12 @@ export type ReaderOptions = {
|
|
|
17
17
|
async function getJSON<T>(url: string): Promise<T> {
|
|
18
18
|
const res = await fetch(url, { headers: { Accept: 'application/json' } })
|
|
19
19
|
if (!res.ok) {
|
|
20
|
-
throw Object.assign(
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
throw Object.assign(
|
|
21
|
+
new Error(`Base request failed: ${res.status} ${res.statusText}`),
|
|
22
|
+
{
|
|
23
|
+
status: res.status,
|
|
24
|
+
}
|
|
25
|
+
)
|
|
23
26
|
}
|
|
24
27
|
return (await res.json()) as T
|
|
25
28
|
}
|
|
@@ -53,7 +56,10 @@ export type ResolveOptions = ReaderOptions & {
|
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
/** Walks recipeDeps transitively, computes target paths under the resolved install dir. */
|
|
56
|
-
export async function resolveRecipe(
|
|
59
|
+
export async function resolveRecipe(
|
|
60
|
+
slug: string,
|
|
61
|
+
opts: ResolveOptions = {}
|
|
62
|
+
): Promise<InstallPlan> {
|
|
57
63
|
const seen = new Map<string, Recipe>()
|
|
58
64
|
const queue = [slug]
|
|
59
65
|
while (queue.length) {
|
package/src/types.ts
CHANGED
|
@@ -21,7 +21,10 @@ export type RecipeSummary = Pick<
|
|
|
21
21
|
'id' | 'slug' | 'name' | 'description' | 'category'
|
|
22
22
|
>
|
|
23
23
|
|
|
24
|
-
export type RecipeInput = Pick<
|
|
24
|
+
export type RecipeInput = Pick<
|
|
25
|
+
Recipe,
|
|
26
|
+
'slug' | 'name' | 'description' | 'category' | 'npmDeps' | 'recipeDeps' | 'files'
|
|
27
|
+
>
|
|
25
28
|
|
|
26
29
|
export type InstallPlan = {
|
|
27
30
|
recipe: Recipe
|
package/src/write.ts
CHANGED
|
@@ -17,7 +17,10 @@ async function getInstallDir(opts: WriteOptions): Promise<string> {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/** Writes one recipe's files. Returns absolute paths written. */
|
|
20
|
-
export async function installRecipe(
|
|
20
|
+
export async function installRecipe(
|
|
21
|
+
recipe: Recipe,
|
|
22
|
+
opts: WriteOptions = {}
|
|
23
|
+
): Promise<string[]> {
|
|
21
24
|
const installDir = await getInstallDir(opts)
|
|
22
25
|
const written: string[] = []
|
|
23
26
|
for (const file of recipe.files) {
|
|
@@ -31,7 +34,10 @@ export async function installRecipe(recipe: Recipe, opts: WriteOptions = {}): Pr
|
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
/** Writes a recipe and all its transitive dependents from a resolved plan. */
|
|
34
|
-
export async function installPlan(
|
|
37
|
+
export async function installPlan(
|
|
38
|
+
plan: InstallPlan,
|
|
39
|
+
opts: WriteOptions = {}
|
|
40
|
+
): Promise<string[]> {
|
|
35
41
|
const written: string[] = []
|
|
36
42
|
for (const r of [plan.recipe, ...plan.transitiveRecipes]) {
|
|
37
43
|
written.push(...(await installRecipe(r, opts)))
|