@dev-angsu/cli 1.0.4 → 1.0.6

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/bin/index.js CHANGED
@@ -11,6 +11,7 @@ import ora from "ora";
11
11
  import fs from "fs";
12
12
 
13
13
  import { run as review } from "../src/utils/engine.js";
14
+ import { copyCode } from "../src/commands/copy.js";
14
15
 
15
16
  const program = new Command();
16
17
 
@@ -84,5 +85,16 @@ program
84
85
  await review();
85
86
  });
86
87
 
88
+ // 4. Copy Code Command
89
+ program
90
+ .command("copycode")
91
+ .alias("cp")
92
+ .description("Copy codebase context to clipboard for LLMs")
93
+ .option("-o, --output <file>", "Output result to a file instead of clipboard")
94
+ .option("-d, --dry-run", "Dry run: only list files and stats")
95
+ .action(async (options) => {
96
+ await copyCode(process.cwd(), options);
97
+ });
98
+
87
99
  // 6. Parse Arguments
88
100
  program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dev-angsu/cli",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -18,9 +18,13 @@
18
18
  "@actions/core": "^2.0.1",
19
19
  "@actions/github": "^6.0.1",
20
20
  "chalk": "^5.6.2",
21
+ "clipboardy": "^4.0.0",
21
22
  "commander": "^14.0.2",
22
23
  "dotenv": "^17.2.3",
24
+ "fast-glob": "^3.3.3",
25
+ "ignore": "^5.3.2",
23
26
  "inquirer": "^13.1.0",
27
+ "isbinaryfile": "^5.0.7",
24
28
  "openai": "^6.15.0",
25
29
  "ora": "^9.0.0",
26
30
  "simple-git": "^3.30.0"
@@ -0,0 +1,87 @@
1
+ import { getValidFiles } from "../utils/file-reader.js";
2
+ import clipboardy from "clipboardy";
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import ora from "ora";
6
+ import chalk from "chalk";
7
+ import inquirer from "inquirer";
8
+
9
+ export async function copyCode(dir = process.cwd(), options = {}) {
10
+ const spinner = ora("Scanning directory...").start();
11
+
12
+ try {
13
+ const files = await getValidFiles(dir);
14
+
15
+ if (files.length === 0) {
16
+ spinner.fail("No valid files found to copy.");
17
+ return;
18
+ }
19
+
20
+ spinner.text = `Processing ${files.length} files...`;
21
+
22
+ let output = "";
23
+ let tokenCount = 0;
24
+
25
+ // Generate Tree Structure for context
26
+ const tree = files.join("\n");
27
+
28
+ for (const file of files) {
29
+ const content = fs.readFileSync(path.join(dir, file), "utf-8");
30
+ // Wrap in Markdown code blocks with filename
31
+ if (!options.dryRun) {
32
+ output += `\n\n# File: ${file}\n\`\`\`\n${content}\n\`\`\``;
33
+ }
34
+ tokenCount += content.length / 4; // Rough estimate (1 token ~= 4 chars)
35
+ }
36
+
37
+ if (options.dryRun) {
38
+ spinner.succeed(chalk.green("Dry run complete."));
39
+ console.log(chalk.bold("\nFile Structure:"));
40
+ console.log(tree);
41
+ } else {
42
+ // Final Payload
43
+ const finalOutput = `# Project Context\n\n## File Structure\n\`\`\`\n${tree}\n\`\`\`\n${output}`;
44
+
45
+ if (options.output) {
46
+ fs.writeFileSync(path.join(dir, options.output), finalOutput);
47
+ spinner.succeed(chalk.green(`Context saved to ${options.output} šŸ“`));
48
+ } else {
49
+ const LARGE_FILE_LIMIT = 500000; // ~500KB
50
+ if (finalOutput.length > LARGE_FILE_LIMIT) {
51
+ spinner.stop();
52
+ console.log(
53
+ chalk.yellow(
54
+ `\nāš ļø Warning: Output is large (${Math.round(
55
+ finalOutput.length / 1024
56
+ )}KB).`
57
+ )
58
+ );
59
+
60
+ const { proceed } = await inquirer.prompt([
61
+ {
62
+ type: "confirm",
63
+ name: "proceed",
64
+ message: "Copying this might freeze your clipboard. Proceed?",
65
+ default: false,
66
+ },
67
+ ]);
68
+
69
+ if (!proceed)
70
+ return console.log(
71
+ chalk.blue("Aborted. Try using --output <file> instead.")
72
+ );
73
+ spinner.start("Copying to clipboard...");
74
+ }
75
+
76
+ await clipboardy.write(finalOutput);
77
+ spinner.succeed(chalk.green("Context copied to clipboard! šŸ“‹"));
78
+ }
79
+ }
80
+ console.log(chalk.dim(`\nStats:`));
81
+ console.log(chalk.dim(`- Files: ${files.length}`));
82
+ console.log(chalk.dim(`- Est. Tokens: ~${Math.round(tokenCount)}`));
83
+ } catch (error) {
84
+ spinner.fail("Failed to copy context.");
85
+ console.error(error);
86
+ }
87
+ }
@@ -30,25 +30,42 @@ async function getPRDiff() {
30
30
  const octokit = github.getOctokit(token);
31
31
 
32
32
  const context = github.context;
33
- const prNumber = context.payload.pull_request?.number;
34
33
 
35
- if (!prNumber) {
36
- throw new Error(
37
- "āŒ No Pull Request found in context. Are you running this on push?"
38
- );
39
- }
34
+ if (context.payload.pull_request) {
35
+ const prNumber = context.payload.pull_request.number;
36
+ // Fetch the diff specifically for PR
37
+ const { data: diff } = await octokit.rest.pulls.get({
38
+ owner: context.repo.owner,
39
+ repo: context.repo.repo,
40
+ pull_number: prNumber,
41
+ mediaType: {
42
+ format: "diff", // Ask GitHub to return the raw diff text
43
+ },
44
+ });
45
+ return diff;
46
+ } else if (context.eventName === "push") {
47
+ const { before, after, repository } = context.payload;
48
+
49
+ let base = before;
50
+ // Handle new branch creation (before commit is zeroed out)
51
+ if (before === "0000000000000000000000000000000000000000") {
52
+ base = repository.default_branch;
53
+ }
40
54
 
41
- // Fetch the diff specifically
42
- const { data: diff } = await octokit.rest.pulls.get({
43
- owner: context.repo.owner,
44
- repo: context.repo.repo,
45
- pull_number: prNumber,
46
- mediaType: {
47
- format: "diff", // Ask GitHub to return the raw diff text
48
- },
49
- });
55
+ // Compare head commit against previous commit
56
+ const { data: diff } = await octokit.rest.repos.compareCommits({
57
+ owner: context.repo.owner,
58
+ repo: context.repo.repo,
59
+ base,
60
+ head: after,
61
+ mediaType: { format: "diff" },
62
+ });
63
+ return diff;
64
+ }
50
65
 
51
- return diff;
66
+ throw new Error(
67
+ "āŒ Unsupported event type. Only PRs and Pushes are supported."
68
+ );
52
69
  }
