@artilingo/artiframe-cli 1.0.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.
Files changed (60) hide show
  1. package/assets/logo.png +0 -0
  2. package/assets/logo.svg +22 -0
  3. package/assets/logo.webp +0 -0
  4. package/assets/logo@0.5x.png +0 -0
  5. package/assets/logo@0.75x.png +0 -0
  6. package/assets/logo@1.5x.png +0 -0
  7. package/assets/logo@2x.png +0 -0
  8. package/assets/logo@3x.png +0 -0
  9. package/assets/logo@4x.png +0 -0
  10. package/bin/artiframe.js +57 -0
  11. package/bin/artiframe.php +45 -0
  12. package/core-stubs/app/ApiControl.php +81 -0
  13. package/core-stubs/app/Database.php +69 -0
  14. package/core-stubs/app/DotEnv.php +63 -0
  15. package/core-stubs/app/R2Manager.php +130 -0
  16. package/core-stubs/app/ViewControl.php +24 -0
  17. package/core-stubs/bin/SystemMethod.php +271 -0
  18. package/core-stubs/bin/ViewMethod.php +354 -0
  19. package/core-stubs/docs/de.html +951 -0
  20. package/core-stubs/docs/en.html +952 -0
  21. package/core-stubs/docs/es.html +947 -0
  22. package/core-stubs/docs/fr.html +850 -0
  23. package/core-stubs/docs/tr.html +951 -0
  24. package/core-stubs/public/assets/css/components/footer.css +0 -0
  25. package/core-stubs/public/assets/css/components/header.css +0 -0
  26. package/core-stubs/public/assets/css/components/mobilenav.css +0 -0
  27. package/core-stubs/public/assets/css/components/sidebar.css +0 -0
  28. package/core-stubs/public/assets/css/components/theme-modal.css +0 -0
  29. package/core-stubs/public/assets/css/root/app.css +61 -0
  30. package/core-stubs/public/assets/js/components/header.js +0 -0
  31. package/core-stubs/public/assets/js/components/mobilenav.js +0 -0
  32. package/core-stubs/public/assets/js/components/sidebar.js +0 -0
  33. package/core-stubs/public/assets/js/components/theme-modal.js +0 -0
  34. package/core-stubs/public/assets/js/root/app.js +30 -0
  35. package/core-stubs/public/includes/footer.php +5 -0
  36. package/core-stubs/public/includes/head.php +26 -0
  37. package/core-stubs/public/includes/header.php +5 -0
  38. package/core-stubs/public/includes/mobilenav.php +5 -0
  39. package/core-stubs/public/includes/sidebar.php +5 -0
  40. package/core-stubs/public/includes/theme-modal.php +5 -0
  41. package/core-stubs/readme/de.md +29 -0
  42. package/core-stubs/readme/en.md +29 -0
  43. package/core-stubs/readme/es.md +29 -0
  44. package/core-stubs/readme/fr.md +29 -0
  45. package/core-stubs/readme/tr.md +29 -0
  46. package/package.json +49 -0
  47. package/scripts/postinstall.js +21 -0
  48. package/src/App.php +266 -0
  49. package/src/Commands/MakeApiCommand.php +71 -0
  50. package/src/Commands/MakeClassCommand.php +88 -0
  51. package/src/Commands/MakeViewCommand.php +95 -0
  52. package/src/Commands/NewProjectCommand.php +633 -0
  53. package/src/Commands/VersionCommand.php +92 -0
  54. package/src/Lang/de.php +53 -0
  55. package/src/Lang/en.php +53 -0
  56. package/src/Lang/es.php +53 -0
  57. package/src/Lang/fr.php +53 -0
  58. package/src/Lang/tr.php +53 -0
  59. package/src/Services/Safeguard.php +48 -0
  60. package/src/Services/Translator.php +52 -0
