quackstack 1.0.7 ā 1.0.8
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/dist/lib/file-change-detector.js +109 -0
- package/dist/repl.js +55 -51
- package/package.json +1 -1
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { client } from "./database.js";
|
|
4
|
+
export async function detectFileChanges(rootDir, projectName) {
|
|
5
|
+
try {
|
|
6
|
+
const existingSnippets = await client.codeSnippet.findMany({
|
|
7
|
+
where: { projectName },
|
|
8
|
+
select: { filePath: true, updatedAt: true },
|
|
9
|
+
distinct: ['filePath'],
|
|
10
|
+
});
|
|
11
|
+
if (existingSnippets.length === 0) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const lastIndexTime = existingSnippets.reduce((latest, snippet) => {
|
|
15
|
+
return snippet.updatedAt > latest ? snippet.updatedAt : latest;
|
|
16
|
+
}, new Date(0));
|
|
17
|
+
const indexedFiles = new Set(existingSnippets.map(s => s.filePath));
|
|
18
|
+
const currentFiles = await scanDirectory(rootDir);
|
|
19
|
+
const currentFilesSet = new Set(currentFiles);
|
|
20
|
+
let newFiles = 0;
|
|
21
|
+
let modifiedFiles = 0;
|
|
22
|
+
let deletedFiles = 0;
|
|
23
|
+
for (const filePath of currentFiles) {
|
|
24
|
+
if (!indexedFiles.has(filePath)) {
|
|
25
|
+
newFiles++;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
try {
|
|
29
|
+
const stats = fs.statSync(filePath);
|
|
30
|
+
if (stats.mtime > lastIndexTime) {
|
|
31
|
+
modifiedFiles++;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
for (const indexedFile of indexedFiles) {
|
|
40
|
+
if (!currentFilesSet.has(indexedFile)) {
|
|
41
|
+
deletedFiles++;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const totalChanges = newFiles + modifiedFiles + deletedFiles;
|
|
45
|
+
return {
|
|
46
|
+
newFiles,
|
|
47
|
+
modifiedFiles,
|
|
48
|
+
deletedFiles,
|
|
49
|
+
totalChanges,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
console.error("Error detecting file changes:", error);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function scanDirectory(dir) {
|
|
58
|
+
const files = [];
|
|
59
|
+
const ignoreDirs = new Set([
|
|
60
|
+
'node_modules',
|
|
61
|
+
'.git',
|
|
62
|
+
'dist',
|
|
63
|
+
'build',
|
|
64
|
+
'.next',
|
|
65
|
+
'coverage',
|
|
66
|
+
'.vscode',
|
|
67
|
+
'.idea'
|
|
68
|
+
]);
|
|
69
|
+
function walk(currentPath) {
|
|
70
|
+
try {
|
|
71
|
+
const entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
74
|
+
if (entry.isDirectory()) {
|
|
75
|
+
if (!ignoreDirs.has(entry.name) && !entry.name.startsWith('.')) {
|
|
76
|
+
walk(fullPath);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else if (entry.isFile()) {
|
|
80
|
+
const ext = path.extname(entry.name);
|
|
81
|
+
const codeExtensions = [
|
|
82
|
+
'.js', '.ts', '.jsx', '.tsx', '.py', '.java', '.cpp', '.c',
|
|
83
|
+
'.go', '.rs', '.rb', '.php', '.cs', '.swift', '.kt', '.scala'
|
|
84
|
+
];
|
|
85
|
+
if (codeExtensions.includes(ext)) {
|
|
86
|
+
files.push(fullPath);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
walk(dir);
|
|
95
|
+
return files;
|
|
96
|
+
}
|
|
97
|
+
export function formatChangeMessage(stats) {
|
|
98
|
+
const parts = [];
|
|
99
|
+
if (stats.newFiles > 0) {
|
|
100
|
+
parts.push(`${stats.newFiles} new file${stats.newFiles > 1 ? 's' : ''}`);
|
|
101
|
+
}
|
|
102
|
+
if (stats.modifiedFiles > 0) {
|
|
103
|
+
parts.push(`${stats.modifiedFiles} modified file${stats.modifiedFiles > 1 ? 's' : ''}`);
|
|
104
|
+
}
|
|
105
|
+
if (stats.deletedFiles > 0) {
|
|
106
|
+
parts.push(`${stats.deletedFiles} deleted file${stats.deletedFiles > 1 ? 's' : ''}`);
|
|
107
|
+
}
|
|
108
|
+
return parts.join(', ');
|
|
109
|
+
}
|
package/dist/repl.js
CHANGED
|
@@ -1,79 +1,83 @@
|
|
|
1
1
|
import readline from "readline";
|
|
2
2
|
import chalk from "chalk";
|
|
3
|
-
import { ingest } from "./commands/ingest.js";
|
|
4
3
|
import { search } from "./commands/search.js";
|
|
4
|
+
import { ingest } from "./commands/ingest.js";
|
|
5
5
|
import { client } from "./lib/database.js";
|
|
6
6
|
import path from "path";
|
|
7
|
+
import { detectFileChanges, formatChangeMessage } from "./lib/file-change-detector.js";
|
|
7
8
|
const PROJECT_NAME = path.basename(process.cwd());
|
|
8
|
-
async function
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
export async function startREPL(forceReindex = false) {
|
|
10
|
+
console.log(chalk.cyan("\nš” Tip: Press Ctrl+C to exit\n"));
|
|
11
|
+
if (!forceReindex) {
|
|
12
|
+
const changes = await detectFileChanges(process.cwd(), PROJECT_NAME);
|
|
13
|
+
if (changes && changes.totalChanges > 0) {
|
|
14
|
+
console.log(chalk.yellow(`\nā ļø Detected ${changes.totalChanges} file change${changes.totalChanges > 1 ? 's' : ''} since last index:`));
|
|
15
|
+
console.log(chalk.yellow(` ${formatChangeMessage(changes)}`));
|
|
16
|
+
console.log(chalk.yellow(` Run 'quack --reindex' for best results.\n`));
|
|
17
|
+
const shouldReindex = await promptUser(chalk.yellow("Would you like to reindex now? (y/n) > "));
|
|
18
|
+
if (shouldReindex.toLowerCase() === 'y') {
|
|
19
|
+
forceReindex = true;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const existingCount = await client.codeSnippet.count({
|
|
24
|
+
where: { projectName: PROJECT_NAME },
|
|
11
25
|
});
|
|
12
|
-
if (
|
|
13
|
-
if (forceReindex
|
|
14
|
-
console.log("šļø Clearing old index...");
|
|
26
|
+
if (existingCount === 0 || forceReindex) {
|
|
27
|
+
if (forceReindex) {
|
|
28
|
+
console.log(chalk.gray("šļø Clearing old index..."));
|
|
15
29
|
await client.codeSnippet.deleteMany({
|
|
16
|
-
where: { projectName: PROJECT_NAME }
|
|
30
|
+
where: { projectName: PROJECT_NAME },
|
|
17
31
|
});
|
|
18
32
|
}
|
|
19
|
-
console.log("š Indexing your codebase (this may take a moment)...");
|
|
33
|
+
console.log(chalk.gray("š Indexing your codebase (this may take a moment)..."));
|
|
20
34
|
await ingest(process.cwd(), PROJECT_NAME, true);
|
|
21
|
-
console.log("ā
Indexing complete
|
|
35
|
+
console.log(chalk.green("ā
Indexing complete!"));
|
|
22
36
|
}
|
|
23
|
-
}
|
|
24
|
-
function stripMarkdown(text) {
|
|
25
|
-
return text
|
|
26
|
-
.replace(/\*\*(.+?)\*\*/g, chalk.bold('$1'))
|
|
27
|
-
.replace(/\*(.+?)\*/g, chalk.italic('$1'))
|
|
28
|
-
.replace(/`(.+?)`/g, chalk.cyan('$1'))
|
|
29
|
-
.replace(/^#{1,6}\s+(.+)$/gm, chalk.bold.blue('$1'));
|
|
30
|
-
}
|
|
31
|
-
export async function startREPL(forceReindex = false) {
|
|
32
|
-
await ensureIngested(forceReindex);
|
|
33
|
-
console.log("š” Tip: Press Ctrl+C to exit\n");
|
|
34
37
|
const rl = readline.createInterface({
|
|
35
38
|
input: process.stdin,
|
|
36
39
|
output: process.stdout,
|
|
37
|
-
|
|
40
|
+
prompt: chalk.yellow("š„ Quack! How can I help? > "),
|
|
38
41
|
});
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (waitingForDetails) {
|
|
44
|
-
waitingForDetails = false;
|
|
45
|
-
if (trimmed === "y" || trimmed === "yes") {
|
|
46
|
-
console.log("\nš Relevant Code:\n");
|
|
47
|
-
currentSources.slice(0, 3).forEach((r, i) => {
|
|
48
|
-
console.log(chalk.dim(`[${i + 1}] ${r.filePath} (relevance: ${(r.score * 100).toFixed(1)}%)`));
|
|
49
|
-
console.log(chalk.gray(r.content));
|
|
50
|
-
console.log(chalk.dim("---\n"));
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
console.log();
|
|
54
|
-
rl.prompt();
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
if (!trimmed) {
|
|
42
|
+
rl.prompt();
|
|
43
|
+
rl.on("line", async (line) => {
|
|
44
|
+
const query = line.trim();
|
|
45
|
+
if (!query) {
|
|
58
46
|
rl.prompt();
|
|
59
47
|
return;
|
|
60
48
|
}
|
|
61
49
|
try {
|
|
62
|
-
const { answer, sources } = await search(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
50
|
+
const { answer, sources } = await search(query, PROJECT_NAME);
|
|
51
|
+
console.log(chalk.white(`\n${answer}\n`));
|
|
52
|
+
const showDetails = await promptUser(chalk.cyan("š” Want more details? (y/n) > "));
|
|
53
|
+
if (showDetails.toLowerCase() === "y") {
|
|
54
|
+
console.log(chalk.blue("\nš Relevant Code:\n"));
|
|
55
|
+
sources.forEach((src, i) => {
|
|
56
|
+
console.log(chalk.gray(`[${i + 1}] ${src.filePath} (relevance: ${(src.score * 100).toFixed(1)}%)`));
|
|
57
|
+
console.log(chalk.white(src.content));
|
|
58
|
+
console.log(chalk.gray("\n---\n"));
|
|
59
|
+
});
|
|
60
|
+
}
|
|
67
61
|
}
|
|
68
62
|
catch (error) {
|
|
69
|
-
console.error(chalk.red(
|
|
70
|
-
rl.prompt();
|
|
63
|
+
console.error(chalk.red(`\nError: ${error.message}\n`));
|
|
71
64
|
}
|
|
65
|
+
rl.prompt();
|
|
72
66
|
});
|
|
73
67
|
rl.on("close", () => {
|
|
74
|
-
console.log("\nš Happy coding!");
|
|
68
|
+
console.log(chalk.gray("\nš Happy coding!"));
|
|
75
69
|
process.exit(0);
|
|
76
70
|
});
|
|
77
|
-
|
|
78
|
-
|
|
71
|
+
}
|
|
72
|
+
function promptUser(question) {
|
|
73
|
+
const rl = readline.createInterface({
|
|
74
|
+
input: process.stdin,
|
|
75
|
+
output: process.stdout,
|
|
76
|
+
});
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
rl.question(question, (answer) => {
|
|
79
|
+
rl.close();
|
|
80
|
+
resolve(answer);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
79
83
|
}
|