johankit 0.0.3 → 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/dist/cli/commands/paste.js +15 -10
- package/dist/cli/commands/prompt.js +39 -7
- 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/createAsciiTree.js +46 -0
- package/johankit.yaml +2 -0
- package/package.json +2 -1
- package/src/cli/commands/paste.ts +15 -16
- package/src/cli/commands/prompt.ts +45 -7
- 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/createAsciiTree.ts +53 -0
- package/cli/commands/paste.js +0 -42
- package/cli/commands/paste.ts +0 -42
- package/cli/commands/prompt.js +0 -59
- package/cli/commands/prompt.ts +0 -57
- package/cli/commands/sync.js +0 -83
- package/cli/commands/sync.ts +0 -87
- package/commands/paste.js +0 -42
- package/commands/paste.ts +0 -45
- package/core/config.js +0 -44
- package/core/config.ts +0 -42
- package/core/schema.js +0 -30
- package/core/schema.ts +0 -29
- package/core/vm.js +0 -20
- package/core/vm.ts +0 -18
- package/dist/core/clean.js +0 -13
|
@@ -8,29 +8,34 @@ exports.paste = void 0;
|
|
|
8
8
|
const write_1 = require("../../core/write");
|
|
9
9
|
const clipboard_1 = require("../../core/clipboard");
|
|
10
10
|
const cleanCodeBlock_1 = __importDefault(require("../../utils/cleanCodeBlock"));
|
|
11
|
+
const diff_1 = require("../../core/diff");
|
|
12
|
+
const validation_1 = require("../../core/validation");
|
|
11
13
|
async function paste(dir) {
|
|
12
14
|
try {
|
|
13
15
|
const content = await (0, clipboard_1.readClipboard)();
|
|
14
16
|
if (!content) {
|
|
15
17
|
throw new Error("Clipboard empty or inaccessible");
|
|
16
18
|
}
|
|
17
|
-
let
|
|
19
|
+
let parsed;
|
|
18
20
|
try {
|
|
19
|
-
const {
|
|
20
|
-
|
|
21
|
+
const { cleaned } = (0, cleanCodeBlock_1.default)(content);
|
|
22
|
+
parsed = JSON.parse(cleaned);
|
|
21
23
|
}
|
|
22
24
|
catch (e) {
|
|
23
25
|
throw new Error("Clipboard content is not valid JSON");
|
|
24
26
|
}
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
+
if (Array.isArray(parsed) && parsed.every(f => f.path && f.content)) {
|
|
28
|
+
// Caso seja snapshot
|
|
29
|
+
(0, write_1.writeFiles)(dir, parsed, true);
|
|
27
30
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
else if (Array.isArray(parsed) && parsed.every(f => f.type && f.path)) {
|
|
32
|
+
// Caso seja DiffPatch
|
|
33
|
+
const validated = (0, validation_1.validatePatches)(parsed);
|
|
34
|
+
(0, diff_1.applyDiff)(dir, validated);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
throw new Error("JSON is neither FileSnapshot array nor DiffPatch array");
|
|
32
38
|
}
|
|
33
|
-
(0, write_1.writeFiles)(dir, files, true);
|
|
34
39
|
process.stdout.write("✔ Pasted from clipboard\n");
|
|
35
40
|
}
|
|
36
41
|
catch (error) {
|
|
@@ -4,14 +4,43 @@ exports.prompt = void 0;
|
|
|
4
4
|
// src/cli/commands/prompt.ts
|
|
5
5
|
const scan_1 = require("../../core/scan");
|
|
6
6
|
const clipboard_1 = require("../../core/clipboard");
|
|
7
|
-
async function prompt(dir, userPrompt) {
|
|
7
|
+
async function prompt(dir, userPrompt, diff = false) {
|
|
8
8
|
const snapshot = (0, scan_1.scanDir)(dir);
|
|
9
|
+
const llmExamples = [
|
|
10
|
+
{
|
|
11
|
+
type: 'FileSnapshot',
|
|
12
|
+
example: [
|
|
13
|
+
{
|
|
14
|
+
path: 'src/example.ts',
|
|
15
|
+
content: 'export const x = 42;'
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
type: 'DiffPatch',
|
|
21
|
+
example: [
|
|
22
|
+
{
|
|
23
|
+
type: 'modify',
|
|
24
|
+
path: 'src/example.ts',
|
|
25
|
+
content: 'export const x = 43;'
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
type: 'create',
|
|
29
|
+
path: 'src/newFile.ts',
|
|
30
|
+
content: 'export const newFile = true;'
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
type: 'delete',
|
|
34
|
+
path: 'src/oldFile.ts'
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
];
|
|
9
39
|
const template = `
|
|
10
40
|
You are an AI software engineer.
|
|
11
41
|
|
|
12
42
|
You will receive a JSON array representing a snapshot of a codebase.
|
|
13
43
|
Each item has the following structure:
|
|
14
|
-
|
|
15
44
|
{
|
|
16
45
|
"path": "relative/path/to/file.ext",
|
|
17
46
|
"content": "full file content"
|
|
@@ -27,19 +56,22 @@ ${JSON.stringify(snapshot, null, 2)}
|
|
|
27
56
|
YOUR TASK
|
|
28
57
|
Propose changes according to the user request.
|
|
29
58
|
|
|
30
|
-
Return ONLY a JSON array of
|
|
59
|
+
Return ONLY a JSON array of ${diff ? 'DiffPatch' : 'FileSnapshot'}.
|
|
31
60
|
|
|
32
61
|
PATCH FORMAT (STRICT)
|
|
33
62
|
{
|
|
34
|
-
"path": "relative/path/to/file.ext",
|
|
35
|
-
"content": "FULL updated file content (omit for delete)"
|
|
63
|
+
\"path\": \"relative/path/to/file.ext\",
|
|
64
|
+
\"content\": \"FULL updated file content (omit for delete)\"
|
|
36
65
|
}
|
|
37
66
|
|
|
67
|
+
EXAMPLE RESPONSE FROM LLM:
|
|
68
|
+
${JSON.stringify(diff ? llmExamples.find(e => e.type === 'DiffPatch')?.example : llmExamples.find(e => e.type === 'FileSnapshot')?.example, null, 2)}
|
|
69
|
+
|
|
38
70
|
IMPORTANT RULES
|
|
39
71
|
- Do NOT return explanations
|
|
40
72
|
- Do NOT return markdown
|
|
41
73
|
- Return ONLY valid JSON inside the \"\`\`\`\"
|
|
42
|
-
- Always return within a Markdown Code Block
|
|
74
|
+
- Always return within a Markdown Code Block.
|
|
43
75
|
|
|
44
76
|
USER REQUEST
|
|
45
77
|
${userPrompt}
|
|
@@ -47,7 +79,7 @@ ${userPrompt}
|
|
|
47
79
|
try {
|
|
48
80
|
await (0, clipboard_1.copyToClipboard)(template.trim());
|
|
49
81
|
process.stdout.write(template.trim());
|
|
50
|
-
process.stdout.write("\n\n✔ Prompt + Snapshot copied to clipboard\n");
|
|
82
|
+
process.stdout.write("\n\n✔ Prompt + Snapshot + Example copied to clipboard\n");
|
|
51
83
|
}
|
|
52
84
|
catch (e) {
|
|
53
85
|
process.stdout.write(template.trim());
|
|
@@ -6,10 +6,10 @@ const scan_1 = require("../../core/scan");
|
|
|
6
6
|
const diff_1 = require("../../core/diff");
|
|
7
7
|
const clipboard_1 = require("../../core/clipboard");
|
|
8
8
|
const validation_1 = require("../../core/validation");
|
|
9
|
-
|
|
9
|
+
const write_1 = require("../../core/write");
|
|
10
|
+
async function sync(dir, diff = false) {
|
|
10
11
|
try {
|
|
11
12
|
const snapshotBefore = (0, scan_1.scanDir)(dir);
|
|
12
|
-
// Gera prompt genérico para AI com snapshot atual
|
|
13
13
|
const template = `
|
|
14
14
|
You are an AI software engineer.
|
|
15
15
|
|
|
@@ -33,7 +33,7 @@ ${JSON.stringify(snapshotBefore, null, 2)}
|
|
|
33
33
|
YOUR TASK
|
|
34
34
|
Propose changes according to the user request.
|
|
35
35
|
|
|
36
|
-
Return ONLY a JSON array of
|
|
36
|
+
Return ONLY a JSON array of ${diff ? 'DiffPatch' : 'FileSnapshot'}.
|
|
37
37
|
|
|
38
38
|
PATCH FORMAT (STRICT)
|
|
39
39
|
{
|
|
@@ -51,7 +51,6 @@ USER REQUEST
|
|
|
51
51
|
`;
|
|
52
52
|
await (0, clipboard_1.copyToClipboard)(template.trim());
|
|
53
53
|
process.stdout.write("✔ Prompt with snapshot copied to clipboard\n");
|
|
54
|
-
// Aguarda entrada do usuário (resposta da AI) via stdin
|
|
55
54
|
const input = await readStdin();
|
|
56
55
|
let patches;
|
|
57
56
|
try {
|
|
@@ -60,8 +59,13 @@ USER REQUEST
|
|
|
60
59
|
catch {
|
|
61
60
|
throw new Error("Invalid JSON input");
|
|
62
61
|
}
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
if (diff) {
|
|
63
|
+
const validated = (0, validation_1.validatePatches)(patches);
|
|
64
|
+
(0, diff_1.applyDiff)(dir, validated);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
(0, write_1.writeFiles)(dir, patches, true);
|
|
68
|
+
}
|
|
65
69
|
const snapshotAfter = (0, scan_1.scanDir)(dir);
|
|
66
70
|
await (0, clipboard_1.copyToClipboard)(JSON.stringify(snapshotAfter, null, 2));
|
|
67
71
|
process.stdout.write("✔ Sync applied and new snapshot copied to clipboard\n");
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.three = void 0;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const ts_morph_1 = require("ts-morph");
|
|
9
|
+
/**
|
|
10
|
+
* Heurística simples para classificar arquivos
|
|
11
|
+
*/
|
|
12
|
+
function detectFileKind(filePath, exportsCount) {
|
|
13
|
+
if (filePath.includes("/cli/"))
|
|
14
|
+
return "cli";
|
|
15
|
+
if (filePath.endsWith("index.ts") || filePath.endsWith("main.ts"))
|
|
16
|
+
return "entry";
|
|
17
|
+
if (exportsCount === 0)
|
|
18
|
+
return "util";
|
|
19
|
+
if (exportsCount > 3)
|
|
20
|
+
return "domain";
|
|
21
|
+
return "unknown";
|
|
22
|
+
}
|
|
23
|
+
async function three(dir) {
|
|
24
|
+
const project = new ts_morph_1.Project({});
|
|
25
|
+
project.addSourceFilesAtPaths([
|
|
26
|
+
`${dir}/**/*.{ts,tsx,js,jsx}`,
|
|
27
|
+
`!${dir}/**/node_modules/**/*`,
|
|
28
|
+
`!${dir}/**/dist/**/*`,
|
|
29
|
+
]);
|
|
30
|
+
const tree = [];
|
|
31
|
+
for (const file of project.getSourceFiles()) {
|
|
32
|
+
const absolutePath = file.getFilePath();
|
|
33
|
+
const filePath = path_1.default.relative(dir, absolutePath);
|
|
34
|
+
if (filePath.includes("node_modules") || filePath.includes("/dist/")) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
/* ---------------- IMPORTS ---------------- */
|
|
38
|
+
const imports = file
|
|
39
|
+
.getImportDeclarations()
|
|
40
|
+
.map(i => i.getModuleSpecifierValue());
|
|
41
|
+
/* ---------------- CLASSES ---------------- */
|
|
42
|
+
const classes = file.getClasses().map(cls => ({
|
|
43
|
+
name: cls.getName() || "<anonymous>",
|
|
44
|
+
methods: cls.getMethods().map(m => ({
|
|
45
|
+
name: m.getName(),
|
|
46
|
+
params: m.getParameters().map(p => p.getName()),
|
|
47
|
+
returnType: safeType(() => m.getReturnType().getText()),
|
|
48
|
+
scope: "class",
|
|
49
|
+
}))
|
|
50
|
+
}));
|
|
51
|
+
/* ---------------- FUNCTIONS ---------------- */
|
|
52
|
+
const functions = file.getFunctions().map(fn => ({
|
|
53
|
+
name: fn.getName() || "<anonymous>",
|
|
54
|
+
params: fn.getParameters().map(p => p.getName()),
|
|
55
|
+
returnType: safeType(() => fn.getReturnType().getText()),
|
|
56
|
+
scope: "global",
|
|
57
|
+
}));
|
|
58
|
+
/* ---------------- VARIABLES ---------------- */
|
|
59
|
+
const variables = file.getVariableDeclarations().map(v => ({
|
|
60
|
+
name: v.getName(),
|
|
61
|
+
type: safeType(() => v.getType().getText()),
|
|
62
|
+
scope: v.getParent() instanceof ts_morph_1.SourceFile ? "global" : "local",
|
|
63
|
+
}));
|
|
64
|
+
/* ---------------- EXPORTS ---------------- */
|
|
65
|
+
const exports = [];
|
|
66
|
+
let mainExport;
|
|
67
|
+
file.getExportedDeclarations().forEach((decls, name) => {
|
|
68
|
+
decls.forEach(d => {
|
|
69
|
+
let kind = "unknown";
|
|
70
|
+
if (d.getKind() === ts_morph_1.SyntaxKind.ClassDeclaration)
|
|
71
|
+
kind = "class";
|
|
72
|
+
if (d.getKind() === ts_morph_1.SyntaxKind.FunctionDeclaration)
|
|
73
|
+
kind = "function";
|
|
74
|
+
if (d.getKind() === ts_morph_1.SyntaxKind.VariableDeclaration)
|
|
75
|
+
kind = "variable";
|
|
76
|
+
if (d.getKind() === ts_morph_1.SyntaxKind.TypeAliasDeclaration)
|
|
77
|
+
kind = "type";
|
|
78
|
+
exports.push({ name, kind });
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
if (exports.length === 1) {
|
|
82
|
+
mainExport = exports[0].name;
|
|
83
|
+
}
|
|
84
|
+
const kind = detectFileKind(filePath, exports.length);
|
|
85
|
+
tree.push({
|
|
86
|
+
path: filePath,
|
|
87
|
+
kind,
|
|
88
|
+
imports,
|
|
89
|
+
classes,
|
|
90
|
+
functions,
|
|
91
|
+
variables,
|
|
92
|
+
exports,
|
|
93
|
+
mainExport,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return tree;
|
|
97
|
+
}
|
|
98
|
+
exports.three = three;
|
|
99
|
+
function safeType(fn) {
|
|
100
|
+
try {
|
|
101
|
+
return fn();
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return "unknown";
|
|
105
|
+
}
|
|
106
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,54 +1,72 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
const sync_1 = require("./cli/commands/sync");
|
|
4
|
+
exports.JohankitService = void 0;
|
|
5
|
+
const JohankitService_1 = require("./services/JohankitService");
|
|
6
|
+
Object.defineProperty(exports, "JohankitService", { enumerable: true, get: function () { return JohankitService_1.JohankitService; } });
|
|
8
7
|
const [, , command, ...args] = process.argv;
|
|
8
|
+
const service = new JohankitService_1.JohankitService();
|
|
9
9
|
async function main() {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
10
|
+
try {
|
|
11
|
+
switch (command) {
|
|
12
|
+
case "copy": {
|
|
13
|
+
const dir = args[0] ?? ".";
|
|
14
|
+
await service.copy(dir);
|
|
15
|
+
break;
|
|
16
|
+
}
|
|
17
|
+
case "paste": {
|
|
18
|
+
const dir = args[0] ?? ".";
|
|
19
|
+
await service.paste(dir);
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
case "prompt": {
|
|
23
|
+
const dir = args[0] ?? ".";
|
|
24
|
+
const diff = args.includes("--diff");
|
|
25
|
+
const userPrompt = args.filter(a => a !== "--diff").slice(1).join(" ");
|
|
26
|
+
await service.prompt(dir, userPrompt, diff);
|
|
27
|
+
break;
|
|
28
28
|
}
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
case "sync": {
|
|
30
|
+
const dir = args[0] ?? ".";
|
|
31
|
+
await service.sync(dir);
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
case "three": {
|
|
35
|
+
const dir = args[0] ?? ".";
|
|
36
|
+
const output = await service.three(dir);
|
|
37
|
+
console.log(output);
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
default:
|
|
41
|
+
showHelp();
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error instanceof Error) {
|
|
47
|
+
console.error("Error:", error.message);
|
|
31
48
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
await (0, sync_1.sync)(dir);
|
|
35
|
-
break;
|
|
49
|
+
else {
|
|
50
|
+
console.error("Unexpected error:", error);
|
|
36
51
|
}
|
|
37
|
-
|
|
38
|
-
help();
|
|
52
|
+
process.exit(1);
|
|
39
53
|
}
|
|
40
54
|
}
|
|
41
|
-
function
|
|
55
|
+
function showHelp() {
|
|
42
56
|
console.log(`
|
|
43
57
|
Usage:
|
|
44
|
-
johankit copy <dir>
|
|
58
|
+
johankit copy <dir>
|
|
45
59
|
johankit paste <dir>
|
|
46
|
-
johankit prompt <dir> "<user request>"
|
|
60
|
+
johankit prompt <dir> "<user request>" [--diff]
|
|
47
61
|
johankit sync <dir>
|
|
62
|
+
johankit three <dir>
|
|
48
63
|
|
|
49
64
|
Examples:
|
|
50
|
-
johankit
|
|
65
|
+
johankit copy src
|
|
66
|
+
johankit paste src
|
|
67
|
+
johankit prompt src "refactor to async/await"
|
|
51
68
|
johankit sync src
|
|
69
|
+
johankit three src
|
|
52
70
|
`);
|
|
53
71
|
}
|
|
54
72
|
main();
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.JohankitService = void 0;
|
|
7
|
+
const createAsciiTree_1 = __importDefault(require("../utils/createAsciiTree"));
|
|
8
|
+
const copy_1 = require("../cli/commands/copy");
|
|
9
|
+
const paste_1 = require("../cli/commands/paste");
|
|
10
|
+
const prompt_1 = require("../cli/commands/prompt");
|
|
11
|
+
const three_1 = require("../cli/commands/three");
|
|
12
|
+
const scan_1 = require("../core/scan");
|
|
13
|
+
const diff_1 = require("../core/diff");
|
|
14
|
+
const validation_1 = require("../core/validation");
|
|
15
|
+
const write_1 = require("../core/write");
|
|
16
|
+
class JohankitService {
|
|
17
|
+
constructor(options = {}) {
|
|
18
|
+
this.isolated = options.isolated ?? false;
|
|
19
|
+
}
|
|
20
|
+
setInput(input) {
|
|
21
|
+
this.internalInput = input;
|
|
22
|
+
}
|
|
23
|
+
async copy(dir = ".") {
|
|
24
|
+
return (0, copy_1.copy)(dir);
|
|
25
|
+
}
|
|
26
|
+
async paste(dir = ".") {
|
|
27
|
+
return (0, paste_1.paste)(dir);
|
|
28
|
+
}
|
|
29
|
+
async prompt(dir, userPrompt, diff = false) {
|
|
30
|
+
if (!userPrompt) {
|
|
31
|
+
throw new Error("Missing user prompt");
|
|
32
|
+
}
|
|
33
|
+
return (0, prompt_1.prompt)(dir, userPrompt, diff);
|
|
34
|
+
}
|
|
35
|
+
async sync(dir = ".", diff = false) {
|
|
36
|
+
const snapshotBefore = (0, scan_1.scanDir)(dir);
|
|
37
|
+
const input = this.isolated ? this.internalInput : undefined;
|
|
38
|
+
if (!this.isolated && input === undefined) {
|
|
39
|
+
throw new Error("sync() without isolation must be used via CLI (stdin)");
|
|
40
|
+
}
|
|
41
|
+
if (!input) {
|
|
42
|
+
throw new Error("No input provided for isolated sync");
|
|
43
|
+
}
|
|
44
|
+
if (diff) {
|
|
45
|
+
const validated = (0, validation_1.validatePatches)(input);
|
|
46
|
+
(0, diff_1.applyDiff)(dir, validated);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
(0, write_1.writeFiles)(dir, input, true);
|
|
50
|
+
}
|
|
51
|
+
const snapshotAfter = (0, scan_1.scanDir)(dir);
|
|
52
|
+
return { before: snapshotBefore, after: snapshotAfter };
|
|
53
|
+
}
|
|
54
|
+
async three(dir = ".") {
|
|
55
|
+
const tree = await (0, three_1.three)(dir);
|
|
56
|
+
return (0, createAsciiTree_1.default)(tree);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
exports.JohankitService = JohankitService;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
function createAsciiTree(files) {
|
|
4
|
+
let out = "";
|
|
5
|
+
files.forEach((file, i) => {
|
|
6
|
+
const lastFile = i === files.length - 1;
|
|
7
|
+
const fPrefix = lastFile ? "└── " : "├── ";
|
|
8
|
+
const fIndent = lastFile ? " " : "│ ";
|
|
9
|
+
out += `${fPrefix}📄 ${file.path} (${file.kind})\n`;
|
|
10
|
+
/* -------- Imports -------- */
|
|
11
|
+
if (file.imports.length) {
|
|
12
|
+
out += `${fIndent}├── 📦 Imports [${file.imports.length}]\n`;
|
|
13
|
+
file.imports.forEach((imp, idx) => {
|
|
14
|
+
const last = idx === file.imports.length - 1;
|
|
15
|
+
out += `${fIndent}${last ? "└── " : "├── "}${imp}\n`;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
/* -------- Exports -------- */
|
|
19
|
+
if (file.exports.length) {
|
|
20
|
+
out += `${fIndent}├── 🔑 Exports\n`;
|
|
21
|
+
file.exports.forEach((e, idx) => {
|
|
22
|
+
const last = idx === file.exports.length - 1;
|
|
23
|
+
const main = e.name === file.mainExport ? " ⭐" : "";
|
|
24
|
+
out += `${fIndent}${last ? "└── " : "├── "}${e.name} (${e.kind})${main}\n`;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/* -------- Variables -------- */
|
|
28
|
+
file.variables.forEach(v => {
|
|
29
|
+
out += `${fIndent}├── 💡 ${v.name}: ${v.type} (${v.scope})\n`;
|
|
30
|
+
});
|
|
31
|
+
/* -------- Functions -------- */
|
|
32
|
+
file.functions.forEach(fn => {
|
|
33
|
+
out += `${fIndent}├── ⚡ ${fn.name}(${fn.params.join(", ")}): ${fn.returnType}\n`;
|
|
34
|
+
});
|
|
35
|
+
/* -------- Classes -------- */
|
|
36
|
+
file.classes.forEach(cls => {
|
|
37
|
+
out += `${fIndent}├── ⚙️ Class ${cls.name}\n`;
|
|
38
|
+
cls.methods.forEach((m, mi) => {
|
|
39
|
+
const last = mi === cls.methods.length - 1;
|
|
40
|
+
out += `${fIndent}│ ${last ? "└── " : "├── "}➡️ ${m.name}(${m.params.join(", ")}): ${m.returnType}\n`;
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
exports.default = createAsciiTree;
|
package/johankit.yaml
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "johankit",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"clipboardy": "^5.0.2",
|
|
28
28
|
"commander": "^14.0.2",
|
|
29
29
|
"js-yaml": "^4.1.1",
|
|
30
|
+
"ts-morph": "^27.0.2",
|
|
30
31
|
"vm2": "^3.10.0"
|
|
31
32
|
}
|
|
32
33
|
}
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { writeFiles } from "../../core/write";
|
|
3
3
|
import { readClipboard } from "../../core/clipboard";
|
|
4
4
|
import cleanCodeBlock from "../../utils/cleanCodeBlock";
|
|
5
|
+
import { applyDiff } from "../../core/diff";
|
|
6
|
+
import { validatePatches } from "../../core/validation";
|
|
5
7
|
|
|
6
8
|
export async function paste(dir: string) {
|
|
7
9
|
try {
|
|
@@ -11,28 +13,25 @@ export async function paste(dir: string) {
|
|
|
11
13
|
throw new Error("Clipboard empty or inaccessible");
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
let
|
|
16
|
+
let parsed;
|
|
15
17
|
try {
|
|
16
|
-
const {
|
|
17
|
-
|
|
18
|
+
const { cleaned } = cleanCodeBlock(content);
|
|
19
|
+
parsed = JSON.parse(cleaned);
|
|
18
20
|
} catch (e) {
|
|
19
21
|
throw new Error("Clipboard content is not valid JSON");
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
if (
|
|
23
|
-
|
|
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");
|
|
24
33
|
}
|
|
25
34
|
|
|
26
|
-
// Validação simples do snapshot
|
|
27
|
-
const isValidSnapshot = files.every(f =>
|
|
28
|
-
typeof f.path === 'string' && typeof f.content === 'string'
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
if (!isValidSnapshot) {
|
|
32
|
-
throw new Error("JSON does not match FileSnapshot structure {path, content}");
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
writeFiles(dir, files, true);
|
|
36
35
|
process.stdout.write("✔ Pasted from clipboard\n");
|
|
37
36
|
} catch (error) {
|
|
38
37
|
process.stderr.write("✖ Paste failed\n");
|
|
@@ -41,4 +40,4 @@ export async function paste(dir: string) {
|
|
|
41
40
|
}
|
|
42
41
|
process.exit(1);
|
|
43
42
|
}
|
|
44
|
-
}
|
|
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
78
|
- Return ONLY valid JSON inside the \"\`\`\`\"
|
|
41
|
-
- Always return within a Markdown Code Block
|
|
79
|
+
- Always return within a Markdown Code Block.
|
|
42
80
|
|
|
43
81
|
USER REQUEST
|
|
44
82
|
${userPrompt}
|
|
@@ -47,7 +85,7 @@ ${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");
|