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.
Files changed (117) hide show
  1. package/LICENSE +21 -0
  2. package/dist/adapters/cli/commands/context-resolver.d.ts +29 -0
  3. package/dist/adapters/cli/commands/context-resolver.js +93 -0
  4. package/dist/adapters/cli/commands/context.d.ts +26 -0
  5. package/dist/adapters/cli/commands/context.js +39 -0
  6. package/dist/adapters/cli/commands/git-helpers.d.ts +43 -0
  7. package/dist/adapters/cli/commands/git-helpers.js +99 -0
  8. package/dist/adapters/cli/commands/init-helpers.d.ts +30 -0
  9. package/dist/adapters/cli/commands/init-helpers.js +112 -0
  10. package/dist/adapters/cli/commands/init-interactive.d.ts +25 -0
  11. package/dist/adapters/cli/commands/init-interactive.js +143 -0
  12. package/dist/adapters/cli/commands/init.d.ts +25 -0
  13. package/dist/adapters/cli/commands/init.js +73 -0
  14. package/dist/adapters/cli/commands/io-helpers.d.ts +45 -0
  15. package/dist/adapters/cli/commands/io-helpers.js +144 -0
  16. package/dist/adapters/cli/commands/list-units.d.ts +39 -0
  17. package/dist/adapters/cli/commands/list-units.js +92 -0
  18. package/dist/adapters/cli/commands/prompt-helpers.d.ts +36 -0
  19. package/dist/adapters/cli/commands/prompt-helpers.js +113 -0
  20. package/dist/adapters/cli/commands/set-unit.d.ts +44 -0
  21. package/dist/adapters/cli/commands/set-unit.js +90 -0
  22. package/dist/adapters/cli/commands/skill-install.d.ts +31 -0
  23. package/dist/adapters/cli/commands/skill-install.js +85 -0
  24. package/dist/adapters/cli/commands/state-helpers.d.ts +28 -0
  25. package/dist/adapters/cli/commands/state-helpers.js +55 -0
  26. package/dist/adapters/cli/commands/update-commit.d.ts +42 -0
  27. package/dist/adapters/cli/commands/update-commit.js +180 -0
  28. package/dist/adapters/cli/commands/validate.d.ts +40 -0
  29. package/dist/adapters/cli/commands/validate.js +219 -0
  30. package/dist/adapters/cli/formatters-list.d.ts +36 -0
  31. package/dist/adapters/cli/formatters-list.js +106 -0
  32. package/dist/adapters/cli/formatters-unit.d.ts +44 -0
  33. package/dist/adapters/cli/formatters-unit.js +287 -0
  34. package/dist/adapters/cli/formatters.d.ts +2 -0
  35. package/dist/adapters/cli/formatters.js +3 -0
  36. package/dist/adapters/cli/index.d.ts +17 -0
  37. package/dist/adapters/cli/index.js +60 -0
  38. package/dist/adapters/cli/output.d.ts +58 -0
  39. package/dist/adapters/cli/output.js +159 -0
  40. package/dist/adapters/cli/router.d.ts +99 -0
  41. package/dist/adapters/cli/router.js +419 -0
  42. package/dist/adapters/cli/scanner.d.ts +19 -0
  43. package/dist/adapters/cli/scanner.js +227 -0
  44. package/dist/adapters/pipe/stdin-reader.d.ts +23 -0
  45. package/dist/adapters/pipe/stdin-reader.js +56 -0
  46. package/dist/config/loader.d.ts +27 -0
  47. package/dist/config/loader.js +84 -0
  48. package/dist/config/resolver.d.ts +71 -0
  49. package/dist/config/resolver.js +124 -0
  50. package/dist/config/schema.d.ts +205 -0
  51. package/dist/config/schema.js +186 -0
  52. package/dist/core/parsers/backlog-parser.d.ts +25 -0
  53. package/dist/core/parsers/backlog-parser.js +255 -0
  54. package/dist/core/parsers/dep-line-parser.d.ts +36 -0
  55. package/dist/core/parsers/dep-line-parser.js +176 -0
  56. package/dist/core/parsers/dependency-extractor.d.ts +64 -0
  57. package/dist/core/parsers/dependency-extractor.js +193 -0
  58. package/dist/core/parsers/index.d.ts +20 -0
  59. package/dist/core/parsers/index.js +20 -0
  60. package/dist/core/parsers/md-utils.d.ts +81 -0
  61. package/dist/core/parsers/md-utils.js +144 -0
  62. package/dist/core/parsers/metadata-parser.d.ts +49 -0
  63. package/dist/core/parsers/metadata-parser.js +133 -0
  64. package/dist/core/parsers/pairing-question-extractor.d.ts +42 -0
  65. package/dist/core/parsers/pairing-question-extractor.js +146 -0
  66. package/dist/core/parsers/plan-parser-helpers.d.ts +61 -0
  67. package/dist/core/parsers/plan-parser-helpers.js +90 -0
  68. package/dist/core/parsers/plan-parser.d.ts +55 -0
  69. package/dist/core/parsers/plan-parser.js +69 -0
  70. package/dist/core/parsers/title-extractor.d.ts +36 -0
  71. package/dist/core/parsers/title-extractor.js +101 -0
  72. package/dist/core/renderers/index.d.ts +15 -0
  73. package/dist/core/renderers/index.js +18 -0
  74. package/dist/core/renderers/interpolate.d.ts +92 -0
  75. package/dist/core/renderers/interpolate.js +117 -0
  76. package/dist/core/renderers/marker-utils.d.ts +60 -0
  77. package/dist/core/renderers/marker-utils.js +85 -0
  78. package/dist/core/renderers/section-renderer.d.ts +31 -0
  79. package/dist/core/renderers/section-renderer.js +171 -0
  80. package/dist/core/renderers/section-updater.d.ts +72 -0
  81. package/dist/core/renderers/section-updater.js +175 -0
  82. package/dist/core/renderers/template-engine.d.ts +69 -0
  83. package/dist/core/renderers/template-engine.js +92 -0
  84. package/dist/core/renderers/updater-helpers.d.ts +45 -0
  85. package/dist/core/renderers/updater-helpers.js +83 -0
  86. package/dist/core/resolvers/commit-filter.d.ts +84 -0
  87. package/dist/core/resolvers/commit-filter.js +95 -0
  88. package/dist/core/resolvers/config-generator.d.ts +32 -0
  89. package/dist/core/resolvers/config-generator.js +112 -0
  90. package/dist/core/resolvers/config-merger.d.ts +26 -0
  91. package/dist/core/resolvers/config-merger.js +89 -0
  92. package/dist/core/resolvers/config-validator.d.ts +42 -0
  93. package/dist/core/resolvers/config-validator.js +61 -0
  94. package/dist/core/resolvers/dep-commit-resolver.d.ts +63 -0
  95. package/dist/core/resolvers/dep-commit-resolver.js +158 -0
  96. package/dist/core/resolvers/dep-doc-resolver.d.ts +70 -0
  97. package/dist/core/resolvers/dep-doc-resolver.js +84 -0
  98. package/dist/core/resolvers/index.d.ts +15 -0
  99. package/dist/core/resolvers/index.js +12 -0
  100. package/dist/index.d.ts +8 -0
  101. package/dist/index.js +8 -0
  102. package/dist/types/config.d.ts +10 -0
  103. package/dist/types/config.js +10 -0
  104. package/dist/types/context.d.ts +55 -0
  105. package/dist/types/context.js +10 -0
  106. package/dist/types/index.d.ts +15 -0
  107. package/dist/types/index.js +10 -0
  108. package/dist/types/init.d.ts +56 -0
  109. package/dist/types/init.js +10 -0
  110. package/dist/types/tool-result.d.ts +75 -0
  111. package/dist/types/tool-result.js +40 -0
  112. package/dist/types/unit.d.ts +118 -0
  113. package/dist/types/unit.js +10 -0
  114. package/package.json +71 -0
  115. package/skills/claude/SKILL.md +375 -0
  116. package/skills/common/cli-reference.md +251 -0
  117. package/skills/cursor/SKILL.md +353 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 viilab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,29 @@