53
70
 
54
71
  async function generateReview(diff) {
@@ -120,13 +137,23 @@ export async function run() {
120
137
  const octokit = github.getOctokit(token);
121
138
  const context = github.context;
122
139
 
123
- await octokit.rest.issues.createComment({
124
- owner: context.repo.owner,
125
- repo: context.repo.repo,
126
- issue_number: context.payload.pull_request.number,
127
- body: review,
128
- });
129
- console.log("āœ… Review posted to GitHub PR!");
140
+ if (context.payload.pull_request) {
141
+ await octokit.rest.issues.createComment({
142
+ owner: context.repo.owner,
143
+ repo: context.repo.repo,
144
+ issue_number: context.payload.pull_request.number,
145
+ body: review,
146
+ });
147
+ console.log("āœ… Review posted to GitHub PR!");
148
+ } else if (context.eventName === "push") {
149
+ await octokit.rest.repos.createCommitComment({
150
+ owner: context.repo.owner,
151
+ repo: context.repo.repo,
152
+ commit_sha: context.payload.after,
153
+ body: review,
154
+ });
155
+ console.log("āœ… Review posted to Commit!");
156
+ }
130
157
  } else {
131
158
  // Locally, we just print it
132
159
  console.log("\n================ šŸ¤– AI CODE REVIEW ================ \n");
@@ -0,0 +1,58 @@
1
+ import glob from "fast-glob";
2
+ import ignore from "ignore";
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import { isBinaryFile } from "isbinaryfile";
6
+
7
+ export async function getValidFiles(dir) {
8
+ const ig = ignore();
9
+
10
+ // 1. Default Ignores (Standard noise)
11
+ ig.add([
12
+ ".git",
13
+ "node_modules",
14
+ "dist",
15
+ "build",
16
+ "coverage",
17
+ ".DS_Store",
18
+ "package-lock.json",
19
+ "yarn.lock",
20
+ "pnpm-lock.yaml",
21
+ ".env",
22
+ ".env.local",
23
+ "*.png",
24
+ "*.jpg",
25
+ "*.jpeg",
26
+ "*.gif",
27
+ "*.ico",
28
+ "*.svg", // Images
29
+ ]);
30
+
31
+ // 2. Load .gitignore if exists
32
+ const gitignorePath = path.join(dir, ".gitignore");
33
+ if (fs.existsSync(gitignorePath)) {
34
+ const gitignoreContent = fs.readFileSync(gitignorePath, "utf-8");
35
+ ig.add(gitignoreContent);
36
+ }
37
+
38
+ // 3. Scan Files (Fast Glob)
39
+ const allFiles = await glob("**/*", {
40
+ cwd: dir,
41
+ dot: true,
42
+ ignore: ["**/node_modules/**", "**/.git/**"], // Optimization: Hard ignore
43
+ onlyFiles: true,
44
+ });
45
+
46
+ // 4. Filter Results
47
+ const validFiles = [];
48
+ for (const file of allFiles) {
49
+ // Check .gitignore rules
50
+ if (ig.ignores(file)) continue;
51
+
52
+ // Check if Binary (prevents garbage text)
53
+ const isBinary = await isBinaryFile(path.join(dir, file));
54
+ if (!isBinary) validFiles.push(file);
55
+ }
56
+
57
+ return validFiles;
58
+ }