@hanzo/docs-cli 1.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Fuma
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,181 @@
1
+ import { z } from 'zod';
2
+ import { Project, SourceFile } from 'ts-morph';
3
+ import { Registry as Registry$1 } from 'shadcn/schema';
4
+
5
+ type NamespaceType = (typeof namespaces)[number];
6
+ type CompiledComponent = z.input<typeof componentSchema>;
7
+ declare const namespaces: readonly ["components", "lib", "css", "route", "ui", "block"];
8
+ declare const indexSchema: z.ZodObject<{
9
+ name: z.ZodString;
10
+ title: z.ZodOptional<z.ZodString>;
11
+ description: z.ZodOptional<z.ZodString>;
12
+ }, z.core.$strip>;
13
+ declare const componentSchema: z.ZodObject<{
14
+ name: z.ZodString;
15
+ title: z.ZodOptional<z.ZodString>;
16
+ description: z.ZodOptional<z.ZodString>;
17
+ files: z.ZodArray<z.ZodObject<{
18
+ type: z.ZodLiteral<"components" | "lib" | "css" | "route" | "ui" | "block">;
19
+ path: z.ZodString;
20
+ target: z.ZodOptional<z.ZodString>;
21
+ content: z.ZodString;
22
+ }, z.core.$strip>>;
23
+ dependencies: z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodString, z.ZodNull]>>;
24
+ devDependencies: z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodString, z.ZodNull]>>;
25
+ subComponents: z.ZodDefault<z.ZodArray<z.ZodString>>;
26
+ }, z.core.$strip>;
27
+
28
+ type OnResolve = (reference: SourceReference) => Reference;
29
+ interface CompiledRegistry {
30
+ name: string;
31
+ index: z.input<typeof indexSchema>[];
32
+ components: CompiledComponent[];
33
+ switchables?: Record<string, Switchable>;
34
+ }
35
+ interface ComponentFile {
36
+ type: NamespaceType;
37
+ path: string;
38
+ target?: string;
39
+ }
40
+ interface Switchable {
41
+ specifier: string;
42
+ members: Record<string, string>;
43
+ }
44
+ interface Component {
45
+ name: string;
46
+ title?: string;
47
+ description?: string;
48
+ files: ComponentFile[];
49
+ /**
50
+ * Don't list the component in registry index file
51
+ */
52
+ unlisted?: boolean;
53
+ /**
54
+ * Map imported file paths, inherit from registry if not defined.
55
+ */
56
+ onResolve?: OnResolve;
57
+ }
58
+ interface PackageJson {
59
+ dependencies?: Record<string, string>;
60
+ devDependencies?: Record<string, string>;
61
+ }
62
+ interface Registry {
63
+ name: string;
64
+ packageJson: string | PackageJson;
65
+ tsconfigPath: string;
66
+ components: Component[];
67
+ switchables?: Record<string, Switchable>;
68
+ /**
69
+ * The directory of registry, used to resolve relative paths
70
+ */
71
+ dir: string;
72
+ /**
73
+ * Map import paths of components
74
+ */
75
+ onResolve?: OnResolve;
76
+ /**
77
+ * When a referenced file is not found in component files, this function is called.
78
+ * @returns file, or `false` to mark as external.
79
+ */
80
+ onUnknownFile?: (absolutePath: string) => ComponentFile | false | undefined;
81
+ dependencies?: Record<string, string | null>;
82
+ devDependencies?: Record<string, string | null>;
83
+ }
84
+ declare class RegistryCompiler {
85
+ readonly raw: Registry;
86
+ readonly project: Project;
87
+ resolver: RegistryResolver;
88
+ constructor(registry: Registry);
89
+ private readPackageJson;
90
+ createSourceFile(file: string): Promise<SourceFile>;
91
+ compile(): Promise<CompiledRegistry>;
92
+ }
93
+ declare class RegistryResolver {
94
+ private readonly compiler;
95
+ private readonly deps;
96
+ private readonly devDeps;
97
+ private readonly fileToComponent;
98
+ constructor(compiler: RegistryCompiler, packageJson?: PackageJson);
99
+ getDepFromSpecifier(specifier: string): string;
100
+ getDepInfo(name: string): {
101
+ type: 'runtime' | 'dev';
102
+ name: string;
103
+ version: string | null;
104
+ } | undefined;
105
+ getComponentByName(name: string): Component | undefined;
106
+ getSubComponent(file: string): {
107
+ component: Component;
108
+ file: ComponentFile;
109
+ } | undefined;
110
+ }
111
+ type SourceReference = {
112
+ type: 'file';
113
+ /**
114
+ * Absolute path
115
+ */
116
+ file: string;
117
+ } | {
118
+ type: 'dependency';
119
+ dep: string;
120
+ specifier: string;
121
+ } | {
122
+ type: 'sub-component';
123
+ resolved: {
124
+ type: 'local';
125
+ component: Component;
126
+ file: ComponentFile;
127
+ } | {
128
+ type: 'remote';
129
+ component: Component;
130
+ file: ComponentFile;
131
+ registryName: string;
132
+ };
133
+ };
134
+ type Reference = SourceReference | {
135
+ type: 'custom';
136
+ specifier: string;
137
+ };
138
+ declare class ComponentCompiler {
139
+ private readonly compiler;
140
+ private readonly component;
141
+ private readonly processedFiles;
142
+ private readonly registry;
143
+ private readonly subComponents;
144
+ private readonly devDependencies;
145
+ private readonly dependencies;
146
+ constructor(compiler: RegistryCompiler, component: Component);
147
+ private toImportPath;
148
+ build(): Promise<CompiledComponent>;
149
+ private buildFileAndDeps;
150
+ private resolveImport;
151
+ private buildFile;
152
+ }
153
+
154
+ declare function toShadcnRegistry(out: CompiledRegistry, baseUrl: string): {
155
+ registry: Registry$1;
156
+ index: Registry$1;
157
+ };
158
+
159
+ declare function combineRegistry(...items: CompiledRegistry[]): CompiledRegistry;
160
+ declare function writeShadcnRegistry(out: CompiledRegistry, options: {
161
+ dir: string;
162
+ /**
163
+ * Remove previous outputs
164
+ *
165
+ * @defaultValue false
166
+ */
167
+ cleanDir?: boolean;
168
+ baseUrl: string;
169
+ }): Promise<void>;
170
+ declare function writeFumadocsRegistry(out: CompiledRegistry, options: {
171
+ dir: string;
172
+ /**
173
+ * Remove previous outputs
174
+ *
175
+ * @defaultValue false
176
+ */
177
+ cleanDir?: boolean;
178
+ log?: boolean;
179
+ }): Promise<void>;
180
+
181
+ export { type CompiledRegistry, type Component, ComponentCompiler, type ComponentFile, type OnResolve, type PackageJson, type Reference, type Registry, RegistryCompiler, type SourceReference, type Switchable, combineRegistry, toShadcnRegistry, writeFumadocsRegistry, writeShadcnRegistry };
@@ -0,0 +1,423 @@
1
+ // src/build/index.ts
2
+ import * as fs2 from "fs/promises";
3
+ import * as path2 from "path";
4
+ import picocolors from "picocolors";
5
+
6
+ // src/build/shadcn.ts
7
+ function mapDeps(deps) {
8
+ return Object.entries(deps).map(([k, v]) => {
9
+ if (v) return `${k}@${v}`;
10
+ return k;
11
+ });
12
+ }
13
+ function escapeName(name) {
14
+ return name;
15
+ }
16
+ function toShadcnRegistry(out, baseUrl) {
17
+ const registry = {
18
+ homepage: baseUrl,
19
+ name: out.name,
20
+ items: out.components.map((comp) => componentToShadcn(comp, baseUrl))
21
+ };
22
+ return {
23
+ registry,
24
+ index: {
25
+ ...registry,
26
+ items: out.components.map(
27
+ (comp) => componentToShadcn(comp, baseUrl, true)
28
+ )
29
+ }
30
+ };
31
+ }
32
+ function componentToShadcn(comp, baseUrl, noFile = false) {
33
+ const FileType = {
34
+ components: "registry:component",
35
+ lib: "registry:lib",
36
+ css: "registry:style",
37
+ route: "registry:page",
38
+ ui: "registry:ui",
39
+ block: "registry:block"
40
+ };
41
+ function onFile(file) {
42
+ return {
43
+ type: FileType[file.type],
44
+ content: file.content,
45
+ path: file.path,
46
+ target: file.target
47
+ };
48
+ }
49
+ return {
50
+ extends: "none",
51
+ type: "registry:block",
52
+ name: escapeName(comp.name),
53
+ title: comp.title ?? comp.name,
54
+ description: comp.description,
55
+ dependencies: mapDeps(comp.dependencies),
56
+ devDependencies: mapDeps(comp.devDependencies),
57
+ registryDependencies: comp.subComponents?.map((comp2) => {
58
+ if (comp2.startsWith("https://") || comp2.startsWith("http://"))
59
+ return comp2;
60
+ return new URL(`/r/${escapeName(comp2)}.json`, baseUrl).toString();
61
+ }),
62
+ files: noFile ? [] : comp.files.map(onFile)
63
+ };
64
+ }
65
+
66
+ // src/build/validate.ts
67
+ function validateOutput(registry) {
68
+ const validatedComps = /* @__PURE__ */ new Set();
69
+ const fileToComps = /* @__PURE__ */ new Map();
70
+ function validateComponent(comp) {
71
+ if (validatedComps.has(comp.name)) return;
72
+ validatedComps.add(comp.name);
73
+ for (const file of comp.files) {
74
+ const parents = fileToComps.get(file.path);
75
+ if (parents) {
76
+ parents.add(comp.name);
77
+ } else {
78
+ fileToComps.set(file.path, /* @__PURE__ */ new Set([comp.name]));
79
+ }
80
+ }
81
+ for (const name of comp.subComponents ?? []) {
82
+ const subComp = registry.components.find((item) => item.name === name);
83
+ if (!subComp) {
84
+ console.warn(`skipped component ${name}: not found`);
85
+ continue;
86
+ }
87
+ validateComponent(subComp);
88
+ }
89
+ for (const file of comp.files) {
90
+ const parents = fileToComps.get(file.path);
91
+ if (!parents || parents.size <= 1) continue;
92
+ throw new Error(
93
+ `Duplicated file in same component ${Array.from(parents).join(", ")}: ${file.path}`
94
+ );
95
+ }
96
+ }
97
+ for (const comp of registry.components) {
98
+ fileToComps.clear();
99
+ validateComponent(comp);
100
+ }
101
+ }
102
+
103
+ // src/build/compiler.ts
104
+ import * as fs from "fs/promises";
105
+ import * as path from "path";
106
+ import { Project, ts } from "ts-morph";
107
+ var RegistryCompiler = class {
108
+ constructor(registry) {
109
+ this.raw = registry;
110
+ this.project = new Project({
111
+ tsConfigFilePath: path.join(registry.dir, registry.tsconfigPath)
112
+ });
113
+ }
114
+ async readPackageJson() {
115
+ if (typeof this.raw.packageJson !== "string") return this.raw.packageJson;
116
+ return fs.readFile(path.join(this.raw.dir, this.raw.packageJson)).then((res) => JSON.parse(res.toString())).catch(() => void 0);
117
+ }
118
+ async createSourceFile(file) {
119
+ const content = await fs.readFile(file);
120
+ return this.project.createSourceFile(file, content.toString(), {
121
+ overwrite: true
122
+ });
123
+ }
124
+ async compile() {
125
+ const registry = this.raw;
126
+ this.resolver = new RegistryResolver(this, await this.readPackageJson());
127
+ const output = {
128
+ name: registry.name,
129
+ index: [],
130
+ components: [],
131
+ switchables: registry.switchables
132
+ };
133
+ const builtComps = await Promise.all(
134
+ registry.components.map(async (component) => {
135
+ const compiler = new ComponentCompiler(this, component);
136
+ return [component, await compiler.build()];
137
+ })
138
+ );
139
+ for (const [input, comp] of builtComps) {
140
+ if (!input.unlisted) {
141
+ output.index.push({
142
+ name: input.name,
143
+ title: input.title,
144
+ description: input.description
145
+ });
146
+ }
147
+ output.components.push(comp);
148
+ }
149
+ return output;
150
+ }
151
+ };
152
+ var RegistryResolver = class {
153
+ constructor(compiler, packageJson = {}) {
154
+ this.compiler = compiler;
155
+ this.fileToComponent = /* @__PURE__ */ new Map();
156
+ const registry = compiler.raw;
157
+ for (const comp of registry.components) {
158
+ for (const file of comp.files) {
159
+ if (this.fileToComponent.has(file.path))
160
+ console.warn(
161
+ `the same file ${file.path} exists in multiple component, you should make the shared file a separate component.`
162
+ );
163
+ this.fileToComponent.set(file.path, [comp, file]);
164
+ }
165
+ }
166
+ this.deps = {
167
+ ...packageJson?.dependencies,
168
+ ...registry.dependencies
169
+ };
170
+ this.devDeps = {
171
+ ...packageJson?.devDependencies,
172
+ ...registry.devDependencies
173
+ };
174
+ }
175
+ getDepFromSpecifier(specifier) {
176
+ return specifier.startsWith("@") ? specifier.split("/").slice(0, 2).join("/") : specifier.split("/")[0];
177
+ }
178
+ getDepInfo(name) {
179
+ if (name in this.deps)
180
+ return {
181
+ name,
182
+ type: "runtime",
183
+ version: this.deps[name]
184
+ };
185
+ if (name in this.devDeps)
186
+ return {
187
+ name,
188
+ type: "dev",
189
+ version: this.devDeps[name]
190
+ };
191
+ console.warn(`dep info for ${name} cannot be found`);
192
+ }
193
+ getComponentByName(name) {
194
+ return this.compiler.raw.components.find((comp) => comp.name === name);
195
+ }
196
+ getSubComponent(file) {
197
+ const relativeFile = path.relative(this.compiler.raw.dir, file);
198
+ const comp = this.fileToComponent.get(relativeFile);
199
+ if (!comp) return;
200
+ return {
201
+ component: comp[0],
202
+ file: comp[1]
203
+ };
204
+ }
205
+ };
206
+ var ComponentCompiler = class {
207
+ constructor(compiler, component) {
208
+ this.compiler = compiler;
209
+ this.component = component;
210
+ this.processedFiles = /* @__PURE__ */ new Set();
211
+ this.subComponents = /* @__PURE__ */ new Set();
212
+ this.devDependencies = /* @__PURE__ */ new Map();
213
+ this.dependencies = /* @__PURE__ */ new Map();
214
+ this.registry = compiler.raw;
215
+ }
216
+ // see https://github.com/shadcn-ui/ui/blob/396275e46a58333caa1fa0a991bd9bc5237d2ee3/packages/shadcn/src/utils/updaters/update-files.ts#L585
217
+ // to hit the fast-path step, we need to import `target` path first because it's detected from `fileSet`, a set of output file paths
218
+ toImportPath(file) {
219
+ let filePath = file.target ?? file.path;
220
+ if (filePath.startsWith("./")) filePath = filePath.slice(2);
221
+ return `@/${filePath.replaceAll(path.sep, "/")}`;
222
+ }
223
+ async build() {
224
+ return {
225
+ name: this.component.name,
226
+ title: this.component.title,
227
+ description: this.component.description,
228
+ files: (await Promise.all(
229
+ this.component.files.map((file) => this.buildFileAndDeps(file))
230
+ )).flat(),
231
+ subComponents: Array.from(this.subComponents),
232
+ dependencies: Object.fromEntries(this.dependencies),
233
+ devDependencies: Object.fromEntries(this.devDependencies)
234
+ };
235
+ }
236
+ async buildFileAndDeps(file) {
237
+ if (this.processedFiles.has(file.path)) return [];
238
+ this.processedFiles.add(file.path);
239
+ const resolver = this.compiler.resolver;
240
+ const queue = [];
241
+ const result = await this.buildFile(file, (reference) => {
242
+ if (reference.type === "custom") return reference.specifier;
243
+ if (reference.type === "file") {
244
+ const refFile = this.registry.onUnknownFile?.(reference.file);
245
+ if (refFile) {
246
+ queue.push(refFile);
247
+ return this.toImportPath(refFile);
248
+ }
249
+ if (refFile === false) return;
250
+ throw new Error(
251
+ `Unknown file ${reference.file} referenced by ${file.path}`
252
+ );
253
+ }
254
+ if (reference.type === "sub-component") {
255
+ const resolved = reference.resolved;
256
+ if (resolved.component.name !== this.component.name) {
257
+ this.subComponents.add(resolved.component.name);
258
+ }
259
+ return this.toImportPath(resolved.file);
260
+ }
261
+ const dep = resolver.getDepInfo(reference.dep);
262
+ if (dep) {
263
+ const map = dep.type === "dev" ? this.devDependencies : this.dependencies;
264
+ map.set(dep.name, dep.version);
265
+ }
266
+ return reference.specifier;
267
+ });
268
+ return [
269
+ result,
270
+ ...(await Promise.all(queue.map((file2) => this.buildFileAndDeps(file2)))).flat()
271
+ ];
272
+ }
273
+ resolveImport(sourceFilePath, specifier, specified) {
274
+ let filePath;
275
+ if (specified) {
276
+ filePath = specified.getFilePath();
277
+ } else if (specifier.startsWith("./") || specifier.startsWith("../")) {
278
+ filePath = path.join(path.dirname(sourceFilePath), specifier);
279
+ } else {
280
+ if (!specifier.startsWith("node:"))
281
+ console.warn(`Unknown specifier ${specifier}, skipping for now`);
282
+ return;
283
+ }
284
+ const resolver = this.compiler.resolver;
285
+ if (path.relative(this.registry.dir, filePath).startsWith("../")) {
286
+ return {
287
+ type: "dependency",
288
+ dep: resolver.getDepFromSpecifier(specifier),
289
+ specifier
290
+ };
291
+ }
292
+ const sub = resolver.getSubComponent(filePath);
293
+ if (sub) {
294
+ return {
295
+ type: "sub-component",
296
+ resolved: {
297
+ type: "local",
298
+ component: sub.component,
299
+ file: sub.file
300
+ }
301
+ };
302
+ }
303
+ return {
304
+ type: "file",
305
+ file: filePath
306
+ };
307
+ }
308
+ async buildFile(file, writeReference) {
309
+ const sourceFilePath = path.join(this.registry.dir, file.path);
310
+ const process2 = (specifier, specifiedFile) => {
311
+ const onResolve = this.component.onResolve ?? this.registry.onResolve;
312
+ let resolved = this.resolveImport(
313
+ sourceFilePath,
314
+ specifier.getLiteralValue(),
315
+ specifiedFile
316
+ );
317
+ if (!resolved) return;
318
+ if (onResolve) resolved = onResolve(resolved);
319
+ const out = writeReference(resolved);
320
+ if (out) specifier.setLiteralValue(out);
321
+ };
322
+ const sourceFile = await this.compiler.createSourceFile(sourceFilePath);
323
+ for (const item of sourceFile.getImportDeclarations()) {
324
+ process2(item.getModuleSpecifier(), item.getModuleSpecifierSourceFile());
325
+ }
326
+ for (const item of sourceFile.getExportDeclarations()) {
327
+ const specifier = item.getModuleSpecifier();
328
+ if (!specifier) continue;
329
+ process2(specifier, item.getModuleSpecifierSourceFile());
330
+ }
331
+ const calls = sourceFile.getDescendantsOfKind(ts.SyntaxKind.CallExpression);
332
+ for (const expression of calls) {
333
+ if (expression.getExpression().isKind(ts.SyntaxKind.ImportKeyword) && expression.getArguments().length === 1) {
334
+ const argument = expression.getArguments()[0];
335
+ if (!argument.isKind(ts.SyntaxKind.StringLiteral)) continue;
336
+ process2(
337
+ argument,
338
+ argument.getSymbol()?.getDeclarations()[0].getSourceFile()
339
+ );
340
+ }
341
+ }
342
+ return {
343
+ content: sourceFile.getFullText(),
344
+ type: file.type,
345
+ path: file.path,
346
+ target: file.target
347
+ };
348
+ }
349
+ };
350
+
351
+ // src/build/index.ts
352
+ function combineRegistry(...items) {
353
+ const out = {
354
+ index: [],
355
+ components: [],
356
+ name: items[0].name
357
+ };
358
+ for (const item of items) {
359
+ out.components.push(...item.components);
360
+ out.index.push(...item.index);
361
+ }
362
+ validateOutput(out);
363
+ return out;
364
+ }
365
+ async function writeShadcnRegistry(out, options) {
366
+ const { dir, cleanDir = false, baseUrl } = options;
367
+ if (cleanDir) {
368
+ await fs2.rm(dir, {
369
+ recursive: true,
370
+ force: true
371
+ });
372
+ console.log(picocolors.bold(picocolors.greenBright("Cleaned directory")));
373
+ }
374
+ const { registry, index } = toShadcnRegistry(out, baseUrl);
375
+ const write = registry.items.map(async (item) => {
376
+ const file = path2.join(dir, `${item.name}.json`);
377
+ await writeFile2(file, JSON.stringify(item, null, 2));
378
+ });
379
+ write.push(
380
+ writeFile2(path2.join(dir, "registry.json"), JSON.stringify(index, null, 2))
381
+ );
382
+ await Promise.all(write);
383
+ }
384
+ async function writeFumadocsRegistry(out, options) {
385
+ const { dir, cleanDir = false, log = true } = options;
386
+ if (cleanDir) {
387
+ await fs2.rm(dir, {
388
+ recursive: true,
389
+ force: true
390
+ });
391
+ console.log(picocolors.bold(picocolors.greenBright("Cleaned directory")));
392
+ }
393
+ async function writeIndex() {
394
+ const file = path2.join(dir, "_registry.json");
395
+ const json = JSON.stringify(out.index, null, 2);
396
+ await writeFile2(file, json, log);
397
+ }
398
+ const write = out.components.map(async (comp) => {
399
+ const file = path2.join(dir, `${comp.name}.json`);
400
+ const json = JSON.stringify(comp, null, 2);
401
+ await writeFile2(file, json, log);
402
+ });
403
+ write.push(writeIndex());
404
+ await Promise.all(write);
405
+ }
406
+ async function writeFile2(file, content, log = true) {
407
+ await fs2.mkdir(path2.dirname(file), { recursive: true });
408
+ await fs2.writeFile(file, content);
409
+ if (log) {
410
+ const size = (Buffer.byteLength(content) / 1024).toFixed(2);
411
+ console.log(
412
+ `${picocolors.greenBright("+")} ${path2.relative(process.cwd(), file)} ${picocolors.dim(`${size} KB`)}`
413
+ );
414
+ }
415
+ }
416
+ export {
417
+ ComponentCompiler,
418
+ RegistryCompiler,
419
+ combineRegistry,
420
+ toShadcnRegistry,
421
+ writeFumadocsRegistry,
422
+ writeShadcnRegistry
423
+ };
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,728 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import fs6 from "fs/promises";
5
+ import path5 from "path";
6
+ import { Command } from "commander";
7
+ import picocolors3 from "picocolors";
8
+
9
+ // src/config.ts
10
+ import fs2 from "fs/promises";
11
+
12
+ // src/utils/fs.ts
13
+ import fs from "fs/promises";
14
+ import path from "path";
15
+ async function exists(pathLike) {
16
+ try {
17
+ await fs.access(pathLike);
18
+ return true;
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ // src/utils/is-src.ts
25
+ async function isSrc() {
26
+ return exists("./src");
27
+ }
28
+
29
+ // src/config.ts
30
+ import { z } from "zod";
31
+ function createConfigSchema(isSrc2) {
32
+ const defaultAliases = {
33
+ uiDir: "./components/ui",
34
+ componentsDir: "./components",
35
+ blockDir: "./components",
36
+ cssDir: "./styles",
37
+ libDir: "./lib"
38
+ };
39
+ return z.object({
40
+ aliases: z.object({
41
+ uiDir: z.string().default(defaultAliases.uiDir),
42
+ componentsDir: z.string().default(defaultAliases.uiDir),
43
+ blockDir: z.string().default(defaultAliases.blockDir),
44
+ cssDir: z.string().default(defaultAliases.componentsDir),
45
+ libDir: z.string().default(defaultAliases.libDir)
46
+ }).default(defaultAliases),
47
+ baseDir: z.string().default(isSrc2 ? "src" : ""),
48
+ commands: z.object({
49
+ /**
50
+ * command to format output code automatically
51
+ */
52
+ format: z.string().optional()
53
+ }).default({})
54
+ });
55
+ }
56
+ async function createOrLoadConfig(file = "./cli.json") {
57
+ const inited = await initConfig(file);
58
+ if (inited) return inited;
59
+ const content = (await fs2.readFile(file)).toString();
60
+ const src = await isSrc();
61
+ const configSchema = createConfigSchema(src);
62
+ return configSchema.parse(JSON.parse(content));
63
+ }
64
+ async function initConfig(file = "./cli.json") {
65
+ if (await fs2.stat(file).then(() => true).catch(() => false)) {
66
+ return;
67
+ }
68
+ const src = await isSrc();
69
+ const defaultConfig = createConfigSchema(src).parse({});
70
+ await fs2.writeFile(file, JSON.stringify(defaultConfig, null, 2));
71
+ return defaultConfig;
72
+ }
73
+
74
+ // src/commands/file-tree.ts
75
+ var scanned = ["file", "directory", "link"];
76
+ function treeToMdx(input, noRoot = false) {
77
+ function toNode(item) {
78
+ if (item.type === "file" || item.type === "link") {
79
+ return `<File name=${JSON.stringify(item.name)} />`;
80
+ }
81
+ if (item.type === "directory") {
82
+ if (item.contents.length === 1 && "name" in item.contents[0]) {
83
+ const child = item.contents[0];
84
+ return toNode({
85
+ ...child,
86
+ name: `${item.name}/${child.name}`
87
+ });
88
+ }
89
+ return `<Folder name=${JSON.stringify(item.name)}>
90
+ ${item.contents.map(toNode).filter(Boolean).join("\n")}
91
+ </Folder>`;
92
+ }
93
+ return "";
94
+ }
95
+ let children = input.filter((v) => scanned.includes(v.type));
96
+ if (noRoot && children.length === 1 && input[0].type === "directory") {
97
+ children = input[0].contents;
98
+ }
99
+ return `<Files>
100
+ ${children.map(toNode).filter(Boolean).join("\n")}
101
+ </Files>`;
102
+ }
103
+ function treeToJavaScript(input, noRoot, importName = "@hanzo/docs-ui/components/files") {
104
+ return `import { File, Files, Folder } from ${JSON.stringify(importName)}
105
+
106
+ export default (${treeToMdx(input, noRoot)})`;
107
+ }
108
+
109
+ // src/utils/file-tree/run-tree.ts
110
+ import { x } from "tinyexec";
111
+ async function runTree(args) {
112
+ const out = await x("tree", [args, "--gitignore", "--prune", "-J"]);
113
+ try {
114
+ return JSON.parse(out.stdout);
115
+ } catch (e) {
116
+ throw new Error("failed to run `tree` command", {
117
+ cause: e
118
+ });
119
+ }
120
+ }
121
+
122
+ // package.json
123
+ var package_default = {
124
+ name: "@hanzo/docs-cli",
125
+ version: "1.1.0",
126
+ description: "The CLI tool for Hanzo Docs",
127
+ keywords: [
128
+ "Hanzo",
129
+ "Docs",
130
+ "CLI"
131
+ ],
132
+ homepage: "https://hanzo.ai/docs",
133
+ repository: "github:hanzoai/docs",
134
+ license: "MIT",
135
+ author: "Fuma Nama",
136
+ type: "module",
137
+ exports: {
138
+ "./build": {
139
+ import: "./dist/build/index.js",
140
+ types: "./dist/build/index.d.ts"
141
+ }
142
+ },
143
+ main: "./dist/index.js",
144
+ bin: {
145
+ "hanzo-docs": "./dist/index.js"
146
+ },
147
+ files: [
148
+ "dist/*"
149
+ ],
150
+ scripts: {
151
+ build: "tsup",
152
+ clean: "rimraf dist",
153
+ dev: "tsup --watch",
154
+ lint: "eslint .",
155
+ "types:check": "tsc --noEmit"
156
+ },
157
+ dependencies: {
158
+ "@clack/prompts": "^0.11.0",
159
+ commander: "^14.0.2",
160
+ "package-manager-detector": "^1.6.0",
161
+ picocolors: "^1.1.1",
162
+ tinyexec: "^1.0.2",
163
+ "ts-morph": "^27.0.2",
164
+ zod: "^4.1.13"
165
+ },
166
+ devDependencies: {
167
+ "@types/node": "24.10.2",
168
+ "eslint-config-custom": "workspace:*",
169
+ shadcn: "3.6.0",
170
+ tsconfig: "workspace:*"
171
+ },
172
+ publishConfig: {
173
+ access: "public"
174
+ }
175
+ };
176
+
177
+ // src/commands/customise.ts
178
+ import { cancel, group, intro as intro2, log as log4, outro as outro3, select } from "@clack/prompts";
179
+ import picocolors2 from "picocolors";
180
+
181
+ // src/commands/add.ts
182
+ import {
183
+ intro,
184
+ isCancel as isCancel3,
185
+ log as log3,
186
+ multiselect,
187
+ outro as outro2,
188
+ spinner as spinner2
189
+ } from "@clack/prompts";
190
+ import picocolors from "picocolors";
191
+
192
+ // src/registry/installer/index.ts
193
+ import path3 from "path";
194
+ import fs4 from "fs/promises";
195
+ import { confirm as confirm2, isCancel as isCancel2, log, outro } from "@clack/prompts";
196
+
197
+ // src/utils/typescript.ts
198
+ import { Project } from "ts-morph";
199
+ function createEmptyProject() {
200
+ return new Project({
201
+ compilerOptions: {}
202
+ });
203
+ }
204
+
205
+ // src/constants.ts
206
+ var typescriptExtensions = [".ts", ".tsx", ".js", ".jsx"];
207
+
208
+ // src/utils/ast.ts
209
+ import path2 from "path";
210
+ function toImportSpecifier(sourceFile, referenceFile) {
211
+ const extname = path2.extname(referenceFile);
212
+ const removeExt = typescriptExtensions.includes(extname);
213
+ const importPath = path2.relative(
214
+ path2.dirname(sourceFile),
215
+ removeExt ? referenceFile.substring(0, referenceFile.length - extname.length) : referenceFile
216
+ ).replaceAll(path2.sep, "/");
217
+ return importPath.startsWith("../") ? importPath : `./${importPath}`;
218
+ }
219
+
220
+ // src/registry/installer/index.ts
221
+ import { x as x3 } from "tinyexec";
222
+
223
+ // src/registry/installer/dep-manager.ts
224
+ import fs3 from "fs/promises";
225
+
226
+ // src/utils/get-package-manager.ts
227
+ import { detect } from "package-manager-detector";
228
+ async function getPackageManager() {
229
+ const result = await detect();
230
+ return result?.name ?? "npm";
231
+ }
232
+
233
+ // src/registry/installer/dep-manager.ts
234
+ import { confirm, isCancel, spinner } from "@clack/prompts";
235
+ import { x as x2 } from "tinyexec";
236
+ var DependencyManager = class {
237
+ /**
238
+ * Get dependencies from `package.json`
239
+ */
240
+ async getDeps() {
241
+ if (this.cachedInstalledDeps) return this.cachedInstalledDeps;
242
+ const dependencies = /* @__PURE__ */ new Map();
243
+ if (!await exists("package.json")) return dependencies;
244
+ const content = await fs3.readFile("package.json");
245
+ const parsed = JSON.parse(content.toString());
246
+ if ("dependencies" in parsed && typeof parsed.dependencies === "object") {
247
+ const records = parsed.dependencies;
248
+ for (const [k, v] of Object.entries(records)) {
249
+ dependencies.set(k, v);
250
+ }
251
+ }
252
+ if ("devDependencies" in parsed && typeof parsed.devDependencies === "object") {
253
+ const records = parsed.devDependencies;
254
+ for (const [k, v] of Object.entries(records)) {
255
+ dependencies.set(k, v);
256
+ }
257
+ }
258
+ return this.cachedInstalledDeps = dependencies;
259
+ }
260
+ async resolveInstallDependencies(deps) {
261
+ const cachedInstalledDeps = await this.getDeps();
262
+ return Object.entries(deps).filter(([k]) => !cachedInstalledDeps.has(k)).map(([k, v]) => v === null || v.length === 0 ? k : `${k}@${v}`);
263
+ }
264
+ async installDeps(deps, devDeps) {
265
+ const items = await this.resolveInstallDependencies(deps);
266
+ const devItems = await this.resolveInstallDependencies(devDeps);
267
+ if (items.length === 0 && devItems.length === 0) return;
268
+ const manager = await getPackageManager();
269
+ const value = await confirm({
270
+ message: `Do you want to install with ${manager}?
271
+ ${[...items, ...devItems].map((v) => `- ${v}`).join("\n")}`
272
+ });
273
+ if (isCancel(value) || !value) {
274
+ return;
275
+ }
276
+ const spin = spinner();
277
+ spin.start("Installing dependencies...");
278
+ if (items.length > 0) await x2(manager, ["install", ...items]);
279
+ if (devItems.length > 0) await x2(manager, ["install", ...devItems, "-D"]);
280
+ spin.stop("Dependencies installed.");
281
+ }
282
+ };
283
+
284
+ // src/utils/cache.ts
285
+ var AsyncCache = class {
286
+ constructor() {
287
+ this.store = /* @__PURE__ */ new Map();
288
+ }
289
+ cached(key, fn) {
290
+ let cached = this.store.get(key);
291
+ if (cached !== void 0) return cached;
292
+ cached = fn();
293
+ this.store.set(key, cached);
294
+ return cached;
295
+ }
296
+ };
297
+
298
+ // src/registry/installer/index.ts
299
+ var ComponentInstaller = class {
300
+ constructor(client) {
301
+ this.project = createEmptyProject();
302
+ this.installedFiles = /* @__PURE__ */ new Set();
303
+ this.downloadCache = new AsyncCache();
304
+ this.dependencies = {};
305
+ this.devDependencies = {};
306
+ this.client = client;
307
+ }
308
+ async install(name) {
309
+ const downloaded = await this.download(name);
310
+ for (const item of downloaded) {
311
+ Object.assign(this.dependencies, item.dependencies);
312
+ Object.assign(this.devDependencies, item.devDependencies);
313
+ }
314
+ const fileList = this.buildFileList(downloaded);
315
+ for (const file of fileList) {
316
+ const filePath = file.target ?? file.path;
317
+ if (this.installedFiles.has(filePath)) continue;
318
+ const outPath = this.resolveOutputPath(file);
319
+ const output = typescriptExtensions.includes(path3.extname(filePath)) ? await this.transform(outPath, file, fileList) : file.content;
320
+ const status = await fs4.readFile(outPath).then((res) => {
321
+ if (res.toString() === output) return "ignore";
322
+ return "need-update";
323
+ }).catch(() => "write");
324
+ this.installedFiles.add(filePath);
325
+ if (status === "ignore") continue;
326
+ if (status === "need-update") {
327
+ const override = await confirm2({
328
+ message: `Do you want to override ${outPath}?`,
329
+ initialValue: false
330
+ });
331
+ if (isCancel2(override)) {
332
+ outro("Ended");
333
+ process.exit(0);
334
+ }
335
+ if (!override) continue;
336
+ }
337
+ await fs4.mkdir(path3.dirname(outPath), { recursive: true });
338
+ await fs4.writeFile(outPath, output);
339
+ log.step(`downloaded ${outPath}`);
340
+ }
341
+ }
342
+ async installDeps() {
343
+ await new DependencyManager().installDeps(
344
+ this.dependencies,
345
+ this.devDependencies
346
+ );
347
+ }
348
+ async onEnd() {
349
+ const config = this.client.config;
350
+ if (config.commands.format) {
351
+ await x3(config.commands.format);
352
+ }
353
+ }
354
+ buildFileList(downloaded) {
355
+ const map = /* @__PURE__ */ new Map();
356
+ for (const item of downloaded) {
357
+ for (const file of item.files) {
358
+ const filePath = file.target ?? file.path;
359
+ if (map.has(filePath)) {
360
+ console.warn(
361
+ `noticed duplicated output file for ${filePath}, ignoring for now.`
362
+ );
363
+ continue;
364
+ }
365
+ map.set(filePath, file);
366
+ }
367
+ }
368
+ return Array.from(map.values());
369
+ }
370
+ /**
371
+ * return a list of components, merged with child components.
372
+ */
373
+ download(name) {
374
+ return this.downloadCache.cached(name, async () => {
375
+ const comp = await this.client.fetchComponent(name);
376
+ const result = [comp];
377
+ this.downloadCache.store.set(name, result);
378
+ const child = await Promise.all(
379
+ comp.subComponents.map((sub) => this.download(sub))
380
+ );
381
+ for (const sub of child) result.push(...sub);
382
+ return result;
383
+ });
384
+ }
385
+ async transform(filePath, file, fileList) {
386
+ const sourceFile = this.project.createSourceFile(filePath, file.content, {
387
+ overwrite: true
388
+ });
389
+ const prefix = "@/";
390
+ for (const specifier of sourceFile.getImportStringLiterals()) {
391
+ if (specifier.getLiteralValue().startsWith(prefix)) {
392
+ const lookup = specifier.getLiteralValue().substring(prefix.length);
393
+ const target = fileList.find((item) => {
394
+ const filePath2 = item.target ?? item.path;
395
+ return filePath2 === lookup;
396
+ });
397
+ if (target) {
398
+ specifier.setLiteralValue(
399
+ toImportSpecifier(filePath, this.resolveOutputPath(target))
400
+ );
401
+ } else {
402
+ console.warn(`cannot find the referenced file of ${specifier}`);
403
+ }
404
+ }
405
+ }
406
+ for (const statement of sourceFile.getImportDeclarations()) {
407
+ const info = await this.client.fetchRegistryInfo();
408
+ const specifier = statement.getModuleSpecifier().getLiteralValue();
409
+ if (info.switchables && specifier in info.switchables) {
410
+ const switchable = info.switchables[specifier];
411
+ statement.setModuleSpecifier(switchable.specifier);
412
+ for (const member of statement.getNamedImports()) {
413
+ const name = member.getName();
414
+ if (name in switchable.members) {
415
+ member.setName(switchable.members[name]);
416
+ }
417
+ }
418
+ }
419
+ }
420
+ return sourceFile.getFullText();
421
+ }
422
+ resolveOutputPath(ref) {
423
+ const config = this.client.config;
424
+ if (ref.target) {
425
+ return path3.join(config.baseDir, ref.target);
426
+ }
427
+ const base = path3.basename(ref.path);
428
+ const dir = {
429
+ components: config.aliases.componentsDir,
430
+ block: config.aliases.blockDir,
431
+ ui: config.aliases.uiDir,
432
+ css: config.aliases.cssDir,
433
+ lib: config.aliases.libDir,
434
+ route: "./"
435
+ }[ref.type];
436
+ return path3.join(config.baseDir, dir, base);
437
+ }
438
+ };
439
+
440
+ // src/registry/schema.ts
441
+ import { z as z2 } from "zod";
442
+ var namespaces = [
443
+ "components",
444
+ "lib",
445
+ "css",
446
+ "route",
447
+ "ui",
448
+ "block"
449
+ ];
450
+ var indexSchema = z2.object({
451
+ name: z2.string(),
452
+ title: z2.string().optional(),
453
+ description: z2.string().optional()
454
+ });
455
+ var fileSchema = z2.object({
456
+ type: z2.literal(namespaces),
457
+ path: z2.string(),
458
+ target: z2.string().optional(),
459
+ content: z2.string()
460
+ });
461
+ var componentSchema = z2.object({
462
+ name: z2.string(),
463
+ title: z2.string().optional(),
464
+ description: z2.string().optional(),
465
+ files: z2.array(fileSchema),
466
+ dependencies: z2.record(z2.string(), z2.string().or(z2.null())),
467
+ devDependencies: z2.record(z2.string(), z2.string().or(z2.null())),
468
+ subComponents: z2.array(z2.string()).default([])
469
+ });
470
+ var switchableEntitySchema = z2.object({
471
+ /**
472
+ * map specifier string
473
+ */
474
+ specifier: z2.string(),
475
+ /**
476
+ * map names of exported members
477
+ */
478
+ members: z2.record(z2.string(), z2.string())
479
+ });
480
+ var registryInfoSchema = z2.object({
481
+ switchables: z2.record(z2.string(), switchableEntitySchema).optional(),
482
+ indexes: z2.array(indexSchema)
483
+ });
484
+
485
+ // src/registry/client.ts
486
+ import path4 from "path";
487
+ import fs5 from "fs/promises";
488
+ import { log as log2 } from "@clack/prompts";
489
+ function remoteResolver(url) {
490
+ return async (file) => {
491
+ const res = await fetch(`${url}/${file}`);
492
+ if (!res.ok) {
493
+ throw new Error(`failed to fetch ${url}/${file}: ${res.statusText}`);
494
+ }
495
+ return await res.json();
496
+ };
497
+ }
498
+ function localResolver(dir) {
499
+ return async (file) => {
500
+ const filePath = path4.join(dir, file);
501
+ return await fs5.readFile(filePath).then((res) => JSON.parse(res.toString())).catch((e) => {
502
+ throw new Error(`failed to resolve local file "${filePath}"`, {
503
+ cause: e
504
+ });
505
+ });
506
+ };
507
+ }
508
+ var RegistryClient = class {
509
+ constructor(config, resolver) {
510
+ this.config = config;
511
+ this.resolver = resolver;
512
+ }
513
+ async fetchRegistryInfo() {
514
+ this.registryInfo ??= registryInfoSchema.parse(
515
+ await this.resolver("_registry.json").catch((e) => {
516
+ log2.error(String(e));
517
+ process.exit(1);
518
+ })
519
+ );
520
+ return this.registryInfo;
521
+ }
522
+ async fetchComponent(name) {
523
+ return componentSchema.parse(
524
+ await this.resolver(`${name}.json`).catch((e) => {
525
+ log2.error(`component ${name} not found:`);
526
+ log2.error(String(e));
527
+ process.exit(1);
528
+ })
529
+ );
530
+ }
531
+ };
532
+
533
+ // src/commands/add.ts
534
+ async function add(input, resolver, config) {
535
+ const client = new RegistryClient(config, resolver);
536
+ const installer = new ComponentInstaller(client);
537
+ let target = input;
538
+ if (input.length === 0) {
539
+ const spin = spinner2();
540
+ spin.start("fetching registry");
541
+ const info = await client.fetchRegistryInfo();
542
+ spin.stop(picocolors.bold(picocolors.greenBright("registry fetched")));
543
+ const value = await multiselect({
544
+ message: "Select components to install",
545
+ options: info.indexes.map((item) => ({
546
+ label: item.title,
547
+ value: item.name,
548
+ hint: item.description
549
+ }))
550
+ });
551
+ if (isCancel3(value)) {
552
+ outro2("Ended");
553
+ return;
554
+ }
555
+ target = value;
556
+ }
557
+ await install(target, installer);
558
+ }
559
+ async function install(target, installer) {
560
+ for (const name of target) {
561
+ intro(
562
+ picocolors.bold(
563
+ picocolors.inverse(picocolors.cyanBright(`Add Component: ${name}`))
564
+ )
565
+ );
566
+ try {
567
+ await installer.install(name);
568
+ outro2(picocolors.bold(picocolors.greenBright(`${name} installed`)));
569
+ } catch (e) {
570
+ log3.error(String(e));
571
+ throw e;
572
+ }
573
+ }
574
+ intro(picocolors.bold("New Dependencies"));
575
+ await installer.installDeps();
576
+ await installer.onEnd();
577
+ outro2(picocolors.bold(picocolors.greenBright("Successful")));
578
+ }
579
+
580
+ // src/commands/customise.ts
581
+ async function customise(resolver, config) {
582
+ const client = new RegistryClient(config, resolver);
583
+ intro2(picocolors2.bgBlack(picocolors2.whiteBright("Customise Hanzo Docs UI")));
584
+ const installer = new ComponentInstaller(client);
585
+ const result = await group(
586
+ {
587
+ target: () => select({
588
+ message: "What do you want to customise?",
589
+ options: [
590
+ {
591
+ label: "Docs Layout",
592
+ value: "docs",
593
+ hint: "main UI of your docs"
594
+ },
595
+ {
596
+ label: "Home Layout",
597
+ value: "home",
598
+ hint: "the navbar for your other pages"
599
+ }
600
+ ]
601
+ }),
602
+ mode: (v) => {
603
+ if (v.results.target !== "docs") return;
604
+ return select({
605
+ message: "Which variant do you want to start from?",
606
+ options: [
607
+ {
608
+ label: "Start from minimal styles",
609
+ value: "minimal",
610
+ hint: "for those who want to build their own variant from ground up."
611
+ },
612
+ {
613
+ label: "Start from default layout",
614
+ value: "full-default",
615
+ hint: "useful for adjusting small details."
616
+ },
617
+ {
618
+ label: "Start from Notebook layout",
619
+ value: "full-notebook",
620
+ hint: "useful for adjusting small details."
621
+ }
622
+ ]
623
+ });
624
+ }
625
+ },
626
+ {
627
+ onCancel: () => {
628
+ cancel("Installation Stopped.");
629
+ process.exit(0);
630
+ }
631
+ }
632
+ );
633
+ if (result.target === "docs") {
634
+ const targets = [];
635
+ if (result.mode === "minimal") {
636
+ targets.push("layouts/docs-min");
637
+ } else {
638
+ targets.push(
639
+ result.mode === "full-default" ? "layouts/docs" : "layouts/notebook"
640
+ );
641
+ }
642
+ await install(targets, installer);
643
+ const maps = result.mode === "full-notebook" ? [
644
+ ["@hanzo/docs-ui/layouts/notebook", "@/components/layout/notebook"],
645
+ [
646
+ "@hanzo/docs-ui/layouts/notebook/page",
647
+ "@/components/layout/notebook/page"
648
+ ]
649
+ ] : [
650
+ ["@hanzo/docs-ui/layouts/docs", "@/components/layout/docs"],
651
+ ["@hanzo/docs-ui/layouts/docs/page", "@/components/layout/docs/page"]
652
+ ];
653
+ printNext(...maps);
654
+ }
655
+ if (result.target === "home") {
656
+ await install(["layouts/home"], installer);
657
+ printNext(["@hanzo/docs-ui/layouts/home", `@/components/layout/home`]);
658
+ }
659
+ outro3(picocolors2.bold("Have fun!"));
660
+ }
661
+ function printNext(...maps) {
662
+ intro2(picocolors2.bold("What is Next?"));
663
+ log4.info(
664
+ [
665
+ "You can check the installed components in `components`.",
666
+ picocolors2.dim("---"),
667
+ "Open your `layout.tsx` files, replace the imports of components:",
668
+ ...maps.map(
669
+ ([from, to]) => picocolors2.greenBright(`"${from}" -> "${to}"`)
670
+ )
671
+ ].join("\n")
672
+ );
673
+ }
674
+
675
+ // src/index.ts
676
+ var program = new Command().option("--config <string>");
677
+ program.name("fumadocs").description("CLI to setup Fumadocs, init a config").version(package_default.version).action(async () => {
678
+ if (await initConfig()) {
679
+ console.log(picocolors3.green("Initialized a `./cli.json` config file."));
680
+ } else {
681
+ console.log(picocolors3.redBright("A config file already exists."));
682
+ }
683
+ });
684
+ program.command("customise").alias("customize").description("simple way to customise layouts with Fumadocs UI").option("--dir <string>", "the root url or directory to resolve registry").action(async (options) => {
685
+ const resolver = getResolverFromDir(options.dir);
686
+ await customise(resolver, await createOrLoadConfig(options.config));
687
+ });
688
+ var dirShortcuts = {
689
+ ":dev": "https://preview.fumadocs.dev/registry",
690
+ ":localhost": "http://localhost:3000/registry"
691
+ };
692
+ program.command("add").description("add a new component to your docs").argument("[components...]", "components to download").option("--dir <string>", "the root url or directory to resolve registry").action(
693
+ async (input, options) => {
694
+ const resolver = getResolverFromDir(options.dir);
695
+ await add(input, resolver, await createOrLoadConfig(options.config));
696
+ }
697
+ );
698
+ program.command("tree").argument(
699
+ "[json_or_args]",
700
+ "JSON output of `tree` command or arguments for the `tree` command"
701
+ ).argument("[output]", "output path of file").option("--js", "output as JavaScript file").option("--no-root", "remove the root node").option("--import-name <name>", "where to import components (JS only)").action(
702
+ async (str, output, {
703
+ js,
704
+ root,
705
+ importName
706
+ }) => {
707
+ const jsExtensions = [".js", ".tsx", ".jsx"];
708
+ const noRoot = !root;
709
+ let nodes;
710
+ try {
711
+ nodes = JSON.parse(str ?? "");
712
+ } catch {
713
+ nodes = await runTree(str ?? "./");
714
+ }
715
+ const out = js || output && jsExtensions.includes(path5.extname(output)) ? treeToJavaScript(nodes, noRoot, importName) : treeToMdx(nodes, noRoot);
716
+ if (output) {
717
+ await fs6.mkdir(path5.dirname(output), { recursive: true });
718
+ await fs6.writeFile(output, out);
719
+ } else {
720
+ console.log(out);
721
+ }
722
+ }
723
+ );
724
+ function getResolverFromDir(dir = "https://fumadocs.dev/registry") {
725
+ if (dir in dirShortcuts) dir = dirShortcuts[dir];
726
+ return dir.startsWith("http://") || dir.startsWith("https://") ? remoteResolver(dir) : localResolver(dir);
727
+ }
728
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@hanzo/docs-cli",
3
+ "version": "1.1.0",
4
+ "description": "The CLI tool for Hanzo Docs",
5
+ "keywords": [
6
+ "Hanzo",
7
+ "Docs",
8
+ "CLI"
9
+ ],
10
+ "homepage": "https://hanzo.ai/docs",
11
+ "repository": "github:hanzoai/docs",
12
+ "license": "MIT",
13
+ "author": "Fuma Nama",
14
+ "type": "module",
15
+ "exports": {
16
+ "./build": {
17
+ "import": "./dist/build/index.js",
18
+ "types": "./dist/build/index.d.ts"
19
+ }
20
+ },
21
+ "main": "./dist/index.js",
22
+ "bin": {
23
+ "hanzo-docs": "./dist/index.js"
24
+ },
25
+ "files": [
26
+ "dist/*"
27
+ ],
28
+ "dependencies": {
29
+ "@clack/prompts": "^0.11.0",
30
+ "commander": "^14.0.2",
31
+ "package-manager-detector": "^1.6.0",
32
+ "picocolors": "^1.1.1",
33
+ "tinyexec": "^1.0.2",
34
+ "ts-morph": "^27.0.2",
35
+ "zod": "^4.1.13"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "24.10.2",
39
+ "shadcn": "3.6.0",
40
+ "eslint-config-custom": "0.0.0",
41
+ "tsconfig": "0.0.0"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "scripts": {
47
+ "build": "tsup",
48
+ "clean": "rimraf dist",
49
+ "dev": "tsup --watch",
50
+ "lint": "eslint .",
51
+ "types:check": "tsc --noEmit"
52
+ }
53
+ }