Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 84 additions & 12 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,37 @@ function printList(resolved: ReturnType<typeof resolveComponents>): void {
process.stdout.write(`\n`);
}

export function printResultStatus(
name: string,
result: { success: boolean; failed: boolean; dryRun: boolean; skipped?: boolean }
): void {
if (result.dryRun) {
process.stdout.write(` ${color("~", "yellow")} ${name}\n`);
} else if (result.failed) {
process.stdout.write(` ${color("✗", "red")} ${name}\n`);
} else if (result.skipped) {
process.stdout.write(` ${color("-", "dim")} ${name}\n`);
} else {
process.stdout.write(` ${color("✓", "green")} ${name}\n`);
}
}

export function printLinkStatus(name: string, results: Array<{ success: boolean; failed: boolean; dryRun: boolean; skipped?: boolean }>): void {
if (results.length === 0) return;
const anyFailed = results.some((r) => r.failed);
const allDryRun = results.every((r) => r.dryRun);
const allSkipped = results.every((r) => r.skipped || r.dryRun);
if (allDryRun) {
process.stdout.write(` ${color("~", "yellow")} ${name}\n`);
} else if (anyFailed) {
process.stdout.write(` ${color("✗", "red")} ${name}\n`);
} else if (allSkipped) {
process.stdout.write(` ${color("-", "dim")} ${name}\n`);
} else {
process.stdout.write(` ${color("✓", "green")} ${name}\n`);
}
}

