johankit 0.1.5 → 1.0.0
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 +45 -62
- package/dist/cli/commands/sync.js +37 -57
- package/dist/core/validation.js +6 -18
- package/dist/core/write.js +9 -7
- package/dist/src/cli/commands/copy.js +11 -0
- package/dist/src/cli/commands/paste.js +128 -0
- package/dist/src/cli/commands/prompt.js +54 -0
- package/dist/src/cli/commands/sync.js +166 -0
- package/dist/src/core/clipboard.js +64 -0
- package/dist/src/core/config.js +41 -0
- package/dist/{core → src/core}/diff.js +9 -12
- package/dist/src/core/scan.js +75 -0
- package/dist/src/core/schema.js +18 -0
- package/dist/src/core/validation.js +11 -0
- package/dist/src/core/write.js +24 -0
- package/dist/src/index.js +39 -0
- package/dist/src/tests/cleanCodeBlock.test.js +23 -0
- package/dist/src/tests/scan.test.js +35 -0
- package/dist/src/tests/schema.test.js +22 -0
- package/dist/src/utils/cleanCodeBlock.js +13 -0
- package/dist/types.js +1 -0
- package/johankit.yml +6 -0
- package/package.json +20 -10
- package/src/cli/commands/copy.ts +6 -19
- package/src/cli/commands/paste.ts +79 -31
- package/src/cli/commands/prompt.ts +24 -64
- package/src/cli/commands/sync.ts +112 -71
- package/src/core/clipboard.ts +46 -80
- package/src/core/config.ts +22 -32
- package/src/core/diff.ts +10 -21
- package/src/core/scan.ts +52 -43
- package/src/core/schema.ts +17 -34
- package/src/core/validation.ts +8 -27
- package/src/core/write.ts +11 -17
- package/src/index.ts +43 -77
- package/src/tests/cleanCodeBlock.test.ts +21 -0
- package/src/tests/scan.test.ts +33 -0
- package/src/tests/schema.test.ts +24 -0
- package/src/types.ts +4 -50
- package/src/utils/cleanCodeBlock.ts +12 -12
- package/tsconfig.json +14 -6
- package/Readme.md +0 -56
- package/dist/cli/commands/copy.js +0 -21
- package/dist/cli/commands/paste.js +0 -48
- package/dist/cli/commands/prompt.js +0 -88
- package/dist/cli/commands/three.js +0 -106
- package/dist/cli/commands/tree.js +0 -106
- package/dist/core/clipboard.js +0 -88
- package/dist/core/config.js +0 -51
- package/dist/core/scan.js +0 -66
- package/dist/core/schema.js +0 -40
- package/dist/index.js +0 -72
- package/dist/services/JohankitService.js +0 -59
- package/dist/utils/cleanCodeBlock.js +0 -12
- package/dist/utils/createAsciiTree.js +0 -46
- package/johankit.yaml +0 -2
- package/src/cli/commands/tree.ts +0 -119
- package/src/services/JohankitService.ts +0 -70
- package/src/utils/createAsciiTree.ts +0 -53
- package/types.ts +0 -11
- /package/dist/{core → src/core}/git.js +0 -0
- /package/{types.js → dist/src/types.js} +0 -0
package/src/core/config.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
// src/core/config.ts
|
|
2
1
|
import path from "path";
|
|
3
|
-
import { readFileSync } from "fs";
|
|
2
|
+
import { readFileSync, existsSync } from "fs";
|
|
4
3
|
import { load } from "js-yaml";
|
|
5
4
|
import { Config } from "../types";
|
|
6
5
|
|
|
7
|
-
const CONFIG_FILENAME = "johankit.yaml";
|
|
8
6
|
const DEFAULT_IGNORE = [
|
|
9
7
|
".git",
|
|
10
8
|
"node_modules",
|
|
@@ -15,36 +13,28 @@ const DEFAULT_IGNORE = [
|
|
|
15
13
|
"temp",
|
|
16
14
|
];
|
|
17
15
|
|
|
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
16
|
export function loadConfig(basePath: string): Config {
|
|
25
|
-
|
|
17
|
+
// Lista de possíveis nomes para o arquivo de configuração
|
|
18
|
+
const configFilenames = ["johankit.yaml", "johankit.yml"];
|
|
19
|
+
let loadedConfig: Partial<Config> = {};
|
|
26
20
|
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
};
|
|
21
|
+
for (const filename of configFilenames) {
|
|
22
|
+
const configPath = path.join(basePath, filename);
|
|
23
|
+
if (existsSync(configPath)) {
|
|
24
|
+
try {
|
|
25
|
+
const content = readFileSync(configPath, "utf8");
|
|
26
|
+
loadedConfig = (load(content) as Partial<Config>) || {};
|
|
27
|
+
break; // Para no primeiro que encontrar
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.warn(`[johankit] Erro ao ler ${filename}, tentando próximo...`);
|
|
30
|
+
}
|
|
43
31
|
}
|
|
44
|
-
|
|
45
|
-
console.warn(`[johankit] Aviso: Falha ao carregar ${CONFIG_FILENAME}. Usando defaults.`, error);
|
|
46
|
-
return {
|
|
47
|
-
ignore: DEFAULT_IGNORE,
|
|
48
|
-
};
|
|
49
32
|
}
|
|
50
|
-
|
|
33
|
+
|
|
34
|
+
// Set para garantir que não existam duplicatas nos padrões de ignore
|
|
35
|
+
const ignoreSet = new Set([...DEFAULT_IGNORE, ...(loadedConfig.ignore || [])]);
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
ignore: Array.from(ignoreSet),
|
|
39
|
+
};
|
|
40
|
+
}
|
package/src/core/diff.ts
CHANGED
|
@@ -1,31 +1,20 @@
|
|
|
1
1
|
// src/core/diff.ts
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import path from "path";
|
|
4
|
+
import { PatchItem } from "./schema";
|
|
4
5
|
|
|
5
|
-
export
|
|
6
|
-
type: "modify" | "create" | "delete";
|
|
7
|
-
path: string;
|
|
8
|
-
content?: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function applyDiff(basePath: string, patches: DiffPatch[]) {
|
|
6
|
+
export function applyDiff(basePath: string, patches: PatchItem[]) {
|
|
12
7
|
for (const patch of patches) {
|
|
8
|
+
if (!patch.path) continue;
|
|
13
9
|
const fullPath = path.join(basePath, patch.path);
|
|
14
10
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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;
|
|
11
|
+
if (patch.content === null || patch.content === undefined) {
|
|
12
|
+
if (fs.existsSync(fullPath)) {
|
|
13
|
+
fs.unlinkSync(fullPath);
|
|
28
14
|
}
|
|
15
|
+
} else {
|
|
16
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
17
|
+
fs.writeFileSync(fullPath, patch.content, "utf8");
|
|
29
18
|
}
|
|
30
19
|
}
|
|
31
|
-
}
|
|
20
|
+
}
|
package/src/core/scan.ts
CHANGED
|
@@ -1,70 +1,79 @@
|
|
|
1
|
-
// src/core/scan.ts
|
|
2
1
|
import fs from "fs";
|
|
3
2
|
import path from "path";
|
|
4
|
-
import { FileSnapshot, ScanOptions } from "../types";
|
|
3
|
+
import { FileSnapshot, ScanOptions, Config } from "../types";
|
|
4
|
+
import { loadConfig } from "./config";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
// Cache para evitar re-leitura de configurações durante o mesmo processo
|
|
7
|
+
let cachedConfig: Config | null = null;
|
|
8
|
+
let cachedIgnoreList: string[] | null = null;
|
|
9
|
+
|
|
10
|
+
export function scanDir(basePath: string, options: ScanOptions = {}): FileSnapshot[] {
|
|
10
11
|
const result: FileSnapshot[] = [];
|
|
11
12
|
const base = path.resolve(basePath);
|
|
13
|
+
const exts = options.extensions?.map(e => e.startsWith('.') ? e : `.${e}`);
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
// Default ignores
|
|
18
|
-
const ignoreSet = new Set([
|
|
19
|
-
"node_modules", ".git", "dist", "build", ".DS_Store", "coverage", ".env", "yarn.lock",
|
|
20
|
-
]);
|
|
15
|
+
if (!cachedConfig) {
|
|
16
|
+
cachedConfig = loadConfig(base);
|
|
17
|
+
const ignorePatterns = new Set(cachedConfig.ignore);
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// ignore read errors
|
|
19
|
+
const gitignorePath = path.join(base, '.gitignore');
|
|
20
|
+
if (fs.existsSync(gitignorePath)) {
|
|
21
|
+
try {
|
|
22
|
+
fs.readFileSync(gitignorePath, 'utf8')
|
|
23
|
+
.split('\n')
|
|
24
|
+
.forEach(line => {
|
|
25
|
+
const trimmed = line.trim();
|
|
26
|
+
if (trimmed && !trimmed.startsWith('#')) {
|
|
27
|
+
ignorePatterns.add(trimmed.replace(/^\//, '').replace(/\/$/, ''));
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
} catch { }
|
|
35
31
|
}
|
|
32
|
+
cachedIgnoreList = Array.from(ignorePatterns);
|
|
36
33
|
}
|
|
37
34
|
|
|
38
|
-
|
|
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
|
-
}
|
|
35
|
+
const finalIgnoreList = cachedIgnoreList!;
|
|
46
36
|
|
|
47
37
|
function loop(currentPath: string) {
|
|
48
38
|
const entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
|
49
39
|
|
|
50
40
|
for (const entry of entries) {
|
|
51
|
-
|
|
41
|
+
const name = entry.name;
|
|
42
|
+
|
|
43
|
+
// Otimização: Verificação rápida de binários e pastas ocultas comuns
|
|
44
|
+
if (name.startsWith('.') && name !== '.gitignore') continue;
|
|
45
|
+
if (name.match(/\.(png|jpg|jpeg|gif|pdf|zip|exe|dll|so|db|map|lock)$/i)) continue;
|
|
46
|
+
|
|
47
|
+
const fullPath = path.join(currentPath, name);
|
|
48
|
+
const relPath = path.relative(base, fullPath).replace(/\\/g, '/');
|
|
49
|
+
|
|
50
|
+
const shouldIgnore = finalIgnoreList.some(p =>
|
|
51
|
+
relPath === p || relPath.startsWith(p + '/')
|
|
52
|
+
);
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
if (shouldIgnore) continue;
|
|
54
55
|
|
|
55
56
|
if (entry.isDirectory()) {
|
|
56
57
|
loop(fullPath);
|
|
57
58
|
} else {
|
|
58
|
-
if (exts && !exts.includes(path.extname(
|
|
59
|
+
if (exts && !exts.includes(path.extname(name))) continue;
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
try {
|
|
62
|
+
const stats = fs.statSync(fullPath);
|
|
63
|
+
// Ignora arquivos maiores que 200KB para manter performance do clipboard
|
|
64
|
+
if (stats.size > 200 * 1024) continue;
|
|
65
|
+
|
|
66
|
+
result.push({
|
|
67
|
+
path: relPath,
|
|
68
|
+
content: fs.readFileSync(fullPath, 'utf8')
|
|
69
|
+
});
|
|
70
|
+
} catch (e) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
64
73
|
}
|
|
65
74
|
}
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
loop(base);
|
|
69
78
|
return result;
|
|
70
|
-
}
|
|
79
|
+
}
|
package/src/core/schema.ts
CHANGED
|
@@ -1,41 +1,24 @@
|
|
|
1
1
|
// src/core/schema.ts
|
|
2
|
-
import { DiffPatch } from "./diff";
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
}
|
|
3
|
+
export interface PatchItem {
|
|
4
|
+
path?: string;
|
|
5
|
+
content?: string | null;
|
|
6
|
+
type?: 'console';
|
|
7
|
+
command?: string;
|
|
20
8
|
}
|
|
21
9
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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");
|
|
10
|
+
export function validatePatches(json: any): PatchItem[] {
|
|
11
|
+
if (!Array.isArray(json)) {
|
|
12
|
+
throw new Error("Input must be a valid JSON array");
|
|
31
13
|
}
|
|
14
|
+
// Validação permissiva: ou tem path (arquivo) ou tem type console + command
|
|
15
|
+
return json.map((item, index) => {
|
|
16
|
+
const isFile = typeof item.path === 'string';
|
|
17
|
+
const isCommand = item.type === 'console' && typeof item.command === 'string';
|
|
32
18
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
throw new Error(`Patch inválido no índice ${index}: ${JSON.stringify(patch, null, 2)}.\nEsperado: { type: 'modify'|'create'|'delete', path: string, content?: string }`);
|
|
19
|
+
if (!isFile && !isCommand) {
|
|
20
|
+
throw new Error(`Item at index ${index} is invalid. Must have 'path' or 'type: console'`);
|
|
36
21
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return patches as DiffPatch[];
|
|
41
|
-
}
|
|
22
|
+
return item as PatchItem;
|
|
23
|
+
});
|
|
24
|
+
}
|
package/src/core/validation.ts
CHANGED
|
@@ -1,28 +1,9 @@
|
|
|
1
1
|
// src/core/validation.ts
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
}
|
|
2
|
+
import { validatePatches as sharedValidate } from "./schema";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @deprecated Use validatePatches from core/schema instead.
|
|
6
|
+
*/
|
|
7
|
+
export function validatePatches(json: any): any[] {
|
|
8
|
+
return sharedValidate(json);
|
|
9
|
+
}
|
package/src/core/write.ts
CHANGED
|
@@ -1,26 +1,20 @@
|
|
|
1
1
|
// src/core/write.ts
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import path from "path";
|
|
4
|
-
import { FileSnapshot } from "../types";
|
|
5
4
|
import { ensureGitCommit } from "./git";
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
6
|
+
/**
|
|
7
|
+
* @deprecated Use applyDiff from core/diff for more flexibility (supports deletes and console commands).
|
|
8
|
+
*/
|
|
9
|
+
export function writeFiles(basePath: string, files: any, commit = true) {
|
|
10
|
+
// if (commit && files.length > 0) {
|
|
11
|
+
// ensureGitCommit("johankit: before write");
|
|
12
|
+
// }
|
|
15
13
|
|
|
16
14
|
for (const file of files) {
|
|
15
|
+
if (!file.path) continue;
|
|
17
16
|
const fullPath = path.join(basePath, file.path);
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (!fs.existsSync(dir)) {
|
|
21
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
fs.writeFileSync(fullPath, file.content, "utf8");
|
|
17
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
18
|
+
fs.writeFileSync(fullPath, file.content || "", "utf8");
|
|
25
19
|
}
|
|
26
|
-
}
|
|
20
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,78 +1,44 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function showHelp() {
|
|
59
|
-
console.log(`
|
|
60
|
-
Usage:
|
|
61
|
-
johankit copy <dir>
|
|
62
|
-
johankit paste <dir>
|
|
63
|
-
johankit prompt <dir> "<user request>" [--diff]
|
|
64
|
-
johankit sync <dir>
|
|
65
|
-
johankit tree <dir>
|
|
66
|
-
|
|
67
|
-
Examples:
|
|
68
|
-
johankit copy src
|
|
69
|
-
johankit paste src
|
|
70
|
-
johankit prompt src "refactor to async/await"
|
|
71
|
-
johankit sync src
|
|
72
|
-
johankit tree src
|
|
73
|
-
`);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
main();
|
|
77
|
-
|
|
78
|
-
export { JohankitService };
|
|
2
|
+
import { Command } from "commander";
|
|
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 program = new Command();
|
|
9
|
+
|
|
10
|
+
program
|
|
11
|
+
.name('johankit')
|
|
12
|
+
.description('Developer-friendly CLI for codebase snapshots and AI vibe-coding')
|
|
13
|
+
.version('0.0.3');
|
|
14
|
+
|
|
15
|
+
program
|
|
16
|
+
.command('copy [dir] [exts]')
|
|
17
|
+
.action((dir = '.', exts) => copy(dir, exts?.split(',')));
|
|
18
|
+
|
|
19
|
+
program
|
|
20
|
+
.command('paste [dir]')
|
|
21
|
+
.option('--run', 'execute console commands')
|
|
22
|
+
.option('-y', 'auto accept commands without confirmation')
|
|
23
|
+
.option('--dry-run', 'list changes without applying them')
|
|
24
|
+
.option('--diff', 'show diff and ask for confirmation for each file')
|
|
25
|
+
.action((dir = '.', opts) => paste(dir, !!opts.run, !!opts.dryRun, !!opts.diff));
|
|
26
|
+
|
|
27
|
+
program
|
|
28
|
+
.command('prompt [dir] <request...>')
|
|
29
|
+
.action((dir = '.', request) => prompt(dir, request.join(' ')));
|
|
30
|
+
|
|
31
|
+
program
|
|
32
|
+
.command('sync [dir]')
|
|
33
|
+
.option('--run', 'execute console commands')
|
|
34
|
+
.option('-y', 'auto accept commands without confirmation')
|
|
35
|
+
.option('--dry-run', 'list changes without applying them')
|
|
36
|
+
.option('--diff', 'show diff and ask for confirmation for each file')
|
|
37
|
+
.option('--watch', 'continuously watch clipboard for new patches')
|
|
38
|
+
.option('--auto', 'alias for --watch with auto-accept')
|
|
39
|
+
.action((dir = '.', opts) => {
|
|
40
|
+
const isAuto = !!opts.auto;
|
|
41
|
+
sync(dir, isAuto || !!opts.run, !!opts.dryRun, !!opts.diff, isAuto || !!opts.watch);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
program.parse();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import cleanCodeBlock from '../utils/cleanCodeBlock';
|
|
2
|
+
|
|
3
|
+
describe('cleanCodeBlock', () => {
|
|
4
|
+
it('should extract JSON from markdown blocks', () => {
|
|
5
|
+
const input = 'Check this: ```json [{"path": "test"}] ``` and some text';
|
|
6
|
+
const { cleaned } = cleanCodeBlock(input);
|
|
7
|
+
expect(cleaned).toBe('[{"path": "test"}]');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('should handle raw JSON arrays', () => {
|
|
11
|
+
const input = '[{"path": "test"}]';
|
|
12
|
+
const { cleaned } = cleanCodeBlock(input);
|
|
13
|
+
expect(cleaned).toBe('[{"path": "test"}]');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should remove invisible characters', () => {
|
|
17
|
+
const input = '\uFEFF[{"path": "test"}]';
|
|
18
|
+
const { cleaned } = cleanCodeBlock(input);
|
|
19
|
+
expect(cleaned).toBe('[{"path": "test"}]');
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { scanDir } from '../core/scan';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
describe('scanDir', () => {
|
|
6
|
+
const testDir = path.join(__dirname, 'test-tmp');
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
if (fs.existsSync(testDir)) fs.rmSync(testDir, { recursive: true });
|
|
10
|
+
fs.mkdirSync(testDir);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
if (fs.existsSync(testDir)) fs.rmSync(testDir, { recursive: true });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should scan files in a directory', () => {
|
|
18
|
+
fs.writeFileSync(path.join(testDir, 'test.ts'), 'console.log("hello")');
|
|
19
|
+
const results = scanDir(testDir);
|
|
20
|
+
expect(results.length).toBe(1);
|
|
21
|
+
expect(results[0].path).toBe('test.ts');
|
|
22
|
+
expect(results[0].content).toBe('console.log("hello")');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should respect ignore patterns from config', () => {
|
|
26
|
+
fs.mkdirSync(path.join(testDir, 'node_modules'));
|
|
27
|
+
fs.writeFileSync(path.join(testDir, 'node_modules/ignore.ts'), 'ignore');
|
|
28
|
+
fs.writeFileSync(path.join(testDir, 'keep.ts'), 'keep');
|
|
29
|
+
const results = scanDir(testDir);
|
|
30
|
+
expect(results.find(r => r.path.includes('node_modules'))).toBeUndefined();
|
|
31
|
+
expect(results.find(r => r.path === 'keep.ts')).toBeDefined();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { validatePatches } from '../core/schema';
|
|
2
|
+
|
|
3
|
+
describe('validatePatches', () => {
|
|
4
|
+
it('should validate correct file patches', () => {
|
|
5
|
+
const input = [{ path: 'src/index.ts', content: 'test' }];
|
|
6
|
+
const output = validatePatches(input);
|
|
7
|
+
expect(output).toEqual(input);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('should validate correct console patches', () => {
|
|
11
|
+
const input = [{ type: 'console', command: 'npm install' }];
|
|
12
|
+
const output = validatePatches(input);
|
|
13
|
+
expect(output).toEqual(input);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should throw error for invalid items', () => {
|
|
17
|
+
const input = [{ invalid: 'item' }];
|
|
18
|
+
expect(() => validatePatches(input)).toThrow();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should throw error if input is not an array', () => {
|
|
22
|
+
expect(() => validatePatches({})).toThrow();
|
|
23
|
+
});
|
|
24
|
+
});
|
package/src/types.ts
CHANGED
|
@@ -1,59 +1,13 @@
|
|
|
1
1
|
// src/types.ts
|
|
2
2
|
export interface FileSnapshot {
|
|
3
3
|
path: string;
|
|
4
|
-
content: string;
|
|
4
|
+
content: string | null;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
export interface ScanOptions {
|
|
8
|
-
extensions?: string[];
|
|
9
|
-
// A opção 'ignore' foi movida para o arquivo de configuração e 'loadConfig'
|
|
8
|
+
extensions?: string[];
|
|
10
9
|
}
|
|
11
10
|
|
|
12
11
|
export interface Config {
|
|
13
|
-
ignore: string[];
|
|
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
|
-
}
|
|
12
|
+
ignore: string[];
|
|
13
|
+
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
+
// src/utils/cleanCodeBlock.ts
|
|
1
2
|
export default function cleanCodeBlock(content: string) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
3
|
+
// Regex robusta para capturar o primeiro array JSON ou bloco de código em string suja
|
|
4
|
+
const jsonRegex = /```json\s*([\s\S]*?)\s*```|(\[\s*{[\s\S]*}\s*\])/;
|
|
5
|
+
const match = content.match(jsonRegex);
|
|
6
|
+
|
|
7
|
+
let cleaned = match ? (match[1] || match[2]) : content;
|
|
8
|
+
|
|
9
|
+
cleaned = cleaned.replace(/^\uFEFF/, '');
|
|
10
|
+
cleaned = cleaned.replace(/[\u0000-\u0008\u000B-\u000C\u000E-\u001F\u007F-\u009F]/g, '');
|
|
11
|
+
|
|
12
|
+
return { cleaned: cleaned.trim() };
|
|
13
|
+
}
|