@kybird/llm-wiki 0.2.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/lib/init.js ADDED
@@ -0,0 +1,236 @@
1
+ // llm-wiki init — 타겟 리포(cwd)에 wiki 시스템 골격을 복사.
2
+ // 복사 대상: doc/ 골격 + skills/ 4개 + githooks/ + scripts/(동기화).
3
+ // 스크립트는 복사하지 않는다 — 스킬이 글로벌 `llm-wiki` CLI를 호출하므로
4
+ // 타겟 리포에 스크립트를 둘 필요가 없다 (npm update 한 번으로 모든 리포 갱신).
5
+ const fs = require('fs');
6
+ const os = require('os');
7
+ const path = require('path');
8
+ const { defaultCollectionNames, loadConfig } = require('./find-doc-root');
9
+
10
+ const PKG_ROOT = path.resolve(__dirname, '..');
11
+
12
+ // 스킬 설치 대상 — 에이전트 CLI별로 읽는 위치가 다르다.
13
+ // .agents/skills/ — 표준 정본 (ZCode, Cursor 등)
14
+ // .claude/skills/ — Claude Code 복사본
15
+ const SKILL_TARGETS = ['.agents/skills', '.claude/skills'];
16
+
17
+ // qmd 전역 설정 디렉토리 경로 — @tobilu/qmd의 우선순위(QMD_CONFIG_DIR > XDG_CONFIG_HOME >
18
+ // ~/.config/qmd)와 동일하게 맞춘다(collections.js 참고). 여기 없으면 qmd 미설치/미사용 상태라
19
+ // 충돌 자체가 불가능하므로 검사를 건너뛴다.
20
+ function qmdConfigDir() {
21
+ if (process.env.QMD_CONFIG_DIR) return process.env.QMD_CONFIG_DIR;
22
+ if (process.env.XDG_CONFIG_HOME) return path.join(process.env.XDG_CONFIG_HOME, 'qmd');
23
+ return path.join(os.homedir(), '.config', 'qmd');
24
+ }
25
+
26
+ // 전역 QMD 컬렉션 이름 충돌 검사 — 다른 프로젝트가 이미 같은 이름으로 등록돼 있으면
27
+ // (2026-07-25 실제로 발생: 두 프로젝트가 기본값을 그대로 써서 서로의 로그가 뒤섞여 검색됨,
28
+ // 에러 없이 조용히 실패하는 유형이라 알아채기 어려움) init 시점에 경고한다.
29
+ function warnIfCollectionNameCollides(names, cwd) {
30
+ const indexPath = path.join(qmdConfigDir(), 'index.yml');
31
+ if (!exists(indexPath)) return; // qmd 미사용 — 충돌 불가능
32
+
33
+ let text;
34
+ try {
35
+ text = fs.readFileSync(indexPath, 'utf8');
36
+ } catch {
37
+ return;
38
+ }
39
+
40
+ for (const name of [names.wiki, names.raw]) {
41
+ const re = new RegExp(`^\\s{2}${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}:\\s*$`, 'm');
42
+ if (re.test(text)) {
43
+ console.log(
44
+ `⚠️ QMD collection "${name}" already exists in ${indexPath} (possibly from another project).\n` +
45
+ ` Run 'llm-wiki compile index' and check the printed collection paths — if it points to a\n` +
46
+ ` different repo, set a unique name in llm-wiki.config.json: { "collections": { "wiki": "...", "raw": "..." } }`
47
+ );
48
+ }
49
+ }
50
+ }
51
+
52
+ function copyRecursive(src, dest) {
53
+ const stat = fs.statSync(src);
54
+ if (stat.isDirectory()) {
55
+ fs.mkdirSync(dest, { recursive: true });
56
+ for (const entry of fs.readdirSync(src)) {
57
+ copyRecursive(path.join(src, entry), path.join(dest, entry));
58
+ }
59
+ } else {
60
+ fs.copyFileSync(src, dest);
61
+ }
62
+ }
63
+
64
+ // ── 마커 있는 사본 동기화 (plan.md 6.1 + 개선계획 1-7, 2026-08-29 훅/스크립트로 확장) ──
65
+ // 패키지 정본(skills/, templates/)의 각 파일은 버전 마커를 갖는다.
66
+ // 복사본과 정본이 다를 때:
67
+ // - 복사본에 마커가 살아있으면 → 미수정 판정. 정본이 이겨서 덮어쓴다(갱신 가능).
68
+ // - 복사본에서 마커가 사라졌으면 → 사용자가 손댄 것. 건너뛴다.
69
+ // 스킬은 frontmatter의 skill-version:, 훅·스크립트는 본문의 llm-wiki-template-version:.
70
+ // apply=false면 아무것도 쓰지 않고 판정만 돌려준다 (init --check).
71
+ function syncMarkedFiles(srcDir, targets, markerRe, { apply = false } = {}) {
72
+ const actions = [];
73
+ for (const rel of listFilesRecursive(srcDir)) {
74
+ const srcPath = path.join(srcDir, rel);
75
+ const srcContent = fs.readFileSync(srcPath, 'utf8');
76
+ for (const target of targets) {
77
+ const destPath = path.join(target, rel);
78
+ const action = { rel, target, status: 'ok' };
79
+ if (!fs.existsSync(destPath)) {
80
+ action.status = 'missing';
81
+ if (apply) {
82
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
83
+ fs.copyFileSync(srcPath, destPath);
84
+ action.status = 'copied';
85
+ }
86
+ } else {
87
+ const destContent = fs.readFileSync(destPath, 'utf8');
88
+ if (destContent !== srcContent) {
89
+ if (!markerRe.test(destContent)) {
90
+ action.status = 'user-modified';
91
+ } else {
92
+ action.status = apply ? 'updated' : 'stale';
93
+ if (apply) fs.writeFileSync(destPath, srcContent);
94
+ }
95
+ }
96
+ }
97
+ actions.push(action);
98
+ }
99
+ }
100
+ return actions;
101
+ }
102
+
103
+ const SKILL_VERSION_RE = /^skill-version:\s*\d+\s*$/m;
104
+ // 훅(bash #주석)과 .bat(REM 주석)을 함께 커버하는 부분문자열 마커.
105
+ const TEMPLATE_VERSION_RE = /llm-wiki-template-version:\s*\d+/;
106
+
107
+ function syncSkills(skillsSrc, targets, options = {}) {
108
+ return syncMarkedFiles(skillsSrc, targets, SKILL_VERSION_RE, options);
109
+ }
110
+
111
+ function listFilesRecursive(dir, base = dir) {
112
+ const files = [];
113
+ for (const entry of fs.readdirSync(dir)) {
114
+ const full = path.join(dir, entry);
115
+ if (fs.statSync(full).isDirectory()) {
116
+ files.push(...listFilesRecursive(full, base));
117
+ } else {
118
+ files.push(path.relative(base, full));
119
+ }
120
+ }
121
+ return files;
122
+ }
123
+
124
+ function printSkillSyncReport(actions, cwd) {
125
+ const label = { copied: 'installed', updated: 'updated', stale: 'stale', 'user-modified': 'user-modified', ok: 'ok', missing: 'missing' };
126
+ for (const a of actions) {
127
+ console.log(` [${label[a.status] || a.status}] ${path.relative(cwd, path.join(a.target, a.rel))}`);
128
+ }
129
+ }
130
+
131
+ function exists(p) {
132
+ try {
133
+ fs.accessSync(p);
134
+ return true;
135
+ } catch {
136
+ return false;
137
+ }
138
+ }
139
+
140
+ function init(options = {}) {
141
+ const cwd = process.cwd();
142
+
143
+ // --check: 아무것도 쓰지 않고 레포 복사본 vs 패키지 정본의 신선함만 보고 (개선계획 1-7).
144
+ if (options.check) {
145
+ console.log(`Checking llm-wiki skills in: ${cwd}\n`);
146
+ const cfg = loadConfig(path.join(cwd, 'doc'));
147
+ const actions = syncSkills(path.join(PKG_ROOT, 'skills'), SKILL_TARGETS.map(t => path.join(cwd, t)), { apply: false });
148
+ // 스킬뿐 아니라 훅·스크립트 사본도 같은 마커 규칙으로 점검한다.
149
+ actions.push(...syncMarkedFiles(path.join(PKG_ROOT, 'templates', 'scripts'), [path.join(cwd, 'scripts')], TEMPLATE_VERSION_RE, { apply: false }));
150
+ if (cfg.hooksPath) {
151
+ console.log(` [declared] hooks → ${cfg.hooksPath} (레포가 정본 경로를 직접 쓴다 — 사본 점검 생략)\n`);
152
+ } else {
153
+ actions.push(...syncMarkedFiles(path.join(PKG_ROOT, 'templates', 'githooks'), [path.join(cwd, 'githooks')], TEMPLATE_VERSION_RE, { apply: false }));
154
+ }
155
+ printSkillSyncReport(actions, cwd);
156
+ console.log('\nLegend:');
157
+ console.log(' stale — 패키지 정본이 더 새롭다. `llm-wiki init`으로 갱신.');
158
+ console.log(' user-modified — 버전 마커가 없어 건너뛴 복사본 (사용자 수정 존중, plan.md 6.1).');
159
+ console.log(' missing — 미설치. `llm-wiki init`으로 설치.');
160
+ return;
161
+ }
162
+
163
+ console.log(`Initializing llm-wiki in: ${cwd}\n`);
164
+
165
+ // 1. doc/ 골격
166
+ const docDir = path.join(cwd, 'doc');
167
+ const templatesDoc = path.join(PKG_ROOT, 'templates', 'doc');
168
+ if (exists(path.join(docDir, 'wiki', 'index.md'))) {
169
+ console.log('✓ doc/ already exists, skipping scaffold.');
170
+ } else {
171
+ copyRecursive(templatesDoc, docDir);
172
+ console.log('✓ Created doc/ (raw/, wiki/index.md)');
173
+ }
174
+
175
+ // 1b. doc/kanban/ 골격 — 칸반 코어 (개선계획 2단계). 이미 있으면 아무것도 덮지 않는다.
176
+ const kanbanDir = path.join(docDir, 'kanban');
177
+ if (!exists(kanbanDir)) {
178
+ require('./kanban').scaffold(docDir);
179
+ console.log('✓ Created doc/kanban/ (board.yml, cards/, done/, superseded/, abandoned/, activity.jsonl)');
180
+ }
181
+
182
+ // 2. skills/ (정본 → 복사본). 마커 규칙: skill-version이 살아있는 복사본만 갱신하고,
183
+ // 마커를 지운 파일(사용자 수정)은 건너뛴다 — syncSkills 주석 참고.
184
+ const skillsSrc = path.join(PKG_ROOT, 'skills');
185
+ const actions = syncSkills(skillsSrc, SKILL_TARGETS.map(t => path.join(cwd, t)), { apply: true });
186
+ for (const target of SKILL_TARGETS) {
187
+ console.log(`✓ Synced skills/ → ${target}/`);
188
+ }
189
+ printSkillSyncReport(actions, cwd);
190
+
191
+ // 3. scripts/ 동기화 스크립트 — 스킬과 같은 마커 규칙으로 동기화(2026-08-29 확장).
192
+ // 마커를 지운 파일(사용자 수정)은 건너뛴다 — init이 훅 보조 스크립트를 덮어쓰지 않는다.
193
+ const scriptsSrc = path.join(PKG_ROOT, 'templates', 'scripts');
194
+ const scriptsDest = path.join(cwd, 'scripts');
195
+ fs.mkdirSync(scriptsDest, { recursive: true });
196
+ const scriptsActions = syncMarkedFiles(scriptsSrc, [scriptsDest], TEMPLATE_VERSION_RE, { apply: true });
197
+ console.log('✓ Synced templates/scripts → scripts/');
198
+ printSkillSyncReport(scriptsActions, cwd);
199
+
200
+ // 4. githooks — 기본은 마커 규칙 사본 동기화. 레포가 config로 hooksPath를 선언했으면
201
+ // (이 레포처럼 templates/를 정본으로 직접 쓰는 경우) 사본을 만들지 않는다.
202
+ const cfg = loadConfig(docDir);
203
+ if (cfg.hooksPath) {
204
+ console.log(`✓ hooks: 레포 선언 경로 ${cfg.hooksPath} 를 그대로 쓴다 (githooks/ 사본 없음).`);
205
+ } else {
206
+ const hooksSrc = path.join(PKG_ROOT, 'templates', 'githooks');
207
+ const hooksDest = path.join(cwd, 'githooks');
208
+ fs.mkdirSync(hooksDest, { recursive: true });
209
+ const hooksActions = syncMarkedFiles(hooksSrc, [hooksDest], TEMPLATE_VERSION_RE, { apply: true });
210
+ console.log('✓ Synced templates/githooks → githooks/');
211
+ printSkillSyncReport(hooksActions, cwd);
212
+ }
213
+
214
+ // 5. QMD 컬렉션 이름 충돌 검사 (리포 폴더명 기반 기본값이라도, 여러 리포명이 sanitize 후
215
+ // 같은 이름으로 축약되거나 llm-wiki.config.json으로 수동 지정한 이름이 겹칠 수 있음).
216
+ const defaultNames = defaultCollectionNames(docDir);
217
+ warnIfCollectionNameCollides(defaultNames, cwd);
218
+
219
+ // 6. git hooks 활성화 안내 (자동 실행 아님 — 사용자 의도 확인)
220
+ console.log('\n--- Next steps ---');
221
+ console.log('1. Enable git hooks (run once):');
222
+ console.log(` git config core.hooksPath ${cfg.hooksPath || 'githooks'}`);
223
+ console.log('');
224
+ console.log('2. (Optional) Enable semantic search:');
225
+ console.log(' npm i @tobilu/qmd');
226
+ console.log(' # without qmd, llm-wiki search falls back to grep');
227
+ console.log(` # QMD collection names default to this repo's folder name: "${defaultNames.wiki}" / "${defaultNames.raw}"`);
228
+ console.log('');
229
+ console.log('3. Set project name / collection names / hooks path (optional) — llm-wiki.config.json:');
230
+ console.log(' { "projectName": "my-project", "collections": { "wiki": "my-project-wiki", "raw": "my-project-wiki-raw" },');
231
+ console.log(' "hooksPath": "templates/githooks" } # githooks/ 사본 대신 정본 경로를 직접 사용');
232
+ console.log('');
233
+ console.log('Done. AI agents will now use wiki-search/log/compile/lint + work-loop/board.');
234
+ }
235
+
236
+ module.exports = { init };