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.
Files changed (59) hide show
  1. package/README.md +26 -58
  2. package/dist/cli/commands/sync.js +37 -57
  3. package/dist/core/validation.js +6 -18
  4. package/dist/core/write.js +9 -7
  5. package/dist/src/cli/commands/copy.js +11 -0
  6. package/dist/src/cli/commands/paste.js +120 -0
  7. package/dist/src/cli/commands/prompt.js +54 -0
  8. package/dist/src/cli/commands/sync.js +173 -0
  9. package/dist/src/core/clipboard.js +64 -0
  10. package/dist/src/core/config.js +39 -0
  11. package/dist/{core → src/core}/diff.js +9 -12
  12. package/dist/src/core/scan.js +70 -0
  13. package/dist/src/core/schema.js +18 -0
  14. package/dist/src/core/validation.js +11 -0
  15. package/dist/src/core/write.js +24 -0
  16. package/dist/src/index.js +34 -0
  17. package/dist/src/tests/cleanCodeBlock.test.js +23 -0
  18. package/dist/src/tests/scan.test.js +35 -0
  19. package/dist/src/tests/schema.test.js +22 -0
  20. package/dist/src/utils/cleanCodeBlock.js +21 -0
  21. package/dist/types.js +1 -0
  22. package/johankit.yml +6 -0
  23. package/package.json +20 -10
  24. package/src/cli/commands/copy.ts +6 -19
  25. package/src/cli/commands/paste.ts +70 -31
  26. package/src/cli/commands/prompt.ts +24 -64
  27. package/src/cli/commands/sync.ts +121 -73
  28. package/src/core/clipboard.ts +46 -80
  29. package/src/core/config.ts +20 -32
  30. package/src/core/diff.ts +10 -21
  31. package/src/core/scan.ts +43 -40
  32. package/src/core/schema.ts +17 -34
  33. package/src/core/validation.ts +8 -27
  34. package/src/core/write.ts +11 -17
  35. package/src/index.ts +38 -77
  36. package/src/types.ts +4 -50
  37. package/src/utils/cleanCodeBlock.ts +17 -8
  38. package/tsconfig.json +14 -6
  39. package/Readme.md +0 -56
  40. package/dist/cli/commands/copy.js +0 -21
  41. package/dist/cli/commands/paste.js +0 -48
  42. package/dist/cli/commands/prompt.js +0 -88
  43. package/dist/cli/commands/three.js +0 -106
  44. package/dist/cli/commands/tree.js +0 -106
  45. package/dist/core/clipboard.js +0 -88
  46. package/dist/core/config.js +0 -51
  47. package/dist/core/scan.js +0 -66
  48. package/dist/core/schema.js +0 -40
  49. package/dist/index.js +0 -72
  50. package/dist/services/JohankitService.js +0 -59
  51. package/dist/utils/cleanCodeBlock.js +0 -12
  52. package/dist/utils/createAsciiTree.js +0 -46
  53. package/johankit.yaml +0 -2
  54. package/src/cli/commands/tree.ts +0 -119
  55. package/src/services/JohankitService.ts +0 -70
  56. package/src/utils/createAsciiTree.ts +0 -53
  57. package/types.ts +0 -11
  58. /package/dist/{core → src/core}/git.js +0 -0
  59. /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
- switch (patch.type) {
14
- case "delete": {
15
- if (fs_1.default.existsSync(fullPath)) {
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
@@ -1,2 +1,3 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ // deleted as redundant to src/types.ts
package/johankit.yml ADDED
@@ -0,0 +1,6 @@
1
+ ignore:
2
+ - node_modules
3
+ - .git
4
+ - dist
5
+ - yarn.lock
6
+ - package-lock.json
package/package.json CHANGED
@@ -1,33 +1,43 @@
1
1
  {
2
2
  "name": "johankit",
3
- "version": "0.1.5",
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": "^25.0.1",
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.9.3"
33
+ "typescript": "^5.0.0"
25
34
  },
26
35
  "dependencies": {
27
- "clipboardy": "^5.0.2",
28
- "commander": "^14.0.2",
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
  }
@@ -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(input: string | string[]) {
5
- let snapshot: any[] = [];
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
- const paths = Array.isArray(input) ? input : [input];
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 { writeFiles } from "../../core/write";
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 { applyDiff } from "../../core/diff";
6
- import { validatePatches } from "../../core/validation";
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
- export async function paste(dir: string) {
9
- try {
10
- const content = await readClipboard();
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
- if (!content) {
13
- throw new Error("Clipboard empty or inaccessible");
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
- let parsed;
17
- try {
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
- if (Array.isArray(parsed) && parsed.every(f => f.path && f.content)) {
25
- // Caso seja snapshot
26
- writeFiles(dir, parsed, true);
27
- } else if (Array.isArray(parsed) && parsed.every(f => f.type && f.path)) {
28
- // Caso seja DiffPatch
29
- const validated = validatePatches(parsed);
30
- applyDiff(dir, validated);
31
- } else {
32
- throw new Error("JSON is neither FileSnapshot array nor DiffPatch array");
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
- 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`);
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
- process.exit(1);
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
  }