@chispear/vibekit 0.3.0 → 0.6.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/dist/brief.js ADDED
@@ -0,0 +1,276 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { hitungToken } from "./token.js";
4
+ /**
5
+ * Brief proyek (`.vibekit/brief.md`): ringkasan konteks proyek yang padat &
6
+ * faktual, disusun dari pemindaian lokal folder kerja — pohon direktori
7
+ * (maks 3 tingkat), stack dari package.json, potongan README, dan file
8
+ * konfigurasi penting. Brief ditulis sekali lewat `vibekit brief`, lalu
9
+ * dipakai otomatis sebagai konteks tambahan pada ask/chat/do.
10
+ */
11
+ const DIR_LEWATI = new Set(["node_modules", ".git", "dist", "build", ".vibekit"]);
12
+ const EKSTENSI_BINER = new Set([
13
+ ".png", ".jpg", ".jpeg", ".gif", ".webp", ".ico", ".bmp", ".tiff", ".svgz",
14
+ ".exe", ".dll", ".so", ".dylib", ".bin", ".o", ".class",
15
+ ".zip", ".tar", ".gz", ".rar", ".7z",
16
+ ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx",
17
+ ".mp3", ".mp4", ".wav", ".mov", ".avi", ".mkv",
18
+ ".woff", ".woff2", ".ttf", ".eot", ".wasm",
19
+ ]);
20
+ /** Peta ekstensi → bahasa, untuk bahasa dominan & daftar ekstensi. */
21
+ const BAHASA_DARI_EKSTENSI = {
22
+ ".ts": "TypeScript",
23
+ ".tsx": "TypeScript",
24
+ ".js": "JavaScript",
25
+ ".jsx": "JavaScript",
26
+ ".mjs": "JavaScript",
27
+ ".cjs": "JavaScript",
28
+ ".py": "Python",
29
+ ".go": "Go",
30
+ ".rs": "Rust",
31
+ ".java": "Java",
32
+ ".php": "PHP",
33
+ ".rb": "Ruby",
34
+ ".cs": "C#",
35
+ ".swift": "Swift",
36
+ ".kt": "Kotlin",
37
+ ".dart": "Dart",
38
+ ".vue": "Vue",
39
+ ".svelte": "Svelte",
40
+ };
41
+ /** Nama paket → label stack yang dikenal, untuk deteksi dari dependencies. */
42
+ const LABEL_STACK = {
43
+ typescript: "TypeScript",
44
+ react: "React",
45
+ "react-dom": "React",
46
+ next: "Next.js",
47
+ vue: "Vue",
48
+ "@angular/core": "Angular",
49
+ svelte: "Svelte",
50
+ express: "Express",
51
+ fastify: "Fastify",
52
+ "@nestjs/core": "NestJS",
53
+ tailwindcss: "Tailwind CSS",
54
+ prisma: "Prisma",
55
+ "@prisma/client": "Prisma",
56
+ mongoose: "Mongoose",
57
+ pg: "PostgreSQL",
58
+ mysql2: "MySQL",
59
+ vite: "Vite",
60
+ webpack: "Webpack",
61
+ electron: "Electron",
62
+ "react-native": "React Native",
63
+ expo: "Expo",
64
+ };
65
+ /** File konfigurasi penting yang dideteksi keberadaannya (hanya NAMA yang dicatat). */
66
+ const FILE_KONFIGURASI = [
67
+ "tsconfig.json",
68
+ "jsconfig.json",
69
+ ".env.example",
70
+ "vite.config.ts",
71
+ "vite.config.js",
72
+ "next.config.js",
73
+ "next.config.mjs",
74
+ "next.config.ts",
75
+ "webpack.config.js",
76
+ "eslint.config.js",
77
+ ".eslintrc.json",
78
+ ".eslintrc.js",
79
+ "prettier.config.js",
80
+ ".prettierrc",
81
+ "Dockerfile",
82
+ "docker-compose.yml",
83
+ "docker-compose.yaml",
84
+ "vercel.json",
85
+ "netlify.toml",
86
+ ];
87
+ function pindaiPohon(akar) {
88
+ const pohon = [];
89
+ const perEkstensi = new Map();
90
+ let jumlahFile = 0;
91
+ let jumlahTerpotong = 0;
92
+ const MAKS_BARIS_POHON = 150;
93
+ const kunjungi = (dir, kedalaman) => {
94
+ if (kedalaman > 3)
95
+ return;
96
+ let entri;
97
+ try {
98
+ entri = fs.readdirSync(dir, { withFileTypes: true });
99
+ }
100
+ catch {
101
+ return;
102
+ }
103
+ entri.sort((a, b) => a.name.localeCompare(b.name));
104
+ for (const e of entri) {
105
+ const rel = path.relative(akar, path.join(dir, e.name)).split(path.sep).join("/");
106
+ if (e.isDirectory()) {
107
+ if (DIR_LEWATI.has(e.name))
108
+ continue;
109
+ if (pohon.length < MAKS_BARIS_POHON)
110
+ pohon.push(`${rel}/`);
111
+ else
112
+ jumlahTerpotong++;
113
+ kunjungi(path.join(dir, e.name), kedalaman + 1);
114
+ }
115
+ else if (e.isFile()) {
116
+ // File .env asli tidak pernah dicatat (bisa memuat rahasia); hanya
117
+ // .env.example yang boleh muncul sebagai nama file konfigurasi.
118
+ if (e.name.startsWith(".env") && e.name !== ".env.example")
119
+ continue;
120
+ const ekst = path.extname(e.name).toLowerCase();
121
+ if (EKSTENSI_BINER.has(ekst))
122
+ continue;
123
+ jumlahFile++;
124
+ if (ekst)
125
+ perEkstensi.set(ekst, (perEkstensi.get(ekst) ?? 0) + 1);
126
+ if (pohon.length < MAKS_BARIS_POHON)
127
+ pohon.push(rel);
128
+ else
129
+ jumlahTerpotong++;
130
+ }
131
+ }
132
+ };
133
+ kunjungi(akar, 1);
134
+ return { pohon, jumlahFile, jumlahTerpotong, perEkstensi };
135
+ }
136
+ /** Pindai folder `cwd`, susun brief markdown, dan tulis ke `<cwd>/.vibekit/brief.md`. */
137
+ export function buatBrief(cwd) {
138
+ const akar = path.resolve(cwd);
139
+ const pindai = pindaiPohon(akar);
140
+ const baris = [];
141
+ // package.json: nama, stack, dependencies utama, scripts.
142
+ let pkg = null;
143
+ try {
144
+ pkg = JSON.parse(fs.readFileSync(path.join(akar, "package.json"), "utf8"));
145
+ }
146
+ catch {
147
+ pkg = null;
148
+ }
149
+ const namaProyek = pkg && typeof pkg.name === "string" && pkg.name ? pkg.name : path.basename(akar);
150
+ baris.push(`# Brief Proyek — ${namaProyek}`, "");
151
+ const fakta = [];
152
+ if (pkg && typeof pkg.version === "string")
153
+ fakta.push(`Versi paket: ${pkg.version}`);
154
+ fakta.push(`Total file terpindai: ${pindai.jumlahFile}`);
155
+ // Bahasa dominan dari ekstensi berkode terbanyak.
156
+ const perBahasa = new Map();
157
+ for (const [ekst, n] of pindai.perEkstensi) {
158
+ const bahasa = BAHASA_DARI_EKSTENSI[ekst];
159
+ if (bahasa)
160
+ perBahasa.set(bahasa, (perBahasa.get(bahasa) ?? 0) + n);
161
+ }
162
+ const bahasaUrut = [...perBahasa.entries()].sort((a, b) => b[1] - a[1]);
163
+ if (bahasaUrut.length > 0)
164
+ fakta.push(`Bahasa dominan: ${bahasaUrut[0][0]}`);
165
+ baris.push(...fakta.map((f) => `- ${f}`), "");
166
+ // Stack terdeteksi dari dependencies package.json.
167
+ const semuaDep = {};
168
+ if (pkg) {
169
+ for (const kunci of ["dependencies", "devDependencies"]) {
170
+ const dep = pkg[kunci];
171
+ if (dep && typeof dep === "object") {
172
+ for (const nama of Object.keys(dep)) {
173
+ semuaDep[nama] = String(dep[nama]);
174
+ }
175
+ }
176
+ }
177
+ }
178
+ const stack = Object.keys(semuaDep)
179
+ .filter((nama) => LABEL_STACK[nama])
180
+ .map((nama) => LABEL_STACK[nama]);
181
+ if (stack.length > 0) {
182
+ baris.push("## Stack", ...[...new Set(stack)].map((s) => `- ${s}`), "");
183
+ }
184
+ const namaDep = Object.keys(semuaDep).sort();
185
+ if (namaDep.length > 0) {
186
+ baris.push("## Dependencies", ...namaDep.slice(0, 30).map((n) => `- ${n}@${semuaDep[n]}`), "");
187
+ }
188
+ const scripts = pkg?.scripts;
189
+ if (scripts && typeof scripts === "object") {
190
+ const entri = Object.entries(scripts);
191
+ if (entri.length > 0) {
192
+ baris.push("## Scripts (package.json)", ...entri.map(([nama, perintah]) => {
193
+ const p = String(perintah);
194
+ return `- ${nama}: ${p.length > 60 ? p.slice(0, 60) + "…" : p}`;
195
+ }), "");
196
+ }
197
+ }
198
+ // File konfigurasi penting yang terdeteksi (nama saja).
199
+ const konfigurasi = FILE_KONFIGURASI.filter((nama) => fs.existsSync(path.join(akar, nama)));
200
+ if (konfigurasi.length > 0) {
201
+ baris.push("## File Konfigurasi", ...konfigurasi.map((k) => `- ${k}`), "");
202
+ }
203
+ // Ekstensi terbanyak (10 teratas).
204
+ const ekstUrut = [...pindai.perEkstensi.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10);
205
+ if (ekstUrut.length > 0) {
206
+ baris.push("## Ekstensi Terbanyak", ...ekstUrut.map(([e, n]) => `- ${e}: ${n} file`), "");
207
+ }
208
+ // Pohon direktori (maks 3 tingkat).
209
+ baris.push("## Struktur Folder (maks 3 tingkat)", ...pindai.pohon.map((p) => `- ${p}`));
210
+ if (pindai.jumlahTerpotong > 0)
211
+ baris.push(`- … (${pindai.jumlahTerpotong} entri lain tidak ditampilkan)`);
212
+ baris.push("");
213
+ // README: judul + potongan awal ±600 karakter.
214
+ let namaReadme = null;
215
+ try {
216
+ namaReadme = fs.readdirSync(akar).find((n) => /^readme(\..+)?$/i.test(n)) ?? null;
217
+ }
218
+ catch {
219
+ namaReadme = null;
220
+ }
221
+ if (namaReadme) {
222
+ try {
223
+ const isiReadme = fs.readFileSync(path.join(akar, namaReadme), "utf8");
224
+ const judul = isiReadme
225
+ .split("\n")
226
+ .map((l) => l.trim())
227
+ .find((l) => l.startsWith("# "))?.replace(/^#\s+/, "") ??
228
+ isiReadme.split("\n").find((l) => l.trim() !== "")?.trim() ??
229
+ "";
230
+ baris.push("## README (potongan)");
231
+ if (judul)
232
+ baris.push(`- Judul: ${judul}`);
233
+ const potongan = isiReadme.trim().slice(0, 600);
234
+ baris.push("", potongan + (isiReadme.trim().length > 600 ? "…" : ""), "");
235
+ }
236
+ catch {
237
+ /* README tidak terbaca: lewati bagian ini */
238
+ }
239
+ }
240
+ const isi = baris.join("\n").trimEnd() + "\n";
241
+ const lokasi = path.join(akar, ".vibekit", "brief.md");
242
+ fs.mkdirSync(path.dirname(lokasi), { recursive: true });
243
+ fs.writeFileSync(lokasi, isi, "utf8");
244
+ return { lokasi, isi, token: hitungToken(isi), jumlahFile: pindai.jumlahFile };
245
+ }
246
+ /** Batas token brief yang disuntikkan otomatis ke konteks model. */
247
+ export const BATAS_TOKEN_BRIEF = 4000;
248
+ /** Awalan penanda pesan konteks brief (dipakai untuk menghindari suntikan ganda). */
249
+ export const AWALAN_KONTEKS_BRIEF = "Konteks proyek (dari .vibekit/brief.md):";
250
+ /**
251
+ * Muat brief proyek dari `<cwd>/.vibekit/brief.md` untuk disuntikkan sebagai
252
+ * konteks model. Mengembalikan null bila brief tidak ada. Isi yang melewati
253
+ * BATAS_TOKEN_BRIEF dipotong dengan penanda yang jelas.
254
+ */
255
+ export function muatKonteksBrief(cwd) {
256
+ const lokasi = path.join(path.resolve(cwd), ".vibekit", "brief.md");
257
+ let isi;
258
+ try {
259
+ isi = fs.readFileSync(lokasi, "utf8");
260
+ }
261
+ catch {
262
+ return null;
263
+ }
264
+ let token = hitungToken(isi);
265
+ if (token > BATAS_TOKEN_BRIEF) {
266
+ let potong = isi;
267
+ // Potong proporsional lalu rapikan sampai muat (maks beberapa putaran).
268
+ for (let i = 0; i < 6 && hitungToken(potong) > BATAS_TOKEN_BRIEF; i++) {
269
+ const rasio = BATAS_TOKEN_BRIEF / hitungToken(potong);
270
+ potong = potong.slice(0, Math.max(200, Math.floor(potong.length * rasio * 0.95)));
271
+ }
272
+ isi = potong.trimEnd() + "\n\n…[brief dipotong: melebihi batas 4.000 token]";
273
+ token = hitungToken(isi);
274
+ }
275
+ return { teks: `${AWALAN_KONTEKS_BRIEF}\n\n${isi}`, token };
276
+ }