vibe-commander 0.2.4 → 0.2.5
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/dist/adapters/cli/commands/init-helpers.d.ts +1 -1
- package/dist/adapters/cli/commands/init-helpers.js +1 -1
- package/dist/adapters/cli/commands/init-interactive.js +3 -3
- package/dist/core/parsers/dep-line-parser.js +82 -25
- package/dist/core/resolvers/config-generator.d.ts +1 -0
- package/dist/core/resolvers/config-generator.js +2 -1
- package/dist/core/resolvers/config-merger.js +0 -1
- package/package.json +1 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* @module
|
|
11
11
|
*/
|
|
12
12
|
import type { ToolResult, ScanResult, InitAnswers, InitResult } from '../../../types/index.js';
|
|
13
|
-
export { DEFAULT_COMMANDS_PATH, DEFAULT_ROADMAP_PATH, DEFAULT_PLAN_DIR, DEFAULT_RESULT_DIR, DEFAULT_ID_PATTERN, DEFAULT_COMMAND_SECTION, DEFAULT_HEADER_TEMPLATE, extractStdinAnswers, buildAnswersFromScan, } from '../../../core/resolvers/config-generator.js';
|
|
13
|
+
export { DEFAULT_COMMANDS_PATH, DEFAULT_ROADMAP_PATH, DEFAULT_PLAN_DIR, DEFAULT_RESULT_DIR, DEFAULT_RUNBOOK_DIR, DEFAULT_ID_PATTERN, DEFAULT_COMMAND_SECTION, DEFAULT_HEADER_TEMPLATE, extractStdinAnswers, buildAnswersFromScan, } from '../../../core/resolvers/config-generator.js';
|
|
14
14
|
/**
|
|
15
15
|
* 수집된 정보로 설정 객체를 구성하고, 검증 후 파일에 쓴다
|
|
16
16
|
*
|
|
@@ -15,7 +15,7 @@ import { projectConfigSchema } from '../../../config/schema.js';
|
|
|
15
15
|
import { CYAN, YELLOW, GREEN, DIM, RESET, BOLD } from '../output.js';
|
|
16
16
|
import { mergeConfigs } from '../../../core/resolvers/config-merger.js';
|
|
17
17
|
import { buildConfigObject } from '../../../core/resolvers/config-generator.js';
|
|
18
|
-
export { DEFAULT_COMMANDS_PATH, DEFAULT_ROADMAP_PATH, DEFAULT_PLAN_DIR, DEFAULT_RESULT_DIR, DEFAULT_ID_PATTERN, DEFAULT_COMMAND_SECTION, DEFAULT_HEADER_TEMPLATE, extractStdinAnswers, buildAnswersFromScan, } from '../../../core/resolvers/config-generator.js';
|
|
18
|
+
export { DEFAULT_COMMANDS_PATH, DEFAULT_ROADMAP_PATH, DEFAULT_PLAN_DIR, DEFAULT_RESULT_DIR, DEFAULT_RUNBOOK_DIR, DEFAULT_ID_PATTERN, DEFAULT_COMMAND_SECTION, DEFAULT_HEADER_TEMPLATE, extractStdinAnswers, buildAnswersFromScan, } from '../../../core/resolvers/config-generator.js';
|
|
19
19
|
/**
|
|
20
20
|
* 수집된 정보로 설정 객체를 구성하고, 검증 후 파일에 쓴다
|
|
21
21
|
*
|
|
@@ -17,7 +17,7 @@ import { fail } from '../../../types/index.js';
|
|
|
17
17
|
import { CONFIG_FILENAME } from '../../../config/schema.js';
|
|
18
18
|
import { isValidRegex } from '../../../core/resolvers/config-validator.js';
|
|
19
19
|
import { CYAN, YELLOW, DIM, RESET, BOLD } from '../output.js';
|
|
20
|
-
import { DEFAULT_COMMANDS_PATH, DEFAULT_ROADMAP_PATH, DEFAULT_PLAN_DIR, DEFAULT_RESULT_DIR, DEFAULT_ID_PATTERN, DEFAULT_COMMAND_SECTION, buildAnswersFromScan, buildAndWriteConfig, printScanResults, } from './init-helpers.js';
|
|
20
|
+
import { DEFAULT_COMMANDS_PATH, DEFAULT_ROADMAP_PATH, DEFAULT_PLAN_DIR, DEFAULT_RESULT_DIR, DEFAULT_RUNBOOK_DIR, DEFAULT_ID_PATTERN, DEFAULT_COMMAND_SECTION, buildAnswersFromScan, buildAndWriteConfig, printScanResults, } from './init-helpers.js';
|
|
21
21
|
/**
|
|
22
22
|
* 대화형 모드 — readline으로 질문을 수집하고 설정 파일을 생성한다
|
|
23
23
|
*/
|
|
@@ -105,7 +105,7 @@ async function collectAnswers(rl) {
|
|
|
105
105
|
const roadmapPath = await askNullable(rl, '로드맵 파일 경로 (없으면 Enter)', DEFAULT_ROADMAP_PATH);
|
|
106
106
|
const planDir = await askWithDefault(rl, '계획서 디렉토리', DEFAULT_PLAN_DIR);
|
|
107
107
|
const resultDir = await askWithDefault(rl, '결과 보고서 디렉토리', DEFAULT_RESULT_DIR);
|
|
108
|
-
const runbookDir = await askNullable(rl, '런북 디렉토리 (없으면 Enter)',
|
|
108
|
+
const runbookDir = await askNullable(rl, '런북 디렉토리 (없으면 Enter)', DEFAULT_RUNBOOK_DIR);
|
|
109
109
|
const idPattern = await askWithDefault(rl, '유닛 ID 패턴 (정규식)', DEFAULT_ID_PATTERN);
|
|
110
110
|
const validPattern = isValidRegex(idPattern);
|
|
111
111
|
if (!validPattern) {
|
|
@@ -132,7 +132,7 @@ async function askWithDefault(rl, prompt, defaultValue) {
|
|
|
132
132
|
/**
|
|
133
133
|
* null 가능한 질문 프롬프트 (빈 입력 → null)
|
|
134
134
|
*/
|
|
135
|
-
async function askNullable(rl, prompt, defaultValue) {
|
|
135
|
+
async function askNullable(rl, prompt, defaultValue = '') {
|
|
136
136
|
const display = defaultValue ? ` ${DIM}[${defaultValue}]${RESET}` : '';
|
|
137
137
|
const answer = await rl.question(`${CYAN}? ${prompt}${RESET}${display}: `);
|
|
138
138
|
const trimmed = answer.trim();
|
|
@@ -43,12 +43,19 @@ export function parseDependencyLines(lines, idPattern) {
|
|
|
43
43
|
artifacts: currentArtifacts,
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
|
-
// 새 dep 파싱
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
// 새 dep 파싱 (멀티 반환 지원)
|
|
47
|
+
const parsedList = extractDepFromLine(trimmed, idPattern);
|
|
48
|
+
if (parsedList.length > 0) {
|
|
49
|
+
// 마지막 이외 항목은 즉시 deps에 push (하위 항목은 마지막 유닛에만 연결)
|
|
50
|
+
for (let i = 0; i < parsedList.length - 1; i++) {
|
|
51
|
+
const dep = parsedList[i];
|
|
52
|
+
if (dep)
|
|
53
|
+
deps.push(dep);
|
|
54
|
+
}
|
|
55
|
+
const last = parsedList[parsedList.length - 1];
|
|
56
|
+
currentUnitId = last?.unitId ?? '';
|
|
57
|
+
currentDescription = last?.description ?? '';
|
|
58
|
+
currentArtifacts = last?.artifacts;
|
|
52
59
|
hasCurrent = true;
|
|
53
60
|
}
|
|
54
61
|
else {
|
|
@@ -83,49 +90,86 @@ export function parseDependencyLines(lines, idPattern) {
|
|
|
83
90
|
return { deps, contextNotes };
|
|
84
91
|
}
|
|
85
92
|
/**
|
|
86
|
-
* 한 라인에서 의존 유닛 정보를 추출한다
|
|
93
|
+
* 한 라인에서 의존 유닛 정보를 추출한다 (멀티 반환 지원)
|
|
87
94
|
*
|
|
88
95
|
* 추출 우선순위:
|
|
89
96
|
* 1. 링크 텍스트 `[ID](url)` — 더 구체적 (문서 링크 포함)
|
|
90
97
|
* 2. 볼드 텍스트 `**ID**` — 일반적인 의존성 표기
|
|
91
98
|
*
|
|
92
99
|
* idPattern이 제공되면 추출된 후보를 필터링하여 실제 유닛 ID만 선별.
|
|
100
|
+
* 한 줄에 콤마로 구분된 복수 유닛이 있을 경우 모두 추출.
|
|
93
101
|
*
|
|
94
102
|
* @param line - 리스트 항목 라인 (리스트 마커 제거 전)
|
|
95
103
|
* @param idPattern - 유닛 ID 필터링 패턴 (선택)
|
|
96
|
-
* @returns 추출된 DepInfo
|
|
104
|
+
* @returns 추출된 DepInfo 배열 (없으면 빈 배열)
|
|
97
105
|
*/
|
|
98
106
|
function extractDepFromLine(line, idPattern) {
|
|
99
|
-
// 리스트 마커 제거
|
|
100
107
|
const content = line.replace(/^[-*]\s+/, '');
|
|
101
|
-
// 후보 ID 수집 (링크 우선, 볼드 후순위)
|
|
102
108
|
const candidates = [];
|
|
103
|
-
// 링크 텍스트: [ID](url) — 중첩 괄호 1레벨 지원 (예: [U-002[Mvp]])
|
|
104
109
|
const linkRegex = /\[((?:[^[\]]*(?:\[[^\]]*\])?[^[\]]*)*)\]\([^)]*\)/g;
|
|
105
110
|
let match;
|
|
106
111
|
while ((match = linkRegex.exec(content)) !== null) {
|
|
107
112
|
if (match[1])
|
|
108
|
-
candidates.push({
|
|
113
|
+
candidates.push({
|
|
114
|
+
id: match[1].trim(),
|
|
115
|
+
source: 'link',
|
|
116
|
+
matchStart: match.index,
|
|
117
|
+
matchEnd: match.index + match[0].length,
|
|
118
|
+
});
|
|
109
119
|
}
|
|
110
|
-
// 볼드 텍스트: **ID**
|
|
111
120
|
const boldRegex = /\*\*([^*]+)\*\*/g;
|
|
112
121
|
while ((match = boldRegex.exec(content)) !== null) {
|
|
113
122
|
if (match[1])
|
|
114
|
-
candidates.push({
|
|
123
|
+
candidates.push({
|
|
124
|
+
id: match[1].trim(),
|
|
125
|
+
source: 'bold',
|
|
126
|
+
matchStart: match.index,
|
|
127
|
+
matchEnd: match.index + match[0].length,
|
|
128
|
+
});
|
|
115
129
|
}
|
|
116
130
|
if (candidates.length === 0)
|
|
117
|
-
return
|
|
118
|
-
// idPattern으로 필터링: 볼드 헤더(`**헤더**: [ID](url)`)에서 비유닛 볼드를 건너뜀
|
|
131
|
+
return [];
|
|
119
132
|
const idRegex = idPattern ? safeRegex(idPattern) : null;
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
133
|
+
const validCandidates = idRegex
|
|
134
|
+
? candidates
|
|
135
|
+
.filter((c) => idRegex.test(c.id))
|
|
136
|
+
.map((c) => {
|
|
137
|
+
// 볼드가 링크를 감싸는 경우(`**[ID](url)**`) ID를 실제 유닛 ID로 정규화
|
|
138
|
+
const actualMatch = c.id.match(idRegex);
|
|
139
|
+
return actualMatch ? { ...c, id: actualMatch[0] } : c;
|
|
140
|
+
})
|
|
141
|
+
: candidates[0]
|
|
142
|
+
? [candidates[0]]
|
|
143
|
+
: [];
|
|
144
|
+
// None 의존성 필터링 + 동일 ID 중복 제거 (첫 등장 유지)
|
|
145
|
+
const seen = new Set();
|
|
146
|
+
const filtered = validCandidates.filter((c) => {
|
|
147
|
+
if (isNoneDependency(c.id))
|
|
148
|
+
return false;
|
|
149
|
+
if (seen.has(c.id))
|
|
150
|
+
return false;
|
|
151
|
+
seen.add(c.id);
|
|
152
|
+
return true;
|
|
153
|
+
});
|
|
154
|
+
if (filtered.length === 0)
|
|
155
|
+
return [];
|
|
156
|
+
// 단일 후보: 기존 extractDescription 로직 사용 (성능/호환성 보장)
|
|
157
|
+
if (filtered.length === 1) {
|
|
158
|
+
const c = filtered[0];
|
|
159
|
+
if (!c)
|
|
160
|
+
return [];
|
|
161
|
+
const description = extractDescription(content, c.id, c.source);
|
|
162
|
+
return [{ unitId: c.id, description }];
|
|
163
|
+
}
|
|
164
|
+
// 복수 후보: 등장 순서 정렬 후 위치 기반 description 범위 분할
|
|
165
|
+
const sorted = [...filtered].sort((a, b) => a.matchStart - b.matchStart);
|
|
166
|
+
return sorted.map((c, i) => {
|
|
167
|
+
const nextCandidate = sorted[i + 1];
|
|
168
|
+
const nextStart = nextCandidate ? nextCandidate.matchStart : content.length;
|
|
169
|
+
const descText = content.slice(c.matchEnd, nextStart);
|
|
170
|
+
const description = cleanupMultiDescription(descText);
|
|
171
|
+
return { unitId: c.id, description };
|
|
172
|
+
});
|
|
129
173
|
}
|
|
130
174
|
/**
|
|
131
175
|
* ID 참조 이후의 설명 텍스트를 추출한다
|
|
@@ -157,6 +201,19 @@ function extractDescription(content, unitId, source) {
|
|
|
157
201
|
rest = rest.replace(/^[—-]\s+/, '');
|
|
158
202
|
return rest.trim();
|
|
159
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* 멀티 의존성 description 텍스트를 정리한다
|
|
206
|
+
*
|
|
207
|
+
* 위치 기반 범위 분할로 추출된 raw description에서
|
|
208
|
+
* 선행 구분자(`:`, `—`, `-`)와 후행 콤마를 제거.
|
|
209
|
+
*/
|
|
210
|
+
function cleanupMultiDescription(text) {
|
|
211
|
+
let rest = text.trim();
|
|
212
|
+
rest = rest.replace(/^[:—]\s*/, '');
|
|
213
|
+
rest = rest.replace(/^[—-]\s+/, '');
|
|
214
|
+
rest = rest.replace(/,\s*$/, '');
|
|
215
|
+
return rest.trim();
|
|
216
|
+
}
|
|
160
217
|
/**
|
|
161
218
|
* 하위 항목에서 artifact 텍스트를 추출한다
|
|
162
219
|
*
|
|
@@ -12,6 +12,7 @@ export declare const DEFAULT_ROADMAP_PATH = "vibe/roadmap.md";
|
|
|
12
12
|
export declare const DEFAULT_PLAN_DIR = "vibe/unit-plans";
|
|
13
13
|
export declare const DEFAULT_RESULT_DIR = "vibe/unit-results";
|
|
14
14
|
export declare const DEFAULT_ID_PATTERN = "^(U-\\d+|CP-).*";
|
|
15
|
+
export declare const DEFAULT_RUNBOOK_DIR = "vibe/unit-runbooks";
|
|
15
16
|
export declare const DEFAULT_COMMAND_SECTION = "# \uC720\uB2DB \uAD6C\uD604";
|
|
16
17
|
export declare const DEFAULT_HEADER_TEMPLATE = "### \uD604\uC7AC \uAD6C\uD604 \uC720\uB2DB: {{title}}\n- \uD604\uC7AC \uAD6C\uD604 \uC720\uB2DB \uAC1C\uBC1C \uACC4\uD68D\uC11C: @{{planPath}}\n- \uD604\uC7AC \uAD6C\uD604 Commit(\uBCC0\uACBD\uC810 \uD655\uC778\uD558\uC5EC \uB9E5\uB77D\uC73C\uB85C \uC0AC\uC6A9): -";
|
|
17
18
|
/**
|
|
@@ -12,6 +12,7 @@ export const DEFAULT_ROADMAP_PATH = 'vibe/roadmap.md';
|
|
|
12
12
|
export const DEFAULT_PLAN_DIR = 'vibe/unit-plans';
|
|
13
13
|
export const DEFAULT_RESULT_DIR = 'vibe/unit-results';
|
|
14
14
|
export const DEFAULT_ID_PATTERN = '^(U-\\d+|CP-).*';
|
|
15
|
+
export const DEFAULT_RUNBOOK_DIR = 'vibe/unit-runbooks';
|
|
15
16
|
export const DEFAULT_COMMAND_SECTION = '# 유닛 구현';
|
|
16
17
|
export const DEFAULT_HEADER_TEMPLATE = '### 현재 구현 유닛: {{title}}\n- 현재 구현 유닛 개발 계획서: @{{planPath}}\n- 현재 구현 Commit(변경점 확인하여 맥락으로 사용): -';
|
|
17
18
|
/**
|
|
@@ -45,7 +46,7 @@ export function extractStdinAnswers(data) {
|
|
|
45
46
|
roadmapPath: DEFAULT_ROADMAP_PATH,
|
|
46
47
|
planDir: DEFAULT_PLAN_DIR,
|
|
47
48
|
resultDir: DEFAULT_RESULT_DIR,
|
|
48
|
-
runbookDir:
|
|
49
|
+
runbookDir: DEFAULT_RUNBOOK_DIR,
|
|
49
50
|
idPattern: DEFAULT_ID_PATTERN,
|
|
50
51
|
commandSection: DEFAULT_COMMAND_SECTION,
|
|
51
52
|
};
|
|
@@ -53,7 +53,6 @@ function mergePaths(existing, scanned) {
|
|
|
53
53
|
const existingDocRoots = isPlainObject(existing['docRoots']) ? existing['docRoots'] : {};
|
|
54
54
|
const mergedDocRoots = { ...existingDocRoots };
|
|
55
55
|
for (const key of Object.keys(scanned['docRoots'])) {
|
|
56
|
-
// 스캔 결과에 있는 docRoots 키는 무조건 반영 (갱신 또는 추가)
|
|
57
56
|
mergedDocRoots[key] = scanned['docRoots'][key];
|
|
58
57
|
}
|
|
59
58
|
merged['docRoots'] = mergedDocRoots;
|
package/package.json
CHANGED