1
+ /**
2
+ * 유닛 컨텍스트 해석기 — Layer 3 (Adapter)
3
+ *
4
+ * 유닛 ID를 받아 설정 로드, 계획서 파싱, 의존성 수집을 거쳐
5
+ * 최종 CommandContext를 구성하는 공통 로직을 담당한다.
6
+ * set-unit과 context 커맨드에서 공통으로 사용.
7
+ *
8
+ * @module
9
+ */
10
+ import type { ToolResult, CommandContext, ProjectConfig, UnitTypeConfig } from '../../../types/index.js';
11
+ /** 컨텍스트 해석 결과 (Context + 추가 메타데이터) */
12
+ export interface ResolvedContext {
13
+ context: CommandContext;
14
+ config: ProjectConfig;
15
+ unitTypeKey: string;
16
+ unitTypeConfig: UnitTypeConfig;
17
+ planRelPath: string;
18
+ warnings: string[];
19
+ }
20
+ /**
21
+ * 유닛 ID에 해당하는 전체 컨텍스트를 해석한다
22
+ *
23
+ * @param unitId - 유닛 ID
24
+ * @param projectRoot - 프로젝트 루트 절대 경로
25
+ * @param requestKeys - 커스텀 요청사항 키 목록 (--request 옵션)
26
+ * @returns 해석된 컨텍스트 또는 에러
27
+ */
28
+ export declare function resolveCommandContext(unitId: string, projectRoot: string, requestKeys?: string[]): ToolResult<ResolvedContext>;
29
+ //# sourceMappingURL=context-resolver.d.ts.map
@@ -0,0 +1,93 @@
1
+ /**
2
+ * 유닛 컨텍스트 해석기 — Layer 3 (Adapter)
3
+ *
4
+ * 유닛 ID를 받아 설정 로드, 계획서 파싱, 의존성 수집을 거쳐
5
+ * 최종 CommandContext를 구성하는 공통 로직을 담당한다.
6
+ * set-unit과 context 커맨드에서 공통으로 사용.
7
+ *
8
+ * @module
9
+ */
10
+ import { readFileSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import { ok, fail } from '../../../types/index.js';
13
+ import { loadConfig } from '../../../config/loader.js';
14
+ import { resolveUnitType, locatePlan } from '../../../config/resolver.js';
15
+ import { parseUnitPlan } from '../../../core/parsers/plan-parser.js';
16
+ import { resolveDepDocs } from '../../../core/resolvers/dep-doc-resolver.js';
17
+ import { resolveActualDocs, collectGitCommits, buildSpecialRequests } from './io-helpers.js';
18
+ /**
19
+ * 유닛 ID에 해당하는 전체 컨텍스트를 해석한다
20
+ *
21
+ * @param unitId - 유닛 ID
22
+ * @param projectRoot - 프로젝트 루트 절대 경로
23
+ * @param requestKeys - 커스텀 요청사항 키 목록 (--request 옵션)
24
+ * @returns 해석된 컨텍스트 또는 에러
25
+ */
26
+ export function resolveCommandContext(unitId, projectRoot, requestKeys) {
27
+ // 1. 설정 로드
28
+ const configResult = loadConfig(projectRoot);
29
+ if (!configResult.success)
30
+ return configResult;
31
+ const config = configResult.data;
32
+ // 2. 유닛 유형 판별
33
+ const typeResult = resolveUnitType(unitId, config);
34
+ if (!typeResult.success)
35
+ return typeResult;
36
+ const unitTypeConfig = typeResult.data;
37
+ // 3. 계획서 경로 결정
38
+ const planResult = locatePlan(unitId, config);
39
+ if (!planResult.success)
40
+ return planResult;
41
+ const planRelPath = planResult.data;
42
+ const planAbsPath = join(projectRoot, planRelPath);
43
+ // 4. 계획서 읽기 (Adapter I/O)
44
+ let planContent;
45
+ try {
46
+ planContent = readFileSync(planAbsPath, 'utf-8');
47
+ }
48
+ catch {
49
+ return fail('PLAN_NOT_FOUND', `계획서를 찾을 수 없습니다: ${planRelPath}`, `절대 경로: ${planAbsPath}`);
50
+ }
51
+ // 5. 계획서 파싱 (Core 순수 함수)
52
+ const parseResult = parseUnitPlan(planContent, unitId, config.planParsing, unitTypeConfig.idPattern);
53
+ if (!parseResult.success)
54
+ return parseResult;
55
+ const { meta, questions, warnings } = parseResult.data;
56
+ // Layer 2/3 메타데이터 추가
57
+ meta.unitType = unitTypeConfig.key;
58
+ meta.planPath = planRelPath;
59
+ // 6. 의존성 수집 (collectDeps=true이고 의존성이 있을 때만)
60
+ let depDocs = [];
61
+ let depCommits = [];
62
+ if (unitTypeConfig.collectDeps && meta.depends.length > 0) {
63
+ const depIds = meta.depends.map((d) => d.unitId);
64
+ // 6a. 의존 문서 경로 패턴 → 실제 파일 확인 (Adapter I/O)
65
+ const docResult = resolveDepDocs(depIds, config.paths.docRoots, config.docDiscovery);
66
+ if (docResult.success) {
67
+ depDocs = resolveActualDocs(docResult.data, projectRoot);
68
+ }
69
+ // 6b. Git 커밋 수집 (Adapter I/O)
70
+ depCommits = collectGitCommits(depIds, config.commitSearch, projectRoot);
71
+ }
72
+ // 7. 특별 요청사항 조합
73
+ const srResult = buildSpecialRequests(config, unitTypeConfig.key, requestKeys);
74
+ const specialRequests = srResult.requests;
75
+ warnings.push(...srResult.warnings);
76
+ // 8. CommandContext 조립
77
+ const context = {
78
+ unit: meta,
79
+ depDocs,
80
+ depCommits,
81
+ pairingQuestions: questions,
82
+ specialRequests,
83
+ };
84
+ return ok({
85
+ context,
86
+ config,
87
+ unitTypeKey: unitTypeConfig.key,
88
+ unitTypeConfig,
89
+ planRelPath,
90
+ warnings,
91
+ });
92
+ }
93
+ //# sourceMappingURL=context-resolver.js.map
@@ -0,0 +1,26 @@
1
+ /**
2
+ * context 커맨드 핸들러 — Layer 3 (Adapter)
3
+ *
4
+ * set-unit과 동일한 파싱 파이프라인을 실행하되,
5
+ * 커맨드 파일을 수정하지 않고 CommandContext만 반환한다.
6
+ * 읽기 전용 유닛 컨텍스트 조회에 사용.
7
+ *
8
+ * Core 순수 함수들(parseUnitPlan, resolveDepDocs, findDepCommits)을 조합하며,
9
+ * I/O(파일 읽기, git 실행)는 이 Adapter에서만 수행.
10
+ *
11
+ * @module
12
+ */
13
+ import type { ToolResult, CommandContext } from '../../../types/index.js';
14
+ import type { ParsedArgs } from '../router.js';
15
+ /**
16
+ * context 커맨드를 실행한다
17
+ *
18
+ * set-unit의 컨텍스트 해석 로직(resolveCommandContext)을 재사용하되,
19
+ * 섹션 렌더링·커맨드 파일 업데이트를 생략하고 JSON/텍스트 결과만 반환한다.
20
+ *
21
+ * @param args - 파싱된 CLI 인자 (command === 'context' 보장)
22
+ * @param projectRoot - 프로젝트 루트 절대 경로
23
+ * @returns CommandContext 또는 에러
24
+ */
25
+ export declare function handleContext(args: ParsedArgs, projectRoot: string): ToolResult<CommandContext>;
26
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1,39 @@
1
+ /**
2
+ * context 커맨드 핸들러 — Layer 3 (Adapter)
3
+ *
4
+ * set-unit과 동일한 파싱 파이프라인을 실행하되,
5
+ * 커맨드 파일을 수정하지 않고 CommandContext만 반환한다.
6
+ * 읽기 전용 유닛 컨텍스트 조회에 사용.
7
+ *
8
+ * Core 순수 함수들(parseUnitPlan, resolveDepDocs, findDepCommits)을 조합하며,
9
+ * I/O(파일 읽기, git 실행)는 이 Adapter에서만 수행.
10
+ *
11
+ * @module
12
+ */
13
+ import { fail } from '../../../types/index.js';
14
+ import { resolveCommandContext } from './context-resolver.js';
15
+ /**
16
+ * context 커맨드를 실행한다
17
+ *
18
+ * set-unit의 컨텍스트 해석 로직(resolveCommandContext)을 재사용하되,
19
+ * 섹션 렌더링·커맨드 파일 업데이트를 생략하고 JSON/텍스트 결과만 반환한다.
20
+ *
21
+ * @param args - 파싱된 CLI 인자 (command === 'context' 보장)
22
+ * @param projectRoot - 프로젝트 루트 절대 경로
23
+ * @returns CommandContext 또는 에러
24
+ */
25
+ export function handleContext(args, projectRoot) {
26
+ if (args.command !== 'context') {
27
+ return fail('INTERNAL_ERROR', 'handleContext에 잘못된 커맨드가 전달되었습니다');
28
+ }
29
+ const { unitId } = args;
30
+ // 1. 전체 컨텍스트 해석 (I/O + 파싱 + 의존성 수집)
31
+ const resolveResult = resolveCommandContext(unitId, projectRoot);
32
+ if (!resolveResult.success)
33
+ return resolveResult;
34
+ return {
35
+ success: true,
36
+ data: resolveResult.data.context,
37
+ };
38
+ }
39
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Git 헬퍼 함수 — Layer 3 (Adapter)
3
+ *
4
+ * git CLI를 child_process로 실행하여 커밋 SHA를 수집한다.
5
+ * Layer 3에 배치: child_process 사용이므로 Core에 배치 불가.
6
+ *
7
+ * @module
8
+ */
9
+ import type { ToolResult } from '../../../types/index.js';
10
+ /**
11
+ * 현재 HEAD 커밋의 short SHA(8자)를 반환한다
12
+ *
13
+ * @param projectRoot - git 저장소 루트 경로
14
+ * @returns short SHA 문자열 또는 에러
15
+ */
16
+ export declare function getHeadSha(projectRoot: string): ToolResult<string>;
17
+ /**
18
+ * 특정 커밋에서 변경된 파일 경로 목록을 반환한다
19
+ *
20
+ * `git diff-tree --no-commit-id --name-only -r <SHA>` 실행.
21
+ * 프로젝트 루트 기준 상대 경로 배열로 반환.
22
+ * merge commit의 경우 첫 번째 parent 기준 diff 사용 (`-c` 옵션 미사용).
23
+ *
24
+ * @param projectRoot - git 저장소 루트 경로
25
+ * @param sha - 커밋 SHA (축약 또는 전체)
26
+ * @returns 변경 파일 경로 배열 또는 에러
27
+ */
28
+ export declare function getChangedFiles(projectRoot: string, sha: string): ToolResult<string[]>;
29
+ /**
30
+ * 최근 N개 커밋의 short SHA(8자) 배열을 반환한다
31
+ *
32
+ * 커밋 수가 요청보다 적으면 있는 만큼만 반환 (Graceful Degradation)
33
+ *
34
+ * @param projectRoot - git 저장소 루트 경로
35
+ * @param count - 가져올 커밋 수
36
+ * @returns short SHA 배열과 실제 개수 정보 또는 에러
37
+ */
38
+ export declare function getRecentShas(projectRoot: string, count: number): ToolResult<{
39
+ shas: string[];
40
+ requested: number;
41
+ actual: number;
42
+ }>;
43
+ //# sourceMappingURL=git-helpers.d.ts.map
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Git 헬퍼 함수 — Layer 3 (Adapter)
3
+ *
4
+ * git CLI를 child_process로 실행하여 커밋 SHA를 수집한다.
5
+ * Layer 3에 배치: child_process 사용이므로 Core에 배치 불가.
6
+ *
7
+ * @module
8
+ */
9
+ import { execSync } from 'node:child_process';
10
+ import { ok, fail } from '../../../types/index.js';
11
+ /**
12
+ * 현재 HEAD 커밋의 short SHA(8자)를 반환한다
13
+ *
14
+ * @param projectRoot - git 저장소 루트 경로
15
+ * @returns short SHA 문자열 또는 에러
16
+ */
17
+ export function getHeadSha(projectRoot) {
18
+ try {
19
+ const sha = execSync('git rev-parse --short=8 HEAD', {
20
+ cwd: projectRoot,
21
+ encoding: 'utf-8',
22
+ timeout: 5000,
23
+ stdio: ['pipe', 'pipe', 'pipe'],
24
+ }).replace(/[\r\n]+$/, '');
25
+ if (!sha) {
26
+ return fail('GIT_NO_COMMITS', '커밋이 없는 저장소입니다', '최소 1개의 커밋이 필요합니다');
27
+ }
28
+ return ok(sha);
29
+ }
30
+ catch {
31
+ return fail('GIT_NOT_AVAILABLE', 'git 명령을 실행할 수 없습니다', '현재 디렉토리가 git 저장소인지 확인하세요');
32
+ }
33
+ }
34
+ /**
35
+ * 특정 커밋에서 변경된 파일 경로 목록을 반환한다
36
+ *
37
+ * `git diff-tree --no-commit-id --name-only -r <SHA>` 실행.
38
+ * 프로젝트 루트 기준 상대 경로 배열로 반환.
39
+ * merge commit의 경우 첫 번째 parent 기준 diff 사용 (`-c` 옵션 미사용).
40
+ *
41
+ * @param projectRoot - git 저장소 루트 경로
42
+ * @param sha - 커밋 SHA (축약 또는 전체)
43
+ * @returns 변경 파일 경로 배열 또는 에러
44
+ */
45
+ export function getChangedFiles(projectRoot, sha) {
46
+ try {
47
+ const output = execSync(`git diff-tree --no-commit-id --name-only -r ${sha}`, {
48
+ cwd: projectRoot,
49
+ encoding: 'utf-8',
50
+ timeout: 5000,
51
+ stdio: ['pipe', 'pipe', 'pipe'],
52
+ });
53
+ const files = output
54
+ .replace(/\r\n/g, '\n')
55
+ .trim()
56
+ .split('\n')
57
+ .map((s) => s.trim())
58
+ .filter(Boolean);
59
+ return ok(files);
60
+ }
61
+ catch {
62
+ return fail('GIT_DIFF_TREE_FAILED', `변경 파일 목록을 가져올 수 없습니다: ${sha}`, 'SHA가 유효하지 않거나 git 명령이 실패했습니다');
63
+ }
64
+ }
65
+ /**
66
+ * 최근 N개 커밋의 short SHA(8자) 배열을 반환한다
67
+ *
68
+ * 커밋 수가 요청보다 적으면 있는 만큼만 반환 (Graceful Degradation)
69
+ *
70
+ * @param projectRoot - git 저장소 루트 경로
71
+ * @param count - 가져올 커밋 수
72
+ * @returns short SHA 배열과 실제 개수 정보 또는 에러
73
+ */
74
+ export function getRecentShas(projectRoot, count) {
75
+ try {
76
+ const output = execSync(`git log --format=%h --abbrev=8 -n ${String(count)}`, {
77
+ cwd: projectRoot,
78
+ encoding: 'utf-8',
79
+ timeout: 5000,
80
+ stdio: ['pipe', 'pipe', 'pipe'],
81
+ })
82
+ .replace(/\r\n/g, '\n')
83
+ .trim();
84
+ const shas = output
85
+ ? output
86
+ .split('\n')
87
+ .map((s) => s.trim())
88
+ .filter(Boolean)
89
+ : [];
90
+ if (shas.length === 0) {
91
+ return fail('GIT_NO_COMMITS', '커밋이 없는 저장소입니다', '최소 1개의 커밋이 필요합니다');
92
+ }
93
+ return ok({ shas, requested: count, actual: shas.length });
94
+ }
95
+ catch {
96
+ return fail('GIT_NOT_AVAILABLE', 'git 명령을 실행할 수 없습니다', '현재 디렉토리가 git 저장소인지 확인하세요');
97
+ }
98
+ }
99
+ //# sourceMappingURL=git-helpers.js.map
@@ -0,0 +1,30 @@
1
+ /**
2
+ * init 공용 유틸리티 — Layer 3 (Adapter)
3
+ *
4
+ * init 커맨드의 모드(대화형/stdin/from-existing)에 관계없이
5
+ * 공통으로 사용되는 유틸리티 함수들을 모아 놓는다.
6
+ *
7
+ * - stdin 추출, 스캔 결과 변환, config 빌드/검증/쓰기
8
+ * - 스캔 결과 콘솔 출력, 기존 config 읽기, 정규식 검증
9
+ *
10
+ * @module
11
+ */
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';
14
+ /**
15
+ * 수집된 정보로 설정 객체를 구성하고, 검증 후 파일에 쓴다
16
+ *
17
+ * @param mergeWith - 기존 설정 객체. 제공되면 스캔 결과와 병합한다.
18
+ */
19
+ export declare function buildAndWriteConfig(answers: InitAnswers, configPath: string, alreadyExists: boolean, scanResult?: ScanResult, mergeWith?: Record<string, unknown>): ToolResult<InitResult>;
20
+ /**
21
+ * 기존 설정 파일을 읽어 JSON 객체로 파싱한다
22
+ *
23
+ * 파싱 실패 시 null을 반환하여 병합 대신 새 config 생성으로 fallback.
24
+ */
25
+ export declare function readExistingConfig(configPath: string): Record<string, unknown> | null;
26
+ /**
27
+ * 스캔 결과를 콘솔에 표시한다
28
+ */
29
+ export declare function printScanResults(scan: ScanResult): void;
30
+ //# sourceMappingURL=init-helpers.d.ts.map
@@ -0,0 +1,112 @@
1
+ /**
2
+ * init 공용 유틸리티 — Layer 3 (Adapter)
3
+ *
4
+ * init 커맨드의 모드(대화형/stdin/from-existing)에 관계없이
5
+ * 공통으로 사용되는 유틸리티 함수들을 모아 놓는다.
6
+ *
7
+ * - stdin 추출, 스캔 결과 변환, config 빌드/검증/쓰기
8
+ * - 스캔 결과 콘솔 출력, 기존 config 읽기, 정규식 검증
9
+ *
10
+ * @module
11
+ */
12
+ import { readFileSync, writeFileSync } from 'node:fs';
13
+ import { ok, fail } from '../../../types/index.js';
14
+ import { projectConfigSchema } from '../../../config/schema.js';
15
+ import { CYAN, YELLOW, GREEN, DIM, RESET, BOLD } from '../output.js';
16
+ import { mergeConfigs } from '../../../core/resolvers/config-merger.js';
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';
19
+ /**
20
+ * 수집된 정보로 설정 객체를 구성하고, 검증 후 파일에 쓴다
21
+ *
22
+ * @param mergeWith - 기존 설정 객체. 제공되면 스캔 결과와 병합한다.
23
+ */
24
+ export function buildAndWriteConfig(answers, configPath, alreadyExists, scanResult, mergeWith) {
25
+ let configObj = buildConfigObject(answers);
26
+ if (mergeWith) {
27
+ configObj = mergeConfigs(mergeWith, configObj);
28
+ }
29
+ const result = projectConfigSchema.safeParse(configObj);
30
+ if (!result.success) {
31
+ const details = result.error.issues
32
+ .map((issue) => ` - ${issue.path.map(String).join('.') || '(root)'}: ${issue.message}`)
33
+ .join('\n');
34
+ return fail('INIT_VALIDATION_FAILED', '생성된 설정이 스키마 검증에 실패했습니다', details);
35
+ }
36
+ const jsonContent = JSON.stringify(configObj, null, 2) + '\n';
37
+ try {
38
+ writeFileSync(configPath, jsonContent, 'utf-8');
39
+ }
40
+ catch (err) {
41
+ const detail = err instanceof Error ? err.message : String(err);
42
+ return fail('INIT_WRITE_FAILED', '설정 파일 쓰기에 실패했습니다', `경로: ${configPath}, 원인: ${detail}`);
43
+ }
44
+ return ok({
45
+ configPath,
46
+ overwritten: alreadyExists,
47
+ merged: mergeWith !== undefined,
48
+ ...(scanResult && { scanResult }),
49
+ });
50
+ }
51
+ /**
52
+ * 기존 설정 파일을 읽어 JSON 객체로 파싱한다
53
+ *
54
+ * 파싱 실패 시 null을 반환하여 병합 대신 새 config 생성으로 fallback.
55
+ */
56
+ export function readExistingConfig(configPath) {
57
+ try {
58
+ const raw = readFileSync(configPath, 'utf-8');
59
+ const content = raw.startsWith('\uFEFF') ? raw.slice(1) : raw;
60
+ const parsed = JSON.parse(content);
61
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
62
+ return parsed;
63
+ }
64
+ return null;
65
+ }
66
+ catch {
67
+ console.log(`${YELLOW}⚠️ 기존 설정 파일을 파싱할 수 없습니다. 새 설정을 생성합니다.${RESET}`);
68
+ return null;
69
+ }
70
+ }
71
+ /**
72
+ * 스캔 결과를 콘솔에 표시한다
73
+ */
74
+ export function printScanResults(scan) {
75
+ console.log(`${DIM}🔍 스캔 완료: ${String(scan.totalMdFiles)}개 .md 파일 분석${RESET}`);
76
+ console.log('');
77
+ if (scan.idPatterns.length > 0) {
78
+ console.log(`${BOLD}📊 감지된 ID 패턴:${RESET}`);
79
+ for (const p of scan.idPatterns) {
80
+ const examples = p.examples.slice(0, 3).join(', ');
81
+ console.log(` → ${CYAN}${p.displayLabel}${RESET} 패턴 (${GREEN}${String(p.matchCount)}개${RESET} 파일) 예: ${DIM}${examples}${RESET}`);
82
+ }
83
+ if (scan.suggestedIdPattern) {
84
+ console.log(` ${DIM}→ 통합 패턴: ${scan.suggestedIdPattern}${RESET}`);
85
+ }
86
+ console.log('');
87
+ }
88
+ if (scan.docRoots.length > 0) {
89
+ console.log(`${BOLD}📂 감지된 문서 디렉토리:${RESET}`);
90
+ for (const d of scan.docRoots) {
91
+ const roleLabel = d.role !== 'other' ? `[${d.role}] ` : '';
92
+ console.log(` → ${CYAN}${roleLabel}${d.dirPath}${RESET} (${String(d.fileCount)}개 파일)`);
93
+ }
94
+ console.log('');
95
+ }
96
+ if (scan.commandFiles.length > 0) {
97
+ console.log(`${BOLD}📄 커맨드 파일 후보:${RESET}`);
98
+ for (const c of scan.commandFiles) {
99
+ const sepInfo = c.hasSeparator ? ', 구분선 있음' : '';
100
+ console.log(` → ${CYAN}${c.filePath}${RESET} (${String(c.sections.length)}개 섹션${sepInfo})`);
101
+ }
102
+ console.log('');
103
+ }
104
+ if (scan.roadmapCandidates.length > 0) {
105
+ console.log(`${BOLD}📋 로드맵 후보:${RESET}`);
106
+ for (const r of scan.roadmapCandidates) {
107
+ console.log(` → ${CYAN}${r}${RESET}`);
108
+ }
109
+ console.log('');
110
+ }
111
+ }
112
+ //# sourceMappingURL=init-helpers.js.map
@@ -0,0 +1,25 @@
1
+ /**
2
+ * init 대화형 모드 — Layer 3 (Adapter)
3
+ *
4
+ * readline 기반 대화형 질문 수집 로직을 담당한다.
5
+ * TTY 환경에서만 동작하며, stdin/pipe 모드와 완전히 분리된 관심사이다.
6
+ *
7
+ * - handleInteractive: 기본 대화형 초기화
8
+ * - handleFromExistingInteractive: 스캔 결과 확인 후 대화형 초기화
9
+ * - collectAnswers: 질문 세트 순서 실행
10
+ * - askWithDefault / askNullable: readline 헬퍼
11
+ *
12
+ * @module
13
+ */
14
+ import type { ToolResult, ScanResult, InitResult } from '../../../types/index.js';
15
+ /**
16
+ * 대화형 모드 — readline으로 질문을 수집하고 설정 파일을 생성한다
17
+ */
18
+ export declare function handleInteractive(configPath: string, alreadyExists: boolean): Promise<ToolResult<InitResult>>;
19
+ /**
20
+ * --from-existing 대화형 모드 — 스캔 결과를 보여주고 사용자 확인 후 설정 생성
21
+ *
22
+ * 기존 config가 파싱 가능하면 병합/덮어쓰기/취소 3지선다 제시
23
+ */
24
+ export declare function handleFromExistingInteractive(scanResult: ScanResult, configPath: string, alreadyExists: boolean, existingConfig: Record<string, unknown> | null): Promise<ToolResult<InitResult>>;
25
+ //# sourceMappingURL=init-interactive.d.ts.map
@@ -0,0 +1,143 @@
1
+ /**
2
+ * init 대화형 모드 — Layer 3 (Adapter)
3
+ *
4
+ * readline 기반 대화형 질문 수집 로직을 담당한다.
5
+ * TTY 환경에서만 동작하며, stdin/pipe 모드와 완전히 분리된 관심사이다.
6
+ *
7
+ * - handleInteractive: 기본 대화형 초기화
8
+ * - handleFromExistingInteractive: 스캔 결과 확인 후 대화형 초기화
9
+ * - collectAnswers: 질문 세트 순서 실행
10
+ * - askWithDefault / askNullable: readline 헬퍼
11
+ *
12
+ * @module
13
+ */
14
+ import { createInterface } from 'node:readline/promises';
15
+ import { stdin, stdout } from 'node:process';
16
+ import { fail } from '../../../types/index.js';
17
+ import { CONFIG_FILENAME } from '../../../config/schema.js';
18
+ import { isValidRegex } from '../../../core/resolvers/config-validator.js';
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';
21
+ /**
22
+ * 대화형 모드 — readline으로 질문을 수집하고 설정 파일을 생성한다
23
+ */
24
+ export async function handleInteractive(configPath, alreadyExists) {
25
+ const rl = createInterface({ input: stdin, output: stdout });
26
+ try {
27
+ console.log('');
28
+ console.log(`${BOLD}📝 vibe-commander 프로젝트 초기화${RESET}`);
29
+ console.log(`${DIM} 각 질문에 Enter를 누르면 [기본값]이 적용됩니다.${RESET}`);
30
+ console.log('');
31
+ if (alreadyExists) {
32
+ const confirm = await rl.question(`${YELLOW}⚠️ 이미 ${CONFIG_FILENAME}이 존재합니다. 덮어쓰시겠습니까? (y/N): ${RESET}`);
33
+ if (confirm.trim().toLowerCase() !== 'y') {
34
+ rl.close();
35
+ return fail('INIT_CANCELLED', '초기화가 취소되었습니다');
36
+ }
37
+ console.log('');
38
+ }
39
+ const answers = await collectAnswers(rl);
40
+ rl.close();
41
+ return buildAndWriteConfig(answers, configPath, alreadyExists);
42
+ }
43
+ catch {
44
+ rl.close();
45
+ return fail('INIT_CANCELLED', '초기화가 취소되었습니다 (사용자 중단)');
46
+ }
47
+ }
48
+ /**
49
+ * --from-existing 대화형 모드 — 스캔 결과를 보여주고 사용자 확인 후 설정 생성
50
+ *
51
+ * 기존 config가 파싱 가능하면 병합/덮어쓰기/취소 3지선다 제시
52
+ */
53
+ export async function handleFromExistingInteractive(scanResult, configPath, alreadyExists, existingConfig) {
54
+ const rl = createInterface({ input: stdin, output: stdout });
55
+ try {
56
+ console.log('');
57
+ console.log(`${BOLD}📝 vibe-commander 프로젝트 스캔${RESET}`);
58
+ console.log('');
59
+ printScanResults(scanResult);
60
+ if (alreadyExists && existingConfig) {
61
+ console.log(`${YELLOW}⚠️ 기존 ${CONFIG_FILENAME}이 존재합니다.${RESET}`);
62
+ console.log(` ${CYAN}1${RESET}) 병합 (권장) — 기존 커스텀 설정 보존 + 스캔 결과 반영`);
63
+ console.log(` ${CYAN}2${RESET}) 덮어쓰기 — 기존 설정을 완전히 대체`);
64
+ console.log(` ${CYAN}3${RESET}) 취소`);
65
+ console.log('');
66
+ const choice = await rl.question(`${CYAN}? 선택 (1/2/3) [1]: ${RESET}`);
67
+ const trimmed = choice.trim();
68
+ if (trimmed === '3') {
69
+ rl.close();
70
+ return fail('INIT_CANCELLED', '초기화가 취소되었습니다');
71
+ }
72
+ const answers = buildAnswersFromScan(scanResult);
73
+ rl.close();
74
+ if (trimmed === '2') {
75
+ return buildAndWriteConfig(answers, configPath, alreadyExists, scanResult);
76
+ }
77
+ return buildAndWriteConfig(answers, configPath, alreadyExists, scanResult, existingConfig);
78
+ }
79
+ if (alreadyExists) {
80
+ const overwrite = await rl.question(`${YELLOW}⚠️ 이미 ${CONFIG_FILENAME}이 존재합니다. 덮어쓰시겠습니까? (y/N): ${RESET}`);
81
+ if (overwrite.trim().toLowerCase() !== 'y') {
82
+ rl.close();
83
+ return fail('INIT_CANCELLED', '초기화가 취소되었습니다');
84
+ }
85
+ console.log('');
86
+ }
87
+ const confirm = await rl.question(`${CYAN}? 감지된 패턴으로 설정을 생성할까요? (Y/n): ${RESET}`);
88
+ rl.close();
89
+ if (confirm.trim().toLowerCase() === 'n') {
90
+ return fail('INIT_CANCELLED', '초기화가 취소되었습니다');
91
+ }
92
+ const answers = buildAnswersFromScan(scanResult);
93
+ return buildAndWriteConfig(answers, configPath, alreadyExists, scanResult);
94
+ }
95
+ catch {
96
+ rl.close();
97
+ return fail('INIT_CANCELLED', '초기화가 취소되었습니다 (사용자 중단)');
98
+ }
99
+ }
100
+ /**
101
+ * 대화형 질문 세트를 순서대로 실행한다
102
+ */
103
+ async function collectAnswers(rl) {
104
+ const commandsPath = await askWithDefault(rl, '커맨드 파일 경로', DEFAULT_COMMANDS_PATH);
105
+ const roadmapPath = await askNullable(rl, '로드맵 파일 경로 (없으면 Enter)', DEFAULT_ROADMAP_PATH);
106
+ const planDir = await askWithDefault(rl, '계획서 디렉토리', DEFAULT_PLAN_DIR);
107
+ const resultDir = await askWithDefault(rl, '결과 보고서 디렉토리', DEFAULT_RESULT_DIR);
108
+ const runbookDir = await askNullable(rl, '런북 디렉토리 (없으면 Enter)', '');
109
+ const idPattern = await askWithDefault(rl, '유닛 ID 패턴 (정규식)', DEFAULT_ID_PATTERN);
110
+ const validPattern = isValidRegex(idPattern);
111
+ if (!validPattern) {
112
+ console.log(`${YELLOW}⚠️ 유효하지 않은 정규식입니다. 기본값을 사용합니다.${RESET}`);
113
+ }
114
+ const commandSection = await askWithDefault(rl, '커맨드 파일 섹션 헤더', DEFAULT_COMMAND_SECTION);
115
+ return {
116
+ commandsPath,
117
+ roadmapPath,
118
+ planDir,
119
+ resultDir,
120
+ runbookDir,
121
+ idPattern: validPattern ? idPattern : DEFAULT_ID_PATTERN,
122
+ commandSection,
123
+ };
124
+ }
125
+ /**
126
+ * 기본값이 있는 질문 프롬프트
127
+ */
128
+ async function askWithDefault(rl, prompt, defaultValue) {
129
+ const answer = await rl.question(`${CYAN}? ${prompt}${RESET} ${DIM}[${defaultValue}]${RESET}: `);
130
+ return answer.trim() || defaultValue;
131
+ }
132
+ /**
133
+ * null 가능한 질문 프롬프트 (빈 입력 → null)
134
+ */
135
+ async function askNullable(rl, prompt, defaultValue) {
136
+ const display = defaultValue ? ` ${DIM}[${defaultValue}]${RESET}` : '';
137
+ const answer = await rl.question(`${CYAN}? ${prompt}${RESET}${display}: `);
138
+ const trimmed = answer.trim();
139
+ if (trimmed === '')
140
+ return defaultValue || null;
141
+ return trimmed;
142
+ }
143
+ //# sourceMappingURL=init-interactive.js.map