johankit 0.0.2
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 +115 -0
- package/dist/cli/commands/copy.js +24 -0
- package/dist/cli/commands/paste.js +40 -0
- package/dist/cli/commands/prompt.js +57 -0
- package/dist/cli/commands/sync.js +84 -0
- package/dist/core/clipboard.js +54 -0
- package/dist/core/config.js +52 -0
- package/dist/core/diff.js +29 -0
- package/dist/core/git.js +43 -0
- package/dist/core/scan.js +67 -0
- package/dist/core/schema.js +41 -0
- package/dist/core/validation.js +24 -0
- package/dist/core/write.js +24 -0
- package/dist/index.js +54 -0
- package/dist/tests/cli/commands/copy.test.js +47 -0
- package/dist/tests/cli/commands/paste.test.js +41 -0
- package/dist/tests/cli/commands/prompt.test.js +37 -0
- package/dist/tests/cli/commands/sync.test.js +47 -0
- package/dist/tests/core/clipboard.test.js +20 -0
- package/dist/tests/core/config.test.js +23 -0
- package/dist/tests/core/diff.test.js +24 -0
- package/dist/tests/core/git.test.js +11 -0
- package/dist/tests/core/scan.test.js +16 -0
- package/dist/tests/core/schema.test.js +13 -0
- package/dist/tests/core/validation.test.js +13 -0
- package/dist/tests/core/write.test.js +41 -0
- package/dist/types.js +2 -0
- package/jest.config.js +6 -0
- package/package-lock.json +250 -0
- package/package.json +30 -0
- package/src/cli/commands/copy.ts +22 -0
- package/src/cli/commands/paste.ts +43 -0
- package/src/cli/commands/prompt.ts +55 -0
- package/src/cli/commands/sync.ts +88 -0
- package/src/core/clipboard.ts +56 -0
- package/src/core/config.ts +50 -0
- package/src/core/diff.ts +31 -0
- package/src/core/git.ts +39 -0
- package/src/core/scan.ts +70 -0
- package/src/core/schema.ts +41 -0
- package/src/core/validation.ts +28 -0
- package/src/core/write.ts +26 -0
- package/src/index.ts +61 -0
- package/src/tests/cli/commands/copy.test.ts +26 -0
- package/src/tests/cli/commands/paste.test.ts +19 -0
- package/src/tests/cli/commands/prompt.test.ts +14 -0
- package/src/tests/cli/commands/sync.test.ts +26 -0
- package/src/tests/core/clipboard.test.ts +21 -0
- package/src/tests/core/config.test.ts +21 -0
- package/src/tests/core/diff.test.ts +22 -0
- package/src/tests/core/git.test.ts +11 -0
- package/src/tests/core/scan.test.ts +13 -0
- package/src/tests/core/schema.test.ts +13 -0
- package/src/tests/core/validation.test.ts +13 -0
- package/src/tests/core/write.test.ts +15 -0
- package/src/types.ts +14 -0
- package/tsconfig.json +12 -0
- package/types.js +2 -0
- package/types.ts +11 -0
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "johankit",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"johankit": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "jest",
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"start": "ts-node src/index.ts"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [],
|
|
15
|
+
"author": "",
|
|
16
|
+
"license": "ISC",
|
|
17
|
+
"type": "commonjs",
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/jest": "^29.5.3",
|
|
20
|
+
"@types/node": "^25.0.1",
|
|
21
|
+
"jest": "^29.5.0",
|
|
22
|
+
"ts-jest": "^29.1.0",
|
|
23
|
+
"ts-node": "^10.9.2",
|
|
24
|
+
"typescript": "^5.9.3"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"clipboardy": "^5.0.2",
|
|
28
|
+
"commander": "^14.0.2"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { scanDir } from "../../core/scan";
|
|
2
|
+
import { copyToClipboard } from "../../core/clipboard";
|
|
3
|
+
|
|
4
|
+
export function copy(input: string | string[]) {
|
|
5
|
+
let snapshot;
|
|
6
|
+
|
|
7
|
+
if (Array.isArray(input)) {
|
|
8
|
+
snapshot = input.map(path => {
|
|
9
|
+
const fileSnapshot = scanDir(path);
|
|
10
|
+
if (fileSnapshot.length !== 1) {
|
|
11
|
+
throw new Error(`Expected single file for path: ${path}`);
|
|
12
|
+
}
|
|
13
|
+
return fileSnapshot[0];
|
|
14
|
+
});
|
|
15
|
+
} else {
|
|
16
|
+
const stat = scanDir(input);
|
|
17
|
+
snapshot = stat.length === 1 ? stat : scanDir(input);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const clipboardJSON = JSON.stringify(snapshot, null, 2); // <- garante JSON válido
|
|
21
|
+
copyToClipboard(clipboardJSON);
|
|
22
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// src/cli/commands/paste.ts
|
|
2
|
+
import { writeFiles } from "../../core/write";
|
|
3
|
+
import { readClipboard } from "../../core/clipboard";
|
|
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 cleanContent = content.replace(/^\uFEFF/, "").trim();
|
|
16
|
+
files = JSON.parse(cleanContent);
|
|
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
|
+
// 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
|
+
process.stdout.write("✔ Pasted from clipboard\n");
|
|
36
|
+
} catch (error) {
|
|
37
|
+
process.stderr.write("✖ Paste failed\n");
|
|
38
|
+
if (error instanceof Error) {
|
|
39
|
+
process.stderr.write(`${error.message}\n`);
|
|
40
|
+
}
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// src/cli/commands/prompt.ts
|
|
2
|
+
import { scanDir } from "../../core/scan";
|
|
3
|
+
import { copyToClipboard } from "../../core/clipboard";
|
|
4
|
+
|
|
5
|
+
export async function prompt(dir: string, userPrompt: string) {
|
|
6
|
+
const snapshot = scanDir(dir);
|
|
7
|
+
|
|
8
|
+
const template = `
|
|
9
|
+
You are an AI software engineer.
|
|
10
|
+
|
|
11
|
+
You will receive a JSON array representing a snapshot of a codebase.
|
|
12
|
+
Each item has the following structure:
|
|
13
|
+
|
|
14
|
+
{
|
|
15
|
+
"path": "relative/path/to/file.ext",
|
|
16
|
+
"content": "full file content"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
SNAPSHOT
|
|
22
|
+
${JSON.stringify(snapshot, null, 2)}
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
YOUR TASK
|
|
27
|
+
Propose changes according to the user request.
|
|
28
|
+
|
|
29
|
+
Return ONLY a JSON array of patches.
|
|
30
|
+
|
|
31
|
+
PATCH FORMAT (STRICT)
|
|
32
|
+
{
|
|
33
|
+
"path": "relative/path/to/file.ext",
|
|
34
|
+
"content": "FULL updated file content (omit for delete)"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
IMPORTANT RULES
|
|
38
|
+
- Do NOT return explanations
|
|
39
|
+
- Do NOT return markdown
|
|
40
|
+
- Return ONLY valid JSON inside the "\`\`\`"
|
|
41
|
+
- Always return within a Markdown Code Block (with "\`\`\`json" syntax highlighting)")
|
|
42
|
+
|
|
43
|
+
USER REQUEST
|
|
44
|
+
${userPrompt}
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
await copyToClipboard(template.trim());
|
|
49
|
+
process.stdout.write(template.trim());
|
|
50
|
+
process.stdout.write("\n\n✔ Prompt + Snapshot copied to clipboard\n");
|
|
51
|
+
} catch (e) {
|
|
52
|
+
process.stdout.write(template.trim());
|
|
53
|
+
process.stderr.write("\n✖ Failed to copy to clipboard (output only)\n");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// src/cli/commands/sync.ts
|
|
2
|
+
import { scanDir } from "../../core/scan";
|
|
3
|
+
import { applyDiff } from "../../core/diff";
|
|
4
|
+
import { copyToClipboard } from "../../core/clipboard";
|
|
5
|
+
import { validatePatches } from "../../core/validation";
|
|
6
|
+
import { writeFiles } from "../../core/write";
|
|
7
|
+
|
|
8
|
+
export async function sync(dir: string) {
|
|
9
|
+
try {
|
|
10
|
+
const snapshotBefore = scanDir(dir);
|
|
11
|
+
|
|
12
|
+
// Gera prompt genérico para AI com snapshot atual
|
|
13
|
+
const template = `
|
|
14
|
+
You are an AI software engineer.
|
|
15
|
+
|
|
16
|
+
You will receive a JSON array representing a snapshot of a codebase.
|
|
17
|
+
Each item has the following structure:
|
|
18
|
+
|
|
19
|
+
\`\`\`json
|
|
20
|
+
{
|
|
21
|
+
"path": "relative/path/to/file.ext",
|
|
22
|
+
"content": "full file content"
|
|
23
|
+
}
|
|
24
|
+
\`\`\`
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
SNAPSHOT
|
|
29
|
+
${JSON.stringify(snapshotBefore, null, 2)}
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
YOUR TASK
|
|
34
|
+
Propose changes according to the user request.
|
|
35
|
+
|
|
36
|
+
Return ONLY a JSON array of patches.
|
|
37
|
+
|
|
38
|
+
PATCH FORMAT (STRICT)
|
|
39
|
+
{
|
|
40
|
+
\"path\": \"relative/path/to/file.ext\",
|
|
41
|
+
\"content\": \"FULL updated file content (omit for delete)\"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
IMPORTANT RULES
|
|
45
|
+
- Do NOT return explanations
|
|
46
|
+
- Do NOT return markdown
|
|
47
|
+
- Return ONLY valid JSON
|
|
48
|
+
|
|
49
|
+
USER REQUEST
|
|
50
|
+
<Replace this with the user request>
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
await copyToClipboard(template.trim());
|
|
54
|
+
process.stdout.write("✔ Prompt with snapshot copied to clipboard\n");
|
|
55
|
+
|
|
56
|
+
// Aguarda entrada do usuário (resposta da AI) via stdin
|
|
57
|
+
const input = await readStdin();
|
|
58
|
+
|
|
59
|
+
let patches;
|
|
60
|
+
try {
|
|
61
|
+
patches = JSON.parse(input);
|
|
62
|
+
} catch {
|
|
63
|
+
throw new Error("Invalid JSON input");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const validated = validatePatches(patches);
|
|
67
|
+
applyDiff(dir, validated);
|
|
68
|
+
|
|
69
|
+
const snapshotAfter = scanDir(dir);
|
|
70
|
+
await copyToClipboard(JSON.stringify(snapshotAfter, null, 2));
|
|
71
|
+
|
|
72
|
+
process.stdout.write("✔ Sync applied and new snapshot copied to clipboard\n");
|
|
73
|
+
} catch (error) {
|
|
74
|
+
process.stderr.write("✖ Sync failed\n");
|
|
75
|
+
if (error instanceof Error) {
|
|
76
|
+
process.stderr.write(`${error.message}\n`);
|
|
77
|
+
}
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readStdin(): Promise<string> {
|
|
83
|
+
return new Promise(resolve => {
|
|
84
|
+
let data = "";
|
|
85
|
+
process.stdin.on("data", c => (data += c));
|
|
86
|
+
process.stdin.on("end", () => resolve(data));
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// src/core/clipboard.ts
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
|
|
4
|
+
export function copyToClipboard(text: string): Promise<void> {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
let command = "xclip";
|
|
7
|
+
let args = ["-selection", "clipboard"];
|
|
8
|
+
|
|
9
|
+
if (process.platform === "darwin") {
|
|
10
|
+
command = "pbcopy";
|
|
11
|
+
args = [];
|
|
12
|
+
} else if (process.platform === "win32") {
|
|
13
|
+
command = "clip";
|
|
14
|
+
args = [];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const child = spawn(command, args, { stdio: ["pipe", "ignore", "ignore"] });
|
|
18
|
+
|
|
19
|
+
child.on("error", (err) => reject(err));
|
|
20
|
+
child.on("close", () => resolve());
|
|
21
|
+
|
|
22
|
+
child.stdin.write(text);
|
|
23
|
+
child.stdin.end();
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function readClipboard(): Promise<string> {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
let command = "xclip";
|
|
30
|
+
let args = ["-selection", "clipboard", "-o"];
|
|
31
|
+
|
|
32
|
+
if (process.platform === "darwin") {
|
|
33
|
+
command = "pbpaste";
|
|
34
|
+
args = [];
|
|
35
|
+
} else if (process.platform === "win32") {
|
|
36
|
+
command = "powershell";
|
|
37
|
+
args = ["-command", "Get-Clipboard"];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
41
|
+
let output = "";
|
|
42
|
+
let error = "";
|
|
43
|
+
|
|
44
|
+
child.stdout.on("data", (d) => (output += d.toString()));
|
|
45
|
+
child.stderr.on("data", (d) => (error += d.toString()));
|
|
46
|
+
|
|
47
|
+
child.on("error", (err) => reject(err));
|
|
48
|
+
child.on("close", (code) => {
|
|
49
|
+
if (code !== 0 && error && !output) {
|
|
50
|
+
reject(new Error(error || "Clipboard read failed"));
|
|
51
|
+
} else {
|
|
52
|
+
resolve(output.trim().replace(/^\uFEFF/, ""));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// src/core/config.ts
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { readFileSync } from "fs";
|
|
4
|
+
import { load } from "js-yaml";
|
|
5
|
+
import { Config } from "../types";
|
|
6
|
+
|
|
7
|
+
const CONFIG_FILENAME = "johankit.yaml";
|
|
8
|
+
const DEFAULT_IGNORE = [
|
|
9
|
+
".git",
|
|
10
|
+
"node_modules",
|
|
11
|
+
"dist",
|
|
12
|
+
"build",
|
|
13
|
+
"coverage",
|
|
14
|
+
"tmp",
|
|
15
|
+
"temp",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Tenta carregar as configurações do arquivo johankit.yaml na basePath.
|
|
20
|
+
* Retorna um objeto Config com defaults se o arquivo não for encontrado.
|
|
21
|
+
* @param basePath O diretório base para procurar o arquivo de configuração.
|
|
22
|
+
* @returns O objeto de configuração.
|
|
23
|
+
*/
|
|
24
|
+
export function loadConfig(basePath: string): Config {
|
|
25
|
+
const configPath = path.join(basePath, CONFIG_FILENAME);
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const content = readFileSync(configPath, "utf8");
|
|
29
|
+
const loadedConfig = load(content) as Partial<Config>;
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
ignore: [
|
|
33
|
+
...DEFAULT_IGNORE,
|
|
34
|
+
...(loadedConfig.ignore || []),
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (error instanceof Error && (error as any).code === "ENOENT") {
|
|
39
|
+
// Arquivo não encontrado, retorna configuração padrão
|
|
40
|
+
return {
|
|
41
|
+
ignore: DEFAULT_IGNORE,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.warn(`[johankit] Aviso: Falha ao carregar ${CONFIG_FILENAME}. Usando defaults.`, error);
|
|
46
|
+
return {
|
|
47
|
+
ignore: DEFAULT_IGNORE,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/core/diff.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// src/core/diff.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
export interface DiffPatch {
|
|
6
|
+
type: "modify" | "create" | "delete";
|
|
7
|
+
path: string;
|
|
8
|
+
content?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function applyDiff(basePath: string, patches: DiffPatch[]) {
|
|
12
|
+
for (const patch of patches) {
|
|
13
|
+
const fullPath = path.join(basePath, patch.path);
|
|
14
|
+
|
|
15
|
+
switch (patch.type) {
|
|
16
|
+
case "delete": {
|
|
17
|
+
if (fs.existsSync(fullPath)) {
|
|
18
|
+
fs.unlinkSync(fullPath);
|
|
19
|
+
}
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
case "create":
|
|
24
|
+
case "modify": {
|
|
25
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
26
|
+
fs.writeFileSync(fullPath, patch.content ?? "", "utf8");
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/core/git.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// src/core/git.ts
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import crypto from "crypto";
|
|
4
|
+
|
|
5
|
+
function getDiffHash() {
|
|
6
|
+
try {
|
|
7
|
+
const diff = execSync("git diff --staged", { encoding: "utf8" });
|
|
8
|
+
if (!diff.trim()) return null;
|
|
9
|
+
|
|
10
|
+
return crypto
|
|
11
|
+
.createHash("sha1")
|
|
12
|
+
.update(diff)
|
|
13
|
+
.digest("hex")
|
|
14
|
+
.slice(0, 7);
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function defaultCommitMessage(prefix = "Auto Commit") {
|
|
21
|
+
const timestamp = new Date().toISOString().slice(0, 16);
|
|
22
|
+
const diffHash = getDiffHash();
|
|
23
|
+
|
|
24
|
+
return diffHash
|
|
25
|
+
? `${prefix}: ${timestamp} · ${diffHash}`
|
|
26
|
+
: `${prefix}: ${timestamp}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function ensureGitCommit(message?: string) {
|
|
30
|
+
try {
|
|
31
|
+
execSync("git rev-parse --is-inside-work-tree", { stdio: "ignore" });
|
|
32
|
+
execSync("git add .");
|
|
33
|
+
|
|
34
|
+
const commitMessage = message ?? defaultCommitMessage();
|
|
35
|
+
execSync(`git commit -m "${commitMessage}"`, { stdio: "ignore" });
|
|
36
|
+
} catch {
|
|
37
|
+
// noop: no git or nothing to commit
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/core/scan.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// src/core/scan.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { FileSnapshot, ScanOptions } from "../types";
|
|
5
|
+
|
|
6
|
+
export function scanDir(
|
|
7
|
+
basePath: string,
|
|
8
|
+
options: ScanOptions = {}
|
|
9
|
+
): FileSnapshot[] {
|
|
10
|
+
const result: FileSnapshot[] = [];
|
|
11
|
+
const base = path.resolve(basePath);
|
|
12
|
+
|
|
13
|
+
const exts = options.extensions?.map(e =>
|
|
14
|
+
e.startsWith(".") ? e : `.${e}`
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
// Default ignores
|
|
18
|
+
const ignoreSet = new Set([
|
|
19
|
+
"node_modules", ".git", "dist", "build", ".DS_Store", "coverage", ".env", "yarn.lock",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
// Read .gitignore if exists
|
|
23
|
+
const gitignorePath = path.join(base, ".gitignore");
|
|
24
|
+
if (fs.existsSync(gitignorePath)) {
|
|
25
|
+
try {
|
|
26
|
+
const lines = fs.readFileSync(gitignorePath, "utf8").split("\n");
|
|
27
|
+
for (const line of lines) {
|
|
28
|
+
const trimmed = line.trim();
|
|
29
|
+
if (trimmed && !trimmed.startsWith("#")) {
|
|
30
|
+
ignoreSet.add(trimmed.replace(/^\//, "").replace(/\/$/, ""));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
} catch (e) {
|
|
34
|
+
// ignore read errors
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function shouldIgnore(name: string): boolean {
|
|
39
|
+
if (ignoreSet.has(name)) return true;
|
|
40
|
+
for (const pattern of ignoreSet) {
|
|
41
|
+
if (pattern.startsWith("*") && name.endsWith(pattern.slice(1))) return true;
|
|
42
|
+
if (name.startsWith(pattern + "/")) return true;
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function loop(currentPath: string) {
|
|
48
|
+
const entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
|
49
|
+
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
if (shouldIgnore(entry.name)) continue;
|
|
52
|
+
|
|
53
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
54
|
+
|
|
55
|
+
if (entry.isDirectory()) {
|
|
56
|
+
loop(fullPath);
|
|
57
|
+
} else {
|
|
58
|
+
if (exts && !exts.includes(path.extname(entry.name))) continue;
|
|
59
|
+
|
|
60
|
+
result.push({
|
|
61
|
+
path: path.relative(base, fullPath).replace(/\\/g, "/"),
|
|
62
|
+
content: fs.readFileSync(fullPath, "utf8"),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
loop(base);
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// src/core/schema.ts
|
|
2
|
+
import { DiffPatch } from "./diff";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Valida se um objeto se parece com um Patch de DiffPatch válido.
|
|
6
|
+
* Não faz validação completa de esquema (JSON Schema), mas verifica a estrutura básica.
|
|
7
|
+
*/
|
|
8
|
+
function isValidPatch(patch: any): boolean {
|
|
9
|
+
if (typeof patch !== "object" || patch === null) return false;
|
|
10
|
+
if (typeof patch.path !== "string" || patch.path.length === 0) return false;
|
|
11
|
+
|
|
12
|
+
const validTypes = ["modify", "create", "delete"];
|
|
13
|
+
if (!validTypes.includes(patch.type)) return false;
|
|
14
|
+
|
|
15
|
+
if (patch.type === "delete") {
|
|
16
|
+
return patch.content === undefined;
|
|
17
|
+
} else {
|
|
18
|
+
return typeof patch.content === "string";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Valida um array de patches de diff (DiffPatch[]).
|
|
24
|
+
* @param patches O array a ser validado.
|
|
25
|
+
* @returns O array de patches se for válido.
|
|
26
|
+
* @throws Um erro se a validação falhar.
|
|
27
|
+
*/
|
|
28
|
+
export function validatePatches(patches: any): DiffPatch[] {
|
|
29
|
+
if (!Array.isArray(patches)) {
|
|
30
|
+
throw new Error("O patch deve ser um array JSON válido");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const [index, patch] of patches.entries()) {
|
|
34
|
+
if (!isValidPatch(patch)) {
|
|
35
|
+
throw new Error(`Patch inválido no índice ${index}: ${JSON.stringify(patch, null, 2)}.\nEsperado: { type: 'modify'|'create'|'delete', path: string, content?: string }`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Assume que o array validado está no formato correto de DiffPatch[]
|
|
40
|
+
return patches as DiffPatch[];
|
|
41
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// src/core/validation.ts
|
|
2
|
+
import { DiffPatch } from "./diff";
|
|
3
|
+
|
|
4
|
+
export function validatePatches(json: any): DiffPatch[] {
|
|
5
|
+
if (!Array.isArray(json)) {
|
|
6
|
+
throw new Error("Validation Error: Input is not a JSON array.");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return json.map((item, index) => {
|
|
10
|
+
if (typeof item !== "object" || item === null) {
|
|
11
|
+
throw new Error(`Validation Error: Item at index ${index} is not an object.`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (!["modify", "create", "delete"].includes(item.type)) {
|
|
15
|
+
throw new Error(`Validation Error: Invalid type '${item.type}' at index ${index}.`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (typeof item.path !== "string" || !item.path.trim()) {
|
|
19
|
+
throw new Error(`Validation Error: Invalid or missing path at index ${index}.`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (item.type !== "delete" && typeof item.content !== "string") {
|
|
23
|
+
throw new Error(`Validation Error: Missing content for '${item.type}' at index ${index}.`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return item as DiffPatch;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// src/core/write.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { FileSnapshot } from "../types";
|
|
5
|
+
import { ensureGitCommit } from "./git";
|
|
6
|
+
|
|
7
|
+
export function writeFiles(
|
|
8
|
+
basePath: string,
|
|
9
|
+
files: FileSnapshot[],
|
|
10
|
+
commit = true
|
|
11
|
+
) {
|
|
12
|
+
if (commit) {
|
|
13
|
+
ensureGitCommit("johankit: before paste");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
for (const file of files) {
|
|
17
|
+
const fullPath = path.join(basePath, file.path);
|
|
18
|
+
const dir = path.dirname(fullPath);
|
|
19
|
+
|
|
20
|
+
if (!fs.existsSync(dir)) {
|
|
21
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
fs.writeFileSync(fullPath, file.content, "utf8");
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
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";
|
|
7
|
+
|
|
8
|
+
const [, , command, ...args] = process.argv;
|
|
9
|
+
|
|
10
|
+
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
|
+
}
|
|
18
|
+
|
|
19
|
+
case "paste": {
|
|
20
|
+
const dir = args[0] ?? ".";
|
|
21
|
+
await paste(dir);
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
|
|
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);
|
|
31
|
+
}
|
|
32
|
+
await prompt(dir, userPrompt);
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
case "sync": {
|
|
37
|
+
const dir = args[0] ?? ".";
|
|
38
|
+
await sync(dir);
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
default:
|
|
43
|
+
help();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function help() {
|
|
48
|
+
console.log(`
|
|
49
|
+
Usage:
|
|
50
|
+
johankit copy <dir> [exts]
|
|
51
|
+
johankit paste <dir>
|
|
52
|
+
johankit prompt <dir> "<user request>"
|
|
53
|
+
johankit sync <dir>
|
|
54
|
+
|
|
55
|
+
Examples:
|
|
56
|
+
johankit prompt src "refatorar para async/await"
|
|
57
|
+
johankit sync src
|
|
58
|
+
`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
main();
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { copy } from '../../../cli/commands/copy';
|
|
2
|
+
import * as scan from '../../../core/scan';
|
|
3
|
+
import * as clipboard from '../../../core/clipboard';
|
|
4
|
+
|
|
5
|
+
jest.mock('../../../core/scan');
|
|
6
|
+
jest.mock('../../../core/clipboard');
|
|
7
|
+
|
|
8
|
+
describe('copy', () => {
|
|
9
|
+
it('should copy single file snapshot to clipboard', () => {
|
|
10
|
+
(scan.scanDir as jest.Mock).mockReturnValue([{ path: 'file.txt', content: 'hello' }]);
|
|
11
|
+
copy('file.txt');
|
|
12
|
+
expect(clipboard.copyToClipboard).toHaveBeenCalledWith(JSON.stringify([{ path: 'file.txt', content: 'hello' }], null, 2));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should copy multiple file snapshots to clipboard', () => {
|
|
16
|
+
(scan.scanDir as jest.Mock)
|
|
17
|
+
.mockReturnValueOnce([{ path: 'file1.txt', content: 'a' }])
|
|
18
|
+
.mockReturnValueOnce([{ path: 'file2.txt', content: 'b' }]);
|
|
19
|
+
|
|
20
|
+
copy(['file1.txt', 'file2.txt']);
|
|
21
|
+
expect(clipboard.copyToClipboard).toHaveBeenCalledWith(JSON.stringify([
|
|
22
|
+
{ path: 'file1.txt', content: 'a' },
|
|
23
|
+
{ path: 'file2.txt', content: 'b' }
|
|
24
|
+
], null, 2));
|
|
25
|
+
});
|
|
26
|
+
});
|