zames_pro 2.5.1 → 2.7.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 +32 -0
- package/dist/agent-loop.js +224 -49
- package/dist/browser.js +41 -42
- package/dist/config-menu.js +231 -0
- package/dist/config.js +143 -0
- package/dist/gitTools.js +4 -4
- package/dist/i18n.js +280 -0
- package/dist/index.js +548 -319
- package/dist/input.js +137 -67
- package/dist/markdown.js +1 -1
- package/dist/net-capture.js +20 -20
- package/dist/self-review.js +16 -16
- package/dist/sessions.js +11 -11
- package/dist/spinner.js +30 -52
- package/dist/system-prompt.js +31 -2
- package/dist/theme.js +26 -26
- package/dist/tools.js +9 -9
- package/dist/transcript.js +2 -2
- package/dist/types.js +2 -2
- package/dist/web.js +16 -16
- package/dist/xml-toolcall.js +14 -14
- package/package.json +1 -1
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// Interactive settings menu for the /config command.
|
|
2
|
+
//
|
|
3
|
+
// The menu renders a list of parameters grouped by sections and lets you
|
|
4
|
+
// change them with the arrow keys: boolean/enum toggle in place, number/string
|
|
5
|
+
// prompt for input. All strings come from i18n (cfg.*), so the menu is
|
|
6
|
+
// fully localized.
|
|
7
|
+
//
|
|
8
|
+
// The module doesn't know about config.ts directly (except for the schema
|
|
9
|
+
// types): the caller reads and writes values through callbacks. This makes the
|
|
10
|
+
// menu easy to test and reuse.
|
|
11
|
+
import { theme } from './theme.js';
|
|
12
|
+
const ESC = String.fromCharCode(27);
|
|
13
|
+
// Strip ANSI sequences to compute the visible length of a line.
|
|
14
|
+
function stripAnsi(s) {
|
|
15
|
+
return s.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '');
|
|
16
|
+
}
|
|
17
|
+
function displayValue(field, value, t) {
|
|
18
|
+
if (value === undefined)
|
|
19
|
+
return t('cfg.menu.default');
|
|
20
|
+
if (typeof value === 'boolean')
|
|
21
|
+
return value ? t('common.on') : t('common.off');
|
|
22
|
+
return String(value);
|
|
23
|
+
}
|
|
24
|
+
function groupLabel(field, t) {
|
|
25
|
+
return t(field.groupKey);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Show the interactive menu. Returns a promise that resolves when the
|
|
29
|
+
* user exits (q/Esc/Ctrl+C). In a non-TTY it throws — the caller
|
|
30
|
+
* must show a text list.
|
|
31
|
+
*/
|
|
32
|
+
export function runConfigMenu(opts) {
|
|
33
|
+
const input = opts.input || process.stdin;
|
|
34
|
+
const output = opts.output || process.stdout;
|
|
35
|
+
const { fields, t, get, set, reset } = opts;
|
|
36
|
+
if (!input.isTTY || !input.setRawMode) {
|
|
37
|
+
return Promise.reject(new Error(t('cfg.menu.notty')));
|
|
38
|
+
}
|
|
39
|
+
let cursor = 0;
|
|
40
|
+
let editing = false;
|
|
41
|
+
let editBuf = '';
|
|
42
|
+
let message = null;
|
|
43
|
+
let exited = false;
|
|
44
|
+
let resolveDone = null;
|
|
45
|
+
const wasRaw = input.isRaw;
|
|
46
|
+
input.setRawMode(true);
|
|
47
|
+
input.resume();
|
|
48
|
+
output.write(ESC + '[?25l'); // hide the cursor while rendering
|
|
49
|
+
// Menu rendering: lines go top to bottom, and we return the cursor to the
|
|
50
|
+
// start of the block via relative moves so we don't spawn blank lines.
|
|
51
|
+
let lastRows = 0;
|
|
52
|
+
const render = () => {
|
|
53
|
+
const lines = [];
|
|
54
|
+
lines.push(theme.bold(t('cfg.menu.title')));
|
|
55
|
+
let lastGroup = '';
|
|
56
|
+
for (let i = 0; i < fields.length; i++) {
|
|
57
|
+
const f = fields[i];
|
|
58
|
+
const g = groupLabel(f, t);
|
|
59
|
+
if (g !== lastGroup) {
|
|
60
|
+
lines.push(theme.system(' ' + g));
|
|
61
|
+
lastGroup = g;
|
|
62
|
+
}
|
|
63
|
+
const selected = i === cursor;
|
|
64
|
+
const marker = selected ? theme.prompt('\u276f ') : ' ';
|
|
65
|
+
const label = selected ? theme.prompt(f.labelKey ? t(f.labelKey) : f.path) : t(f.labelKey || f.path);
|
|
66
|
+
const val = displayValue(f, get(f.path), t);
|
|
67
|
+
const valText = selected ? theme.assistant(val) : theme.dim(val);
|
|
68
|
+
const pathText = selected ? theme.dim(' ' + f.path) : '';
|
|
69
|
+
lines.push(marker + label + theme.dim(' = ') + valText + pathText);
|
|
70
|
+
}
|
|
71
|
+
if (editing) {
|
|
72
|
+
lines.push('');
|
|
73
|
+
lines.push(theme.user(t('cfg.menu.edit_hint')));
|
|
74
|
+
lines.push(theme.prompt('\u276f ') + editBuf);
|
|
75
|
+
}
|
|
76
|
+
else if (message) {
|
|
77
|
+
lines.push('');
|
|
78
|
+
lines.push(theme.assistant(message));
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
lines.push('');
|
|
82
|
+
lines.push(theme.dim(t('cfg.menu.hint')));
|
|
83
|
+
}
|
|
84
|
+
// Erase the previous block and print the new one. We count VISUAL lines
|
|
85
|
+
// (accounting for wrapping by terminal width), otherwise on narrow
|
|
86
|
+
// terminals you get "garbage" from unerased tails.
|
|
87
|
+
const cols = output.columns || 80;
|
|
88
|
+
const rowsOf = (s) => {
|
|
89
|
+
const len = stripAnsi(s).length;
|
|
90
|
+
return Math.max(1, Math.ceil(len / Math.max(1, cols)));
|
|
91
|
+
};
|
|
92
|
+
if (lastRows > 0) {
|
|
93
|
+
output.write(ESC + '[' + lastRows + 'A');
|
|
94
|
+
for (let i = 0; i < lastRows; i++) {
|
|
95
|
+
output.write(ESC + '[2K' + ESC + '[1B');
|
|
96
|
+
}
|
|
97
|
+
output.write(ESC + '[' + lastRows + 'A');
|
|
98
|
+
}
|
|
99
|
+
output.write(lines.join('\n') + '\n');
|
|
100
|
+
lastRows = lines.reduce((n, l) => n + rowsOf(l), 0);
|
|
101
|
+
};
|
|
102
|
+
const cleanup = () => {
|
|
103
|
+
if (exited)
|
|
104
|
+
return;
|
|
105
|
+
exited = true;
|
|
106
|
+
input.removeListener('data', onData);
|
|
107
|
+
if (input.setRawMode)
|
|
108
|
+
input.setRawMode(wasRaw || false);
|
|
109
|
+
output.write(ESC + '[?25h'); // restore the cursor
|
|
110
|
+
if (resolveDone)
|
|
111
|
+
resolveDone();
|
|
112
|
+
};
|
|
113
|
+
const commitEdit = () => {
|
|
114
|
+
const f = fields[cursor];
|
|
115
|
+
const raw = editBuf.trim();
|
|
116
|
+
editing = false;
|
|
117
|
+
editBuf = '';
|
|
118
|
+
if (!raw) {
|
|
119
|
+
message = null;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
set(f, raw);
|
|
124
|
+
message = t('cfg.menu.saved', {
|
|
125
|
+
v: f.path,
|
|
126
|
+
value: displayValue(f, get(f.path), t),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
message = String(e.message || e);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
const toggle = () => {
|
|
134
|
+
const f = fields[cursor];
|
|
135
|
+
if (f.type === 'boolean') {
|
|
136
|
+
const cur = get(f.path);
|
|
137
|
+
set(f, cur === true ? 'false' : 'true');
|
|
138
|
+
message = null;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (f.type === 'enum' && f.values && f.values.length) {
|
|
142
|
+
const cur = String(get(f.path) ?? '');
|
|
143
|
+
const idx = f.values.indexOf(cur);
|
|
144
|
+
const next = f.values[(idx + 1) % f.values.length];
|
|
145
|
+
set(f, next);
|
|
146
|
+
message = null;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// number/string — prompt for input.
|
|
150
|
+
editing = true;
|
|
151
|
+
editBuf = get(f.path) === undefined ? '' : String(get(f.path));
|
|
152
|
+
};
|
|
153
|
+
const onData = (buf) => {
|
|
154
|
+
const s = buf.toString('utf-8');
|
|
155
|
+
if (editing) {
|
|
156
|
+
// Process input character by character: the terminal may send several
|
|
157
|
+
// bytes at once (paste, auto-repeat), Enter is 0x0d/0x0a, backspace is 0x7f.
|
|
158
|
+
for (const ch of s) {
|
|
159
|
+
if (ch === '\r' || ch === '\n') {
|
|
160
|
+
commitEdit();
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
if (ch === ESC) {
|
|
164
|
+
editing = false;
|
|
165
|
+
editBuf = '';
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
if (ch === '\x03') {
|
|
169
|
+
cleanup();
|
|
170
|
+
output.write('\n');
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (ch === '\x7f' || ch === '\b') {
|
|
174
|
+
editBuf = editBuf.slice(0, -1);
|
|
175
|
+
}
|
|
176
|
+
else if (ch >= ' ') {
|
|
177
|
+
editBuf += ch;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
render();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// The terminal may send several keys in one packet (fast typing,
|
|
184
|
+
// auto-repeat, automation). We parse the buffer into tokens: first
|
|
185
|
+
// escape sequences (arrows), then single characters.
|
|
186
|
+
let i = 0;
|
|
187
|
+
while (i < s.length) {
|
|
188
|
+
if (s.startsWith('\x1b[A', i)) {
|
|
189
|
+
cursor = (cursor - 1 + fields.length) % fields.length;
|
|
190
|
+
message = null;
|
|
191
|
+
i += 3;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (s.startsWith('\x1b[B', i)) {
|
|
195
|
+
cursor = (cursor + 1) % fields.length;
|
|
196
|
+
message = null;
|
|
197
|
+
i += 3;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const ch = s[i];
|
|
201
|
+
if (ch === 'q' || ch === 'Q' || ch === '\x03' || ch === ESC) {
|
|
202
|
+
cleanup();
|
|
203
|
+
output.write('\n');
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (ch === 'k') {
|
|
207
|
+
cursor = (cursor - 1 + fields.length) % fields.length;
|
|
208
|
+
message = null;
|
|
209
|
+
}
|
|
210
|
+
else if (ch === 'j') {
|
|
211
|
+
cursor = (cursor + 1) % fields.length;
|
|
212
|
+
message = null;
|
|
213
|
+
}
|
|
214
|
+
else if (ch === '\r' || ch === '\n' || ch === ' ') {
|
|
215
|
+
toggle();
|
|
216
|
+
}
|
|
217
|
+
else if (ch === 'd' || ch === 'D') {
|
|
218
|
+
const f = fields[cursor];
|
|
219
|
+
reset(f);
|
|
220
|
+
message = t('cfg.reset', { v: f.path });
|
|
221
|
+
}
|
|
222
|
+
i++;
|
|
223
|
+
}
|
|
224
|
+
render();
|
|
225
|
+
};
|
|
226
|
+
return new Promise((resolve) => {
|
|
227
|
+
resolveDone = resolve;
|
|
228
|
+
input.on('data', onData);
|
|
229
|
+
render();
|
|
230
|
+
});
|
|
231
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
|
+
import { DEFAULT_LOCALE } from './i18n.js';
|
|
4
5
|
const ZAMES_HOME = path.join(os.homedir(), '.zames');
|
|
5
6
|
const HOME_CONFIG = path.join(ZAMES_HOME, 'config.json');
|
|
6
7
|
const PROJECT_CONFIG = path.join(process.cwd(), '.zamesrc.json');
|
|
@@ -41,6 +42,9 @@ export const DEFAULTS = {
|
|
|
41
42
|
rateLimitWaitMs: 300000,
|
|
42
43
|
maxRateLimitRetries: 6,
|
|
43
44
|
},
|
|
45
|
+
ui: {
|
|
46
|
+
locale: DEFAULT_LOCALE,
|
|
47
|
+
},
|
|
44
48
|
};
|
|
45
49
|
export function loadConfig() {
|
|
46
50
|
const merged = JSON.parse(JSON.stringify(DEFAULTS));
|
|
@@ -71,5 +75,144 @@ function deepMerge(target, source) {
|
|
|
71
75
|
}
|
|
72
76
|
}
|
|
73
77
|
}
|
|
78
|
+
export const CONFIG_SCHEMA = [
|
|
79
|
+
{ path: 'ui.locale', type: 'enum', values: ['ru', 'en'], labelKey: 'cfg.f.ui_locale', groupKey: 'cfg.group.ui' },
|
|
80
|
+
{ path: 'maxIterations', type: 'number', min: 1, max: 1000, labelKey: 'cfg.f.maxIterations', groupKey: 'cfg.group.agent' },
|
|
81
|
+
{ path: 'headless', type: 'boolean', labelKey: 'cfg.f.headless', groupKey: 'cfg.group.agent' },
|
|
82
|
+
{ path: 'debug', type: 'boolean', labelKey: 'cfg.f.debug', groupKey: 'cfg.group.agent' },
|
|
83
|
+
{ path: 'hotReload', type: 'boolean', labelKey: 'cfg.f.hotReload', groupKey: 'cfg.group.agent' },
|
|
84
|
+
{ path: 'confirmation.write', type: 'boolean', labelKey: 'cfg.f.confirmation_write', groupKey: 'cfg.group.confirmation' },
|
|
85
|
+
{ path: 'confirmation.edit', type: 'boolean', labelKey: 'cfg.f.confirmation_edit', groupKey: 'cfg.group.confirmation' },
|
|
86
|
+
{ path: 'confirmation.bash', type: 'boolean', labelKey: 'cfg.f.confirmation_bash', groupKey: 'cfg.group.confirmation' },
|
|
87
|
+
{ path: 'undo.enabled', type: 'boolean', labelKey: 'cfg.f.undo_enabled', groupKey: 'cfg.group.undo' },
|
|
88
|
+
{ path: 'undo.maxBackups', type: 'number', min: 1, max: 100000, labelKey: 'cfg.f.undo_maxBackups', groupKey: 'cfg.group.undo' },
|
|
89
|
+
{ path: 'transcript.enabled', type: 'boolean', labelKey: 'cfg.f.transcript_enabled', groupKey: 'cfg.group.transcript' },
|
|
90
|
+
{ path: 'browser.answerTimeoutMs', type: 'number', min: 1000, max: 3600000, labelKey: 'cfg.f.browser_answerTimeoutMs', groupKey: 'cfg.group.browser' },
|
|
91
|
+
{ path: 'browser.askRetries', type: 'number', min: 1, max: 100, labelKey: 'cfg.f.browser_askRetries', groupKey: 'cfg.group.browser' },
|
|
92
|
+
{ path: 'browser.stabilityChecks', type: 'number', min: 1, max: 100, labelKey: 'cfg.f.browser_stabilityChecks', groupKey: 'cfg.group.browser' },
|
|
93
|
+
{ path: 'browser.stabilityDelayMs', type: 'number', min: 0, max: 60000, labelKey: 'cfg.f.browser_stabilityDelayMs', groupKey: 'cfg.group.browser' },
|
|
94
|
+
{ path: 'browser.minSendIntervalMs', type: 'number', min: 0, max: 600000, labelKey: 'cfg.f.browser_minSendIntervalMs', groupKey: 'cfg.group.browser' },
|
|
95
|
+
{ path: 'browser.rateLimitWaitMs', type: 'number', min: 0, max: 3600000, labelKey: 'cfg.f.browser_rateLimitWaitMs', groupKey: 'cfg.group.browser' },
|
|
96
|
+
{ path: 'browser.maxRateLimitRetries', type: 'number', min: 0, max: 100, labelKey: 'cfg.f.browser_maxRateLimitRetries', groupKey: 'cfg.group.browser' },
|
|
97
|
+
];
|
|
98
|
+
export function getConfigField(path) {
|
|
99
|
+
return CONFIG_SCHEMA.find((f) => f.path === path);
|
|
100
|
+
}
|
|
101
|
+
export function getByPath(obj, path) {
|
|
102
|
+
const parts = path.split('.');
|
|
103
|
+
let cur = obj;
|
|
104
|
+
for (const p of parts) {
|
|
105
|
+
if (cur === null || cur === undefined || typeof cur !== 'object') {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
cur = cur[p];
|
|
109
|
+
}
|
|
110
|
+
return cur;
|
|
111
|
+
}
|
|
112
|
+
export function setByPath(obj, path, value) {
|
|
113
|
+
const parts = path.split('.');
|
|
114
|
+
let cur = obj;
|
|
115
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
116
|
+
const p = parts[i];
|
|
117
|
+
if (typeof cur[p] !== 'object' || cur[p] === null)
|
|
118
|
+
cur[p] = {};
|
|
119
|
+
cur = cur[p];
|
|
120
|
+
}
|
|
121
|
+
cur[parts[parts.length - 1]] = value;
|
|
122
|
+
}
|
|
123
|
+
function parseConfigValue(field, raw) {
|
|
124
|
+
switch (field.type) {
|
|
125
|
+
case 'boolean': {
|
|
126
|
+
const s = raw.trim().toLowerCase();
|
|
127
|
+
if (['true', '1', 'yes', 'y', 'on', 'да', 'вкл'].includes(s))
|
|
128
|
+
return true;
|
|
129
|
+
if (['false', '0', 'no', 'n', 'off', 'нет', 'выкл'].includes(s))
|
|
130
|
+
return false;
|
|
131
|
+
throw new Error('boolean');
|
|
132
|
+
}
|
|
133
|
+
case 'number': {
|
|
134
|
+
const n = Number(raw);
|
|
135
|
+
if (!Number.isFinite(n))
|
|
136
|
+
throw new Error('number');
|
|
137
|
+
if (field.min !== undefined && n < field.min)
|
|
138
|
+
throw new Error('number');
|
|
139
|
+
if (field.max !== undefined && n > field.max)
|
|
140
|
+
throw new Error('number');
|
|
141
|
+
return n;
|
|
142
|
+
}
|
|
143
|
+
case 'enum': {
|
|
144
|
+
const s = raw.trim().toLowerCase();
|
|
145
|
+
if (field.values && !field.values.includes(s))
|
|
146
|
+
throw new Error('enum');
|
|
147
|
+
return s;
|
|
148
|
+
}
|
|
149
|
+
default:
|
|
150
|
+
return raw;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export function validateConfigValue(field, raw) {
|
|
154
|
+
return parseConfigValue(field, raw);
|
|
155
|
+
}
|
|
156
|
+
export function configPathFor(scope) {
|
|
157
|
+
return scope === 'project' ? PROJECT_CONFIG : HOME_CONFIG;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Read only the specified config file (without merging with defaults), so that
|
|
161
|
+
* writing does not "freeze" all defaults into the user's file.
|
|
162
|
+
*/
|
|
163
|
+
export function readConfigFile(file) {
|
|
164
|
+
try {
|
|
165
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
166
|
+
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
167
|
+
return data;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch (e) {
|
|
171
|
+
const err = e;
|
|
172
|
+
if (err.code !== 'ENOENT') {
|
|
173
|
+
throw new Error(`не удалось прочитать ${file}: ${err.message}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return {};
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Write a setting value to the file. Returns the final object that was
|
|
180
|
+
* written. The value is first validated against the schema.
|
|
181
|
+
*/
|
|
182
|
+
export function writeConfigValue(scope, key, raw) {
|
|
183
|
+
const field = getConfigField(key);
|
|
184
|
+
if (!field)
|
|
185
|
+
throw new Error(`unknown setting: ${key}`);
|
|
186
|
+
const value = parseConfigValue(field, raw);
|
|
187
|
+
const file = configPathFor(scope);
|
|
188
|
+
const data = readConfigFile(file);
|
|
189
|
+
setByPath(data, key, value);
|
|
190
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
191
|
+
fs.writeFileSync(file, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
192
|
+
return { value, file };
|
|
193
|
+
}
|
|
194
|
+
/** Reset a setting to the default (remove the key from the file). */
|
|
195
|
+
export function resetConfigValue(scope, key) {
|
|
196
|
+
const field = getConfigField(key);
|
|
197
|
+
if (!field)
|
|
198
|
+
throw new Error(`unknown setting: ${key}`);
|
|
199
|
+
const file = configPathFor(scope);
|
|
200
|
+
const data = readConfigFile(file);
|
|
201
|
+
const parts = key.split('.');
|
|
202
|
+
let cur = data;
|
|
203
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
204
|
+
const p = parts[i];
|
|
205
|
+
if (!cur || typeof cur[p] !== 'object' || cur[p] === null) {
|
|
206
|
+
cur = undefined;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
cur = cur[p];
|
|
210
|
+
}
|
|
211
|
+
if (cur)
|
|
212
|
+
delete cur[parts[parts.length - 1]];
|
|
213
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
214
|
+
fs.writeFileSync(file, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
215
|
+
return file;
|
|
216
|
+
}
|
|
74
217
|
export const CONFIG_PATHS = { HOME_CONFIG, PROJECT_CONFIG };
|
|
75
218
|
export { ZAMES_HOME };
|
package/dist/gitTools.js
CHANGED
|
@@ -36,8 +36,8 @@ export function runGit(cmd, cwd, timeout = 15_000) {
|
|
|
36
36
|
});
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
|
-
//
|
|
40
|
-
//
|
|
39
|
+
// Safely quotes paths/arguments for passing to the shell.
|
|
40
|
+
// String or array -> a string with double quotes and escaping.
|
|
41
41
|
function quoteArgs(input) {
|
|
42
42
|
const list = Array.isArray(input) ? input : [input];
|
|
43
43
|
return list
|
|
@@ -46,7 +46,7 @@ function quoteArgs(input) {
|
|
|
46
46
|
.map((v) => JSON.stringify(v))
|
|
47
47
|
.join(' ');
|
|
48
48
|
}
|
|
49
|
-
//
|
|
49
|
+
// Checks whether the directory is a git repository and gathers the context.
|
|
50
50
|
export async function getGitContext(workdir) {
|
|
51
51
|
const probe = await runGit('git rev-parse --is-inside-work-tree', workdir, 5000);
|
|
52
52
|
if (probe.trim() !== 'true')
|
|
@@ -175,7 +175,7 @@ export function createGitTools(workdir) {
|
|
|
175
175
|
if (!msg.trim()) {
|
|
176
176
|
return 'Ошибка: message пустой.';
|
|
177
177
|
}
|
|
178
|
-
//
|
|
178
|
+
// We stage everything only if the index is empty (as the description promises).
|
|
179
179
|
const staged = await runGit('git diff --cached --name-only', workdir, 10_000);
|
|
180
180
|
const hasStaged = !/^\s*/.test(staged) &&
|
|
181
181
|
staged.trim() !== '(команда выполнена, вывода нет)';
|