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
@@ -2,81 +2,41 @@
2
2
  import { scanDir } from "../../core/scan";
3
3
  import { copyToClipboard } from "../../core/clipboard";
4
4
 
5
- interface LLMResponseExample {
6
- type: 'FileSnapshot' | 'DiffPatch';
7
- example: any;
8
- }
9
-
10
- export async function prompt(dir: string, userPrompt: string, diff = false) {
5
+ export async function prompt(dir: string, userPrompt: string) {
11
6
  const snapshot = scanDir(dir);
12
7
 
13
- const llmExamples: LLMResponseExample[] = [
14
- {
15
- type: 'FileSnapshot',
16
- example: [
17
- {
18
- path: 'src/example.ts',
19
- content: 'export const x = 42;'
20
- }
21
- ]
22
- },
23
- {
24
- type: 'DiffPatch',
25
- example: [
26
- {
27
- type: 'modify',
28
- path: 'src/example.ts',
29
- content: 'export const x = 43;'
30
- },
31
- {
32
- type: 'create',
33
- path: 'src/newFile.ts',
34
- content: 'export const newFile = true;'
35
- },
36
- {
37
- type: 'delete',
38
- path: 'src/oldFile.ts'
39
- }
40
- ]
41
- }
42
- ];
43
-
44
8
  const template = `
45
- You are an AI software engineer.
46
-
47
- You will receive a JSON array representing a snapshot of a codebase.
48
- Each item has the following structure:
49
- {
50
- "path": "relative/path/to/file.ext",
51
- "content": "full file content"
52
- }
9
+ You are an AI software engineer.
10
+ Your goal is to help the user with their codebase using a specific JSON patch format.
53
11
 
12
+ Maintain 100% of previous functionality for full compatibility.
13
+ Always submit the complete code; never use comments.
14
+ Use "git add {files} && git commit -m {message}" after each change via the console.
54
15
  ---
55
16
 
56
- SNAPSHOT
57
- ${JSON.stringify(snapshot, null, 2)}
17
+ ### CAPABILITIES
18
+ 1. **File Updates**: You can create, update, or delete files.
19
+ 2. **Console Commands**: You can execute shell commands (e.g., npm install, mkdir, rm, vitest).
58
20
 
59
- ---
21
+ ### RESPONSE FORMAT
22
+ Return ONLY a JSON array. No conversational text. No explanations.
23
+ Wrap the JSON in a markdown code block: \`\`\`json [your_array] \`\`\`
60
24
 
61
- YOUR TASK
62
- Propose changes according to the user request.
25
+ ### PATCH TYPES
26
+ - **File Patch**: { "path": "src/file.ts", "content": "full code" }
27
+ - **Delete File**: { "path": "src/old-file.ts", "content": null }
28
+ - **Console**: { "type": "console", "command": "npm install lodash" }
63
29
 
64
- Return ONLY a JSON array of ${diff ? 'DiffPatch' : 'FileSnapshot'}.
30
+ ### STRATEGY
31
+ If the user request requires a new library, include the "npm install" command in the array before the file updates.
32
+ If the user wants to refactor and ensure it works, you can include a command to run tests.
65
33
 
66
- PATCH FORMAT (STRICT)
67
- {
68
- \"path\": \"relative/path/to/file.ext\",
69
- \"content\": \"FULL updated file content (omit for delete)\"
70
- }
34
+ ---
71
35
 
72
- EXAMPLE RESPONSE FROM LLM:
73
- ${JSON.stringify(diff ? llmExamples.find(e => e.type === 'DiffPatch')?.example : llmExamples.find(e => e.type === 'FileSnapshot')?.example, null, 2)}
36
+ SNAPSHOT
37
+ ${JSON.stringify(snapshot, null, 2)}
74
38
 
75
- IMPORTANT RULES
76
- - Do NOT return explanations
77
- - Do NOT return markdown
78
- - Return ONLY valid JSON inside the \"\`\`\`\"
79
- - Always return within a Markdown Code Block.
39
+ ---
80
40
 
81
41
  USER REQUEST
82
42
  ${userPrompt}
@@ -85,7 +45,7 @@ ${userPrompt}
85
45
  try {
86
46
  await copyToClipboard(template.trim());
87
47
  process.stdout.write(template.trim());
88
- process.stdout.write("\n\n✔ Prompt + Snapshot + Example copied to clipboard\n");
48
+ process.stdout.write("\n\n✔ Prompt + Snapshot copied to clipboard\n");
89
49
  } catch (e) {
90
50
  process.stdout.write(template.trim());
91
51
  process.stderr.write("\n✖ Failed to copy to clipboard (output only)\n");
@@ -1,90 +1,138 @@
1
1
  // src/cli/commands/sync.ts
2
2
  import { scanDir } from "../../core/scan";
3
+ import { validatePatches } from "../../core/schema";
3
4
  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, diff = false) {
9
- try {
10
- const snapshotBefore = scanDir(dir);
11
-
12
- const template = `
13
- You are an AI software engineer.
14
-
15
- You will receive a JSON array representing a snapshot of a codebase.
16
- Each item has the following structure:
17
-
18
- \`\`\`json
19
- {
20
- "path": "relative/path/to/file.ext",
21
- "content": "full file content"
5
+ import { execSync } from "child_process";
6
+ import cleanCodeBlock from "../../utils/cleanCodeBlock";
7
+ import readline from "readline";
8
+ import { copyToClipboard, readClipboard } from "../../core/clipboard";
9
+ import fs from "fs";
10
+ import path from "path";
11
+ import * as diff from "diff";
12
+ import "colors";
13
+
14
+ async function confirm(msg: string): Promise<boolean> {
15
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
16
+ return new Promise(resolve => {
17
+ rl.question(`${msg} (y/N): `, ans => {
18
+ rl.close();
19
+ resolve(ans.toLowerCase() === 'y');
20
+ });
21
+ });
22
22
  }
23
- \`\`\`
24
23
 
25
- ---
26
-
27
- SNAPSHOT
28
- ${JSON.stringify(snapshotBefore, null, 2)}
29
-
30
- ---
31
-
32
- YOUR TASK
33
- Propose changes according to the user request.
34
-
35
- Return ONLY a JSON array of ${diff ? 'DiffPatch' : 'FileSnapshot'}.
36
-
37
- PATCH FORMAT (STRICT)
38
- {
39
- \"path\": \"relative/path/to/file.ext\",
40
- \"content\": \"FULL updated file content (omit for delete)\"
24
+ function showDiff(filename: string, oldContent: string, newContent: string) {
25
+ console.log(`\n--- DIFF FOR: ${filename.bold} ---`);
26
+ const patches = diff.diffLines(oldContent, newContent);
27
+ patches.forEach((part: diff.Change) => {
28
+ const color = part.added ? 'green' : part.removed ? 'red' : 'gray';
29
+ const prefix = part.added ? '+' : part.removed ? '-' : ' ';
30
+ const value = part.value.endsWith('\n') ? part.value : part.value + '\n';
31
+ process.stdout.write((value.split('\n').map((line: string) => line ? `${prefix}${line}` : '').join('\n'))[color as any]);
32
+ });
33
+ console.log('\n-----------------------');
41
34
  }
42
35
 
43
- IMPORTANT RULES
44
- - Do NOT return explanations
45
- - Do NOT return markdown
46
- - Return ONLY valid JSON
36
+ async function processInput(input: string, dir: string, runAll: boolean, dryRun: boolean, interactiveDiff: boolean) {
37
+ const autoAccept = process.argv.includes('-y');
38
+ const { cleaned } = cleanCodeBlock(input);
39
+ let patchesData;
40
+
41
+ try {
42
+ patchesData = JSON.parse(cleaned);
43
+ } catch (e) {
44
+ return false;
45
+ }
47
46
 
48
- USER REQUEST
49
- <Replace this with the user request>
50
- `;
47
+ const patches = validatePatches(patchesData);
48
+ if (patches.length === 0) return false;
49
+
50
+ for (const patch of patches) {
51
+ if (patch.type === 'console' && patch.command) {
52
+ if (dryRun) {
53
+ process.stdout.write(`[DRY-RUN] Would execute: ${patch.command}\n`);
54
+ } else if (runAll) {
55
+ const ok = autoAccept || await confirm(`> Execute: ${patch.command}`);
56
+ if (ok) execSync(patch.command, { stdio: 'inherit', cwd: dir });
57
+ }
58
+ } else if (patch.path) {
59
+ const fullPath = path.join(dir, patch.path);
60
+ const exists = fs.existsSync(fullPath);
61
+ const oldContent = exists ? fs.readFileSync(fullPath, 'utf8') : "";
62
+ const newContent = patch.content || "";
63
+
64
+ if (interactiveDiff && patch.content !== null) {
65
+ showDiff(patch.path, oldContent, newContent);
66
+ if (await confirm(`Apply changes to ${patch.path}?`)) {
67
+ applyDiff(dir, [patch]);
68
+ } else {
69
+ console.log(`Skipped: ${patch.path}`);
70
+ }
71
+ } else if (dryRun) {
72
+ const action = patch.content === null ? "Delete" : "Write";
73
+ process.stdout.write(`[DRY-RUN] Would ${action}: ${patch.path}\n`);
74
+ } else {
75
+ applyDiff(dir, [patch]);
76
+ }
77
+ }
78
+ }
79
+ return true;
80
+ }
51
81
 
52
- await copyToClipboard(template.trim());
53
- process.stdout.write("✔ Prompt with snapshot copied to clipboard\n");
82
+ export async function sync(dir: string, runAll = false, dryRun = false, interactiveDiff = false, watch = false) {
83
+ try {
84
+ const snapshotBefore = scanDir(dir);
85
+ const systemPrompt = `
86
+ YOU ARE AN AI SOFTWARE ENGINEER.
87
+ ALWAYS RESPOND USING THE FOLLOWING JSON PATCH FORMAT ONLY.
54
88
 
55
- const input = await readStdin();
89
+ FORMAT:
90
+ [{"path": "file.ts", "content": "full code"}, {"type": "console", "command": "npm install"}]
56
91
 
57
- let patches;
58
- try {
59
- patches = JSON.parse(input);
60
- } catch {
61
- throw new Error("Invalid JSON input");
62
- }
92
+ SNAPSHOT:
93
+ ${JSON.stringify(snapshotBefore, null, 2)}
63
94
 
64
- if (diff) {
65
- const validated = validatePatches(patches);
66
- applyDiff(dir, validated);
95
+ PLEASE APPLY THE USER REQUESTS TO THIS SNAPSHOT AND RETURN ONLY THE JSON ARRAY.`;
96
+
97
+ await copyToClipboard(systemPrompt);
98
+ process.stdout.write('√ System Prompt + Snapshot copied to clipboard.\n');
99
+
100
+ if (watch) {
101
+ process.stdout.write('√ Watching clipboard for AI response (Press Ctrl+C to stop)...\n');
102
+ let lastClipboard = await readClipboard();
103
+
104
+ while (true) {
105
+ await new Promise(r => setTimeout(r, 1000));
106
+ const currentClipboard = await readClipboard();
107
+
108
+ if (currentClipboard !== lastClipboard && currentClipboard.trim().length > 0) {
109
+ lastClipboard = currentClipboard;
110
+ const success = await processInput(currentClipboard, dir, runAll, dryRun, interactiveDiff);
111
+ if (success) {
112
+ process.stdout.write('√ Patch applied automatically from clipboard!\n');
113
+ const snapshotAfter = scanDir(dir);
114
+ await copyToClipboard(JSON.stringify(snapshotAfter, null, 2));
115
+ process.stdout.write('√ Updated snapshot copied to clipboard. Ready for next turn.\n');
116
+ }
117
+ }
118
+ }
67
119
  } else {
68
- writeFiles(dir, patches as any, true);
69
- }
70
-
71
- const snapshotAfter = scanDir(dir);
72
- await copyToClipboard(JSON.stringify(snapshotAfter, null, 2));
73
-
74
- process.stdout.write("✔ Sync applied and new snapshot copied to clipboard\n");
75
- } catch (error) {
76
- process.stderr.write("✖ Sync failed\n");
77
- if (error instanceof Error) {
78
- process.stderr.write(`${error.message}\n`);
120
+ process.stdout.write('√ Go to your AI, paste it, copy the result, and come back here.\n');
121
+ await confirm('Press [Enter] when you have the AI response in your clipboard...');
122
+
123
+ const input = await readClipboard();
124
+ if (!input) throw new Error("Clipboard is empty");
125
+
126
+ const success = await processInput(input, dir, runAll, dryRun, interactiveDiff);
127
+
128
+ if (success && !dryRun) {
129
+ const snapshotAfter = scanDir(dir);
130
+ await copyToClipboard(JSON.stringify(snapshotAfter, null, 2));
131
+ process.stdout.write('√ Sync applied! Updated snapshot is now in your clipboard.\n');
132
+ }
79
133
  }
134
+ } catch (e: any) {
135
+ process.stderr.write(`× Sync failed: ${e.message}\n`);
80
136
  process.exit(1);
81
137
  }
82
- }
83
-
84
- function readStdin(): Promise<string> {
85
- return new Promise(resolve => {
86
- let data = "";
87
- process.stdin.on("data", c => (data += c));
88
- process.stdin.on("end", () => resolve(data));
89
- });
90
138
  }
@@ -1,94 +1,60 @@
1
1
  // src/core/clipboard.ts
2
2
  import { spawn } from "child_process";
3
+ import clipboardy from "clipboardy";
3
4
 
4
- let memoryClipboard = "";
5
+ export async function copyToClipboard(text: string): Promise<void> {
6
+ const platform = process.platform;
7
+
8
+ const isWSL = process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP;
5
9
 
6
- export function copyToClipboard(text: string): Promise<void> {
7
10
  return new Promise((resolve, reject) => {
8
- let command = "xclip";
9
- let args = ["-selection", "clipboard"];
11
+ let command = "";
12
+ let args: string[] = [];
10
13
 
11
- if (process.platform === "darwin") {
14
+ if (isWSL) {
15
+ command = "clip.exe";
16
+ } else if (platform === "darwin") {
12
17
  command = "pbcopy";
13
- args = [];
14
- } else if (process.platform === "win32") {
18
+ } else if (platform === "win32") {
15
19
  command = "clip";
16
- args = [];
17
- }
18
-
19
- const child = spawn(command, args, {
20
- stdio: ["pipe", "ignore", "ignore"]
21
- });
22
-
23
- let resolved = false;
24
-
25
- child.on("error", (err) => {
26
- memoryClipboard = text;
27
- resolved = true;
28
- resolve();
29
- });
30
-
31
- child.stdin.on("error", (err: any) => {
32
- if (err.code === "EPIPE") {
33
- resolved = true;
34
- resolve();
35
- } else {
36
- memoryClipboard = text;
37
- if (!resolved) resolve();
38
- }
39
- });
40
-
41
- child.stdin.write(text);
42
- child.stdin.end();
43
-
44
- child.on("close", () => {
45
- if (!resolved) resolve();
46
- });
47
- });
48
- }
49
-
50
- export function readClipboard(): Promise<string> {
51
- return new Promise((resolve, reject) => {
52
- let command: string;
53
- let args: string[] = [];
54
-
55
- if (process.platform === "darwin") {
56
- command = "pbpaste";
57
- } else if (process.platform === "win32") {
58
- command = "powershell";
59
- args = ["-Command", "Get-Clipboard"];
60
20
  } else {
61
21
  command = "xclip";
62
- args = ["-selection", "clipboard", "-o"];
22
+ args = ["-selection", "clipboard"];
63
23
  }
64
24
 
65
- const child = spawn(command, args, {
66
- stdio: ["ignore", "pipe", "pipe"]
67
- });
68
-
69
- let data = "";
70
- let error = "";
71
- let fallback = false;
72
-
73
- child.stdout.on("data", chunk => {
74
- data += chunk.toString();
75
- });
76
-
77
- child.stderr.on("data", chunk => {
78
- error += chunk.toString();
79
- });
80
-
81
- child.on("error", () => {
82
- fallback = true;
83
- resolve(memoryClipboard);
84
- });
85
-
86
- child.on("close", code => {
87
- if (code !== 0 || fallback) {
88
- resolve(memoryClipboard);
89
- } else {
90
- resolve(data);
91
- }
92
- });
25
+ try {
26
+ const child = spawn(command, args);
27
+
28
+ child.on("error", (err) => reject(err));
29
+ child.on("close", (code) => {
30
+ if (code === 0) resolve();
31
+ else reject(new Error(`Clipboard error: ${code}`));
32
+ });
33
+
34
+ child.stdin.write(text);
35
+ child.stdin.end();
36
+ } catch (e) {
37
+ reject(e);
38
+ }
93
39
  });
94
40
  }
41
+
42
+ export async function readClipboard(): Promise<string> {
43
+ try {
44
+ return await clipboardy.read();
45
+ } catch (err) {
46
+ return new Promise((resolve) => {
47
+ const platform = process.platform;
48
+ let command = platform === "darwin" ? "pbpaste" : platform === "win32" ? "powershell" : "xclip";
49
+ let args = platform === "win32" ? ["-NoProfile", "-Command", "Get-Clipboard"] :
50
+ platform === "linux" ? ["-selection", "clipboard", "-o"] : [];
51
+
52
+ const child = spawn(command, args);
53
+ let output = "";
54
+
55
+ child.stdout.on("data", (d) => (output += d.toString()));
56
+ child.on("close", () => resolve(output.trim()));
57
+ child.on("error", () => resolve(""));
58
+ });
59
+ }
60
+ }
@@ -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,26 @@ 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
- const configPath = path.join(basePath, CONFIG_FILENAME);
17
+ const configFilenames = ["johankit.yaml", "johankit.yml"];
18
+ let loadedConfig: Partial<Config> = {};
26
19
 
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
- };
20
+ for (const filename of configFilenames) {
21
+ const configPath = path.join(basePath, filename);
22
+ if (existsSync(configPath)) {
23
+ try {
24
+ const content = readFileSync(configPath, "utf8");
25
+ loadedConfig = (load(content) as Partial<Config>) || {};
26
+ break;
27
+ } catch (error) {
28
+ console.warn(`[johankit] Erro ao ler ${filename}, tentando próximo...`);
29
+ }
43
30
  }
44
-
45
- console.warn(`[johankit] Aviso: Falha ao carregar ${CONFIG_FILENAME}. Usando defaults.`, error);
46
- return {
47
- ignore: DEFAULT_IGNORE,
48
- };
49
31
  }
50
- }
32
+
33
+ const ignoreSet = new Set([...DEFAULT_IGNORE, ...(loadedConfig.ignore || [])]);
34
+
35
+ return {
36
+ ignore: Array.from(ignoreSet),
37
+ };
38
+ }
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 interface DiffPatch {
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
- 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;
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
+ }