Skip to content

Commit 1ae6094

Browse files
authored
Improve --prompt clipboard output for agent fixes (#2)
* feat: copy verbose diagnostics output to clipboard prompt * refactor: rename prompt copy helpers and update prompt docs * feat: copy verbose diagnostics output to clipboard prompt * refactor: rename prompt copy helpers and update prompt docs
1 parent 8894a54 commit 1ae6094

File tree

7 files changed

+162
-17
lines changed

7 files changed

+162
-17
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ Options:
4646
--score output only the score
4747
-y, --yes skip prompts, scan all workspace projects
4848
--project <name> select workspace project (comma-separated for multiple)
49+
--fix open Ami to auto-fix all issues
50+
--prompt copy latest scan output to clipboard
4951
-h, --help display help for command
5052
```
5153

packages/react-doctor/src/cli.ts

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import { execSync } from "node:child_process";
22
import path from "node:path";
33
import { Command } from "commander";
4+
import { SEPARATOR_LENGTH_CHARS } from "./constants.js";
45
import type { ScanOptions } from "./types.js";
56
import { handleError } from "./utils/handle-error.js";
67
import { highlighter } from "./utils/highlighter.js";
7-
import { logger } from "./utils/logger.js";
8+
import { logger, startLoggerCapture, stopLoggerCapture } from "./utils/logger.js";
89
import { scan } from "./scan.js";
910
import { selectProjects } from "./utils/select-projects.js";
1011
import { prompts } from "./utils/prompts.js";
1112
import { maybePromptSkillInstall } from "./utils/skill-prompt.js";
1213
import { maybeInstallGlobally } from "./utils/global-install.js";
14+
import { copyToClipboard } from "./utils/copy-to-clipboard.js";
1315

1416
const VERSION = process.env.VERSION ?? "0.0.0";
1517

@@ -19,6 +21,7 @@ interface CliFlags {
1921
verbose: boolean;
2022
score: boolean;
2123
fix: boolean;
24+
prompt: boolean;
2225
yes: boolean;
2326
project?: string;
2427
}
@@ -38,10 +41,17 @@ const program = new Command()
3841
.option("-y, --yes", "skip prompts, scan all workspace projects")
3942
.option("--project <name>", "select workspace project (comma-separated for multiple)")
4043
.option("--fix", "open Ami to auto-fix all issues")
44+
.option("--prompt", "copy latest scan output to clipboard")
4145
.action(async (directory: string, flags: CliFlags) => {
46+
const isScoreOnly = flags.score && !flags.prompt;
47+
const shouldCopyPromptOutput = flags.prompt;
48+
49+
if (shouldCopyPromptOutput) {
50+
startLoggerCapture();
51+
}
52+
4253
try {
4354
const resolvedDirectory = path.resolve(directory);
44-
const isScoreOnly = flags.score;
4555

4656
if (!isScoreOnly) {
4757
logger.log(`react-doctor v${VERSION}`);
@@ -51,7 +61,7 @@ const program = new Command()
5161
const scanOptions: ScanOptions = {
5262
lint: flags.lint,
5363
deadCode: flags.deadCode,
54-
verbose: Boolean(flags.verbose),
64+
verbose: flags.prompt || Boolean(flags.verbose),
5565
scoreOnly: isScoreOnly,
5666
};
5767

@@ -87,14 +97,19 @@ const program = new Command()
8797
openAmiToFix(resolvedDirectory);
8898
}
8999

90-
if (!isScoreOnly) {
100+
if (!isScoreOnly && !flags.prompt) {
91101
await maybePromptSkillInstall(shouldSkipPrompts);
92102
if (!shouldSkipPrompts && !flags.fix) {
93103
await maybePromptAmiFix(resolvedDirectory);
94104
}
95105
}
96106
} catch (error) {
97-
handleError(error);
107+
handleError(error, { shouldExit: !shouldCopyPromptOutput });
108+
} finally {
109+
if (shouldCopyPromptOutput) {
110+
const capturedOutput = stopLoggerCapture();
111+
copyPromptToClipboard(capturedOutput, !isScoreOnly);
112+
}
98113
}
99114
})
100115
.addHelpText(
@@ -106,8 +121,10 @@ ${highlighter.dim("Learn more:")}
106121
);
107122

108123
const AMI_INSTALL_URL = "https://ami.dev/install.sh";
109-
const AMI_FIX_PROMPT =
110-
"Run npx -y react-doctor@latest . --verbose, read every diagnostic, then fix all issues one by one. After fixing, re-run react-doctor to verify the score improved.";
124+
const FIX_PROMPT =
125+
"Fix all issues reported in the react-doctor diagnostics below, one by one. After applying fixes, run `react-dcotor` again to verify the results improved.";
126+
const REACT_DOCTOR_OUTPUT_LABEL = "react-doctor output";
127+
const SCAN_SUMMARY_SEPARATOR = "─".repeat(SEPARATOR_LENGTH_CHARS);
111128

112129
const isAmiInstalled = (): boolean => {
113130
try {
@@ -140,7 +157,7 @@ const openAmiToFix = (directory: string): void => {
140157
logger.log("Opening Ami to fix react-doctor issues...");
141158

142159
const encodedDirectory = encodeURIComponent(resolvedDirectory);
143-
const encodedPrompt = encodeURIComponent(AMI_FIX_PROMPT);
160+
const encodedPrompt = encodeURIComponent(FIX_PROMPT);
144161
const deeplink = `ami://open-project?cwd=${encodedDirectory}&prompt=${encodedPrompt}&mode=agent`;
145162

146163
try {
@@ -153,6 +170,35 @@ const openAmiToFix = (directory: string): void => {
153170
}
154171
};
155172

173+
const buildPromptWithOutput = (reactDoctorOutput: string): string => {
174+
const summaryStartIndex = reactDoctorOutput.indexOf(SCAN_SUMMARY_SEPARATOR);
175+
const diagnosticsOutput =
176+
summaryStartIndex === -1
177+
? reactDoctorOutput
178+
: reactDoctorOutput.slice(0, summaryStartIndex).trimEnd();
179+
const normalizedReactDoctorOutput = diagnosticsOutput.trim();
180+
const outputContent =
181+
normalizedReactDoctorOutput.length > 0 ? normalizedReactDoctorOutput : "No output captured.";
182+
return `${FIX_PROMPT}\n\n${REACT_DOCTOR_OUTPUT_LABEL}:\n\`\`\`\n${outputContent}\n\`\`\``;
183+
};
184+
185+
const copyPromptToClipboard = (reactDoctorOutput: string, shouldLogResult: boolean): void => {
186+
const promptWithOutput = buildPromptWithOutput(reactDoctorOutput);
187+
const didCopyPromptToClipboard = copyToClipboard(promptWithOutput);
188+
189+
if (!shouldLogResult) {
190+
return;
191+
}
192+
193+
if (didCopyPromptToClipboard) {
194+
logger.success("Copied latest scan output to clipboard");
195+
return;
196+
}
197+
198+
logger.warn("Could not copy prompt to clipboard automatically. Use this prompt:");
199+
logger.info(promptWithOutput);
200+
};
201+
156202
const maybePromptAmiFix = async (directory: string): Promise<void> => {
157203
logger.break();
158204
logger.log(`Fix these issues with ${highlighter.info("Ami")}?`);

packages/react-doctor/src/types.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,20 @@ export interface ScanOptions {
8888
scoreOnly: boolean;
8989
}
9090

91+
export interface ClipboardCommand {
92+
command: string;
93+
args: string[];
94+
}
95+
96+
export interface LoggerCaptureState {
97+
isEnabled: boolean;
98+
lines: string[];
99+
}
100+
101+
export interface HandleErrorOptions {
102+
shouldExit: boolean;
103+
}
104+
91105
export interface WorkspacePackage {
92106
name: string;
93107
directory: string;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { spawnSync } from "node:child_process";
2+
import type { ClipboardCommand } from "../types.js";
3+
4+
const getClipboardCommands = (): ClipboardCommand[] => {
5+
if (process.platform === "darwin") {
6+
return [{ command: "pbcopy", args: [] }];
7+
}
8+
9+
if (process.platform === "win32") {
10+
return [{ command: "clip", args: [] }];
11+
}
12+
13+
return [
14+
{ command: "wl-copy", args: [] },
15+
{ command: "xclip", args: ["-selection", "clipboard"] },
16+
{ command: "xsel", args: ["--clipboard", "--input"] },
17+
];
18+
};
19+
20+
export const copyToClipboard = (text: string): boolean => {
21+
const clipboardCommands = getClipboardCommands();
22+
23+
for (const clipboardCommand of clipboardCommands) {
24+
const clipboardProcess = spawnSync(clipboardCommand.command, clipboardCommand.args, {
25+
input: text,
26+
stdio: ["pipe", "ignore", "ignore"],
27+
encoding: "utf8",
28+
});
29+
30+
if (clipboardProcess.status === 0) {
31+
return true;
32+
}
33+
}
34+
35+
return false;
36+
};
Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import { logger } from "./logger.js";
2+
import type { HandleErrorOptions } from "../types.js";
23

3-
export const handleError = (error: unknown): void => {
4+
const DEFAULT_HANDLE_ERROR_OPTIONS: HandleErrorOptions = {
5+
shouldExit: true,
6+
};
7+
8+
export const handleError = (
9+
error: unknown,
10+
options: HandleErrorOptions = DEFAULT_HANDLE_ERROR_OPTIONS,
11+
): void => {
412
logger.break();
513
logger.error("Something went wrong. Please check the error below for more details.");
614
logger.error("If the problem persists, please open an issue on GitHub.");
@@ -9,5 +17,8 @@ export const handleError = (error: unknown): void => {
917
logger.error(error.message);
1018
}
1119
logger.break();
12-
process.exit(1);
20+
if (options.shouldExit) {
21+
process.exit(1);
22+
}
23+
process.exitCode = 1;
1324
};
Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,57 @@
11
import { highlighter } from "./highlighter.js";
2+
import { stripAnsi } from "./strip-ansi.js";
3+
import type { LoggerCaptureState } from "../types.js";
4+
5+
const loggerCaptureState: LoggerCaptureState = {
6+
isEnabled: false,
7+
lines: [],
8+
};
9+
10+
const captureLogLine = (text: string): void => {
11+
if (!loggerCaptureState.isEnabled) {
12+
return;
13+
}
14+
15+
loggerCaptureState.lines.push(stripAnsi(text));
16+
};
17+
18+
const writeLogLine = (text: string): void => {
19+
console.log(text);
20+
captureLogLine(text);
21+
};
22+
23+
export const startLoggerCapture = (): void => {
24+
loggerCaptureState.isEnabled = true;
25+
loggerCaptureState.lines = [];
26+
};
27+
28+
export const stopLoggerCapture = (): string => {
29+
const capturedOutput = loggerCaptureState.lines.join("\n");
30+
loggerCaptureState.isEnabled = false;
31+
loggerCaptureState.lines = [];
32+
return capturedOutput;
33+
};
234

335
export const logger = {
436
error(...args: unknown[]) {
5-
console.log(highlighter.error(args.join(" ")));
37+
writeLogLine(highlighter.error(args.join(" ")));
638
},
739
warn(...args: unknown[]) {
8-
console.log(highlighter.warn(args.join(" ")));
40+
writeLogLine(highlighter.warn(args.join(" ")));
941
},
1042
info(...args: unknown[]) {
11-
console.log(highlighter.info(args.join(" ")));
43+
writeLogLine(highlighter.info(args.join(" ")));
1244
},
1345
success(...args: unknown[]) {
14-
console.log(highlighter.success(args.join(" ")));
46+
writeLogLine(highlighter.success(args.join(" ")));
1547
},
1648
dim(...args: unknown[]) {
17-
console.log(highlighter.dim(args.join(" ")));
49+
writeLogLine(highlighter.dim(args.join(" ")));
1850
},
1951
log(...args: unknown[]) {
20-
console.log(args.join(" "));
52+
writeLogLine(args.join(" "));
2153
},
2254
break() {
23-
console.log("");
55+
writeLogLine("");
2456
},
2557
};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
const ANSI_ESCAPE_SEQUENCE = String.raw`\u001B\[[0-9;]*m`;
2+
const ANSI_ESCAPE_PATTERN = new RegExp(ANSI_ESCAPE_SEQUENCE, "g");
3+
4+
export const stripAnsi = (text: string): string => text.replace(ANSI_ESCAPE_PATTERN, "");

0 commit comments

Comments
 (0)