johankit 0.1.5 → 0.4.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 +26 -58
- 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 +120 -0
- package/dist/src/cli/commands/prompt.js +54 -0
- package/dist/src/cli/commands/sync.js +173 -0
- package/dist/src/core/clipboard.js +64 -0
- package/dist/src/core/config.js +39 -0
- package/dist/{core → src/core}/diff.js +9 -12
- package/dist/src/core/scan.js +70 -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 +34 -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 +21 -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 +70 -31
- package/src/cli/commands/prompt.ts +24 -64
- package/src/cli/commands/sync.ts +121 -73
- package/src/core/clipboard.ts +46 -80
- package/src/core/config.ts +20 -32
- package/src/core/diff.ts +10 -21
- package/src/core/scan.ts +43 -40
- 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 +38 -77
- package/src/types.ts +4 -50
- package/src/utils/cleanCodeBlock.ts +17 -8
- 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
|
@@ -0,0 +1,64 @@
|
|
|
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.copyToClipboard = copyToClipboard;
|
|
7
|
+
exports.readClipboard = readClipboard;
|
|
8
|
+
// src/core/clipboard.ts
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const clipboardy_1 = __importDefault(require("clipboardy"));
|
|
11
|
+
async function copyToClipboard(text) {
|
|
12
|
+
const platform = process.platform;
|
|
13
|
+
const isWSL = process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP;
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
let command = "";
|
|
16
|
+
let args = [];
|
|
17
|
+
if (isWSL) {
|
|
18
|
+
command = "clip.exe";
|
|
19
|
+
}
|
|
20
|
+
else if (platform === "darwin") {
|
|
21
|
+
command = "pbcopy";
|
|
22
|
+
}
|
|
23
|
+
else if (platform === "win32") {
|
|
24
|
+
command = "clip";
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
command = "xclip";
|
|
28
|
+
args = ["-selection", "clipboard"];
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const child = (0, child_process_1.spawn)(command, args);
|
|
32
|
+
child.on("error", (err) => reject(err));
|
|
33
|
+
child.on("close", (code) => {
|
|
34
|
+
if (code === 0)
|
|
35
|
+
resolve();
|
|
36
|
+
else
|
|
37
|
+
reject(new Error(`Clipboard error: ${code}`));
|
|
38
|
+
});
|
|
39
|
+
child.stdin.write(text);
|
|
40
|
+
child.stdin.end();
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
reject(e);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async function readClipboard() {
|
|
48
|
+
try {
|
|
49
|
+
return await clipboardy_1.default.read();
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
const platform = process.platform;
|
|
54
|
+
let command = platform === "darwin" ? "pbpaste" : platform === "win32" ? "powershell" : "xclip";
|
|
55
|
+
let args = platform === "win32" ? ["-NoProfile", "-Command", "Get-Clipboard"] :
|
|
56
|
+
platform === "linux" ? ["-selection", "clipboard", "-o"] : [];
|
|
57
|
+
const child = (0, child_process_1.spawn)(command, args);
|
|
58
|
+
let output = "";
|
|
59
|
+
child.stdout.on("data", (d) => (output += d.toString()));
|
|
60
|
+
child.on("close", () => resolve(output.trim()));
|
|
61
|
+
child.on("error", () => resolve(""));
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
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.loadConfig = loadConfig;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const fs_1 = require("fs");
|
|
9
|
+
const js_yaml_1 = require("js-yaml");
|
|
10
|
+
const DEFAULT_IGNORE = [
|
|
11
|
+
".git",
|
|
12
|
+
"node_modules",
|
|
13
|
+
"dist",
|
|
14
|
+
"build",
|
|
15
|
+
"coverage",
|
|
16
|
+
"tmp",
|
|
17
|
+
"temp",
|
|
18
|
+
];
|
|
19
|
+
function loadConfig(basePath) {
|
|
20
|
+
const configFilenames = ["johankit.yaml", "johankit.yml"];
|
|
21
|
+
let loadedConfig = {};
|
|
22
|
+
for (const filename of configFilenames) {
|
|
23
|
+
const configPath = path_1.default.join(basePath, filename);
|
|
24
|
+
if ((0, fs_1.existsSync)(configPath)) {
|
|
25
|
+
try {
|
|
26
|
+
const content = (0, fs_1.readFileSync)(configPath, "utf8");
|
|
27
|
+
loadedConfig = (0, js_yaml_1.load)(content) || {};
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
console.warn(`[johankit] Erro ao ler ${filename}, tentando próximo...`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const ignoreSet = new Set([...DEFAULT_IGNORE, ...(loadedConfig.ignore || [])]);
|
|
36
|
+
return {
|
|
37
|
+
ignore: Array.from(ignoreSet),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -9,20 +9,17 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
10
|
function applyDiff(basePath, patches) {
|
|
11
11
|
for (const patch of patches) {
|
|
12
|
+
if (!patch.path)
|
|
13
|
+
continue;
|
|
12
14
|
const fullPath = path_1.default.join(basePath, patch.path);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
fs_1.default.unlinkSync(fullPath);
|
|
17
|
-
}
|
|
18
|
-
break;
|
|
19
|
-
}
|
|
20
|
-
case "create":
|
|
21
|
-
case "modify": {
|
|
22
|
-
fs_1.default.mkdirSync(path_1.default.dirname(fullPath), { recursive: true });
|
|
23
|
-
fs_1.default.writeFileSync(fullPath, patch.content ?? "", "utf8");
|
|
24
|
-
break;
|
|
15
|
+
if (patch.content === null || patch.content === undefined) {
|
|
16
|
+
if (fs_1.default.existsSync(fullPath)) {
|
|
17
|
+
fs_1.default.unlinkSync(fullPath);
|
|
25
18
|
}
|
|
26
19
|
}
|
|
20
|
+
else {
|
|
21
|
+
fs_1.default.mkdirSync(path_1.default.dirname(fullPath), { recursive: true });
|
|
22
|
+
fs_1.default.writeFileSync(fullPath, patch.content, "utf8");
|
|
23
|
+
}
|
|
27
24
|
}
|
|
28
25
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
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.scanDir = scanDir;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const config_1 = require("./config");
|
|
10
|
+
function scanDir(basePath, options = {}) {
|
|
11
|
+
const result = [];
|
|
12
|
+
const base = path_1.default.resolve(basePath);
|
|
13
|
+
const exts = options.extensions?.map(e => e.startsWith('.') ? e : `.${e}`);
|
|
14
|
+
const config = (0, config_1.loadConfig)(base);
|
|
15
|
+
const ignorePatterns = new Set(config.ignore);
|
|
16
|
+
// Lê .gitignore e adiciona ao Set
|
|
17
|
+
const gitignorePath = path_1.default.join(base, '.gitignore');
|
|
18
|
+
if (fs_1.default.existsSync(gitignorePath)) {
|
|
19
|
+
try {
|
|
20
|
+
fs_1.default.readFileSync(gitignorePath, 'utf8')
|
|
21
|
+
.split('\n')
|
|
22
|
+
.forEach(line => {
|
|
23
|
+
const trimmed = line.trim();
|
|
24
|
+
if (trimmed && !trimmed.startsWith('#')) {
|
|
25
|
+
// Normaliza o caminho do gitignore para o padrão do scanner
|
|
26
|
+
ignorePatterns.add(trimmed.replace(/^\//, '').replace(/\/$/, ''));
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch { }
|
|
31
|
+
}
|
|
32
|
+
// PERFORMANCE: Converte o Set em Array UMA VEZ antes de iniciar o loop
|
|
33
|
+
const finalIgnoreList = Array.from(ignorePatterns);
|
|
34
|
+
function loop(currentPath) {
|
|
35
|
+
const entries = fs_1.default.readdirSync(currentPath, { withFileTypes: true });
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
const isBinary = entry.name.match(/\.(png|jpg|jpeg|gif|pdf|zip|exe|dll|so|db)$/i);
|
|
38
|
+
if (isBinary)
|
|
39
|
+
continue;
|
|
40
|
+
const fullPath = path_1.default.join(currentPath, entry.name);
|
|
41
|
+
const relPath = path_1.default.relative(base, fullPath).replace(/\\/g, '/');
|
|
42
|
+
// PERFORMANCE: O some() agora opera sobre um array fixo, sem Array.from() interno
|
|
43
|
+
const shouldIgnore = finalIgnoreList.some(p => relPath === p || relPath.startsWith(p + '/'));
|
|
44
|
+
if (shouldIgnore)
|
|
45
|
+
continue;
|
|
46
|
+
if (entry.isDirectory()) {
|
|
47
|
+
loop(fullPath);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
if (exts && !exts.includes(path_1.default.extname(entry.name)))
|
|
51
|
+
continue;
|
|
52
|
+
try {
|
|
53
|
+
// PERFORMANCE: Pular arquivos muito grandes evita travar o clipboard
|
|
54
|
+
const stats = fs_1.default.statSync(fullPath);
|
|
55
|
+
if (stats.size > 1024 * 300)
|
|
56
|
+
continue; // Reduzi para 300KB para ser mais seguro
|
|
57
|
+
result.push({
|
|
58
|
+
path: relPath,
|
|
59
|
+
content: fs_1.default.readFileSync(fullPath, 'utf8')
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
loop(base);
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/core/schema.ts
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.validatePatches = validatePatches;
|
|
5
|
+
function validatePatches(json) {
|
|
6
|
+
if (!Array.isArray(json)) {
|
|
7
|
+
throw new Error("Input must be a valid JSON array");
|
|
8
|
+
}
|
|
9
|
+
// Validação permissiva: ou tem path (arquivo) ou tem type console + command
|
|
10
|
+
return json.map((item, index) => {
|
|
11
|
+
const isFile = typeof item.path === 'string';
|
|
12
|
+
const isCommand = item.type === 'console' && typeof item.command === 'string';
|
|
13
|
+
if (!isFile && !isCommand) {
|
|
14
|
+
throw new Error(`Item at index ${index} is invalid. Must have 'path' or 'type: console'`);
|
|
15
|
+
}
|
|
16
|
+
return item;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validatePatches = validatePatches;
|
|
4
|
+
// src/core/validation.ts
|
|
5
|
+
const schema_1 = require("./schema");
|
|
6
|
+
/**
|
|
7
|
+
* @deprecated Use validatePatches from core/schema instead.
|
|
8
|
+
*/
|
|
9
|
+
function validatePatches(json) {
|
|
10
|
+
return (0, schema_1.validatePatches)(json);
|
|
11
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
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.writeFiles = writeFiles;
|
|
7
|
+
// src/core/write.ts
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
/**
|
|
11
|
+
* @deprecated Use applyDiff from core/diff for more flexibility (supports deletes and console commands).
|
|
12
|
+
*/
|
|
13
|
+
function writeFiles(basePath, files, commit = true) {
|
|
14
|
+
// if (commit && files.length > 0) {
|
|
15
|
+
// ensureGitCommit("johankit: before write");
|
|
16
|
+
// }
|
|
17
|
+
for (const file of files) {
|
|
18
|
+
if (!file.path)
|
|
19
|
+
continue;
|
|
20
|
+
const fullPath = path_1.default.join(basePath, file.path);
|
|
21
|
+
fs_1.default.mkdirSync(path_1.default.dirname(fullPath), { recursive: true });
|
|
22
|
+
fs_1.default.writeFileSync(fullPath, file.content || "", "utf8");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const copy_1 = require("./cli/commands/copy");
|
|
6
|
+
const paste_1 = require("./cli/commands/paste");
|
|
7
|
+
const prompt_1 = require("./cli/commands/prompt");
|
|
8
|
+
const sync_1 = require("./cli/commands/sync");
|
|
9
|
+
const program = new commander_1.Command();
|
|
10
|
+
program
|
|
11
|
+
.name('johankit')
|
|
12
|
+
.description('Developer-friendly CLI for codebase snapshots and AI vibe-coding')
|
|
13
|
+
.version('0.0.3');
|
|
14
|
+
program
|
|
15
|
+
.command('copy [dir] [exts]')
|
|
16
|
+
.action((dir = '.', exts) => (0, copy_1.copy)(dir, exts?.split(',')));
|
|
17
|
+
program
|
|
18
|
+
.command('paste [dir]')
|
|
19
|
+
.option('--run', 'execute console commands')
|
|
20
|
+
.option('-y', 'auto accept commands without confirmation')
|
|
21
|
+
.option('--dry-run', 'list changes without applying them')
|
|
22
|
+
.option('--diff', 'show diff and ask for confirmation for each file')
|
|
23
|
+
.action((dir = '.', opts) => (0, paste_1.paste)(dir, !!opts.run, !!opts.dryRun, !!opts.diff));
|
|
24
|
+
program
|
|
25
|
+
.command('prompt [dir] <request...>')
|
|
26
|
+
.action((dir = '.', request) => (0, prompt_1.prompt)(dir, request.join(' ')));
|
|
27
|
+
program
|
|
28
|
+
.command('sync [dir]')
|
|
29
|
+
.option('--run', 'execute console commands')
|
|
30
|
+
.option('-y', 'auto accept commands without confirmation')
|
|
31
|
+
.option('--dry-run', 'list changes without applying them')
|
|
32
|
+
.option('--diff', 'show diff and ask for confirmation for each file')
|
|
33
|
+
.action((dir = '.', opts) => (0, sync_1.sync)(dir, !!opts.run, !!opts.dryRun, !!opts.diff));
|
|
34
|
+
program.parse();
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
const cleanCodeBlock_1 = __importDefault(require("../utils/cleanCodeBlock"));
|
|
7
|
+
describe('cleanCodeBlock', () => {
|
|
8
|
+
it('should extract JSON from markdown blocks', () => {
|
|
9
|
+
const input = 'Check this: ```json [{"path": "test"}] ``` and some text';
|
|
10
|
+
const { cleaned } = (0, cleanCodeBlock_1.default)(input);
|
|
11
|
+
expect(cleaned).toBe('[{"path": "test"}]');
|
|
12
|
+
});
|
|
13
|
+
it('should handle raw JSON arrays', () => {
|
|
14
|
+
const input = '[{"path": "test"}]';
|
|
15
|
+
const { cleaned } = (0, cleanCodeBlock_1.default)(input);
|
|
16
|
+
expect(cleaned).toBe('[{"path": "test"}]');
|
|
17
|
+
});
|
|
18
|
+
it('should remove invisible characters', () => {
|
|
19
|
+
const input = '\uFEFF[{"path": "test"}]';
|
|
20
|
+
const { cleaned } = (0, cleanCodeBlock_1.default)(input);
|
|
21
|
+
expect(cleaned).toBe('[{"path": "test"}]');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
const scan_1 = require("../core/scan");
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
describe('scanDir', () => {
|
|
10
|
+
const testDir = path_1.default.join(__dirname, 'test-tmp');
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
if (fs_1.default.existsSync(testDir))
|
|
13
|
+
fs_1.default.rmSync(testDir, { recursive: true });
|
|
14
|
+
fs_1.default.mkdirSync(testDir);
|
|
15
|
+
});
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
if (fs_1.default.existsSync(testDir))
|
|
18
|
+
fs_1.default.rmSync(testDir, { recursive: true });
|
|
19
|
+
});
|
|
20
|
+
it('should scan files in a directory', () => {
|
|
21
|
+
fs_1.default.writeFileSync(path_1.default.join(testDir, 'test.ts'), 'console.log("hello")');
|
|
22
|
+
const results = (0, scan_1.scanDir)(testDir);
|
|
23
|
+
expect(results.length).toBe(1);
|
|
24
|
+
expect(results[0].path).toBe('test.ts');
|
|
25
|
+
expect(results[0].content).toBe('console.log("hello")');
|
|
26
|
+
});
|
|
27
|
+
it('should respect ignore patterns from config', () => {
|
|
28
|
+
fs_1.default.mkdirSync(path_1.default.join(testDir, 'node_modules'));
|
|
29
|
+
fs_1.default.writeFileSync(path_1.default.join(testDir, 'node_modules/ignore.ts'), 'ignore');
|
|
30
|
+
fs_1.default.writeFileSync(path_1.default.join(testDir, 'keep.ts'), 'keep');
|
|
31
|
+
const results = (0, scan_1.scanDir)(testDir);
|
|
32
|
+
expect(results.find(r => r.path.includes('node_modules'))).toBeUndefined();
|
|
33
|
+
expect(results.find(r => r.path === 'keep.ts')).toBeDefined();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const schema_1 = require("../core/schema");
|
|
4
|
+
describe('validatePatches', () => {
|
|
5
|
+
it('should validate correct file patches', () => {
|
|
6
|
+
const input = [{ path: 'src/index.ts', content: 'test' }];
|
|
7
|
+
const output = (0, schema_1.validatePatches)(input);
|
|
8
|
+
expect(output).toEqual(input);
|
|
9
|
+
});
|
|
10
|
+
it('should validate correct console patches', () => {
|
|
11
|
+
const input = [{ type: 'console', command: 'npm install' }];
|
|
12
|
+
const output = (0, schema_1.validatePatches)(input);
|
|
13
|
+
expect(output).toEqual(input);
|
|
14
|
+
});
|
|
15
|
+
it('should throw error for invalid items', () => {
|
|
16
|
+
const input = [{ invalid: 'item' }];
|
|
17
|
+
expect(() => (0, schema_1.validatePatches)(input)).toThrow();
|
|
18
|
+
});
|
|
19
|
+
it('should throw error if input is not an array', () => {
|
|
20
|
+
expect(() => (0, schema_1.validatePatches)({})).toThrow();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = cleanCodeBlock;
|
|
4
|
+
// src/utils/cleanCodeBlock.ts
|
|
5
|
+
function cleanCodeBlock(content) {
|
|
6
|
+
// Regex robusta para capturar o primeiro array JSON dentro ou fora de blocos de código markdown
|
|
7
|
+
const codeBlockRegex = /```(?:json)?\s*([\s\S]*?)\s*```/;
|
|
8
|
+
const match = content.match(codeBlockRegex);
|
|
9
|
+
let cleaned = match ? match[1] : content;
|
|
10
|
+
// Remove caracteres invisíveis como BOM (Byte Order Mark) e espaços extras
|
|
11
|
+
cleaned = cleaned.trim().replace(/\uFEFF/g, "");
|
|
12
|
+
// Se ainda assim parecer que tem texto antes do array, tenta encontrar o início do array
|
|
13
|
+
if (!cleaned.startsWith("[")) {
|
|
14
|
+
const arrayStart = cleaned.indexOf("[");
|
|
15
|
+
const arrayEnd = cleaned.lastIndexOf("]");
|
|
16
|
+
if (arrayStart !== -1 && arrayEnd !== -1) {
|
|
17
|
+
cleaned = cleaned.substring(arrayStart, arrayEnd + 1);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return { cleaned };
|
|
21
|
+
}
|
package/dist/types.js
CHANGED
package/johankit.yml
ADDED
package/package.json
CHANGED
|
@@ -1,33 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "johankit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "",
|
|
3
|
+
"version": "0.4.2",
|
|
4
|
+
"description": "Developer-friendly CLI for codebase snapshots and AI vibe-coding.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"johankit": "./dist/index.js"
|
|
7
|
+
"johankit": "./dist/src/index.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"test": "jest",
|
|
11
11
|
"build": "tsc",
|
|
12
|
-
"start": "ts-node src/index.ts"
|
|
12
|
+
"start": "ts-node src/index.ts",
|
|
13
|
+
"prepare": "npm run build"
|
|
13
14
|
},
|
|
14
|
-
"keywords": [
|
|
15
|
+
"keywords": [
|
|
16
|
+
"cli",
|
|
17
|
+
"ai",
|
|
18
|
+
"snapshot",
|
|
19
|
+
"vibe-coding",
|
|
20
|
+
"productivity"
|
|
21
|
+
],
|
|
15
22
|
"author": "",
|
|
16
23
|
"license": "ISC",
|
|
24
|
+
"type": "commonjs",
|
|
17
25
|
"devDependencies": {
|
|
26
|
+
"@types/diff": "^7.0.2",
|
|
18
27
|
"@types/jest": "^29.5.3",
|
|
19
28
|
"@types/js-yaml": "^4.0.9",
|
|
20
|
-
"@types/node": "^
|
|
29
|
+
"@types/node": "^20.0.0",
|
|
21
30
|
"jest": "^29.5.0",
|
|
22
31
|
"ts-jest": "^29.1.0",
|
|
23
32
|
"ts-node": "^10.9.2",
|
|
24
|
-
"typescript": "^5.
|
|
33
|
+
"typescript": "^5.0.0"
|
|
25
34
|
},
|
|
26
35
|
"dependencies": {
|
|
27
|
-
"clipboardy": "^
|
|
28
|
-
"
|
|
36
|
+
"clipboardy": "^4.0.0",
|
|
37
|
+
"colors": "^1.4.0",
|
|
38
|
+
"commander": "^11.0.0",
|
|
39
|
+
"diff": "^8.0.2",
|
|
29
40
|
"js-yaml": "^4.1.1",
|
|
30
|
-
"ts-morph": "^27.0.2",
|
|
31
41
|
"vm2": "^3.10.0"
|
|
32
42
|
}
|
|
33
43
|
}
|
package/src/cli/commands/copy.ts
CHANGED
|
@@ -1,23 +1,10 @@
|
|
|
1
1
|
import { scanDir } from "../../core/scan";
|
|
2
2
|
import { copyToClipboard } from "../../core/clipboard";
|
|
3
3
|
|
|
4
|
-
export async function copy(
|
|
5
|
-
|
|
4
|
+
export async function copy(dir: string, extensions?: string[]) {
|
|
5
|
+
const snapshot = scanDir(dir, { extensions });
|
|
6
|
+
const clipboardJSON = JSON.stringify(snapshot);
|
|
7
|
+
await copyToClipboard(clipboardJSON);
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
for (const p of paths) {
|
|
10
|
-
const scanned = scanDir(p);
|
|
11
|
-
snapshot.push(...scanned);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const clipboardJSON = JSON.stringify(snapshot, null, 2);
|
|
15
|
-
|
|
16
|
-
try {
|
|
17
|
-
await copyToClipboard(clipboardJSON);
|
|
18
|
-
} catch (err) {
|
|
19
|
-
console.warn("Clipboard unavailable:", err);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return clipboardJSON;
|
|
23
|
-
}
|
|
9
|
+
process.stdout.write(`✔ Snapshot de ${dir} copiado (${(clipboardJSON.length / 1024).toFixed(2)} KB)\n`);
|
|
10
|
+
}
|
|
@@ -1,43 +1,82 @@
|
|
|
1
1
|
// src/cli/commands/paste.ts
|
|
2
|
-
import {
|
|
2
|
+
import { applyDiff } from "../../core/diff";
|
|
3
3
|
import { readClipboard } from "../../core/clipboard";
|
|
4
|
+
import { validatePatches } from "../../core/schema";
|
|
4
5
|
import cleanCodeBlock from "../../utils/cleanCodeBlock";
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
6
|
+
import { execSync } from "child_process";
|
|
7
|
+
import readline from "readline";
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import * as diff from "diff";
|
|
11
|
+
import "colors";
|
|
7
12
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
async function confirm(msg: string): Promise<boolean> {
|
|
14
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
15
|
+
return new Promise(resolve => {
|
|
16
|
+
rl.question(`${msg} (y/N): `, (ans) => {
|
|
17
|
+
rl.close();
|
|
18
|
+
resolve(ans.toLowerCase() === 'y');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
11
22
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
23
|
+
function showDiff(filename: string, oldContent: string, newContent: string) {
|
|
24
|
+
console.log(`\n--- DIFF FOR: ${filename.bold} ---`);
|
|
25
|
+
const patches = diff.diffLines(oldContent, newContent);
|
|
26
|
+
patches.forEach((part: diff.Change) => {
|
|
27
|
+
const color = part.added ? 'green' : part.removed ? 'red' : 'gray';
|
|
28
|
+
const prefix = part.added ? '+' : part.removed ? '-' : ' ';
|
|
29
|
+
const value = part.value.endsWith('\n') ? part.value : part.value + '\n';
|
|
30
|
+
process.stdout.write((value.split('\n').map((line: string) => line ? `${prefix}${line}` : '').join('\n'))[color as any]);
|
|
31
|
+
});
|
|
32
|
+
console.log('\n-----------------------');
|
|
33
|
+
}
|
|
15
34
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const { cleaned } = cleanCodeBlock(content);
|
|
19
|
-
parsed = JSON.parse(cleaned);
|
|
20
|
-
} catch (e) {
|
|
21
|
-
throw new Error("Clipboard content is not valid JSON");
|
|
22
|
-
}
|
|
35
|
+
export async function paste(dir: string, runAll = false, dryRun = false, interactiveDiff = false) {
|
|
36
|
+
const autoAccept = process.argv.includes("-y");
|
|
23
37
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
try {
|
|
39
|
+
const content = await readClipboard();
|
|
40
|
+
if (!content) throw new Error("Clipboard empty");
|
|
41
|
+
|
|
42
|
+
const { cleaned } = cleanCodeBlock(content);
|
|
43
|
+
const items = validatePatches(JSON.parse(cleaned));
|
|
44
|
+
|
|
45
|
+
if (dryRun) process.stdout.write("--- DRY RUN MODE ---\n");
|
|
46
|
+
|
|
47
|
+
for (const item of items) {
|
|
48
|
+
if (item.type === 'console' && item.command) {
|
|
49
|
+
if (dryRun) {
|
|
50
|
+
process.stdout.write(`[DRY-RUN] Would execute: ${item.command}\n`);
|
|
51
|
+
} else if (runAll) {
|
|
52
|
+
if (autoAccept || await confirm(`> Execute: ${item.command}`)) {
|
|
53
|
+
execSync(item.command, { stdio: 'inherit', cwd: dir });
|
|
54
|
+
}
|
|
33
55
|
}
|
|
56
|
+
} else if (item.path) {
|
|
57
|
+
const fullPath = path.join(dir, item.path);
|
|
58
|
+
const exists = fs.existsSync(fullPath);
|
|
59
|
+
const oldContent = exists ? fs.readFileSync(fullPath, 'utf8') : "";
|
|
60
|
+
const newContent = item.content || "";
|
|
34
61
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
62
|
+
if (interactiveDiff && item.content !== null) {
|
|
63
|
+
showDiff(item.path, oldContent, newContent);
|
|
64
|
+
if (await confirm(`Apply changes to ${item.path}?`)) {
|
|
65
|
+
applyDiff(dir, [item]);
|
|
66
|
+
} else {
|
|
67
|
+
console.log(`Skipped: ${item.path}`);
|
|
68
|
+
}
|
|
69
|
+
} else if (dryRun) {
|
|
70
|
+
process.stdout.write(`[DRY-RUN] Would ${item.content === null ? 'Delete' : 'Write'}: ${item.path}\n`);
|
|
71
|
+
} else {
|
|
72
|
+
applyDiff(dir, [item]);
|
|
40
73
|
}
|
|
41
|
-
|
|
74
|
+
}
|
|
42
75
|
}
|
|
76
|
+
|
|
77
|
+
process.stdout.write("√ Operation completed\n");
|
|
78
|
+
} catch (error: any) {
|
|
79
|
+
process.stderr.write(`× Failed: ${error.message}\n`);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
43
82
|
}
|