zames_pro 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/LICENSE +21 -0
- package/README.md +48 -0
- package/package.json +36 -0
- package/src/agent-loop.js +419 -0
- package/src/browser.js +468 -0
- package/src/config.js +80 -0
- package/src/confirm.js +85 -0
- package/src/diff.js +124 -0
- package/src/gitTools.js +256 -0
- package/src/index.js +792 -0
- package/src/self-review.js +304 -0
- package/src/spinner.js +55 -0
- package/src/system-prompt.js +84 -0
- package/src/tools.js +206 -0
- package/src/transcript.js +46 -0
- package/src/undo.js +101 -0
- package/src/web.js +266 -0
package/src/browser.js
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
import { chromium } from 'playwright'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import os from 'os'
|
|
4
|
+
import fs from 'fs/promises'
|
|
5
|
+
import { execSync } from 'child_process'
|
|
6
|
+
|
|
7
|
+
const USER_DATA_DIR = path.join(os.homedir(), '.zames', 'profile')
|
|
8
|
+
const CHAT_URL = 'https://chat.deepseek.com/'
|
|
9
|
+
|
|
10
|
+
const INPUT_SELECTORS = [
|
|
11
|
+
'textarea',
|
|
12
|
+
'div[contenteditable="true"]',
|
|
13
|
+
'[role="textbox"]',
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
const ANSWER_SELECTORS = [
|
|
17
|
+
'div[class*="ds-markdown"]',
|
|
18
|
+
'div[class*="markdown"]',
|
|
19
|
+
'[class*="message"]',
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
const STOP_SELECTORS = [
|
|
23
|
+
'button:has-text("Stop")',
|
|
24
|
+
'button:has-text("Остановить")',
|
|
25
|
+
'button[aria-label*="Stop" i]',
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
// ---------- profile cleanup ----------
|
|
29
|
+
|
|
30
|
+
async function killStaleChrome() {
|
|
31
|
+
if (process.platform !== 'win32') return
|
|
32
|
+
try {
|
|
33
|
+
execSync(
|
|
34
|
+
`powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \\"Name='chrome.exe'\\" | Where-Object { $_.CommandLine -like '*\\.zames\\profile*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }"`,
|
|
35
|
+
{ stdio: 'ignore', timeout: 5000 },
|
|
36
|
+
)
|
|
37
|
+
await new Promise((r) => setTimeout(r, 800))
|
|
38
|
+
} catch {}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function cleanSingletonFiles() {
|
|
42
|
+
const names = ['SingletonLock', 'SingletonCookie', 'SingletonSocket']
|
|
43
|
+
for (const name of names) {
|
|
44
|
+
const p = path.join(USER_DATA_DIR, name)
|
|
45
|
+
try {
|
|
46
|
+
await fs.unlink(p)
|
|
47
|
+
} catch {}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function profileLooksLocked() {
|
|
52
|
+
try {
|
|
53
|
+
await fs.access(path.join(USER_DATA_DIR, 'SingletonLock'))
|
|
54
|
+
return true
|
|
55
|
+
} catch {
|
|
56
|
+
return false
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function nukeProfile() {
|
|
61
|
+
try {
|
|
62
|
+
await fs.rm(USER_DATA_DIR, { recursive: true, force: true })
|
|
63
|
+
} catch {}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------- class ----------
|
|
67
|
+
|
|
68
|
+
export class DeepSeekBrowser {
|
|
69
|
+
constructor({
|
|
70
|
+
headless = false,
|
|
71
|
+
debug = false,
|
|
72
|
+
channel = 'chrome',
|
|
73
|
+
answerTimeoutMs = 180000,
|
|
74
|
+
askRetries = 3,
|
|
75
|
+
stabilityChecks = 3,
|
|
76
|
+
stabilityDelayMs = 1000,
|
|
77
|
+
} = {}) {
|
|
78
|
+
this.headless = headless
|
|
79
|
+
this.debug = debug
|
|
80
|
+
this.channel = channel
|
|
81
|
+
this.answerTimeoutMs = answerTimeoutMs
|
|
82
|
+
this.askRetries = askRetries
|
|
83
|
+
this.stabilityChecks = stabilityChecks
|
|
84
|
+
this.stabilityDelayMs = stabilityDelayMs
|
|
85
|
+
this.context = null
|
|
86
|
+
this.page = null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async launch() {
|
|
90
|
+
await killStaleChrome()
|
|
91
|
+
if (await profileLooksLocked()) {
|
|
92
|
+
if (this.debug) console.error('profile: удаляю Singleton-файлы')
|
|
93
|
+
await cleanSingletonFiles()
|
|
94
|
+
}
|
|
95
|
+
await this._launchOnce()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async _launchOnce() {
|
|
99
|
+
const options = {
|
|
100
|
+
headless: this.headless,
|
|
101
|
+
slowMo: 30,
|
|
102
|
+
args: ['--disable-blink-features=AutomationControlled'],
|
|
103
|
+
}
|
|
104
|
+
if (this.channel) options.channel = this.channel
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
this.context = await chromium.launchPersistentContext(
|
|
108
|
+
USER_DATA_DIR,
|
|
109
|
+
options,
|
|
110
|
+
)
|
|
111
|
+
} catch (e) {
|
|
112
|
+
if (
|
|
113
|
+
/Target page, context or browser has been closed|profile.*in use/i.test(
|
|
114
|
+
e.message,
|
|
115
|
+
)
|
|
116
|
+
) {
|
|
117
|
+
if (this.debug)
|
|
118
|
+
console.error('profile: занят, перезапускаю после очистки')
|
|
119
|
+
await killStaleChrome()
|
|
120
|
+
await cleanSingletonFiles()
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
this.context = await chromium.launchPersistentContext(
|
|
124
|
+
USER_DATA_DIR,
|
|
125
|
+
options,
|
|
126
|
+
)
|
|
127
|
+
} catch (e2) {
|
|
128
|
+
if (this.debug) console.error('profile: сношу целиком')
|
|
129
|
+
await nukeProfile()
|
|
130
|
+
this.context = await chromium.launchPersistentContext(
|
|
131
|
+
USER_DATA_DIR,
|
|
132
|
+
options,
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
throw e
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this.page = this.context.pages()[0] || (await this.context.newPage())
|
|
141
|
+
await this.page.goto(CHAT_URL, { waitUntil: 'domcontentloaded' })
|
|
142
|
+
return this
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async restart() {
|
|
146
|
+
try {
|
|
147
|
+
if (this.context) await this.context.close()
|
|
148
|
+
} catch {}
|
|
149
|
+
await this.launch()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async waitForLogin() {
|
|
153
|
+
const loggedIn = await this.isLoggedIn()
|
|
154
|
+
if (loggedIn) return
|
|
155
|
+
|
|
156
|
+
console.log('\n🔐 Залогиньтесь в DeepSeek в открытом браузере.')
|
|
157
|
+
console.log(' После входа нажмите Enter в терминале...\n')
|
|
158
|
+
|
|
159
|
+
const readline = await import('readline')
|
|
160
|
+
const rl = readline.createInterface({
|
|
161
|
+
input: process.stdin,
|
|
162
|
+
output: process.stdout,
|
|
163
|
+
})
|
|
164
|
+
await new Promise((resolve) => {
|
|
165
|
+
rl.question('', () => {
|
|
166
|
+
rl.close()
|
|
167
|
+
resolve()
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async isLoggedIn() {
|
|
173
|
+
for (const sel of INPUT_SELECTORS) {
|
|
174
|
+
try {
|
|
175
|
+
await this.page.locator(sel).first().waitFor({
|
|
176
|
+
state: 'visible',
|
|
177
|
+
timeout: 3000,
|
|
178
|
+
})
|
|
179
|
+
return true
|
|
180
|
+
} catch {}
|
|
181
|
+
}
|
|
182
|
+
return false
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async newChat() {
|
|
186
|
+
const newChatBtn = this.page
|
|
187
|
+
.locator('button, a')
|
|
188
|
+
.filter({ hasText: /new chat|новый чат|новый диалог/i })
|
|
189
|
+
.first()
|
|
190
|
+
try {
|
|
191
|
+
await newChatBtn.click({ timeout: 2000 })
|
|
192
|
+
await this.page.waitForTimeout(1000)
|
|
193
|
+
} catch {
|
|
194
|
+
await this.page.goto(CHAT_URL, { waitUntil: 'domcontentloaded' })
|
|
195
|
+
await this.page.waitForTimeout(1000)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async _findVisible(selectors, timeout = 1000) {
|
|
200
|
+
for (const sel of selectors) {
|
|
201
|
+
const loc = this.page.locator(sel).last()
|
|
202
|
+
try {
|
|
203
|
+
await loc.waitFor({ state: 'visible', timeout })
|
|
204
|
+
return loc
|
|
205
|
+
} catch {}
|
|
206
|
+
}
|
|
207
|
+
return null
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async _readLastAnswerText() {
|
|
211
|
+
return await this.page.evaluate((sels) => {
|
|
212
|
+
let el = null
|
|
213
|
+
for (const s of sels) {
|
|
214
|
+
const list = document.querySelectorAll(s)
|
|
215
|
+
if (list.length) el = list[list.length - 1]
|
|
216
|
+
}
|
|
217
|
+
if (!el) return ''
|
|
218
|
+
|
|
219
|
+
const clone = el.cloneNode(true)
|
|
220
|
+
|
|
221
|
+
Array.from(clone.querySelectorAll('pre')).forEach((pre) => {
|
|
222
|
+
const codeEl = pre.querySelector('code')
|
|
223
|
+
const source = codeEl || pre
|
|
224
|
+
const text = (source.textContent || '').replace(/\n$/, '')
|
|
225
|
+
const langMatch = (source.className || '').match(/language-([\w-]+)/)
|
|
226
|
+
const lang = langMatch ? langMatch[1] : ''
|
|
227
|
+
const replacement = document.createTextNode(
|
|
228
|
+
'```' + lang + '\n' + text + '\n```',
|
|
229
|
+
)
|
|
230
|
+
if (pre.parentNode) pre.parentNode.replaceChild(replacement, pre)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
Array.from(clone.querySelectorAll('code')).forEach((c) => {
|
|
234
|
+
const replacement = document.createTextNode(
|
|
235
|
+
'`' + (c.textContent || '') + '`',
|
|
236
|
+
)
|
|
237
|
+
if (c.parentNode) c.parentNode.replaceChild(replacement, c)
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
return clone.textContent || ''
|
|
241
|
+
}, ANSWER_SELECTORS)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async _isGenerating() {
|
|
245
|
+
const stop = await this._findVisible(STOP_SELECTORS, 300)
|
|
246
|
+
return !!stop
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async ask(prompt, { timeout = this.answerTimeoutMs } = {}) {
|
|
250
|
+
let lastErr = null
|
|
251
|
+
|
|
252
|
+
for (let attempt = 1; attempt <= this.askRetries; attempt++) {
|
|
253
|
+
try {
|
|
254
|
+
return await this._askOnce(prompt, { timeout })
|
|
255
|
+
} catch (e) {
|
|
256
|
+
lastErr = e
|
|
257
|
+
console.error(
|
|
258
|
+
`\n⚠ ask() попытка ${attempt}/${this.askRetries} провалилась: ${e.message}`,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
if (/closed|crash|Target page|browser/i.test(e.message)) {
|
|
262
|
+
console.error('⚠ перезапускаю браузер...')
|
|
263
|
+
try {
|
|
264
|
+
await this.restart()
|
|
265
|
+
await this.waitForLogin()
|
|
266
|
+
} catch (re) {
|
|
267
|
+
console.error(`⚠ не удалось перезапустить: ${re.message}`)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (attempt < this.askRetries) {
|
|
272
|
+
await new Promise((r) => setTimeout(r, 2000 * attempt))
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
throw new Error(
|
|
278
|
+
`ask() провалился после ${this.askRetries} попыток: ${lastErr?.message}`,
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async _askOnce(prompt, { timeout }) {
|
|
283
|
+
const input = await this._findVisible(INPUT_SELECTORS, 10_000)
|
|
284
|
+
if (!input) {
|
|
285
|
+
throw new Error(
|
|
286
|
+
'Не найдено поле ввода. Запустите /debug-dom и поправьте INPUT_SELECTORS.',
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const beforeText = await this._readLastAnswerText().catch(() => '')
|
|
291
|
+
|
|
292
|
+
await input.click()
|
|
293
|
+
try {
|
|
294
|
+
await input.fill(prompt, { timeout: 5000 })
|
|
295
|
+
} catch {
|
|
296
|
+
// contenteditable / [role=textbox] не поддерживает fill() — печатаем
|
|
297
|
+
// текст в уже сфокусированное поле, не задевая раскладку.
|
|
298
|
+
await this.page.keyboard.insertText(prompt)
|
|
299
|
+
}
|
|
300
|
+
await this.page.waitForTimeout(200)
|
|
301
|
+
|
|
302
|
+
const sendBtn = this.page
|
|
303
|
+
.locator('button')
|
|
304
|
+
.filter({ hasText: /send|отправить/i })
|
|
305
|
+
.first()
|
|
306
|
+
try {
|
|
307
|
+
await sendBtn.click({ timeout: 1500 })
|
|
308
|
+
} catch {
|
|
309
|
+
await this.page.keyboard.press('Enter')
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const startDeadline = Date.now() + 15_000
|
|
313
|
+
let started = false
|
|
314
|
+
while (Date.now() < startDeadline) {
|
|
315
|
+
const gen = await this._isGenerating()
|
|
316
|
+
const cur = await this._readLastAnswerText().catch(() => '')
|
|
317
|
+
if (gen || (cur && cur !== beforeText)) {
|
|
318
|
+
started = true
|
|
319
|
+
break
|
|
320
|
+
}
|
|
321
|
+
await this.page.waitForTimeout(300)
|
|
322
|
+
}
|
|
323
|
+
if (!started) {
|
|
324
|
+
throw new Error(
|
|
325
|
+
'Ответ не начал генерироваться за 15с. Возможно, сообщение не отправилось.',
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const deadline = Date.now() + timeout
|
|
330
|
+
let last = ''
|
|
331
|
+
let stable = 0
|
|
332
|
+
while (Date.now() < deadline) {
|
|
333
|
+
const gen = await this._isGenerating()
|
|
334
|
+
const cur = await this._readLastAnswerText().catch(() => '')
|
|
335
|
+
if (cur && cur === last && !gen) {
|
|
336
|
+
stable++
|
|
337
|
+
if (stable >= 2) return cur
|
|
338
|
+
} else {
|
|
339
|
+
stable = 0
|
|
340
|
+
}
|
|
341
|
+
last = cur
|
|
342
|
+
await this.page.waitForTimeout(800)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (last) return last
|
|
346
|
+
throw new Error('Таймаут ожидания ответа. Попробуйте /debug-dom.')
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async dumpDom(filePath) {
|
|
350
|
+
if (!this.page) throw new Error('браузер не запущен')
|
|
351
|
+
const html = await this.page.content()
|
|
352
|
+
await fs.writeFile(filePath, html, 'utf-8')
|
|
353
|
+
|
|
354
|
+
const report = await this.page.evaluate(
|
|
355
|
+
(sels) => {
|
|
356
|
+
const result = { answers: {}, stops: {}, inputs: {} }
|
|
357
|
+
for (const s of sels.answers) {
|
|
358
|
+
result.answers[s] = document.querySelectorAll(s).length
|
|
359
|
+
}
|
|
360
|
+
for (const s of sels.stops) {
|
|
361
|
+
result.stops[s] = document.querySelectorAll(s).length
|
|
362
|
+
}
|
|
363
|
+
for (const s of sels.inputs) {
|
|
364
|
+
result.inputs[s] = document.querySelectorAll(s).length
|
|
365
|
+
}
|
|
366
|
+
return result
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
answers: ANSWER_SELECTORS,
|
|
370
|
+
stops: STOP_SELECTORS,
|
|
371
|
+
inputs: INPUT_SELECTORS,
|
|
372
|
+
},
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
return { file: filePath, selectors: report }
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ---------- список чатов ----------
|
|
379
|
+
|
|
380
|
+
async _ensureSidebarOpen() {
|
|
381
|
+
const toggles = [
|
|
382
|
+
'button[aria-label*="sidebar" i]',
|
|
383
|
+
'button[aria-label*="история" i]',
|
|
384
|
+
'button[aria-label*="history" i]',
|
|
385
|
+
'button[class*="sidebar-toggle"]',
|
|
386
|
+
'button[class*="sidebarToggle"]',
|
|
387
|
+
]
|
|
388
|
+
for (const sel of toggles) {
|
|
389
|
+
try {
|
|
390
|
+
const btn = this.page.locator(sel).first()
|
|
391
|
+
if ((await btn.count()) === 0) continue
|
|
392
|
+
if (!(await btn.isVisible().catch(() => false))) continue
|
|
393
|
+
await btn.click({ timeout: 1500 })
|
|
394
|
+
await this.page.waitForTimeout(500)
|
|
395
|
+
return
|
|
396
|
+
} catch {}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async listChats(limit = 30) {
|
|
401
|
+
await this._ensureSidebarOpen()
|
|
402
|
+
return await this.page.evaluate((lim) => {
|
|
403
|
+
const out = []
|
|
404
|
+
const seen = new Set()
|
|
405
|
+
const anchors = document.querySelectorAll('a[href*="/chat/"]')
|
|
406
|
+
for (const a of anchors) {
|
|
407
|
+
const href = a.getAttribute('href') || ''
|
|
408
|
+
const m =
|
|
409
|
+
href.match(/\/chat\/s\/([a-zA-Z0-9_-]+)/) ||
|
|
410
|
+
href.match(/\/a\/chat\/s\/([a-zA-Z0-9_-]+)/)
|
|
411
|
+
if (!m) continue
|
|
412
|
+
const id = m[1]
|
|
413
|
+
if (seen.has(id)) continue
|
|
414
|
+
seen.add(id)
|
|
415
|
+
|
|
416
|
+
const titleEl = a.querySelector('[class*="title"], [class*="text"]')
|
|
417
|
+
let title = (titleEl ? titleEl.textContent : a.textContent) || ''
|
|
418
|
+
title = title.trim().replace(/\s+/g, ' ')
|
|
419
|
+
if (!title) title = '(без названия)'
|
|
420
|
+
|
|
421
|
+
out.push({ id, title, href })
|
|
422
|
+
if (out.length >= lim) break
|
|
423
|
+
}
|
|
424
|
+
return out
|
|
425
|
+
}, limit)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async openChat(id) {
|
|
429
|
+
const candidates = [`a[href$="/chat/s/${id}"]`, `a[href*="${id}"]`]
|
|
430
|
+
for (const sel of candidates) {
|
|
431
|
+
try {
|
|
432
|
+
const loc = this.page.locator(sel).first()
|
|
433
|
+
if ((await loc.count()) === 0) continue
|
|
434
|
+
await loc.click({ timeout: 3000 })
|
|
435
|
+
await this.page.waitForTimeout(1500)
|
|
436
|
+
return true
|
|
437
|
+
} catch {}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
try {
|
|
441
|
+
await this.page.goto(`https://chat.deepseek.com/a/chat/s/${id}`, {
|
|
442
|
+
waitUntil: 'domcontentloaded',
|
|
443
|
+
timeout: 20_000,
|
|
444
|
+
})
|
|
445
|
+
await this.page.waitForTimeout(1500)
|
|
446
|
+
return true
|
|
447
|
+
} catch (e) {
|
|
448
|
+
throw new Error(`Не удалось открыть чат ${id}: ${e.message}`)
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async getCurrentChatId() {
|
|
453
|
+
try {
|
|
454
|
+
const url = this.page.url()
|
|
455
|
+
const m = url.match(/\/chat\/s\/([a-zA-Z0-9_-]+)/)
|
|
456
|
+
return m ? m[1] : null
|
|
457
|
+
} catch {
|
|
458
|
+
return null
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async close() {
|
|
463
|
+
try {
|
|
464
|
+
if (this.context) await this.context.close()
|
|
465
|
+
} catch {}
|
|
466
|
+
await killStaleChrome()
|
|
467
|
+
}
|
|
468
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import os from 'os'
|
|
4
|
+
|
|
5
|
+
const ZAMES_HOME = path.join(os.homedir(), '.zames')
|
|
6
|
+
const HOME_CONFIG = path.join(ZAMES_HOME, 'config.json')
|
|
7
|
+
const PROJECT_CONFIG = path.join(process.cwd(), '.zamesrc.json')
|
|
8
|
+
|
|
9
|
+
const DEFAULTS = {
|
|
10
|
+
maxIterations: 40,
|
|
11
|
+
headless: false,
|
|
12
|
+
debug: false,
|
|
13
|
+
browserChannel: null,
|
|
14
|
+
|
|
15
|
+
confirmation: {
|
|
16
|
+
write: true,
|
|
17
|
+
edit: true,
|
|
18
|
+
bash: true,
|
|
19
|
+
alwaysConfirm: [
|
|
20
|
+
'rm\\s+-rf',
|
|
21
|
+
'rmdir\\s+/s',
|
|
22
|
+
'del\\s+/[sqf]',
|
|
23
|
+
'format\\s+[a-z]:',
|
|
24
|
+
'shutdown',
|
|
25
|
+
'reg\\s+delete',
|
|
26
|
+
'remove-item.*-recurse',
|
|
27
|
+
'git\\s+push\\s+--force',
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
undo: {
|
|
32
|
+
enabled: true,
|
|
33
|
+
maxBackups: 200,
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
transcript: {
|
|
37
|
+
enabled: true,
|
|
38
|
+
dir: path.join(os.homedir(), '.zames', 'logs'),
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
browser: {
|
|
42
|
+
answerTimeoutMs: 180000,
|
|
43
|
+
askRetries: 3,
|
|
44
|
+
stabilityChecks: 3,
|
|
45
|
+
stabilityDelayMs: 1000,
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function loadConfig() {
|
|
50
|
+
const merged = JSON.parse(JSON.stringify(DEFAULTS))
|
|
51
|
+
for (const file of [HOME_CONFIG, PROJECT_CONFIG]) {
|
|
52
|
+
try {
|
|
53
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf-8'))
|
|
54
|
+
deepMerge(merged, data)
|
|
55
|
+
} catch (e) {
|
|
56
|
+
if (e.code !== 'ENOENT') {
|
|
57
|
+
console.error(`config: не удалось прочитать ${file}: ${e.message}`)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return merged
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function deepMerge(target, source) {
|
|
65
|
+
for (const key of Object.keys(source)) {
|
|
66
|
+
if (
|
|
67
|
+
source[key] &&
|
|
68
|
+
typeof source[key] === 'object' &&
|
|
69
|
+
!Array.isArray(source[key])
|
|
70
|
+
) {
|
|
71
|
+
target[key] = target[key] || {}
|
|
72
|
+
deepMerge(target[key], source[key])
|
|
73
|
+
} else {
|
|
74
|
+
target[key] = source[key]
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const CONFIG_PATHS = { HOME_CONFIG, PROJECT_CONFIG }
|
|
80
|
+
export { ZAMES_HOME }
|
package/src/confirm.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as readlinePromises from 'readline/promises'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { unifiedDiff, colorDiff } from './diff.js'
|
|
4
|
+
|
|
5
|
+
export class ConfirmManager {
|
|
6
|
+
constructor({ config }) {
|
|
7
|
+
const c = config || {}
|
|
8
|
+
this.settings = {
|
|
9
|
+
write: c.write !== false,
|
|
10
|
+
edit: c.edit !== false,
|
|
11
|
+
bash: c.bash !== false,
|
|
12
|
+
}
|
|
13
|
+
this.alwaysConfirm = (c.alwaysConfirm || []).map((p) => new RegExp(p, 'i'))
|
|
14
|
+
this.allowedForSession = new Set()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
_matchesAlwaysConfirm(command) {
|
|
18
|
+
return this.alwaysConfirm.some((re) => re.test(command))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
shouldAsk(kind, target = null) {
|
|
22
|
+
if (kind === 'bash' && target && this._matchesAlwaysConfirm(target)) {
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
if (!this.settings[kind]) return false
|
|
26
|
+
if (this.allowedForSession.has(kind)) return false
|
|
27
|
+
return true
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async ask({ kind, target, preview = null, message }) {
|
|
31
|
+
if (!this.shouldAsk(kind, target)) return true
|
|
32
|
+
|
|
33
|
+
if (preview) {
|
|
34
|
+
console.log(preview)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const label = message || `Разрешить ${kind}?`
|
|
38
|
+
const question =
|
|
39
|
+
chalk.yellow(`${label} `) +
|
|
40
|
+
chalk.gray('[') +
|
|
41
|
+
chalk.green('y') +
|
|
42
|
+
chalk.gray(' = да, ') +
|
|
43
|
+
chalk.red('n') +
|
|
44
|
+
chalk.gray(' = нет, ') +
|
|
45
|
+
chalk.cyan('a') +
|
|
46
|
+
chalk.gray(' = всегда для этого типа] ')
|
|
47
|
+
|
|
48
|
+
const answer = await this._prompt(question)
|
|
49
|
+
|
|
50
|
+
if (answer === 'a') {
|
|
51
|
+
if (kind !== 'bash' || !this._matchesAlwaysConfirm(target)) {
|
|
52
|
+
this.allowedForSession.add(kind)
|
|
53
|
+
}
|
|
54
|
+
return true
|
|
55
|
+
}
|
|
56
|
+
return answer === 'y'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async _prompt(question) {
|
|
60
|
+
process.stdin.resume()
|
|
61
|
+
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
62
|
+
process.stdin.setRawMode(false)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const rl = readlinePromises.createInterface({
|
|
66
|
+
input: process.stdin,
|
|
67
|
+
output: process.stdout,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const ans = await rl.question(question)
|
|
72
|
+
const v = (ans || '').trim().toLowerCase()
|
|
73
|
+
if (v === 'a' || v === 'always' || v === 'в') return 'a'
|
|
74
|
+
if (v === 'y' || v === 'yes' || v === 'д' || v === 'да') return 'y'
|
|
75
|
+
return 'n'
|
|
76
|
+
} finally {
|
|
77
|
+
rl.close()
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function formatDiffPreview(oldText, newText, { label } = {}) {
|
|
83
|
+
const diff = unifiedDiff(oldText, newText, { label })
|
|
84
|
+
return colorDiff(diff, chalk)
|
|
85
|
+
}
|