beast-agent 2.40.0 → 2.42.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/assets/app.ico +0 -0
- package/package.json +1 -1
- package/src/agent/engine.js +50 -0
- package/src/agent/opencode/agents.js +4 -3
- package/src/agent/tools.js +16 -0
- package/src/main.js +482 -38
- package/src/preload.js +7 -1
- package/src/renderer/index.html +11 -4
- package/src/renderer/renderer.js +270 -40
- package/src/renderer/style.css +74 -3
package/src/main.js
CHANGED
|
@@ -272,8 +272,15 @@ try {
|
|
|
272
272
|
try { nodemailer = require('nodemailer'); } catch {}
|
|
273
273
|
|
|
274
274
|
const APP_DIR = path.join(app.getPath('appData'), 'beast');
|
|
275
|
-
/* Windows toast bildirimleri için AppUserModelID erken
|
|
276
|
-
|
|
275
|
+
/* Windows toast bildirimleri için AppUserModelID erken ayarlanmalı. ANCAK özel
|
|
276
|
+
AUMID, Windows'ta kayıtlı kısayol yoksa taskbar ikonunu electron.exe logosuna
|
|
277
|
+
düşürür. Bu yüzden yalnız paketli kurulumda ya da npm modunda masaüstü
|
|
278
|
+
kısayolu (aynı AUMID ile oluşturulur) mevcutsa atanır. */
|
|
279
|
+
try {
|
|
280
|
+
let aumidOk = app.isPackaged;
|
|
281
|
+
if (!aumidOk) aumidOk = fs.existsSync(path.join(app.getPath('desktop'), 'Beast Agent.lnk'));
|
|
282
|
+
if (aumidOk) app.setAppUserModelId('com.quantumalgo.beastagent');
|
|
283
|
+
} catch {}
|
|
277
284
|
/* mem0-native embedding modeli whisper ile AYNI cache'i kullanır (src/agent/mem0.js okur) */
|
|
278
285
|
process.env.BEAST_MODELS_DIR = path.join(APP_DIR, 'models');
|
|
279
286
|
|
|
@@ -3474,6 +3481,15 @@ function reloadBackend() {
|
|
|
3474
3481
|
/* PANEL RUN köprüsü: ajanın panel_run aracı → ÇALIŞTIR panelindeki yönetilen
|
|
3475
3482
|
süreç koşucusu (sandbox:run IPC ile aynı makine). */
|
|
3476
3483
|
engine.sbRunHook = sbRunStartManaged;
|
|
3484
|
+
/* SANDBOX KÖPRÜSÜ: ana sohbetteki ajanın sandbox_repo aracı → panel ile aynı
|
|
3485
|
+
makine (indir/kur/başlat/durdur); kullanıcı chatte repo linki bırakınca
|
|
3486
|
+
ajan kendisi indirip kurabilir. */
|
|
3487
|
+
engine.sbCloneHook = sbCloneRepo;
|
|
3488
|
+
engine.sbFolderHook = sbResolveFolder;
|
|
3489
|
+
engine.sbDetectHook = sbProjectInfoMerged;
|
|
3490
|
+
engine.sbInstallHook = sbInstallRepo;
|
|
3491
|
+
engine.sbStartHook = sbStartRepo;
|
|
3492
|
+
engine.sbStopHook = sbRunStopFolder;
|
|
3477
3493
|
|
|
3478
3494
|
/* SQUEEZE (opsiyonel token sıkıştırma): varsayılan KAPALI; ayar açıksa
|
|
3479
3495
|
her LLM isteğinin kopyası gönderimden önce sıkıştırılır. */
|
|
@@ -3594,6 +3610,7 @@ function ensureDesktopShortcut() {
|
|
|
3594
3610
|
description: 'Beast Agent — hızlı, hafif ve becerikli',
|
|
3595
3611
|
icon: path.join(__dirname, '..', 'assets', 'app.ico'),
|
|
3596
3612
|
iconIndex: 0,
|
|
3613
|
+
appUserModelId: 'com.quantumalgo.beastagent',
|
|
3597
3614
|
});
|
|
3598
3615
|
log.info('main', ok ? 'Masaüstü kısayolu oluşturuldu (npm modu)' : 'Masaüstü kısayolu oluşturulamadı');
|
|
3599
3616
|
} catch (e) {
|
|
@@ -10814,6 +10831,7 @@ ipcMain.handle('studio:delete', async (_e, rel) => {
|
|
|
10814
10831
|
run_command/read/write araçları repo klasöründe çalışır). Ana sohbet VE
|
|
10815
10832
|
Code/Studio panellerinden tamamen ayrıdır; ana chat solda yerinde kalır. */
|
|
10816
10833
|
const sbSessions = new Map(); /* klasör → sessionId */
|
|
10834
|
+
let sbLastFolder = ''; /* panelde seçilen / son indirilen repo — ajan araçları boş repo alanında bunu kullanır */
|
|
10817
10835
|
const SB_DEBOUNCE_MS = 900;
|
|
10818
10836
|
const sbQueue = new Map(); /* klasör → { timer, msgs[] } */
|
|
10819
10837
|
|
|
@@ -10935,6 +10953,29 @@ function sbRepoFromInput(input) {
|
|
|
10935
10953
|
return null;
|
|
10936
10954
|
}
|
|
10937
10955
|
|
|
10956
|
+
/* repo adres/klasör çözümleyici: hem panel hem ajan araçları (sandbox_repo)
|
|
10957
|
+
kullanır — tam yol, klasör adı, owner/repo ya da boş ("son repo") kabul eder */
|
|
10958
|
+
function sbResolveFolder(input) {
|
|
10959
|
+
const root = sandboxRoot();
|
|
10960
|
+
const s = String(input || '').trim();
|
|
10961
|
+
if (!s) return sbLastFolder;
|
|
10962
|
+
try {
|
|
10963
|
+
if (fs.existsSync(s) && fs.statSync(s).isDirectory()) return path.resolve(s);
|
|
10964
|
+
const direct = path.join(root, s);
|
|
10965
|
+
if (fs.existsSync(direct) && fs.statSync(direct).isDirectory()) return direct;
|
|
10966
|
+
const name = s.toLowerCase().replace(/[\\/]+$/, '').split(/[\\/]/).pop().replace(/\.git$/i, '');
|
|
10967
|
+
let partial = '';
|
|
10968
|
+
for (const e of fs.readdirSync(root, { withFileTypes: true })) {
|
|
10969
|
+
if (!e.isDirectory()) continue;
|
|
10970
|
+
const n = e.name.toLowerCase();
|
|
10971
|
+
if (n === name) return path.join(root, e.name);
|
|
10972
|
+
if (!partial && n.startsWith(name + '-')) partial = path.join(root, e.name);
|
|
10973
|
+
}
|
|
10974
|
+
if (partial) return partial;
|
|
10975
|
+
} catch {}
|
|
10976
|
+
return sbLastFolder;
|
|
10977
|
+
}
|
|
10978
|
+
|
|
10938
10979
|
ipcMain.handle('sandbox:list', async () => {
|
|
10939
10980
|
const root = sandboxRoot();
|
|
10940
10981
|
const items = [];
|
|
@@ -10950,15 +10991,18 @@ ipcMain.handle('sandbox:list', async () => {
|
|
|
10950
10991
|
sid = String(known);
|
|
10951
10992
|
try { busy = engine.isBusy(known); } catch {}
|
|
10952
10993
|
}
|
|
10953
|
-
|
|
10994
|
+
const slot = sbProcs.get(folder);
|
|
10995
|
+
items.push({ name: e.name, folder, sid, busy, running: !!(slot && slot.proc), kind: slot ? slot.kind : '' });
|
|
10954
10996
|
}
|
|
10955
10997
|
} catch {}
|
|
10956
10998
|
return { ok: true, root, items };
|
|
10957
10999
|
});
|
|
10958
11000
|
|
|
10959
|
-
|
|
11001
|
+
/* klonlama çekirdeği: hem Sandbox paneli IPC'si hem de ajanın sandbox_repo
|
|
11002
|
+
aracı buradan geçer (engine.sbCloneHook) */
|
|
11003
|
+
function sbCloneRepo(input) {
|
|
10960
11004
|
const repo = sbRepoFromInput(input);
|
|
10961
|
-
if (!repo) return { ok: false, error: 'github.com/owner/repo, owner/repo ya da tam .git adresi gir' };
|
|
11005
|
+
if (!repo) return Promise.resolve({ ok: false, error: 'github.com/owner/repo, owner/repo ya da tam .git adresi gir' });
|
|
10962
11006
|
const root = sandboxRoot();
|
|
10963
11007
|
const base = String(repo.name || 'repo').replace(/[\\/:*?"<>|]/g, '_').slice(0, 60) || 'repo';
|
|
10964
11008
|
let dest = path.join(root, base);
|
|
@@ -11019,6 +11063,7 @@ ipcMain.handle('sandbox:clone', async (_e, input) => {
|
|
|
11019
11063
|
return;
|
|
11020
11064
|
}
|
|
11021
11065
|
log.info('main', 'sandbox: klonlandı ' + repo.url + ' → ' + finalName);
|
|
11066
|
+
sbLastFolder = dest;
|
|
11022
11067
|
resolve({ ok: true, name: finalName, folder: dest });
|
|
11023
11068
|
};
|
|
11024
11069
|
proc.on('error', done);
|
|
@@ -11031,7 +11076,9 @@ ipcMain.handle('sandbox:clone', async (_e, input) => {
|
|
|
11031
11076
|
}
|
|
11032
11077
|
}, 600000);
|
|
11033
11078
|
});
|
|
11034
|
-
}
|
|
11079
|
+
}
|
|
11080
|
+
|
|
11081
|
+
ipcMain.handle('sandbox:clone', async (_e, input) => sbCloneRepo(input));
|
|
11035
11082
|
|
|
11036
11083
|
ipcMain.handle('sandbox:send', async (_e, payload) => {
|
|
11037
11084
|
const text = String((payload && payload.msg) || '').trim();
|
|
@@ -11097,6 +11144,8 @@ ipcMain.handle('sandbox:remove', async (_e, payload) => {
|
|
|
11097
11144
|
sbLoadMap();
|
|
11098
11145
|
const sid = sbSessions.get(folder);
|
|
11099
11146
|
if (sid && engine.isBusy(sid)) return { ok: false, error: 'repo çalışıyor — önce ■ ile durdur' };
|
|
11147
|
+
const slot = sbProcs.get(folder);
|
|
11148
|
+
if (slot && slot.proc) return { ok: false, error: 'repoda süreç çalışıyor — önce ■ Durdur' };
|
|
11100
11149
|
try { fs.rmSync(folder, { recursive: true, force: true }); } catch (e) {
|
|
11101
11150
|
return { ok: false, error: String((e && e.message) || e) };
|
|
11102
11151
|
}
|
|
@@ -11105,6 +11154,13 @@ ipcMain.handle('sandbox:remove', async (_e, payload) => {
|
|
|
11105
11154
|
sbSessions.delete(folder);
|
|
11106
11155
|
sbSaveMap();
|
|
11107
11156
|
}
|
|
11157
|
+
sbProcs.delete(folder);
|
|
11158
|
+
sbLoadCfg();
|
|
11159
|
+
if (sbCfg[folder]) {
|
|
11160
|
+
delete sbCfg[folder];
|
|
11161
|
+
sbSaveCfg();
|
|
11162
|
+
}
|
|
11163
|
+
try { fs.rmSync(path.join(beastDir(), 'sandbox-installed', path.basename(folder)), { force: true }); } catch {}
|
|
11108
11164
|
return { ok: true };
|
|
11109
11165
|
});
|
|
11110
11166
|
|
|
@@ -11121,6 +11177,7 @@ ipcMain.handle('sandbox:open', async (_e, payload) => {
|
|
|
11121
11177
|
const folder = String((payload && payload.folder) || '');
|
|
11122
11178
|
if (!folder.startsWith(sandboxRoot() + path.sep)) return { ok: false, error: 'geçersiz klasör' };
|
|
11123
11179
|
if (!engine) return { ok: false, error: 'ajan hazır değil' };
|
|
11180
|
+
sbLastFolder = folder;
|
|
11124
11181
|
const s = sandboxGetSession(folder);
|
|
11125
11182
|
s.workspace = folder;
|
|
11126
11183
|
engine.cache.set(s.id, s);
|
|
@@ -11162,25 +11219,188 @@ ipcMain.handle('sandbox:tree', (_e, payload) => {
|
|
|
11162
11219
|
}
|
|
11163
11220
|
});
|
|
11164
11221
|
|
|
11165
|
-
/* Sandbox ÇALIŞTIRICI: repo
|
|
11166
|
-
|
|
11167
|
-
|
|
11168
|
-
|
|
11169
|
-
|
|
11170
|
-
|
|
11222
|
+
/* Sandbox ÇALIŞTIRICI v2: repo başına YÖNETİLEN SÜREÇLER.
|
|
11223
|
+
Başlat/Dev/Durdur butonları artık deterministik: proje tipi algılanır
|
|
11224
|
+
(package.json, requirements.txt, pyproject.toml, Cargo.toml, go.mod,
|
|
11225
|
+
index.html) ve doğru komut DOĞRUDAN çalıştırılır. Her repo kendi süreç
|
|
11226
|
+
yuvasında koşar — birden çok repo aynı anda çalışabilir; çıktı tamponlanır,
|
|
11227
|
+
repo değiştirilince panel geçmişi yeniden basılır. Dev server adresi
|
|
11228
|
+
çıktıdan yakalanır → sağdaki dahili tarayıcıda açılır. Aynı fonksiyon
|
|
11229
|
+
engine.sbRunHook'tur: ajanın panel_run aracı da buradan geçer. */
|
|
11230
|
+
|
|
11231
|
+
const sbProcs = new Map(); /* klasör → { proc, kind, cmd, url, lines[], startedAt } */
|
|
11232
|
+
const SB_LOG_CAP = 500; /* repo başına tamponlanan çıktı satırı */
|
|
11233
|
+
const SB_CFG_FILE = 'sandbox-config.json';
|
|
11234
|
+
|
|
11235
|
+
function sbCfgPath() {
|
|
11236
|
+
return path.join(beastDir(), SB_CFG_FILE);
|
|
11237
|
+
}
|
|
11238
|
+
|
|
11239
|
+
let sbCfgLoaded = false;
|
|
11240
|
+
let sbCfg = {}; /* klasör → { install, run, dev } — kullanıcının kaydettiği özel komutlar */
|
|
11241
|
+
|
|
11242
|
+
function sbLoadCfg() {
|
|
11243
|
+
if (sbCfgLoaded) return sbCfg;
|
|
11244
|
+
sbCfgLoaded = true;
|
|
11245
|
+
try {
|
|
11246
|
+
const raw = JSON.parse(fs.readFileSync(sbCfgPath(), 'utf8'));
|
|
11247
|
+
if (raw && typeof raw === 'object') sbCfg = raw;
|
|
11248
|
+
} catch {}
|
|
11249
|
+
return sbCfg;
|
|
11250
|
+
}
|
|
11251
|
+
|
|
11252
|
+
function sbSaveCfg() {
|
|
11253
|
+
try { fs.writeFileSync(sbCfgPath(), JSON.stringify(sbCfg, null, 2)); } catch {}
|
|
11254
|
+
}
|
|
11255
|
+
|
|
11256
|
+
/* embedded python varsa onu kullan — PATH'te python olmasa bile çalışsın */
|
|
11257
|
+
function sbPythonCmd() {
|
|
11258
|
+
const cands = [process.env.BEAST_PYTHON, path.join(beastDir(), 'py', 'python.exe')];
|
|
11259
|
+
for (const c of cands) {
|
|
11260
|
+
try { if (c && fs.existsSync(c)) return '"' + c + '"'; } catch {}
|
|
11261
|
+
}
|
|
11262
|
+
return 'python';
|
|
11263
|
+
}
|
|
11264
|
+
|
|
11265
|
+
/* statik site üreticileri: hugo / jekyll / mkdocs — doküman repoları sık böyle;
|
|
11266
|
+
Başlat/Dev butonları bunları da tanısın (ör. docs/ altında hugo.yaml) */
|
|
11267
|
+
function sbSiteGenInfo(folder) {
|
|
11268
|
+
for (const d of ['', 'docs', 'site', 'website']) {
|
|
11269
|
+
const base = d ? path.join(folder, d) : folder;
|
|
11270
|
+
const has = (f) => { try { return fs.existsSync(path.join(base, f)); } catch { return false; } };
|
|
11271
|
+
if (has('hugo.toml') || has('hugo.yaml') || has('hugo.json') || (has('config.toml') && (has('content') || has('themes')))) {
|
|
11272
|
+
return { kind: 'static', label: 'hugo', install: '', run: 'hugo server' + (d ? ' -s ' + d : '') };
|
|
11273
|
+
}
|
|
11274
|
+
if (has('_config.yml') && (has('Gemfile') || has('Gemfile.lock'))) {
|
|
11275
|
+
return { kind: 'ruby', label: 'jekyll', install: 'bundle install', run: 'bundle exec jekyll serve' + (d ? ' -s ' + d : '') };
|
|
11276
|
+
}
|
|
11277
|
+
if (has('mkdocs.yml') || has('mkdocs.yaml')) {
|
|
11278
|
+
return { kind: 'python', label: 'mkdocs', install: sbPythonCmd() + ' -m pip install mkdocs', run: sbPythonCmd() + ' -m mkdocs serve' };
|
|
11279
|
+
}
|
|
11280
|
+
}
|
|
11281
|
+
return null;
|
|
11282
|
+
}
|
|
11283
|
+
|
|
11284
|
+
/* repo köküne bakıp proje tipini + kur/çalıştır komutlarını çıkarır */
|
|
11285
|
+
function sbProjectInfo(folder) {
|
|
11286
|
+
const out = { kind: 'unknown', label: 'bilinmiyor', name: path.basename(folder), install: '', run: '', dev: '', build: '' };
|
|
11287
|
+
try {
|
|
11288
|
+
const pkgPath = path.join(folder, 'package.json');
|
|
11289
|
+
if (fs.existsSync(pkgPath)) {
|
|
11290
|
+
let pkg = {};
|
|
11291
|
+
try { pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); } catch {}
|
|
11292
|
+
out.name = String(pkg.name || out.name);
|
|
11293
|
+
let pm = 'npm';
|
|
11294
|
+
if (fs.existsSync(path.join(folder, 'pnpm-lock.yaml'))) pm = 'pnpm';
|
|
11295
|
+
else if (fs.existsSync(path.join(folder, 'yarn.lock'))) pm = 'yarn';
|
|
11296
|
+
else if (fs.existsSync(path.join(folder, 'bun.lockb')) || fs.existsSync(path.join(folder, 'bun.lock'))) pm = 'bun';
|
|
11297
|
+
const s = pkg.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {};
|
|
11298
|
+
out.kind = 'node';
|
|
11299
|
+
out.label = pm;
|
|
11300
|
+
out.install = pm + ' install';
|
|
11301
|
+
out.dev = s.dev ? pm + ' run dev' : '';
|
|
11302
|
+
out.run = s.start ? pm + ' run start' : s.dev ? pm + ' run dev' : s.serve ? pm + ' run serve' : '';
|
|
11303
|
+
out.build = s.build ? pm + ' run build' : '';
|
|
11304
|
+
return out;
|
|
11305
|
+
}
|
|
11306
|
+
/* statik site üreticisi (hugo/jekyll/mkdocs) python paketinden ÖNCE gelir:
|
|
11307
|
+
docs/ altındaki hugo.yaml, kökteki requirements.txt'i gölgeleyebilsin */
|
|
11308
|
+
const site = sbSiteGenInfo(folder);
|
|
11309
|
+
if (site) {
|
|
11310
|
+
out.kind = site.kind;
|
|
11311
|
+
out.label = site.label;
|
|
11312
|
+
if (site.install) out.install = site.install;
|
|
11313
|
+
if (site.run) { out.run = site.run; out.dev = site.run; }
|
|
11314
|
+
if (!out.install) {
|
|
11315
|
+
const py = sbPythonCmd();
|
|
11316
|
+
if (fs.existsSync(path.join(folder, 'requirements.txt'))) out.install = py + ' -m pip install -r requirements.txt';
|
|
11317
|
+
else if (fs.existsSync(path.join(folder, 'pyproject.toml'))) out.install = py + ' -m pip install -e .';
|
|
11318
|
+
}
|
|
11319
|
+
return out;
|
|
11320
|
+
}
|
|
11321
|
+
const hasPy = ['pyproject.toml', 'requirements.txt', 'setup.py'].some((f) => fs.existsSync(path.join(folder, f)));
|
|
11322
|
+
if (hasPy) {
|
|
11323
|
+
const py = sbPythonCmd();
|
|
11324
|
+
out.kind = 'python';
|
|
11325
|
+
out.label = 'python';
|
|
11326
|
+
if (fs.existsSync(path.join(folder, 'uv.lock'))) out.install = 'uv sync';
|
|
11327
|
+
else if (fs.existsSync(path.join(folder, 'requirements.txt'))) out.install = py + ' -m pip install -r requirements.txt';
|
|
11328
|
+
else out.install = py + ' -m pip install -e .';
|
|
11329
|
+
if (fs.existsSync(path.join(folder, 'manage.py'))) out.run = py + ' manage.py runserver';
|
|
11330
|
+
else if (fs.existsSync(path.join(folder, 'main.py'))) out.run = py + ' main.py';
|
|
11331
|
+
else if (fs.existsSync(path.join(folder, 'app.py'))) out.run = py + ' app.py';
|
|
11332
|
+
else if (fs.existsSync(path.join(folder, 'run.py'))) out.run = py + ' run.py';
|
|
11333
|
+
else if (fs.existsSync(path.join(folder, 'bot.py'))) out.run = py + ' bot.py';
|
|
11334
|
+
out.dev = out.run;
|
|
11335
|
+
return out;
|
|
11336
|
+
}
|
|
11337
|
+
if (fs.existsSync(path.join(folder, 'Cargo.toml'))) {
|
|
11338
|
+
out.kind = 'rust';
|
|
11339
|
+
out.label = 'cargo';
|
|
11340
|
+
out.install = 'cargo fetch';
|
|
11341
|
+
out.run = 'cargo run';
|
|
11342
|
+
out.dev = 'cargo run';
|
|
11343
|
+
return out;
|
|
11344
|
+
}
|
|
11345
|
+
if (fs.existsSync(path.join(folder, 'go.mod'))) {
|
|
11346
|
+
out.kind = 'go';
|
|
11347
|
+
out.label = 'go';
|
|
11348
|
+
out.install = 'go mod download';
|
|
11349
|
+
out.run = 'go run .';
|
|
11350
|
+
out.dev = 'go run .';
|
|
11351
|
+
return out;
|
|
11352
|
+
}
|
|
11353
|
+
if (fs.existsSync(path.join(folder, 'index.html'))) {
|
|
11354
|
+
out.kind = 'static';
|
|
11355
|
+
out.label = 'statik';
|
|
11356
|
+
out.run = sbPythonCmd() + ' -m http.server 8123 --bind 127.0.0.1';
|
|
11357
|
+
out.dev = out.run;
|
|
11358
|
+
return out;
|
|
11359
|
+
}
|
|
11360
|
+
} catch {}
|
|
11361
|
+
return out;
|
|
11362
|
+
}
|
|
11363
|
+
|
|
11364
|
+
/* algılanan komutlar + kullanıcının kaydettiği özel komutlar birleşir */
|
|
11365
|
+
function sbProjectInfoMerged(folder) {
|
|
11366
|
+
const info = sbProjectInfo(folder);
|
|
11367
|
+
const c = sbLoadCfg()[folder];
|
|
11368
|
+
if (c && typeof c === 'object') {
|
|
11369
|
+
if (c.install) info.install = String(c.install);
|
|
11370
|
+
if (c.run) info.run = String(c.run);
|
|
11371
|
+
if (c.dev) info.dev = String(c.dev);
|
|
11372
|
+
if (c.install || c.run || c.dev) info.custom = true;
|
|
11373
|
+
}
|
|
11374
|
+
return info;
|
|
11375
|
+
}
|
|
11171
11376
|
|
|
11172
|
-
function sbRunEmit(data) {
|
|
11377
|
+
function sbRunEmit(folder, data, kind) {
|
|
11173
11378
|
try {
|
|
11174
|
-
if (win && !win.isDestroyed())
|
|
11379
|
+
if (win && !win.isDestroyed()) {
|
|
11380
|
+
win.webContents.send('agent:event', {
|
|
11381
|
+
type: 'sb-run',
|
|
11382
|
+
folder: String(folder || ''),
|
|
11383
|
+
kind: kind || 'run',
|
|
11384
|
+
data: String(data || ''),
|
|
11385
|
+
});
|
|
11386
|
+
}
|
|
11175
11387
|
} catch {}
|
|
11176
11388
|
}
|
|
11177
11389
|
|
|
11178
|
-
function
|
|
11390
|
+
function sbSlotPush(slot, folder, text) {
|
|
11391
|
+
slot.lines.push(String(text || ''));
|
|
11392
|
+
if (slot.lines.length > SB_LOG_CAP) slot.lines.splice(0, slot.lines.length - SB_LOG_CAP);
|
|
11393
|
+
sbRunEmit(folder, text, slot.kind);
|
|
11394
|
+
}
|
|
11395
|
+
|
|
11396
|
+
function sbRunStartManaged(folder, cmd, kind, opts) {
|
|
11179
11397
|
folder = String(folder || '');
|
|
11180
11398
|
cmd = String(cmd || '').trim();
|
|
11399
|
+
kind = kind === 'install' ? 'install' : 'run';
|
|
11181
11400
|
if (!folder.startsWith(sandboxRoot() + path.sep)) return { ok: false, error: 'geçersiz klasör' };
|
|
11182
11401
|
if (!cmd) return { ok: false, error: 'komut boş' };
|
|
11183
|
-
|
|
11402
|
+
const cur = sbProcs.get(folder);
|
|
11403
|
+
if (cur && cur.proc) return { ok: false, error: 'Bu repoda zaten bir süreç çalışıyor — önce ■ Durdur' };
|
|
11184
11404
|
const { spawn } = require('child_process');
|
|
11185
11405
|
try {
|
|
11186
11406
|
const proc = spawn('cmd.exe', ['/d', '/s', '/c', cmd], {
|
|
@@ -11188,19 +11408,19 @@ function sbRunStartManaged(folder, cmd) {
|
|
|
11188
11408
|
windowsHide: true,
|
|
11189
11409
|
env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1' },
|
|
11190
11410
|
});
|
|
11191
|
-
|
|
11192
|
-
|
|
11193
|
-
sbRunProc.url = '';
|
|
11411
|
+
const slot = { proc, kind, cmd, url: '', lines: [], startedAt: Date.now(), installCmd: (opts && opts.installCmd) || '' };
|
|
11412
|
+
sbProcs.set(folder, slot);
|
|
11194
11413
|
const onLine = (buf) => {
|
|
11195
11414
|
for (const piece of String(buf).split(/\r?\n/)) {
|
|
11196
11415
|
if (!piece.trim()) continue;
|
|
11197
|
-
|
|
11416
|
+
sbSlotPush(slot, folder, piece);
|
|
11417
|
+
if (kind !== 'run') continue;
|
|
11198
11418
|
/* dev server adresi yakala (sadece localhost) */
|
|
11199
11419
|
const m = /https?:\/\/(?:localhost|127\.0\.0\.1)(?::\d{2,5})?[^\s'"<>]*/i.exec(piece);
|
|
11200
|
-
if (m && !
|
|
11201
|
-
|
|
11420
|
+
if (m && !slot.url) {
|
|
11421
|
+
slot.url = m[0].replace(/[)\].,;:'"]+$/, '');
|
|
11202
11422
|
try {
|
|
11203
|
-
if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'sb-run-url', url:
|
|
11423
|
+
if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'sb-run-url', folder, url: slot.url });
|
|
11204
11424
|
} catch {}
|
|
11205
11425
|
}
|
|
11206
11426
|
}
|
|
@@ -11208,40 +11428,172 @@ function sbRunStartManaged(folder, cmd) {
|
|
|
11208
11428
|
proc.stdout.on('data', onLine);
|
|
11209
11429
|
proc.stderr.on('data', onLine);
|
|
11210
11430
|
proc.on('close', (code) => {
|
|
11211
|
-
|
|
11212
|
-
|
|
11431
|
+
sbSlotPush(slot, folder, '[süreç bitti' + (code != null ? ' — kod ' + code : '') + ']');
|
|
11432
|
+
slot.proc = null;
|
|
11433
|
+
/* başarılı kurulum damgalanır: sonraki Başlat/Dev kurulumu tekrarlamaz */
|
|
11434
|
+
if (code === 0 && (slot.kind === 'install' || slot.installCmd)) sbMarkInstalled(folder);
|
|
11435
|
+
/* hızlı çöküş (10 sn içinde sıfırdan farklı çıkış): renderer bunu ajan
|
|
11436
|
+
devralması için işaret sayar — yanlış algılanan komut ajanla düzeltilir */
|
|
11437
|
+
const quickFail = code != null && Number(code) !== 0 && Date.now() - slot.startedAt < 10000;
|
|
11213
11438
|
try {
|
|
11214
|
-
if (win && !win.isDestroyed())
|
|
11439
|
+
if (win && !win.isDestroyed()) {
|
|
11440
|
+
win.webContents.send('agent:event', {
|
|
11441
|
+
type: 'sb-run-end',
|
|
11442
|
+
folder,
|
|
11443
|
+
code: code == null ? null : Number(code),
|
|
11444
|
+
kind: slot.kind,
|
|
11445
|
+
quickFail,
|
|
11446
|
+
});
|
|
11447
|
+
}
|
|
11215
11448
|
} catch {}
|
|
11216
11449
|
});
|
|
11217
|
-
|
|
11450
|
+
sbSlotPush(slot, folder, '▶ ' + cmd + ' (klasör: ' + path.basename(folder) + ')');
|
|
11218
11451
|
return {
|
|
11219
11452
|
ok: true,
|
|
11453
|
+
cmd,
|
|
11454
|
+
kind,
|
|
11220
11455
|
note: 'süreç ÇALIŞTIR panelinde başlatıldı — çıktı orada canlı akar; durdurmak için paneldeki ■. Kullanıcıya adres varsa bildir.',
|
|
11221
11456
|
};
|
|
11222
11457
|
} catch (e) {
|
|
11223
|
-
sbRunProc.proc = null;
|
|
11224
11458
|
return { ok: false, error: String((e && e.message) || e) };
|
|
11225
11459
|
}
|
|
11226
11460
|
}
|
|
11227
11461
|
|
|
11462
|
+
function sbRunStopFolder(folder) {
|
|
11463
|
+
const slot = sbProcs.get(String(folder || ''));
|
|
11464
|
+
if (!slot || !slot.proc) return false;
|
|
11465
|
+
const p = slot.proc;
|
|
11466
|
+
slot.proc = null;
|
|
11467
|
+
try {
|
|
11468
|
+
/* cmd.exe /c zinciriyle çocuklar (node/vite) geride kalmasın — ağaç kesimi */
|
|
11469
|
+
if (process.platform === 'win32') {
|
|
11470
|
+
const { exec } = require('child_process');
|
|
11471
|
+
exec('taskkill /pid ' + p.pid + '/T /F', { windowsHide: true });
|
|
11472
|
+
} else p.kill('SIGTERM');
|
|
11473
|
+
} catch {}
|
|
11474
|
+
return true;
|
|
11475
|
+
}
|
|
11476
|
+
|
|
11228
11477
|
ipcMain.handle('sandbox:run', async (_e, payload) => {
|
|
11229
11478
|
const folder = String((payload && payload.folder) || '');
|
|
11230
11479
|
const cmd = String((payload && payload.cmd) || '').trim();
|
|
11231
|
-
return sbRunStartManaged(folder, cmd);
|
|
11480
|
+
return sbRunStartManaged(folder, cmd, (payload && payload.kind) || 'run');
|
|
11232
11481
|
});
|
|
11233
11482
|
|
|
11234
|
-
|
|
11235
|
-
|
|
11236
|
-
|
|
11237
|
-
|
|
11483
|
+
/* proje tipi + önerilen komutlar (kullanıcı özel komutlarıyla birleşik) */
|
|
11484
|
+
ipcMain.handle('sandbox:detect', async (_e, payload) => {
|
|
11485
|
+
const folder = String((payload && payload.folder) || '');
|
|
11486
|
+
if (!folder.startsWith(sandboxRoot() + path.sep)) return { ok: false, error: 'geçersiz klasör' };
|
|
11487
|
+
return { ok: true, info: sbProjectInfoMerged(folder) };
|
|
11488
|
+
});
|
|
11489
|
+
|
|
11490
|
+
/* Kur: algılanan/kayıtlı kurulum komutunu doğrudan çalıştırır (panel + ajan ortak) */
|
|
11491
|
+
function sbInstallRepo(folder) {
|
|
11492
|
+
folder = String(folder || '');
|
|
11493
|
+
if (!folder.startsWith(sandboxRoot() + path.sep)) return { ok: false, error: 'geçersiz klasör' };
|
|
11494
|
+
const info = sbProjectInfoMerged(folder);
|
|
11495
|
+
if (!info.install) return { ok: false, error: 'Bu repoda kurulum komutu algılanamadı — özel komutu kutuya yazıp ▶ ile çalıştır' };
|
|
11496
|
+
const r = sbRunStartManaged(folder, info.install, 'install');
|
|
11497
|
+
if (r.ok) r.cmd = info.install;
|
|
11498
|
+
return r;
|
|
11499
|
+
}
|
|
11500
|
+
|
|
11501
|
+
ipcMain.handle('sandbox:install', async (_e, payload) => sbInstallRepo(String((payload && payload.folder) || '')));
|
|
11502
|
+
|
|
11503
|
+
/* bağımlılıklar eksik mi? — Başlat/Dev gerekirse ÖNCE kurar; kurulum bir kez
|
|
11504
|
+
yapıldıktan sonra damga dosyasıyla (node_modules var / %APPDATA% damgası)
|
|
11505
|
+
tekrarlanmaz */
|
|
11506
|
+
function sbInstallNeeded(folder, info) {
|
|
11238
11507
|
try {
|
|
11239
|
-
|
|
11240
|
-
|
|
11241
|
-
|
|
11242
|
-
|
|
11508
|
+
if (!info.install) return false;
|
|
11509
|
+
if (info.kind === 'node') {
|
|
11510
|
+
const nm = path.join(folder, 'node_modules');
|
|
11511
|
+
return !fs.existsSync(nm) || !fs.readdirSync(nm).length;
|
|
11512
|
+
}
|
|
11513
|
+
if (info.label === 'jekyll') return !fs.existsSync(path.join(folder, 'Gemfile.lock'));
|
|
11514
|
+
if (info.kind === 'python' || info.label === 'mkdocs') {
|
|
11515
|
+
if (fs.existsSync(path.join(folder, '.venv'))) return false;
|
|
11516
|
+
return !fs.existsSync(path.join(beastDir(), 'sandbox-installed', path.basename(folder)));
|
|
11517
|
+
}
|
|
11518
|
+
} catch {}
|
|
11519
|
+
return false;
|
|
11520
|
+
}
|
|
11521
|
+
|
|
11522
|
+
function sbMarkInstalled(folder) {
|
|
11523
|
+
try {
|
|
11524
|
+
const dir = path.join(beastDir(), 'sandbox-installed');
|
|
11525
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
11526
|
+
fs.writeFileSync(path.join(dir, path.basename(folder)), new Date().toISOString());
|
|
11243
11527
|
} catch {}
|
|
11244
|
-
|
|
11528
|
+
}
|
|
11529
|
+
|
|
11530
|
+
/* Başlat/Dev: algılanan çalıştırma komutunu başlatır; bağımlılıklar eksikse
|
|
11531
|
+
önce kurulum komutunu zincirler (npm install && npm run dev gibi) */
|
|
11532
|
+
function sbStartRepo(folder, mode) {
|
|
11533
|
+
folder = String(folder || '');
|
|
11534
|
+
mode = String(mode || 'run') === 'dev' ? 'dev' : 'run';
|
|
11535
|
+
if (!folder.startsWith(sandboxRoot() + path.sep)) return { ok: false, error: 'geçersiz klasör' };
|
|
11536
|
+
const info = sbProjectInfoMerged(folder);
|
|
11537
|
+
const cmd = mode === 'dev' ? info.dev || info.run : info.run || info.dev;
|
|
11538
|
+
if (!cmd) return { ok: false, error: 'Bu repo için çalıştırma komutu algılanamadı — özel komutu kutuya yazıp ▶ ile çalıştır' };
|
|
11539
|
+
const pre = sbInstallNeeded(folder, info) ? info.install : '';
|
|
11540
|
+
const full = pre ? pre + ' && ' + cmd : cmd;
|
|
11541
|
+
const r = sbRunStartManaged(folder, full, 'run', pre ? { installCmd: pre } : null);
|
|
11542
|
+
if (r.ok) r.cmd = full;
|
|
11543
|
+
return r;
|
|
11544
|
+
}
|
|
11545
|
+
|
|
11546
|
+
ipcMain.handle('sandbox:start', async (_e, payload) => {
|
|
11547
|
+
const folder = String((payload && payload.folder) || '');
|
|
11548
|
+
return sbStartRepo(folder, String((payload && payload.mode) || 'run'));
|
|
11549
|
+
});
|
|
11550
|
+
|
|
11551
|
+
/* seçili repo paneli için: çalışıyor mu + tamponlanmış çıktı */
|
|
11552
|
+
ipcMain.handle('sandbox:procstate', async (_e, payload) => {
|
|
11553
|
+
const folder = String((payload && payload.folder) || '');
|
|
11554
|
+
if (!folder.startsWith(sandboxRoot() + path.sep)) return { ok: false, error: 'geçersiz klasör' };
|
|
11555
|
+
const slot = sbProcs.get(folder);
|
|
11556
|
+
return {
|
|
11557
|
+
ok: true,
|
|
11558
|
+
running: !!(slot && slot.proc),
|
|
11559
|
+
kind: slot ? slot.kind : '',
|
|
11560
|
+
cmd: slot ? slot.cmd : '',
|
|
11561
|
+
url: slot ? slot.url : '',
|
|
11562
|
+
lines: slot ? slot.lines.slice() : [],
|
|
11563
|
+
};
|
|
11564
|
+
});
|
|
11565
|
+
|
|
11566
|
+
/* özel komutları kalıcı kaydet (repo klasörü → install/run/dev) */
|
|
11567
|
+
ipcMain.handle('sandbox:cfg:set', async (_e, payload) => {
|
|
11568
|
+
const folder = String((payload && payload.folder) || '');
|
|
11569
|
+
if (!folder.startsWith(sandboxRoot() + path.sep)) return { ok: false, error: 'geçersiz klasör' };
|
|
11570
|
+
sbLoadCfg();
|
|
11571
|
+
const cur = sbCfg[folder] && typeof sbCfg[folder] === 'object' ? sbCfg[folder] : {};
|
|
11572
|
+
const next = { ...cur };
|
|
11573
|
+
for (const k of ['install', 'run', 'dev']) {
|
|
11574
|
+
if (payload && payload[k] !== undefined) {
|
|
11575
|
+
const v = String(payload[k] || '').trim().slice(0, 500);
|
|
11576
|
+
if (v) next[k] = v;
|
|
11577
|
+
else delete next[k];
|
|
11578
|
+
}
|
|
11579
|
+
}
|
|
11580
|
+
if (Object.keys(next).length) sbCfg[folder] = next;
|
|
11581
|
+
else delete sbCfg[folder];
|
|
11582
|
+
sbSaveCfg();
|
|
11583
|
+
return { ok: true, cfg: sbCfg[folder] || {} };
|
|
11584
|
+
});
|
|
11585
|
+
|
|
11586
|
+
ipcMain.handle('sandbox:runstop', async (_e, payload) => {
|
|
11587
|
+
const folder = String((payload && payload.folder) || '');
|
|
11588
|
+
if (folder) {
|
|
11589
|
+
if (!folder.startsWith(sandboxRoot() + path.sep)) return { ok: false, error: 'geçersiz klasör' };
|
|
11590
|
+
return { ok: true, wasRunning: sbRunStopFolder(folder) };
|
|
11591
|
+
}
|
|
11592
|
+
let any = false;
|
|
11593
|
+
for (const f of Array.from(sbProcs.keys())) {
|
|
11594
|
+
if (sbRunStopFolder(f)) any = true;
|
|
11595
|
+
}
|
|
11596
|
+
return { ok: true, wasRunning: any };
|
|
11245
11597
|
});
|
|
11246
11598
|
|
|
11247
11599
|
ipcMain.handle('sandbox:openurl', async (_e, url) => {
|
|
@@ -11256,6 +11608,98 @@ ipcMain.handle('sandbox:openurl', async (_e, url) => {
|
|
|
11256
11608
|
}
|
|
11257
11609
|
});
|
|
11258
11610
|
|
|
11611
|
+
/* ---------- SANDBOX PANEL İÇİ CANLI ÖNİZLEME (native WebContentsView) ----------
|
|
11612
|
+
ÇALIŞTIR bölümünde, çıktı günlüğünün yerine repo'nun çalışan hâlini gösterir.
|
|
11613
|
+
Tarayıcı dock'undan bağımsızdır: renderer host alanının DOM dikdörtgenini
|
|
11614
|
+
gönderir, view tam o alana oturur. Yalnız localhost adresleri yüklenir. */
|
|
11615
|
+
const sbPreview = { view: null, attached: false, visible: false, url: '', bounds: null };
|
|
11616
|
+
|
|
11617
|
+
function ensureSbPreview() {
|
|
11618
|
+
if (sbPreview.view) return sbPreview.view;
|
|
11619
|
+
const view = new WebContentsView({
|
|
11620
|
+
webPreferences: {
|
|
11621
|
+
partition: 'persist:sandbox-preview',
|
|
11622
|
+
contextIsolation: true,
|
|
11623
|
+
nodeIntegration: false,
|
|
11624
|
+
sandbox: true,
|
|
11625
|
+
spellcheck: false,
|
|
11626
|
+
backgroundThrottling: false,
|
|
11627
|
+
},
|
|
11628
|
+
});
|
|
11629
|
+
view.setBackgroundColor(settings.theme === 'dark' ? '#0d0d0f' : '#ffffff');
|
|
11630
|
+
const wc = view.webContents;
|
|
11631
|
+
try {
|
|
11632
|
+
const chromeUA = `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${process.versions.chrome} Safari/537.36`;
|
|
11633
|
+
wc.setUserAgent(chromeUA);
|
|
11634
|
+
} catch {}
|
|
11635
|
+
wc.setWindowOpenHandler(({ url }) => {
|
|
11636
|
+
if (/^https?:\/\//i.test(url)) wc.loadURL(url).catch(() => {});
|
|
11637
|
+
return { action: 'deny' };
|
|
11638
|
+
});
|
|
11639
|
+
wc.on('render-process-gone', () => {
|
|
11640
|
+
if (win && !win.isDestroyed() && sbPreview.view) {
|
|
11641
|
+
try { win.contentView.removeChildView(sbPreview.view); } catch {}
|
|
11642
|
+
}
|
|
11643
|
+
sbPreview.view = null;
|
|
11644
|
+
sbPreview.attached = false;
|
|
11645
|
+
sbPreview.visible = false;
|
|
11646
|
+
});
|
|
11647
|
+
sbPreview.view = view;
|
|
11648
|
+
return view;
|
|
11649
|
+
}
|
|
11650
|
+
|
|
11651
|
+
function sbPreviewDetach() {
|
|
11652
|
+
if (sbPreview.view && win && !win.isDestroyed()) {
|
|
11653
|
+
try { sbPreview.view.setVisible(false); } catch {}
|
|
11654
|
+
try { win.contentView.removeChildView(sbPreview.view); } catch {}
|
|
11655
|
+
}
|
|
11656
|
+
sbPreview.attached = false;
|
|
11657
|
+
sbPreview.visible = false;
|
|
11658
|
+
}
|
|
11659
|
+
|
|
11660
|
+
function sbPreviewSet(payload) {
|
|
11661
|
+
if (!win || win.isDestroyed()) return { ok: false, error: 'pencere yok' };
|
|
11662
|
+
const p = payload || {};
|
|
11663
|
+
if (p.bounds && typeof p.bounds === 'object') {
|
|
11664
|
+
sbPreview.bounds = {
|
|
11665
|
+
x: Math.max(0, Math.round(Number(p.bounds.x) || 0)),
|
|
11666
|
+
y: Math.max(0, Math.round(Number(p.bounds.y) || 0)),
|
|
11667
|
+
width: Math.max(0, Math.round(Number(p.bounds.width) || 0)),
|
|
11668
|
+
height: Math.max(0, Math.round(Number(p.bounds.height) || 0)),
|
|
11669
|
+
};
|
|
11670
|
+
}
|
|
11671
|
+
if (p.url) {
|
|
11672
|
+
const u = String(p.url);
|
|
11673
|
+
if (/^https?:\/\/(?:localhost|127\.0\.0\.1)(?::\d{2,5})?(?:[/?#]|$)/i.test(u)) sbPreview.url = u;
|
|
11674
|
+
}
|
|
11675
|
+
const b = sbPreview.bounds;
|
|
11676
|
+
const want = p.visible !== false && !!sbPreview.url && !!b && b.width > 0 && b.height > 0;
|
|
11677
|
+
if (!want) {
|
|
11678
|
+
if (sbPreview.view) {
|
|
11679
|
+
try { sbPreview.view.setVisible(false); } catch {}
|
|
11680
|
+
}
|
|
11681
|
+
sbPreview.visible = false;
|
|
11682
|
+
return { ok: true, visible: false };
|
|
11683
|
+
}
|
|
11684
|
+
const view = ensureSbPreview();
|
|
11685
|
+
if (sbPreview.url) {
|
|
11686
|
+
let cur = '';
|
|
11687
|
+
try { cur = view.webContents.getURL(); } catch {}
|
|
11688
|
+
if (cur !== sbPreview.url) view.webContents.loadURL(sbPreview.url).catch(() => {});
|
|
11689
|
+
}
|
|
11690
|
+
if (!sbPreview.attached) {
|
|
11691
|
+
try { win.contentView.addChildView(view); sbPreview.attached = true; } catch {}
|
|
11692
|
+
}
|
|
11693
|
+
try {
|
|
11694
|
+
view.setBounds({ x: b.x, y: b.y, width: b.width, height: b.height });
|
|
11695
|
+
view.setVisible(true);
|
|
11696
|
+
sbPreview.visible = true;
|
|
11697
|
+
} catch {}
|
|
11698
|
+
return { ok: true, visible: true, url: sbPreview.url };
|
|
11699
|
+
}
|
|
11700
|
+
|
|
11701
|
+
ipcMain.handle('sbpreview:set', async (_e, payload) => sbPreviewSet(payload || {}));
|
|
11702
|
+
|
|
11259
11703
|
/* Sağ tık menüsü: HTML dosyasını dahili tarayıcıda GÖRÜNÜR aç */
|
|
11260
11704
|
/* ---------- BC DAHİLİ STATİK SUNUCU ----------
|
|
11261
11705
|
Beast Code çıktısı ASLA file:// ile açılmaz: ES modülleri, fetch, ServiceWorker
|