natureco-cli 5.1.1 → 5.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "natureco-cli",
3
- "version": "5.1.1",
3
+ "version": "5.2.0",
4
4
  "description": "OpenClaw'dan daha güvenli, daha hızlı, daha ucuz AI agent CLI. Multi-agent, self-evolving skills, audit log, maliyet optimizasyonu ve NatureCo platform-native.",
5
5
  "bin": {
6
6
  "natureco": "bin/natureco.js"
@@ -78,11 +78,41 @@ async function createCron({ name, schedule, command, description = "" }) {
78
78
  }
79
79
  } catch {}
80
80
 
81
+ // v5.2.0: Gercek macOS crontab'a ekle (kullanici crontab, sudo gerekmez)
82
+ const marker = `# natureco:${name}`;
83
+ let crontabUpdated = false;
84
+ let crontabError = null;
85
+
86
+ try {
87
+ const { execSync } = require("child_process");
88
+ // Mevcut crontab'i oku
89
+ let existing = "";
90
+ try { existing = execSync("crontab -l 2>/dev/null", { encoding: "utf8" }); }
91
+ catch { existing = ""; }
92
+
93
+ // Ayni isimde var mi kontrol
94
+ if (existing.includes(marker)) {
95
+ crontabUpdated = false;
96
+ } else {
97
+ const newLine = `${resolvedSchedule} ${command} ${marker}`;
98
+ const updated = existing + (existing.endsWith("\n") || existing === "" ? "" : "\n") + newLine + "\n";
99
+ // Yeni crontab yukle (heredoc yerine - ile stdin)
100
+ execSync("crontab -", { input: updated, encoding: "utf8" });
101
+ crontabUpdated = true;
102
+ }
103
+ } catch (e) {
104
+ crontabError = e.message;
105
+ }
106
+
81
107
  return {
82
108
  success: true,
83
109
  cron: newCron,
84
- message: `Cron olusturuldu: ${name} (${resolvedSchedule})`,
85
- hint: systemCrontabInstalled ? undefined : `Sistem crontab'a eklemek icin: crontab -e ve ekleyin: ${resolvedSchedule} ${command}`,
110
+ message: crontabUpdated
111
+ ? `Cron olusturuldu VE macOS crontab'a eklendi: ${name} (${resolvedSchedule})`
112
+ : `Cron olusturuldu: ${name} (${resolvedSchedule})${crontabError ? ` - crontab eklenemedi: ${crontabError}` : ""}`,
113
+ crontabUpdated,
114
+ crontabError,
115
+ schedule: resolvedSchedule,
86
116
  };
87
117
  }
88
118
 
@@ -9,9 +9,31 @@ const { spawn } = require("child_process");
9
9
  const path = require("path");
10
10
  const os = require("os");
11
11
 
