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.
@@ -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 patches.
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
- const validated = validatePatches(patches);
67
- applyDiff(dir, validated);
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
@@ -34,7 +34,7 @@ export function scanDir(
34
34
  // ignore read errors
35
35
  }
36
36
  }
37
-
37
+
38
38
  function shouldIgnore(name: string): boolean {
39
39
  if (ignoreSet.has(name)) return true;
40
40
  for (const pattern of ignoreSet) {
package/src/index.ts CHANGED
@@ -1,61 +1,78 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { copy } from "./cli/commands/copy";
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
- switch (command) {
12
- case "copy": {
13
- const dir = args[0] ?? ".";
14
- const exts = args[1]?.split(",");
15
- await copy(dir);
16
- break;
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
- case "paste": {
20
- const dir = args[0] ?? ".";
21
- await paste(dir);
22
- break;
23
- }
17
+ case "paste": {
18
+ const dir = args[0] ?? ".";
19
+ await service.paste(dir);
20
+ break;
21
+ }
24
22
 
25
- case "prompt": {
26
- const dir = args[0] ?? ".";
27
- const userPrompt = args.slice(1).join(" ");
28
- if (!userPrompt) {
29
- console.error("Missing user prompt");
30
- process.exit(1);
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
- case "sync": {
37
- const dir = args[0] ?? ".";
38
- await sync(dir);
39
- break;
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
- default:
43
- help();
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 help() {
58
+ function showHelp() {
48
59
  console.log(`
49
60
  Usage:
50
- johankit copy <dir> [exts]
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 prompt src "refatorar para async/await"
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,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
+ }
@@ -1,42 +0,0 @@
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.paste = void 0;
7
- const write_1 = require("../../core/write");
8
- const clipboard_1 = require("../../core/clipboard");
9
- const cleanCodeBlock_1 = __importDefault(require("../../utils/cleanCodeBlock"));
10
- async function paste(dir) {
11
- try {
12
- const content = await (0, clipboard_1.readClipboard)();
13
- if (!content) {
14
- throw new Error("Clipboard empty or inaccessible");
15
- }
16
- let files;
17
- try {
18
- const { lang, cleaned } = (0, cleanCodeBlock_1.default)(content);
19
- files = JSON.parse(cleaned);
20
- }
21
- catch (e) {
22
- throw new Error("Clipboard content is not valid JSON");
23
- }
24
- if (!Array.isArray(files)) {
25
- throw new Error("Clipboard content is not a JSON array");
26
- }
27
- const isValidSnapshot = files.every(f => typeof f.path === 'string' && typeof f.content === 'string');
28
- if (!isValidSnapshot) {
29
- throw new Error("JSON does not match FileSnapshot structure {path, content}");
30
- }
31
- (0, write_1.writeFiles)(dir, files, true);
32
- process.stdout.write("✔ Pasted from clipboard\n");
33
- }
34
- catch (error) {
35
- process.stderr.write("✖ Paste failed\n");
36
- if (error instanceof Error) {
37
- process.stderr.write(`${error.message}\n`);
38
- }
39
- process.exit(1);
40
- }
41
- }
42
- exports.paste = paste;
@@ -1,42 +0,0 @@
1
- import { writeFiles } from "../../core/write";
2
- import { readClipboard } from "../../core/clipboard";
3
- import cleanCodeBlock from "../../utils/cleanCodeBlock";
4
-
5
- export async function paste(dir: string) {
6
- try {
7
- const content = await readClipboard();
8
-
9
- if (!content) {
10
- throw new Error("Clipboard empty or inaccessible");
11
- }
12
-
13
- let files;
14
- try {
15
- const { lang, cleaned } = cleanCodeBlock(content);
16
- files = JSON.parse(cleaned);
17
- } catch (e) {
18
- throw new Error("Clipboard content is not valid JSON");
19
- }
20
-
21
- if (!Array.isArray(files)) {
22
- throw new Error("Clipboard content is not a JSON array");
23
- }
24
-
25
- const isValidSnapshot = files.every(f =>
26
- typeof f.path === 'string' && typeof f.content === 'string'
27
- );
28
-
29
- if (!isValidSnapshot) {
30
- throw new Error("JSON does not match FileSnapshot structure {path, content}");
31
- }
32
-
33
- writeFiles(dir, files, true);
34
- process.stdout.write("✔ Pasted from clipboard\n");
35
- } catch (error) {
36
- process.stderr.write("✖ Paste failed\n");
37
- if (error instanceof Error) {
38
- process.stderr.write(`${error.message}\n`);
39
- }
40
- process.exit(1);
41
- }
42
- }
@@ -1,59 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.prompt = void 0;
4
- // src/cli/commands/prompt.ts
5
- const scan_1 = require("../../core/scan");
6
- const clipboard_1 = require("../../core/clipboard");
7
- const vm_1 = require("../../core/vm");
8
- async function prompt(dir, userPrompt) {
9
- const snapshot = (0, scan_1.scanDir)(dir);
10
- const template = `
11
- You are an AI software engineer.
12
-
13
- You will receive a JSON array representing a snapshot of a codebase.
14
- Each item has the following structure:
15
-
16
- {
17
- "path": "relative/path/to/file.ext",
18
- "content": "full file content"
19
- }
20
-
21
- ---
22
-
23
- SNAPSHOT
24
- ${JSON.stringify(snapshot, null, 2)}
25
-
26
- ---
27
-
28
- YOUR TASK
29
- Propose changes according to the user request.
30
-
31
- Return ONLY a JSON array of patches.
32
-
33
- PATCH FORMAT (STRICT)
34
- {
35
- "path": "relative/path/to/file.ext",
36
- "content": "FULL updated file content (omit for delete)"
37
- }
38
-
39
- IMPORTANT RULES
40
- - Do NOT return explanations
41
- - Do NOT return markdown
42
- - Return ONLY valid JSON inside the \"\`\`\`\"
43
- - Always return within a Markdown Code Block (with \"\`\`\`json\" syntax highlighting)\")
44
-
45
- USER REQUEST
46
- ${userPrompt}
47
- `;
48
- try {
49
- (0, vm_1.runInVm)(`module.exports = async function() { return ${JSON.stringify(template.trim())}; }`)();
50
- await (0, clipboard_1.copyToClipboard)(template.trim());
51
- process.stdout.write(template.trim());
52
- process.stdout.write("\n\n✔ Prompt + Snapshot copied to clipboard\n");
53
- }
54
- catch (e) {
55
- process.stdout.write(template.trim());
56
- process.stderr.write("\n✖ Failed to execute in VM or copy to clipboard\n");
57
- }
58
- }
59
- exports.prompt = prompt;
@@ -1,57 +0,0 @@
1
- // src/cli/commands/prompt.ts
2
- import { scanDir } from "../../core/scan";
3
- import { copyToClipboard } from "../../core/clipboard";
4
- import { runInVm } from "../../core/vm";
5
-
6
- export async function prompt(dir: string, userPrompt: string) {
7
- const snapshot = scanDir(dir);
8
-
9
- const template = `
10
- You are an AI software engineer.
11
-
12
- You will receive a JSON array representing a snapshot of a codebase.
13
- Each item has the following structure:
14
-
15
- {
16
- "path": "relative/path/to/file.ext",
17
- "content": "full file content"
18
- }
19
-
20
- ---
21
-
22
- SNAPSHOT
23
- ${JSON.stringify(snapshot, null, 2)}
24
-
25
- ---
26
-
27
- YOUR TASK
28
- Propose changes according to the user request.
29
-
30
- Return ONLY a JSON array of patches.
31
-
32
- PATCH FORMAT (STRICT)
33
- {
34
- "path": "relative/path/to/file.ext",
35
- "content": "FULL updated file content (omit for delete)"
36
- }
37
-
38
- IMPORTANT RULES
39
- - Do NOT return explanations
40
- - Do NOT return markdown
41
- - Return ONLY valid JSON inside the \"\`\`\`\"
42
- - Always return within a Markdown Code Block (with \"\`\`\`json\" syntax highlighting)\")
43
-
44
- USER REQUEST
45
- ${userPrompt}
46
- `;
47
-
48
- try {
49
- runInVm(`module.exports = async function() { return ${JSON.stringify(template.trim())}; }`)();
50
- await copyToClipboard(template.trim());
51
- process.stdout.write(template.trim());
52
- process.stdout.write("\n\n✔ Prompt + Snapshot copied to clipboard\n");
53
- } catch (e) {
54
- process.stdout.write(template.trim());
55
- process.stderr.write("\n✖ Failed to execute in VM or copy to clipboard\n");
56
- }
57
- }