prosicht 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -3
- package/index.js +91 -20
- package/lib/files.js +87 -0
- package/lib/layout.js +31 -0
- package/lib/migrate.js +72 -0
- package/lib/publish.js +179 -0
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# prosicht
|
|
2
2
|
|
|
3
|
-
Pro Sicht AI
|
|
3
|
+
Pro Sicht AI standartlarını, sunum ve blog altyapısını projeye indiren CLI. Şablonlar [prosicht/ai-standards](https://github.com/prosicht/ai-standards) deposundan canlı olarak çekilir.
|
|
4
4
|
|
|
5
5
|
## Kullanım
|
|
6
6
|
|
|
@@ -10,18 +10,33 @@ npx prosicht init
|
|
|
10
10
|
Önce ne yapmak istediğinizi sorar:
|
|
11
11
|
- **Uygulama:** Proje şablonunu seçersiniz (web, landing, mobile, backend); kurallar `AGENTS.md` olarak iner.
|
|
12
12
|
- **Sunum:** Etkileşimli, animasyonlu sunum altyapısı (çekirdek dosyalar, `AGENTS.md` ve çalışan bir örnek deste) bulunduğunuz klasöre iner.
|
|
13
|
+
- **Blog:** Etkileşimli, animasyonlu blog yazısı altyapısı (çekirdek dosyalar, `AGENTS.md` ve çalışan bir örnek yazı) bulunduğunuz klasöre iner.
|
|
13
14
|
|
|
14
15
|
Şablonu doğrudan vermek için:
|
|
15
16
|
```bash
|
|
16
17
|
npx prosicht init -t web
|
|
17
18
|
npx prosicht init -t presentation
|
|
19
|
+
npx prosicht init -t blog
|
|
18
20
|
```
|
|
19
21
|
|
|
20
22
|
Güncelleme (şablon, `AGENTS.md` başındaki etiketten otomatik tespit edilir):
|
|
21
23
|
```bash
|
|
22
24
|
npx prosicht update
|
|
23
25
|
```
|
|
24
|
-
Sunum klasörlerinde `update` yalnızca çekirdek dosyaları yeniler; `scenes.js`, `sims/`, `
|
|
26
|
+
Sunum klasörlerinde `update` yalnızca çekirdek dosyaları (`index.html`, `css/core/`, `js/core/`, `AGENTS.md`) yeniler; `js/scenes.js`, `js/sims/`, `css/theme.css`, `css/sims/`, `assets/` ve `sources.md` dokunulmadan kalır. Blog klasörlerinde de yalnızca çekirdek yenilenir (`js/post.js`, `js/sims/`, `css/theme.css`, `css/sims/`, `assets/`, `sources.md` dokunulmaz). Eski (1.x) klasör yapısındaki sunumlar onayla yeni yapıya taşınır (`--yes` sormadan taşır). Sunum ve blog aynı çekirdek yollarını kullanır: bir kitin klasörüne diğeri yazılmadan önce onay istenir, `--yes` ile bu durumda işlem yapılmaz.
|
|
27
|
+
|
|
28
|
+
## Yayınlama
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx prosicht publish
|
|
32
|
+
```
|
|
33
|
+
1. deck.prosicht.com'da hesap açın ve **Yeni sunum ekle** ile 32 karakterlik bir key alın.
|
|
34
|
+
2. Sunum veya blog klasöründe `npx prosicht publish` çalıştırın: **Sunum** / **Blog** (klasörden tespit edilip önceden seçilir) → adres (varsayılan `deck.prosicht.com`) → key.
|
|
35
|
+
3. Link terminalde verilir. Key ve adres isterseniz `.prosicht/publish.json` içine kaydedilir (git dışı); sonraki yayınlar sormaz.
|
|
36
|
+
|
|
37
|
+
Yalnızca `index.html`, `css/`, `js/` ve `assets/` yüklenir (en fazla 50 MB, dosya başına 25 MB, 500 dosya). `/` ile başlayan yollar için uyarı verilir. Kayıtlı key panelde yenilendiyse CLI yenisini sorar.
|
|
38
|
+
|
|
39
|
+
Seçenekler: `--key <key>`, `--host <url>`, `-y` (soruları atla; tür klasörden tespit edilir).
|
|
25
40
|
|
|
26
41
|
## Sunum hazırlama
|
|
27
42
|
|
|
@@ -32,12 +47,22 @@ python3 -m http.server 8000 # http://localhost:8000
|
|
|
32
47
|
```
|
|
33
48
|
Ardından AI aracına konuyu söyleyin: *"KOBİ'ler için yapay zekâ konusunda 10 dakikalık sunum hazırla."*
|
|
34
49
|
|
|
50
|
+
## Blog yazısı hazırlama
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
mkdir yeni-yazi && cd yeni-yazi
|
|
54
|
+
npx prosicht init -t blog
|
|
55
|
+
python3 -m http.server 8000 # http://localhost:8000
|
|
56
|
+
```
|
|
57
|
+
Görselleri `assets/images/` içine koyun ya da yolunu verin, sonra AI aracına konuyu söyleyin: *"Ev kahvesi demleme yöntemleri hakkında, gorseller/ klasöründeki fotoğrafları kullanarak etkileşimli bir blog yazısı hazırla."*
|
|
58
|
+
|
|
35
59
|
## Geliştirme
|
|
36
60
|
|
|
37
|
-
Şablonları push etmeden yerel bir sunucudan denemek için:
|
|
61
|
+
Şablonları push etmeden yerel bir sunucudan denemek için (`PROSICHT_PUBLISH_URL` aynı şekilde yayın adresini değiştirir):
|
|
38
62
|
```bash
|
|
39
63
|
cd ai-standards/templates && python3 -m http.server 8765
|
|
40
64
|
PROSICHT_TEMPLATES_URL=http://127.0.0.1:8765 node /path/to/prosicht-cli/index.js init -t presentation
|
|
65
|
+
PROSICHT_TEMPLATES_URL=http://127.0.0.1:8765 node /path/to/prosicht-cli/index.js init -t blog
|
|
41
66
|
```
|
|
42
67
|
|
|
43
68
|
Node.js 18 veya üstü gerekir.
|
package/index.js
CHANGED
|
@@ -5,13 +5,17 @@ const path = require('path');
|
|
|
5
5
|
const { program } = require('commander');
|
|
6
6
|
const prompts = require('prompts');
|
|
7
7
|
const { version } = require('./package.json');
|
|
8
|
+
const { detectLayout } = require('./lib/layout');
|
|
9
|
+
const { migrateV1 } = require('./lib/migrate');
|
|
10
|
+
const { publish } = require('./lib/publish');
|
|
8
11
|
|
|
9
12
|
// PROSICHT_TEMPLATES_URL: sablonlari push etmeden yerel bir sunucudan denemek icin
|
|
10
13
|
const BASE_URL = (process.env.PROSICHT_TEMPLATES_URL || 'https://raw.githubusercontent.com/ProSicht/ai-standards/main/templates').replace(/\/+$/, '');
|
|
11
14
|
|
|
12
15
|
const KINDS = {
|
|
13
16
|
app: { title: 'Uygulama', description: 'Proje turune gore AGENTS.md (web, landing, mobile, backend)' },
|
|
14
|
-
presentation: { title: 'Sunum', description: 'Etkilesimli, animasyonlu sunum altyapisi + AGENTS.md' }
|
|
17
|
+
presentation: { title: 'Sunum', description: 'Etkilesimli, animasyonlu sunum altyapisi + AGENTS.md' },
|
|
18
|
+
blog: { title: 'Blog', description: 'Etkilesimli, animasyonlu blog yazisi altyapisi + AGENTS.md' }
|
|
15
19
|
};
|
|
16
20
|
|
|
17
21
|
const APP_TEMPLATES = {
|
|
@@ -21,10 +25,35 @@ const APP_TEMPLATES = {
|
|
|
21
25
|
backend: { name: 'Backend API Service', file: 'backend.md' }
|
|
22
26
|
};
|
|
23
27
|
|
|
24
|
-
// Sunum tek dosya degil, klasor: dosya listesi templates
|
|
25
|
-
|
|
28
|
+
// Sunum ve blog tek dosya degil, klasor: dosya listesi templates/<dir>/manifest.json icinde.
|
|
29
|
+
// owned: update'in dokunmadigi icerik dosyalari; migrate: kit 1.x klasorleri tasinabilir
|
|
30
|
+
const KITS = {
|
|
31
|
+
presentation: {
|
|
32
|
+
name: 'Sunum altyapisi',
|
|
33
|
+
dir: 'presentation',
|
|
34
|
+
migrate: true,
|
|
35
|
+
owned: 'Sunuma ait dosyalar (js/scenes.js, js/sims/, css/theme.css, css/sims/, assets/, sources.md) degistirilmedi.',
|
|
36
|
+
nextSteps: [
|
|
37
|
+
'1. python3 -m http.server 8000 -> http://localhost:8000 (ornek sunum)',
|
|
38
|
+
'2. AI aracina konuyu soyleyin: "<konu> hakkinda 10 dakikalik sunum hazirla"',
|
|
39
|
+
'3. npx prosicht publish -> deck.prosicht.com uzerinde yayinlayin'
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
blog: {
|
|
43
|
+
name: 'Blog altyapisi',
|
|
44
|
+
dir: 'blog',
|
|
45
|
+
migrate: false,
|
|
46
|
+
owned: 'Yaziya ait dosyalar (js/post.js, js/sims/, css/theme.css, css/sims/, assets/, sources.md) degistirilmedi.',
|
|
47
|
+
nextSteps: [
|
|
48
|
+
'1. python3 -m http.server 8000 -> http://localhost:8000 (ornek yazi)',
|
|
49
|
+
'2. Gorselleri assets/images/ icine koyun ya da AI aracina yolunu verin',
|
|
50
|
+
'3. AI aracina konuyu soyleyin: "<konu> hakkinda, gorselleri kullanarak etkilesimli bir blog yazisi hazirla"',
|
|
51
|
+
'4. npx prosicht publish -> deck.prosicht.com uzerinde yayinlayin'
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
};
|
|
26
55
|
|
|
27
|
-
const TEMPLATE_KEYS = [...Object.keys(APP_TEMPLATES),
|
|
56
|
+
const TEMPLATE_KEYS = [...Object.keys(APP_TEMPLATES), ...Object.keys(KITS)];
|
|
28
57
|
const TAG_PATTERN = /<!--\s*template:\s*(\w+)\s*-->\n?/;
|
|
29
58
|
|
|
30
59
|
// Projedeki mevcut AGENTS.md dosyasından şablon türünü okur
|
|
@@ -77,11 +106,11 @@ async function askAppTemplate(message) {
|
|
|
77
106
|
return response.template;
|
|
78
107
|
}
|
|
79
108
|
|
|
80
|
-
// Önce tür (Uygulama / Sunum), uygulamaysa ardından şablon sorulur
|
|
109
|
+
// Önce tür (Uygulama / Sunum / Blog), uygulamaysa ardından şablon sorulur
|
|
81
110
|
async function askTemplate(message) {
|
|
82
111
|
const kind = await askKind();
|
|
83
112
|
if (!kind) return null;
|
|
84
|
-
if (kind
|
|
113
|
+
if (KITS[kind]) return kind;
|
|
85
114
|
return askAppTemplate(message);
|
|
86
115
|
}
|
|
87
116
|
|
|
@@ -113,11 +142,34 @@ function resolveInsideCwd(relPath) {
|
|
|
113
142
|
return target;
|
|
114
143
|
}
|
|
115
144
|
|
|
145
|
+
async function confirm(message, initial = true) {
|
|
146
|
+
const response = await prompts({ type: 'confirm', name: 'ok', initial, message });
|
|
147
|
+
return Boolean(response.ok);
|
|
148
|
+
}
|
|
149
|
+
|
|
116
150
|
// init: çekirdek + başlangıç dosyaları (mevcut başlangıç dosyaları korunur)
|
|
117
|
-
// update: yalnızca çekirdek dosyalar; sunum içeriğine dokunulmaz
|
|
118
|
-
async function
|
|
119
|
-
const
|
|
120
|
-
|
|
151
|
+
// update: yalnızca çekirdek dosyalar; sunum / yazı içeriğine dokunulmaz
|
|
152
|
+
async function installKit(key, mode, options = {}) {
|
|
153
|
+
const kit = KITS[key];
|
|
154
|
+
const base = `${BASE_URL}/${kit.dir}`;
|
|
155
|
+
|
|
156
|
+
// Sunum ve blog aynı çekirdek yollarını kullanır (index.html, js/core/app.js):
|
|
157
|
+
// başka bir kitin klasörüne yazmak onu bozar
|
|
158
|
+
const existing = detectExistingTemplate();
|
|
159
|
+
if (existing && existing !== key && (KITS[existing] || detectLayout(process.cwd()))) {
|
|
160
|
+
const label = KITS[existing] ? KITS[existing].name : `"${existing}" sablonu`;
|
|
161
|
+
if (options.yes) {
|
|
162
|
+
console.error(`Hata: Bu klasorde ${label} var; ${kit.name} cekirdek dosyalari ustune yazilmadi.`);
|
|
163
|
+
process.exitCode = 1;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (!(await confirm(`Bu klasorde ${label} var. ${kit.name} cekirdek dosyalari (index.html, js/core/ ...) ustune yazilsin mi?`, false))) {
|
|
167
|
+
console.log('Iptal edildi; hicbir dosya degistirilmedi.\n');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
console.log(`\n[Pro Sicht] ${kit.name} ${mode === 'update' ? 'guncelleniyor' : 'indiriliyor'}...`);
|
|
121
173
|
|
|
122
174
|
try {
|
|
123
175
|
const manifest = JSON.parse((await download(`${base}/manifest.json`)).toString('utf8'));
|
|
@@ -136,11 +188,21 @@ async function installPresentation(mode) {
|
|
|
136
188
|
const url = `${base}/${job.file.split('/').map(encodeURIComponent).join('/')}`;
|
|
137
189
|
let data = await download(url);
|
|
138
190
|
if (job.file === 'AGENTS.md') {
|
|
139
|
-
data = Buffer.from(withTemplateTag(data.toString('utf8'),
|
|
191
|
+
data = Buffer.from(withTemplateTag(data.toString('utf8'), key), 'utf8');
|
|
140
192
|
}
|
|
141
193
|
return { ...job, data };
|
|
142
194
|
}));
|
|
143
195
|
|
|
196
|
+
// update: kit 1.x klasörünü önce yeni yapıya taşı (dosyalar indikten sonra, onayla).
|
|
197
|
+
// Yalnızca uzaktaki kit de yeni yapıdaysa: eski manifest eski yollara yazar.
|
|
198
|
+
if (kit.migrate && mode === 'update' && detectLayout(process.cwd()) === 1 && Number(manifest.layout) >= 2) {
|
|
199
|
+
if (!options.yes && !(await confirm('Bu sunum eski klasor yapisinda (kit 1.x). Yeni yapiya (index.html + css/ js/ assets/) tasinsin mi?'))) {
|
|
200
|
+
console.log('Iptal edildi; hicbir dosya degistirilmedi.\n');
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
migrateV1(process.cwd());
|
|
204
|
+
}
|
|
205
|
+
|
|
144
206
|
for (const file of files) {
|
|
145
207
|
fs.mkdirSync(path.dirname(file.target), { recursive: true });
|
|
146
208
|
fs.writeFileSync(file.target, file.data);
|
|
@@ -148,13 +210,13 @@ async function installPresentation(mode) {
|
|
|
148
210
|
}
|
|
149
211
|
for (const job of kept) console.log(` = ${job.file} (mevcut, korundu)`);
|
|
150
212
|
|
|
151
|
-
console.log(`Basarili: ${files.length} dosya yazildi (
|
|
213
|
+
console.log(`Basarili: ${files.length} dosya yazildi (${kit.name} v${manifest.version || '?'}).`);
|
|
152
214
|
if (mode === 'init') {
|
|
153
215
|
console.log('\nSonraki adimlar:');
|
|
154
|
-
|
|
155
|
-
console.log('
|
|
216
|
+
for (const step of kit.nextSteps) console.log(` ${step}`);
|
|
217
|
+
console.log('');
|
|
156
218
|
} else {
|
|
157
|
-
console.log(
|
|
219
|
+
console.log(`${kit.owned}\n`);
|
|
158
220
|
}
|
|
159
221
|
} catch (error) {
|
|
160
222
|
console.error('Indirme hatasi:', error.message);
|
|
@@ -162,12 +224,12 @@ async function installPresentation(mode) {
|
|
|
162
224
|
}
|
|
163
225
|
}
|
|
164
226
|
|
|
165
|
-
async function install(templateKey, mode) {
|
|
227
|
+
async function install(templateKey, mode, options = {}) {
|
|
166
228
|
if (!TEMPLATE_KEYS.includes(templateKey)) {
|
|
167
229
|
console.error(`Hata: Gecersiz sablon "${templateKey}". Gecerli: ${TEMPLATE_KEYS.join(', ')}`);
|
|
168
230
|
process.exit(1);
|
|
169
231
|
}
|
|
170
|
-
if (templateKey
|
|
232
|
+
if (KITS[templateKey]) return installKit(templateKey, mode, options);
|
|
171
233
|
return fetchAndSaveTemplate(templateKey);
|
|
172
234
|
}
|
|
173
235
|
|
|
@@ -178,7 +240,7 @@ program
|
|
|
178
240
|
|
|
179
241
|
program
|
|
180
242
|
.command('init')
|
|
181
|
-
.description('Projeye AI standartlarini (AGENTS.md) veya
|
|
243
|
+
.description('Projeye AI standartlarini (AGENTS.md), sunum veya blog altyapisini indirir')
|
|
182
244
|
.option('-t, --template <type>', `Sablon turu (${TEMPLATE_KEYS.join(', ')})`)
|
|
183
245
|
.action(async (options) => {
|
|
184
246
|
let templateKey = options.template;
|
|
@@ -193,8 +255,9 @@ program
|
|
|
193
255
|
|
|
194
256
|
program
|
|
195
257
|
.command('update')
|
|
196
|
-
.description('Projedeki AGENTS.md (ve sunum altyapisinin cekirdek dosyalarini) tespit edip gunceller')
|
|
258
|
+
.description('Projedeki AGENTS.md (ve sunum / blog altyapisinin cekirdek dosyalarini) tespit edip gunceller')
|
|
197
259
|
.option('-t, --template <type>', 'Sablon turunu zorla belirle')
|
|
260
|
+
.option('-y, --yes', 'Sormadan devam et: eski sunum yapisini yeni yapiya tasir')
|
|
198
261
|
.action(async (options) => {
|
|
199
262
|
let templateKey = options.template;
|
|
200
263
|
|
|
@@ -210,7 +273,15 @@ program
|
|
|
210
273
|
}
|
|
211
274
|
}
|
|
212
275
|
|
|
213
|
-
await install(templateKey, 'update');
|
|
276
|
+
await install(templateKey, 'update', { yes: options.yes });
|
|
214
277
|
});
|
|
215
278
|
|
|
279
|
+
program
|
|
280
|
+
.command('publish')
|
|
281
|
+
.description('Sunumu veya blog yazisini deck.prosicht.com uzerinde yayinlar ve linki verir')
|
|
282
|
+
.option('-k, --key <key>', 'Panelden alinan 32 karakterlik yayin keyi')
|
|
283
|
+
.option('--host <url>', 'Yayin adresi (varsayilan: https://deck.prosicht.com)')
|
|
284
|
+
.option('-y, --yes', 'Sorulari atla: tur klasorden tespit edilir, key kaydedilir')
|
|
285
|
+
.action((options) => publish(options));
|
|
286
|
+
|
|
216
287
|
program.parse(process.argv);
|
package/lib/files.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
// Yayına giden dosyalar: giriş noktası + üç klasör. Sunucu aynı kuralları uygular.
|
|
5
|
+
const PUBLISH_ROOTS = ['index.html', 'css', 'js', 'assets'];
|
|
6
|
+
const LIMITS = { totalBytes: 50 * 1024 * 1024, fileBytes: 25 * 1024 * 1024, files: 500 };
|
|
7
|
+
|
|
8
|
+
const MIME_TYPES = {
|
|
9
|
+
'.html': 'text/html', '.css': 'text/css', '.js': 'text/javascript', '.mjs': 'text/javascript',
|
|
10
|
+
'.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg',
|
|
11
|
+
'.jpeg': 'image/jpeg', '.webp': 'image/webp', '.avif': 'image/avif', '.gif': 'image/gif',
|
|
12
|
+
'.ico': 'image/x-icon', '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf',
|
|
13
|
+
'.otf': 'font/otf', '.mp4': 'video/mp4', '.webm': 'video/webm', '.mp3': 'audio/mpeg',
|
|
14
|
+
'.wav': 'audio/wav', '.ogg': 'audio/ogg', '.txt': 'text/plain', '.csv': 'text/csv', '.vtt': 'text/vtt'
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function walk(root, rel, out) {
|
|
18
|
+
const abs = path.join(root, rel);
|
|
19
|
+
if (!fs.existsSync(abs)) return;
|
|
20
|
+
const stat = fs.lstatSync(abs);
|
|
21
|
+
if (stat.isSymbolicLink()) return;
|
|
22
|
+
if (stat.isFile()) {
|
|
23
|
+
out.push({ path: rel.split(path.sep).join('/'), abs, size: stat.size });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
for (const name of fs.readdirSync(abs).sort()) {
|
|
27
|
+
if (name.startsWith('.')) continue; // .gitkeep, .DS_Store
|
|
28
|
+
walk(root, path.join(rel, name), out);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Yayınlanacak dosyaları toplar ve kurallara göre denetler
|
|
33
|
+
function collectFiles(root) {
|
|
34
|
+
const files = [];
|
|
35
|
+
for (const entry of PUBLISH_ROOTS) walk(root, entry, files);
|
|
36
|
+
const errors = [];
|
|
37
|
+
if (!files.some((f) => f.path === 'index.html')) errors.push('index.html bulunamadi (giris noktasi zorunlu)');
|
|
38
|
+
for (const f of files) {
|
|
39
|
+
const ext = path.extname(f.path).toLowerCase();
|
|
40
|
+
if (!MIME_TYPES[ext]) errors.push(`${f.path}: izin verilmeyen dosya turu`);
|
|
41
|
+
else if (ext === '.html' && f.path !== 'index.html') errors.push(`${f.path}: yalnizca kokteki index.html yayinlanir`);
|
|
42
|
+
if (f.size > LIMITS.fileBytes) errors.push(`${f.path}: dosya 25 MB sinirini asiyor`);
|
|
43
|
+
}
|
|
44
|
+
const total = files.reduce((sum, f) => sum + f.size, 0);
|
|
45
|
+
if (files.length > LIMITS.files) errors.push(`${files.length} dosya: en fazla ${LIMITS.files}`);
|
|
46
|
+
if (total > LIMITS.totalBytes) errors.push(`toplam ${formatBytes(total)}: en fazla 50 MB`);
|
|
47
|
+
return { files, total, errors };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Yayında alt yolda servis edildiği için / ile başlayan yollar kırılır
|
|
51
|
+
function findAbsolutePaths(files) {
|
|
52
|
+
const pattern = /(?:\b(?:src|href)\s*=\s*["']|url\(\s*["']?|\bfrom\s+["']|\bimport\(\s*["'])\/(?!\/)/;
|
|
53
|
+
const hits = [];
|
|
54
|
+
for (const f of files) {
|
|
55
|
+
if (!/\.(html|css|m?js)$/.test(f.path)) continue;
|
|
56
|
+
const lines = fs.readFileSync(f.abs, 'utf8').split('\n');
|
|
57
|
+
lines.forEach((line, i) => { if (pattern.test(line)) hits.push(`${f.path}:${i + 1}`); });
|
|
58
|
+
}
|
|
59
|
+
return hits;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Başlık panelde ve paylaşım önizlemesinde görünür: sunumda js/scenes.js
|
|
63
|
+
// içindeki deck.title, blogda js/post.js içindeki post.title (ilk title alanı)
|
|
64
|
+
const CONTENT = { presentation: ['scenes.js', 'deck'], blog: ['post.js', 'post'] };
|
|
65
|
+
|
|
66
|
+
function readTitle(root, kind = 'presentation') {
|
|
67
|
+
const [fileName, name] = CONTENT[kind] || CONTENT.presentation;
|
|
68
|
+
const file = path.join(root, 'js', fileName);
|
|
69
|
+
if (!fs.existsSync(file)) return null;
|
|
70
|
+
const source = fs.readFileSync(file, 'utf8');
|
|
71
|
+
// önce satır başındaki kapanış (iç içe nesneler), sonra tek satırlık yazım
|
|
72
|
+
const head = `export\\s+const\\s+${name}\\s*=\\s*\\{([\\s\\S]*?)`;
|
|
73
|
+
const block = source.match(new RegExp(`${head}\\n\\};`)) || source.match(new RegExp(`${head}\\};`));
|
|
74
|
+
const title = block && block[1].match(/\btitle\s*:\s*(['"`])((?:(?!\1).)*)\1/);
|
|
75
|
+
if (!title) return null;
|
|
76
|
+
// <br> ve *vurgu* işaretleri başlıkta görünmesin
|
|
77
|
+
const plain = title[2].replace(/<br\s*\/?>/gi, ' ').replace(/\*/g, '').replace(/\s+/g, ' ').trim();
|
|
78
|
+
return plain.slice(0, 120) || null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function formatBytes(n) {
|
|
82
|
+
if (n < 1024) return `${n} B`;
|
|
83
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(0)} KB`;
|
|
84
|
+
return `${(n / 1024 / 1024).toFixed(1)} MB`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = { collectFiles, findAbsolutePaths, readTitle, formatBytes, MIME_TYPES };
|
package/lib/layout.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
// Kit klasör yapıları: 1 = kök dizinde app.js/sims/styles (sunum kit 1.x),
|
|
5
|
+
// 2 = index.html + css/ js/ assets/ (sunum kit 2.x ve blog). 0 = kit değil.
|
|
6
|
+
function detectLayout(root) {
|
|
7
|
+
const has = (rel) => fs.existsSync(path.join(root, rel));
|
|
8
|
+
if (has('js/core/app.js')) return 2;
|
|
9
|
+
if (has('app.js') && has('sims/engine.js')) return 1;
|
|
10
|
+
return 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// 2.x klasörünün türü: içerik dosyasından (blog: js/post.js, sunum: js/scenes.js)
|
|
14
|
+
function detectKit(root) {
|
|
15
|
+
const has = (rel) => fs.existsSync(path.join(root, rel));
|
|
16
|
+
if (has('js/post.js')) return 'blog';
|
|
17
|
+
if (has('js/scenes.js')) return 'presentation';
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Proje klasöründeki .gitignore'a satır ekler (yoksa oluşturur)
|
|
22
|
+
function ensureGitignore(root, line) {
|
|
23
|
+
const file = path.join(root, '.gitignore');
|
|
24
|
+
const current = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
25
|
+
if (current.split(/\r?\n/).some((l) => l.trim() === line)) return false;
|
|
26
|
+
const prefix = current && !current.endsWith('\n') ? '\n' : '';
|
|
27
|
+
fs.writeFileSync(file, `${current}${prefix}${line}\n`, 'utf8');
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = { detectLayout, detectKit, ensureGitignore };
|
package/lib/migrate.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { ensureGitignore } = require('./layout');
|
|
4
|
+
|
|
5
|
+
// Kit 1.x çekirdek dosyaları: 2.x'te js/core/ ve css/core/ altına taşındı,
|
|
6
|
+
// update yenilerini indirdiği için eskileri silinir
|
|
7
|
+
const OLD_CORE = ['app.js', 'sound.js', 'sims/engine.js', 'sims/kit.js', 'styles/shell.css'];
|
|
8
|
+
|
|
9
|
+
// Sunuma ait dosyaların 1.x -> 2.x taşıma planı
|
|
10
|
+
function planMoves(root) {
|
|
11
|
+
const moves = [
|
|
12
|
+
{ from: 'scenes.js', to: 'js/scenes.js' },
|
|
13
|
+
{ from: 'styles/theme.css', to: 'css/theme.css' }
|
|
14
|
+
];
|
|
15
|
+
const simsDir = path.join(root, 'sims');
|
|
16
|
+
if (fs.existsSync(simsDir)) {
|
|
17
|
+
for (const name of fs.readdirSync(simsDir)) {
|
|
18
|
+
if (name === 'engine.js' || name === 'kit.js') continue;
|
|
19
|
+
if (name.endsWith('.js')) moves.push({ from: `sims/${name}`, to: `js/sims/${name}`, rewrite: true });
|
|
20
|
+
else if (name.endsWith('.css')) moves.push({ from: `sims/${name}`, to: `css/sims/${name}` });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return moves.filter((m) => fs.existsSync(path.join(root, m.from)));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Taşınan sim dosyalarında göreli yolları yeni konuma göre düzeltir
|
|
27
|
+
function rewriteSim(source) {
|
|
28
|
+
return source
|
|
29
|
+
.replace(/(['"])\.\/engine\.js\1/g, '$1../core/engine.js$1')
|
|
30
|
+
.replace(/(['"])\.\/kit\.js\1/g, '$1../core/kit.js$1')
|
|
31
|
+
.replace(/(['"`])\.\.\/assets\//g, '$1../../assets/');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function removeIfEmpty(dir) {
|
|
35
|
+
if (fs.existsSync(dir) && fs.readdirSync(dir).filter((n) => n !== '.DS_Store').length === 0) {
|
|
36
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 1.x sunum klasörünü 2.x yapısına taşır; yapılan her işlemi yazdırır
|
|
41
|
+
function migrateV1(root) {
|
|
42
|
+
console.log('\n[Pro Sicht] Sunum yeni klasor yapisina tasiniyor...');
|
|
43
|
+
for (const move of planMoves(root)) {
|
|
44
|
+
const from = path.join(root, move.from);
|
|
45
|
+
const to = path.join(root, move.to);
|
|
46
|
+
if (fs.existsSync(to)) {
|
|
47
|
+
console.log(` ! ${move.to} zaten var, ${move.from} yerinde birakildi`);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
51
|
+
if (move.rewrite) {
|
|
52
|
+
fs.writeFileSync(to, rewriteSim(fs.readFileSync(from, 'utf8')), 'utf8');
|
|
53
|
+
fs.rmSync(from);
|
|
54
|
+
} else {
|
|
55
|
+
fs.renameSync(from, to);
|
|
56
|
+
}
|
|
57
|
+
console.log(` > ${move.from} -> ${move.to}`);
|
|
58
|
+
}
|
|
59
|
+
for (const rel of OLD_CORE) {
|
|
60
|
+
const file = path.join(root, rel);
|
|
61
|
+
if (fs.existsSync(file)) {
|
|
62
|
+
fs.rmSync(file);
|
|
63
|
+
console.log(` - ${rel} (eski cekirdek dosyasi)`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
removeIfEmpty(path.join(root, 'sims'));
|
|
67
|
+
removeIfEmpty(path.join(root, 'styles'));
|
|
68
|
+
fs.mkdirSync(path.join(root, 'assets'), { recursive: true });
|
|
69
|
+
if (ensureGitignore(root, '.prosicht/')) console.log(' + .gitignore (.prosicht/)');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { migrateV1, planMoves, rewriteSim };
|
package/lib/publish.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const prompts = require('prompts');
|
|
4
|
+
const { detectLayout, detectKit, ensureGitignore } = require('./layout');
|
|
5
|
+
const { collectFiles, findAbsolutePaths, readTitle, formatBytes, MIME_TYPES } = require('./files');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_HOST = (process.env.PROSICHT_PUBLISH_URL || 'https://deck.prosicht.com').replace(/\/+$/, '');
|
|
8
|
+
const KEY_PATTERN = /^[A-Za-z0-9]{32}$/;
|
|
9
|
+
const CONFIG_FILE = path.join('.prosicht', 'publish.json');
|
|
10
|
+
|
|
11
|
+
function fail(message, details = []) {
|
|
12
|
+
console.error(`\nHata: ${message}`);
|
|
13
|
+
for (const line of details) console.error(` - ${line}`);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readConfig(root) {
|
|
18
|
+
try {
|
|
19
|
+
const data = JSON.parse(fs.readFileSync(path.join(root, CONFIG_FILE), 'utf8'));
|
|
20
|
+
return data && typeof data === 'object' ? data : {};
|
|
21
|
+
} catch {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Key yalnızca bu klasörde, git'e girmeyen .prosicht/ altında tutulur
|
|
27
|
+
function saveConfig(root, config) {
|
|
28
|
+
const file = path.join(root, CONFIG_FILE);
|
|
29
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
30
|
+
fs.writeFileSync(file, JSON.stringify(config, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
|
|
31
|
+
ensureGitignore(root, '.prosicht/');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeHost(value) {
|
|
35
|
+
const raw = String(value || '').trim().replace(/\/+$/, '');
|
|
36
|
+
const url = /^https?:\/\//.test(raw) ? raw : `https://${raw}`;
|
|
37
|
+
try {
|
|
38
|
+
const parsed = new URL(url);
|
|
39
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function ask(question) {
|
|
46
|
+
const answer = await prompts(question, { onCancel: () => process.exit(0) });
|
|
47
|
+
return answer[question.name];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function chooseHost(saved, flag) {
|
|
51
|
+
if (flag) return normalizeHost(flag) || fail(`Gecersiz adres: ${flag}`);
|
|
52
|
+
if (saved) return saved;
|
|
53
|
+
const choice = await ask({
|
|
54
|
+
type: 'select', name: 'host', message: 'Yayin adresi',
|
|
55
|
+
choices: [
|
|
56
|
+
{ title: `${DEFAULT_HOST.replace(/^https?:\/\//, '')} (varsayilan)`, value: DEFAULT_HOST },
|
|
57
|
+
{ title: 'Baska bir adres', value: 'custom' }
|
|
58
|
+
]
|
|
59
|
+
});
|
|
60
|
+
if (choice !== 'custom') return choice;
|
|
61
|
+
const custom = await ask({ type: 'text', name: 'url', message: 'Adres (or. https://deck.prosicht.com)', validate: (v) => (normalizeHost(v) ? true : 'Gecerli bir adres girin') });
|
|
62
|
+
return normalizeHost(custom);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function chooseKey(saved, flag) {
|
|
66
|
+
if (flag) return KEY_PATTERN.test(flag) ? flag : fail('Key 32 karakter olmali (harf ve rakam).');
|
|
67
|
+
if (saved && KEY_PATTERN.test(saved)) {
|
|
68
|
+
console.log(`Kayitli key kullaniliyor (....${saved.slice(-4)}). Degistirmek icin: --key`);
|
|
69
|
+
return saved;
|
|
70
|
+
}
|
|
71
|
+
return ask({
|
|
72
|
+
type: 'password', name: 'key', message: 'Lutfen keyi giriniz',
|
|
73
|
+
validate: (v) => (KEY_PATTERN.test(String(v).trim()) ? true : 'Key 32 karakter olmali (harf ve rakam)'),
|
|
74
|
+
format: (v) => String(v).trim()
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function upload(host, key, files, title) {
|
|
79
|
+
const form = new FormData();
|
|
80
|
+
form.append('manifest', JSON.stringify({ title, files: files.map((f) => ({ path: f.path, size: f.size })) }));
|
|
81
|
+
for (const f of files) {
|
|
82
|
+
const type = MIME_TYPES[path.extname(f.path).toLowerCase()];
|
|
83
|
+
form.append('files', new Blob([fs.readFileSync(f.abs)], { type }), path.basename(f.path));
|
|
84
|
+
}
|
|
85
|
+
const { version } = require('../package.json');
|
|
86
|
+
const response = await fetch(`${host}/api/publish`, {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: { Authorization: `Bearer ${key}`, 'User-Agent': `prosicht-cli/${version}` },
|
|
89
|
+
body: form
|
|
90
|
+
});
|
|
91
|
+
let body = {};
|
|
92
|
+
try { body = await response.json(); } catch { /* HTML error page from a proxy */ }
|
|
93
|
+
return { status: response.status, body };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function explain(status, body) {
|
|
97
|
+
if (status === 401) fail('Key gecersiz veya yenilenmis. Panelden guncel keyi alin.');
|
|
98
|
+
if (status === 413) fail('Dosyalar boyut sinirini asiyor (en fazla 50 MB).');
|
|
99
|
+
if (status === 400 || status === 422) fail(body.error || 'Dosyalar reddedildi.', body.details || []);
|
|
100
|
+
fail(`Sunucu hatasi (HTTP ${status})${body.error ? `: ${body.error}` : ''}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function publish(options = {}) {
|
|
104
|
+
const root = process.cwd();
|
|
105
|
+
|
|
106
|
+
const detected = detectKit(root);
|
|
107
|
+
const choices = [
|
|
108
|
+
{ title: 'Sunum', value: 'presentation' },
|
|
109
|
+
{ title: 'Blog', value: 'blog' },
|
|
110
|
+
{ title: 'Uygulama', value: 'app', disabled: true, description: 'yakinda' }
|
|
111
|
+
];
|
|
112
|
+
const kind = options.yes ? (detected || 'presentation') : await ask({
|
|
113
|
+
type: 'select', name: 'kind', message: 'Ne yayinlamak istiyorsunuz?',
|
|
114
|
+
choices,
|
|
115
|
+
initial: Math.max(0, choices.findIndex((c) => c.value === detected))
|
|
116
|
+
});
|
|
117
|
+
if (kind !== 'presentation' && kind !== 'blog') process.exit(0);
|
|
118
|
+
const label = kind === 'blog' ? 'blog yazisi' : 'sunum';
|
|
119
|
+
|
|
120
|
+
const layout = detectLayout(root);
|
|
121
|
+
if (layout === 1) fail('Bu sunum eski klasor yapisinda. Once `npx prosicht update` ile yeni yapiya tasiyin.');
|
|
122
|
+
if (layout === 0) fail(`Bu klasorde bir ${label} bulunamadi (index.html ve js/core/app.js gerekli). ${kind === 'blog' ? 'Blog' : 'Sunum'} klasorunde calistirin.`);
|
|
123
|
+
if (detected && detected !== kind) {
|
|
124
|
+
fail(`Bu klasordeki icerik bir ${detected === 'blog' ? 'blog yazisi' : 'sunum'}; "${kind === 'blog' ? 'Blog' : 'Sunum'}" secildi.`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const { files, total, errors } = collectFiles(root);
|
|
128
|
+
if (errors.length) fail('Yayin oncesi kontrol basarisiz:', errors);
|
|
129
|
+
const absolute = findAbsolutePaths(files);
|
|
130
|
+
if (absolute.length) {
|
|
131
|
+
console.log('\nUyari: "/" ile baslayan yollar yayinda calismaz (goreli yol kullanin):');
|
|
132
|
+
for (const hit of absolute.slice(0, 10)) console.log(` - ${hit}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const config = readConfig(root);
|
|
136
|
+
const host = await chooseHost(options.host ? null : config.host, options.host);
|
|
137
|
+
let key = await chooseKey(options.key ? null : config.key, options.key);
|
|
138
|
+
if (!key) process.exit(0);
|
|
139
|
+
|
|
140
|
+
const title = readTitle(root, kind);
|
|
141
|
+
const send = async () => {
|
|
142
|
+
console.log(`\n[Pro Sicht] ${files.length} dosya (${formatBytes(total)}) yukleniyor...`);
|
|
143
|
+
try {
|
|
144
|
+
return await upload(host, key, files, title);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
return fail(`${host} adresine ulasilamadi: ${error.cause?.code || error.message}`);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
let result = await send();
|
|
150
|
+
// kayıtlı key panelden yenilenmişse bir kez yenisini sor ve tekrar dene
|
|
151
|
+
if (result.status === 401 && key === config.key && !options.key && !options.yes) {
|
|
152
|
+
console.log('Kayitli key gecersiz; panelde yenilenmis olabilir.');
|
|
153
|
+
key = await chooseKey(null, null);
|
|
154
|
+
if (!key) process.exit(0);
|
|
155
|
+
result = await send();
|
|
156
|
+
}
|
|
157
|
+
if (result.status !== 200 || !result.body.url) explain(result.status, result.body);
|
|
158
|
+
|
|
159
|
+
const { url, privacy, version } = result.body;
|
|
160
|
+
console.log(`Yayinlandi (surum ${version}): ${url}`);
|
|
161
|
+
if (privacy === 'private') {
|
|
162
|
+
console.log('Gizlilik: yalnizca siz gorebilirsiniz. Paylasmak icin panelden "Baglantiya sahip herkes" secin.');
|
|
163
|
+
} else {
|
|
164
|
+
console.log('Gizlilik: baglantiya sahip herkes gorebilir.');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (config.key !== key || config.host !== host) {
|
|
168
|
+
const remember = options.yes ? true : await ask({
|
|
169
|
+
type: 'confirm', name: 'save', initial: true,
|
|
170
|
+
message: 'Key ve adres bu klasor icin kaydedilsin mi? (.prosicht/, git disi)'
|
|
171
|
+
});
|
|
172
|
+
if (remember) saveConfig(root, { host, key, deckId: result.body.deckId, url });
|
|
173
|
+
} else {
|
|
174
|
+
saveConfig(root, { ...config, deckId: result.body.deckId, url });
|
|
175
|
+
}
|
|
176
|
+
console.log('');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = { publish };
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prosicht",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Pro Sicht AI Standards CLI: AGENTS.md sablonlari ve
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "Pro Sicht AI Standards CLI: AGENTS.md sablonlari, sunum ve blog altyapisi",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"prosicht": "./index.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"index.js"
|
|
10
|
+
"index.js",
|
|
11
|
+
"lib/"
|
|
11
12
|
],
|
|
12
13
|
"engines": {
|
|
13
14
|
"node": ">=18"
|
|
@@ -20,7 +21,8 @@
|
|
|
20
21
|
"agents",
|
|
21
22
|
"cli",
|
|
22
23
|
"prosicht",
|
|
23
|
-
"presentation"
|
|
24
|
+
"presentation",
|
|
25
|
+
"blog"
|
|
24
26
|
],
|
|
25
27
|
"author": "Pro Sicht",
|
|
26
28
|
"license": "MIT",
|