prosicht 1.1.0 → 1.3.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 +56 -0
- package/index.js +177 -52
- package/lib/files.js +78 -0
- package/lib/layout.js +23 -0
- package/lib/migrate.js +72 -0
- package/lib/publish.js +171 -0
- package/package.json +17 -3
- package/tasks/todo.md +0 -14
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# prosicht
|
|
2
|
+
|
|
3
|
+
Pro Sicht AI standartlarını ve sunum altyapısını projeye indiren CLI. Şablonlar [prosicht/ai-standards](https://github.com/prosicht/ai-standards) deposundan canlı olarak çekilir.
|
|
4
|
+
|
|
5
|
+
## Kullanım
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx prosicht init
|
|
9
|
+
```
|
|
10
|
+
Önce ne yapmak istediğinizi sorar:
|
|
11
|
+
- **Uygulama:** Proje şablonunu seçersiniz (web, landing, mobile, backend); kurallar `AGENTS.md` olarak iner.
|
|
12
|
+
- **Sunum:** Etkileşimli, animasyonlu sunum altyapısı (çekirdek dosyalar, `AGENTS.md` ve çalışan bir örnek deste) bulunduğunuz klasöre iner.
|
|
13
|
+
|
|
14
|
+
Şablonu doğrudan vermek için:
|
|
15
|
+
```bash
|
|
16
|
+
npx prosicht init -t web
|
|
17
|
+
npx prosicht init -t presentation
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Güncelleme (şablon, `AGENTS.md` başındaki etiketten otomatik tespit edilir):
|
|
21
|
+
```bash
|
|
22
|
+
npx prosicht update
|
|
23
|
+
```
|
|
24
|
+
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. Eski (1.x) klasör yapısındaki sunumlar onayla yeni yapıya taşınır (`--yes` sormadan taşır).
|
|
25
|
+
|
|
26
|
+
## Yayınlama
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx prosicht publish
|
|
30
|
+
```
|
|
31
|
+
1. deck.prosicht.com'da hesap açın ve **Yeni sunum ekle** ile 32 karakterlik bir key alın.
|
|
32
|
+
2. Sunum klasöründe `npx prosicht publish` çalıştırın: **Sunum** → adres (varsayılan `deck.prosicht.com`) → key.
|
|
33
|
+
3. Link terminalde verilir. Key ve adres isterseniz `.prosicht/publish.json` içine kaydedilir (git dışı); sonraki yayınlar sormaz.
|
|
34
|
+
|
|
35
|
+
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.
|
|
36
|
+
|
|
37
|
+
Seçenekler: `--key <key>`, `--host <url>`, `-y` (soruları atla).
|
|
38
|
+
|
|
39
|
+
## Sunum hazırlama
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
mkdir yeni-sunum && cd yeni-sunum
|
|
43
|
+
npx prosicht init -t presentation
|
|
44
|
+
python3 -m http.server 8000 # http://localhost:8000
|
|
45
|
+
```
|
|
46
|
+
Ardından AI aracına konuyu söyleyin: *"KOBİ'ler için yapay zekâ konusunda 10 dakikalık sunum hazırla."*
|
|
47
|
+
|
|
48
|
+
## Geliştirme
|
|
49
|
+
|
|
50
|
+
Şablonları push etmeden yerel bir sunucudan denemek için (`PROSICHT_PUBLISH_URL` aynı şekilde yayın adresini değiştirir):
|
|
51
|
+
```bash
|
|
52
|
+
cd ai-standards/templates && python3 -m http.server 8765
|
|
53
|
+
PROSICHT_TEMPLATES_URL=http://127.0.0.1:8765 node /path/to/prosicht-cli/index.js init -t presentation
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Node.js 18 veya üstü gerekir.
|
package/index.js
CHANGED
|
@@ -4,96 +4,222 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { program } = require('commander');
|
|
6
6
|
const prompts = require('prompts');
|
|
7
|
+
const { version } = require('./package.json');
|
|
8
|
+
const { detectLayout } = require('./lib/layout');
|
|
9
|
+
const { migrateV1 } = require('./lib/migrate');
|
|
10
|
+
const { publish } = require('./lib/publish');
|
|
7
11
|
|
|
8
|
-
|
|
12
|
+
// PROSICHT_TEMPLATES_URL: sablonlari push etmeden yerel bir sunucudan denemek icin
|
|
13
|
+
const BASE_URL = (process.env.PROSICHT_TEMPLATES_URL || 'https://raw.githubusercontent.com/ProSicht/ai-standards/main/templates').replace(/\/+$/, '');
|
|
9
14
|
|
|
10
|
-
const
|
|
15
|
+
const KINDS = {
|
|
16
|
+
app: { title: 'Uygulama', description: 'Proje turune gore AGENTS.md (web, landing, mobile, backend)' },
|
|
17
|
+
presentation: { title: 'Sunum', description: 'Etkilesimli, animasyonlu sunum altyapisi + AGENTS.md' }
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const APP_TEMPLATES = {
|
|
11
21
|
web: { name: 'Web Application (Next.js, Shadcn, Docker)', file: 'web.md' },
|
|
12
22
|
landing: { name: 'Landing Page (SEO & Speed Focused)', file: 'landing.md' },
|
|
13
23
|
mobile: { name: 'Mobile Application', file: 'mobile.md' },
|
|
14
24
|
backend: { name: 'Backend API Service', file: 'backend.md' }
|
|
15
25
|
};
|
|
16
26
|
|
|
27
|
+
// Sunum tek dosya degil, klasor: dosya listesi templates/presentation/manifest.json icinde
|
|
28
|
+
const PRESENTATION = { key: 'presentation', name: 'Sunum Altyapisi', dir: 'presentation' };
|
|
29
|
+
|
|
30
|
+
const TEMPLATE_KEYS = [...Object.keys(APP_TEMPLATES), PRESENTATION.key];
|
|
31
|
+
const TAG_PATTERN = /<!--\s*template:\s*(\w+)\s*-->\n?/;
|
|
32
|
+
|
|
17
33
|
// Projedeki mevcut AGENTS.md dosyasından şablon türünü okur
|
|
18
34
|
function detectExistingTemplate() {
|
|
19
35
|
const targetPath = path.join(process.cwd(), 'AGENTS.md');
|
|
20
36
|
if (!fs.existsSync(targetPath)) return null;
|
|
21
37
|
|
|
22
38
|
const content = fs.readFileSync(targetPath, 'utf8');
|
|
23
|
-
const match = content.match(
|
|
39
|
+
const match = content.match(TAG_PATTERN);
|
|
24
40
|
return match ? match[1] : null;
|
|
25
41
|
}
|
|
26
42
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
43
|
+
// En üste gizli metadata etiketini ekler; önceki etiket varsa temizler
|
|
44
|
+
function withTemplateTag(content, templateKey) {
|
|
45
|
+
return `<!-- template: ${templateKey} -->\n` + content.replace(TAG_PATTERN, '');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function download(url) {
|
|
49
|
+
const response = await fetch(url);
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
throw new Error(`HTTP ${response.status}: Dosya bulunamadi (${url})`);
|
|
32
52
|
}
|
|
53
|
+
return Buffer.from(await response.arrayBuffer());
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function askKind() {
|
|
57
|
+
const response = await prompts({
|
|
58
|
+
type: 'select',
|
|
59
|
+
name: 'kind',
|
|
60
|
+
message: 'Ne yapmak istiyorsunuz?',
|
|
61
|
+
choices: Object.entries(KINDS).map(([value, kind]) => ({
|
|
62
|
+
title: kind.title,
|
|
63
|
+
description: kind.description,
|
|
64
|
+
value
|
|
65
|
+
}))
|
|
66
|
+
});
|
|
67
|
+
return response.kind;
|
|
68
|
+
}
|
|
33
69
|
|
|
70
|
+
async function askAppTemplate(message) {
|
|
71
|
+
const response = await prompts({
|
|
72
|
+
type: 'select',
|
|
73
|
+
name: 'template',
|
|
74
|
+
message,
|
|
75
|
+
choices: Object.entries(APP_TEMPLATES).map(([key, value]) => ({
|
|
76
|
+
title: value.name,
|
|
77
|
+
value: key
|
|
78
|
+
}))
|
|
79
|
+
});
|
|
80
|
+
return response.template;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Önce tür (Uygulama / Sunum), uygulamaysa ardından şablon sorulur
|
|
84
|
+
async function askTemplate(message) {
|
|
85
|
+
const kind = await askKind();
|
|
86
|
+
if (!kind) return null;
|
|
87
|
+
if (kind === 'presentation') return PRESENTATION.key;
|
|
88
|
+
return askAppTemplate(message);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function fetchAndSaveTemplate(templateKey) {
|
|
92
|
+
const selected = APP_TEMPLATES[templateKey];
|
|
34
93
|
const fileUrl = `${BASE_URL}/${selected.file}`;
|
|
35
94
|
console.log(`\n[Pro Sicht] "${selected.name}" sablonu indiriliyor...`);
|
|
36
95
|
|
|
37
96
|
try {
|
|
38
|
-
const
|
|
39
|
-
if (!response.ok) {
|
|
40
|
-
throw new Error(`HTTP ${response.status}: Dosya bulunamadi (${fileUrl})`);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
let content = await response.text();
|
|
44
|
-
|
|
45
|
-
// En üste gizli metadata etiketini ekle
|
|
46
|
-
const metadataHeader = `<!-- template: ${templateKey} -->\n`;
|
|
47
|
-
|
|
48
|
-
// Eğer dosya önceden indirildiyse eski etiketi temizle, yenisini bas
|
|
49
|
-
content = content.replace(/<!--\s*template:\s*\w+\s*-->\n?/, '');
|
|
50
|
-
const finalContent = metadataHeader + content;
|
|
51
|
-
|
|
97
|
+
const content = (await download(fileUrl)).toString('utf8');
|
|
52
98
|
const targetPath = path.join(process.cwd(), 'AGENTS.md');
|
|
53
|
-
fs.writeFileSync(targetPath,
|
|
54
|
-
|
|
99
|
+
fs.writeFileSync(targetPath, withTemplateTag(content, templateKey), 'utf8');
|
|
100
|
+
|
|
55
101
|
console.log(`Basarili: AGENTS.md kaydedildi! (Sablon: ${templateKey})`);
|
|
56
102
|
console.log(`Konum: ${targetPath}\n`);
|
|
57
|
-
|
|
58
103
|
} catch (error) {
|
|
59
104
|
console.error('Indirme hatasi:', error.message);
|
|
105
|
+
process.exitCode = 1;
|
|
60
106
|
}
|
|
61
107
|
}
|
|
62
108
|
|
|
109
|
+
// Manifest uzaktan gelir: proje klasörünün dışına yazan yolları reddet
|
|
110
|
+
function resolveInsideCwd(relPath) {
|
|
111
|
+
const root = process.cwd();
|
|
112
|
+
const target = path.resolve(root, relPath);
|
|
113
|
+
if (typeof relPath !== 'string' || path.isAbsolute(relPath) || !target.startsWith(root + path.sep)) {
|
|
114
|
+
throw new Error(`Gecersiz dosya yolu: ${relPath}`);
|
|
115
|
+
}
|
|
116
|
+
return target;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// init: çekirdek + başlangıç dosyaları (mevcut başlangıç dosyaları korunur)
|
|
120
|
+
// update: yalnızca çekirdek dosyalar; sunum içeriğine dokunulmaz
|
|
121
|
+
async function confirmMigration() {
|
|
122
|
+
const response = await prompts({
|
|
123
|
+
type: 'confirm',
|
|
124
|
+
name: 'ok',
|
|
125
|
+
initial: true,
|
|
126
|
+
message: 'Bu sunum eski klasor yapisinda (kit 1.x). Yeni yapiya (index.html + css/ js/ assets/) tasinsin mi?'
|
|
127
|
+
});
|
|
128
|
+
return Boolean(response.ok);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function installPresentation(mode, options = {}) {
|
|
132
|
+
const base = `${BASE_URL}/${PRESENTATION.dir}`;
|
|
133
|
+
console.log(`\n[Pro Sicht] Sunum altyapisi ${mode === 'update' ? 'guncelleniyor' : 'indiriliyor'}...`);
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const manifest = JSON.parse((await download(`${base}/manifest.json`)).toString('utf8'));
|
|
137
|
+
const core = Array.isArray(manifest.core) ? manifest.core : [];
|
|
138
|
+
const starter = mode === 'init' && Array.isArray(manifest.starter) ? manifest.starter : [];
|
|
139
|
+
|
|
140
|
+
const jobs = [
|
|
141
|
+
...core.map((file) => ({ file, starter: false })),
|
|
142
|
+
...starter.map((file) => ({ file, starter: true }))
|
|
143
|
+
].map((job) => ({ ...job, target: resolveInsideCwd(job.file) }));
|
|
144
|
+
const kept = jobs.filter((job) => job.starter && fs.existsSync(job.target));
|
|
145
|
+
const toWrite = jobs.filter((job) => !kept.includes(job));
|
|
146
|
+
|
|
147
|
+
// Önce hepsini indir, sonra yaz: ağ hatasında yarım kurulum kalmasın
|
|
148
|
+
const files = await Promise.all(toWrite.map(async (job) => {
|
|
149
|
+
const url = `${base}/${job.file.split('/').map(encodeURIComponent).join('/')}`;
|
|
150
|
+
let data = await download(url);
|
|
151
|
+
if (job.file === 'AGENTS.md') {
|
|
152
|
+
data = Buffer.from(withTemplateTag(data.toString('utf8'), PRESENTATION.key), 'utf8');
|
|
153
|
+
}
|
|
154
|
+
return { ...job, data };
|
|
155
|
+
}));
|
|
156
|
+
|
|
157
|
+
// update: kit 1.x klasörünü önce yeni yapıya taşı (dosyalar indikten sonra, onayla).
|
|
158
|
+
// Yalnızca uzaktaki kit de yeni yapıdaysa: eski manifest eski yollara yazar.
|
|
159
|
+
if (mode === 'update' && detectLayout(process.cwd()) === 1 && Number(manifest.layout) >= 2) {
|
|
160
|
+
if (!options.yes && !(await confirmMigration())) {
|
|
161
|
+
console.log('Iptal edildi; hicbir dosya degistirilmedi.\n');
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
migrateV1(process.cwd());
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
for (const file of files) {
|
|
168
|
+
fs.mkdirSync(path.dirname(file.target), { recursive: true });
|
|
169
|
+
fs.writeFileSync(file.target, file.data);
|
|
170
|
+
console.log(` + ${file.file}`);
|
|
171
|
+
}
|
|
172
|
+
for (const job of kept) console.log(` = ${job.file} (mevcut, korundu)`);
|
|
173
|
+
|
|
174
|
+
console.log(`Basarili: ${files.length} dosya yazildi (Sunum altyapisi v${manifest.version || '?'}).`);
|
|
175
|
+
if (mode === 'init') {
|
|
176
|
+
console.log('\nSonraki adimlar:');
|
|
177
|
+
console.log(' 1. python3 -m http.server 8000 -> http://localhost:8000 (ornek sunum)');
|
|
178
|
+
console.log(' 2. AI aracina konuyu soyleyin: "<konu> hakkinda 10 dakikalik sunum hazirla"');
|
|
179
|
+
console.log(' 3. npx prosicht publish -> deck.prosicht.com uzerinde yayinlayin\n');
|
|
180
|
+
} else {
|
|
181
|
+
console.log('Sunuma ait dosyalar (js/scenes.js, js/sims/, css/theme.css, css/sims/, assets/, sources.md) degistirilmedi.\n');
|
|
182
|
+
}
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error('Indirme hatasi:', error.message);
|
|
185
|
+
process.exitCode = 1;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function install(templateKey, mode, options = {}) {
|
|
190
|
+
if (!TEMPLATE_KEYS.includes(templateKey)) {
|
|
191
|
+
console.error(`Hata: Gecersiz sablon "${templateKey}". Gecerli: ${TEMPLATE_KEYS.join(', ')}`);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
if (templateKey === PRESENTATION.key) return installPresentation(mode, options);
|
|
195
|
+
return fetchAndSaveTemplate(templateKey);
|
|
196
|
+
}
|
|
197
|
+
|
|
63
198
|
program
|
|
64
199
|
.name('prosicht')
|
|
65
200
|
.description('Pro Sicht AI Standards CLI')
|
|
66
|
-
.version(
|
|
201
|
+
.version(version);
|
|
67
202
|
|
|
68
203
|
program
|
|
69
204
|
.command('init')
|
|
70
|
-
.description('Projeye AI
|
|
71
|
-
.option('-t, --template <type>',
|
|
205
|
+
.description('Projeye AI standartlarini (AGENTS.md) veya sunum altyapisini indirir')
|
|
206
|
+
.option('-t, --template <type>', `Sablon turu (${TEMPLATE_KEYS.join(', ')})`)
|
|
72
207
|
.action(async (options) => {
|
|
73
208
|
let templateKey = options.template;
|
|
74
209
|
|
|
75
210
|
if (!templateKey) {
|
|
76
|
-
|
|
77
|
-
type: 'select',
|
|
78
|
-
name: 'template',
|
|
79
|
-
message: 'Lutfen bir Pro Sicht AI sablonu secin:',
|
|
80
|
-
choices: Object.entries(TEMPLATES).map(([key, value]) => ({
|
|
81
|
-
title: value.name,
|
|
82
|
-
value: key
|
|
83
|
-
}))
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
templateKey = response.template;
|
|
211
|
+
templateKey = await askTemplate('Lutfen bir Pro Sicht AI sablonu secin:');
|
|
87
212
|
if (!templateKey) process.exit(0);
|
|
88
213
|
}
|
|
89
214
|
|
|
90
|
-
await
|
|
215
|
+
await install(templateKey, 'init');
|
|
91
216
|
});
|
|
92
217
|
|
|
93
218
|
program
|
|
94
219
|
.command('update')
|
|
95
|
-
.description('Projedeki AGENTS.md
|
|
220
|
+
.description('Projedeki AGENTS.md (ve sunum altyapisinin cekirdek dosyalarini) tespit edip gunceller')
|
|
96
221
|
.option('-t, --template <type>', 'Sablon turunu zorla belirle')
|
|
222
|
+
.option('-y, --yes', 'Eski sunum yapisini sormadan yeni yapiya tasi')
|
|
97
223
|
.action(async (options) => {
|
|
98
224
|
let templateKey = options.template;
|
|
99
225
|
|
|
@@ -104,21 +230,20 @@ program
|
|
|
104
230
|
console.log(`🔍 Tespit edilen mevcut sablon: "${templateKey}"`);
|
|
105
231
|
} else {
|
|
106
232
|
console.log('⚠️ Projede mevcut bir AGENTS.md veya sablon etiketi bulunamadi.');
|
|
107
|
-
|
|
108
|
-
type: 'select',
|
|
109
|
-
name: 'template',
|
|
110
|
-
message: 'Guncellemek istediginiz sablonu secin:',
|
|
111
|
-
choices: Object.entries(TEMPLATES).map(([key, value]) => ({
|
|
112
|
-
title: value.name,
|
|
113
|
-
value: key
|
|
114
|
-
}))
|
|
115
|
-
});
|
|
116
|
-
templateKey = response.template;
|
|
233
|
+
templateKey = await askTemplate('Guncellemek istediginiz sablonu secin:');
|
|
117
234
|
if (!templateKey) process.exit(0);
|
|
118
235
|
}
|
|
119
236
|
}
|
|
120
237
|
|
|
121
|
-
await
|
|
238
|
+
await install(templateKey, 'update', { yes: options.yes });
|
|
122
239
|
});
|
|
123
240
|
|
|
241
|
+
program
|
|
242
|
+
.command('publish')
|
|
243
|
+
.description('Sunumu deck.prosicht.com uzerinde yayinlar ve linki verir')
|
|
244
|
+
.option('-k, --key <key>', 'Panelden alinan 32 karakterlik yayin keyi')
|
|
245
|
+
.option('--host <url>', 'Yayin adresi (varsayilan: https://deck.prosicht.com)')
|
|
246
|
+
.option('-y, --yes', 'Sorulari atla: Sunum secilir, key kaydedilir')
|
|
247
|
+
.action((options) => publish(options));
|
|
248
|
+
|
|
124
249
|
program.parse(process.argv);
|
package/lib/files.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
// js/scenes.js içindeki deck.title (panelde ve paylaşım önizlemesinde görünür)
|
|
63
|
+
function readDeckTitle(root) {
|
|
64
|
+
const file = path.join(root, 'js', 'scenes.js');
|
|
65
|
+
if (!fs.existsSync(file)) return null;
|
|
66
|
+
const source = fs.readFileSync(file, 'utf8');
|
|
67
|
+
const deck = source.match(/export\s+const\s+deck\s*=\s*\{([\s\S]*?)\};/);
|
|
68
|
+
const title = deck && deck[1].match(/\btitle\s*:\s*(['"`])((?:(?!\1).)*)\1/);
|
|
69
|
+
return title ? title[2].trim().slice(0, 120) : null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function formatBytes(n) {
|
|
73
|
+
if (n < 1024) return `${n} B`;
|
|
74
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(0)} KB`;
|
|
75
|
+
return `${(n / 1024 / 1024).toFixed(1)} MB`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { collectFiles, findAbsolutePaths, readDeckTitle, formatBytes, MIME_TYPES };
|
package/lib/layout.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
// Sunum klasör yapıları: 1 = kök dizinde app.js/sims/styles (kit 1.x),
|
|
5
|
+
// 2 = index.html + css/ js/ assets/ (kit 2.x). 0 = sunum 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
|
+
// Proje klasöründeki .gitignore'a satır ekler (yoksa oluşturur)
|
|
14
|
+
function ensureGitignore(root, line) {
|
|
15
|
+
const file = path.join(root, '.gitignore');
|
|
16
|
+
const current = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
17
|
+
if (current.split(/\r?\n/).some((l) => l.trim() === line)) return false;
|
|
18
|
+
const prefix = current && !current.endsWith('\n') ? '\n' : '';
|
|
19
|
+
fs.writeFileSync(file, `${current}${prefix}${line}\n`, 'utf8');
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = { detectLayout, 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,171 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const prompts = require('prompts');
|
|
4
|
+
const { detectLayout, ensureGitignore } = require('./layout');
|
|
5
|
+
const { collectFiles, findAbsolutePaths, readDeckTitle, 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('Sunum 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 kind = options.yes ? 'presentation' : await ask({
|
|
107
|
+
type: 'select', name: 'kind', message: 'Ne yayinlamak istiyorsunuz?',
|
|
108
|
+
choices: [
|
|
109
|
+
{ title: 'Sunum', value: 'presentation' },
|
|
110
|
+
{ title: 'Uygulama', value: 'app', disabled: true, description: 'yakinda' }
|
|
111
|
+
]
|
|
112
|
+
});
|
|
113
|
+
if (kind !== 'presentation') process.exit(0);
|
|
114
|
+
|
|
115
|
+
const layout = detectLayout(root);
|
|
116
|
+
if (layout === 1) fail('Bu sunum eski klasor yapisinda. Once `npx prosicht update` ile yeni yapiya tasiyin.');
|
|
117
|
+
if (layout === 0) fail('Bu klasorde bir sunum bulunamadi (index.html ve js/core/app.js gerekli). Sunum klasorunde calistirin.');
|
|
118
|
+
|
|
119
|
+
const { files, total, errors } = collectFiles(root);
|
|
120
|
+
if (errors.length) fail('Yayin oncesi kontrol basarisiz:', errors);
|
|
121
|
+
const absolute = findAbsolutePaths(files);
|
|
122
|
+
if (absolute.length) {
|
|
123
|
+
console.log('\nUyari: "/" ile baslayan yollar yayinda calismaz (goreli yol kullanin):');
|
|
124
|
+
for (const hit of absolute.slice(0, 10)) console.log(` - ${hit}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const config = readConfig(root);
|
|
128
|
+
const host = await chooseHost(options.host ? null : config.host, options.host);
|
|
129
|
+
let key = await chooseKey(options.key ? null : config.key, options.key);
|
|
130
|
+
if (!key) process.exit(0);
|
|
131
|
+
|
|
132
|
+
const title = readDeckTitle(root);
|
|
133
|
+
const send = async () => {
|
|
134
|
+
console.log(`\n[Pro Sicht] ${files.length} dosya (${formatBytes(total)}) yukleniyor...`);
|
|
135
|
+
try {
|
|
136
|
+
return await upload(host, key, files, title);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
return fail(`${host} adresine ulasilamadi: ${error.cause?.code || error.message}`);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
let result = await send();
|
|
142
|
+
// kayıtlı key panelden yenilenmişse bir kez yenisini sor ve tekrar dene
|
|
143
|
+
if (result.status === 401 && key === config.key && !options.key && !options.yes) {
|
|
144
|
+
console.log('Kayitli key gecersiz; panelde yenilenmis olabilir.');
|
|
145
|
+
key = await chooseKey(null, null);
|
|
146
|
+
if (!key) process.exit(0);
|
|
147
|
+
result = await send();
|
|
148
|
+
}
|
|
149
|
+
if (result.status !== 200 || !result.body.url) explain(result.status, result.body);
|
|
150
|
+
|
|
151
|
+
const { url, privacy, version } = result.body;
|
|
152
|
+
console.log(`Yayinlandi (surum ${version}): ${url}`);
|
|
153
|
+
if (privacy === 'private') {
|
|
154
|
+
console.log('Gizlilik: yalnizca siz gorebilirsiniz. Paylasmak icin panelden "Baglantiya sahip herkes" secin.');
|
|
155
|
+
} else {
|
|
156
|
+
console.log('Gizlilik: baglantiya sahip herkes gorebilir.');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (config.key !== key || config.host !== host) {
|
|
160
|
+
const remember = options.yes ? true : await ask({
|
|
161
|
+
type: 'confirm', name: 'save', initial: true,
|
|
162
|
+
message: 'Key ve adres bu klasor icin kaydedilsin mi? (.prosicht/, git disi)'
|
|
163
|
+
});
|
|
164
|
+
if (remember) saveConfig(root, { host, key, deckId: result.body.deckId, url });
|
|
165
|
+
} else {
|
|
166
|
+
saveConfig(root, { ...config, deckId: result.body.deckId, url });
|
|
167
|
+
}
|
|
168
|
+
console.log('');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = { publish };
|
package/package.json
CHANGED
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prosicht",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Pro Sicht AI Standards CLI",
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "Pro Sicht AI Standards CLI: AGENTS.md sablonlari ve sunum altyapisi",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"prosicht": "./index.js"
|
|
8
8
|
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.js",
|
|
11
|
+
"lib/"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
9
16
|
"scripts": {
|
|
10
17
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
18
|
},
|
|
12
|
-
"keywords": [
|
|
19
|
+
"keywords": [
|
|
20
|
+
"ai",
|
|
21
|
+
"agents",
|
|
22
|
+
"cli",
|
|
23
|
+
"prosicht",
|
|
24
|
+
"presentation"
|
|
25
|
+
],
|
|
13
26
|
"author": "Pro Sicht",
|
|
14
27
|
"license": "MIT",
|
|
28
|
+
"homepage": "https://github.com/prosicht/ai-standards#readme",
|
|
15
29
|
"dependencies": {
|
|
16
30
|
"commander": "^11.1.0",
|
|
17
31
|
"prompts": "^2.4.2"
|
package/tasks/todo.md
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
# Şablon etiketi ve otomatik update
|
|
2
|
-
|
|
3
|
-
## Plan
|
|
4
|
-
|
|
5
|
-
- [x] `index.js`: init sırasında `<!-- template: xxx -->` yaz
|
|
6
|
-
- [x] `index.js`: update mevcut etiketi okuyup doğru şablonu çeksin
|
|
7
|
-
- [x] `package.json` sürümünü `1.1.0` yap (CLI `--version` ile eşleşsin)
|
|
8
|
-
- [x] Lokal test: init `-t web` + etiket; update `-t` olmadan tespit
|
|
9
|
-
|
|
10
|
-
## Review
|
|
11
|
-
|
|
12
|
-
Init, `AGENTS.md` başına `<!-- template: web -->` yazıyor. `prosicht update` (`-t` yok) etiketi okuyup web şablonunu tekrar indirdi; dosyada tek etiket kaldı.
|
|
13
|
-
|
|
14
|
-
Sürüm lokal `1.1.0`. npm'deki yayın hâlâ `1.0.0` — yeni davranış için `npm publish` gerekir.
|