vibe-commander 0.1.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/dist/adapters/cli/commands/context-resolver.d.ts +29 -0
- package/dist/adapters/cli/commands/context-resolver.js +93 -0
- package/dist/adapters/cli/commands/context.d.ts +26 -0
- package/dist/adapters/cli/commands/context.js +39 -0
- package/dist/adapters/cli/commands/git-helpers.d.ts +43 -0
- package/dist/adapters/cli/commands/git-helpers.js +99 -0
- package/dist/adapters/cli/commands/init-helpers.d.ts +30 -0
- package/dist/adapters/cli/commands/init-helpers.js +112 -0
- package/dist/adapters/cli/commands/init-interactive.d.ts +25 -0
- package/dist/adapters/cli/commands/init-interactive.js +143 -0
- package/dist/adapters/cli/commands/init.d.ts +25 -0
- package/dist/adapters/cli/commands/init.js +73 -0
- package/dist/adapters/cli/commands/io-helpers.d.ts +45 -0
- package/dist/adapters/cli/commands/io-helpers.js +144 -0
- package/dist/adapters/cli/commands/list-units.d.ts +39 -0
- package/dist/adapters/cli/commands/list-units.js +92 -0
- package/dist/adapters/cli/commands/prompt-helpers.d.ts +36 -0
- package/dist/adapters/cli/commands/prompt-helpers.js +113 -0
- package/dist/adapters/cli/commands/set-unit.d.ts +44 -0
- package/dist/adapters/cli/commands/set-unit.js +90 -0
- package/dist/adapters/cli/commands/skill-install.d.ts +31 -0
- package/dist/adapters/cli/commands/skill-install.js +85 -0
- package/dist/adapters/cli/commands/state-helpers.d.ts +28 -0
- package/dist/adapters/cli/commands/state-helpers.js +55 -0
- package/dist/adapters/cli/commands/update-commit.d.ts +42 -0
- package/dist/adapters/cli/commands/update-commit.js +180 -0
- package/dist/adapters/cli/commands/validate.d.ts +40 -0
- package/dist/adapters/cli/commands/validate.js +219 -0
- package/dist/adapters/cli/formatters-list.d.ts +36 -0
- package/dist/adapters/cli/formatters-list.js +106 -0
- package/dist/adapters/cli/formatters-unit.d.ts +44 -0
- package/dist/adapters/cli/formatters-unit.js +287 -0
- package/dist/adapters/cli/formatters.d.ts +2 -0
- package/dist/adapters/cli/formatters.js +3 -0
- package/dist/adapters/cli/index.d.ts +17 -0
- package/dist/adapters/cli/index.js +60 -0
- package/dist/adapters/cli/output.d.ts +58 -0
- package/dist/adapters/cli/output.js +159 -0
- package/dist/adapters/cli/router.d.ts +99 -0
- package/dist/adapters/cli/router.js +419 -0
- package/dist/adapters/cli/scanner.d.ts +19 -0
- package/dist/adapters/cli/scanner.js +227 -0
- package/dist/adapters/pipe/stdin-reader.d.ts +23 -0
- package/dist/adapters/pipe/stdin-reader.js +56 -0
- package/dist/config/loader.d.ts +27 -0
- package/dist/config/loader.js +84 -0
- package/dist/config/resolver.d.ts +71 -0
- package/dist/config/resolver.js +124 -0
- package/dist/config/schema.d.ts +205 -0
- package/dist/config/schema.js +186 -0
- package/dist/core/parsers/backlog-parser.d.ts +25 -0
- package/dist/core/parsers/backlog-parser.js +255 -0
- package/dist/core/parsers/dep-line-parser.d.ts +36 -0
- package/dist/core/parsers/dep-line-parser.js +176 -0
- package/dist/core/parsers/dependency-extractor.d.ts +64 -0
- package/dist/core/parsers/dependency-extractor.js +193 -0
- package/dist/core/parsers/index.d.ts +20 -0
- package/dist/core/parsers/index.js +20 -0
- package/dist/core/parsers/md-utils.d.ts +81 -0
- package/dist/core/parsers/md-utils.js +144 -0
- package/dist/core/parsers/metadata-parser.d.ts +49 -0
- package/dist/core/parsers/metadata-parser.js +133 -0
- package/dist/core/parsers/pairing-question-extractor.d.ts +42 -0
- package/dist/core/parsers/pairing-question-extractor.js +146 -0
- package/dist/core/parsers/plan-parser-helpers.d.ts +61 -0
- package/dist/core/parsers/plan-parser-helpers.js +90 -0
- package/dist/core/parsers/plan-parser.d.ts +55 -0
- package/dist/core/parsers/plan-parser.js +69 -0
- package/dist/core/parsers/title-extractor.d.ts +36 -0
- package/dist/core/parsers/title-extractor.js +101 -0
- package/dist/core/renderers/index.d.ts +15 -0
- package/dist/core/renderers/index.js +18 -0
- package/dist/core/renderers/interpolate.d.ts +92 -0
- package/dist/core/renderers/interpolate.js +117 -0
- package/dist/core/renderers/marker-utils.d.ts +60 -0
- package/dist/core/renderers/marker-utils.js +85 -0
- package/dist/core/renderers/section-renderer.d.ts +31 -0
- package/dist/core/renderers/section-renderer.js +171 -0
- package/dist/core/renderers/section-updater.d.ts +72 -0
- package/dist/core/renderers/section-updater.js +175 -0
- package/dist/core/renderers/template-engine.d.ts +69 -0
- package/dist/core/renderers/template-engine.js +92 -0
- package/dist/core/renderers/updater-helpers.d.ts +45 -0
- package/dist/core/renderers/updater-helpers.js +83 -0
- package/dist/core/resolvers/commit-filter.d.ts +84 -0
- package/dist/core/resolvers/commit-filter.js +95 -0
- package/dist/core/resolvers/config-generator.d.ts +32 -0
- package/dist/core/resolvers/config-generator.js +112 -0
- package/dist/core/resolvers/config-merger.d.ts +26 -0
- package/dist/core/resolvers/config-merger.js +89 -0
- package/dist/core/resolvers/config-validator.d.ts +42 -0
- package/dist/core/resolvers/config-validator.js +61 -0
- package/dist/core/resolvers/dep-commit-resolver.d.ts +63 -0
- package/dist/core/resolvers/dep-commit-resolver.js +158 -0
- package/dist/core/resolvers/dep-doc-resolver.d.ts +70 -0
- package/dist/core/resolvers/dep-doc-resolver.js +84 -0
- package/dist/core/resolvers/index.d.ts +15 -0
- package/dist/core/resolvers/index.js +12 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/types/config.d.ts +10 -0
- package/dist/types/config.js +10 -0
- package/dist/types/context.d.ts +55 -0
- package/dist/types/context.js +10 -0
- package/dist/types/index.d.ts +15 -0
- package/dist/types/index.js +10 -0
- package/dist/types/init.d.ts +56 -0
- package/dist/types/init.js +10 -0
- package/dist/types/tool-result.d.ts +75 -0
- package/dist/types/tool-result.js +40 -0
- package/dist/types/unit.d.ts +118 -0
- package/dist/types/unit.js +10 -0
- package/package.json +71 -0
- package/skills/claude/SKILL.md +375 -0
- package/skills/common/cli-reference.md +251 -0
- package/skills/cursor/SKILL.md +353 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI 서브커맨드 라우터 — Layer 3 (Adapter)
|
|
3
|
+
*
|
|
4
|
+
* process.argv를 직접 파싱하여 4개 서브커맨드(set-unit, update-commit,
|
|
5
|
+
* list-units, context)로 라우팅한다. Commander.js 등 외부 의존성 없이
|
|
6
|
+
* 자체 arg 파싱을 구현 (RULE-003, 페어링 Q1 Option B 채택).
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import { ok, fail } from '../../types/index.js';
|
|
11
|
+
import { printHelp, printVersion, printUnknownCommand, printMissingArg, printSuccess, printError, } from './output.js';
|
|
12
|
+
import { readStdin, parseStdinJson } from '../pipe/stdin-reader.js';
|
|
13
|
+
// ── arg 파싱 ──
|
|
14
|
+
/**
|
|
15
|
+
* process.argv로부터 서브커맨드와 인자/옵션을 파싱한다
|
|
16
|
+
*
|
|
17
|
+
* argv[0] = node, argv[1] = script, argv[2] = subcommand 형태.
|
|
18
|
+
* 글로벌 플래그(--help, --version)는 서브커맨드보다 우선 처리.
|
|
19
|
+
*
|
|
20
|
+
* @param argv - process.argv 배열
|
|
21
|
+
* @returns 파싱된 커맨드 인자 또는 에러
|
|
22
|
+
*/
|
|
23
|
+
export function parseArgs(argv) {
|
|
24
|
+
const args = argv.slice(2);
|
|
25
|
+
// 글로벌 플래그 우선 처리
|
|
26
|
+
if (args.length === 0 || hasFlag(args, '--help') || hasFlag(args, '-h')) {
|
|
27
|
+
return { success: true, data: { command: 'help' } };
|
|
28
|
+
}
|
|
29
|
+
if (hasFlag(args, '--version') || hasFlag(args, '-v')) {
|
|
30
|
+
return { success: true, data: { command: 'version' } };
|
|
31
|
+
}
|
|
32
|
+
const subcommand = args[0] ?? '';
|
|
33
|
+
const rest = args.slice(1);
|
|
34
|
+
const json = hasFlag(rest, '--json');
|
|
35
|
+
switch (subcommand) {
|
|
36
|
+
case 'set-unit':
|
|
37
|
+
return parseSetUnit(rest, json);
|
|
38
|
+
case 'update-commit':
|
|
39
|
+
return parseUpdateCommit(rest, json);
|
|
40
|
+
case 'list-units':
|
|
41
|
+
return parseListUnits(rest, json);
|
|
42
|
+
case 'context':
|
|
43
|
+
return parseContext(rest, json);
|
|
44
|
+
case 'validate':
|
|
45
|
+
return { success: true, data: { command: 'validate', json } };
|
|
46
|
+
case 'init': {
|
|
47
|
+
const fromExisting = hasFlag(rest, '--from-existing');
|
|
48
|
+
const force = hasFlag(rest, '--force');
|
|
49
|
+
return { success: true, data: { command: 'init', json, fromExisting, force } };
|
|
50
|
+
}
|
|
51
|
+
case 'skill':
|
|
52
|
+
return parseSkillCommand(rest, json);
|
|
53
|
+
case 'help':
|
|
54
|
+
case '--help':
|
|
55
|
+
case '-h':
|
|
56
|
+
return { success: true, data: { command: 'help' } };
|
|
57
|
+
case 'version':
|
|
58
|
+
case '--version':
|
|
59
|
+
case '-v':
|
|
60
|
+
return { success: true, data: { command: 'version' } };
|
|
61
|
+
default:
|
|
62
|
+
return fail('UNKNOWN_COMMAND', `알 수 없는 명령어: ${subcommand}`, `사용 가능한 명령어: init, set-unit, update-commit, list-units, context, validate, skill`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* set-unit 서브커맨드의 인자를 파싱한다
|
|
67
|
+
*
|
|
68
|
+
* 필수: <unitId> — 위치 인자 (첫 번째 non-flag 인자)
|
|
69
|
+
*/
|
|
70
|
+
function parseSetUnit(args, json) {
|
|
71
|
+
const unitId = getPositionalArg(args);
|
|
72
|
+
if (!unitId) {
|
|
73
|
+
return fail('MISSING_ARGUMENT', "'set-unit' 명령어에 <unitId> 인자가 필요합니다", '사용법: vc set-unit <unitId> [--request key1,key2] [--no-prompt]');
|
|
74
|
+
}
|
|
75
|
+
const requestRaw = getOptionValue(args, '--request');
|
|
76
|
+
const requestKeys = requestRaw
|
|
77
|
+
? requestRaw
|
|
78
|
+
.split(',')
|
|
79
|
+
.map((k) => k.trim())
|
|
80
|
+
.filter(Boolean)
|
|
81
|
+
: [];
|
|
82
|
+
const noPrompt = hasFlag(args, '--no-prompt');
|
|
83
|
+
return { success: true, data: { command: 'set-unit', unitId, requestKeys, noPrompt, json } };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* update-commit 서브커맨드의 인자를 파싱한다
|
|
87
|
+
*
|
|
88
|
+
* 인자 분기:
|
|
89
|
+
* - 없음 → auto-head (HEAD SHA 자동 감지)
|
|
90
|
+
* - 순수 숫자 → auto-recent (최근 N개 SHA)
|
|
91
|
+
* - 그 외 → manual (기존 SHA 직접 지정)
|
|
92
|
+
*
|
|
93
|
+
* 선택: --mode replace|append (기본: replace)
|
|
94
|
+
* 선택: --section <sectionName>
|
|
95
|
+
*/
|
|
96
|
+
function parseUpdateCommit(args, json) {
|
|
97
|
+
const positional = getPositionalArg(args);
|
|
98
|
+
const modeValue = getOptionValue(args, '--mode');
|
|
99
|
+
const mode = modeValue === 'append' ? 'append' : 'replace';
|
|
100
|
+
const section = getOptionValue(args, '--section');
|
|
101
|
+
const base = {
|
|
102
|
+
command: 'update-commit',
|
|
103
|
+
mode,
|
|
104
|
+
...(section !== undefined && { section }),
|
|
105
|
+
json,
|
|
106
|
+
};
|
|
107
|
+
if (!positional) {
|
|
108
|
+
return { success: true, data: { ...base, commitMode: 'auto-head' } };
|
|
109
|
+
}
|
|
110
|
+
if (/^\d+$/.test(positional)) {
|
|
111
|
+
return {
|
|
112
|
+
success: true,
|
|
113
|
+
data: { ...base, commitMode: 'auto-recent', count: parseInt(positional, 10) },
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return { success: true, data: { ...base, commitMode: 'manual', sha: positional } };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* list-units 서브커맨드의 인자를 파싱한다
|
|
120
|
+
*
|
|
121
|
+
* 선택: --phase <phaseName>
|
|
122
|
+
*/
|
|
123
|
+
function parseListUnits(args, json) {
|
|
124
|
+
const phase = getOptionValue(args, '--phase');
|
|
125
|
+
return {
|
|
126
|
+
success: true,
|
|
127
|
+
data: {
|
|
128
|
+
command: 'list-units',
|
|
129
|
+
...(phase !== undefined && { phase }),
|
|
130
|
+
json,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* context 서브커맨드의 인자를 파싱한다
|
|
136
|
+
*
|
|
137
|
+
* 필수: <unitId> — 위치 인자
|
|
138
|
+
*/
|
|
139
|
+
function parseContext(args, json) {
|
|
140
|
+
const unitId = getPositionalArg(args);
|
|
141
|
+
if (!unitId) {
|
|
142
|
+
return fail('MISSING_ARGUMENT', "'context' 명령어에 <unitId> 인자가 필요합니다", '사용법: vc context <unitId> [--json]');
|
|
143
|
+
}
|
|
144
|
+
return { success: true, data: { command: 'context', unitId, json } };
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* skill 그룹 커맨드의 서브커맨드를 파싱한다
|
|
148
|
+
*
|
|
149
|
+
* 현재 지원: skill install [--cursor] [--claude] [--gemini] [--force]
|
|
150
|
+
* 플랫폼 플래그 미지정 시 Cursor + Claude + Gemini 모두 설치
|
|
151
|
+
*/
|
|
152
|
+
function parseSkillCommand(args, json) {
|
|
153
|
+
const sub = getPositionalArg(args);
|
|
154
|
+
if (!sub) {
|
|
155
|
+
return fail('MISSING_SUBCOMMAND', "'skill' 명령어에 서브커맨드가 필요합니다", '사용법: vc skill install [--cursor] [--claude] [--gemini] [--force]');
|
|
156
|
+
}
|
|
157
|
+
if (sub !== 'install') {
|
|
158
|
+
return fail('UNKNOWN_SUBCOMMAND', `알 수 없는 skill 서브커맨드: ${sub}`, '사용법: vc skill install [--cursor] [--claude] [--gemini] [--force]');
|
|
159
|
+
}
|
|
160
|
+
const cursor = hasFlag(args, '--cursor');
|
|
161
|
+
const claude = hasFlag(args, '--claude');
|
|
162
|
+
const gemini = hasFlag(args, '--gemini');
|
|
163
|
+
const force = hasFlag(args, '--force');
|
|
164
|
+
const targets = !cursor && !claude && !gemini
|
|
165
|
+
? ['cursor', 'claude', 'gemini']
|
|
166
|
+
: [
|
|
167
|
+
...(cursor ? ['cursor'] : []),
|
|
168
|
+
...(claude ? ['claude'] : []),
|
|
169
|
+
...(gemini ? ['gemini'] : []),
|
|
170
|
+
];
|
|
171
|
+
return { success: true, data: { command: 'skill-install', targets, force, json } };
|
|
172
|
+
}
|
|
173
|
+
// ── 유틸리티 함수 ──
|
|
174
|
+
/**
|
|
175
|
+
* args에 특정 플래그가 존재하는지 확인
|
|
176
|
+
*/
|
|
177
|
+
function hasFlag(args, flag) {
|
|
178
|
+
return args.includes(flag);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* 첫 번째 non-flag 위치 인자를 추출
|
|
182
|
+
*
|
|
183
|
+
* --로 시작하지 않는 첫 번째 인자를 반환.
|
|
184
|
+
* 옵션 값으로 사용되는 인자를 건너뛰기 위해 --option value 패턴을 인식.
|
|
185
|
+
*/
|
|
186
|
+
function getPositionalArg(args) {
|
|
187
|
+
for (let i = 0; i < args.length; i++) {
|
|
188
|
+
const arg = args[i];
|
|
189
|
+
if (arg === undefined)
|
|
190
|
+
continue;
|
|
191
|
+
// 플래그는 건너뜀
|
|
192
|
+
if (arg.startsWith('--')) {
|
|
193
|
+
// 값을 갖는 옵션이면 다음 인자도 건너뜀
|
|
194
|
+
if (isValueOption(arg)) {
|
|
195
|
+
i++;
|
|
196
|
+
}
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
return arg;
|
|
200
|
+
}
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* 특정 옵션의 값을 추출 (--option value 패턴)
|
|
205
|
+
*/
|
|
206
|
+
function getOptionValue(args, option) {
|
|
207
|
+
const idx = args.indexOf(option);
|
|
208
|
+
if (idx === -1 || idx + 1 >= args.length) {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
return args[idx + 1];
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* 값을 갖는 옵션인지 판별
|
|
215
|
+
*/
|
|
216
|
+
function isValueOption(arg) {
|
|
217
|
+
return ['--mode', '--section', '--phase', '--request'].includes(arg);
|
|
218
|
+
}
|
|
219
|
+
// ── stdin → ParsedArgs 변환 ──
|
|
220
|
+
/**
|
|
221
|
+
* stdin JSON 데이터로부터 ParsedArgs를 생성한다
|
|
222
|
+
*
|
|
223
|
+
* 서브커맨드는 argv에서 결정되고, 인자/옵션은 stdin JSON에서 추출.
|
|
224
|
+
* --stdin은 --no-prompt를 암시한다 (stdin이 JSON 데이터에 사용되므로 인터랙티브 불가).
|
|
225
|
+
*
|
|
226
|
+
* @param subcommand - argv에서 추출한 서브커맨드 이름
|
|
227
|
+
* @param data - stdin에서 파싱한 JSON 객체
|
|
228
|
+
* @param json - --json 플래그 여부
|
|
229
|
+
* @returns ParsedArgs 또는 에러
|
|
230
|
+
*/
|
|
231
|
+
export function parseStdinArgs(subcommand, data, json, rawArgs) {
|
|
232
|
+
switch (subcommand) {
|
|
233
|
+
case 'set-unit': {
|
|
234
|
+
const unitId = typeof data.unitId === 'string' ? data.unitId : undefined;
|
|
235
|
+
if (!unitId) {
|
|
236
|
+
return fail('STDIN_MISSING_FIELD', 'stdin JSON에 "unitId" (string) 필드가 필요합니다', '예시: {"unitId": "U-001[Mvp]"}');
|
|
237
|
+
}
|
|
238
|
+
const requestKeys = Array.isArray(data.requestKeys)
|
|
239
|
+
? data.requestKeys.filter((k) => typeof k === 'string')
|
|
240
|
+
: [];
|
|
241
|
+
return ok({ command: 'set-unit', unitId, requestKeys, noPrompt: true, json });
|
|
242
|
+
}
|
|
243
|
+
case 'update-commit': {
|
|
244
|
+
const modeValue = typeof data.mode === 'string' ? data.mode : undefined;
|
|
245
|
+
const mode = modeValue === 'append' ? 'append' : 'replace';
|
|
246
|
+
const section = typeof data.section === 'string' ? data.section : undefined;
|
|
247
|
+
const base = {
|
|
248
|
+
command: 'update-commit',
|
|
249
|
+
mode,
|
|
250
|
+
...(section !== undefined && { section }),
|
|
251
|
+
json,
|
|
252
|
+
};
|
|
253
|
+
if (typeof data.sha === 'string') {
|
|
254
|
+
return ok({ ...base, commitMode: 'manual', sha: data.sha });
|
|
255
|
+
}
|
|
256
|
+
if (typeof data.count === 'number' && Number.isInteger(data.count) && data.count > 0) {
|
|
257
|
+
return ok({ ...base, commitMode: 'auto-recent', count: data.count });
|
|
258
|
+
}
|
|
259
|
+
return ok({ ...base, commitMode: 'auto-head' });
|
|
260
|
+
}
|
|
261
|
+
case 'list-units': {
|
|
262
|
+
const phase = typeof data.phase === 'string' ? data.phase : undefined;
|
|
263
|
+
return ok({
|
|
264
|
+
command: 'list-units',
|
|
265
|
+
...(phase !== undefined && { phase }),
|
|
266
|
+
json,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
case 'context': {
|
|
270
|
+
const unitId = typeof data.unitId === 'string' ? data.unitId : undefined;
|
|
271
|
+
if (!unitId) {
|
|
272
|
+
return fail('STDIN_MISSING_FIELD', 'stdin JSON에 "unitId" (string) 필드가 필요합니다', '예시: {"unitId": "U-001[Mvp]"}');
|
|
273
|
+
}
|
|
274
|
+
return ok({ command: 'context', unitId, json });
|
|
275
|
+
}
|
|
276
|
+
case 'validate':
|
|
277
|
+
return ok({ command: 'validate', json });
|
|
278
|
+
case 'init':
|
|
279
|
+
return ok({
|
|
280
|
+
command: 'init',
|
|
281
|
+
json,
|
|
282
|
+
stdinData: data,
|
|
283
|
+
fromExisting: rawArgs?.includes('--from-existing') ?? false,
|
|
284
|
+
force: rawArgs?.includes('--force') ?? false,
|
|
285
|
+
});
|
|
286
|
+
case 'skill': {
|
|
287
|
+
if (!rawArgs?.includes('install')) {
|
|
288
|
+
return fail('UNKNOWN_COMMAND', 'skill 서브커맨드가 필요합니다', '사용법: vc skill install --stdin --json');
|
|
289
|
+
}
|
|
290
|
+
const stdinTargets = Array.isArray(data.targets)
|
|
291
|
+
? data.targets.filter((t) => t === 'cursor' || t === 'claude' || t === 'gemini')
|
|
292
|
+
: [];
|
|
293
|
+
const targets = stdinTargets.length > 0 ? stdinTargets : ['cursor', 'claude', 'gemini'];
|
|
294
|
+
return ok({
|
|
295
|
+
command: 'skill-install',
|
|
296
|
+
targets,
|
|
297
|
+
force: rawArgs.includes('--force'),
|
|
298
|
+
json,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
default:
|
|
302
|
+
return fail('UNKNOWN_COMMAND', `알 수 없는 명령어: ${subcommand}`, '사용 가능한 명령어: init, set-unit, update-commit, list-units, context, validate, skill');
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// ── 메인 라우터 ──
|
|
306
|
+
/**
|
|
307
|
+
* CLI 라우터 메인 실행 함수
|
|
308
|
+
*
|
|
309
|
+
* process.argv를 파싱하고, 해당 서브커맨드의 핸들러를 호출하여 결과를 출력한다.
|
|
310
|
+
* 커맨드별 커스텀 포맷터가 등록되어 있으면 기본 출력 대신 포맷터를 사용.
|
|
311
|
+
*
|
|
312
|
+
* @param argv - process.argv
|
|
313
|
+
* @param projectRoot - 프로젝트 루트 경로
|
|
314
|
+
* @param handlers - 서브커맨드별 핸들러 맵 (선택, 미제공 시 stub 사용)
|
|
315
|
+
* @param formatters - 서브커맨드별 커스텀 출력 포맷터 (선택)
|
|
316
|
+
*/
|
|
317
|
+
export async function route(argv, projectRoot, handlers, formatters) {
|
|
318
|
+
const rawArgs = argv.slice(2);
|
|
319
|
+
const hasJsonFlag = rawArgs.includes('--json');
|
|
320
|
+
const hasStdinFlag = rawArgs.includes('--stdin');
|
|
321
|
+
let parseResult;
|
|
322
|
+
if (hasStdinFlag) {
|
|
323
|
+
// ── stdin 모드: argv에서 서브커맨드, stdin에서 인자 ──
|
|
324
|
+
const subcommand = rawArgs.find((a) => !a.startsWith('--'));
|
|
325
|
+
if (!subcommand) {
|
|
326
|
+
emitError(fail('MISSING_COMMAND', '--stdin 모드에서 서브커맨드가 필요합니다', '사용법: vc <command> --stdin [--json]'), hasJsonFlag);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (['help', '--help', '-h'].includes(subcommand)) {
|
|
330
|
+
printHelp();
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (['version', '--version', '-v'].includes(subcommand)) {
|
|
334
|
+
printVersion();
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const stdinRaw = await readStdin();
|
|
338
|
+
const stdinResult = parseStdinJson(stdinRaw);
|
|
339
|
+
if (!stdinResult.success) {
|
|
340
|
+
emitError(stdinResult, hasJsonFlag);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
parseResult = parseStdinArgs(subcommand, stdinResult.data, hasJsonFlag, rawArgs);
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
// ── 일반 모드: argv에서 모든 인자 ──
|
|
347
|
+
parseResult = parseArgs(argv);
|
|
348
|
+
}
|
|
349
|
+
// 파싱 실패
|
|
350
|
+
if (!parseResult.success) {
|
|
351
|
+
if (hasJsonFlag) {
|
|
352
|
+
printError(parseResult, true);
|
|
353
|
+
}
|
|
354
|
+
else if (parseResult.error.code === 'UNKNOWN_COMMAND') {
|
|
355
|
+
const cmd = argv[2] ?? '';
|
|
356
|
+
printUnknownCommand(cmd);
|
|
357
|
+
}
|
|
358
|
+
else if (parseResult.error.code === 'MISSING_ARGUMENT') {
|
|
359
|
+
const cmd = argv[2] ?? '';
|
|
360
|
+
const argName = extractArgName(parseResult.error.details ?? '');
|
|
361
|
+
printMissingArg(cmd, argName);
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
printError(parseResult, false);
|
|
365
|
+
}
|
|
366
|
+
process.exitCode = 1;
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const parsed = parseResult.data;
|
|
370
|
+
// 글로벌 커맨드 처리
|
|
371
|
+
if (parsed.command === 'help') {
|
|
372
|
+
printHelp();
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (parsed.command === 'version') {
|
|
376
|
+
printVersion();
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
// 서브커맨드 핸들러 조회
|
|
380
|
+
const handler = handlers?.[parsed.command] ?? stubHandler;
|
|
381
|
+
const json = 'json' in parsed ? parsed.json : false;
|
|
382
|
+
const formatter = formatters?.[parsed.command];
|
|
383
|
+
const result = await handler(parsed, projectRoot);
|
|
384
|
+
if (result.success) {
|
|
385
|
+
if (formatter) {
|
|
386
|
+
formatter(result, json);
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
printSuccess(result, json);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
printError(result, json);
|
|
394
|
+
process.exitCode = 1;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* 에러를 출력하고 프로세스 종료 코드를 설정한다
|
|
399
|
+
*/
|
|
400
|
+
function emitError(result, json) {
|
|
401
|
+
printError(result, json);
|
|
402
|
+
process.exitCode = 1;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* 스텁 핸들러 — 후속 유닛에서 교체될 임시 핸들러
|
|
406
|
+
*
|
|
407
|
+
* 해당 커맨드가 아직 구현되지 않았음을 알린다.
|
|
408
|
+
*/
|
|
409
|
+
function stubHandler(args) {
|
|
410
|
+
return fail('NOT_IMPLEMENTED', `'${args.command}' 명령어는 아직 구현되지 않았습니다`, '후속 유닛(U-014 ~ U-016)에서 구현 예정');
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* 에러 상세 메시지에서 인자 이름을 추출
|
|
414
|
+
*/
|
|
415
|
+
function extractArgName(details) {
|
|
416
|
+
const match = details.match(/<(\w+)>/);
|
|
417
|
+
return match?.[1] ?? 'arg';
|
|
418
|
+
}
|
|
419
|
+
//# sourceMappingURL=router.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 프로젝트 파일 스캐너 — Layer 3 (Adapter)
|
|
3
|
+
*
|
|
4
|
+
* 기존 프로젝트의 파일/디렉토리를 자동 스캔하여 ID 패턴, 경로 구조,
|
|
5
|
+
* 커맨드 파일 섹션 헤더를 감지하고 설정 초안 데이터를 생성한다.
|
|
6
|
+
*
|
|
7
|
+
* `vc init --from-existing` 커맨드에서 사용.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
import type { ScanResult } from '../../types/index.js';
|
|
12
|
+
/**
|
|
13
|
+
* 프로젝트 루트를 스캔하여 ID 패턴, 문서 디렉토리, 커맨드 파일을 감지한다
|
|
14
|
+
*
|
|
15
|
+
* @param projectRoot - 프로젝트 루트 절대 경로
|
|
16
|
+
* @returns 스캔 결과
|
|
17
|
+
*/
|
|
18
|
+
export declare function scanProject(projectRoot: string): ScanResult;
|
|
19
|
+
//# sourceMappingURL=scanner.d.ts.map
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 프로젝트 파일 스캐너 — Layer 3 (Adapter)
|
|
3
|
+
*
|
|
4
|
+
* 기존 프로젝트의 파일/디렉토리를 자동 스캔하여 ID 패턴, 경로 구조,
|
|
5
|
+
* 커맨드 파일 섹션 헤더를 감지하고 설정 초안 데이터를 생성한다.
|
|
6
|
+
*
|
|
7
|
+
* `vc init --from-existing` 커맨드에서 사용.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
12
|
+
import { join, relative, basename, dirname, extname } from 'node:path';
|
|
13
|
+
// ── 상수 ──
|
|
14
|
+
const EXCLUDED_DIRS = new Set([
|
|
15
|
+
'node_modules',
|
|
16
|
+
'.git',
|
|
17
|
+
'dist',
|
|
18
|
+
'build',
|
|
19
|
+
'coverage',
|
|
20
|
+
'.cache',
|
|
21
|
+
'.next',
|
|
22
|
+
'.nuxt',
|
|
23
|
+
'.svelte-kit',
|
|
24
|
+
'.turbo',
|
|
25
|
+
'.vercel',
|
|
26
|
+
'out',
|
|
27
|
+
'__pycache__',
|
|
28
|
+
]);
|
|
29
|
+
const MAX_DEPTH = 6;
|
|
30
|
+
/**
|
|
31
|
+
* 프로젝트 루트를 스캔하여 ID 패턴, 문서 디렉토리, 커맨드 파일을 감지한다
|
|
32
|
+
*
|
|
33
|
+
* @param projectRoot - 프로젝트 루트 절대 경로
|
|
34
|
+
* @returns 스캔 결과
|
|
35
|
+
*/
|
|
36
|
+
export function scanProject(projectRoot) {
|
|
37
|
+
const mdFiles = collectMdFiles(projectRoot, projectRoot, 0);
|
|
38
|
+
const idPatterns = detectIdPatterns(mdFiles);
|
|
39
|
+
const docRoots = detectDocRoots(mdFiles);
|
|
40
|
+
const commandFiles = detectCommandFiles(mdFiles, projectRoot);
|
|
41
|
+
const roadmapCandidates = detectRoadmapCandidates(mdFiles);
|
|
42
|
+
const suggestedIdPattern = buildCombinedPattern(idPatterns);
|
|
43
|
+
return {
|
|
44
|
+
totalMdFiles: mdFiles.length,
|
|
45
|
+
idPatterns,
|
|
46
|
+
docRoots,
|
|
47
|
+
commandFiles,
|
|
48
|
+
roadmapCandidates,
|
|
49
|
+
suggestedIdPattern,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// ── 파일 수집 ──
|
|
53
|
+
function collectMdFiles(root, dir, depth) {
|
|
54
|
+
if (depth > MAX_DEPTH)
|
|
55
|
+
return [];
|
|
56
|
+
const results = [];
|
|
57
|
+
try {
|
|
58
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
if (entry.isDirectory()) {
|
|
61
|
+
if (!EXCLUDED_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
|
|
62
|
+
results.push(...collectMdFiles(root, join(dir, entry.name), depth + 1));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else if (entry.isFile() && extname(entry.name).toLowerCase() === '.md') {
|
|
66
|
+
const relPath = relative(root, join(dir, entry.name)).replace(/\\/g, '/');
|
|
67
|
+
results.push(relPath);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// 읽기 권한 없는 디렉토리 건너뜀
|
|
73
|
+
}
|
|
74
|
+
return results;
|
|
75
|
+
}
|
|
76
|
+
function detectIdPatterns(mdFiles) {
|
|
77
|
+
const prefixGroups = new Map();
|
|
78
|
+
for (const file of mdFiles) {
|
|
79
|
+
const name = basename(file, '.md');
|
|
80
|
+
// [우선순위 1] PREFIX-NNN[Tag] (예: U-001[Mvp], RU-001[Mmp])
|
|
81
|
+
const tagMatch = name.match(/^([A-Z][A-Z0-9]*)-\d+\[/);
|
|
82
|
+
if (tagMatch?.[1]) {
|
|
83
|
+
upsertGroup(prefixGroups, tagMatch[1], name, true, false);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
// [우선순위 2] PREFIX-WORD-NNN (예: CP-MVP-01)
|
|
87
|
+
const compoundMatch = name.match(/^([A-Z][A-Z0-9]*)-[A-Z][A-Z0-9]*-\d+/);
|
|
88
|
+
if (compoundMatch?.[1]) {
|
|
89
|
+
upsertGroup(prefixGroups, compoundMatch[1], name, false, true);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
// [우선순위 3] PREFIX-NNN (예: TASK-001)
|
|
93
|
+
const basicMatch = name.match(/^([A-Z][A-Z0-9]*)-\d+/);
|
|
94
|
+
if (basicMatch?.[1]) {
|
|
95
|
+
upsertGroup(prefixGroups, basicMatch[1], name, false, false);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const patterns = [];
|
|
99
|
+
for (const [prefix, group] of prefixGroups) {
|
|
100
|
+
let regex;
|
|
101
|
+
let displayLabel;
|
|
102
|
+
if (group.isCompound) {
|
|
103
|
+
regex = `${prefix}-`;
|
|
104
|
+
displayLabel = `${prefix}-*-NN`;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
regex = `${prefix}-\\d+`;
|
|
108
|
+
displayLabel = group.hasTag ? `${prefix}-NNN[*]` : `${prefix}-NNN`;
|
|
109
|
+
}
|
|
110
|
+
patterns.push({
|
|
111
|
+
prefix,
|
|
112
|
+
regex: `^${regex}`,
|
|
113
|
+
displayLabel,
|
|
114
|
+
matchCount: group.names.length,
|
|
115
|
+
examples: group.names.slice(0, 3),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
patterns.sort((a, b) => b.matchCount - a.matchCount);
|
|
119
|
+
return patterns;
|
|
120
|
+
}
|
|
121
|
+
function upsertGroup(groups, prefix, name, hasTag, isCompound) {
|
|
122
|
+
const existing = groups.get(prefix);
|
|
123
|
+
if (existing) {
|
|
124
|
+
existing.names.push(name);
|
|
125
|
+
if (hasTag)
|
|
126
|
+
existing.hasTag = true;
|
|
127
|
+
if (isCompound)
|
|
128
|
+
existing.isCompound = true;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
groups.set(prefix, { names: [name], hasTag, isCompound });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// ── 문서 디렉토리 감지 ──
|
|
135
|
+
const ROLE_KEYWORDS = {
|
|
136
|
+
plan: ['plan', 'plans', 'unit-plan', 'unit-plans'],
|
|
137
|
+
result: ['result', 'results', 'unit-result', 'unit-results', 'report', 'reports'],
|
|
138
|
+
runbook: ['runbook', 'runbooks', 'unit-runbook', 'unit-runbooks'],
|
|
139
|
+
};
|
|
140
|
+
function detectDocRoots(mdFiles) {
|
|
141
|
+
const dirCounts = new Map();
|
|
142
|
+
for (const file of mdFiles) {
|
|
143
|
+
const dir = dirname(file);
|
|
144
|
+
if (dir === '.')
|
|
145
|
+
continue;
|
|
146
|
+
dirCounts.set(dir, (dirCounts.get(dir) ?? 0) + 1);
|
|
147
|
+
}
|
|
148
|
+
const docRoots = [];
|
|
149
|
+
for (const [dir, count] of dirCounts) {
|
|
150
|
+
const dirBasename = basename(dir).toLowerCase();
|
|
151
|
+
let role = 'other';
|
|
152
|
+
for (const [r, keywords] of Object.entries(ROLE_KEYWORDS)) {
|
|
153
|
+
if (keywords.includes(dirBasename)) {
|
|
154
|
+
role = r;
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (count >= 2 || role !== 'other') {
|
|
159
|
+
docRoots.push({ role, dirPath: dir, fileCount: count });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const roleOrder = { plan: 0, result: 1, runbook: 2, other: 3 };
|
|
163
|
+
docRoots.sort((a, b) => {
|
|
164
|
+
const orderDiff = (roleOrder[a.role] ?? 3) - (roleOrder[b.role] ?? 3);
|
|
165
|
+
if (orderDiff !== 0)
|
|
166
|
+
return orderDiff;
|
|
167
|
+
return b.fileCount - a.fileCount;
|
|
168
|
+
});
|
|
169
|
+
return docRoots;
|
|
170
|
+
}
|
|
171
|
+
// ── 커맨드 파일 감지 ──
|
|
172
|
+
const COMMAND_FILE_INDICATORS = ['command', 'commands', 'task', 'tasks'];
|
|
173
|
+
function detectCommandFiles(mdFiles, projectRoot) {
|
|
174
|
+
const candidates = [];
|
|
175
|
+
for (const file of mdFiles) {
|
|
176
|
+
const name = basename(file, '.md').toLowerCase();
|
|
177
|
+
if (!COMMAND_FILE_INDICATORS.some((ind) => name.includes(ind)))
|
|
178
|
+
continue;
|
|
179
|
+
try {
|
|
180
|
+
const content = readFileSync(join(projectRoot, file), 'utf-8');
|
|
181
|
+
const sections = extractHeaders(content);
|
|
182
|
+
const separatorMatch = content.match(/^(-{10,}|={10,})$/m);
|
|
183
|
+
if (sections.length > 0) {
|
|
184
|
+
candidates.push({
|
|
185
|
+
filePath: file,
|
|
186
|
+
sections,
|
|
187
|
+
hasSeparator: separatorMatch !== null,
|
|
188
|
+
separatorSample: separatorMatch?.[1] ?? null,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// 읽기 실패한 파일 건너뜀
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return candidates;
|
|
197
|
+
}
|
|
198
|
+
function extractHeaders(content) {
|
|
199
|
+
const lines = content.split('\n');
|
|
200
|
+
const headers = [];
|
|
201
|
+
for (const line of lines) {
|
|
202
|
+
const match = line.match(/^(#{1,3})\s+(.+)/);
|
|
203
|
+
if (match?.[1] && match[2]) {
|
|
204
|
+
headers.push(`${'#'.repeat(match[1].length)} ${match[2].trim()}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return headers;
|
|
208
|
+
}
|
|
209
|
+
// ── 로드맵 감지 ──
|
|
210
|
+
function detectRoadmapCandidates(mdFiles) {
|
|
211
|
+
return mdFiles.filter((file) => {
|
|
212
|
+
const name = basename(file, '.md').toLowerCase();
|
|
213
|
+
return name === 'roadmap' || name.includes('roadmap');
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
// ── 통합 ID 패턴 생성 ──
|
|
217
|
+
function buildCombinedPattern(patterns) {
|
|
218
|
+
if (patterns.length === 0)
|
|
219
|
+
return null;
|
|
220
|
+
if (patterns.length === 1) {
|
|
221
|
+
const first = patterns[0];
|
|
222
|
+
return first ? `${first.regex}.*` : null;
|
|
223
|
+
}
|
|
224
|
+
const parts = patterns.map((p) => p.regex.replace(/^\^/, ''));
|
|
225
|
+
return `^(${parts.join('|')}).*`;
|
|
226
|
+
}
|
|
227
|
+
//# sourceMappingURL=scanner.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stdin JSON 리더 — Layer 3 (Adapter)
|
|
3
|
+
*
|
|
4
|
+
* stdin에서 전체 입력을 버퍼링하고 JSON으로 파싱한다.
|
|
5
|
+
* 에이전트/오케스트레이터의 파이프 연동을 위한 입력 어댑터.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import type { ToolResult } from '../../types/index.js';
|
|
10
|
+
/**
|
|
11
|
+
* stdin에서 전체 데이터를 읽는다
|
|
12
|
+
*
|
|
13
|
+
* stdin stream을 EOF까지 버퍼링하여 문자열로 반환.
|
|
14
|
+
* TTY(터미널 직접 실행)인 경우 빈 문자열 반환.
|
|
15
|
+
*/
|
|
16
|
+
export declare function readStdin(): Promise<string>;
|
|
17
|
+
/**
|
|
18
|
+
* stdin 문자열을 JSON 객체로 파싱한다
|
|
19
|
+
*
|
|
20
|
+
* 빈 입력, 비-객체, 파싱 오류를 구조화된 ToolResult로 반환.
|
|
21
|
+
*/
|
|
22
|
+
export declare function parseStdinJson(raw: string): ToolResult<Record<string, unknown>>;
|
|
23
|
+
//# sourceMappingURL=stdin-reader.d.ts.map
|