@nibgat/aide 0.0.0-stage → 0.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/README.md +79 -2
- package/aide.mjs +205 -0
- package/package.json +29 -4
- package/src/client.mjs +260 -0
- package/src/commands.mjs +360 -0
- package/src/device.mjs +488 -0
- package/src/repl.mjs +501 -0
- package/src/ui.mjs +260 -0
package/src/device.mjs
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cihaz modu (terminal/OS yönetimi): bu makineyi Ai-de'ye cihaz olarak bağlar. Masaüstü uygulamasının çalıştırıcısıyla
|
|
3
|
+
* aynı protokol (`/ws/device`): sunucu personelin terminal ve dosya işlerini bu makinede, yalnızca çalışma klasöründe ve
|
|
4
|
+
* yazma/komut için kullanıcı onayıyla yürütür. Ekransız sunucularda `--approve-all` ile (bilerek) onaysız çalışır.
|
|
5
|
+
*/
|
|
6
|
+
import { exec, execFile } from 'node:child_process';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import { settings, ask, AideError } from './client.mjs';
|
|
12
|
+
import { c } from './ui.mjs';
|
|
13
|
+
|
|
14
|
+
const MAX_OUTPUT = 20000;
|
|
15
|
+
const IGNORED = new Set(['node_modules', '.git', 'dist', '.next', '__pycache__']);
|
|
16
|
+
|
|
17
|
+
const jail = (workspaceDir, raw = '.') => {
|
|
18
|
+
const resolved = path.resolve(workspaceDir, String(raw || '.').replace(/^['"]|['"]$/g, ''));
|
|
19
|
+
const relative = path.relative(workspaceDir, resolved);
|
|
20
|
+
|
|
21
|
+
if(relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
22
|
+
throw new Error(`"${raw}" çalışma klasörünün dışında; erişim engellendi.`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return resolved;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const listDir = (dir, depth = 0, lines = []) => {
|
|
29
|
+
if(depth > 2 || lines.length > 300) {
|
|
30
|
+
return lines;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for(const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
34
|
+
if(IGNORED.has(entry.name)) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
lines.push(`${' '.repeat(depth)}${entry.isDirectory() ? '📁' : '📄'} ${entry.name}`);
|
|
39
|
+
|
|
40
|
+
if(entry.isDirectory()) {
|
|
41
|
+
listDir(path.join(dir, entry.name), depth + 1, lines);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return lines;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const searchFiles = (dir, needle, results = []) => {
|
|
49
|
+
for(const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
50
|
+
if(IGNORED.has(entry.name) || results.length >= 50) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const full = path.join(dir, entry.name);
|
|
55
|
+
|
|
56
|
+
if(entry.isDirectory()) {
|
|
57
|
+
searchFiles(full, needle, results);
|
|
58
|
+
} else if(fs.statSync(full).size < 1024 * 1024) {
|
|
59
|
+
fs.readFileSync(full, 'utf8').split('\n').forEach((line, index) => {
|
|
60
|
+
if(results.length < 50 && line.includes(needle)) {
|
|
61
|
+
results.push(`${full}:${index + 1}: ${line.trim().slice(0, 160)}`);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return results;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const codeEditor = async (workspaceDir, args, approve) => {
|
|
71
|
+
const action = String(args.action || 'read').toLowerCase();
|
|
72
|
+
const target = jail(workspaceDir, args.path || args.filePath || args.dirPath || '.');
|
|
73
|
+
|
|
74
|
+
if(action === 'list') {
|
|
75
|
+
return listDir(target).join('\n') || '(boş klasör)';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if(action === 'read') {
|
|
79
|
+
return fs.readFileSync(target, 'utf8').slice(0, MAX_OUTPUT);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if(action === 'search' || action === 'find_files') {
|
|
83
|
+
return searchFiles(target, String(args.query || args.pattern || '')).join('\n') || 'Eşleşme yok.';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if(action === 'write' || action === 'edit') {
|
|
87
|
+
if(!(await approve(`Dosya ${action === 'write' ? 'yazılacak' : 'düzenlenecek'}: ${target}`))) {
|
|
88
|
+
throw new Error('Kullanıcı dosya işlemini onaylamadı.');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
92
|
+
|
|
93
|
+
if(action === 'write') {
|
|
94
|
+
fs.writeFileSync(target, String(args.content ?? ''), 'utf8');
|
|
95
|
+
|
|
96
|
+
return `Yazıldı: ${target}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const current = fs.readFileSync(target, 'utf8');
|
|
100
|
+
const search = String(args.search ?? args.oldText ?? '');
|
|
101
|
+
|
|
102
|
+
if(!search || !current.includes(search)) {
|
|
103
|
+
throw new Error('Düzenlenecek metin dosyada bulunamadı.');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fs.writeFileSync(target, current.replace(search, String(args.replace ?? args.newText ?? '')), 'utf8');
|
|
107
|
+
|
|
108
|
+
return `Düzenlendi: ${target}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
throw new Error(`Desteklenmeyen işlem: ${action}`);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const terminal = async (workspaceDir, args, approve) => {
|
|
115
|
+
const command = String(args.command || '').trim();
|
|
116
|
+
|
|
117
|
+
if(!command) {
|
|
118
|
+
throw new Error('Komut boş.');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const cwd = jail(workspaceDir, args.cwd || '.');
|
|
122
|
+
|
|
123
|
+
if(!(await approve(`Komut çalıştırılacak (${cwd}):\n ${c.bold(command)}`))) {
|
|
124
|
+
throw new Error('Kullanıcı komutu onaylamadı.');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return new Promise((resolve) => {
|
|
128
|
+
exec(command, {
|
|
129
|
+
shell: process.platform === 'win32' ? 'powershell.exe' : '/bin/sh',
|
|
130
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
131
|
+
windowsHide: true,
|
|
132
|
+
timeout: 120000,
|
|
133
|
+
cwd
|
|
134
|
+
}, (error, stdout, stderr) => {
|
|
135
|
+
const output = `${stdout || ''}${stderr ? `\n[stderr]\n${stderr}` : ''}`.slice(0, MAX_OUTPUT);
|
|
136
|
+
|
|
137
|
+
resolve(error && !stdout ? `Çıkış kodu ${error.code ?? '?'}: ${output || error.message}` : output || '(çıktı yok)');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/** Sistem durumu (salt okuma, onaysız): sunucudaki src/services/systemInfo.js ile aynı biçim. */
|
|
143
|
+
const run = (file, args) => new Promise((resolve) => {
|
|
144
|
+
execFile(file, args, { timeout: 15000, maxBuffer: 2 * 1024 * 1024, windowsHide: true }, (error, stdout) => resolve(error ? '' : String(stdout || '')));
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const gb = (bytes) => `${(bytes / 1073741824).toFixed(1)} GB`;
|
|
148
|
+
|
|
149
|
+
const disks = async () => {
|
|
150
|
+
if(process.platform === 'win32') {
|
|
151
|
+
const json = await run('powershell.exe', ['-NoProfile', '-Command', 'Get-PSDrive -PSProvider FileSystem | Select-Object Name,Used,Free | ConvertTo-Json -Compress']);
|
|
152
|
+
try {
|
|
153
|
+
const rows = [].concat(JSON.parse(json || '[]'));
|
|
154
|
+
return rows.filter((row) => row.Used !== null && row.Free !== null).map((row) => `${row.Name}: ${gb(row.Used)} dolu / ${gb(row.Used + row.Free)} (%${Math.round(row.Used / (row.Used + row.Free) * 100)})`);
|
|
155
|
+
} catch {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const text = await run('df', ['-kP']);
|
|
160
|
+
return text.split('\n').slice(1).map((line) => line.trim().split(/\s+/)).filter((cols) => cols.length >= 6 && /^\//.test(cols[5]) && !/^\/(dev|run|sys|proc|snap)/.test(cols[5])).map((cols) => `${cols[5]}: ${gb(Number(cols[2]) * 1024)} dolu / ${gb(Number(cols[1]) * 1024)} (${cols[4]})`);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const topProcesses = async () => {
|
|
164
|
+
if(process.platform === 'win32') {
|
|
165
|
+
const json = await run('powershell.exe', ['-NoProfile', '-Command', 'Get-Process | Sort-Object WS -Descending | Select-Object -First 8 Name,Id,WS | ConvertTo-Json -Compress']);
|
|
166
|
+
try {
|
|
167
|
+
return [].concat(JSON.parse(json || '[]')).map((row) => `${row.Name} (PID ${row.Id}): ${gb(row.WS)}`);
|
|
168
|
+
} catch {
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const text = await run('ps', process.platform === 'darwin' ? ['-axo', 'pid=,rss=,comm=', '-m'] : ['-eo', 'pid=,rss=,comm=', '--sort=-rss']);
|
|
173
|
+
return text.split('\n').filter(Boolean).slice(0, 8).map((line) => {
|
|
174
|
+
const [pid, rss, ...name] = line.trim().split(/\s+/);
|
|
175
|
+
return `${name.join(' ').split('/').pop()} (PID ${pid}): ${gb(Number(rss) * 1024)}`;
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const systemInfo = async (label = 'Ai-de sunucusu') => {
|
|
180
|
+
const [diskLines, processLines] = await Promise.all([disks(), topProcesses()]);
|
|
181
|
+
const total = os.totalmem();
|
|
182
|
+
const free = os.freemem();
|
|
183
|
+
const hours = Math.round(os.uptime() / 3600);
|
|
184
|
+
return [
|
|
185
|
+
`${label}: ${os.hostname()} · ${os.type()} ${os.release()} (${os.arch()})`,
|
|
186
|
+
`CPU: ${os.cpus()[0]?.model || '?'} × ${os.cpus().length} · yük ${os.loadavg().map((value) => value.toFixed(2)).join(' / ')}`,
|
|
187
|
+
`Bellek: ${gb(total - free)} dolu / ${gb(total)} (%${Math.round((total - free) / total * 100)})`,
|
|
188
|
+
`Açık süre: ${hours >= 24 ? `${Math.floor(hours / 24)} gün ${hours % 24} saat` : `${hours} saat`}`,
|
|
189
|
+
`Diskler:\n${diskLines.map((line) => `- ${line}`).join('\n') || '- (okunamadı)'}`,
|
|
190
|
+
`En çok bellek kullanan süreçler:\n${processLines.map((line) => `- ${line}`).join('\n') || '- (okunamadı)'}`
|
|
191
|
+
].join('\n');
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/** Bağlı veri kaynağı köprüsü: @aide/connectors kuruluysa (repo içinde ya da yanında) yerel ağdaki veritabanlarını sorgular. */
|
|
195
|
+
const loadConnectors = async () => {
|
|
196
|
+
try {
|
|
197
|
+
return await import('@aide/connectors/runtime');
|
|
198
|
+
} catch {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const webSocketClass = async () => {
|
|
204
|
+
if(typeof globalThis.WebSocket === 'function') {
|
|
205
|
+
return globalThis.WebSocket;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
return (await import('ws')).default;
|
|
210
|
+
} catch {
|
|
211
|
+
throw new AideError('Cihaz modu için Node 22+ (yerleşik WebSocket) ya da "ws" paketi gerekli.');
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Cihazı bağlar ve işleri dinler. Onaylar sıralı sorulur (aynı anda iki soru ekrana düşmez).
|
|
217
|
+
* Bayraklar: --workspace <klasör> (varsayılan: bulunulan klasör), --approve-all, --read-only.
|
|
218
|
+
*/
|
|
219
|
+
const connectDevice = async (flags) => {
|
|
220
|
+
const { server, token } = settings(flags);
|
|
221
|
+
|
|
222
|
+
if(!server || !token) {
|
|
223
|
+
throw new AideError('Önce giriş yapın: aide login');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const workspaceDir = path.resolve(String(flags.workspace || process.cwd()));
|
|
227
|
+
const WebSocketImpl = await webSocketClass();
|
|
228
|
+
const connectors = await loadConnectors();
|
|
229
|
+
const tools = ['code_editor', 'terminal', 'system_info', ...(connectors ? ['data_query'] : [])];
|
|
230
|
+
let queue = Promise.resolve();
|
|
231
|
+
let stopped = false;
|
|
232
|
+
|
|
233
|
+
fs.mkdirSync(workspaceDir, { recursive: true });
|
|
234
|
+
|
|
235
|
+
// Onaylar sıraya girer; ekransız ortamda --approve-all yoksa yazma/komut reddedilir
|
|
236
|
+
const approve = (message) => {
|
|
237
|
+
const decision = queue.then(async () => {
|
|
238
|
+
if(flags['read-only']) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if(flags['approve-all']) {
|
|
243
|
+
console.log(`${c.yellow('⚑')} ${message} ${c.dim('(otomatik onay)')}`);
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if(!process.stdin.isTTY) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return /^(e|evet|y|yes)$/i.test(await ask(`${c.yellow('?')} ${message}\n Onaylıyor musunuz? (e/H) `));
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
queue = decision.catch(() => false);
|
|
255
|
+
|
|
256
|
+
return decision;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const handle = async (socket, message) => {
|
|
260
|
+
const summary = message.tool === 'terminal' ? String(message.args?.command || '') : `${message.args?.action || ''} ${message.args?.path || ''}`.trim();
|
|
261
|
+
|
|
262
|
+
console.log(`${c.accent('●')} ${c.bold(message.tool)} ${c.dim(summary.slice(0, 120))}`);
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
const output = message.tool === 'terminal'
|
|
266
|
+
? await terminal(workspaceDir, message.args || {}, approve)
|
|
267
|
+
: message.tool === 'system_info'
|
|
268
|
+
? await systemInfo(`Cihaz (${os.hostname()})`)
|
|
269
|
+
: message.tool === 'code_editor'
|
|
270
|
+
? await codeEditor(workspaceDir, message.args || {}, approve)
|
|
271
|
+
: message.tool === 'data_query' && connectors
|
|
272
|
+
? await runDataQuery(connectors, message.args || {})
|
|
273
|
+
: (() => {
|
|
274
|
+
throw new Error(`Bu cihazda desteklenmeyen araç: ${message.tool}`);
|
|
275
|
+
})();
|
|
276
|
+
|
|
277
|
+
socket.send(JSON.stringify({ type: 'result', id: message.id, ok: true, output }));
|
|
278
|
+
console.log(c.dim(` ✓ tamamlandı (${String(output).length} karakter)`));
|
|
279
|
+
} catch (error) {
|
|
280
|
+
socket.send(JSON.stringify({ type: 'result', id: message.id, ok: false, output: error.message }));
|
|
281
|
+
console.log(c.red(` ✗ ${error.message}`));
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const connect = () => new Promise((resolve) => {
|
|
286
|
+
const url = `${server.replace(/^http/i, 'ws')}/ws/device?token=${encodeURIComponent(token)}`;
|
|
287
|
+
const socket = new WebSocketImpl(url);
|
|
288
|
+
|
|
289
|
+
socket.addEventListener('open', () => {
|
|
290
|
+
socket.send(JSON.stringify({ type: 'hello', deviceName: `${os.hostname()} (aide-cli)`, platform: `${process.platform}-${process.arch}`, workspaceDir, tools }));
|
|
291
|
+
console.log(`${c.green('✓')} Bağlandı: ${server} · klasör: ${workspaceDir} · araçlar: ${tools.join(', ')}${flags['approve-all'] ? c.yellow(' · ONAYSIZ MOD') : ''}`);
|
|
292
|
+
});
|
|
293
|
+
socket.addEventListener('message', (event) => {
|
|
294
|
+
try {
|
|
295
|
+
const message = JSON.parse(String(event.data));
|
|
296
|
+
|
|
297
|
+
if(message.type === 'invoke') {
|
|
298
|
+
void handle(socket, message);
|
|
299
|
+
}
|
|
300
|
+
} catch {
|
|
301
|
+
// geçersiz mesaj
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
socket.addEventListener('close', () => resolve());
|
|
305
|
+
socket.addEventListener('error', () => null);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
process.on('SIGINT', () => {
|
|
309
|
+
stopped = true;
|
|
310
|
+
console.log(c.dim('\nCihaz bağlantısı kapatıldı.'));
|
|
311
|
+
process.exit(0);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
console.log(c.dim('Cihaz modu: personel bu makinede dosya/komut işleri yapabilir. Çıkış: Ctrl+C'));
|
|
315
|
+
|
|
316
|
+
while(!stopped) {
|
|
317
|
+
await connect();
|
|
318
|
+
|
|
319
|
+
if(!stopped) {
|
|
320
|
+
console.log(c.dim('Bağlantı koptu; 5 sn sonra yeniden denenecek…'));
|
|
321
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const runDataQuery = async (runtime, args) => {
|
|
327
|
+
// Cihaz kullanıcının kendi ağındadır: yerel ağdaki REST API'lere erişim burada serbesttir (sunucuda SSRF koruması açık)
|
|
328
|
+
process.env.ALLOW_PRIVATE_DATASOURCE_URLS = 'true';
|
|
329
|
+
const driver = runtime.createDriver(args.config || {}, args.secrets || {});
|
|
330
|
+
|
|
331
|
+
if(args.op === 'test') {
|
|
332
|
+
await driver.test();
|
|
333
|
+
|
|
334
|
+
return JSON.stringify({ ok: true });
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if(args.op === 'discover') {
|
|
338
|
+
return JSON.stringify(await driver.discover());
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return JSON.stringify(await driver.query(runtime.validateIR(args.ir || {})));
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
// ─────────────────────────────── servis olarak kurulum ───────────────────────────────
|
|
345
|
+
|
|
346
|
+
const SERVICE = 'aide-device';
|
|
347
|
+
const MAC_LABEL = 'app.aide.device';
|
|
348
|
+
const WIN_TASK = 'Ai-de Cihaz';
|
|
349
|
+
|
|
350
|
+
const runQuiet = (file, args) => new Promise((resolve) => {
|
|
351
|
+
execFile(file, args, { timeout: 30000, windowsHide: true }, (error, stdout, stderr) => resolve({ ok: !error, output: String(stdout || stderr || error?.message || '').trim() }));
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
/** Servisin çalıştıracağı komut: node + bu CLI + device connect (ekransız: onay sorulamaz → varsayılan salt okuma). */
|
|
355
|
+
const serviceCommand = (flags) => ({
|
|
356
|
+
script: fileURLToPath(new URL('../aide.mjs', import.meta.url)),
|
|
357
|
+
workspace: path.resolve(String(flags.workspace || process.cwd())),
|
|
358
|
+
mode: flags['approve-all'] ? '--approve-all' : '--read-only',
|
|
359
|
+
node: process.execPath
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
/** Platforma göre kurulum adımları: yazılacak dosyalar, çalıştırılacak komutlar, silinecekler. */
|
|
363
|
+
const serviceSteps = (action, flags) => {
|
|
364
|
+
const { node, script, workspace, mode } = serviceCommand(flags);
|
|
365
|
+
const home = os.homedir();
|
|
366
|
+
const steps = [];
|
|
367
|
+
|
|
368
|
+
if(process.platform === 'linux') {
|
|
369
|
+
const unit = path.join(home, '.config', 'systemd', 'user', `${SERVICE}.service`);
|
|
370
|
+
const content = [
|
|
371
|
+
'[Unit]',
|
|
372
|
+
'Description=Ai-de cihaz çalıştırıcısı',
|
|
373
|
+
'After=network-online.target',
|
|
374
|
+
'',
|
|
375
|
+
'[Service]',
|
|
376
|
+
`ExecStart="${node}" "${script}" device connect --workspace "${workspace}" ${mode}`,
|
|
377
|
+
'Restart=always',
|
|
378
|
+
'RestartSec=10',
|
|
379
|
+
'',
|
|
380
|
+
'[Install]',
|
|
381
|
+
'WantedBy=default.target',
|
|
382
|
+
''
|
|
383
|
+
].join('\n');
|
|
384
|
+
|
|
385
|
+
if(action === 'install') {
|
|
386
|
+
steps.push({ write: unit, content }, { run: ['systemctl', ['--user', 'daemon-reload']] }, { run: ['systemctl', ['--user', 'enable', '--now', SERVICE]] });
|
|
387
|
+
} else if(action === 'uninstall') {
|
|
388
|
+
steps.push({ run: ['systemctl', ['--user', 'disable', '--now', SERVICE]], optional: true }, { remove: unit }, { run: ['systemctl', ['--user', 'daemon-reload']] });
|
|
389
|
+
} else {
|
|
390
|
+
steps.push({ run: ['systemctl', ['--user', 'is-active', SERVICE]] });
|
|
391
|
+
}
|
|
392
|
+
} else if(process.platform === 'darwin') {
|
|
393
|
+
const plist = path.join(home, 'Library', 'LaunchAgents', `${MAC_LABEL}.plist`);
|
|
394
|
+
const domain = `gui/${process.getuid?.() ?? 501}`;
|
|
395
|
+
const log = path.join(home, 'Library', 'Logs', 'aide-device.log');
|
|
396
|
+
const args = [node, script, 'device', 'connect', '--workspace', workspace, mode].map((item) => ` <string>${item}</string>`);
|
|
397
|
+
const content = [
|
|
398
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
399
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
400
|
+
'<plist version="1.0">',
|
|
401
|
+
'<dict>',
|
|
402
|
+
` <key>Label</key><string>${MAC_LABEL}</string>`,
|
|
403
|
+
' <key>ProgramArguments</key>',
|
|
404
|
+
' <array>',
|
|
405
|
+
...args,
|
|
406
|
+
' </array>',
|
|
407
|
+
' <key>RunAtLoad</key><true/>',
|
|
408
|
+
' <key>KeepAlive</key><true/>',
|
|
409
|
+
` <key>StandardOutPath</key><string>${log}</string>`,
|
|
410
|
+
` <key>StandardErrorPath</key><string>${log}</string>`,
|
|
411
|
+
'</dict>',
|
|
412
|
+
'</plist>',
|
|
413
|
+
''
|
|
414
|
+
].join('\n');
|
|
415
|
+
|
|
416
|
+
if(action === 'install') {
|
|
417
|
+
steps.push({ write: plist, content }, { run: ['launchctl', ['bootout', `${domain}/${MAC_LABEL}`]], optional: true }, { run: ['launchctl', ['bootstrap', domain, plist]] });
|
|
418
|
+
} else if(action === 'uninstall') {
|
|
419
|
+
steps.push({ run: ['launchctl', ['bootout', `${domain}/${MAC_LABEL}`]], optional: true }, { remove: plist });
|
|
420
|
+
} else {
|
|
421
|
+
steps.push({ run: ['launchctl', ['print', `${domain}/${MAC_LABEL}`]] });
|
|
422
|
+
}
|
|
423
|
+
} else if(process.platform === 'win32') {
|
|
424
|
+
// Gizli pencereli başlatıcı (wscript): oturum açılışında konsol penceresi açılmaz
|
|
425
|
+
const launcher = path.join(home, '.aide', 'aide-device.vbs');
|
|
426
|
+
const quoted = (value) => `""${value}""`;
|
|
427
|
+
const content = `CreateObject("WScript.Shell").Run "${quoted(node)} ${quoted(script)} device connect --workspace ${quoted(workspace)} ${mode}", 0, False\r\n`;
|
|
428
|
+
|
|
429
|
+
if(action === 'install') {
|
|
430
|
+
steps.push({ write: launcher, content }, { run: ['schtasks', ['/Create', '/TN', WIN_TASK, '/TR', `wscript.exe "${launcher}"`, '/SC', 'ONLOGON', '/RL', 'LIMITED', '/F']] }, { run: ['schtasks', ['/Run', '/TN', WIN_TASK]] });
|
|
431
|
+
} else if(action === 'uninstall') {
|
|
432
|
+
steps.push({ run: ['schtasks', ['/End', '/TN', WIN_TASK]], optional: true }, { run: ['schtasks', ['/Delete', '/TN', WIN_TASK, '/F']] }, { remove: launcher });
|
|
433
|
+
} else {
|
|
434
|
+
steps.push({ run: ['schtasks', ['/Query', '/TN', WIN_TASK]] });
|
|
435
|
+
}
|
|
436
|
+
} else {
|
|
437
|
+
throw new AideError(`Bu işletim sistemi için servis kurulumu yok (${process.platform}); aide device connect ile elle çalıştırın.`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return { steps, mode, workspace };
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* `aide device install|uninstall|status`: açılışta kendiliğinden kalkan servis (Linux systemd kullanıcı servisi, macOS
|
|
445
|
+
* LaunchAgent, Windows oturum açılışı görevi). `--dry` yalnızca ne yapacağını gösterir.
|
|
446
|
+
*/
|
|
447
|
+
const manageService = async (action, flags) => {
|
|
448
|
+
const { steps, mode, workspace } = serviceSteps(action, flags);
|
|
449
|
+
|
|
450
|
+
if(action === 'install' && flags['approve-all']) {
|
|
451
|
+
console.log(c.yellow('⚑ ONAYSIZ MOD: personelin bu makinedeki dosya ve komut işleri onay sorulmadan yürür. Yalnızca güvendiğiniz sunucular için.'));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
for(const step of steps) {
|
|
455
|
+
if(flags.dry) {
|
|
456
|
+
console.log(c.dim(step.write ? `yazılacak: ${step.write}\n${step.content.trim().split('\n').map((line) => ` ${line}`).join('\n')}` : step.remove ? `silinecek: ${step.remove}` : `çalıştırılacak: ${step.run[0]} ${step.run[1].join(' ')}`));
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if(step.write) {
|
|
461
|
+
fs.mkdirSync(path.dirname(step.write), { recursive: true });
|
|
462
|
+
// wscript UTF-8 okumaz: Türkçe karakterli yollar (Masaüstü) için .vbs UTF-16LE + BOM yazılır
|
|
463
|
+
fs.writeFileSync(step.write, step.write.endsWith('.vbs') ? Buffer.from(`\ufeff${step.content}`, 'utf16le') : step.content, { mode: 0o600 });
|
|
464
|
+
} else if(step.remove) {
|
|
465
|
+
fs.rmSync(step.remove, { force: true });
|
|
466
|
+
} else {
|
|
467
|
+
const result = await runQuiet(step.run[0], step.run[1]);
|
|
468
|
+
|
|
469
|
+
if(action === 'status') {
|
|
470
|
+
console.log(result.ok ? result.output.split('\n').slice(0, 8).join('\n') : c.dim('Servis kurulu değil ya da çalışmıyor.'));
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if(!result.ok && !step.optional) {
|
|
475
|
+
throw new AideError(`${step.run[0]} başarısız: ${result.output.slice(0, 200)}`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if(!flags.dry && action !== 'status') {
|
|
481
|
+
console.log(c.green(action === 'install' ? `✓ Servis kuruldu: açılışta kendiliğinden bağlanır (${mode === '--read-only' ? 'salt okuma' : 'onaysız'}, klasör: ${workspace}).` : '✓ Servis kaldırıldı.'));
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
export {
|
|
486
|
+
manageService,
|
|
487
|
+
connectDevice
|
|
488
|
+
};
|