natureco-cli 2.23.27 → 2.23.29

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.
@@ -1,11 +1,24 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const os = require('os');
4
+ const crypto = require('crypto');
5
+ const { ConfigParseError, ConfigMutationConflictError, ConfigValidationError, handleError } = require('./errors');
6
+
7
+ let json5;
8
+ try {
9
+ json5 = require('json5');
10
+ } catch {
11
+ json5 = null;
12
+ }
4
13
 
5
14
  const CONFIG_DIR = path.join(os.homedir(), '.natureco');
6
15
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
16
+ const CONFIG_BACKUP_DIR = path.join(CONFIG_DIR, 'backups');
17
+ const MAX_BACKUPS = 10;
18
+
19
+ let _configCache = null;
20
+ let _configHash = null;
7
21
 
8
- // --profile flag desteği: ~/.natureco-<profile>/
9
22
  function getProfileDir() {
10
23
  const profileArg = process.argv.find(a => a.startsWith('--profile='));
11
24
  const profileIdx = process.argv.indexOf('--profile');
@@ -27,50 +40,134 @@ function ensureConfigDir() {
27
40
  }
28
41
  }
29
42
 
30
- function saveConfig(data) {
43
+ function computeHash(data) {
44
+ return crypto.createHash('sha256').update(JSON.stringify(data)).digest('hex');
45
+ }
46
+
47
+ function createBackup() {
48
+ if (!fs.existsSync(ACTIVE_CONFIG_FILE)) return;
31
49
  ensureConfigDir();
32
- fs.writeFileSync(ACTIVE_CONFIG_FILE, JSON.stringify(data, null, 2), 'utf8');
50
+ if (!fs.existsSync(CONFIG_BACKUP_DIR)) {
51
+ fs.mkdirSync(CONFIG_BACKUP_DIR, { recursive: true });
52
+ }
53
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
54
+ const backupFile = path.join(CONFIG_BACKUP_DIR, `config-${timestamp}.json`);
55
+ fs.copyFileSync(ACTIVE_CONFIG_FILE, backupFile);
56
+ const backups = fs.readdirSync(CONFIG_BACKUP_DIR)
57
+ .filter(f => f.startsWith('config-') && f.endsWith('.json'))
58
+ .sort()
59
+ .reverse();
60
+ if (backups.length > MAX_BACKUPS) {
61
+ backups.slice(MAX_BACKUPS).forEach(f => {
62
+ try { fs.unlinkSync(path.join(CONFIG_BACKUP_DIR, f)); } catch {}
63
+ });
64
+ }
65
+ }
66
+
67
+ function parseConfigContent(content) {
68
+ if (!content || !content.trim()) return {};
69
+ const trimmed = content.trim();
70
+ if (trimmed.startsWith('{')) {
71
+ if (json5) {
72
+ try { return json5.parse(trimmed); } catch {}
73
+ }
74
+ return JSON.parse(trimmed);
75
+ }
76
+ return JSON.parse(trimmed);
77
+ }
78
+
79
+ function validateConfig(data) {
80
+ if (data === null || data === undefined) throw new ConfigValidationError('Config cannot be null', { field: 'root' });
81
+ if (typeof data !== 'object' || Array.isArray(data)) throw new ConfigValidationError('Config must be a JSON object', { field: 'root' });
82
+ if (data.apiKey !== undefined && typeof data.apiKey !== 'string') throw new ConfigValidationError('apiKey must be a string', { field: 'apiKey' });
83
+ if (data.providerUrl !== undefined && typeof data.providerUrl !== 'string') throw new ConfigValidationError('providerUrl must be a string', { field: 'providerUrl' });
84
+ if (data.providerModel !== undefined && typeof data.providerModel !== 'string') throw new ConfigValidationError('providerModel must be a string', { field: 'providerModel' });
85
+ return true;
86
+ }
87
+
88
+ function saveConfig(data, options = {}) {
89
+ const { skipBackup = false, skipValidation = false } = options;
90
+ ensureConfigDir();
91
+ if (!skipValidation) validateConfig(data);
92
+ if (!skipBackup) createBackup();
93
+ const content = JSON.stringify(data, null, 2);
94
+ fs.writeFileSync(ACTIVE_CONFIG_FILE, content, 'utf8');
95
+ _configCache = data;
96
+ _configHash = computeHash(data);
33
97
  }
34
98
 
35
- function loadConfig() {
99
+ function loadConfig(options = {}) {
100
+ const { useCache = true, skipValidation = false } = options;
101
+ if (useCache && _configCache) return _configCache;
36
102
  if (!fs.existsSync(ACTIVE_CONFIG_FILE)) {
103
+ _configCache = null;
104
+ _configHash = null;
37
105
  return null;
38
106
  }
39
107
  try {
40
108
  const content = fs.readFileSync(ACTIVE_CONFIG_FILE, 'utf8');
41
- return JSON.parse(content);
109
+ const data = parseConfigContent(content);
110
+ if (!skipValidation) validateConfig(data);
111
+ _configCache = data;
112
+ _configHash = computeHash(data);
113
+ return data;
42
114
  } catch (err) {
115
+ _configCache = null;
116
+ _configHash = null;
117
+ if (err instanceof ConfigValidationError || err instanceof ConfigParseError) throw err;
43
118
  return null;
44
119
  }
45
120
  }
46
121
 
122
+ function loadConfigWithRetry(maxRetries = 3) {
123
+ for (let i = 0; i < maxRetries; i++) {
124
+ try {
125
+ return loadConfig({ useCache: false });
126
+ } catch (err) {
127
+ if (i === maxRetries - 1) throw err;
128
+ }
129
+ }
130
+ return null;
131
+ }
132
+
47
133
  function deleteConfig() {
48
134
  if (fs.existsSync(ACTIVE_CONFIG_FILE)) {
135
+ createBackup();
49
136
  fs.unlinkSync(ACTIVE_CONFIG_FILE);
50
137
  }
138
+ _configCache = null;
139
+ _configHash = null;
51
140
  }
52
141
 
53
142
  function getApiKey() {
54
143
  const config = loadConfig();
55
- return config?.apiKey || null;
144
+ return config?.apiKey ?? null;
56
145
  }
57
146
 
58
147
  function saveApiKey(apiKey) {
59
- const config = loadConfig() || {};
148
+ const config = loadConfig() ?? {};
60
149
  config.apiKey = apiKey;
61
150
  saveConfig(config);
62
151
  }
63
152
 
64
153
  function getConfig() {
65
- return loadConfig() || {};
154
+ try {
155
+ return loadConfig() ?? {};
156
+ } catch {
157
+ return {};
158
+ }
66
159
  }
67
160
 
68
161
  function getAllConfig() {
69
- return loadConfig() || {};
162
+ try {
163
+ return loadConfig() ?? {};
164
+ } catch {
165
+ return {};
166
+ }
70
167
  }
71
168
 
72
169
  function setConfigValue(key, value) {
73
- const config = loadConfig() || {};
170
+ const config = loadConfig() ?? {};
74
171
  const keys = key.split('.');
75
172
  let current = config;
76
173
  for (let i = 0; i < keys.length - 1; i++) {
@@ -83,15 +180,49 @@ function setConfigValue(key, value) {
83
180
  saveConfig(config);
84
181
  }
85
182
 
183
+ function getConfigHash() {
184
+ return _configHash;
185
+ }
186
+
187
+ function listBackups() {
188
+ if (!fs.existsSync(CONFIG_BACKUP_DIR)) return [];
189
+ return fs.readdirSync(CONFIG_BACKUP_DIR)
190
+ .filter(f => f.startsWith('config-') && f.endsWith('.json'))
191
+ .sort()
192
+ .reverse();
193
+ }
194
+
195
+ function restoreConfig(backupFile) {
196
+ const backupPath = path.isAbsolute(backupFile)
197
+ ? backupFile
198
+ : path.join(CONFIG_BACKUP_DIR, backupFile);
199
+ if (!fs.existsSync(backupPath)) {
200
+ throw new ConfigValidationError(`Yedek dosyası bulunamadı: ${backupPath}`, { field: 'backupFile' });
201
+ }
202
+ const content = fs.readFileSync(backupPath, 'utf8');
203
+ const data = parseConfigContent(content);
204
+ validateConfig(data);
205
+ createBackup();
206
+ fs.writeFileSync(ACTIVE_CONFIG_FILE, JSON.stringify(data, null, 2), 'utf8');
207
+ _configCache = data;
208
+ _configHash = computeHash(data);
209
+ return { path: backupPath, timestamp: path.basename(backupPath).replace(/^config-|\.json$/g, '') };
210
+ }
211
+
86
212
  module.exports = {
87
213
  saveConfig,
88
214
  loadConfig,
215
+ loadConfigWithRetry,
89
216
  deleteConfig,
90
217
  getApiKey,
91
218
  saveApiKey,
92
219
  getConfig,
93
220
  getAllConfig,
94
221
  setConfigValue,
222
+ getConfigHash,
223
+ listBackups,
224
+ restoreConfig,
95
225
  CONFIG_FILE: ACTIVE_CONFIG_FILE,
96
226
  CONFIG_DIR: ACTIVE_CONFIG_DIR,
227
+ CONFIG_BACKUP_DIR,
97
228
  };
@@ -0,0 +1,148 @@
1
+ const chalk = require('chalk');
2
+
3
+ class NatureCoError extends Error {
4
+ constructor(message, options = {}) {
5
+ super(message);
6
+ this.name = this.constructor.name;
7
+ this.cause = options.cause || null;
8
+ this.exitCode = options.exitCode || 1;
9
+ if (Error.captureStackTrace) {
10
+ Error.captureStackTrace(this, this.constructor);
11
+ }
12
+ }
13
+ }
14
+
15
+ class ConfigError extends NatureCoError {
16
+ constructor(message, options = {}) {
17
+ super(message, options);
18
+ this.configPath = options.configPath || null;
19
+ }
20
+ }
21
+
22
+ class ConfigParseError extends ConfigError {
23
+ constructor(message, options = {}) {
24
+ super(`Config parse error: ${message}`, options);
25
+ }
26
+ }
27
+
28
+ class ConfigMutationConflictError extends ConfigError {
29
+ constructor(message, options = {}) {
30
+ super(`Config conflict: ${message}`, options);
31
+ this.currentHash = options.currentHash || null;
32
+ }
33
+ }
34
+
35
+ class ConfigValidationError extends ConfigError {
36
+ constructor(message, options = {}) {
37
+ super(`Config validation error: ${message}`, options);
38
+ this.field = options.field || null;
39
+ }
40
+ }
41
+
42
+ class ApiError extends NatureCoError {
43
+ constructor(message, options = {}) {
44
+ super(message, options);
45
+ this.statusCode = options.statusCode || null;
46
+ this.provider = options.provider || null;
47
+ }
48
+ }
49
+
50
+ class ProviderError extends ApiError {
51
+ constructor(message, options = {}) {
52
+ super(`Provider error (${options.provider || 'unknown'}): ${message}`, options);
53
+ }
54
+ }
55
+
56
+ class AuthenticationError extends ApiError {
57
+ constructor(message, options = {}) {
58
+ super(`Authentication error: ${message}`, options);
59
+ }
60
+ }
61
+
62
+ class ToolError extends NatureCoError {
63
+ constructor(message, options = {}) {
64
+ super(message, options);
65
+ this.toolName = options.toolName || null;
66
+ }
67
+ }
68
+
69
+ class ToolInputError extends ToolError {
70
+ constructor(message, options = {}) {
71
+ super(`Invalid tool input: ${message}`, options);
72
+ }
73
+ }
74
+
75
+ class ToolExecutionError extends ToolError {
76
+ constructor(message, options = {}) {
77
+ super(`Tool execution failed: ${message}`, options);
78
+ }
79
+ }
80
+
81
+ class ChannelError extends NatureCoError {
82
+ constructor(message, options = {}) {
83
+ super(message, options);
84
+ this.channel = options.channel || null;
85
+ }
86
+ }
87
+
88
+ class GatewayError extends NatureCoError {
89
+ constructor(message, options = {}) {
90
+ super(message, options);
91
+ }
92
+ }
93
+
94
+ class PluginError extends NatureCoError {
95
+ constructor(message, options = {}) {
96
+ super(message, options);
97
+ this.plugin = options.plugin || null;
98
+ }
99
+ }
100
+
101
+ class SkillError extends NatureCoError {
102
+ constructor(message, options = {}) {
103
+ super(message, options);
104
+ this.skill = options.skill || null;
105
+ }
106
+ }
107
+
108
+ class MigrationError extends NatureCoError {
109
+ constructor(message, options = {}) {
110
+ super(message, options);
111
+ this.step = options.step || null;
112
+ }
113
+ }
114
+
115
+ function handleError(err, options = {}) {
116
+ const { prefix = '', exit = true, log = true } = options;
117
+
118
+ if (log) {
119
+ const message = err instanceof NatureCoError
120
+ ? `${prefix}${err.message}`
121
+ : `${prefix}${err.message || 'An unknown error occurred'}`;
122
+ console.log(chalk.red(`\n${message}\n`));
123
+ }
124
+
125
+ if (exit) {
126
+ process.exit(err instanceof NatureCoError ? err.exitCode : 1);
127
+ }
128
+ }
129
+
130
+ module.exports = {
131
+ NatureCoError,
132
+ ConfigError,
133
+ ConfigParseError,
134
+ ConfigMutationConflictError,
135
+ ConfigValidationError,
136
+ ApiError,
137
+ ProviderError,
138
+ AuthenticationError,
139
+ ToolError,
140
+ ToolInputError,
141
+ ToolExecutionError,
142
+ ChannelError,
143
+ GatewayError,
144
+ PluginError,
145
+ SkillError,
146
+ MigrationError,
147
+ handleError,
148
+ };
@@ -1,4 +1,4 @@
1
- const { select, input, password, confirm } = require('@inquirer/prompts');
1
+ const { select, input, password, confirm, checkbox } = require('@inquirer/prompts');
2
2
 
3
3
  module.exports = {
4
4
  async prompt(questions) {
@@ -14,7 +14,6 @@ module.exports = {
14
14
  } else if (q.type === 'password') {
15
15
  results[q.name] = await password({ message: q.message, mask: q.mask });
16
16
  } else if (q.type === 'checkbox') {
17
- const { checkbox } = require('@inquirer/prompts');
18
17
  results[q.name] = await checkbox({
19
18
  message: q.message,
20
19
  choices: q.choices.map(c =>
@@ -3,19 +3,19 @@ const os = require('os');
3
3
  function normalizeWindowsPaths(str) {
4
4
  let result = str.replace(/\\/g, '/');
5
5
 
6
- result = result
7
- .replace(/C:\/\/\/\/Users\/\/\/\/user\/\/\/\//g, `${os.homedir()}/`)
8
- .replace(/C:\/Users\/user\//g, `${os.homedir()}/`)
9
- .replace(/C:\/Users\/user\//g, `${os.homedir()}/`)
10
- .replace(/C:\/\/Users\/\/user\/\/\.natureco\/\//g, `${os.homedir()}/.natureco/`)
11
- .replace(/C:\/\/Users\/\/user\/\//g, `${os.homedir()}/`)
12
- .replace(/E:\/\/\.natureco\/\//g, `${os.homedir()}/.natureco/`)
13
- .replace(/'C:\/Users\/user\/\.natureco\//g, `'${os.homedir()}/.natureco/`)
14
- .replace(/"C:\/Users\/user\/\.natureco\//g, `"${os.homedir()}/.natureco/`)
15
- .replace(/'C:\/\/\/\/Users\/\/\/\/user\/\/\/\/\.natureco\/\/\/\//g, `'${os.homedir()}/.natureco/`)
16
- .replace(/E:\/\.openclaw\//g, `${os.homedir()}/.natureco/`)
17
- .replace(/\.openclaw\//g, '.natureco/')
18
- .replace(/workspace\/scripts\\/g, 'workspace/scripts/');
6
+ const homeDir = os.homedir().replace(/\\/g, '/');
7
+
8
+ // Replace any Windows user profile path with actual homedir
9
+ result = result.replace(/[A-Za-z]:\/Users\/[^/]+\//g, `${homeDir}/`);
10
+
11
+ // Replace bare drive-letter references pointing to .openclaw
12
+ result = result.replace(/[A-Za-z]:\/\.openclaw\//g, `${homeDir}/.natureco/`);
13
+
14
+ // Migrate .openclaw paths to .natureco
15
+ result = result.replace(/\.openclaw\//g, '.natureco/');
16
+
17
+ // Normalize mixed path separators
18
+ result = result.replace(/workspace\/scripts\\/g, 'workspace/scripts/');
19
19
 
20
20
  return result;
21
21
  }
@@ -0,0 +1,238 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const { execSync } = require('child_process');
5
+ const { PluginError, handleError } = require('./errors');
6
+
7
+ const PLUGINS_DIR = path.join(os.homedir(), '.natureco', 'plugins');
8
+ const REGISTRY_FILE = path.join(os.homedir(), '.natureco', 'plugin-registry.json');
9
+
10
+ function ensureDir(dir) {
11
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
12
+ }
13
+
14
+ function loadRegistry() {
15
+ if (!fs.existsSync(REGISTRY_FILE)) return { version: 1, plugins: [], updatedAt: null };
16
+ try {
17
+ return JSON.parse(fs.readFileSync(REGISTRY_FILE, 'utf-8'));
18
+ } catch {
19
+ return { version: 1, plugins: [], updatedAt: null };
20
+ }
21
+ }
22
+
23
+ function saveRegistry(registry) {
24
+ ensureDir(path.dirname(REGISTRY_FILE));
25
+ registry.updatedAt = new Date().toISOString();
26
+ fs.writeFileSync(REGISTRY_FILE, JSON.stringify(registry, null, 2), 'utf-8');
27
+ }
28
+
29
+ function scanInstalled() {
30
+ ensureDir(PLUGINS_DIR);
31
+ return fs.readdirSync(PLUGINS_DIR, { withFileTypes: true })
32
+ .filter(d => d.isDirectory())
33
+ .map(d => {
34
+ const manifestFile = path.join(PLUGINS_DIR, d.name, 'plugin.json');
35
+ const pkgFile = path.join(PLUGINS_DIR, d.name, 'package.json');
36
+ const manifest = readManifest(manifestFile, pkgFile, d.name);
37
+ const disabledFile = path.join(PLUGINS_DIR, d.name, '.disabled');
38
+ return { ...manifest, slug: d.name, enabled: !fs.existsSync(disabledFile), installPath: path.join(PLUGINS_DIR, d.name) };
39
+ });
40
+ }
41
+
42
+ function readManifest(manifestFile, pkgFile, fallbackName) {
43
+ let meta = { name: fallbackName, description: '', version: '1.0.0', author: '', license: 'MIT', keywords: [], entry: 'index.js' };
44
+ if (fs.existsSync(pkgFile)) {
45
+ try {
46
+ const pkg = JSON.parse(fs.readFileSync(pkgFile, 'utf-8'));
47
+ meta = { ...meta, name: pkg.name || meta.name, description: pkg.description || meta.description, version: pkg.version || meta.version, author: pkg.author || meta.author, license: pkg.license || meta.license, keywords: pkg.keywords || meta.keywords, entry: pkg.main || meta.entry, dependencies: pkg.dependencies, openclaw: pkg.openclaw };
48
+ } catch {}
49
+ }
50
+ if (fs.existsSync(manifestFile)) {
51
+ try {
52
+ const m = JSON.parse(fs.readFileSync(manifestFile, 'utf-8'));
53
+ meta = { ...meta, ...m };
54
+ } catch {}
55
+ }
56
+ return meta;
57
+ }
58
+
59
+ function validateManifest(manifest) {
60
+ const errors = [];
61
+ if (!manifest.name) errors.push('Plugin adı (name) gerekli');
62
+ if (!manifest.entry && !manifest.openclaw?.tool) errors.push('Entry noktası (entry/index.js) gerekli');
63
+ if (manifest.version && !/^\d+\.\d+\.\d+/.test(manifest.version)) errors.push('Version semver formatında olmalı (x.y.z)');
64
+ return errors;
65
+ }
66
+
67
+ function getInstalledIds() {
68
+ const registry = loadRegistry();
69
+ const installed = new Set(registry.plugins.map(p => p.id));
70
+ scanInstalled().forEach(p => installed.add(p.slug));
71
+ return [...installed];
72
+ }
73
+
74
+ function getPlugin(slug) {
75
+ return scanInstalled().find(p => p.slug === slug) || null;
76
+ }
77
+
78
+ async function installPlugin(spec) {
79
+ ensureDir(PLUGINS_DIR);
80
+
81
+ if (spec.startsWith('./') || spec.startsWith('/') || spec.startsWith('.\\') || spec.includes(':\\') || spec.startsWith('\\\\')) {
82
+ return installLocal(spec);
83
+ }
84
+ if (spec.startsWith('clawhub:') || spec.startsWith('naturehub:')) {
85
+ return installFromHub(spec);
86
+ }
87
+ if (spec.startsWith('npm:')) {
88
+ return installFromNpm(spec.slice(4));
89
+ }
90
+ if (spec.startsWith('git:')) {
91
+ return installFromGit(spec.slice(4));
92
+ }
93
+ if (spec.includes('/') && !spec.startsWith('@')) {
94
+ return installFromNpm(spec);
95
+ }
96
+ return installFromNpm(spec);
97
+ }
98
+
99
+ function installLocal(spec) {
100
+ const src = path.resolve(spec);
101
+ if (!fs.existsSync(src)) throw new PluginError(`Yol bulunamadı: ${src}`, 'install', spec);
102
+ const slug = path.basename(src);
103
+ const dest = path.join(PLUGINS_DIR, slug);
104
+ if (fs.existsSync(dest)) {
105
+ fs.rmSync(dest, { recursive: true, force: true });
106
+ }
107
+ fs.cpSync(src, dest, { recursive: true });
108
+ const manifest = readManifest(path.join(dest, 'plugin.json'), path.join(dest, 'package.json'), slug);
109
+ const errors = validateManifest(manifest);
110
+ if (errors.length > 0) {
111
+ fs.rmSync(dest, { recursive: true, force: true });
112
+ throw new PluginError(`Geçersiz manifest: ${errors.join(', ')}`, 'install', spec);
113
+ }
114
+ const registry = loadRegistry();
115
+ registry.plugins = registry.plugins.filter(p => p.id !== slug);
116
+ registry.plugins.push({ id: slug, name: manifest.name, version: manifest.version, source: 'local', spec, installedAt: new Date().toISOString() });
117
+ saveRegistry(registry);
118
+ return { slug, name: manifest.name, version: manifest.version, source: 'local' };
119
+ }
120
+
121
+ function installFromNpm(pkg) {
122
+ const tmpDir = path.join(os.tmpdir(), `nc-plugin-${Date.now()}`);
123
+ ensureDir(tmpDir);
124
+ try {
125
+ execSync(`npm install ${pkg} --prefix "${tmpDir}" --no-save --ignore-scripts --no-audit --no-fund`, { stdio: 'pipe', timeout: 120000 });
126
+ const pkgDir = path.join(tmpDir, 'node_modules', pkg.split('/').pop());
127
+ const scopedPkgDir = pkg.startsWith('@') ? path.join(tmpDir, 'node_modules', pkg) : null;
128
+ const srcDir = scopedPkgDir && fs.existsSync(scopedPkgDir) ? scopedPkgDir : (fs.existsSync(pkgDir) ? pkgDir : null);
129
+ if (!srcDir) throw new PluginError(`Paket bulunamadı: ${pkg}`, 'install', pkg);
130
+ const pkgJson = JSON.parse(fs.readFileSync(path.join(srcDir, 'package.json'), 'utf-8'));
131
+ const slug = pkgJson.name?.replace(/@/g, '').replace(/\//g, '-') || pkg.replace(/[@\/]/g, '-').replace(/^-/, '');
132
+ const dest = path.join(PLUGINS_DIR, slug);
133
+ if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true });
134
+ fs.cpSync(srcDir, dest, { recursive: true });
135
+ const manifest = readManifest(path.join(dest, 'plugin.json'), path.join(dest, 'package.json'), slug);
136
+ const errors = validateManifest(manifest);
137
+ if (errors.length > 0) {
138
+ fs.rmSync(dest, { recursive: true, force: true });
139
+ throw new PluginError(`Geçersiz manifest: ${errors.join(', ')}`, 'install', pkg);
140
+ }
141
+ fs.rmSync(tmpDir, { recursive: true, force: true });
142
+ const registry = loadRegistry();
143
+ registry.plugins = registry.plugins.filter(p => p.id !== slug);
144
+ registry.plugins.push({ id: slug, name: manifest.name, version: manifest.version, source: 'npm', spec: pkg, installedAt: new Date().toISOString() });
145
+ saveRegistry(registry);
146
+ return { slug, name: manifest.name, version: manifest.version, source: 'npm' };
147
+ } catch (err) {
148
+ if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
149
+ throw err;
150
+ }
151
+ }
152
+
153
+ function installFromGit(spec) {
154
+ const repoUrl = spec.startsWith('github.com/') ? `https://${spec}` : spec.startsWith('http') ? spec : spec.includes('/') ? `https://github.com/${spec}` : spec;
155
+ const tmpDir = path.join(os.tmpdir(), `nc-plugin-git-${Date.now()}`);
156
+ ensureDir(tmpDir);
157
+ try {
158
+ execSync(`git clone --depth 1 "${repoUrl}" "${tmpDir}"`, { stdio: 'pipe', timeout: 60000 });
159
+ const slug = spec.split('/').pop().replace(/\.git$/, '');
160
+ const dest = path.join(PLUGINS_DIR, slug);
161
+ if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true });
162
+ const items = fs.readdirSync(tmpDir);
163
+ const subDir = items.length === 1 && fs.statSync(path.join(tmpDir, items[0])).isDirectory() ? path.join(tmpDir, items[0]) : tmpDir;
164
+ fs.cpSync(subDir, dest, { recursive: true });
165
+ const manifest = readManifest(path.join(dest, 'plugin.json'), path.join(dest, 'package.json'), slug);
166
+ const errors = validateManifest(manifest);
167
+ if (errors.length > 0) {
168
+ fs.rmSync(dest, { recursive: true, force: true });
169
+ throw new PluginError(`Geçersiz manifest: ${errors.join(', ')}`, 'install', spec);
170
+ }
171
+ fs.rmSync(tmpDir, { recursive: true, force: true });
172
+ const registry = loadRegistry();
173
+ registry.plugins = registry.plugins.filter(p => p.id !== slug);
174
+ registry.plugins.push({ id: slug, name: manifest.name, version: manifest.version, source: 'git', spec: repoUrl, installedAt: new Date().toISOString() });
175
+ saveRegistry(registry);
176
+ return { slug, name: manifest.name, version: manifest.version, source: 'git' };
177
+ } catch (err) {
178
+ if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
179
+ throw err;
180
+ }
181
+ }
182
+
183
+ function installFromHub(spec) {
184
+ const pkg = spec.replace(/^(clawhub|naturehub):\/?/, '');
185
+ return installFromNpm(pkg);
186
+ }
187
+
188
+ async function uninstallPlugin(slug, options = {}) {
189
+ const plugin = getPlugin(slug);
190
+ if (!plugin) throw new PluginError(`Plugin bulunamadı: ${slug}`, 'uninstall', slug);
191
+ if (!options.keepFiles) {
192
+ fs.rmSync(plugin.installPath, { recursive: true, force: true });
193
+ }
194
+ const registry = loadRegistry();
195
+ registry.plugins = registry.plugins.filter(p => p.id !== slug);
196
+ saveRegistry(registry);
197
+ return { slug, name: plugin.name };
198
+ }
199
+
200
+ async function updatePlugin(slug) {
201
+ const plugin = getPlugin(slug);
202
+ if (!plugin) throw new PluginError(`Plugin bulunamadı: ${slug}`, 'update', slug);
203
+ const registry = loadRegistry();
204
+ const record = registry.plugins.find(p => p.id === slug);
205
+ if (!record || !record.spec) throw new PluginError(`Plugin kaydı bulunamadı veya spec eksik: ${slug}`, 'update', slug);
206
+ if (record.source === 'npm') {
207
+ fs.rmSync(plugin.installPath, { recursive: true, force: true });
208
+ return installFromNpm(record.spec);
209
+ }
210
+ if (record.source === 'git' && record.spec) {
211
+ fs.rmSync(plugin.installPath, { recursive: true, force: true });
212
+ return installFromGit(record.spec);
213
+ }
214
+ throw new PluginError(`${record.source} kaynağından güncelleme desteklenmiyor`, 'update', slug);
215
+ }
216
+
217
+ function searchRegistry(query) {
218
+ const registry = loadRegistry();
219
+ const q = query.toLowerCase();
220
+ return registry.plugins.filter(p =>
221
+ p.id.toLowerCase().includes(q) ||
222
+ (p.name || '').toLowerCase().includes(q)
223
+ );
224
+ }
225
+
226
+ module.exports = {
227
+ PLUGINS_DIR,
228
+ loadRegistry,
229
+ saveRegistry,
230
+ scanInstalled,
231
+ getPlugin,
232
+ getInstalledIds,
233
+ installPlugin,
234
+ uninstallPlugin,
235
+ updatePlugin,
236
+ searchRegistry,
237
+ validateManifest,
238
+ };