aiag-cli 2.12.0 → 2.13.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.
@@ -1,31 +1,31 @@
1
1
  /**
2
- * sync 명령어
2
+ * sync 명령어 (GitHub 중심)
3
3
  *
4
- * aiag-adp 플랫폼과 로컬 feature_list.json 양방향 동기화
4
+ * GitHub를 Single Source of Truth로 두고 자동으로 동기화합니다.
5
5
  *
6
6
  * 사용법:
7
- * aiag sync 기본 동기화 (로컬 서버)
8
- * aiag sync --pull 서버 로컬 동기화
9
- * aiag sync --force 충돌 로컬 우선
10
- * aiag sync --watch 실시간 이벤트 스트리밍
11
- * aiag sync --status 동기화 상태 확인
7
+ * aiag sync Git 상태 감지 자동 pull/push
8
+ * aiag sync --status 동기화 상태 확인
9
+ * aiag sync --reset GitHub 상태로 로컬 초기화 (원격 우선)
10
+ * aiag sync --watch 실시간 이벤트 스트리밍
12
11
  *
13
- * 동기화 방향:
14
- * - 기본: 로컬 변경사항을 서버에 반영
15
- * - --pull: 서버 상태를 로컬에 반영
16
- * - 양방향: 충돌 감지 해결 옵션 제공
12
+ * 동작 방식:
13
+ * - up-to-date: "이미 최신 상태"
14
+ * - behind: git pull 자동 실행
15
+ * - ahead: git push 자동 실행
16
+ * - diverged: 경고 메시지 + --reset 권장
17
17
  */
18
18
  import chalk from 'chalk';
19
19
  import ora from 'ora';
20
- import { isLoggedIn } from '../auth/credentials.js';
21
- import { isConnected, loadConnection, updateLastSyncAt, isProjectInitialized, loadPendingSync, clearPendingSync, getPendingSyncCount, } from '../utils/connection.js';
22
- import { readFeatureList, writeFeatureList, getProgress, } from '../utils/featureList.js';
23
- import { readProgress } from '../utils/progress.js';
24
- import { getApiClient, ApiError, NetworkError } from '../api/client.js';
25
- import { printError, printWarning, printInfo } from '../utils/output.js';
20
+ import { isLoggedIn, loadCredentials } from '../auth/credentials.js';
21
+ import { isConnected, loadConnection, updateLastSyncAt, isProjectInitialized, } from '../utils/connection.js';
22
+ import { getApiClient } from '../api/client.js';
23
+ import { printError, printSuccess, printWarning, printInfo } from '../utils/output.js';
26
24
  import { messages } from '../utils/messages.js';
27
25
  import { SSEClient, formatSSEEvent } from '../utils/sseClient.js';
28
- import { getRemoteSyncStatus, gitPull, gitPush } from '../utils/git.js';
26
+ import { getRemoteSyncStatus, gitPull, gitPush, gitFetch, gitResetHard, } from '../utils/git.js';
27
+ import { readProgress } from '../utils/progress.js';
28
+ import { getProgress } from '../utils/featureList.js';
29
29
  // ============================================
30
30
  // 메인 명령어 함수
31
31
  // ============================================
@@ -33,7 +33,7 @@ import { getRemoteSyncStatus, gitPull, gitPush } from '../utils/git.js';
33
33
  * sync 명령어 실행
34
34
  */