12
+ // v5.2.0: Agent alias mapping (Parton'un testinden — "review" diye bir agent yok)
13
+ const AGENT_ALIASES = {
14
+ "review": "general", // eskiden review diye bir vardi, simdi general
15
+ "analyze": "explore",
16
+ "scan": "security",
17
+ "audit": "security",
18
+ "check": "general",
19
+ };
20
+
21
+ const VALID_AGENTS = ["explore", "general", "seo", "content", "security", "debugger", "translator"];
22
+
12
23
  async function delegate({ task, agent = "general", timeoutMs = 60000 }) {
13
24
  if (!task) return { success: false, error: "task gerekli" };
14
25
 
26
+ // Alias cozumu
27
+ if (AGENT_ALIASES[agent]) {
28
+ agent = AGENT_ALIASES[agent];
29
+ }
30
+ if (!VALID_AGENTS.includes(agent)) {
31
+ return {
32
+ success: false,
33
+ error: `Gecersiz agent: ${agent}. Var olan agent tipleri: ${VALID_AGENTS.join(", ")}. Alias'lar: ${Object.keys(AGENT_ALIASES).join(", ")}`,
34
+ };
35
+ }
36
+
15
37
  // Bu basitlestirilmis versiyon: kendi agent sistemi yerine
16
38
  // natureco'nun REPL'ini sub-process olarak baslatip gorev verir
17
39
  return new Promise((resolve) => {
@@ -1,4 +1,5 @@
1
1
  const fs = require('fs');
2
+ const { expandPath } = require('../utils/paths');
2
3
  const path = require('path');
3
4
  const { execSync } = require('child_process');
4
5
 
@@ -16,7 +17,7 @@ module.exports = {
16
17
 
17
18
  async execute(params) {
18
19
  try {
19
- const filePath = path.resolve(params.path.replace(/^~/, require('os').homedir()));
20
+ const filePath = path.resolve(expandPath(params.path));
20
21
  const maxChars = params.maxChars || 50000;
21
22
 
22
23
  if (!fs.existsSync(filePath)) {
@@ -1,7 +1,7 @@
1
- const listDir = require('./list_dir');
2
-
3
- module.exports = {
4
- ...listDir,
5
- name: 'filesystem',
6
- description: 'List files and directories',
7
- };
1
+ const listDir = require('./list_dir');
2
+
3
+ module.exports = {
4
+ ...listDir,
5
+ name: 'filesystem',
6
+ description: 'List files and directories',
7
+ };
@@ -1,127 +1,127 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const os = require('os');
4
-
5
- /**
6
- * List Directory Tool
7
- * Lists files and directories in a given path
8
- */
9
-
10
- module.exports = {
11
- name: 'list_dir',
12
- description: 'List files and directories in a given path. Returns file names, sizes, and types.',
13
- inputSchema: {
14
- type: 'object',
15
- properties: {
16
- path: {
17
- type: 'string',
18
- description: 'Directory path to list (relative or absolute). Use "." for current directory.'
19
- }
20
- },
21
- required: ['path']
22
- },
23
-
24
- async execute(params) {
25
- try {
26
- // Expand ~ to home directory
27
- let dirPath = params.path || '.';
28
- dirPath = dirPath.replace(/^~/, os.homedir());
29
-
30
- // Fix /home path - replace with actual home directory (Unix only)
31
- // Handles: /home, home, /home/Documents, /home/anything
32
- if (process.platform !== 'win32') {
33
- if (dirPath === '/home' || dirPath === 'home' || dirPath.startsWith('/home/')) {
34
- dirPath = dirPath.replace(/^\/home/, os.homedir());
35
- }
36
- }
37
-
38
- const absolutePath = path.resolve(dirPath);
39
-
40
- // Check if directory exists
41
- if (!fs.existsSync(absolutePath)) {
42
- return {
43
- success: false,
44
- error: `Directory not found: ${params.path}`
45
- };
46
- }
47
-
48
- // Check if it's a directory
49
- const stats = fs.statSync(absolutePath);
50
- if (!stats.isDirectory()) {
51
- return {
52
- success: false,
53
- error: `Not a directory: ${params.path}`
54
- };
55
- }
56
-
57
- // Read directory
58
- const entries = fs.readdirSync(absolutePath, { withFileTypes: true });
59
-
60
- // Format entries
61
- const items = entries.map(entry => {
62
- const itemPath = path.join(absolutePath, entry.name);
63
- let size = 0;
64
- let type = 'unknown';
65
-
66
- try {
67
- const itemStats = fs.statSync(itemPath);
68
- size = itemStats.size;
69
-
70
- if (entry.isDirectory()) {
71
- type = 'directory';
72
- } else if (entry.isFile()) {
73
- type = 'file';
74
- } else if (entry.isSymbolicLink()) {
75
- type = 'symlink';
76
- }
77
- } catch (err) {
78
- // Ignore stat errors
79
- }
80
-
81
- return {
82
- name: entry.name,
83
- type: type,
84
- size: size
85
- };
86
- });
87
-
88
- // Sort: directories first, then files
89
- items.sort((a, b) => {
90
- if (a.type === 'directory' && b.type !== 'directory') return -1;
91
- if (a.type !== 'directory' && b.type === 'directory') return 1;
92
- return a.name.localeCompare(b.name);
93
- });
94
-
95
- // Format output
96
- let output = `Directory: ${absolutePath}\n`;
97
- output += `Total items: ${items.length}\n\n`;
98
-
99
- items.forEach(item => {
100
- const typeIcon = item.type === 'directory' ? '📁' : '📄';
101
- const sizeStr = item.type === 'file' ? ` (${formatSize(item.size)})` : '';
102
- output += `${typeIcon} ${item.name}${sizeStr}\n`;
103
- });
104
-
105
- return {
106
- success: true,
107
- output: output,
108
- items: items,
109
- path: absolutePath
110
- };
111
-
112
- } catch (error) {
113
- return {
114
- success: false,
115
- error: error.message
116
- };
117
- }
118
- }
119
- };
120
-
121
- function formatSize(bytes) {
122
- if (bytes === 0) return '0 B';
123
- const k = 1024;
124
- const sizes = ['B', 'KB', 'MB', 'GB'];
125
- const i = Math.floor(Math.log(bytes) / Math.log(k));
126
- return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
127
- }
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ /**
6
+ * List Directory Tool
7
+ * Lists files and directories in a given path
8
+ */
9
+
10
+ module.exports = {
11
+ name: 'list_dir',
12
+ description: 'List files and directories in a given path. Returns file names, sizes, and types.',
13
+ inputSchema: {
14
+ type: 'object',
15
+ properties: {
16
+ path: {
17
+ type: 'string',
18
+ description: 'Directory path to list (relative or absolute). Use "." for current directory.'
19
+ }
20
+ },
21
+ required: ['path']
22
+ },
23
+
24
+ async execute(params) {
25
+ try {
26
+ // Expand ~ to home directory
27
+ let dirPath = params.path || '.';
28
+ dirPath = dirPath.replace(/^~/, os.homedir());
29
+
30
+ // Fix /home path - replace with actual home directory (Unix only)
31
+ // Handles: /home, home, /home/Documents, /home/anything
32
+ if (process.platform !== 'win32') {
33
+ if (dirPath === '/home' || dirPath === 'home' || dirPath.startsWith('/home/')) {
34
+ dirPath = dirPath.replace(/^\/home/, os.homedir());
35
+ }
36
+ }
37
+
38
+ const absolutePath = path.resolve(dirPath);
39
+
40
+ // Check if directory exists
41
+ if (!fs.existsSync(absolutePath)) {
42
+ return {
43
+ success: false,
44
+ error: `Directory not found: ${params.path}`
45
+ };
46
+ }
47
+
48
+ // Check if it's a directory
49
+ const stats = fs.statSync(absolutePath);
50
+ if (!stats.isDirectory()) {
51
+ return {
52
+ success: false,
53
+ error: `Not a directory: ${params.path}`
54
+ };
55
+ }
56
+
57
+ // Read directory
58
+ const entries = fs.readdirSync(absolutePath, { withFileTypes: true });
59
+
60
+ // Format entries
61
+ const items = entries.map(entry => {
62
+ const itemPath = path.join(absolutePath, entry.name);
63
+ let size = 0;
64
+ let type = 'unknown';
65
+
66
+ try {
67
+ const itemStats = fs.statSync(itemPath);
68
+ size = itemStats.size;
69
+
70
+ if (entry.isDirectory()) {
71
+ type = 'directory';
72
+ } else if (entry.isFile()) {
73
+ type = 'file';
74
+ } else if (entry.isSymbolicLink()) {
75
+ type = 'symlink';
76
+ }
77
+ } catch (err) {
78
+ // Ignore stat errors
79
+ }
80
+
81
+ return {
82
+ name: entry.name,
83
+ type: type,
84
+ size: size
85
+ };
86
+ });
87
+
88
+ // Sort: directories first, then files
89
+ items.sort((a, b) => {
90
+ if (a.type === 'directory' && b.type !== 'directory') return -1;
91
+ if (a.type !== 'directory' && b.type === 'directory') return 1;
92
+ return a.name.localeCompare(b.name);
93
+ });
94
+
95
+ // Format output
96
+ let output = `Directory: ${absolutePath}\n`;
97
+ output += `Total items: ${items.length}\n\n`;
98
+
99
+ items.forEach(item => {
100
+ const typeIcon = item.type === 'directory' ? '📁' : '📄';
101
+ const sizeStr = item.type === 'file' ? ` (${formatSize(item.size)})` : '';
102
+ output += `${typeIcon} ${item.name}${sizeStr}\n`;
103
+ });
104
+
105
+ return {
106
+ success: true,
107
+ output: output,
108
+ items: items,
109
+ path: absolutePath
110
+ };
111
+
112
+ } catch (error) {
113
+ return {
114
+ success: false,
115
+ error: error.message
116
+ };
117
+ }
118
+ }
119
+ };
120
+
121
+ function formatSize(bytes) {
122
+ if (bytes === 0) return '0 B';
123
+ const k = 1024;
124
+ const sizes = ['B', 'KB', 'MB', 'GB'];
125
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
126
+ return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
127
+ }
@@ -90,26 +90,38 @@ async function setAlarm({ time, label = "Alarm", calendarName = "Calendar" }) {
90
90
 
91
91
  // AppleScript ile macOS Calendar'a all-day etkinlik olarak alarm ekle
92
92
  // (Reminder ile ayni sonuc, daha guvenilir cunku Calendar otomasyon izni Reminders'dan once verilir)
93
- const script = `
93
+ // Dual: Calendar event (gorunurluk) + Reminders (bildirim + alarm)
94
+ // Once Calendar'a basit etkinlik (alarm'siz, ama gorunur), sonra Reminders alarm
95
+ const calendarScript = `
94
96
  tell application "Calendar"
95
97
  set targetCal to first calendar whose writable is true
96
98
  set startDate to date "${parsed.formattedDate} ${parsed.formattedTime}"
97
99
  set endDate to startDate + (1 * minutes)
98
100
  set newEvent to make new event at end of events of targetCal with properties {summary:"⏰ ${label.replace(/"/g, "'")} - NatureCo", start date:startDate, end date:endDate, allday event:false}
99
- -- Alarm ekle: 0 dakika once = tam zamanda
100
- set the number of sound alarms of newEvent to 1
101
- make new sound alarm at end of sound alarms of newEvent with properties {trigger interval:-0}
102
101
  save
103
102
  return id of newEvent
104
103
  end tell
105
104
  `;
106
105
 
106
+ // Reminders'a bildirim + sesli alarm
107
+ const reminderScript = `
108
+ tell application "Reminders"
109
+ set targetList to default list
110
+ set startDate to date "${parsed.formattedDate} ${parsed.formattedTime}"
111
+ set newReminder to make new reminder at end of targetList with properties {name:"⏰ ${label.replace(/"/g, "'")} - NatureCo", body:"NatureCo CLI tarafindan ${parsed.humanReadable} icin kuruldu", due date:startDate}
112
+ save
113
+ return id of newReminder
114
+ end tell
115
+ `;
116
+
107
117
  try {
108
- const eventId = await runAppleScript(script);
118
+ const calId = await runAppleScript(calendarScript);
119
+ const remId = await runAppleScript(reminderScript);
109
120
  return {
110
121
  success: true,
111
- eventId,
112
- message: `⏰ Alarm kuruldu: ${parsed.humanReadable} — "${label}"`,
122
+ eventId: remId,
123
+ calendarEventId: calId,
124
+ message: `⏰ Alarm kuruldu: ${parsed.humanReadable} — "${label}" (Calendar + Reminders)`,
113
125
  calendar: calendarName,
114
126
  targetTime: parsed.humanReadable,
115
127
  targetTimestamp: parsed.timestamp,
@@ -0,0 +1,87 @@
1
+ /**
2
+ * macos_screenshot - macOS native screenshot (v5.2.0)
3
+ *
4
+ * Playwright'a gerek kalmadan screencapture komutu ile
5
+ * ekran goruntusu alir. Sonra base64 olarak doner.
6
+ */
7
+
8
+ const { spawn } = require("child_process");
9
+ const os = require("os");
10
+ const fs = require("fs");
11
+ const path = require("path");
12
+
13
+ const IS_MAC = os.platform() === "darwin";
14
+
15
+ async function captureScreen({ outputPath = null, region = "full" } = {}) {
16
+ if (!IS_MAC) return { success: false, error: "Sadece macOS" };
17
+
18
+ const tmpFile = outputPath || path.join(os.tmpdir(), `screenshot-${Date.now()}.png`);
19
+ return new Promise((resolve) => {
20
+ // screencapture -x: ses yok, -t png: png format
21
+ const proc = spawn("screencapture", ["-x", "-t", "png", tmpFile]);
22
+ proc.on("close", (code) => {
23
+ if (code === 0 && fs.existsSync(tmpFile)) {
24
+ const buffer = fs.readFileSync(tmpFile);
25
+ const stats = fs.statSync(tmpFile);
26
+ resolve({
27
+ success: true,
28
+ path: tmpFile,
29
+ size: stats.size,
30
+ mime: "image/png",
31
+ base64: buffer.toString("base64"),
32
+ message: `Screenshot kaydedildi: ${tmpFile}`,
33
+ });
34
+ } else {
35
+ resolve({ success: false, error: `screencapture exit ${code}` });
36
+ }
37
+ });
38
+ proc.on("error", (e) => resolve({ success: false, error: e.message }));
39
+ });
40
+ }
41
+
42
+ async function captureWindow({ windowTitle = null, outputPath = null } = {}) {
43
+ if (!IS_MAC) return { success: false, error: "Sadece macOS" };
44
+
45
+ const tmpFile = outputPath || path.join(os.tmpdir(), `screenshot-${Date.now()}.png`);
46
+ return new Promise((resolve) => {
47
+ // screencapture -l <windowId> veya -w (interaktif, fare ile pencere seç)
48
+ const args = ["-x", "-t", "png", "-o"]; // -o: shadow yok
49
+ if (windowTitle) args.push("-l", String(windowTitle));
50
+ args.push(tmpFile);
51
+
52
+ const proc = spawn("screencapture", args);
53
+ proc.on("close", (code) => {
54
+ if (code === 0 && fs.existsSync(tmpFile)) {
55
+ const buffer = fs.readFileSync(tmpFile);
56
+ const stats = fs.statSync(tmpFile);
57
+ resolve({
58
+ success: true,
59
+ path: tmpFile,
60
+ size: stats.size,
61
+ mime: "image/png",
62
+ base64: buffer.toString("base64"),
63
+ });
64
+ } else {
65
+ resolve({ success: false, error: `screencapture exit ${code}` });
66
+ }
67
+ });
68
+ proc.on("error", (e) => resolve({ success: false, error: e.message }));
69
+ });
70
+ }
71
+
72
+ module.exports = {
73
+ name: "macos_screenshot",
74
+ description: "macOS native ekran goruntusu al (Playwright gerekmez). screencapture komutunu kullanir.",
75
+ inputSchema: {
76
+ type: "object",
77
+ properties: {
78
+ outputPath: { type: "string", description: "Kayit yolu (default: /tmp/screenshot-<ts>.png)" },
79
+ region: { type: "string", description: "full/window (default: full)", enum: ["full", "window"] },
80
+ windowTitle: { type: "string", description: "Pencere basligi (sadece region=window)" },
81
+ },
82
+ },
83
+ async execute(params) {
84
+ if (params.region === "window") return captureWindow(params);
85
+ return captureScreen(params);
86
+ },
87
+ };
@@ -1,75 +1,75 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const os = require('os');
4
-
5
- module.exports = {
6
- name: 'read_file',
7
- description: 'Read content from a file',
8
- inputSchema: {
9
- type: 'object',
10
- properties: {
11
- path: {
12
- type: 'string',
13
- description: 'File path to read'
14
- }
15
- },
16
- required: ['path']
17
- },
18
-
19
- async execute(params) {
20
- try {
21
- // Expand ~ to home directory
22
- let filePath = params.path.replace(/^~/, os.homedir());
23
- filePath = path.resolve(filePath);
24
-
25
- if (!fs.existsSync(filePath)) {
26
- return {
27
- success: false,
28
- error: 'File does not exist'
29
- };
30
- }
31
-
32
- const stats = fs.statSync(filePath);
33
-
34
- if (!stats.isFile()) {
35
- return {
36
- success: false,
37
- error: 'Path is not a file'
38
- };
39
- }
40
-
41
- // Check file size - if > 1MB, read only first 50KB
42
- if (stats.size > 1024 * 1024) {
43
- const fd = fs.openSync(filePath, 'r');
44
- const buf = Buffer.alloc(50000);
45
- fs.readSync(fd, buf, 0, 50000, 0);
46
- fs.closeSync(fd);
47
-
48
- const content = '[Büyük dosya - ilk ~50KB gösteriliyor]\n' + buf.toString('utf8');
49
-
50
- return {
51
- success: true,
52
- path: filePath,
53
- content,
54
- size: stats.size,
55
- truncated: true
56
- };
57
- }
58
-
59
- const content = fs.readFileSync(filePath, 'utf-8');
60
-
61
- return {
62
- success: true,
63
- path: filePath,
64
- content,
65
- size: stats.size,
66
- truncated: false
67
- };
68
- } catch (error) {
69
- return {
70
- success: false,
71
- error: error.message
72
- };
73
- }
74
- }
75
- };
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ module.exports = {
6
+ name: 'read_file',
7
+ description: 'Read content from a file',
8
+ inputSchema: {
9
+ type: 'object',
10
+ properties: {
11
+ path: {
12
+ type: 'string',
13
+ description: 'File path to read'
14
+ }
15
+ },
16
+ required: ['path']
17
+ },
18
+
19
+ async execute(params) {
20
+ try {
21
+ // v5.2.0: Robust path expansion (~, ~/, relative)
22
+ const { expandPath } = require('../utils/paths');
23
+ const filePath = expandPath(params.path);
24
+
25
+ if (!fs.existsSync(filePath)) {
26
+ return {
27
+ success: false,
28
+ error: 'File does not exist'
29
+ };
30
+ }
31
+
32
+ const stats = fs.statSync(filePath);
33
+
34
+ if (!stats.isFile()) {
35
+ return {
36
+ success: false,
37
+ error: 'Path is not a file'
38
+ };
39
+ }
40
+
41
+ // Check file size - if > 1MB, read only first 50KB
42
+ if (stats.size > 1024 * 1024) {
43
+ const fd = fs.openSync(filePath, 'r');
44
+ const buf = Buffer.alloc(50000);
45
+ fs.readSync(fd, buf, 0, 50000, 0);
46
+ fs.closeSync(fd);
47
+
48
+ const content = '[Büyük dosya - ilk ~50KB gösteriliyor]\n' + buf.toString('utf8');
49
+
50
+ return {
51
+ success: true,
52
+ path: filePath,
53
+ content,
54
+ size: stats.size,
55
+ truncated: true
56
+ };
57
+ }
58
+
59
+ const content = fs.readFileSync(filePath, 'utf-8');
60
+
61
+ return {
62
+ success: true,
63
+ path: filePath,
64
+ content,
65
+ size: stats.size,
66
+ truncated: false
67
+ };
68
+ } catch (error) {
69
+ return {
70
+ success: false,
71
+ error: error.message
72
+ };
73
+ }
74
+ }
75
+ };
@@ -0,0 +1,66 @@
1
+ /**
2
+ * paths.js — Path helper utilities (v5.2.0)
3
+ *
4
+ * Tum tool'larda ~/Desktop/test.txt gibi path'leri dogru handle etmek icin.
5
+ * Parton'un gercek testinde "File does not exist" bug'i duzeltildi.
6
+ */
7
+
8
+ const os = require("os");
9
+ const path = require("path");
10
+
11
+ /**
12
+ * Path'i normalize et ve ~/ expansion yap.
13
+ * Mac/Windows/Linux'ta calisir.
14
+ *
15
+ * Ornek:
16
+ * expandPath("~/Desktop/test.txt") -> "/Users/gencay/Desktop/test.txt"
17
+ * expandPath("~") -> "/Users/gencay"
18
+ * expandPath("/tmp/x") -> "/tmp/x"
19
+ * expandPath("Downloads/x.txt") -> "<cwd>/Downloads/x.txt"
20
+ */
21
+ function expandPath(inputPath) {
22
+ if (!inputPath) return inputPath;
23
+ let p = String(inputPath);
24
+
25
+ // ~ veya ~/foo -> home + foo
26
+ if (p === "~") return os.homedir();
27
+ if (p.startsWith("~/")) {
28
+ return path.join(os.homedir(), p.slice(2));
29
+ }
30
+ if (p.startsWith("~")) {
31
+ return path.join(os.homedir(), p.slice(1));
32
+ }
33
+
34
+ // Zaten absolut ise dokunma
35
+ if (path.isAbsolute(p)) return p;
36
+
37
+ // Relative ise cwd'ye gore resolve
38
+ return path.resolve(process.cwd(), p);
39
+ }
40
+
41
+ function isValidPath(p) {
42
+ if (!p) return false;
43
+ if (typeof p !== "string") return false;
44
+ if (p.includes("\0")) return false; // null byte
45
+ return true;
46
+ }
47
+
48
+ /**
49
+ * Path icin shell-safe quote (Parton'un "Downloads/adsiz klasor" gibi yollari icin)
50
+ * shlex.quote kullaniyor — universal
51
+ */
52
+ function quotePath(p) {
53
+ if (!p) return p;
54
+ const { execSync } = require("child_process");
55
+ // shlex.quote Python'dan cagiriliyor cunku Node'da yok
56
+ // Veya: bash -c 'echo $1' ile quote edilmis halini al
57
+ try {
58
+ // macOS/Unix: shlex.quote'a benzer sekilde single-quote wrap
59
+ if (/^[\w\-\.\/=]+$/.test(p)) return p; // guvenli karakterler
60
+ return `'${p.replace(/'/g, "'\\''")}'`;
61
+ } catch {
62
+ return p;
63
+ }
64
+ }
65
+
66
+ module.exports = { expandPath, isValidPath, quotePath };