johankit 0.0.2 → 0.0.4
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/README.md +0 -5
- package/Readme.md +56 -0
- package/dist/cli/commands/copy.js +3 -4
- package/dist/cli/commands/paste.js +19 -10
- package/dist/cli/commands/prompt.js +40 -8
- package/dist/cli/commands/sync.js +10 -6
- package/dist/cli/commands/three.js +106 -0
- package/dist/index.js +52 -34
- package/dist/services/JohankitService.js +59 -0
- package/dist/utils/cleanCodeBlock.js +12 -0
- package/dist/utils/createAsciiTree.js +46 -0
- package/johankit.yaml +2 -0
- package/package.json +5 -2
- package/src/cli/commands/copy.ts +3 -4
- package/src/cli/commands/paste.ts +16 -16
- package/src/cli/commands/prompt.ts +47 -9
- package/src/cli/commands/sync.ts +8 -6
- package/src/cli/commands/three.ts +117 -0
- package/src/core/scan.ts +1 -1
- package/src/index.ts +54 -37
- package/src/services/JohankitService.ts +70 -0
- package/src/types.ts +45 -0
- package/src/utils/cleanCodeBlock.ts +13 -0
- package/src/utils/createAsciiTree.ts +53 -0
- package/tsconfig.json +4 -0
- package/dist/tests/cli/commands/copy.test.js +0 -47
- package/dist/tests/cli/commands/paste.test.js +0 -41
- package/dist/tests/cli/commands/prompt.test.js +0 -37
- package/dist/tests/cli/commands/sync.test.js +0 -47
- package/dist/tests/core/clipboard.test.js +0 -20
- package/dist/tests/core/config.test.js +0 -23
- package/dist/tests/core/diff.test.js +0 -24
- package/dist/tests/core/git.test.js +0 -11
- package/dist/tests/core/scan.test.js +0 -16
- package/dist/tests/core/schema.test.js +0 -13
- package/dist/tests/core/validation.test.js +0 -13
- package/dist/tests/core/write.test.js +0 -41
- package/package-lock.json +0 -250
- package/src/tests/cli/commands/copy.test.ts +0 -26
- package/src/tests/cli/commands/paste.test.ts +0 -19
- package/src/tests/cli/commands/prompt.test.ts +0 -14
- package/src/tests/cli/commands/sync.test.ts +0 -26
- package/src/tests/core/clipboard.test.ts +0 -21
- package/src/tests/core/config.test.ts +0 -21
- package/src/tests/core/diff.test.ts +0 -22
- package/src/tests/core/git.test.ts +0 -11
- package/src/tests/core/scan.test.ts +0 -13
- package/src/tests/core/schema.test.ts +0 -13
- package/src/tests/core/validation.test.ts +0 -13
- package/src/tests/core/write.test.ts +0 -15
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// src/cli/commands/paste.ts
|
|
2
2
|
import { writeFiles } from "../../core/write";
|
|
3
3
|
import { readClipboard } from "../../core/clipboard";
|
|
4
|
+
import cleanCodeBlock from "../../utils/cleanCodeBlock";
|
|
5
|
+
import { applyDiff } from "../../core/diff";
|
|
6
|
+
import { validatePatches } from "../../core/validation";
|
|
4
7
|
|
|
5
8
|
export async function paste(dir: string) {
|
|
6
9
|
try {
|
|
@@ -10,28 +13,25 @@ export async function paste(dir: string) {
|
|
|
10
13
|
throw new Error("Clipboard empty or inaccessible");
|
|
11
14
|
}
|
|
12
15
|
|
|
13
|
-
let
|
|
16
|
+
let parsed;
|
|
14
17
|
try {
|
|
15
|
-
const
|
|
16
|
-
|
|
18
|
+
const { cleaned } = cleanCodeBlock(content);
|
|
19
|
+
parsed = JSON.parse(cleaned);
|
|
17
20
|
} catch (e) {
|
|
18
21
|
throw new Error("Clipboard content is not valid JSON");
|
|
19
22
|
}
|
|
20
23
|
|
|
21
|
-
if (
|
|
22
|
-
|
|
24
|
+
if (Array.isArray(parsed) && parsed.every(f => f.path && f.content)) {
|
|
25
|
+
// Caso seja snapshot
|
|
26
|
+
writeFiles(dir, parsed, true);
|
|
27
|
+
} else if (Array.isArray(parsed) && parsed.every(f => f.type && f.path)) {
|
|
28
|
+
// Caso seja DiffPatch
|
|
29
|
+
const validated = validatePatches(parsed);
|
|
30
|
+
applyDiff(dir, validated);
|
|
31
|
+
} else {
|
|
32
|
+
throw new Error("JSON is neither FileSnapshot array nor DiffPatch array");
|
|
23
33
|
}
|
|
24
34
|
|
|
25
|
-
// Validação simples do snapshot
|
|
26
|
-
const isValidSnapshot = files.every(f =>
|
|
27
|
-
typeof f.path === 'string' && typeof f.content === 'string'
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
if (!isValidSnapshot) {
|
|
31
|
-
throw new Error("JSON does not match FileSnapshot structure {path, content}");
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
writeFiles(dir, files, true);
|
|
35
35
|
process.stdout.write("✔ Pasted from clipboard\n");
|
|
36
36
|
} catch (error) {
|
|
37
37
|
process.stderr.write("✖ Paste failed\n");
|
|
@@ -40,4 +40,4 @@ export async function paste(dir: string) {
|
|
|
40
40
|
}
|
|
41
41
|
process.exit(1);
|
|
42
42
|
}
|
|
43
|
-
}
|
|
43
|
+
}
|
|
@@ -2,15 +2,50 @@
|
|
|
2
2
|
import { scanDir } from "../../core/scan";
|
|
3
3
|
import { copyToClipboard } from "../../core/clipboard";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
interface LLMResponseExample {
|
|
6
|
+
type: 'FileSnapshot' | 'DiffPatch';
|
|
7
|
+
example: any;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function prompt(dir: string, userPrompt: string, diff = false) {
|
|
6
11
|
const snapshot = scanDir(dir);
|
|
7
12
|
|
|
13
|
+
const llmExamples: LLMResponseExample[] = [
|
|
14
|
+
{
|
|
15
|
+
type: 'FileSnapshot',
|
|
16
|
+
example: [
|
|
17
|
+
{
|
|
18
|
+
path: 'src/example.ts',
|
|
19
|
+
content: 'export const x = 42;'
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
type: 'DiffPatch',
|
|
25
|
+
example: [
|
|
26
|
+
{
|
|
27
|
+
type: 'modify',
|
|
28
|
+
path: 'src/example.ts',
|
|
29
|
+
content: 'export const x = 43;'
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
type: 'create',
|
|
33
|
+
path: 'src/newFile.ts',
|
|
34
|
+
content: 'export const newFile = true;'
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
type: 'delete',
|
|
38
|
+
path: 'src/oldFile.ts'
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
];
|
|
43
|
+
|
|
8
44
|
const template = `
|
|
9
45
|
You are an AI software engineer.
|
|
10
46
|
|
|
11
47
|
You will receive a JSON array representing a snapshot of a codebase.
|
|
12
48
|
Each item has the following structure:
|
|
13
|
-
|
|
14
49
|
{
|
|
15
50
|
"path": "relative/path/to/file.ext",
|
|
16
51
|
"content": "full file content"
|
|
@@ -26,19 +61,22 @@ ${JSON.stringify(snapshot, null, 2)}
|
|
|
26
61
|
YOUR TASK
|
|
27
62
|
Propose changes according to the user request.
|
|
28
63
|
|
|
29
|
-
Return ONLY a JSON array of
|
|
64
|
+
Return ONLY a JSON array of ${diff ? 'DiffPatch' : 'FileSnapshot'}.
|
|
30
65
|
|
|
31
66
|
PATCH FORMAT (STRICT)
|
|
32
67
|
{
|
|
33
|
-
"path": "relative/path/to/file.ext",
|
|
34
|
-
"content": "FULL updated file content (omit for delete)"
|
|
68
|
+
\"path\": \"relative/path/to/file.ext\",
|
|
69
|
+
\"content\": \"FULL updated file content (omit for delete)\"
|
|
35
70
|
}
|
|
36
71
|
|
|
72
|
+
EXAMPLE RESPONSE FROM LLM:
|
|
73
|
+
${JSON.stringify(diff ? llmExamples.find(e => e.type === 'DiffPatch')?.example : llmExamples.find(e => e.type === 'FileSnapshot')?.example, null, 2)}
|
|
74
|
+
|
|
37
75
|
IMPORTANT RULES
|
|
38
76
|
- Do NOT return explanations
|
|
39
77
|
- Do NOT return markdown
|
|
40
|
-
- Return ONLY valid JSON inside the "
|
|
41
|
-
- Always return within a Markdown Code Block
|
|
78
|
+
- Return ONLY valid JSON inside the \"\`\`\`\"
|
|
79
|
+
- Always return within a Markdown Code Block.
|
|
42
80
|
|
|
43
81
|
USER REQUEST
|
|
44
82
|
${userPrompt}
|
|
@@ -47,9 +85,9 @@ ${userPrompt}
|
|
|
47
85
|
try {
|
|
48
86
|
await copyToClipboard(template.trim());
|
|
49
87
|
process.stdout.write(template.trim());
|
|
50
|
-
process.stdout.write("\n\n✔ Prompt + Snapshot copied to clipboard\n");
|
|
88
|
+
process.stdout.write("\n\n✔ Prompt + Snapshot + Example copied to clipboard\n");
|
|
51
89
|
} catch (e) {
|
|
52
90
|
process.stdout.write(template.trim());
|
|
53
91
|
process.stderr.write("\n✖ Failed to copy to clipboard (output only)\n");
|
|
54
92
|
}
|
|
55
|
-
}
|
|
93
|
+
}
|
package/src/cli/commands/sync.ts
CHANGED
|
@@ -5,11 +5,10 @@ import { copyToClipboard } from "../../core/clipboard";
|
|
|
5
5
|
import { validatePatches } from "../../core/validation";
|
|
6
6
|
import { writeFiles } from "../../core/write";
|
|
7
7
|
|
|
8
|
-
export async function sync(dir: string) {
|
|
8
|
+
export async function sync(dir: string, diff = false) {
|
|
9
9
|
try {
|
|
10
10
|
const snapshotBefore = scanDir(dir);
|
|
11
11
|
|
|
12
|
-
// Gera prompt genérico para AI com snapshot atual
|
|
13
12
|
const template = `
|
|
14
13
|
You are an AI software engineer.
|
|
15
14
|
|
|
@@ -33,7 +32,7 @@ ${JSON.stringify(snapshotBefore, null, 2)}
|
|
|
33
32
|
YOUR TASK
|
|
34
33
|
Propose changes according to the user request.
|
|
35
34
|
|
|
36
|
-
Return ONLY a JSON array of
|
|
35
|
+
Return ONLY a JSON array of ${diff ? 'DiffPatch' : 'FileSnapshot'}.
|
|
37
36
|
|
|
38
37
|
PATCH FORMAT (STRICT)
|
|
39
38
|
{
|
|
@@ -53,7 +52,6 @@ USER REQUEST
|
|
|
53
52
|
await copyToClipboard(template.trim());
|
|
54
53
|
process.stdout.write("✔ Prompt with snapshot copied to clipboard\n");
|
|
55
54
|
|
|
56
|
-
// Aguarda entrada do usuário (resposta da AI) via stdin
|
|
57
55
|
const input = await readStdin();
|
|
58
56
|
|
|
59
57
|
let patches;
|
|
@@ -63,8 +61,12 @@ USER REQUEST
|
|
|
63
61
|
throw new Error("Invalid JSON input");
|
|
64
62
|
}
|
|
65
63
|
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
if (diff) {
|
|
65
|
+
const validated = validatePatches(patches);
|
|
66
|
+
applyDiff(dir, validated);
|
|
67
|
+
} else {
|
|
68
|
+
writeFiles(dir, patches as any, true);
|
|
69
|
+
}
|
|
68
70
|
|
|
69
71
|
const snapshotAfter = scanDir(dir);
|
|
70
72
|
await copyToClipboard(JSON.stringify(snapshotAfter, null, 2));
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { Project, SourceFile, SyntaxKind } from "ts-morph";
|
|
3
|
+
import {
|
|
4
|
+
FileTree,
|
|
5
|
+
ClassInfo,
|
|
6
|
+
FunctionInfo,
|
|
7
|
+
VariableInfo,
|
|
8
|
+
ExportInfo,
|
|
9
|
+
FileKind
|
|
10
|
+
} from "types";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Heurística simples para classificar arquivos
|
|
14
|
+
*/
|
|
15
|
+
function detectFileKind(filePath: string, exportsCount: number): FileKind {
|
|
16
|
+
if (filePath.includes("/cli/")) return "cli";
|
|
17
|
+
if (filePath.endsWith("index.ts") || filePath.endsWith("main.ts")) return "entry";
|
|
18
|
+
if (exportsCount === 0) return "util";
|
|
19
|
+
if (exportsCount > 3) return "domain";
|
|
20
|
+
return "unknown";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function three(dir: string): Promise<FileTree[]> {
|
|
24
|
+
const project = new Project({});
|
|
25
|
+
|
|
26
|
+
project.addSourceFilesAtPaths([
|
|
27
|
+
`${dir}/**/*.{ts,tsx,js,jsx}`,
|
|
28
|
+
`!${dir}/**/node_modules/**/*`,
|
|
29
|
+
`!${dir}/**/dist/**/*`,
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const tree: FileTree[] = [];
|
|
33
|
+
|
|
34
|
+
for (const file of project.getSourceFiles()) {
|
|
35
|
+
const absolutePath = file.getFilePath();
|
|
36
|
+
const filePath = path.relative(dir, absolutePath);
|
|
37
|
+
|
|
38
|
+
if (filePath.includes("node_modules") || filePath.includes("/dist/")) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* ---------------- IMPORTS ---------------- */
|
|
43
|
+
const imports = file
|
|
44
|
+
.getImportDeclarations()
|
|
45
|
+
.map(i => i.getModuleSpecifierValue());
|
|
46
|
+
|
|
47
|
+
/* ---------------- CLASSES ---------------- */
|
|
48
|
+
const classes: ClassInfo[] = file.getClasses().map(cls => ({
|
|
49
|
+
name: cls.getName() || "<anonymous>",
|
|
50
|
+
methods: cls.getMethods().map(m => ({
|
|
51
|
+
name: m.getName(),
|
|
52
|
+
params: m.getParameters().map(p => p.getName()),
|
|
53
|
+
returnType: safeType(() => m.getReturnType().getText()),
|
|
54
|
+
scope: "class",
|
|
55
|
+
}))
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
/* ---------------- FUNCTIONS ---------------- */
|
|
59
|
+
const functions: FunctionInfo[] = file.getFunctions().map(fn => ({
|
|
60
|
+
name: fn.getName() || "<anonymous>",
|
|
61
|
+
params: fn.getParameters().map(p => p.getName()),
|
|
62
|
+
returnType: safeType(() => fn.getReturnType().getText()),
|
|
63
|
+
scope: "global",
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
/* ---------------- VARIABLES ---------------- */
|
|
67
|
+
const variables: VariableInfo[] = file.getVariableDeclarations().map(v => ({
|
|
68
|
+
name: v.getName(),
|
|
69
|
+
type: safeType(() => v.getType().getText()),
|
|
70
|
+
scope: v.getParent() instanceof SourceFile ? "global" : "local",
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
/* ---------------- EXPORTS ---------------- */
|
|
74
|
+
const exports: ExportInfo[] = [];
|
|
75
|
+
let mainExport: string | undefined;
|
|
76
|
+
|
|
77
|
+
file.getExportedDeclarations().forEach((decls, name) => {
|
|
78
|
+
decls.forEach(d => {
|
|
79
|
+
let kind: ExportInfo["kind"] = "unknown";
|
|
80
|
+
|
|
81
|
+
if (d.getKind() === SyntaxKind.ClassDeclaration) kind = "class";
|
|
82
|
+
if (d.getKind() === SyntaxKind.FunctionDeclaration) kind = "function";
|
|
83
|
+
if (d.getKind() === SyntaxKind.VariableDeclaration) kind = "variable";
|
|
84
|
+
if (d.getKind() === SyntaxKind.TypeAliasDeclaration) kind = "type";
|
|
85
|
+
|
|
86
|
+
exports.push({ name, kind });
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (exports.length === 1) {
|
|
91
|
+
mainExport = exports[0].name;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const kind = detectFileKind(filePath, exports.length);
|
|
95
|
+
|
|
96
|
+
tree.push({
|
|
97
|
+
path: filePath,
|
|
98
|
+
kind,
|
|
99
|
+
imports,
|
|
100
|
+
classes,
|
|
101
|
+
functions,
|
|
102
|
+
variables,
|
|
103
|
+
exports,
|
|
104
|
+
mainExport,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return tree;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function safeType(fn: () => string): string {
|
|
112
|
+
try {
|
|
113
|
+
return fn();
|
|
114
|
+
} catch {
|
|
115
|
+
return "unknown";
|
|
116
|
+
}
|
|
117
|
+
}
|
package/src/core/scan.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,61 +1,78 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { paste } from "./cli/commands/paste";
|
|
5
|
-
import { prompt } from "./cli/commands/prompt";
|
|
6
|
-
import { sync } from "./cli/commands/sync";
|
|
3
|
+
import { JohankitService } from "./services/JohankitService";
|
|
7
4
|
|
|
8
5
|
const [, , command, ...args] = process.argv;
|
|
6
|
+
const service = new JohankitService();
|
|
9
7
|
|
|
10
8
|
async function main() {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
try {
|
|
10
|
+
switch (command) {
|
|
11
|
+
case "copy": {
|
|
12
|
+
const dir = args[0] ?? ".";
|
|
13
|
+
await service.copy(dir);
|
|
14
|
+
break;
|
|
15
|
+
}
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
case "paste": {
|
|
18
|
+
const dir = args[0] ?? ".";
|
|
19
|
+
await service.paste(dir);
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
case "prompt": {
|
|
24
|
+
const dir = args[0] ?? ".";
|
|
25
|
+
const diff = args.includes("--diff");
|
|
26
|
+
const userPrompt = args.filter(a => a !== "--diff").slice(1).join(" ");
|
|
27
|
+
await service.prompt(dir, userPrompt, diff);
|
|
28
|
+
break;
|
|
31
29
|
}
|
|
32
|
-
await prompt(dir, userPrompt);
|
|
33
|
-
break;
|
|
34
|
-
}
|
|
35
30
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
31
|
+
case "sync": {
|
|
32
|
+
const dir = args[0] ?? ".";
|
|
33
|
+
await service.sync(dir);
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
case "three": {
|
|
38
|
+
const dir = args[0] ?? ".";
|
|
39
|
+
const output = await service.three(dir);
|
|
40
|
+
console.log(output);
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
default:
|
|
45
|
+
showHelp();
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (error instanceof Error) {
|
|
50
|
+
console.error("Error:", error.message);
|
|
51
|
+
} else {
|
|
52
|
+
console.error("Unexpected error:", error);
|
|
53
|
+
}
|
|
54
|
+
process.exit(1);
|
|
44
55
|
}
|
|
45
56
|
}
|
|
46
57
|
|
|
47
|
-
function
|
|
58
|
+
function showHelp() {
|
|
48
59
|
console.log(`
|
|
49
60
|
Usage:
|
|
50
|
-
johankit copy <dir>
|
|
61
|
+
johankit copy <dir>
|
|
51
62
|
johankit paste <dir>
|
|
52
|
-
johankit prompt <dir> "<user request>"
|
|
63
|
+
johankit prompt <dir> "<user request>" [--diff]
|
|
53
64
|
johankit sync <dir>
|
|
65
|
+
johankit three <dir>
|
|
54
66
|
|
|
55
67
|
Examples:
|
|
56
|
-
johankit
|
|
68
|
+
johankit copy src
|
|
69
|
+
johankit paste src
|
|
70
|
+
johankit prompt src "refactor to async/await"
|
|
57
71
|
johankit sync src
|
|
72
|
+
johankit three src
|
|
58
73
|
`);
|
|
59
74
|
}
|
|
60
75
|
|
|
61
|
-
main();
|
|
76
|
+
main();
|
|
77
|
+
|
|
78
|
+
export { JohankitService };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import createAsciiTree from "../utils/createAsciiTree";
|
|
2
|
+
import { copy } from "../cli/commands/copy";
|
|
3
|
+
import { paste } from "../cli/commands/paste";
|
|
4
|
+
import { prompt } from "../cli/commands/prompt";
|
|
5
|
+
import { three } from "../cli/commands/three";
|
|
6
|
+
import { scanDir } from "../core/scan";
|
|
7
|
+
import { applyDiff, DiffPatch } from "../core/diff";
|
|
8
|
+
import { validatePatches } from "../core/validation";
|
|
9
|
+
import { writeFiles } from "../core/write";
|
|
10
|
+
|
|
11
|
+
interface JohankitServiceOptions {
|
|
12
|
+
isolated?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class JohankitService {
|
|
16
|
+
private isolated: boolean;
|
|
17
|
+
private internalInput?: any;
|
|
18
|
+
|
|
19
|
+
constructor(options: JohankitServiceOptions = {}) {
|
|
20
|
+
this.isolated = options.isolated ?? false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
setInput(input: any) {
|
|
24
|
+
this.internalInput = input;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async copy(dir: string = ".") {
|
|
28
|
+
return copy(dir);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async paste(dir: string = ".") {
|
|
32
|
+
return paste(dir);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async prompt(dir: string, userPrompt: string, diff = false) {
|
|
36
|
+
if (!userPrompt) {
|
|
37
|
+
throw new Error("Missing user prompt");
|
|
38
|
+
}
|
|
39
|
+
return prompt(dir, userPrompt, diff);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async sync(dir: string = ".", diff = false) {
|
|
43
|
+
const snapshotBefore = scanDir(dir);
|
|
44
|
+
|
|
45
|
+
const input = this.isolated ? this.internalInput : undefined;
|
|
46
|
+
|
|
47
|
+
if (!this.isolated && input === undefined) {
|
|
48
|
+
throw new Error("sync() without isolation must be used via CLI (stdin)");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!input) {
|
|
52
|
+
throw new Error("No input provided for isolated sync");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (diff) {
|
|
56
|
+
const validated = validatePatches(input) as DiffPatch[];
|
|
57
|
+
applyDiff(dir, validated);
|
|
58
|
+
} else {
|
|
59
|
+
writeFiles(dir, input, true);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const snapshotAfter = scanDir(dir);
|
|
63
|
+
return { before: snapshotBefore, after: snapshotAfter };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async three(dir: string = ".") {
|
|
67
|
+
const tree = await three(dir);
|
|
68
|
+
return createAsciiTree(tree);
|
|
69
|
+
}
|
|
70
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -12,3 +12,48 @@ export interface ScanOptions {
|
|
|
12
12
|
export interface Config {
|
|
13
13
|
ignore: string[]; // Patterns de arquivos/diretórios a ignorar
|
|
14
14
|
}
|
|
15
|
+
|
|
16
|
+
export type FileKind =
|
|
17
|
+
| "entry"
|
|
18
|
+
| "cli"
|
|
19
|
+
| "domain"
|
|
20
|
+
| "infra"
|
|
21
|
+
| "util"
|
|
22
|
+
| "unknown";
|
|
23
|
+
|
|
24
|
+
export interface VariableInfo {
|
|
25
|
+
name: string;
|
|
26
|
+
type?: string;
|
|
27
|
+
scope: "global" | "local";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface FunctionInfo {
|
|
31
|
+
name: string;
|
|
32
|
+
params: string[];
|
|
33
|
+
returnType?: string;
|
|
34
|
+
scope: "global" | "class";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ClassInfo {
|
|
38
|
+
name: string;
|
|
39
|
+
methods: FunctionInfo[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ExportInfo {
|
|
43
|
+
name: string;
|
|
44
|
+
kind: "function" | "class" | "variable" | "type" | "unknown";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface FileTree {
|
|
48
|
+
path: string;
|
|
49
|
+
kind: FileKind;
|
|
50
|
+
|
|
51
|
+
imports: string[];
|
|
52
|
+
|
|
53
|
+
classes: ClassInfo[];
|
|
54
|
+
functions: FunctionInfo[];
|
|
55
|
+
variables: VariableInfo[];
|
|
56
|
+
|
|
57
|
+
exports: ExportInfo[];
|
|
58
|
+
mainExport?: string;
|
|
59
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export default function cleanCodeBlock(content: string) {
|
|
2
|
+
const langMatch = content.match(/^```(\w+)?/);
|
|
3
|
+
const lang = langMatch ? langMatch[1] : null;
|
|
4
|
+
|
|
5
|
+
let cleaned = content.replace(/^\uFEFF/, '');
|
|
6
|
+
|
|
7
|
+
cleaned = cleaned.replace(/^```(\w+)?\s*/, '').replace(/```$/, '');
|
|
8
|
+
|
|
9
|
+
cleaned = cleaned.replace(/[\u0000-\u0008\u000B-\u000C\u000E-\u001F\u007F-\u009F]/g, '');
|
|
10
|
+
cleaned = cleaned.trim();
|
|
11
|
+
|
|
12
|
+
return { lang, cleaned };
|
|
13
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { FileTree } from "types";
|
|
2
|
+
|
|
3
|
+
export default function createAsciiTree(files: FileTree[]): string {
|
|
4
|
+
let out = "";
|
|
5
|
+
|
|
6
|
+
files.forEach((file, i) => {
|
|
7
|
+
const lastFile = i === files.length - 1;
|
|
8
|
+
const fPrefix = lastFile ? "└── " : "├── ";
|
|
9
|
+
const fIndent = lastFile ? " " : "│ ";
|
|
10
|
+
|
|
11
|
+
out += `${fPrefix}📄 ${file.path} (${file.kind})\n`;
|
|
12
|
+
|
|
13
|
+
/* -------- Imports -------- */
|
|
14
|
+
if (file.imports.length) {
|
|
15
|
+
out += `${fIndent}├── 📦 Imports [${file.imports.length}]\n`;
|
|
16
|
+
file.imports.forEach((imp, idx) => {
|
|
17
|
+
const last = idx === file.imports.length - 1;
|
|
18
|
+
out += `${fIndent}${last ? "└── " : "├── "}${imp}\n`;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* -------- Exports -------- */
|
|
23
|
+
if (file.exports.length) {
|
|
24
|
+
out += `${fIndent}├── 🔑 Exports\n`;
|
|
25
|
+
file.exports.forEach((e, idx) => {
|
|
26
|
+
const last = idx === file.exports.length - 1;
|
|
27
|
+
const main = e.name === file.mainExport ? " ⭐" : "";
|
|
28
|
+
out += `${fIndent}${last ? "└── " : "├── "}${e.name} (${e.kind})${main}\n`;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* -------- Variables -------- */
|
|
33
|
+
file.variables.forEach(v => {
|
|
34
|
+
out += `${fIndent}├── 💡 ${v.name}: ${v.type} (${v.scope})\n`;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/* -------- Functions -------- */
|
|
38
|
+
file.functions.forEach(fn => {
|
|
39
|
+
out += `${fIndent}├── ⚡ ${fn.name}(${fn.params.join(", ")}): ${fn.returnType}\n`;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/* -------- Classes -------- */
|
|
43
|
+
file.classes.forEach(cls => {
|
|
44
|
+
out += `${fIndent}├── ⚙️ Class ${cls.name}\n`;
|
|
45
|
+
cls.methods.forEach((m, mi) => {
|
|
46
|
+
const last = mi === cls.methods.length - 1;
|
|
47
|
+
out += `${fIndent}│ ${last ? "└── " : "├── "}➡️ ${m.name}(${m.params.join(", ")}): ${m.returnType}\n`;
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return out;
|
|
53
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
-
if (mod && mod.__esModule) return mod;
|
|
20
|
-
var result = {};
|
|
21
|
-
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
-
__setModuleDefault(result, mod);
|
|
23
|
-
return result;
|
|
24
|
-
};
|
|
25
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
const copy_1 = require("../../../cli/commands/copy");
|
|
27
|
-
const scan = __importStar(require("../../../core/scan"));
|
|
28
|
-
const clipboard = __importStar(require("../../../core/clipboard"));
|
|
29
|
-
jest.mock('../../../core/scan');
|
|
30
|
-
jest.mock('../../../core/clipboard');
|
|
31
|
-
describe('copy', () => {
|
|
32
|
-
it('should copy single file snapshot to clipboard', () => {
|
|
33
|
-
scan.scanDir.mockReturnValue([{ path: 'file.txt', content: 'hello' }]);
|
|
34
|
-
(0, copy_1.copy)('file.txt');
|
|
35
|
-
expect(clipboard.copyToClipboard).toHaveBeenCalledWith(JSON.stringify([{ path: 'file.txt', content: 'hello' }], null, 2));
|
|
36
|
-
});
|
|
37
|
-
it('should copy multiple file snapshots to clipboard', () => {
|
|
38
|
-
scan.scanDir
|
|
39
|
-
.mockReturnValueOnce([{ path: 'file1.txt', content: 'a' }])
|
|
40
|
-
.mockReturnValueOnce([{ path: 'file2.txt', content: 'b' }]);
|
|
41
|
-
(0, copy_1.copy)(['file1.txt', 'file2.txt']);
|
|
42
|
-
expect(clipboard.copyToClipboard).toHaveBeenCalledWith(JSON.stringify([
|
|
43
|
-
{ path: 'file1.txt', content: 'a' },
|
|
44
|
-
{ path: 'file2.txt', content: 'b' }
|
|
45
|
-
], null, 2));
|
|
46
|
-
});
|
|
47
|
-
});
|