@rbinar/dev-kit 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.
- package/README.md +103 -0
- package/bin/cli.js +98 -0
- package/bundle/agents-bundle.enc +0 -0
- package/bundle/manifest.json +23 -0
- package/lib/commands/doctor.js +251 -0
- package/lib/commands/init.js +184 -0
- package/lib/commands/status.js +50 -0
- package/lib/commands/update.js +186 -0
- package/lib/constants.js +120 -0
- package/lib/crypto.js +64 -0
- package/lib/installer.js +234 -0
- package/lib/scaffold.js +109 -0
- package/package.json +34 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { isInitialized, readVersion } = require('../installer');
|
|
6
|
+
const { TARGET_AGENTS_DIR, TARGET_PROMPTS_DIR } = require('../constants');
|
|
7
|
+
|
|
8
|
+
async function status() {
|
|
9
|
+
const cwd = process.cwd();
|
|
10
|
+
const cliVersion = require('../../package.json').version;
|
|
11
|
+
|
|
12
|
+
if (!isInitialized(cwd)) {
|
|
13
|
+
console.log('blink-dev kurulu değil. Kurulum için: blink-dev init --key=SIFRE');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const versionInfo = readVersion(cwd);
|
|
18
|
+
const bundleVersion = versionInfo ? versionInfo.version : '—';
|
|
19
|
+
const installedAt = versionInfo ? versionInfo.installedAt : '—';
|
|
20
|
+
const updatedAt = versionInfo ? versionInfo.updatedAt : '—';
|
|
21
|
+
|
|
22
|
+
const agentsDir = path.join(cwd, TARGET_AGENTS_DIR);
|
|
23
|
+
let agentCount = 0;
|
|
24
|
+
try {
|
|
25
|
+
agentCount = fs.readdirSync(agentsDir).filter(f => f.endsWith('.agent.md')).length;
|
|
26
|
+
} catch (_) {
|
|
27
|
+
// dizin yoksa 0
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const promptsDir = path.join(cwd, TARGET_PROMPTS_DIR);
|
|
31
|
+
let promptCount = 0;
|
|
32
|
+
try {
|
|
33
|
+
promptCount = fs.readdirSync(promptsDir).filter(f => f.endsWith('.prompt.md')).length;
|
|
34
|
+
} catch (_) {
|
|
35
|
+
// dizin yoksa 0
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log(`blink-dev durum raporu
|
|
39
|
+
══════════════════════
|
|
40
|
+
|
|
41
|
+
CLI versiyonu: ${cliVersion}
|
|
42
|
+
Bundle versiyonu: ${bundleVersion}
|
|
43
|
+
Kurulum tarihi: ${installedAt}
|
|
44
|
+
Son güncelleme: ${updatedAt}
|
|
45
|
+
|
|
46
|
+
Kurulu ajanlar: ${agentCount}
|
|
47
|
+
Kurulu promptlar: ${promptCount}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = status;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const zlib = require('node:zlib');
|
|
7
|
+
const tar = require('tar');
|
|
8
|
+
|
|
9
|
+
const { decrypt } = require('../crypto');
|
|
10
|
+
const {
|
|
11
|
+
installFiles,
|
|
12
|
+
mergeCopilotInstructions,
|
|
13
|
+
isInitialized,
|
|
14
|
+
writeVersion,
|
|
15
|
+
readVersion,
|
|
16
|
+
createBackup,
|
|
17
|
+
restoreBackup,
|
|
18
|
+
} = require('../installer');
|
|
19
|
+
const { BUNDLE_PATH, TARGET_AGENTS_DIR, TARGET_PROMPTS_DIR } = require('../constants');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Decrypt edilmiş tarball buffer'ını geçici dizine açıp dosya haritası döndürür.
|
|
23
|
+
* copilot-instructions.md içeriğini ayrı olarak döndürür.
|
|
24
|
+
*/
|
|
25
|
+
function extractBundle(decryptedBuffer) {
|
|
26
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'blink-dev-'));
|
|
27
|
+
const tmpTarball = path.join(tmpDir, 'bundle.tar.gz');
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
fs.writeFileSync(tmpTarball, decryptedBuffer);
|
|
31
|
+
tar.x({ file: tmpTarball, cwd: tmpDir, sync: true });
|
|
32
|
+
|
|
33
|
+
const fileMap = new Map();
|
|
34
|
+
let copilotInstructions = null;
|
|
35
|
+
|
|
36
|
+
function walk(dir, prefix) {
|
|
37
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
const fullPath = path.join(dir, entry.name);
|
|
40
|
+
const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
41
|
+
|
|
42
|
+
if (entry.isDirectory()) {
|
|
43
|
+
walk(fullPath, relPath);
|
|
44
|
+
} else if (entry.isFile()) {
|
|
45
|
+
const content = fs.readFileSync(fullPath);
|
|
46
|
+
|
|
47
|
+
if (entry.name === 'copilot-instructions.md') {
|
|
48
|
+
copilotInstructions = content.toString('utf8');
|
|
49
|
+
} else if (relPath.startsWith('agents/')) {
|
|
50
|
+
const targetPath = path.join(TARGET_AGENTS_DIR, relPath.slice('agents/'.length));
|
|
51
|
+
fileMap.set(targetPath, content);
|
|
52
|
+
} else if (relPath.startsWith('prompts/')) {
|
|
53
|
+
const targetPath = path.join(TARGET_PROMPTS_DIR, relPath.slice('prompts/'.length));
|
|
54
|
+
fileMap.set(targetPath, content);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Tarball içindeki kök dizini bul
|
|
61
|
+
const extracted = fs.readdirSync(tmpDir).filter(
|
|
62
|
+
(f) => f !== 'bundle.tar.gz' && fs.statSync(path.join(tmpDir, f)).isDirectory()
|
|
63
|
+
);
|
|
64
|
+
const root = extracted.length === 1 ? path.join(tmpDir, extracted[0]) : tmpDir;
|
|
65
|
+
|
|
66
|
+
walk(root, '');
|
|
67
|
+
|
|
68
|
+
return { fileMap, copilotInstructions };
|
|
69
|
+
} finally {
|
|
70
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Dosya haritasını mevcut kurulumla karşılaştırır.
|
|
76
|
+
*/
|
|
77
|
+
function diffFiles(fileMap, cwd) {
|
|
78
|
+
const updated = [];
|
|
79
|
+
const added = [];
|
|
80
|
+
const unchanged = [];
|
|
81
|
+
|
|
82
|
+
for (const [relPath, content] of fileMap) {
|
|
83
|
+
const target = path.join(cwd, relPath);
|
|
84
|
+
|
|
85
|
+
if (!fs.existsSync(target)) {
|
|
86
|
+
added.push(relPath);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const existing = fs.readFileSync(target);
|
|
91
|
+
if (Buffer.compare(existing, content) === 0) {
|
|
92
|
+
unchanged.push(relPath);
|
|
93
|
+
} else {
|
|
94
|
+
updated.push(relPath);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { updated, added, unchanged };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* blink-dev update --key=SIFRE handler'ı
|
|
103
|
+
*/
|
|
104
|
+
async function update({ key, dryRun }) {
|
|
105
|
+
const cwd = process.cwd();
|
|
106
|
+
|
|
107
|
+
// 1. Kurulum kontrolü
|
|
108
|
+
if (!isInitialized(cwd)) {
|
|
109
|
+
console.error('Kurulum bulunamadı. Önce `blink-dev init --key=SIFRE` çalıştırın.');
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 2. Mevcut versiyon
|
|
114
|
+
const currentVersion = readVersion(cwd);
|
|
115
|
+
if (currentVersion) {
|
|
116
|
+
console.log(`Mevcut versiyon: ${currentVersion.version} (kurulum: ${currentVersion.installedAt})`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 3. Bundle oku + decrypt
|
|
120
|
+
const encBuffer = fs.readFileSync(BUNDLE_PATH);
|
|
121
|
+
const decryptedBuffer = decrypt(encBuffer, key);
|
|
122
|
+
|
|
123
|
+
// 4. Tarball extract
|
|
124
|
+
const { fileMap, copilotInstructions } = extractBundle(decryptedBuffer);
|
|
125
|
+
|
|
126
|
+
// 5. Dry-run
|
|
127
|
+
const { updated, added, unchanged } = diffFiles(fileMap, cwd);
|
|
128
|
+
|
|
129
|
+
if (dryRun) {
|
|
130
|
+
console.log('\n🔍 Dry-run sonucu:\n');
|
|
131
|
+
if (updated.length) {
|
|
132
|
+
console.log('Güncellenecek:');
|
|
133
|
+
updated.forEach((f) => console.log(` 📝 ${f}`));
|
|
134
|
+
}
|
|
135
|
+
if (added.length) {
|
|
136
|
+
console.log('Yeni:');
|
|
137
|
+
added.forEach((f) => console.log(` ➕ ${f}`));
|
|
138
|
+
}
|
|
139
|
+
if (unchanged.length) {
|
|
140
|
+
console.log('Değişiklik yok:');
|
|
141
|
+
unchanged.forEach((f) => console.log(` ⏩ ${f}`));
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 6. Backup al
|
|
147
|
+
const backupDir = createBackup(cwd);
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
// 7. Dosyaları güncelle (force: true — mevcut dosyaları güncelliyoruz)
|
|
151
|
+
installFiles(fileMap, cwd, { force: true });
|
|
152
|
+
|
|
153
|
+
// 8. Copilot instructions merge
|
|
154
|
+
if (copilotInstructions) {
|
|
155
|
+
mergeCopilotInstructions(copilotInstructions, cwd);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 9. Versiyon güncelle
|
|
159
|
+
const newVersion = require('../../package.json').version;
|
|
160
|
+
writeVersion(cwd, newVersion);
|
|
161
|
+
|
|
162
|
+
// 10. Backup temizle
|
|
163
|
+
fs.rmSync(backupDir, { recursive: true, force: true });
|
|
164
|
+
|
|
165
|
+
// 12. Başarı mesajı
|
|
166
|
+
const prevVersion = currentVersion ? currentVersion.version : 'bilinmiyor';
|
|
167
|
+
console.log(`\n✅ blink-dev başarıyla güncellendi!\n`);
|
|
168
|
+
console.log(`📁 Güncellenen: ${updated.length}`);
|
|
169
|
+
console.log(`➕ Yeni eklenen: ${added.length}`);
|
|
170
|
+
console.log(`⏩ Değişmeyen: ${unchanged.length}`);
|
|
171
|
+
console.log(`\nÖnceki versiyon: ${prevVersion}`);
|
|
172
|
+
console.log(`Yeni versiyon: ${newVersion}`);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
// 11. Hata durumu — backup'tan geri yükle
|
|
175
|
+
console.error('Güncelleme sırasında hata oluştu, backup\'tan geri yükleniyor...');
|
|
176
|
+
try {
|
|
177
|
+
restoreBackup(cwd);
|
|
178
|
+
console.error('Backup başarıyla geri yüklendi.');
|
|
179
|
+
} catch (restoreErr) {
|
|
180
|
+
console.error('Backup geri yükleme başarısız:', restoreErr.message);
|
|
181
|
+
}
|
|
182
|
+
throw err;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = update;
|
package/lib/constants.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
// Target directories in user's project
|
|
6
|
+
const TARGET_AGENTS_DIR = '.github/agents';
|
|
7
|
+
const TARGET_PROMPTS_DIR = '.github/prompts';
|
|
8
|
+
const TARGET_BLINK_DIR = '.blink';
|
|
9
|
+
const TARGET_INSTRUCTIONS = '.github/copilot-instructions.md';
|
|
10
|
+
|
|
11
|
+
// CLI internal files
|
|
12
|
+
const VERSION_FILE = '.blink-dev-version';
|
|
13
|
+
const BACKUP_DIR = '.blink-dev-backup';
|
|
14
|
+
const TMP_DIR = '.blink-dev-tmp';
|
|
15
|
+
const BUNDLE_PATH = path.join(__dirname, '..', 'bundle', 'agents-bundle.enc');
|
|
16
|
+
|
|
17
|
+
// Section markers for copilot-instructions.md merge
|
|
18
|
+
const MARKER_START = '<!-- BLINK-DEV-KIT:START — bu bölümü elle düzenlemeyin -->';
|
|
19
|
+
const MARKER_END = '<!-- BLINK-DEV-KIT:END -->';
|
|
20
|
+
|
|
21
|
+
// Encryption parameters
|
|
22
|
+
const PBKDF2_ITERATIONS = 100_000;
|
|
23
|
+
const PBKDF2_KEYLEN = 32; // 256 bits
|
|
24
|
+
const PBKDF2_DIGEST = 'sha256';
|
|
25
|
+
const CIPHER_ALGORITHM = 'aes-256-cbc';
|
|
26
|
+
const SALT_LENGTH = 16;
|
|
27
|
+
const IV_LENGTH = 16;
|
|
28
|
+
const HASH_LENGTH = 32; // SHA-256
|
|
29
|
+
|
|
30
|
+
// Scaffold templates for .blink/ directory
|
|
31
|
+
const SCAFFOLD_FILES = {
|
|
32
|
+
'architecture.md': `<!-- Generated by blink-dev init -->
|
|
33
|
+
|
|
34
|
+
# Architecture
|
|
35
|
+
|
|
36
|
+
## Module Map
|
|
37
|
+
|
|
38
|
+
| Path | Purpose |
|
|
39
|
+
|---|---|
|
|
40
|
+
| | |
|
|
41
|
+
|
|
42
|
+
## Key Dependencies
|
|
43
|
+
|
|
44
|
+
| Dependency | Role |
|
|
45
|
+
|---|---|
|
|
46
|
+
| | |
|
|
47
|
+
`,
|
|
48
|
+
'conventions.md': `<!-- Generated by blink-dev init -->
|
|
49
|
+
|
|
50
|
+
# Conventions
|
|
51
|
+
|
|
52
|
+
## Code Style
|
|
53
|
+
|
|
54
|
+
## Naming
|
|
55
|
+
|
|
56
|
+
## Error Handling
|
|
57
|
+
`,
|
|
58
|
+
'stack.md': `<!-- Generated by blink-dev init -->
|
|
59
|
+
|
|
60
|
+
# Stack
|
|
61
|
+
|
|
62
|
+
## Language
|
|
63
|
+
|
|
64
|
+
## Framework
|
|
65
|
+
|
|
66
|
+
## Package Manager
|
|
67
|
+
|
|
68
|
+
## Key Dependencies
|
|
69
|
+
`,
|
|
70
|
+
'testing.md': `<!-- Generated by blink-dev init -->
|
|
71
|
+
|
|
72
|
+
# Testing
|
|
73
|
+
|
|
74
|
+
## Framework
|
|
75
|
+
|
|
76
|
+
## Strategy
|
|
77
|
+
|
|
78
|
+
## Coverage
|
|
79
|
+
`,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Expected agent files (for doctor checks)
|
|
83
|
+
const EXPECTED_AGENTS = [
|
|
84
|
+
'Analyst.agent.md',
|
|
85
|
+
'Brainstorm.agent.md',
|
|
86
|
+
'Coordinator.agent.md',
|
|
87
|
+
'Critic.agent.md',
|
|
88
|
+
'Debugger.agent.md',
|
|
89
|
+
'Guardian.agent.md',
|
|
90
|
+
'Initializer.agent.md',
|
|
91
|
+
'Ops.agent.md',
|
|
92
|
+
'Planner.agent.md',
|
|
93
|
+
'Reviewer.agent.md',
|
|
94
|
+
'Scribe.agent.md',
|
|
95
|
+
'Security.agent.md',
|
|
96
|
+
'Shipper.agent.md',
|
|
97
|
+
'Triage.agent.md',
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
module.exports = {
|
|
101
|
+
TARGET_AGENTS_DIR,
|
|
102
|
+
TARGET_PROMPTS_DIR,
|
|
103
|
+
TARGET_BLINK_DIR,
|
|
104
|
+
TARGET_INSTRUCTIONS,
|
|
105
|
+
VERSION_FILE,
|
|
106
|
+
BACKUP_DIR,
|
|
107
|
+
TMP_DIR,
|
|
108
|
+
BUNDLE_PATH,
|
|
109
|
+
MARKER_START,
|
|
110
|
+
MARKER_END,
|
|
111
|
+
PBKDF2_ITERATIONS,
|
|
112
|
+
PBKDF2_KEYLEN,
|
|
113
|
+
PBKDF2_DIGEST,
|
|
114
|
+
CIPHER_ALGORITHM,
|
|
115
|
+
SALT_LENGTH,
|
|
116
|
+
IV_LENGTH,
|
|
117
|
+
HASH_LENGTH,
|
|
118
|
+
SCAFFOLD_FILES,
|
|
119
|
+
EXPECTED_AGENTS,
|
|
120
|
+
};
|
package/lib/crypto.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
|
|
5
|
+
const ALGORITHM = 'aes-256-cbc';
|
|
6
|
+
const PBKDF2_ITERATIONS = 100000;
|
|
7
|
+
const PBKDF2_DIGEST = 'sha256';
|
|
8
|
+
const KEY_LENGTH = 32;
|
|
9
|
+
const SALT_LENGTH = 16;
|
|
10
|
+
const IV_LENGTH = 16;
|
|
11
|
+
const HASH_LENGTH = 32;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {Buffer} buffer
|
|
15
|
+
* @param {string} password
|
|
16
|
+
* @returns {Buffer}
|
|
17
|
+
*/
|
|
18
|
+
function encrypt(buffer, password) {
|
|
19
|
+
const salt = crypto.randomBytes(SALT_LENGTH);
|
|
20
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
21
|
+
const key = crypto.pbkdf2Sync(password, salt, PBKDF2_ITERATIONS, KEY_LENGTH, PBKDF2_DIGEST);
|
|
22
|
+
|
|
23
|
+
const hash = crypto.createHash('sha256').update(buffer).digest();
|
|
24
|
+
const plainWithHash = Buffer.concat([hash, buffer]);
|
|
25
|
+
|
|
26
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
27
|
+
const encrypted = Buffer.concat([cipher.update(plainWithHash), cipher.final()]);
|
|
28
|
+
|
|
29
|
+
return Buffer.concat([salt, iv, encrypted]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {Buffer} encBuffer
|
|
34
|
+
* @param {string} password
|
|
35
|
+
* @returns {Buffer}
|
|
36
|
+
*/
|
|
37
|
+
function decrypt(encBuffer, password) {
|
|
38
|
+
const salt = encBuffer.subarray(0, SALT_LENGTH);
|
|
39
|
+
const iv = encBuffer.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);
|
|
40
|
+
const ciphertext = encBuffer.subarray(SALT_LENGTH + IV_LENGTH);
|
|
41
|
+
|
|
42
|
+
const key = crypto.pbkdf2Sync(password, salt, PBKDF2_ITERATIONS, KEY_LENGTH, PBKDF2_DIGEST);
|
|
43
|
+
|
|
44
|
+
let decrypted;
|
|
45
|
+
try {
|
|
46
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
47
|
+
decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
48
|
+
} catch (_) {
|
|
49
|
+
throw new Error('Geçersiz anahtar veya bozuk bundle');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const storedHash = decrypted.subarray(0, HASH_LENGTH);
|
|
53
|
+
const originalData = decrypted.subarray(HASH_LENGTH);
|
|
54
|
+
|
|
55
|
+
const computedHash = crypto.createHash('sha256').update(originalData).digest();
|
|
56
|
+
|
|
57
|
+
if (!crypto.timingSafeEqual(storedHash, computedHash)) {
|
|
58
|
+
throw new Error('Geçersiz anahtar veya bozuk bundle');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return originalData;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { encrypt, decrypt };
|
package/lib/installer.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const constants = require('./constants');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Dosyaları atomik olarak hedefe yazar.
|
|
9
|
+
*/
|
|
10
|
+
function installFiles(fileMap, cwd, options = {}) {
|
|
11
|
+
const { dryRun = false, force = false } = options;
|
|
12
|
+
const installed = [];
|
|
13
|
+
const skipped = [];
|
|
14
|
+
const errors = [];
|
|
15
|
+
|
|
16
|
+
if (dryRun) {
|
|
17
|
+
for (const [relPath] of fileMap) {
|
|
18
|
+
const target = path.join(cwd, relPath);
|
|
19
|
+
if (!force && fs.existsSync(target)) {
|
|
20
|
+
skipped.push(relPath);
|
|
21
|
+
} else {
|
|
22
|
+
installed.push(relPath);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return { installed, skipped, errors };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const tmpDir = path.join(cwd, constants.TMP_DIR);
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// 1. Geçici dizin oluştur
|
|
32
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
33
|
+
|
|
34
|
+
// 2. Tüm dosyaları geçici dizine yaz
|
|
35
|
+
for (const [relPath, content] of fileMap) {
|
|
36
|
+
const tmpTarget = path.join(tmpDir, relPath);
|
|
37
|
+
fs.mkdirSync(path.dirname(tmpTarget), { recursive: true });
|
|
38
|
+
fs.writeFileSync(tmpTarget, content);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 3. Geçici dizinden hedef dizine kopyala
|
|
42
|
+
for (const [relPath] of fileMap) {
|
|
43
|
+
const source = path.join(tmpDir, relPath);
|
|
44
|
+
const target = path.join(cwd, relPath);
|
|
45
|
+
|
|
46
|
+
if (!force && fs.existsSync(target)) {
|
|
47
|
+
skipped.push(relPath);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
52
|
+
fs.cpSync(source, target);
|
|
53
|
+
installed.push(relPath);
|
|
54
|
+
}
|
|
55
|
+
} catch (err) {
|
|
56
|
+
errors.push(err.message);
|
|
57
|
+
throw err;
|
|
58
|
+
} finally {
|
|
59
|
+
// 4. Geçici dizini temizle
|
|
60
|
+
if (fs.existsSync(tmpDir)) {
|
|
61
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { installed, skipped, errors };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* copilot-instructions.md dosyasını section marker'larla günceller.
|
|
70
|
+
*/
|
|
71
|
+
function mergeCopilotInstructions(content, cwd) {
|
|
72
|
+
const targetPath = path.join(cwd, constants.TARGET_INSTRUCTIONS);
|
|
73
|
+
const targetDir = path.dirname(targetPath);
|
|
74
|
+
|
|
75
|
+
// .github/ dizini yoksa oluştur
|
|
76
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
77
|
+
|
|
78
|
+
const markerBlock = `${constants.MARKER_START}\n${content}\n${constants.MARKER_END}`;
|
|
79
|
+
|
|
80
|
+
// Dosya yoksa: marker block olarak oluştur
|
|
81
|
+
if (!fs.existsSync(targetPath)) {
|
|
82
|
+
fs.writeFileSync(targetPath, markerBlock + '\n');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const existing = fs.readFileSync(targetPath, 'utf8');
|
|
87
|
+
|
|
88
|
+
// Dosya varsa ve marker'lar varsa: arasını değiştir
|
|
89
|
+
const startIdx = existing.indexOf(constants.MARKER_START);
|
|
90
|
+
const endIdx = existing.indexOf(constants.MARKER_END);
|
|
91
|
+
|
|
92
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
93
|
+
const before = existing.slice(0, startIdx);
|
|
94
|
+
const after = existing.slice(endIdx + constants.MARKER_END.length);
|
|
95
|
+
fs.writeFileSync(targetPath, before + markerBlock + after);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Dosya varsa ama marker yoksa: başına ekle
|
|
100
|
+
fs.writeFileSync(targetPath, markerBlock + '\n\n' + existing);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* .blink/ dizinine minimum scaffold şablonları yazar. Mevcut dosyaları ezmez.
|
|
105
|
+
*/
|
|
106
|
+
function createScaffold(cwd) {
|
|
107
|
+
const created = [];
|
|
108
|
+
const skipped = [];
|
|
109
|
+
|
|
110
|
+
const blinkDir = path.join(cwd, constants.TARGET_BLINK_DIR);
|
|
111
|
+
fs.mkdirSync(blinkDir, { recursive: true });
|
|
112
|
+
|
|
113
|
+
for (const [fileName, content] of Object.entries(constants.SCAFFOLD_FILES)) {
|
|
114
|
+
const filePath = path.join(blinkDir, fileName);
|
|
115
|
+
|
|
116
|
+
if (fs.existsSync(filePath)) {
|
|
117
|
+
skipped.push(fileName);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
fs.writeFileSync(filePath, content);
|
|
122
|
+
created.push(fileName);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { created, skipped };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Kurulumun mevcut olup olmadığını kontrol eder.
|
|
130
|
+
*/
|
|
131
|
+
function isInitialized(cwd) {
|
|
132
|
+
return fs.existsSync(path.join(cwd, constants.VERSION_FILE));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Versiyon dosyasını yazar.
|
|
137
|
+
*/
|
|
138
|
+
function writeVersion(cwd, version) {
|
|
139
|
+
const filePath = path.join(cwd, constants.VERSION_FILE);
|
|
140
|
+
const now = new Date().toISOString();
|
|
141
|
+
let data;
|
|
142
|
+
|
|
143
|
+
if (fs.existsSync(filePath)) {
|
|
144
|
+
const existing = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
145
|
+
data = {
|
|
146
|
+
version,
|
|
147
|
+
installedAt: existing.installedAt,
|
|
148
|
+
updatedAt: now,
|
|
149
|
+
};
|
|
150
|
+
} else {
|
|
151
|
+
data = {
|
|
152
|
+
version,
|
|
153
|
+
installedAt: now,
|
|
154
|
+
updatedAt: now,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Versiyon dosyasını okur.
|
|
163
|
+
*/
|
|
164
|
+
function readVersion(cwd) {
|
|
165
|
+
const filePath = path.join(cwd, constants.VERSION_FILE);
|
|
166
|
+
|
|
167
|
+
if (!fs.existsSync(filePath)) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Mevcut kurulumun backup'ını alır.
|
|
176
|
+
*/
|
|
177
|
+
function createBackup(cwd) {
|
|
178
|
+
const backupDir = path.join(cwd, constants.BACKUP_DIR);
|
|
179
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
180
|
+
|
|
181
|
+
const sources = [
|
|
182
|
+
constants.TARGET_AGENTS_DIR,
|
|
183
|
+
constants.TARGET_PROMPTS_DIR,
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
for (const relDir of sources) {
|
|
187
|
+
const srcDir = path.join(cwd, relDir);
|
|
188
|
+
if (!fs.existsSync(srcDir)) continue;
|
|
189
|
+
|
|
190
|
+
const destDir = path.join(backupDir, relDir);
|
|
191
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
192
|
+
fs.cpSync(srcDir, destDir, { recursive: true });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return backupDir;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Backup'tan geri yükle.
|
|
200
|
+
*/
|
|
201
|
+
function restoreBackup(cwd) {
|
|
202
|
+
const backupDir = path.join(cwd, constants.BACKUP_DIR);
|
|
203
|
+
|
|
204
|
+
if (!fs.existsSync(backupDir)) {
|
|
205
|
+
throw new Error('Backup dizini bulunamadı: ' + backupDir);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const targets = [
|
|
209
|
+
constants.TARGET_AGENTS_DIR,
|
|
210
|
+
constants.TARGET_PROMPTS_DIR,
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
for (const relDir of targets) {
|
|
214
|
+
const srcDir = path.join(backupDir, relDir);
|
|
215
|
+
if (!fs.existsSync(srcDir)) continue;
|
|
216
|
+
|
|
217
|
+
const destDir = path.join(cwd, relDir);
|
|
218
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
219
|
+
fs.cpSync(srcDir, destDir, { recursive: true });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
fs.rmSync(backupDir, { recursive: true, force: true });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
module.exports = {
|
|
226
|
+
installFiles,
|
|
227
|
+
mergeCopilotInstructions,
|
|
228
|
+
createScaffold,
|
|
229
|
+
isInitialized,
|
|
230
|
+
writeVersion,
|
|
231
|
+
readVersion,
|
|
232
|
+
createBackup,
|
|
233
|
+
restoreBackup,
|
|
234
|
+
};
|