Last active
November 20, 2025 01:15
-
-
Save anonhostpi/6a8e78b65f58177a17326e9c336d275f to your computer and use it in GitHub Desktop.
Generate a yaml tree representing a directory
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env -S deno run --allow-read --allow-run | |
| import { parseArgs } from "jsr:@std/cli/parse-args"; | |
| import { resolve, normalize, relative } from "jsr:@std/path"; | |
| interface Tree { | |
| [key: string]: string | Tree; | |
| } | |
| // ------------------------------------------------------------- | |
| // Helpers | |
| // ------------------------------------------------------------- | |
| // Returns true if the path is ignored by .gitignore (if inside a git repo) | |
| async function gitIgnored(absPath: string): Promise<boolean> { | |
| try { | |
| const p = Deno.run({ | |
| cmd: ["git", "check-ignore", absPath], | |
| stdout: "null", | |
| stderr: "null", | |
| }); | |
| const status = await p.status(); | |
| return status.success; | |
| } catch { | |
| return false; // if not a git repo or git unavailable | |
| } | |
| } | |
| // Ignore any file or directory starting with "." | |
| function isHidden(name: string): boolean { | |
| return name.startsWith("."); | |
| } | |
| // ------------------------------------------------------------- | |
| // Main tree reader | |
| // ------------------------------------------------------------- | |
| async function treeToObject( | |
| dir: string, | |
| maxDepth?: number, | |
| currentDepth = 0, | |
| ): Promise<Tree> { | |
| const tree: Tree = {}; | |
| try { | |
| for await (const entry of Deno.readDir(dir)) { | |
| const absPath = resolve(dir, entry.name); | |
| // Skip hidden | |
| if (isHidden(entry.name)) continue; | |
| // Skip .gitignore-ignored paths | |
| if (await gitIgnored(absPath)) continue; | |
| if (entry.isDirectory) { | |
| if (maxDepth !== undefined && currentDepth >= maxDepth) { | |
| tree[entry.name + "/"] = "__skipped (max depth)__"; | |
| } else { | |
| tree[entry.name] = await treeToObject( | |
| absPath, | |
| maxDepth, | |
| currentDepth + 1, | |
| ); | |
| } | |
| } else if (entry.isFile) { | |
| try { | |
| const text = await Deno.readTextFile(absPath); | |
| tree[entry.name] = text; | |
| } catch (err) { | |
| tree[entry.name] = err instanceof Deno.errors.InvalidData | |
| ? "__binary__" | |
| : `__error__: ${err.message}`; | |
| } | |
| } | |
| } | |
| } catch (err) { | |
| tree["__error__"] = err.message; | |
| } | |
| return tree; | |
| } | |
| // ------------------------------------------------------------- | |
| // YAML literal-block output | |
| // ------------------------------------------------------------- | |
| function literalBlockYaml(obj: unknown, indent = 0): string { | |
| const pad = " ".repeat(indent); | |
| if (obj === null || obj === undefined) return "null"; | |
| if (typeof obj === "string") { | |
| const blockIndent = pad + " "; | |
| const lines = obj.split(/\r?\n/) | |
| .map((line) => blockIndent + line) | |
| .join("\n"); | |
| return `|\n${lines}`; | |
| } | |
| if (typeof obj !== "object") return JSON.stringify(obj); | |
| return Object.entries(obj as Record<string, unknown>) | |
| .map(([key, value]) => { | |
| const rendered = literalBlockYaml(value, indent + 1); | |
| if (rendered.startsWith("|")) { | |
| return `${pad}${key}: ${rendered}`; | |
| } else if (rendered.includes("\n")) { | |
| return `${pad}${key}:\n${rendered}`; | |
| } | |
| return `${pad}${key}: ${rendered}`; | |
| }) | |
| .join("\n"); | |
| } | |
| // ------------------------------------------------------------- | |
| // CLI | |
| // ------------------------------------------------------------- | |
| if (import.meta.main) { | |
| const args = parseArgs(Deno.args, { | |
| string: ["o", "output", "d", "depth"], | |
| boolean: ["sort"], | |
| alias: { o: "output", d: "depth" }, | |
| }); | |
| const input = args._[0]; | |
| if (!input || typeof input !== "string") { | |
| console.error("Usage: tree2yaml.ts <path> [-o file.yaml] [--depth N] [--sort]"); | |
| Deno.exit(1); | |
| } | |
| const root = normalize(resolve(Deno.cwd(), input)); | |
| const depth = args.depth ? Number(args.depth) : undefined; | |
| console.error(`π Scanning ${root}`); | |
| const tree = await treeToObject(root, depth); | |
| function sortObject(obj: Tree): Tree { | |
| return Object.fromEntries( | |
| Object.entries(obj) | |
| .sort(([a], [b]) => a.localeCompare(b)) | |
| .map(([k, v]) => [ | |
| k, | |
| typeof v === "object" ? sortObject(v as Tree) : v, | |
| ]), | |
| ); | |
| } | |
| const finalTree = args.sort ? sortObject(tree) : tree; | |
| const yaml = literalBlockYaml(finalTree) + "\n"; | |
| if (args.output) { | |
| await Deno.writeTextFile(args.output, yaml); | |
| console.error(`π Wrote to ${args.output}`); | |
| } else { | |
| console.log(yaml); | |
| } | |
| console.error("β Done"); | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
run with: