create-confluence-sync 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 +111 -0
- package/package.json +25 -0
- package/src/agents-md.js +273 -0
- package/src/api.js +207 -0
- package/src/cli.js +496 -0
- package/src/git.js +137 -0
- package/src/hook.js +118 -0
- package/src/sync.js +278 -0
- package/src/tree.js +171 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { execSync } from 'node:child_process';
|
|
6
|
+
import { input, password, select, confirm, checkbox } from '@inquirer/prompts';
|
|
7
|
+
import { createApiClient } from './api.js';
|
|
8
|
+
import { loadTree, saveTree, unhidePage } from './tree.js';
|
|
9
|
+
import { initialPull, pull, push, detectHidden } from './sync.js';
|
|
10
|
+
import { initRepo, installHook, commitAll } from './git.js';
|
|
11
|
+
import { generateAgentsMd } from './agents-md.js';
|
|
12
|
+
|
|
13
|
+
const projectRoot = process.cwd();
|
|
14
|
+
|
|
15
|
+
function parseArgs() {
|
|
16
|
+
const args = { positional: [] };
|
|
17
|
+
const argv = process.argv.slice(2);
|
|
18
|
+
|
|
19
|
+
for (let i = 0; i < argv.length; i++) {
|
|
20
|
+
if (argv[i] === '--url' && argv[i + 1]) {
|
|
21
|
+
args.url = argv[++i];
|
|
22
|
+
} else if (argv[i] === '--token' && argv[i + 1]) {
|
|
23
|
+
args.token = argv[++i];
|
|
24
|
+
} else if (argv[i] === '--space' && argv[i + 1]) {
|
|
25
|
+
args.space = argv[++i];
|
|
26
|
+
} else if (argv[i] === '--yes' || argv[i] === '-y') {
|
|
27
|
+
args.yes = true;
|
|
28
|
+
} else if (!argv[i].startsWith('-')) {
|
|
29
|
+
args.positional.push(argv[i]);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return args;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function loadConfig() {
|
|
37
|
+
const configPath = path.join(projectRoot, '.confluence', 'config.json');
|
|
38
|
+
if (!fs.existsSync(configPath)) {
|
|
39
|
+
console.error('Error: .confluence/config.json not found. Run setup first (node cli.js).');
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function printBanner() {
|
|
46
|
+
console.log('');
|
|
47
|
+
console.log('╔══════════════════════════════════════╗');
|
|
48
|
+
console.log('║ Confluence Sync — Setup ║');
|
|
49
|
+
console.log('╚══════════════════════════════════════╝');
|
|
50
|
+
console.log('');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// --- Command handlers ---
|
|
54
|
+
|
|
55
|
+
async function cmdSpaces() {
|
|
56
|
+
const config = loadConfig();
|
|
57
|
+
const apiClient = createApiClient(config);
|
|
58
|
+
const spaces = await apiClient.getSpaces();
|
|
59
|
+
const result = spaces.map((s) => ({ key: s.key, name: s.name }));
|
|
60
|
+
console.log(JSON.stringify(result, null, 2));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function cmdTree() {
|
|
64
|
+
const config = loadConfig();
|
|
65
|
+
const apiClient = createApiClient(config);
|
|
66
|
+
const pages = await apiClient.getPageTree(config.space);
|
|
67
|
+
console.log(JSON.stringify(pages, null, 2));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function cmdPage(pageId) {
|
|
71
|
+
if (!pageId) {
|
|
72
|
+
console.error('Usage: node cli.js page <pageId>');
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
const config = loadConfig();
|
|
76
|
+
const apiClient = createApiClient(config);
|
|
77
|
+
const content = await apiClient.getPageContent(pageId);
|
|
78
|
+
console.log(JSON.stringify(content, null, 2));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function cmdSync(file) {
|
|
82
|
+
const config = loadConfig();
|
|
83
|
+
const apiClient = createApiClient(config);
|
|
84
|
+
const tree = loadTree(projectRoot);
|
|
85
|
+
|
|
86
|
+
// Redirect console.log to stderr so sync progress logs don't pollute JSON stdout
|
|
87
|
+
const origLog = console.log;
|
|
88
|
+
console.log = (...args) => console.error(...args);
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
let result;
|
|
92
|
+
detectHidden(tree, projectRoot);
|
|
93
|
+
if (file) {
|
|
94
|
+
result = await push(apiClient, tree, projectRoot, config.space, [file]);
|
|
95
|
+
} else {
|
|
96
|
+
result = await pull(apiClient, tree, projectRoot, config.space);
|
|
97
|
+
}
|
|
98
|
+
saveTree(projectRoot, tree);
|
|
99
|
+
|
|
100
|
+
// Restore and output clean JSON to stdout
|
|
101
|
+
console.log = origLog;
|
|
102
|
+
console.log(JSON.stringify(result, null, 2));
|
|
103
|
+
} catch (err) {
|
|
104
|
+
console.log = origLog;
|
|
105
|
+
throw err;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function cmdDelete(pageId) {
|
|
110
|
+
if (!pageId) {
|
|
111
|
+
console.error('Usage: node cli.js delete <pageId>');
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
const config = loadConfig();
|
|
115
|
+
const apiClient = createApiClient(config);
|
|
116
|
+
const result = await apiClient.deletePage(pageId);
|
|
117
|
+
console.log(JSON.stringify(result, null, 2));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function cmdLocalTree() {
|
|
121
|
+
const tree = loadTree(projectRoot);
|
|
122
|
+
console.log(JSON.stringify(tree, null, 2));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function buildHiddenTree(tree) {
|
|
126
|
+
// Collect all hidden pages
|
|
127
|
+
const hiddenPages = {};
|
|
128
|
+
for (const [pageId, page] of Object.entries(tree.pages)) {
|
|
129
|
+
if (page.hidden) {
|
|
130
|
+
hiddenPages[pageId] = page;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (Object.keys(hiddenPages).length === 0) return [];
|
|
135
|
+
|
|
136
|
+
const hiddenIds = new Set(Object.keys(hiddenPages));
|
|
137
|
+
|
|
138
|
+
// Build children map (only among hidden pages)
|
|
139
|
+
const childrenMap = {}; // parentId -> [pageId, ...]
|
|
140
|
+
const roots = [];
|
|
141
|
+
|
|
142
|
+
for (const pageId of Object.keys(hiddenPages)) {
|
|
143
|
+
const page = hiddenPages[pageId];
|
|
144
|
+
const parentId = page.parentId ? String(page.parentId) : null;
|
|
145
|
+
|
|
146
|
+
// If parent is also hidden, this is a child node; otherwise it's a root
|
|
147
|
+
if (parentId && hiddenIds.has(parentId)) {
|
|
148
|
+
if (!childrenMap[parentId]) childrenMap[parentId] = [];
|
|
149
|
+
childrenMap[parentId].push(pageId);
|
|
150
|
+
} else {
|
|
151
|
+
roots.push(pageId);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Recursively flatten tree into choices with indentation
|
|
156
|
+
const choices = [];
|
|
157
|
+
|
|
158
|
+
function walk(pageId, depth) {
|
|
159
|
+
const page = hiddenPages[pageId];
|
|
160
|
+
const indent = ' '.repeat(depth);
|
|
161
|
+
choices.push({
|
|
162
|
+
name: `${indent}${page.title}`,
|
|
163
|
+
value: pageId,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const children = childrenMap[pageId] || [];
|
|
167
|
+
// Sort children alphabetically by title
|
|
168
|
+
children.sort((a, b) => hiddenPages[a].title.localeCompare(hiddenPages[b].title));
|
|
169
|
+
for (const childId of children) {
|
|
170
|
+
walk(childId, depth + 1);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Sort roots alphabetically
|
|
175
|
+
roots.sort((a, b) => hiddenPages[a].title.localeCompare(hiddenPages[b].title));
|
|
176
|
+
for (const rootId of roots) {
|
|
177
|
+
walk(rootId, 0);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return choices;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function findPagesByTitle(tree, title) {
|
|
184
|
+
// Find all hidden pages where title matches OR any ancestor title matches
|
|
185
|
+
const result = [];
|
|
186
|
+
|
|
187
|
+
for (const [pageId, page] of Object.entries(tree.pages)) {
|
|
188
|
+
if (!page.hidden) continue;
|
|
189
|
+
|
|
190
|
+
// Check if this page's title matches
|
|
191
|
+
if (page.title === title) {
|
|
192
|
+
result.push(pageId);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Check if any ancestor has the matching title
|
|
197
|
+
let currentId = page.parentId ? String(page.parentId) : null;
|
|
198
|
+
while (currentId) {
|
|
199
|
+
const ancestor = tree.pages[currentId];
|
|
200
|
+
if (!ancestor) break;
|
|
201
|
+
if (ancestor.title === title) {
|
|
202
|
+
result.push(pageId);
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
currentId = ancestor.parentId ? String(ancestor.parentId) : null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function cmdRestore(name) {
|
|
213
|
+
const config = loadConfig();
|
|
214
|
+
const apiClient = createApiClient(config);
|
|
215
|
+
const tree = loadTree(projectRoot);
|
|
216
|
+
|
|
217
|
+
// Step 1: sync (pull) to bring tree up to date
|
|
218
|
+
console.log('Синхронизация...');
|
|
219
|
+
detectHidden(tree, projectRoot);
|
|
220
|
+
await pull(apiClient, tree, projectRoot, config.space);
|
|
221
|
+
saveTree(projectRoot, tree);
|
|
222
|
+
|
|
223
|
+
if (name) {
|
|
224
|
+
// --- Command mode: restore by title ---
|
|
225
|
+
const pageIds = findPagesByTitle(tree, name);
|
|
226
|
+
|
|
227
|
+
if (pageIds.length === 0) {
|
|
228
|
+
console.log(`Нет скрытых страниц с названием "${name}".`);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
for (const pageId of pageIds) {
|
|
233
|
+
console.log(`Восстановлено: ${tree.pages[pageId].title}`);
|
|
234
|
+
unhidePage(tree, pageId);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
saveTree(projectRoot, tree);
|
|
238
|
+
|
|
239
|
+
// Re-sync to download restored pages
|
|
240
|
+
console.log('Скачивание восстановленных страниц...');
|
|
241
|
+
await pull(apiClient, tree, projectRoot, config.space);
|
|
242
|
+
saveTree(projectRoot, tree);
|
|
243
|
+
|
|
244
|
+
console.log(`Восстановлено страниц: ${pageIds.length}`);
|
|
245
|
+
} else {
|
|
246
|
+
// --- Interactive mode ---
|
|
247
|
+
const choices = buildHiddenTree(tree);
|
|
248
|
+
|
|
249
|
+
if (choices.length === 0) {
|
|
250
|
+
console.log('Нет скрытых страниц.');
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const selected = await checkbox({
|
|
255
|
+
message: 'Выберите страницы для восстановления:',
|
|
256
|
+
choices,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
if (selected.length === 0) {
|
|
260
|
+
console.log('Ничего не выбрано.');
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
for (const pageId of selected) {
|
|
265
|
+
console.log(`Восстановлено: ${tree.pages[pageId].title}`);
|
|
266
|
+
unhidePage(tree, pageId);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
saveTree(projectRoot, tree);
|
|
270
|
+
|
|
271
|
+
// Re-sync to download restored pages
|
|
272
|
+
console.log('Скачивание восстановленных страниц...');
|
|
273
|
+
await pull(apiClient, tree, projectRoot, config.space);
|
|
274
|
+
saveTree(projectRoot, tree);
|
|
275
|
+
|
|
276
|
+
console.log(`Восстановлено страниц: ${selected.length}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// --- Setup wizard (existing flow) ---
|
|
281
|
+
|
|
282
|
+
async function setupWizard(args) {
|
|
283
|
+
printBanner();
|
|
284
|
+
|
|
285
|
+
// --- Шаг 1-2: URL и Token ---
|
|
286
|
+
|
|
287
|
+
const baseUrl = args.url || await input({
|
|
288
|
+
message: 'Confluence URL:',
|
|
289
|
+
validate: (v) => v.startsWith('http') || 'URL должен начинаться с http/https',
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const token = args.token || await password({
|
|
293
|
+
message: 'Personal Access Token:',
|
|
294
|
+
mask: '*',
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// --- Шаг 3: Проверка подключения ---
|
|
298
|
+
|
|
299
|
+
const apiClient = createApiClient({ baseUrl, token });
|
|
300
|
+
|
|
301
|
+
let spaces;
|
|
302
|
+
try {
|
|
303
|
+
console.log('');
|
|
304
|
+
console.log('Проверяю подключение...');
|
|
305
|
+
spaces = await apiClient.getSpaces();
|
|
306
|
+
|
|
307
|
+
if (!spaces || spaces.length === 0) {
|
|
308
|
+
throw new Error('Нет доступных пространств. Проверьте токен и права доступа.');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
console.log(`Подключено. Доступно пространств: ${spaces.length}`);
|
|
312
|
+
console.log('');
|
|
313
|
+
} catch (err) {
|
|
314
|
+
console.error(`\nОшибка подключения: ${err.message}`);
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// --- Шаг 4: Выбор space ---
|
|
319
|
+
|
|
320
|
+
let spaceKey;
|
|
321
|
+
|
|
322
|
+
if (args.space) {
|
|
323
|
+
const found = spaces.find((s) => s.key === args.space);
|
|
324
|
+
if (!found) {
|
|
325
|
+
console.error(`Space "${args.space}" не найден. Доступные: ${spaces.map((s) => s.key).join(', ')}`);
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
spaceKey = args.space;
|
|
329
|
+
} else {
|
|
330
|
+
spaceKey = await select({
|
|
331
|
+
message: 'Выберите пространство:',
|
|
332
|
+
choices: spaces.map((s) => ({
|
|
333
|
+
name: `${s.key} — ${s.name}`,
|
|
334
|
+
value: s.key,
|
|
335
|
+
})),
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const spaceName = spaces.find((s) => s.key === spaceKey)?.name || spaceKey;
|
|
340
|
+
|
|
341
|
+
// --- Шаг 5: Подтверждение ---
|
|
342
|
+
|
|
343
|
+
console.log('');
|
|
344
|
+
console.log('Будет создана синхронизация:');
|
|
345
|
+
console.log(` URL: ${baseUrl}`);
|
|
346
|
+
console.log(` Space: ${spaceKey} — ${spaceName}`);
|
|
347
|
+
console.log(` Директория: ./docs/${spaceKey}/`);
|
|
348
|
+
console.log('');
|
|
349
|
+
|
|
350
|
+
if (!args.yes) {
|
|
351
|
+
const ok = await confirm({ message: 'Продолжить?' });
|
|
352
|
+
if (!ok) {
|
|
353
|
+
console.log('Отменено.');
|
|
354
|
+
process.exit(0);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// --- Шаг 6: Инициализация ---
|
|
359
|
+
|
|
360
|
+
console.log('');
|
|
361
|
+
console.log('Начинаю синхронизацию...');
|
|
362
|
+
console.log('');
|
|
363
|
+
|
|
364
|
+
// 6.1 Создать директорию docs/{spaceKey}/
|
|
365
|
+
const docsDir = path.join(projectRoot, 'docs', spaceKey);
|
|
366
|
+
|
|
367
|
+
if (!fs.existsSync(docsDir)) {
|
|
368
|
+
fs.mkdirSync(docsDir, { recursive: true });
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// 6.2 Записать config.json
|
|
372
|
+
const configDir = path.join(projectRoot, '.confluence');
|
|
373
|
+
|
|
374
|
+
if (!fs.existsSync(configDir)) {
|
|
375
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const config = { baseUrl, token, space: spaceKey };
|
|
379
|
+
fs.writeFileSync(path.join(configDir, 'config.json'), JSON.stringify(config, null, 2), 'utf-8');
|
|
380
|
+
|
|
381
|
+
// 6.3 Первичный pull
|
|
382
|
+
const tree = {
|
|
383
|
+
space: spaceKey,
|
|
384
|
+
baseUrl,
|
|
385
|
+
lastSync: null,
|
|
386
|
+
pages: {},
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
let pageCount;
|
|
390
|
+
try {
|
|
391
|
+
pageCount = await initialPull(apiClient, tree, projectRoot, spaceKey);
|
|
392
|
+
} catch (err) {
|
|
393
|
+
console.error(`\nОшибка при загрузке страниц: ${err.message}`);
|
|
394
|
+
process.exit(1);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// 6.4 Сохранить tree.json
|
|
398
|
+
saveTree(projectRoot, tree);
|
|
399
|
+
|
|
400
|
+
// 6.5 npm init + install package
|
|
401
|
+
try {
|
|
402
|
+
console.log('Установка зависимостей...');
|
|
403
|
+
execSync('npm init -y', { cwd: projectRoot, stdio: 'ignore' });
|
|
404
|
+
execSync('npm install create-confluence-sync', { cwd: projectRoot, stdio: 'inherit' });
|
|
405
|
+
} catch (err) {
|
|
406
|
+
console.error(`\nОшибка установки пакета: ${err.message}`);
|
|
407
|
+
process.exit(1);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// 6.6 git init
|
|
411
|
+
try {
|
|
412
|
+
initRepo(projectRoot);
|
|
413
|
+
} catch (err) {
|
|
414
|
+
console.error(`\nОшибка инициализации git: ${err.message}`);
|
|
415
|
+
process.exit(1);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// 6.6 .gitignore
|
|
419
|
+
fs.writeFileSync(
|
|
420
|
+
path.join(projectRoot, '.gitignore'),
|
|
421
|
+
'node_modules/\n.confluence/config.json\n',
|
|
422
|
+
'utf-8'
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
// 6.7 Установить hook
|
|
426
|
+
try {
|
|
427
|
+
installHook(projectRoot);
|
|
428
|
+
} catch (err) {
|
|
429
|
+
console.error(`\nОшибка установки git hook: ${err.message}`);
|
|
430
|
+
process.exit(1);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// 6.8 Генерация AGENTS.md
|
|
434
|
+
try {
|
|
435
|
+
generateAgentsMd(projectRoot, config, tree);
|
|
436
|
+
} catch (err) {
|
|
437
|
+
console.error(`\nОшибка генерации AGENTS.md: ${err.message}`);
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// 6.9 Начальный коммит
|
|
442
|
+
try {
|
|
443
|
+
commitAll(projectRoot, 'Initial confluence sync');
|
|
444
|
+
} catch (err) {
|
|
445
|
+
console.error(`\nОшибка начального коммита: ${err.message}`);
|
|
446
|
+
process.exit(1);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// --- Шаг 7: Done ---
|
|
450
|
+
|
|
451
|
+
console.log('');
|
|
452
|
+
console.log(`✓ Синхронизация настроена!`);
|
|
453
|
+
console.log(` Директория: ./docs/${spaceKey}/`);
|
|
454
|
+
console.log(` Страниц: ${pageCount}`);
|
|
455
|
+
console.log('');
|
|
456
|
+
console.log(' Теперь можно редактировать файлы и коммитить — синхронизация автоматическая.');
|
|
457
|
+
console.log('');
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// --- Main ---
|
|
461
|
+
|
|
462
|
+
const COMMANDS = {
|
|
463
|
+
spaces: cmdSpaces,
|
|
464
|
+
tree: cmdTree,
|
|
465
|
+
page: cmdPage,
|
|
466
|
+
sync: cmdSync,
|
|
467
|
+
delete: cmdDelete,
|
|
468
|
+
'local-tree': cmdLocalTree,
|
|
469
|
+
restore: cmdRestore,
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
async function run() {
|
|
473
|
+
const args = parseArgs();
|
|
474
|
+
const command = args.positional[0];
|
|
475
|
+
|
|
476
|
+
if (command && COMMANDS[command]) {
|
|
477
|
+
const handler = COMMANDS[command];
|
|
478
|
+
const cmdArgs = args.positional.slice(1);
|
|
479
|
+
|
|
480
|
+
// Commands that take an argument pass it; others get no args
|
|
481
|
+
if (command === 'page' || command === 'delete') {
|
|
482
|
+
await handler(cmdArgs[0]);
|
|
483
|
+
} else if (command === 'sync' || command === 'restore') {
|
|
484
|
+
await handler(cmdArgs[0]); // undefined if no arg = default mode
|
|
485
|
+
} else {
|
|
486
|
+
await handler();
|
|
487
|
+
}
|
|
488
|
+
} else {
|
|
489
|
+
await setupWizard(args);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
run().catch((err) => {
|
|
494
|
+
console.error(`\nНеожиданная ошибка: ${err.message}`);
|
|
495
|
+
process.exit(1);
|
|
496
|
+
});
|
package/src/git.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { writeFileSync, chmodSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
function exec(projectRoot, command) {
|
|
6
|
+
try {
|
|
7
|
+
return execSync(command, { cwd: projectRoot, encoding: 'utf-8' }).trim();
|
|
8
|
+
} catch (err) {
|
|
9
|
+
const stderr = err.stderr ? err.stderr.toString().trim() : err.message;
|
|
10
|
+
throw new Error(`Git command failed: ${command}\n${stderr}`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getChangedFiles(projectRoot) {
|
|
15
|
+
const output = exec(projectRoot, 'git diff --name-only HEAD~1 HEAD');
|
|
16
|
+
if (!output) return [];
|
|
17
|
+
return output.split('\n').filter(f => f.endsWith('.xhtml'));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getDeletedFiles(projectRoot) {
|
|
21
|
+
const output = exec(projectRoot, 'git diff --name-only --diff-filter=D HEAD~1 HEAD');
|
|
22
|
+
if (!output) return [];
|
|
23
|
+
return output.split('\n').filter(f => f.endsWith('.xhtml'));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getChangedFilesUncommitted(projectRoot) {
|
|
27
|
+
const output = exec(projectRoot, 'git diff --name-only HEAD');
|
|
28
|
+
if (!output) return [];
|
|
29
|
+
return output.split('\n').filter(f => f.endsWith('.xhtml'));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function createBranch(projectRoot, name) {
|
|
33
|
+
try {
|
|
34
|
+
exec(projectRoot, `git checkout -b ${name}`);
|
|
35
|
+
} catch {
|
|
36
|
+
exec(projectRoot, `git checkout ${name}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function switchBranch(projectRoot, name) {
|
|
41
|
+
exec(projectRoot, `git checkout ${name}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getCurrentBranch(projectRoot) {
|
|
45
|
+
return exec(projectRoot, 'git rev-parse --abbrev-ref HEAD');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function commitAll(projectRoot, message) {
|
|
49
|
+
exec(projectRoot, 'git add -A');
|
|
50
|
+
|
|
51
|
+
const status = exec(projectRoot, 'git status --porcelain');
|
|
52
|
+
if (!status) return false;
|
|
53
|
+
|
|
54
|
+
const escaped = message.replace(/"/g, '\\"');
|
|
55
|
+
try {
|
|
56
|
+
execSync(`git commit -m "${escaped}"`, {
|
|
57
|
+
cwd: projectRoot,
|
|
58
|
+
encoding: 'utf-8',
|
|
59
|
+
env: { ...process.env, CONFLUENCE_SYNC_RUNNING: '1' },
|
|
60
|
+
});
|
|
61
|
+
} catch (err) {
|
|
62
|
+
const stderr = err.stderr ? err.stderr.toString().trim() : err.message;
|
|
63
|
+
throw new Error(`Git commit failed: ${stderr}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function mergeBranch(projectRoot, source) {
|
|
70
|
+
try {
|
|
71
|
+
exec(projectRoot, `git merge ${source}`);
|
|
72
|
+
return true;
|
|
73
|
+
} catch (err) {
|
|
74
|
+
if (err.message.includes('CONFLICT') || err.message.includes('conflict')) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Merge conflict while merging "${source}". Resolve conflicts manually and commit the result.`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function deleteBranch(projectRoot, name) {
|
|
84
|
+
exec(projectRoot, `git branch -D ${name}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function stageFiles(projectRoot, files) {
|
|
88
|
+
if (!files || files.length === 0) return;
|
|
89
|
+
const paths = files.map(f => `"${f}"`).join(' ');
|
|
90
|
+
exec(projectRoot, `git add ${paths}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function installHook(projectRoot) {
|
|
94
|
+
const hookContent = `#!/bin/sh
|
|
95
|
+
if [ "$CONFLUENCE_SYNC_RUNNING" = "1" ]; then
|
|
96
|
+
exit 0
|
|
97
|
+
fi
|
|
98
|
+
export CONFLUENCE_SYNC_RUNNING=1
|
|
99
|
+
node "$(dirname "$0")/../../node_modules/create-confluence-sync/src/hook.js" "$(git rev-parse --show-toplevel)"
|
|
100
|
+
`;
|
|
101
|
+
|
|
102
|
+
const hookPath = join(projectRoot, '.git', 'hooks', 'post-commit');
|
|
103
|
+
writeFileSync(hookPath, hookContent, 'utf-8');
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
chmodSync(hookPath, 0o755);
|
|
107
|
+
} catch {
|
|
108
|
+
// chmod may not work on Windows, hook file is still written
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function hasChanges(projectRoot) {
|
|
113
|
+
const output = exec(projectRoot, 'git status --porcelain');
|
|
114
|
+
return output.length > 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function initRepo(projectRoot) {
|
|
118
|
+
exec(projectRoot, 'git init');
|
|
119
|
+
exec(projectRoot, 'git config core.longpaths true');
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export {
|
|
124
|
+
getChangedFiles,
|
|
125
|
+
getDeletedFiles,
|
|
126
|
+
getChangedFilesUncommitted,
|
|
127
|
+
createBranch,
|
|
128
|
+
switchBranch,
|
|
129
|
+
getCurrentBranch,
|
|
130
|
+
commitAll,
|
|
131
|
+
mergeBranch,
|
|
132
|
+
deleteBranch,
|
|
133
|
+
stageFiles,
|
|
134
|
+
installHook,
|
|
135
|
+
hasChanges,
|
|
136
|
+
initRepo,
|
|
137
|
+
};
|