From 058367e11f02f2b3c498c56a959de4b38bc39240 Mon Sep 17 00:00:00 2001 From: mayconrfreitas Date: Mon, 29 Sep 2025 14:00:01 -0300 Subject: [PATCH 1/5] fix pnpm version duplication --- .github/workflows/ci.yml | 2 +- .github/workflows/publish.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 640db47..84bcbd8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v4 with: - version: 9 + run_install: false - name: Install run: pnpm i diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 797897d..4d38c82 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -21,7 +21,7 @@ jobs: - uses: pnpm/action-setup@v4 with: - version: 9 + run_install: false - run: pnpm i - run: pnpm build From 6c4ccae5d5980dc82e50f562783f31bbb35082ad Mon Sep 17 00:00:00 2001 From: mayconrfreitas Date: Mon, 29 Sep 2025 15:03:29 -0300 Subject: [PATCH 2/5] fix cli error --- src/cli.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cli.ts b/src/cli.ts index 7d5cb0c..99074b0 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -19,7 +19,9 @@ function parseArgs(args: string[]): Record { } async function readInput(fromFile?: string): Promise { - if (fromFile) return await fs.readFile(fromFile, "utf8"); + if (fromFile) { + return await fs.readFile(fromFile, { encoding: "utf8" }); + } const chunks: string[] = []; return await new Promise((resolve, reject) => { process.stdin.setEncoding("utf8"); From c37f4084c4479b297715469f455acfd6c230cbc6 Mon Sep 17 00:00:00 2001 From: mayconrfreitas Date: Mon, 29 Sep 2025 15:07:56 -0300 Subject: [PATCH 3/5] fix cli error 2 --- .github/workflows/ci.yml | 7 ++----- src/cli.ts | 5 +++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84bcbd8..eac70b2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,17 +9,14 @@ on: jobs: build-and-test: runs-on: ubuntu-latest - strategy: - matrix: - node-version: [20.x, 18.x] steps: - name: Checkout uses: actions/checkout@v4 - - name: Use Node.js ${{ matrix.node-version }} + - name: Use Node.js 20 uses: actions/setup-node@v4 with: - node-version: ${{ matrix.node-version }} + node-version: 20 - name: Setup pnpm uses: pnpm/action-setup@v4 diff --git a/src/cli.ts b/src/cli.ts index 99074b0..d299508 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -20,12 +20,13 @@ function parseArgs(args: string[]): Record { async function readInput(fromFile?: string): Promise { if (fromFile) { - return await fs.readFile(fromFile, { encoding: "utf8" }); + const buf = await fs.readFile(fromFile); + return buf.toString("utf8"); } const chunks: string[] = []; return await new Promise((resolve, reject) => { process.stdin.setEncoding("utf8"); - process.stdin.on("data", c => chunks.push(c)); + process.stdin.on("data", c => chunks.push(String(c))); process.stdin.on("end", () => resolve(chunks.join(""))); process.stdin.on("error", reject); }); From efc8311048b0a96e7de6ca492351afade7ac3abf Mon Sep 17 00:00:00 2001 From: mayconrfreitas Date: Mon, 29 Sep 2025 15:55:15 -0300 Subject: [PATCH 4/5] refactor code style for better readability in cli.ts and parser.ts --- src/cli.ts | 16 +++++++++++---- src/parser.ts | 55 ++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 55 insertions(+), 16 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index d299508..6d9d6da 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -12,7 +12,10 @@ function parseArgs(args: string[]): Record { const k = a.slice(2); const next = args[i + 1]; if (!next || next.startsWith("--")) out[k] = true; - else { out[k] = next; i++; } + else { + out[k] = next; + i++; + } } } return out; @@ -26,7 +29,7 @@ async function readInput(fromFile?: string): Promise { const chunks: string[] = []; return await new Promise((resolve, reject) => { process.stdin.setEncoding("utf8"); - process.stdin.on("data", c => chunks.push(String(c))); + process.stdin.on("data", (c) => chunks.push(String(c))); process.stdin.on("end", () => resolve(chunks.join(""))); process.stdin.on("error", reject); }); @@ -56,7 +59,10 @@ Example: (async function main() { const argv = parseArgs(process.argv.slice(2)); - if (argv.help) { help(); process.exit(0); } + if (argv.help) { + help(); + process.exit(0); + } const rootDir = path.resolve(String(argv.root ?? ".")); const inferRoot = Boolean(argv["infer-root"]); @@ -74,7 +80,9 @@ Example: try { const { plan, baseRoot } = parseToPlan(input, { rootDir, inferRoot }); await executePlan(plan, { dryRun, verbose, gitkeep }); - const msg = dryRun ? "Dry run complete. Nothing was created." : `Structure created at: ${baseRoot}`; + const msg = dryRun + ? "Dry run complete. Nothing was created." + : `Structure created at: ${baseRoot}`; process.stdout.write(msg + "\n"); } catch (e: any) { process.stderr.write(`[error] ${e?.message ?? String(e)}\n`); diff --git a/src/parser.ts b/src/parser.ts index f04f792..68dbda5 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,6 +1,15 @@ import path from "node:path"; import { PlanItem, ParseOptions } from "./types.js"; -import { computeDepth, hasExtension, isBareKnownFile, looksLikeDirToken, safeJoin, splitPrefixContent, stripBullets, stripTrailingComments } from "./utils.js"; +import { + computeDepth, + hasExtension, + isBareKnownFile, + looksLikeDirToken, + safeJoin, + splitPrefixContent, + stripBullets, + stripTrailingComments, +} from "./utils.js"; export interface ParseResult { plan: PlanItem[]; @@ -9,17 +18,23 @@ export interface ParseResult { export function parseToPlan(input: string, opts: ParseOptions): ParseResult { const rootDir = path.resolve(opts.rootDir); - let lines = input.split(/\r?\n/) - .map(l => l.replace(/\t/g, " ")) - .map(l => l.replace(/\s+$/, "")) - .filter(l => l.trim().length > 0); + let lines = input + .split(/\r?\n/) + .map((l) => l.replace(/\t/g, " ")) + .map((l) => l.replace(/\s+$/, "")) + .filter((l) => l.trim().length > 0); + + // inferRoot: primeira linha é algo como "my-project/" (apenas a barra de fechamento) let baseRoot = rootDir; - if (opts.inferRoot) { + if (opts.inferRoot && lines.length > 0) { const firstContent = sanitizeContent(lines[0]); - if (!firstContent.includes("/") && firstContent.endsWith("/")) { - baseRoot = safeJoin(rootDir, firstContent.slice(0, -1)); - lines = lines.slice(1); + if (firstContent.endsWith("/")) { + const token = firstContent.replace(/\/+$/, ""); + if (!token.includes("/")) { + baseRoot = safeJoin(rootDir, token); + lines = lines.slice(1); + } } } @@ -28,18 +43,23 @@ export function parseToPlan(input: string, opts: ParseOptions): ParseResult { const stack: string[] = []; stack[0] = baseRoot; + // garanta que o diretório baseRoot apareça no plano + addDir(baseRoot); + for (const raw of lines) { const { prefix, content: unclean } = splitPrefixContent(raw); const depth = computeDepth(prefix); const content0 = sanitizeContent(unclean); if (!content0) continue; + // caminho absoluto relativo ao baseRoot: "/foo/bar" if (content0.startsWith("/")) { const rel = content0.replace(/^\/+/, ""); addAny(rel, 0); continue; } + // possui separadores if (content0.includes("/")) { const isDir = content0.endsWith("/"); const rel = isDir ? content0.slice(0, -1) : content0; @@ -57,6 +77,7 @@ export function parseToPlan(input: string, opts: ParseOptions): ParseResult { continue; } + // token "nu" sem barra if (looksLikeDirToken(content0)) { const parent = stack[depth] ?? baseRoot; const abs = safeJoin(parent, content0.replace(/\/$/, "")); @@ -75,7 +96,7 @@ export function parseToPlan(input: string, opts: ParseOptions): ParseResult { function sanitizeContent(s: string): string { let out = s.trim(); out = stripBullets(out); - out = out.replace(/^(?:[│├└]?[─\-])\s*/, ""); + out = out.replace(/^(?:[│├└]?[─-])\s*/, ""); out = stripTrailingComments(out); out = out.trim(); out = out.replace(/\/\s+$/, "/"); @@ -84,7 +105,9 @@ export function parseToPlan(input: string, opts: ParseOptions): ParseResult { } function addAny(rel: string, depth: number) { - const isDir = rel.endsWith("/") || (!hasExtension(path.basename(rel)) && !isBareKnownFile(path.basename(rel))); + const isDir = + rel.endsWith("/") || + (!hasExtension(path.basename(rel)) && !isBareKnownFile(path.basename(rel))); const abs = safeJoin(baseRoot, rel.replace(/\/$/, "")); if (isDir) { addDir(abs); @@ -104,6 +127,7 @@ export function parseToPlan(input: string, opts: ParseOptions): ParseResult { plan.push({ kind: "dir", absPath: abs }); } } + function addFile(abs: string) { const key = `f:${abs}`; if (!seen.has(key)) { @@ -112,6 +136,13 @@ export function parseToPlan(input: string, opts: ParseOptions): ParseResult { } } - plan.sort((a, b) => a.kind === b.kind ? a.absPath.localeCompare(b.absPath) : (a.kind === "dir" ? -1 : 1)); + plan.sort((a, b) => + a.kind === b.kind + ? a.absPath.localeCompare(b.absPath) + : a.kind === "dir" + ? -1 + : 1 + ); + return { plan, baseRoot }; } From 5f71fef11e0ea7dba06985dbeaf83558b2af5029 Mon Sep 17 00:00:00 2001 From: mayconrfreitas Date: Mon, 29 Sep 2025 15:59:08 -0300 Subject: [PATCH 5/5] refactor parser.ts for improved clarity and functionality --- src/parser.ts | 59 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index 68dbda5..afa62a8 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,7 +1,6 @@ import path from "node:path"; import { PlanItem, ParseOptions } from "./types.js"; import { - computeDepth, hasExtension, isBareKnownFile, looksLikeDirToken, @@ -25,13 +24,13 @@ export function parseToPlan(input: string, opts: ParseOptions): ParseResult { .map((l) => l.replace(/\s+$/, "")) .filter((l) => l.trim().length > 0); - // inferRoot: primeira linha é algo como "my-project/" (apenas a barra de fechamento) + // infer base root from a first-line token like "project/" let baseRoot = rootDir; if (opts.inferRoot && lines.length > 0) { - const firstContent = sanitizeContent(lines[0]); - if (firstContent.endsWith("/")) { - const token = firstContent.replace(/\/+$/, ""); - if (!token.includes("/")) { + const first = sanitizeContent(lines[0]); + if (first.endsWith("/")) { + const token = first.replace(/\/+$/, ""); + if (token && !token.includes("/")) { baseRoot = safeJoin(rootDir, token); lines = lines.slice(1); } @@ -40,10 +39,12 @@ export function parseToPlan(input: string, opts: ParseOptions): ParseResult { const plan: PlanItem[] = []; const seen = new Set(); + + // directory stack by depth const stack: string[] = []; stack[0] = baseRoot; - // garanta que o diretório baseRoot apareça no plano + // ensure baseRoot is part of the plan addDir(baseRoot); for (const raw of lines) { @@ -52,14 +53,14 @@ export function parseToPlan(input: string, opts: ParseOptions): ParseResult { const content0 = sanitizeContent(unclean); if (!content0) continue; - // caminho absoluto relativo ao baseRoot: "/foo/bar" + // "/foo/bar" relative to baseRoot if (content0.startsWith("/")) { const rel = content0.replace(/^\/+/, ""); addAny(rel, 0); continue; } - // possui separadores + // token with slashes if (content0.includes("/")) { const isDir = content0.endsWith("/"); const rel = isDir ? content0.slice(0, -1) : content0; @@ -77,7 +78,7 @@ export function parseToPlan(input: string, opts: ParseOptions): ParseResult { continue; } - // token "nu" sem barra + // bare token if (looksLikeDirToken(content0)) { const parent = stack[depth] ?? baseRoot; const abs = safeJoin(parent, content0.replace(/\/$/, "")); @@ -96,7 +97,8 @@ export function parseToPlan(input: string, opts: ParseOptions): ParseResult { function sanitizeContent(s: string): string { let out = s.trim(); out = stripBullets(out); - out = out.replace(/^(?:[│├└]?[─-])\s*/, ""); + // drop a leading branch marker like "├─ " or "└─ " or "- " + out = out.replace(/^(?:[│├└]\s*)?[─-]\s*/u, ""); out = stripTrailingComments(out); out = out.trim(); out = out.replace(/\/\s+$/, "/"); @@ -104,6 +106,26 @@ export function parseToPlan(input: string, opts: ParseOptions): ParseResult { return out; } + function computeDepth(prefix: string): number { + // remove the trailing branch marker (├─ or └─) so it doesn't count as indent + let p = prefix.replace(/[├└]─\s*$/u, ""); + // if box pipes exist, depth equals number of pipes + const pipes = (p.match(/│/g) || []).length; + if (pipes > 0) return pipes; + // otherwise, count groups of two spaces + p = p.replace(/[├└─]/g, " ").replace(/\t/g, " "); + let groups = 0, acc = 0; + for (const ch of p) { + if (ch === " ") { + acc++; + if (acc === 2) { groups++; acc = 0; } + } else { + acc = 0; + } + } + return groups; + } + function addAny(rel: string, depth: number) { const isDir = rel.endsWith("/") || @@ -121,6 +143,21 @@ export function parseToPlan(input: string, opts: ParseOptions): ParseResult { } function addDir(abs: string) { + // include all ancestor directories under baseRoot + const rel = path.relative(baseRoot, abs); + if (rel && rel !== ".") { + const parts = rel.split(path.sep).filter(Boolean); + let cur = baseRoot; + for (const seg of parts) { + cur = safeJoin(cur, seg); + pushDir(cur); + } + } else { + pushDir(baseRoot); + } + } + + function pushDir(abs: string) { const key = `d:${abs}`; if (!seen.has(key)) { seen.add(key);