@@ -0,0 +1,633 @@
1
+ <?php
2
+ namespace ArtiFrame\Cli\Commands;
3
+
4
+ use ArtiFrame\Cli\Services\Translator;
5
+ use ArtiFrame\Cli\Services\Safeguard;
6
+
7
+ class NewProjectCommand
8
+ {
9
+ private Translator $translator;
10
+ private Safeguard $safeguard;
11
+
12
+ // İlerleme takibi
13
+ private int $totalSteps = 0;
14
+ private int $currentStep = 0;
15
+
16
+ public function __construct(Translator $translator)
17
+ {
18
+ $this->translator = $translator;
19
+ $this->safeguard = new Safeguard($translator);
20
+ }
21
+
22
+ public function execute(array $args): void
23
+ {
24
+ $targetPath = $args[0] ?? null;
25
+
26
+ if (!$targetPath) {
27
+ echo $this->translator->get('DIR_REQUIRED_ERROR') . PHP_EOL;
28
+ exit(1);
29
+ }
30
+
31
+ // 1. Dizin çözümleme
32
+ $targetDir = $this->resolvePath($targetPath);
33
+ $projectName = basename($targetDir);
34
+
35
+ // 2. Safeguard Kontrolü
36
+ if (!$this->safeguard->checkTarget($targetDir)) {
37
+ exit(1);
38
+ }
39
+
40
+ // 3. Composer Kontrolü
41
+ if (!$this->isComposerInstalled()) {
42
+ echo PHP_EOL . "❌ " . $this->translator->get('COMPOSER_MISSING_TITLE') . PHP_EOL;
43
+ echo str_repeat("-", 50) . PHP_EOL;
44
+ echo $this->translator->get('COMPOSER_MISSING_BODY') . PHP_EOL;
45
+ exit(1);
46
+ }
47
+
48
+ // ── Başlık Ekranı ────────────────────────────────────
49
+ echo PHP_EOL;
50
+ $builderTitle = $this->translator->get('PROJECT_BUILDER_TITLE');
51
+ echo " ╔══════════════════════════════════════════════════╗" . PHP_EOL;
52
+ printf(" ║ %-49s║" . PHP_EOL, $builderTitle);
53
+ echo " ╚══════════════════════════════════════════════════╝" . PHP_EOL;
54
+ echo PHP_EOL;
55
+ echo " " . $this->translator->get('PROJECT_LABEL') . " : " . $projectName . PHP_EOL;
56
+ echo " " . $this->translator->get('LOCATION_LABEL') . " : " . $targetDir . PHP_EOL;
57
+ echo PHP_EOL;
58
+
59
+ // Toplam adım sayısını hesapla
60
+ $coreStubs = \ARTIFRAME_CLI_ROOT . '/core-stubs';
61
+ $stubCount = $this->countFiles($coreStubs);
62
+ $dynamicCount = 14; // createDynamicFiles içindeki dosya sayısı
63
+ $dirCount = 10; // oluşturulacak dinamik dizin sayısı
64
+ $this->totalSteps = $stubCount + $dynamicCount + $dirCount;
65
+
66
+ // ── FAZ 1: KOPYALAMA ─────────────────────────────────
67
+ $this->printPhaseHeader(1, 4, $this->translator->get('PHASE_COPYING'), $stubCount);
68
+
69
+ if (!is_dir($targetDir)) {
70
+ mkdir($targetDir, 0755, true);
71
+ }
72
+ $this->copyDirectory($coreStubs, $targetDir);
73
+
74
+ // 1.5 Dökümantasyon ve Readme Yerelleştirmesi
75
+ $lang = $this->translator->getLang();
76
+ $this->localizeDocsAndReadme($targetDir, $lang);
77
+
78
+ $this->finishPhase();
79
+
80
+ // ── FAZ 2: DİZİNLER ──────────────────────────────────
81
+ $this->printPhaseHeader(2, 4, $this->translator->get('PHASE_DIRS'), $dirCount);
82
+
83
+ $directoriesToCreate = [
84
+ 'src/Auth',
85
+ 'src/Email',
86
+ 'src/Service',
87
+ 'config',
88
+ 'app',
89
+ '.agents',
90
+ 'public/api/standart',
91
+ 'public/api/switch-case',
92
+ 'public/auth',
93
+ 'public/assets/images',
94
+ ];
95
+
96
+ foreach ($directoriesToCreate as $dir) {
97
+ $path = $targetDir . \DIRECTORY_SEPARATOR . $dir;
98
+ if (!is_dir($path)) {
99
+ mkdir($path, 0755, true);
100
+ }
101
+ $this->tick($dir . '/');
102
+ }
103
+ $this->finishPhase();
104
+
105
+ // ── FAZ 3: DİNAMİK DOSYALAR ──────────────────────────
106
+ $this->printPhaseHeader(3, 4, $this->translator->get('PHASE_FILES'), $dynamicCount);
107
+ $this->createDynamicFiles($targetDir);
108
+ $this->finishPhase();
109
+
110
+ // ── FAZ 4: COMPOSER ──────────────────────────────────
111
+ echo PHP_EOL;
112
+ $phaseLabel = 'Faz 4/4 — ' . $this->translator->get('PHASE_DEPS');
113
+ echo " ┌─────────────────────────────────────────────────┐" . PHP_EOL;
114
+ printf(" │ 📦 %-43s│" . PHP_EOL, $phaseLabel);
115
+ echo " └─────────────────────────────────────────────────┘" . PHP_EOL;
116
+ echo PHP_EOL;
117
+
118
+ $this->runComposerInstall($targetDir);
119
+
120
+ // ── BAŞARI ────────────────────────────────────────────
121
+ echo PHP_EOL;
122
+ $successTitle = $this->translator->get('SUCCESS_TITLE');
123
+ echo " ╔══════════════════════════════════════════════════╗" . PHP_EOL;
124
+ printf(" ║ %-49s║" . PHP_EOL, $successTitle);
125
+ echo " ╚══════════════════════════════════════════════════╝" . PHP_EOL;
126
+ echo PHP_EOL;
127
+
128
+ // Dizin ağacı
129
+ $this->printDirectoryTree($targetDir, $projectName);
130
+
131
+ echo PHP_EOL;
132
+ echo " " . $this->translator->get('NEXT_STEPS') . PHP_EOL;
133
+ echo " cd " . $projectName . PHP_EOL;
134
+ echo " " . $this->translator->get('NEXT_STEPS_EDIT_ENV') . PHP_EOL;
135
+ echo PHP_EOL;
136
+ }
137
+
138
+ // ─── İlerleme Sistemi ─────────────────────────────────────
139
+
140
+ private function printPhaseHeader(int $phase, int $total, string $label, int $fileCount): void
141
+ {
142
+ $filesLabel = $this->translator->get('FILES_TO_PROCESS', ['count' => $fileCount]);
143
+ echo PHP_EOL;
144
+ echo " ┌─────────────────────────────────────────────────┐" . PHP_EOL;
145
+ printf(" │ 📂 Faz %d/%d — %-36s│" . PHP_EOL, $phase, $total, $label);
146
+ printf(" │ %-43s│" . PHP_EOL, $filesLabel);
147
+ echo " └─────────────────────────────────────────────────┘" . PHP_EOL;
148
+ }
149
+
150
+ private function tick(string $label): void
151
+ {
152
+ $this->currentStep++;
153
+ $pct = (int) round(($this->currentStep / $this->totalSteps) * 100);
154
+ $filled = (int) round($pct / 5); // 20 karakterlik bar
155
+ $empty = max(0, 20 - $filled);
156
+
157
+ $bar = str_repeat('█', $filled) . str_repeat('░', $empty);
158
+ $line = sprintf(
159
+ " [%s] %3d%% (%d/%d) %-25s",
160
+ $bar,
161
+ $pct,
162
+ $this->currentStep,
163
+ $this->totalSteps,
164
+ mb_substr($label, 0, 25)
165
+ );
166
+
167
+ // Aynı satırı güncelle (\r ile başa dön, yeni satır yok)
168
+ echo "\r" . $line;
169
+ flush();
170
+ }
171
+
172
+ private function finishPhase(): void
173
+ {
174
+ echo PHP_EOL; // İlerleme satırını tamamla
175
+ }
176
+
177
+ // ─── Dosya Sayacı ─────────────────────────────────────────
178
+
179
+ private function countFiles(string $dir): int
180
+ {
181
+ $count = 0;
182
+ $items = new \RecursiveIteratorIterator(
183
+ new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS)
184
+ );
185
+ foreach ($items as $item) {
186
+ if ($item->isFile()) {
187
+ $count++;
188
+ }
189
+ }
190
+ return $count;
191
+ }
192
+
193
+ // ─── Dizin Ağacı Çizici ───────────────────────────────────
194
+
195
+ private function printDirectoryTree(string $dir, string $projectName): void
196
+ {
197
+ echo " 📂 " . $projectName . "/" . PHP_EOL;
198
+
199
+ $priorityOrder = [
200
+ 'bin', 'config', 'src', 'app', 'public', '.agents',
201
+ 'vendor', '.env', '.env.example', '.gitignore',
202
+ 'composer.json', 'composer.lock', 'schema.sql',
203
+ 'README.md', 'OKUBENI.md', 'LESEMICH.md', 'LISEZMOI.md', 'LEAME.md',
204
+ 'kilavuz.html', 'guide.html', 'handbuch.html', 'guide_fr.html', 'guia.html', 'LICENSE',
205
+ ];
206
+
207
+ $entries = array_filter(scandir($dir), fn($e) => $e !== '.' && $e !== '..');
208
+
209
+ usort($entries, function ($a, $b) use ($priorityOrder) {
210
+ $ia = array_search($a, $priorityOrder);
211
+ $ib = array_search($b, $priorityOrder);
212
+ $ia = ($ia === false) ? 99 : $ia;
213
+ $ib = ($ib === false) ? 99 : $ib;
214
+ return $ia <=> $ib;
215
+ });
216
+
217
+ $entries = array_values($entries);
218
+ $total = count($entries);
219
+
220
+ foreach ($entries as $i => $entry) {
221
+ $isLast = ($i === $total - 1);
222
+ $connector = $isLast ? '└──' : '├──';
223
+ $fullPath = $dir . \DIRECTORY_SEPARATOR . $entry;
224
+
225
+ if (is_dir($fullPath)) {
226
+ $subItems = array_filter(scandir($fullPath), fn($e) => $e !== '.' && $e !== '..');
227
+ $subCount = count($subItems);
228
+ $countStr = $subCount > 0 ? ' ' . $this->translator->get('DIR_ITEM_COUNT', ['count' => $subCount]) : '';
229
+ echo " │ {$connector} 📂 {$entry}/{$countStr}" . PHP_EOL;
230
+ } else {
231
+ $size = filesize($fullPath);
232
+ $sizeStr = $size > 1024 ? round($size / 1024, 1) . ' KB' : $size . ' B';
233
+ echo " │ {$connector} 📄 {$entry} [{$sizeStr}]" . PHP_EOL;
234
+ }
235
+ }
236
+ echo " │" . PHP_EOL;
237
+ }
238
+
239
+ // ─── Yol Çözümleyici ──────────────────────────────────────
240
+
241
+ private function resolvePath(string $path): string
242
+ {
243
+ if (strpos($path, '/') === 0 || preg_match('/^[a-zA-Z]:\\\\/', $path)) {
244
+ return str_replace('\\', '/', $path);
245
+ }
246
+ return str_replace('\\', '/', getcwd() . '/' . $path);
247
+ }
248
+
249
+ private function isComposerInstalled(): bool
250
+ {
251
+ $output = [];
252
+ $returnVar = -1;
253
+ exec("composer --version 2>&1", $output, $returnVar);
254
+ return $returnVar === 0;
255
+ }
256
+
257
+ // ─── Kopyalama Motoru ─────────────────────────────────────
258
+
259
+ private function copyDirectory(string $source, string $target): void
260
+ {
261
+ if (!is_dir($target)) {
262
+ mkdir($target, 0755, true);
263
+ }
264
+
265
+ $dir = opendir($source);
266
+ while (false !== ($file = readdir($dir))) {
267
+ if ($file === '.' || $file === '..') {
268
+ continue;
269
+ }
270
+
271
+ $srcFile = $source . '/' . $file;
272
+ $targetFile = $target . '/' . $file;
273
+
274
+ if (is_dir($srcFile)) {
275
+ $this->copyDirectory($srcFile, $targetFile);
276
+ } else {
277
+ copy($srcFile, $targetFile);
278
+ $this->tick($file);
279
+ }
280
+ }
281
+ closedir($dir);
282
+ }
283
+
284
+ private function localizeDocsAndReadme(string $targetDir, string $lang): void
285
+ {
286
+ $langMap = [
287
+ 'tr' => ['docs' => 'kilavuz.html', 'readme' => 'OKUBENI.md'],
288
+ 'en' => ['docs' => 'guide.html', 'readme' => 'README.md'],
289
+ 'de' => ['docs' => 'handbuch.html', 'readme' => 'LESEMICH.md'],
290
+ 'fr' => ['docs' => 'guide_fr.html', 'readme' => 'LISEZMOI.md'],
291
+ 'es' => ['docs' => 'guia.html', 'readme' => 'LEAME.md']
292
+ ];
293
+
294
+ $docNames = $langMap[$lang] ?? $langMap['en'];
295
+
296
+ if (file_exists($targetDir . '/docs/' . $lang . '.html')) {
297
+ copy($targetDir . '/docs/' . $lang . '.html', $targetDir . '/' . $docNames['docs']);
298
+ }
299
+ if (file_exists($targetDir . '/readme/' . $lang . '.md')) {
300
+ copy($targetDir . '/readme/' . $lang . '.md', $targetDir . '/' . $docNames['readme']);
301
+ }
302
+
303
+ $this->removeDirectory($targetDir . '/docs');
304
+ $this->removeDirectory($targetDir . '/readme');
305
+ if (file_exists($targetDir . '/README.md') && $lang !== 'en') {
306
+ unlink($targetDir . '/README.md'); // Remove default README if copied directly from core-stubs root
307
+ }
308
+ if (file_exists($targetDir . '/kilavuz.html') && $lang !== 'tr') {
309
+ unlink($targetDir . '/kilavuz.html'); // Remove default kilavuz.html if copied directly from core-stubs root
310
+ }
311
+ }
312
+
313
+ private function removeDirectory(string $dir): void
314
+ {
315
+ if (!is_dir($dir)) return;
316
+ $items = new \RecursiveIteratorIterator(
317
+ new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS),
318
+ \RecursiveIteratorIterator::CHILD_FIRST
319
+ );
320
+ foreach ($items as $item) {
321
+ $item->isDir() ? rmdir($item->getRealPath()) : unlink($item->getRealPath());
322
+ }
323
+ rmdir($dir);
324
+ }
325
+
326
+ // ─── Dinamik Dosya Üretici ────────────────────────────────
327
+
328
+ private function createDynamicFiles(string $targetDir): void
329
+ {
330
+ $licenseHeader = "<?php\n/**\n * ArtiFrame Core Engine\n *\n * @package ArtiFrame\n * @author Artilingo\n * @license AGPLv3 (Attribution-ShareAlike Required)\n * @link https://artiframe.org\n *\n * NOTICE: This file is part of the ArtiFrame ecosystem.\n * Any derivative works or patches MUST retain this original copyright notice\n * and remain open-source under the AGPLv3 license.\n */\n";
331
+
332
+ // composer.json
333
+ $composerJson = <<<JSON
334
+ {
335
+ "name": "artiframe/project",
336
+ "description": "ArtiFrame Core PHP Application",
337
+ "type": "project",
338
+ "license": "AGPL-3.0-or-later",
339
+ "require": {
340
+ "php": ">=8.1",
341
+ "phpmailer/phpmailer": "^6.9",
342
+ "aws/aws-sdk-php": "^3.316",
343
+ "predis/predis": "^2.2",
344
+ "ramsey/uuid": "^4.7"
345
+ },
346
+ "autoload": {
347
+ "psr-4": {
348
+ "Bin\\\\": "bin/",
349
+ "App\\\\": "app/",
350
+ "Src\\\\": "src/"
351
+ }
352
+ },
353
+ "authors": [
354
+ {
355
+ "name": "Artilingo",
356
+ "homepage": "https://artilingo.com"
357
+ }
358
+ ],
359
+ "config": {
360
+ "optimize-autoloader": true,
361
+ "preferred-install": "dist",
362
+ "sort-packages": true
363
+ }
364
+ }
365
+ JSON;
366
+ file_put_contents($targetDir . '/composer.json', $composerJson);
367
+ $this->tick('composer.json');
368
+
369
+ // config/app-version.php
370
+ file_put_contents(
371
+ $targetDir . '/config/app-version.php',
372
+ $licenseHeader . "\n// APP_ENV: 0 = Canlı (Prod), 1 = Geliştirici (Debug)\ndefine('APP_ENV', 1);\ndefine('APP_VERSION', '1.0.0');\n"
373
+ );
374
+ $this->tick('config/app-version.php');
375
+
376
+ // config/central-control.php
377
+ file_put_contents($targetDir . '/config/central-control.php', $licenseHeader . "\n// Central Control Configurations\n");
378
+ $this->tick('config/central-control.php');
379
+
380
+ // config/api-security.php
381
+ file_put_contents($targetDir . '/config/api-security.php', $licenseHeader . "\n// API Security Rules\n");
382
+ $this->tick('config/api-security.php');
383
+
384
+ // .env & .env.example
385
+ $envStub = implode("\n", [
386
+ "# ================================================",
387
+ "# ArtiFrame Environment Configuration",
388
+ "# Hassas bilgileri buraya girin. Bu dosyayı asla",
389
+ "# git reposuna eklemeyin! Bkz: .gitignore",
390
+ "# ================================================",
391
+ "",
392
+ "# Uygulama",
393
+ "APP_NAME=ArtiFrame",
394
+ "APP_URL=http://localhost",
395
+ "",
396
+ "# Veritabanı (MySQL / MariaDB)",
397
+ "DB_HOST=localhost",
398
+ "DB_USER=root",
399
+ "DB_PASS=",
400
+ "DB_NAME=artiframe",
401
+ "",
402
+ "# SMTP (PHPMailer)",
403
+ "MAIL_HOST=smtp.example.com",
404
+ "MAIL_PORT=587",
405
+ "MAIL_USERNAME=",
406
+ "MAIL_PASSWORD=",
407
+ "MAIL_FROM_ADDRESS=noreply@example.com",
408
+ "MAIL_FROM_NAME=ArtiFrame",
409
+ "MAIL_ENCRYPTION=tls",
410
+ "",
411
+ "# Cloudflare R2 (AWS S3 Uyumlu Depolama)",
412
+ "R2_ACCOUNT_ID=",
413
+ "R2_ACCESS_KEY=",
414
+ "R2_SECRET_KEY=",
415
+ "R2_BUCKET_NAME=",
416
+ "R2_PUBLIC_URL=",
417
+ "",
418
+ "# Redis",
419
+ "REDIS_HOST=127.0.0.1",
420
+ "REDIS_PORT=6379",
421
+ "REDIS_PASSWORD=",
422
+ "REDIS_DB=0",
423
+ ]) . "\n";
424
+ file_put_contents($targetDir . '/.env', $envStub);
425
+ $this->tick('.env');
426
+ file_put_contents($targetDir . '/.env.example', $envStub);
427
+ $this->tick('.env.example');
428
+
429
+ // schema.sql
430
+ file_put_contents($targetDir . '/schema.sql', "-- ArtiFrame Initial DB Schema\n-- Created: " . date('Y-m-d') . "\n\n");
431
+ $this->tick('schema.sql');
432
+
433
+ // src/Email/EmailService.php
434
+ $emailServiceContent = <<<'EMAILPHP'
435
+ <?php
436
+ /**
437
+ * ArtiFrame Core Engine
438
+ *
439
+ * @package ArtiFrame
440
+ * @author Artilingo
441
+ * @license AGPLv3 (Attribution-ShareAlike Required)
442
+ * @link https://artiframe.org
443
+ */
444
+
445
+ namespace Src\Email;
446
+
447
+ use PHPMailer\PHPMailer\PHPMailer;
448
+ use PHPMailer\PHPMailer\SMTP;
449
+ use PHPMailer\PHPMailer\Exception;
450
+
451
+ class EmailService
452
+ {
453
+ private PHPMailer $mailer;
454
+
455
+ public function __construct()
456
+ {
457
+ $this->mailer = new PHPMailer(true);
458
+ $this->mailer->isSMTP();
459
+ $this->mailer->Host = $_ENV['MAIL_HOST'] ?? 'smtp.example.com';
460
+ $this->mailer->SMTPAuth = true;
461
+ $this->mailer->Username = $_ENV['MAIL_USERNAME'] ?? '';
462
+ $this->mailer->Password = $_ENV['MAIL_PASSWORD'] ?? '';
463
+ $this->mailer->SMTPSecure = $_ENV['MAIL_ENCRYPTION'] ?? PHPMailer::ENCRYPTION_STARTTLS;
464
+ $this->mailer->Port = (int) ($_ENV['MAIL_PORT'] ?? 587);
465
+ $this->mailer->CharSet = 'UTF-8';
466
+ $this->mailer->setFrom(
467
+ $_ENV['MAIL_FROM_ADDRESS'] ?? 'noreply@example.com',
468
+ $_ENV['MAIL_FROM_NAME'] ?? 'ArtiFrame'
469
+ );
470
+ }
471
+
472
+ /**
473
+ * Tek alıcıya HTML e-posta gönderir.
474
+ */
475
+ public function send(string $toEmail, string $toName, string $subject, string $body): bool
476
+ {
477
+ try {
478
+ $this->mailer->clearAddresses();
479
+ $this->mailer->addAddress($toEmail, $toName);
480
+ $this->mailer->isHTML(true);
481
+ $this->mailer->Subject = $subject;
482
+ $this->mailer->Body = $body;
483
+ $this->mailer->send();
484
+ return true;
485
+ } catch (Exception $e) {
486
+ $isDev = (defined('APP_ENV') && APP_ENV == 1);
487
+ error_log('EmailService Hatası: ' . $e->getMessage());
488
+ if ($isDev) {
489
+ error_log('PHPMailer Detay: ' . $this->mailer->ErrorInfo);
490
+ }
491
+ return false;
492
+ }
493
+ }
494
+ }
495
+ EMAILPHP;
496
+ file_put_contents($targetDir . '/src/Email/EmailService.php', $emailServiceContent);
497
+ $this->tick('EmailService.php');
498
+
499
+ // src/Service/RedisService.php
500
+ $redisServiceContent = <<<'REDISPHP'
501
+ <?php
502
+ /**
503
+ * ArtiFrame Core Engine
504
+ *
505
+ * @package ArtiFrame
506
+ * @author Artilingo
507
+ * @license AGPLv3 (Attribution-ShareAlike Required)
508
+ * @link https://artiframe.org
509
+ */
510
+
511
+ namespace Src\Service;
512
+
513
+ use Predis\Client as RedisClient;
514
+
515
+ class RedisService
516
+ {
517
+ private static ?RedisClient $instance = null;
518
+
519
+ public static function getInstance(): RedisClient
520
+ {
521
+ if (self::$instance === null) {
522
+ self::$instance = new RedisClient([
523
+ 'scheme' => 'tcp',
524
+ 'host' => $_ENV['REDIS_HOST'] ?? '127.0.0.1',
525
+ 'port' => (int) ($_ENV['REDIS_PORT'] ?? 6379),
526
+ 'password' => $_ENV['REDIS_PASSWORD'] ?: null,
527
+ 'database' => (int) ($_ENV['REDIS_DB'] ?? 0),
528
+ ]);
529
+ }
530
+ return self::$instance;
531
+ }
532
+
533
+ /** Değer yazar. Opsiyonel TTL (saniye cinsinden). */
534
+ public static function set(string $key, mixed $value, int $ttl = 0): void
535
+ {
536
+ $client = self::getInstance();
537
+ $encoded = is_array($value) ? json_encode($value) : $value;
538
+ $ttl > 0 ? $client->setex($key, $ttl, $encoded) : $client->set($key, $encoded);
539
+ }
540
+
541
+ /** Değer okur. JSON ise otomatik çözümler, yoksa null döner. */
542
+ public static function get(string $key): mixed
543
+ {
544
+ $value = self::getInstance()->get($key);
545
+ if ($value === null) return null;
546
+ $decoded = json_decode($value, true);
547
+ return (json_last_error() === JSON_ERROR_NONE) ? $decoded : $value;
548
+ }
549
+
550
+ /** Anahtarı siler. */
551
+ public static function delete(string $key): void
552
+ {
553
+ self::getInstance()->del($key);
554
+ }
555
+
556
+ /** Anahtarın var olup olmadığını kontrol eder. */
557
+ public static function exists(string $key): bool
558
+ {
559
+ return (bool) self::getInstance()->exists($key);
560
+ }
561
+ }
562
+ REDISPHP;
563
+ file_put_contents($targetDir . '/src/Service/RedisService.php', $redisServiceContent);
564
+ $this->tick('RedisService.php');
565
+ file_put_contents($targetDir . '/LICENSE', "GNU AFFERO GENERAL PUBLIC LICENSE Version 3\n");
566
+ $this->tick('LICENSE');
567
+
568
+ // public/index.php — localized welcome page
569
+ $lang = $this->translator->getLang();
570
+ $docFile = [
571
+ 'tr' => ['file' => 'kilavuz.html', 'installed' => 'ArtiFrame Başarıyla Kuruldu!', 'guide' => 'Başlamak için'],
572
+ 'en' => ['file' => 'guide.html', 'installed' => 'ArtiFrame Installed Successfully!', 'guide' => 'To get started, see'],
573
+ 'de' => ['file' => 'handbuch.html', 'installed' => 'ArtiFrame Erfolgreich Installiert!', 'guide' => 'Für den Einstieg, siehe'],
574
+ 'fr' => ['file' => 'guide_fr.html', 'installed' => 'ArtiFrame Installé avec Succès !', 'guide' => 'Pour commencer, consultez'],
575
+ 'es' => ['file' => 'guia.html', 'installed' => '¡ArtiFrame Instalado Exitosamente!', 'guide' => 'Para comenzar, vea'],
576
+ ][$lang] ?? ['file' => 'guide.html', 'installed' => 'ArtiFrame Installed Successfully!', 'guide' => 'To get started, see'];
577
+
578
+ $indexContent = "<?php\nrequire_once __DIR__ . '/../app/ViewControl.php';\n?>\n"
579
+ . "<!DOCTYPE html>\n"
580
+ . "<html lang=\"{$lang}\" data-theme=\"default\" data-mode=\"light\">\n"
581
+ . "<head>\n"
582
+ . " <?php require_once __DIR__ . '/includes/head.php'; ?>\n"
583
+ . " <title>ArtiFrame | Welcome</title>\n"
584
+ . "</head>\n"
585
+ . "<body>\n"
586
+ . " <main style=\"display:flex;justify-content:center;align-items:center;height:100vh;flex-direction:column;font-family:'Inter',sans-serif;\">\n"
587
+ . " <h1 style=\"background:linear-gradient(to right,#3b82f6,#8b5cf6);-webkit-background-clip:text;-webkit-text-fill-color:transparent;\">\n"
588
+ . " {$docFile['installed']}\n"
589
+ . " </h1>\n"
590
+ . " <p>{$docFile['guide']} <a href=\"/{$docFile['file']}\">{$docFile['file']}</a>.</p>\n"
591
+ . " </main>\n"
592
+ . "</body>\n"
593
+ . "</html>\n";
594
+ file_put_contents($targetDir . '/public/index.php', $indexContent);
595
+ $this->tick('public/index.php');
596
+
597
+ // public/.htaccess
598
+ $htaccessContent = <<<HTACCESS
599
+ RewriteEngine On
600
+ RewriteCond %{REQUEST_FILENAME} !-f
601
+ RewriteCond %{REQUEST_FILENAME} !-d
602
+ RewriteRule ^(.*)$ index.php [QSA,L]
603
+ HTACCESS;
604
+ file_put_contents($targetDir . '/public/.htaccess', $htaccessContent);
605
+ $this->tick('public/.htaccess');
606
+
607
+ // public/site.webmanifest
608
+ $manifestContent = <<<MANIFEST
609
+ {
610
+ "name": "ArtiFrame App",
611
+ "short_name": "ArtiFrame",
612
+ "start_url": "/",
613
+ "display": "standalone",
614
+ "background_color": "#ffffff",
615
+ "theme_color": "#3b82f6"
616
+ }
617
+ MANIFEST;
618
+ file_put_contents($targetDir . '/public/site.webmanifest', $manifestContent);
619
+ $this->tick('public/site.webmanifest');
620
+
621
+ // .agents/AGENTS.md
622
+ file_put_contents($targetDir . '/.agents/AGENTS.md', "# AI Agent Context Rules\n\nArtiFrame proje standartları...");
623
+ $this->tick('.agents/AGENTS.md');
624
+ }
625
+
626
+ // ─── Composer Install ─────────────────────────────────────
627
+
628
+ private function runComposerInstall(string $targetDir): void
629
+ {
630
+ $command = "cd " . escapeshellarg($targetDir) . " && composer install";
631
+ passthru($command);
632
+ }
633
+ }