35
35
  export async function sync(options) {
36
- // 1. 동기화 상태 확인
36
+ // 1. 상태 확인
37
37
  if (options.status) {
38
38
  await handleSyncStatus();
39
39
  return;
@@ -43,240 +43,165 @@ export async function sync(options) {
43
43
  await handleWatch();
44
44
  return;
45
45
  }
46
- // 3. 사전 조건 확인
47
- if (!checkPrerequisites()) {
46
+ // 3. GitHub 상태로 초기화
47
+ if (options.reset) {
48
+ await handleReset();
48
49
  return;
49
50
  }
50
- // 4. Git 상태 확인 및 처리 (--skip-git이 아닌 경우)
51
- if (!options.skipGit) {
52
- const gitHandled = await handleGitSync(options);
53
- if (!gitHandled) {
54
- // Git 동기화 실패 또는 사용자가 중단
55
- return;
56
- }
57
- }
58
- // 5. 동기화 방향에 따른 처리
59
- if (options.pull) {
60
- await handlePullSync(options);
61
- }
62
- else {
63
- await handlePushSync(options);
64
- }
51
+ // 4. 자동 동기화 (기본 동작)
52
+ await handleAutoSync();
65
53
  }
66
54
  // ============================================
67
- // 사전 조건 확인
55
+ // 자동 동기화 (기본 동작)
68
56
  // ============================================
69
57
  /**
70
- * 동기화 전제 조건 확인
58
+ * Git 상태를 감지하여 자동으로 pull/push 수행
71
59
  */
72
- function checkPrerequisites() {
60
+ async function handleAutoSync() {
73
61
  // 프로젝트 초기화 확인
74
62
  if (!isProjectInitialized()) {
75
63
  printError(messages.common.notInitialized);
76
64
  console.log(chalk.gray(messages.common.runInitFirst));
77
65
  process.exitCode = 1;
78
- return false;
66
+ return;
79
67
  }
80
- // 로그인 확인
81
- if (!isLoggedIn()) {
82
- printError(messages.login.serverRequired);
83
- console.log(chalk.gray('먼저 로그인하세요: aiag login'));
84
- process.exitCode = 1;
85
- return false;
68
+ const spinner = ora('Git 상태 확인 중...').start();
69
+ try {
70
+ // Git fetch로 원격 상태 동기화
71
+ await gitFetch();
72
+ const status = await getRemoteSyncStatus();
73
+ spinner.stop();
74
+ // 상태 출력
75
+ printGitSyncStatus(status);
76
+ switch (status.status) {
77
+ case 'no-remote':
78
+ printInfo('원격 저장소가 설정되지 않았습니다.');
79
+ console.log(chalk.gray(' git remote add origin <url> 로 원격 저장소를 설정하세요.'));
80
+ break;
81
+ case 'no-upstream':
82
+ printInfo('원격 브랜치 트래킹이 설정되지 않았습니다.');
83
+ console.log(chalk.gray(' git push -u origin ' + (status.branch || 'main') + ' 로 설정하세요.'));
84
+ break;
85
+ case 'up-to-date':
86
+ printSuccess('이미 최신 상태입니다.');
87
+ await notifyServerCache();
88
+ break;
89
+ case 'behind':
90
+ await performGitPull(status);
91
+ break;
92
+ case 'ahead':
93
+ await performGitPush(status);
94
+ break;
95
+ case 'diverged':
96
+ printDivergedWarning(status);
97
+ break;
98
+ }
86
99
  }
87
- // 프로젝트 연결 확인
88
- if (!isConnected()) {
89
- printError('웹 플랫폼에 연결되지 않았습니다.');
90
- console.log(chalk.gray('먼저 연결하세요: aiag connect <project-url>'));
100
+ catch (error) {
101
+ spinner.stop();
102
+ if (error instanceof Error) {
103
+ printError(`Git 동기화 실패: ${error.message}`);
104
+ }
105
+ else {
106
+ printError('Git 동기화 중 오류가 발생했습니다.');
107
+ }
91
108
  process.exitCode = 1;
92
- return false;
93
109
  }
94
- return true;
95
110
  }
96
111
  // ============================================
97
- // Push 동기화 (로컬 서버)
112
+ // Reset 처리 (GitHub 상태로 초기화)
98
113
  // ============================================
99
114
  /**
100
- * 로컬 서버 동기화
115
+ * GitHub 상태로 로컬 초기화 (--reset)
101
116
  */
102
- async function handlePushSync(options) {
103
- const connection = loadConnection();
104
- const featureList = readFeatureList();
105
- if (!featureList) {
106
- printError('feature_list.json을 읽을 수 없습니다.');
117
+ async function handleReset() {
118
+ if (!isProjectInitialized()) {
119
+ printError(messages.common.notInitialized);
120
+ console.log(chalk.gray(messages.common.runInitFirst));
107
121
  process.exitCode = 1;
108
122
  return;
109
123
  }
110
- const spinner = ora('동기화 준비 중...').start();
124
+ const spinner = ora('GitHub 상태 확인 중...').start();
111
125
  try {
112
- const client = getApiClient();
113
- if (!client) {
114
- spinner.fail();
115
- printError('API 클라이언트를 초기화할 수 없습니다.');
126
+ // Git fetch로 원격 상태 동기화
127
+ await gitFetch();
128
+ const status = await getRemoteSyncStatus();
129
+ if (status.status === 'no-remote') {
130
+ spinner.fail('원격 저장소가 설정되지 않았습니다.');
116
131
  process.exitCode = 1;
117
132
  return;
118
133
  }
119
- // 로컬 Feature를 동기화 데이터로 변환
120
- const syncData = featureList.features.map(localToSyncFeature);
121
- if (options.dryRun) {
122
- spinner.stop();
123
- printDryRunResult(syncData);
134
+ if (status.status === 'no-upstream') {
135
+ spinner.fail('원격 브랜치 트래킹이 설정되지 않았습니다.');
136
+ process.exitCode = 1;
124
137
  return;
125
138
  }
126
- spinner.text = `${syncData.length}개 Feature 동기화 중...`;
127
- // 서버에 동기화 요청
128
- const response = await client.syncFeatures(connection.projectId, {
129
- features: syncData,
130
- forceOverwrite: options.force,
131
- });
132
- spinner.succeed('동기화 완료');
133
- // 결과 출력
134
- printSyncResult(response, 'push');
135
- // 충돌 처리
136
- if (response.conflicts.length > 0 && !options.force) {
137
- printConflicts(response.conflicts);
139
+ const branch = status.branch || 'main';
140
+ spinner.text = `GitHub 상태로 초기화 중... (origin/${branch})`;
141
+ // git reset --hard origin/{branch}
142
+ const result = await gitResetHard(`origin/${branch}`);
143
+ if (result.success) {
144
+ spinner.succeed(`GitHub 상태로 초기화 완료 (origin/${branch})`);
145
+ await notifyServerCache();
138
146
  }
139
- // 서버 Feature로 로컬 업데이트 (서버에서 생성된 ID 등 반영)
140
- if (response.serverFeatures.length > 0) {
141
- updateLocalFromServer(featureList, response.serverFeatures);
142
- }
143
- // Progress.md 동기화
144
- await syncProgressToServer(client, connection.projectId, spinner);
145
- // 마지막 동기화 시간 업데이트
146
- updateLastSyncAt();
147
- // 미동기화 항목 정리
148
- clearPendingSync();
149
- }
150
- catch (error) {
151
- spinner.fail();
152
- handleSyncError(error);
153
- }
154
- }
155
- // ============================================
156
- // Pull 동기화 (서버 → 로컬)
157
- // ============================================
158
- /**
159
- * 서버 → 로컬 동기화
160
- */
161
- async function handlePullSync(options) {
162
- const connection = loadConnection();
163
- const localFeatureList = readFeatureList();
164
- const spinner = ora('서버에서 데이터 가져오는 중...').start();
165
- try {
166
- const client = getApiClient();
167
- if (!client) {
168
- spinner.fail();
169
- printError('API 클라이언트를 초기화할 수 없습니다.');
147
+ else {
148
+ spinner.fail('초기화 실패');
149
+ if (result.stderr) {
150
+ printError(result.stderr);
151
+ }
170
152
  process.exitCode = 1;
171
- return;
172
153
  }
173
- // 서버에서 프로젝트 컨텍스트 조회
174
- const context = await client.getProjectContext(connection.projectId);
175
- const serverFeatures = context.features;
176
- if (options.dryRun) {
177
- spinner.stop();
178
- printPullDryRunResult(serverFeatures, localFeatureList);
179
- return;
180
- }
181
- spinner.text = `${serverFeatures.length}개 Feature 동기화 중...`;
182
- // 서버 Feature를 로컬 형식으로 변환
183
- const localFeatures = serverFeatures.map(serverToLocalFeature);
184
- // 로컬 feature_list.json 업데이트
185
- const updatedFeatureList = {
186
- project: localFeatureList?.project ?? {
187
- name: context.project.name,
188
- version: '1.0.0',
189
- description: context.project.description ?? '',
190
- totalFeatures: serverFeatures.length,
191
- completedFeatures: serverFeatures.filter(f => f.passes).length,
192
- },
193
- features: localFeatures,
194
- };
195
- // 프로젝트 정보 업데이트
196
- updatedFeatureList.project.totalFeatures = localFeatures.length;
197
- updatedFeatureList.project.completedFeatures = localFeatures.filter(f => f.passes).length;
198
- writeFeatureList(updatedFeatureList);
199
- spinner.succeed('동기화 완료');
200
- // 결과 출력
201
- console.log('');
202
- console.log(` ${chalk.green('가져옴:')} ${serverFeatures.length}개 Feature`);
203
- console.log(` ${chalk.blue('완료:')} ${updatedFeatureList.project.completedFeatures}개`);
204
- console.log(` ${chalk.gray('대기:')} ${updatedFeatureList.project.totalFeatures - updatedFeatureList.project.completedFeatures}개`);
205
- // 마지막 동기화 시간 업데이트
206
- updateLastSyncAt();
207
- clearPendingSync();
208
154
  }
209
155
  catch (error) {
210
- spinner.fail();
211
- handleSyncError(error);
156
+ spinner.fail('초기화 중 오류 발생');
157
+ if (error instanceof Error) {
158
+ printError(error.message);
159
+ }
160
+ process.exitCode = 1;
212
161
  }
213
162
  }
214
163
  // ============================================
215
164
  // 동기화 상태 확인
216
165
  // ============================================
217
166
  /**
218
- * 동기화 상태 표시
167
+ * 동기화 상태 표시 (--status)
219
168
  */
220
169
  async function handleSyncStatus() {
221
170
  if (!isProjectInitialized()) {
222
171
  printInfo('프로젝트가 초기화되지 않았습니다.');
223
172
  return;
224
173
  }
225
- const connection = loadConnection();
226
- const pendingCount = getPendingSyncCount();
227
- const pendingItems = loadPendingSync();
228
174
  console.log('');
229
- console.log(chalk.bold('동기화 상태'));
175
+ console.log(chalk.bold('📊 동기화 상태'));
230
176
  console.log('');
231
- if (!connection) {
232
- console.log(` ${chalk.yellow('⚠')} 플랫폼에 연결되지 않음`);
233
- console.log(chalk.gray(' 연결하려면: aiag connect <project-url>'));
234
- return;
177
+ // Git 상태 확인
178
+ const spinner = ora('Git 상태 확인 중...').start();
179
+ try {
180
+ await gitFetch();
181
+ const status = await getRemoteSyncStatus();
182
+ spinner.stop();
183
+ printGitSyncStatus(status);
184
+ printStatusSummary(status);
235
185
  }
236
- // 연결 정보
237
- console.log(` 프로젝트: ${chalk.cyan(connection.projectName)}`);
238
- console.log(` 서버: ${chalk.gray(connection.serverUrl)}`);
239
- console.log(` 마지막 동기화: ${chalk.gray(formatDate(connection.lastSyncAt))}`);
240
- // 미동기화 항목
241
- console.log('');
242
- if (pendingCount > 0) {
243
- console.log(` ${chalk.yellow('⚠')} 미동기화 항목: ${chalk.yellow(pendingCount)}개`);
244
- console.log('');
245
- for (const item of pendingItems.slice(0, 5)) {
246
- const actionIcon = item.action === 'create' ? '+' : item.action === 'update' ? '~' : '✓';
247
- const actionColor = item.action === 'create' ? chalk.green : item.action === 'update' ? chalk.yellow : chalk.blue;
248
- console.log(` ${actionColor(actionIcon)} ${item.featureId} (${item.action})`);
249
- }
250
- if (pendingItems.length > 5) {
251
- console.log(chalk.gray(` ... 외 ${pendingItems.length - 5}개`));
186
+ catch (error) {
187
+ spinner.stop();
188
+ printWarning('Git 상태 확인 실패');
189
+ if (error instanceof Error) {
190
+ console.log(chalk.gray(` ${error.message}`));
252
191
  }
253
- console.log('');
254
- console.log(chalk.gray(' 동기화하려면: aiag sync'));
255
192
  }
256
- else {
257
- console.log(` ${chalk.green('✓')} 모든 변경사항이 동기화됨`);
193
+ // 서버 연결 상태
194
+ const connection = loadConnection();
195
+ console.log('');
196
+ console.log(chalk.bold('🔗 서버 연결'));
197
+ if (connection && isConnected()) {
198
+ console.log(` 프로젝트: ${chalk.cyan(connection.projectName)}`);
199
+ console.log(` 서버: ${chalk.gray(connection.serverUrl)}`);
200
+ console.log(` 마지막 동기화: ${chalk.gray(formatDate(connection.lastSyncAt))}`);
258
201
  }
259
- // 서버 연결 상태 확인
260
- if (isLoggedIn()) {
261
- const spinner = ora('서버 상태 확인 중...').start();
262
- try {
263
- const client = getApiClient();
264
- if (client) {
265
- const context = await client.getProjectContext(connection.projectId);
266
- spinner.stop();
267
- console.log('');
268
- console.log(chalk.bold('서버 상태'));
269
- console.log(` 총 Feature: ${chalk.yellow(context.stats.totalFeatures)}개`);
270
- console.log(` 완료: ${chalk.green(context.stats.completedFeatures)}개`);
271
- console.log(` 진행률: ${chalk.cyan(context.stats.completionRate.toFixed(1))}%`);
272
- }
273
- else {
274
- spinner.warn('서버 연결 필요');
275
- }
276
- }
277
- catch {
278
- spinner.warn('서버 상태 확인 실패');
279
- }
202
+ else {
203
+ console.log(chalk.gray(' 연결되지 않음'));
204
+ console.log(chalk.gray(' 연결하려면: aiag connect'));
280
205
  }
281
206
  console.log('');
282
207
  }
@@ -284,32 +209,40 @@ async function handleSyncStatus() {
284
209
  // 실시간 이벤트 스트리밍
285
210
  // ============================================
286
211
  /**
287
- * 실시간 이벤트 스트리밍 (watch 모드)
212
+ * 실시간 이벤트 스트리밍 (--watch)
288
213
  */
289
214
  async function handleWatch() {
290
- if (!checkPrerequisites()) {
215
+ // 사전 조건 확인
216
+ if (!isProjectInitialized()) {
217
+ printError(messages.common.notInitialized);
218
+ process.exitCode = 1;
291
219
  return;
292
220
  }
293
- const connection = loadConnection();
294
- const client = getApiClient();
295
- if (!client) {
296
- printError('API 클라이언트를 초기화할 수 없습니다.');
221
+ if (!isLoggedIn()) {
222
+ printError(messages.login.serverRequired);
223
+ console.log(chalk.gray('먼저 로그인하세요: aiag login'));
297
224
  process.exitCode = 1;
298
225
  return;
299
226
  }
300
- console.log('');
301
- console.log(chalk.bold('실시간 이벤트 스트리밍'));
302
- console.log(chalk.gray(`프로젝트: ${connection.projectName}`));
303
- console.log(chalk.gray('종료하려면 Ctrl+C를 누르세요.'));
304
- console.log('');
305
- // SSE 클라이언트 생성
306
- const { loadCredentials } = await import('../auth/credentials.js');
227
+ if (!isConnected()) {
228
+ printError(' 플랫폼에 연결되지 않았습니다.');
229
+ console.log(chalk.gray('먼저 연결하세요: aiag connect'));
230
+ process.exitCode = 1;
231
+ return;
232
+ }
233
+ const connection = loadConnection();
307
234
  const credentials = loadCredentials();
308
235
  if (!credentials) {
309
236
  printError('인증 정보를 찾을 수 없습니다.');
310
237
  process.exitCode = 1;
311
238
  return;
312
239
  }
240
+ console.log('');
241
+ console.log(chalk.bold('📡 실시간 이벤트 스트리밍'));
242
+ console.log(chalk.gray(`프로젝트: ${connection.projectName}`));
243
+ console.log(chalk.gray('종료하려면 Ctrl+C를 누르세요.'));
244
+ console.log('');
245
+ // SSE 클라이언트 설정
313
246
  const sseClient = new SSEClient({
314
247
  serverUrl: connection.serverUrl,
315
248
  projectId: connection.projectId,
@@ -330,35 +263,23 @@ async function handleWatch() {
330
263
  console.log(chalk.red(`✗ 에러: ${error.message}`));
331
264
  });
332
265
  // Feature 이벤트
333
- sseClient.on('feature_claimed', (event) => {
334
- console.log(formatSSEEvent(event));
335
- });
336
- sseClient.on('feature_progress', (event) => {
337
- console.log(formatSSEEvent(event));
338
- });
339
- sseClient.on('feature_completed', (event) => {
340
- console.log(chalk.green(formatSSEEvent(event)));
341
- });
342
- sseClient.on('feature_failed', (event) => {
343
- console.log(chalk.red(formatSSEEvent(event)));
344
- });
266
+ sseClient.on('feature_claimed', (event) => console.log(formatSSEEvent(event)));
267
+ sseClient.on('feature_progress', (event) => console.log(formatSSEEvent(event)));
268
+ sseClient.on('feature_completed', (event) => console.log(chalk.green(formatSSEEvent(event))));
269
+ sseClient.on('feature_failed', (event) => console.log(chalk.red(formatSSEEvent(event))));
345
270
  // 세션 이벤트
346
- sseClient.on('session_started', (event) => {
347
- console.log(chalk.blue(formatSSEEvent(event)));
348
- });
349
- sseClient.on('session_ended', (event) => {
350
- console.log(chalk.blue(formatSSEEvent(event)));
351
- });
271
+ sseClient.on('session_started', (event) => console.log(chalk.blue(formatSSEEvent(event))));
272
+ sseClient.on('session_ended', (event) => console.log(chalk.blue(formatSSEEvent(event))));
352
273
  // 로그 이벤트
274
+ const levelColors = {
275
+ info: chalk.blue,
276
+ warn: chalk.yellow,
277
+ error: chalk.red,
278
+ debug: chalk.gray,
279
+ };
353
280
  sseClient.on('log', (event) => {
354
- const levelColorMap = {
355
- info: chalk.blue,
356
- warn: chalk.yellow,
357
- error: chalk.red,
358
- debug: chalk.gray,
359
- };
360
- const levelColor = levelColorMap[event.level] ?? chalk.white;
361
- console.log(levelColor(formatSSEEvent(event)));
281
+ const color = levelColors[event.level] ?? chalk.white;
282
+ console.log(color(formatSSEEvent(event)));
362
283
  });
363
284
  // Ctrl+C 핸들링
364
285
  process.on('SIGINT', () => {
@@ -377,177 +298,158 @@ async function handleWatch() {
377
298
  }
378
299
  }
379
300
  // ============================================
380
- // 데이터 변환 함수
301
+ // Git 작업 실행
381
302
  // ============================================
382
303
  /**
383
- * 로컬 Feature를 동기화 데이터로 변환
384
- */
385
- function localToSyncFeature(feature) {
386
- return {
387
- localId: feature.id,
388
- category: feature.category,
389
- priority: feature.priority,
390
- description: feature.description,
391
- acceptanceCriteria: feature.acceptanceCriteria,
392
- testCommand: feature.testCommand,
393
- passes: feature.passes,
394
- lastTestedAt: feature.lastTestedAt ?? undefined,
395
- implementedBy: feature.implementedBy ?? undefined,
396
- notes: feature.notes,
397
- dependsOn: feature.dependsOn,
398
- };
399
- }
400
- /**
401
- * 서버 Feature를 로컬 형식으로 변환
304
+ * Git pull 자동 실행
402
305
  */
403
- function serverToLocalFeature(serverFeature) {
404
- return {
405
- id: serverFeature.localId,
406
- category: serverFeature.category,
407
- priority: serverFeature.priority,
408
- description: serverFeature.description,
409
- acceptanceCriteria: serverFeature.acceptanceCriteria,
410
- testCommand: serverFeature.testCommand,
411
- passes: serverFeature.passes,
412
- lastTestedAt: serverFeature.lastTestedAt ?? null,
413
- implementedBy: serverFeature.implementedBy ?? null,
414
- notes: serverFeature.notes,
415
- dependsOn: serverFeature.dependsOn,
416
- };
306
+ async function performGitPull(status) {
307
+ console.log(chalk.yellow(` ⬇️ 원격에 ${status.behind}개의 새 커밋이 있습니다.`));
308
+ // 커밋 목록 표시 (최대 5개)
309
+ for (const commit of status.behindCommits.slice(0, 5)) {
310
+ console.log(chalk.gray(` - ${commit}`));
311
+ }
312
+ if (status.behind > 5) {
313
+ console.log(chalk.gray(` ... 외 ${status.behind - 5}개`));
314
+ }
315
+ console.log('');
316
+ const spinner = ora('git pull 실행 중...').start();
317
+ const result = await gitPull();
318
+ if (result.success) {
319
+ spinner.succeed('git pull 완료');
320
+ await notifyServerCache();
321
+ }
322
+ else {
323
+ spinner.fail('git pull 실패');
324
+ if (result.stderr) {
325
+ printError(result.stderr);
326
+ if (result.stderr.includes('conflict')) {
327
+ printWarning('충돌이 발생했습니다. 수동으로 해결해주세요.');
328
+ }
329
+ }
330
+ process.exitCode = 1;
331
+ }
417
332
  }
418
333
  /**
419
- * 서버 응답으로 로컬 Feature 업데이트
334
+ * Git push 자동 실행
420
335
  */
421
- function updateLocalFromServer(localList, serverFeatures) {
422
- const serverMap = new Map(serverFeatures.map(f => [f.localId, f]));
423
- for (const localFeature of localList.features) {
424
- const serverFeature = serverMap.get(localFeature.id);
425
- if (serverFeature) {
426
- // 서버에서 업데이트된 정보 반영 (passes, lastTestedAt 등)
427
- localFeature.passes = serverFeature.passes;
428
- if (serverFeature.lastTestedAt) {
429
- localFeature.lastTestedAt = serverFeature.lastTestedAt;
430
- }
336
+ async function performGitPush(status) {
337
+ console.log(chalk.yellow(` ⬆️ 로컬에 ${status.ahead}개의 커밋이 있습니다.`));
338
+ // 커밋 목록 표시 (최대 5개)
339
+ for (const commit of status.aheadCommits.slice(0, 5)) {
340
+ console.log(chalk.gray(` - ${commit}`));
341
+ }
342
+ if (status.ahead > 5) {
343
+ console.log(chalk.gray(` ... 외 ${status.ahead - 5}개`));
344
+ }
345
+ console.log('');
346
+ const spinner = ora('git push 실행 중...').start();
347
+ const result = await gitPush();
348
+ if (result.success) {
349
+ spinner.succeed('git push 완료');
350
+ await notifyServerCache();
351
+ }
352
+ else {
353
+ spinner.fail('git push 실패');
354
+ if (result.error) {
355
+ printError(result.error);
431
356
  }
357
+ process.exitCode = 1;
432
358
  }
433
- writeFeatureList(localList);
434
359
  }
435
360
  // ============================================
436
- // 결과 출력 함수
361
+ // 서버 캐시 알림 (선택적)
437
362
  // ============================================
438
363
  /**
439
- * 동기화 결과 출력
364
+ * 서버에 동기화 완료 알림 (서버 캐시 갱신 트리거)
365
+ * GitHub Webhook이 처리하므로 필수는 아님
440
366
  */
441
- function printSyncResult(response, direction) {
442
- console.log('');
443
- const directionText = direction === 'push' ? '서버에 반영' : '로컬에 반영';
444
- if (response.synced > 0) {
445
- console.log(` ${chalk.green('✓')} ${response.synced}개 Feature ${directionText}`);
446
- }
447
- if (response.created > 0) {
448
- console.log(` ${chalk.blue('+')} ${response.created}개 생성됨`);
449
- }
450
- if (response.updated > 0) {
451
- console.log(` ${chalk.yellow('~')} ${response.updated}개 업데이트됨`);
452
- }
453
- if (response.conflicts.length > 0) {
454
- console.log(` ${chalk.red('!')} ${response.conflicts.length}개 충돌`);
367
+ async function notifyServerCache() {
368
+ // 서버 연결이 되어 있으면 마지막 동기화 시간 업데이트
369
+ if (isConnected() && isLoggedIn()) {
370
+ try {
371
+ updateLastSyncAt();
372
+ // 선택적: 서버에 동기화 요청 (GitHub Webhook이 이미 처리하므로 불필요할 수 있음)
373
+ // 하지만 연결 상태 확인 및 프로젝트 존재 확인에 유용
374
+ const connection = loadConnection();
375
+ const client = getApiClient();
376
+ if (connection && client) {
377
+ // 간단한 health check 수준
378
+ await client.getProject(connection.projectId).catch(() => {
379
+ // 무시 (서버 연결 안 되어도 Git 동기화는 성공)
380
+ });
381
+ }
382
+ }
383
+ catch {
384
+ // 서버 알림 실패해도 Git 동기화는 성공으로 처리
385
+ }
455
386
  }
456
- console.log('');
457
387
  }
388
+ // ============================================
389
+ // 출력 유틸리티
390
+ // ============================================
458
391
  /**
459
- * 충돌 정보 출력
392
+ * Git 동기화 상태 출력
460
393
  */
461
- function printConflicts(conflicts) {
462
- console.log(chalk.yellow('충돌이 발생했습니다:'));
394
+ function printGitSyncStatus(status) {
463
395
  console.log('');
464
- for (const conflict of conflicts.slice(0, 5)) {
465
- console.log(` ${chalk.yellow('!')} ${conflict.featureId}.${conflict.field}`);
466
- console.log(` 로컬: ${chalk.gray(String(conflict.localValue))}`);
467
- console.log(` 서버: ${chalk.gray(String(conflict.serverValue))}`);
468
- }
469
- if (conflicts.length > 5) {
470
- console.log(chalk.gray(` ... 외 ${conflicts.length - 5}개 충돌`));
396
+ console.log(chalk.bold('🔄 Git 상태'));
397
+ if (!status.hasRemote) {
398
+ console.log(chalk.gray(' 원격 저장소 없음'));
399
+ return;
471
400
  }
472
- console.log('');
473
- console.log(chalk.gray('충돌 해결 옵션:'));
474
- console.log(chalk.gray(' --force: 로컬 변경사항으로 덮어쓰기'));
475
- console.log(chalk.gray(' --pull: 서버 상태로 덮어쓰기'));
401
+ console.log(chalk.gray(` 브랜치: ${status.branch || 'unknown'}`));
402
+ console.log(chalk.gray(` 로컬: ${status.localCommit?.slice(0, 7) || 'N/A'}`));
403
+ console.log(chalk.gray(` 원격: ${status.remoteCommit?.slice(0, 7) || 'N/A'}`));
476
404
  console.log('');
477
405
  }
478
406
  /**
479
- * Dry-run 결과 출력 (push)
407
+ * 상태 요약 출력
480
408
  */
481
- function printDryRunResult(syncData) {
482
- console.log('');
483
- console.log(chalk.bold('[Dry Run] 동기화 예정 항목:'));
484
- console.log('');
485
- const completed = syncData.filter(f => f.passes);
486
- const pending = syncData.filter(f => !f.passes);
487
- console.log(` Feature: ${syncData.length}개`);
488
- console.log(` ${chalk.green('완료:')} ${completed.length}개`);
489
- console.log(` ${chalk.gray('대기:')} ${pending.length}개`);
490
- console.log('');
491
- if (syncData.length <= 10) {
492
- for (const feature of syncData) {
493
- const icon = feature.passes ? chalk.green('✓') : chalk.gray('○');
494
- console.log(` ${icon} ${feature.localId}: ${feature.description.slice(0, 50)}...`);
495
- }
409
+ function printStatusSummary(status) {
410
+ switch (status.status) {
411
+ case 'up-to-date':
412
+ console.log(chalk.green(' ✓ 최신 상태'));
413
+ break;
414
+ case 'behind':
415
+ console.log(chalk.yellow(` ⬇️ 원격에 ${status.behind}개 커밋 뒤처짐`));
416
+ console.log(chalk.gray(' aiag sync 로 pull 하세요.'));
417
+ break;
418
+ case 'ahead':
419
+ console.log(chalk.yellow(` ⬆️ 로컬에 ${status.ahead}개 커밋 앞섬`));
420
+ console.log(chalk.gray(' aiag sync push 하세요.'));
421
+ break;
422
+ case 'diverged':
423
+ console.log(chalk.red(` ⚠️ 로컬과 원격이 분기됨`));
424
+ console.log(chalk.gray(` 로컬 +${status.ahead}, 원격 +${status.behind}`));
425
+ console.log(chalk.gray(' aiag sync --reset 으로 GitHub 상태로 초기화하세요.'));
426
+ break;
427
+ case 'no-remote':
428
+ console.log(chalk.gray(' 원격 저장소가 설정되지 않음'));
429
+ break;
430
+ case 'no-upstream':
431
+ console.log(chalk.gray(' 원격 브랜치 트래킹이 설정되지 않음'));
432
+ break;
496
433
  }
497
- console.log('');
498
- console.log(chalk.gray('실제 동기화하려면 --dry-run 옵션을 제거하세요.'));
499
- console.log('');
500
434
  }
501
435
  /**
502
- * Dry-run 결과 출력 (pull)
436
+ * 분기 경고 출력
503
437
  */
504
- function printPullDryRunResult(serverFeatures, localList) {
438
+ function printDivergedWarning(status) {
439
+ console.log(chalk.red(' ⚠️ 로컬과 원격이 분기되었습니다:'));
440
+ console.log(chalk.yellow(` - 로컬에 ${status.ahead}개 커밋 (푸시 안 됨)`));
441
+ console.log(chalk.yellow(` - 원격에 ${status.behind}개 커밋 (E2B 등에서 생성)`));
505
442
  console.log('');
506
- console.log(chalk.bold('[Dry Run] 서버에서 가져올 항목:'));
443
+ console.log(chalk.cyan(' 💡 해결 방법:'));
507
444
  console.log('');
508
- const localCount = localList?.features.length ?? 0;
509
- console.log(` 서버 Feature: ${serverFeatures.length}개`);
510
- console.log(` 로컬 Feature: ${localCount}개`);
511
- if (serverFeatures.length !== localCount) {
512
- console.log(` ${chalk.yellow('변경:')} ${Math.abs(serverFeatures.length - localCount)}개 차이`);
513
- }
445
+ console.log(chalk.gray(' 1. GitHub 상태로 초기화 (로컬 변경 삭제):'));
446
+ console.log(chalk.white(' aiag sync --reset'));
514
447
  console.log('');
515
- console.log(chalk.gray('실제 동기화하려면 --dry-run 옵션을 제거하세요.'));
448
+ console.log(chalk.gray(' 2. 수동으로 rebase push:'));
449
+ console.log(chalk.white(' git pull --rebase origin ' + (status.branch || 'main')));
450
+ console.log(chalk.white(' git push'));
516
451
  console.log('');
517
452
  }
518
- // ============================================
519
- // 에러 처리
520
- // ============================================
521
- /**
522
- * 동기화 에러 처리
523
- */
524
- function handleSyncError(error) {
525
- if (error instanceof ApiError) {
526
- if (error.code === 'NOT_FOUND' || error.status === 404) {
527
- printError('프로젝트를 찾을 수 없습니다. 연결을 다시 확인하세요.');
528
- }
529
- else if (error.status === 403) {
530
- printError('이 프로젝트에 대한 동기화 권한이 없습니다.');
531
- }
532
- else if (error.code === 'CONFLICT') {
533
- printError('동기화 충돌이 발생했습니다. --force 옵션을 사용하거나 수동으로 해결하세요.');
534
- }
535
- else {
536
- printError(`동기화 실패: ${error.message}`);
537
- }
538
- }
539
- else if (error instanceof NetworkError) {
540
- printError(`네트워크 오류: ${error.message}`);
541
- printInfo('오프라인 상태에서 변경한 내용은 나중에 동기화됩니다.');
542
- }
543
- else {
544
- printError(`알 수 없는 오류: ${error instanceof Error ? error.message : String(error)}`);
545
- }
546
- process.exitCode = 1;
547
- }
548
- // ============================================
549
- // 유틸리티 함수
550
- // ============================================
551
453
  /**
552
454
  * 날짜 포맷팅
553
455
  */
@@ -562,7 +464,7 @@ function formatDate(isoString) {
562
464
  });
563
465
  }
564
466
  // ============================================
565
- // Progress.md 동기화
467
+ // Progress.md 동기화 (서버 모니터링용)
566
468
  // ============================================
567
469
  /**
568
470
  * Progress.md를 서버에 동기화
@@ -651,171 +553,5 @@ function truncateProgressContent(content, maxSize) {
651
553
  }
652
554
  return result;
653
555
  }
654
- // ============================================
655
- // Git 동기화 처리
656
- // ============================================
657
- /**
658
- * Git 원격 저장소 동기화 상태 확인 및 처리
659
- * @returns true면 계속 진행, false면 중단
660
- */
661
- async function handleGitSync(options) {
662
- const spinner = ora('Git 상태 확인 중...').start();
663
- try {
664
- const status = await getRemoteSyncStatus();
665
- spinner.stop();
666
- // 출력
667
- printGitSyncStatus(status);
668
- // 상태별 처리
669
- const shouldAutoPull = options.autoPull || options.autoGit;
670
- const shouldAutoPush = options.autoPush || options.autoGit;
671
- switch (status.status) {
672
- case 'no-remote':
673
- printInfo('원격 저장소가 설정되지 않았습니다. Feature 동기화만 진행합니다.');
674
- return true;
675
- case 'no-upstream':
676
- printInfo('원격 브랜치 트래킹이 설정되지 않았습니다. Feature 동기화만 진행합니다.');
677
- return true;
678
- case 'up-to-date':
679
- console.log(chalk.green(' ✓ Git 저장소가 최신 상태입니다.\n'));
680
- return true;
681
- case 'behind':
682
- // 원격에 새 커밋이 있음 → pull 권장
683
- if (shouldAutoPull) {
684
- return await performGitPull();
685
- }
686
- else {
687
- printGitPullSuggestion(status);
688
- return true; // Feature 동기화는 계속 진행
689
- }
690
- case 'ahead':
691
- // 로컬에 푸시되지 않은 커밋이 있음 → push 권장
692
- if (shouldAutoPush) {
693
- return await performGitPush();
694
- }
695
- else {
696
- printGitPushSuggestion(status);
697
- return true; // Feature 동기화는 계속 진행
698
- }
699
- case 'diverged':
700
- // 로컬과 원격이 분기됨 → 수동 해결 필요
701
- printGitDivergedWarning(status);
702
- return true; // Feature 동기화는 계속 진행 (경고만)
703
- default:
704
- return true;
705
- }
706
- }
707
- catch (error) {
708
- spinner.stop();
709
- if (error instanceof Error) {
710
- printWarning(`Git 상태 확인 실패: ${error.message}`);
711
- }
712
- // Git 확인 실패해도 Feature 동기화는 계속
713
- return true;
714
- }
715
- }
716
- /**
717
- * Git 동기화 상태 출력
718
- */
719
- function printGitSyncStatus(status) {
720
- console.log();
721
- console.log(chalk.bold('📊 Git 동기화 상태'));
722
- if (!status.hasRemote) {
723
- console.log(chalk.gray(' 원격 저장소 없음'));
724
- return;
725
- }
726
- console.log(chalk.gray(` 브랜치: ${status.branch || 'unknown'}`));
727
- console.log(chalk.gray(` 로컬: ${status.localCommit || 'N/A'}`));
728
- console.log(chalk.gray(` 원격: ${status.remoteCommit || 'N/A'}`));
729
- console.log();
730
- }
731
- /**
732
- * Git pull 제안 출력
733
- */
734
- function printGitPullSuggestion(status) {
735
- console.log(chalk.yellow(` ⬇️ 원격에 ${status.behind}개의 새 커밋이 있습니다:`));
736
- // 최대 5개만 표시
737
- const commits = status.behindCommits.slice(0, 5);
738
- for (const commit of commits) {
739
- console.log(chalk.gray(` - ${commit}`));
740
- }
741
- if (status.behind > 5) {
742
- console.log(chalk.gray(` ... 외 ${status.behind - 5}개`));
743
- }
744
- console.log();
745
- console.log(chalk.cyan(' 💡 권장: git pull 실행'));
746
- console.log(chalk.gray(' 또는: aiag sync --auto-pull'));
747
- console.log();
748
- }
749
- /**
750
- * Git push 제안 출력
751
- */
752
- function printGitPushSuggestion(status) {
753
- console.log(chalk.yellow(` ⬆️ 로컬에 ${status.ahead}개의 푸시되지 않은 커밋이 있습니다:`));
754
- // 최대 5개만 표시
755
- const commits = status.aheadCommits.slice(0, 5);
756
- for (const commit of commits) {
757
- console.log(chalk.gray(` - ${commit}`));
758
- }
759
- if (status.ahead > 5) {
760
- console.log(chalk.gray(` ... 외 ${status.ahead - 5}개`));
761
- }
762
- console.log();
763
- console.log(chalk.cyan(' 💡 권장: git push 실행'));
764
- console.log(chalk.gray(' 또는: aiag sync --auto-push'));
765
- console.log();
766
- }
767
- /**
768
- * Git 분기 경고 출력
769
- */
770
- function printGitDivergedWarning(status) {
771
- console.log(chalk.red(' ⚠️ 로컬과 원격이 분기되었습니다:'));
772
- console.log(chalk.yellow(` - 로컬에 ${status.ahead}개 커밋 (푸시 안 됨)`));
773
- console.log(chalk.yellow(` - 원격에 ${status.behind}개 커밋 (E2B 등에서 생성)`));
774
- console.log();
775
- console.log(chalk.cyan(' 💡 권장 조치:'));
776
- console.log(chalk.gray(' 1. git pull --rebase origin ' + (status.branch || 'main')));
777
- console.log(chalk.gray(' 2. git push'));
778
- console.log();
779
- console.log(chalk.gray(' 또는 로컬 변경 무시:'));
780
- console.log(chalk.gray(' git reset --hard origin/' + (status.branch || 'main')));
781
- console.log();
782
- }
783
- /**
784
- * Git pull 자동 실행
785
- */
786
- async function performGitPull() {
787
- const spinner = ora('git pull 실행 중...').start();
788
- const result = await gitPull();
789
- if (result.success) {
790
- spinner.succeed('git pull 완료');
791
- return true;
792
- }
793
- else {
794
- spinner.fail('git pull 실패');
795
- printError(result.stderr || 'pull 실패');
796
- if (result.stderr?.includes('conflict')) {
797
- printWarning('충돌이 발생했습니다. 수동으로 해결해주세요.');
798
- }
799
- return true; // Feature 동기화는 계속 진행
800
- }
801
- }
802
- /**
803
- * Git push 자동 실행
804
- */
805
- async function performGitPush() {
806
- const spinner = ora('git push 실행 중...').start();
807
- const result = await gitPush();
808
- if (result.success) {
809
- spinner.succeed('git push 완료');
810
- return true;
811
- }
812
- else {
813
- spinner.fail('git push 실패');
814
- if (result.error) {
815
- printError(result.error);
816
- }
817
- return true; // Feature 동기화는 계속 진행
818
- }
819
- }
820
556
  export default sync;
821
557
  //# sourceMappingURL=sync.js.map