export async function main(): Promise<void> {
const args = parseArgs(process.argv);

Expand Down Expand Up @@ -96,7 +127,7 @@ export async function main(): Promise<void> {
}

const isTty = process.stdin.isTTY ?? false;
const options = { dryRun: args.dryRun, verbose: args.verbose, interactive: isTty && args.mode === "direct" };
const options = { dryRun: args.dryRun, verbose: args.verbose, interactive: isTty };

if (args.mode === "interactive") {
if (!isTty) {
Expand All @@ -109,6 +140,7 @@ export async function main(): Promise<void> {
}

const action = args.interactiveAction;
const failures: string[] = [];

for (const item of selected) {
if (item.unavailable) continue;
Expand All @@ -118,45 +150,71 @@ export async function main(): Promise<void> {
if (!action || action === "install") {
if (comp.installCommand) {
const result = await installComponent(comp.name, comp.installCommand, options, comp.availableManager || undefined);
if (result.failed) {
process.stderr.write(` ${color("[error]", "red")} ${comp.name}: install failed\n`);
}
if (result.failed && !result.dryRun) failures.push(comp.name);
printResultStatus(comp.name, result);
} else {
printResultStatus(comp.name, { success: false, failed: false, dryRun: false, skipped: true });
}
}

if (!action || action === "install") {
if (comp.hasDefaults && os === "mac") {
await importDefaults(comp.defaults, process.cwd(), options);
const results = await importDefaults(comp.defaults, process.cwd(), options);
for (const r of results) {
if (r.failed && !r.dryRun) failures.push(r.domain);
printResultStatus(r.domain, r);
}
}
}

if (!action || action === "link") {
if (comp.hasLinks) {
createLinks(comp.name, comp.link, process.cwd(), options);
const results = createLinks(comp.name, comp.link, process.cwd(), options);
const anyFailed = results.some((r) => r.failed && !r.dryRun);
if (anyFailed) failures.push(comp.name);
printLinkStatus(comp.name, results);
} else {
printResultStatus(comp.name, { success: false, failed: false, dryRun: false, skipped: true });
}
}

if (!action || action === "postinstall") {
if (comp.postinstall) {
await runPostInstall(comp.name, comp.postinstall, options);
const result = await runPostInstall(comp.name, comp.postinstall, options);
if (result.failed && !result.dryRun) failures.push(comp.name);
printResultStatus(comp.name, result);
} else {
printResultStatus(comp.name, { success: false, failed: false, dryRun: false, skipped: true });
}
}

if (!action || action === "postlink") {
if (comp.postlink) {
await runPostLink(comp.name, comp.postlink, options);
const result = await runPostLink(comp.name, comp.postlink, options);
if (result.failed && !result.dryRun) failures.push(comp.name);
printResultStatus(comp.name, result);
} else {
printResultStatus(comp.name, { success: false, failed: false, dryRun: false, skipped: true });
}
}

if (action === "uninstall") {
const uninstallCmd = Object.entries(comp.uninstall)[0];
if (uninstallCmd) {
const [, cmd] = uninstallCmd;
await uninstallComponent(comp.name, cmd, options);
const result = await uninstallComponent(comp.name, cmd, options);
if (result.failed && !result.dryRun) failures.push(comp.name);
printResultStatus(comp.name, result);
} else {
printResultStatus(comp.name, { success: false, failed: false, dryRun: false, skipped: true });
}
}
}

if (failures.length > 0) {
process.stderr.write(`\n${color(` ${failures.length} failure(s)`, "red")}\n`);
}
process.stdout.write(`\n${color(" Done.", "green")}\n`);
return;
}

Expand Down Expand Up @@ -197,11 +255,13 @@ export async function main(): Promise<void> {
const uninstallCmd = Object.entries(comp.uninstall)[0];
if (!uninstallCmd) {
process.stdout.write(` ${color("[skip]", "dim")} ${name}: no uninstall command\n`);
printResultStatus(name, { success: false, failed: false, dryRun: false, skipped: true });
continue;
}
const [, cmd] = uninstallCmd;
const result = await uninstallComponent(name, cmd, options);
if (result.failed && !result.dryRun) failures.push(name);
printResultStatus(name, result);
}
}

Expand All @@ -216,6 +276,9 @@ export async function main(): Promise<void> {
if (comp.installCommand) {
const result = await installComponent(name, comp.installCommand, options, comp.availableManager || undefined);
if (result.failed && !result.dryRun) failures.push(name);
printResultStatus(name, result);
} else {
printResultStatus(name, { success: false, failed: false, dryRun: false, skipped: true });
}
}
}
Expand All @@ -230,6 +293,7 @@ export async function main(): Promise<void> {
const results = await importDefaults(allDefaults, process.cwd(), options);
for (const r of results) {
if (r.failed && !r.dryRun) failures.push(r.domain);
printResultStatus(r.domain, r);
}
}

Expand All @@ -243,6 +307,7 @@ export async function main(): Promise<void> {
const results = await exportDefaults(allDefaults, process.cwd(), options);
for (const r of results) {
if (r.failed && !r.dryRun) failures.push(r.domain);
printResultStatus(r.domain, r);
}
}

Expand All @@ -259,6 +324,9 @@ export async function main(): Promise<void> {
for (const r of results) {
if (r.failed && !r.dryRun) failures.push(name);
}
Comment on lines 324 to 326

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Direct mode link handling inflates failure count.

failures.push(name) is called once per failed LinkResult, so a component with multiple failed links gets counted multiple times. Interactive mode (lines 173–174) correctly uses results.some() to push only once.

🐛 Proposed fix: use `some()` like interactive mode
        if (comp.hasLinks) {
          const results = createLinks(name, comp.link, process.cwd(), options);
-         for (const r of results) {
-           if (r.failed && !r.dryRun) failures.push(name);
-         }
+         const anyFailed = results.some((r) => r.failed && !r.dryRun);
+         if (anyFailed) failures.push(name);
          printLinkStatus(name, results);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const r of results) {
if (r.failed && !r.dryRun) failures.push(name);
}
const anyFailed = results.some((r) => r.failed && !r.dryRun);
if (anyFailed) failures.push(name);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/index.ts` around lines 324 - 326, In the direct-mode results handling
loop, avoid adding the same component multiple times when several links fail.
Replace the per-result push logic in the `for (const r of results)` block with a
single `results.some(r => r.failed && !r.dryRun)` check, pushing `name` only
when at least one qualifying failure exists, matching the interactive-mode
behavior.

printLinkStatus(name, results);
} else {
printResultStatus(name, { success: false, failed: false, dryRun: false, skipped: true });
}
}
}
Expand All @@ -274,6 +342,9 @@ export async function main(): Promise<void> {
if (comp.postinstall) {
const result = await runPostInstall(name, comp.postinstall, options);
if (result.failed && !result.dryRun) failures.push(name);
printResultStatus(name, result);
} else {
printResultStatus(name, { success: false, failed: false, dryRun: false, skipped: true });
}
}
}
Expand All @@ -289,6 +360,9 @@ export async function main(): Promise<void> {
if (comp.postlink) {
const result = await runPostLink(name, comp.postlink, options);
if (result.failed && !result.dryRun) failures.push(name);
printResultStatus(name, result);
} else {
printResultStatus(name, { success: false, failed: false, dryRun: false, skipped: true });
}
}
}
Expand All @@ -298,9 +372,7 @@ export async function main(): Promise<void> {
process.exit(1);
}

if (options.verbose) {
process.stdout.write(`\n${color(" Done.", "green")}\n`);
}
process.stdout.write(`\n${color(" Done.", "green")}\n`);
}
}

Expand Down
8 changes: 4 additions & 4 deletions src/installer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,8 @@ export async function installComponent(
result = await Bun.$`${{ raw: command }} < /dev/null`.nothrow().quiet();
}
if (result.exitCode !== 0) {
if (options.verbose) {
const stderr = result.stderr.toString().trim();
if (stderr) process.stderr.write(` ${color("[error]", "red")} ${name}: ${stderr}\n`);
}
const stderr = result.stderr.toString().trim();
if (stderr) process.stderr.write(` ${color("[error]", "red")} ${name}: ${stderr}\n`);
return { ...base, failed: true };
}
} catch (e: any) {
Expand Down Expand Up @@ -95,6 +93,8 @@ export async function uninstallComponent(
result = await Bun.$`${{ raw: command }} < /dev/null`.nothrow().quiet();
}
if (result.exitCode !== 0) {
const stderr = result.stderr.toString().trim();
if (stderr) process.stderr.write(` ${color("[error]", "red")} ${name}: ${stderr}\n`);
return { ...base, failed: true };
}
} catch (e: any) {
Expand Down
Loading
Loading