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/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 ayarlanmalı */
276
- try { app.setAppUserModelId('com.quantumalgo.beastagent'); } catch {}
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
- items.push({ name: e.name, folder, sid, busy });
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
- ipcMain.handle('sandbox:clone', async (_e, input) => {
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 klasöründe uzun süreli komut (npm start/dev vb.)
11166
- YÖNETİLEN SÜREÇ — çıktı satır satır panele akar; ■ ile ağaç-kullanı durdurulur.
11167
- Dev server adresi çıktıdan yakalanır → 'Tarayıcıda aç' aktifleşir.
11168
- Aynı fonksiyon engine.sbRunHook olarak da bağlanır — ajanın panel_run
11169
- aracı turu KİLİTLEMEDEN süreç başlatır (Kur/Başlat butonları bunu kullanır). */
11170
- const sbRunProc = { proc: null, folder: '', url: '' };
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()) win.webContents.send('agent:event', { type: 'sb-run', data: String(data || '') });
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 sbRunStartManaged(folder, cmd) {
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
- if (sbRunProc.proc) return { ok: false, error: 'ÇALIŞTIR panelinde zaten bir süreç çalışıyor — önce ■ ile durdur' };
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
- sbRunProc.proc = proc;
11192
- sbRunProc.folder = folder;
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
- sbRunEmit(piece);
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 && !sbRunProc.url) {
11201
- sbRunProc.url = m[0].replace(/[)\].,;:'"]+$/, '');
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: sbRunProc.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
- sbRunEmit('[süreç bitti' + (code != null ? ' — kod ' + code : '') + ']');
11212
- sbRunProc.proc = null;
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()) win.webContents.send('agent:event', { type: 'sb-run-end' });
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
- sbRunEmit('▶ ' + cmd + ' (klasör: ' + path.basename(folder) + ')');
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
- ipcMain.handle('sandbox:runstop', async () => {
11235
- const p = sbRunProc.proc;
11236
- if (!p) return { ok: true, wasRunning: false };
11237
- sbRunProc.proc = null;
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
- /* cmd.exe /c zinciriyle çocuklar (node/vite) geride kalmasın — ağaç kesimi */
11240
- const { exec } = require('child_process');
11241
- if (process.platform === 'win32') exec('taskkill /pid ' + p.pid + ' /T /F', { windowsHide: true });
11242
- else p.kill('SIGTERM');
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
- return { ok: true, wasRunning: true };
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