@01b/team-kb 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/dist/index.d.ts +2 -0
- package/dist/index.js +747 -0
- package/dist/index.js.map +1 -0
- package/package.json +37 -0
- package/templates/kb-structure/gitignore +6 -0
- package/templates/kb-structure/kb-coverage.yml +50 -0
- package/templates/kb-structure/kb-health.yml +71 -0
- package/templates/kb-structure/kb-rules.md +48 -0
- package/templates/kb-structure/note-templates/business-rule.md +15 -0
- package/templates/kb-structure/note-templates/code-analysis.md +16 -0
- package/templates/kb-structure/note-templates/decision-record.md +14 -0
- package/templates/kb-structure/note-templates/troubleshoot.md +14 -0
- package/templates/kb-structure/pre-commit +31 -0
- package/templates/kb-structure/rebuild-index.sh +22 -0
- package/templates/kb-structure/team-focus.md +11 -0
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/init.ts
|
|
7
|
+
import { input } from "@inquirer/prompts";
|
|
8
|
+
import fs3 from "fs-extra";
|
|
9
|
+
import path2 from "path";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
|
|
12
|
+
// src/utils/log.ts
|
|
13
|
+
import pc from "picocolors";
|
|
14
|
+
var log = {
|
|
15
|
+
info: (msg) => console.log(pc.blue("\u2139") + " " + msg),
|
|
16
|
+
success: (msg) => console.log(pc.green("\u2714") + " " + msg),
|
|
17
|
+
warn: (msg) => console.log(pc.yellow("\u26A0") + " " + msg),
|
|
18
|
+
error: (msg) => console.error(pc.red("\u2716") + " " + msg),
|
|
19
|
+
step: (msg) => console.log(pc.gray(" \u2192") + " " + msg)
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// src/utils/template.ts
|
|
23
|
+
function renderTemplate(template, vars) {
|
|
24
|
+
let result = template;
|
|
25
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
26
|
+
result = result.replaceAll(`{{${key}}}`, value);
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/utils/config.ts
|
|
32
|
+
import fs from "fs-extra";
|
|
33
|
+
import path from "path";
|
|
34
|
+
var CONFIG_DIR = path.join(
|
|
35
|
+
process.env.HOME || "~",
|
|
36
|
+
".config",
|
|
37
|
+
"kb-cli"
|
|
38
|
+
);
|
|
39
|
+
var CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
40
|
+
function getKbPath() {
|
|
41
|
+
try {
|
|
42
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
43
|
+
const config = fs.readJsonSync(CONFIG_PATH);
|
|
44
|
+
return config.kbPath;
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
}
|
|
48
|
+
return void 0;
|
|
49
|
+
}
|
|
50
|
+
async function saveKbPath(kbPath) {
|
|
51
|
+
await fs.ensureDir(CONFIG_DIR);
|
|
52
|
+
await fs.writeJson(CONFIG_PATH, { kbPath }, { spaces: 2 });
|
|
53
|
+
log.step(`\uC124\uC815 \uC800\uC7A5: ${CONFIG_PATH}`);
|
|
54
|
+
}
|
|
55
|
+
async function removeConfig() {
|
|
56
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
57
|
+
await fs.remove(CONFIG_PATH);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/utils/git.ts
|
|
64
|
+
import simpleGit from "simple-git";
|
|
65
|
+
import fs2 from "fs-extra";
|
|
66
|
+
function getGit(cwd) {
|
|
67
|
+
return simpleGit(cwd);
|
|
68
|
+
}
|
|
69
|
+
async function initRepo(dir, remote) {
|
|
70
|
+
const git = getGit(dir);
|
|
71
|
+
await git.init();
|
|
72
|
+
if (remote) {
|
|
73
|
+
await git.addRemote("origin", remote);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function cloneRepo(url, dir) {
|
|
77
|
+
await fs2.ensureDir(dir);
|
|
78
|
+
const git = simpleGit();
|
|
79
|
+
await git.clone(url, dir);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/commands/init.ts
|
|
83
|
+
var __dirname = path2.dirname(fileURLToPath(import.meta.url));
|
|
84
|
+
var TEMPLATES_DIR = path2.join(__dirname, "..", "..", "templates", "kb-structure");
|
|
85
|
+
var KB_DIRS = [
|
|
86
|
+
"global",
|
|
87
|
+
"rules",
|
|
88
|
+
"analysis",
|
|
89
|
+
"decisions",
|
|
90
|
+
"troubleshoot",
|
|
91
|
+
"drafts",
|
|
92
|
+
"areas",
|
|
93
|
+
"templates",
|
|
94
|
+
"ai-context",
|
|
95
|
+
"archive",
|
|
96
|
+
"00-inbox",
|
|
97
|
+
"99-personal",
|
|
98
|
+
".kb/hooks",
|
|
99
|
+
".kb/scripts",
|
|
100
|
+
".kb/setup",
|
|
101
|
+
".github/workflows"
|
|
102
|
+
];
|
|
103
|
+
async function initCommand() {
|
|
104
|
+
log.info("KB \uCD5C\uCD08 \uC0DD\uC131\uC744 \uC2DC\uC791\uD569\uB2C8\uB2E4.");
|
|
105
|
+
const defaultPath = path2.join(process.env.HOME || "~", "team-kb");
|
|
106
|
+
const kbPath = await input({
|
|
107
|
+
message: "KB \uACBD\uB85C",
|
|
108
|
+
default: defaultPath
|
|
109
|
+
});
|
|
110
|
+
const resolvedPath = kbPath.startsWith("~") ? kbPath.replace("~", process.env.HOME || "") : path2.resolve(kbPath);
|
|
111
|
+
if (fs3.existsSync(resolvedPath) && fs3.readdirSync(resolvedPath).length > 0) {
|
|
112
|
+
log.error(`${resolvedPath} \uAC00 \uC774\uBBF8 \uC874\uC7AC\uD558\uACE0 \uBE44\uC5B4\uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const remoteUrl = await input({
|
|
116
|
+
message: "Git remote URL (\uC5C6\uC73C\uBA74 \uC5D4\uD130)",
|
|
117
|
+
default: ""
|
|
118
|
+
});
|
|
119
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
120
|
+
const vars = { date: today };
|
|
121
|
+
log.info("\uD3F4\uB354 \uAD6C\uC870 \uC0DD\uC131 \uC911...");
|
|
122
|
+
for (const dir of KB_DIRS) {
|
|
123
|
+
await fs3.ensureDir(path2.join(resolvedPath, dir));
|
|
124
|
+
}
|
|
125
|
+
log.success("\uD3F4\uB354 \uAD6C\uC870 \uC0DD\uC131 \uC644\uB8CC");
|
|
126
|
+
const gitignoreSrc = path2.join(TEMPLATES_DIR, "gitignore");
|
|
127
|
+
await fs3.copy(gitignoreSrc, path2.join(resolvedPath, ".gitignore"));
|
|
128
|
+
log.step(".gitignore \uC0DD\uC131");
|
|
129
|
+
const noteTemplatesDir = path2.join(TEMPLATES_DIR, "note-templates");
|
|
130
|
+
const noteTemplates = await fs3.readdir(noteTemplatesDir);
|
|
131
|
+
for (const file of noteTemplates) {
|
|
132
|
+
const content = await fs3.readFile(path2.join(noteTemplatesDir, file), "utf-8");
|
|
133
|
+
const rendered = renderTemplate(content, vars);
|
|
134
|
+
await fs3.writeFile(path2.join(resolvedPath, "templates", file), rendered);
|
|
135
|
+
}
|
|
136
|
+
log.step("\uB178\uD2B8 \uD15C\uD50C\uB9BF \uC0DD\uC131");
|
|
137
|
+
const preCommitSrc = path2.join(TEMPLATES_DIR, "pre-commit");
|
|
138
|
+
const preCommitDest = path2.join(resolvedPath, ".kb", "hooks", "pre-commit");
|
|
139
|
+
await fs3.copy(preCommitSrc, preCommitDest);
|
|
140
|
+
await fs3.chmod(preCommitDest, 493);
|
|
141
|
+
log.step("pre-commit hook \uC0DD\uC131");
|
|
142
|
+
const rebuildSrc = path2.join(TEMPLATES_DIR, "rebuild-index.sh");
|
|
143
|
+
const rebuildDest = path2.join(resolvedPath, ".kb", "scripts", "rebuild-index.sh");
|
|
144
|
+
await fs3.copy(rebuildSrc, rebuildDest);
|
|
145
|
+
await fs3.chmod(rebuildDest, 493);
|
|
146
|
+
log.step("rebuild-index.sh \uC0DD\uC131");
|
|
147
|
+
const healthSrc = path2.join(TEMPLATES_DIR, "kb-health.yml");
|
|
148
|
+
const coverageSrc = path2.join(TEMPLATES_DIR, "kb-coverage.yml");
|
|
149
|
+
await fs3.copy(healthSrc, path2.join(resolvedPath, ".github", "workflows", "kb-health.yml"));
|
|
150
|
+
await fs3.copy(coverageSrc, path2.join(resolvedPath, ".github", "workflows", "kb-coverage.yml"));
|
|
151
|
+
log.step("GitHub Actions \uC6CC\uD06C\uD50C\uB85C\uC6B0 \uC0DD\uC131");
|
|
152
|
+
await fs3.writeJson(path2.join(resolvedPath, "kb-index.json"), []);
|
|
153
|
+
log.step("kb-index.json \uCD08\uAE30\uD654 (\uBE48 \uBC30\uC5F4)");
|
|
154
|
+
const kbRulesSrc = path2.join(TEMPLATES_DIR, "kb-rules.md");
|
|
155
|
+
const kbRulesContent = await fs3.readFile(kbRulesSrc, "utf-8");
|
|
156
|
+
await fs3.writeFile(
|
|
157
|
+
path2.join(resolvedPath, "ai-context", "kb-rules.md"),
|
|
158
|
+
renderTemplate(kbRulesContent, vars)
|
|
159
|
+
);
|
|
160
|
+
const teamFocusSrc = path2.join(TEMPLATES_DIR, "team-focus.md");
|
|
161
|
+
const teamFocusContent = await fs3.readFile(teamFocusSrc, "utf-8");
|
|
162
|
+
await fs3.writeFile(
|
|
163
|
+
path2.join(resolvedPath, "ai-context", "team-focus.md"),
|
|
164
|
+
renderTemplate(teamFocusContent, vars)
|
|
165
|
+
);
|
|
166
|
+
log.step("ai-context/ \uD30C\uC77C \uC0DD\uC131 (kb-rules.md, team-focus.md)");
|
|
167
|
+
await initRepo(resolvedPath, remoteUrl || void 0);
|
|
168
|
+
log.step("git init" + (remoteUrl ? ` + remote: ${remoteUrl}` : ""));
|
|
169
|
+
const gitHookDest = path2.join(resolvedPath, ".git", "hooks", "pre-commit");
|
|
170
|
+
await fs3.copy(preCommitDest, gitHookDest);
|
|
171
|
+
await fs3.chmod(gitHookDest, 493);
|
|
172
|
+
log.step("pre-commit hook \uC124\uCE58 (.git/hooks/)");
|
|
173
|
+
await saveKbPath(resolvedPath);
|
|
174
|
+
const git = (await import("simple-git")).default(resolvedPath);
|
|
175
|
+
await git.add(".");
|
|
176
|
+
await git.commit("init: KB \uAD6C\uC870 \uC0DD\uC131");
|
|
177
|
+
log.step("\uCD08\uAE30 \uCEE4\uBC0B \uC644\uB8CC");
|
|
178
|
+
log.success(`KB \uC0DD\uC131 \uC644\uB8CC: ${resolvedPath}`);
|
|
179
|
+
log.info("\uB2E4\uC74C \uB2E8\uACC4:");
|
|
180
|
+
log.step("ai-context/ \uC624\uBC84\uBDF0 \uCD08\uC548 \uC791\uC131");
|
|
181
|
+
log.step("git push");
|
|
182
|
+
log.step("\uD300\uC6D0\uC5D0\uAC8C kb join \uC548\uB0B4");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/commands/join.ts
|
|
186
|
+
import { input as input2 } from "@inquirer/prompts";
|
|
187
|
+
import fs4 from "fs-extra";
|
|
188
|
+
import path3 from "path";
|
|
189
|
+
async function joinCommand(remoteUrl) {
|
|
190
|
+
log.info("\uAE30\uC874 KB\uC5D0 \uD569\uB958\uD569\uB2C8\uB2E4.");
|
|
191
|
+
const defaultPath = path3.join(process.env.HOME || "~", "team-kb");
|
|
192
|
+
const kbPath = await input2({
|
|
193
|
+
message: "\uB85C\uCEEC \uACBD\uB85C",
|
|
194
|
+
default: defaultPath
|
|
195
|
+
});
|
|
196
|
+
const resolvedPath = kbPath.startsWith("~") ? kbPath.replace("~", process.env.HOME || "") : path3.resolve(kbPath);
|
|
197
|
+
if (fs4.existsSync(resolvedPath) && fs4.readdirSync(resolvedPath).length > 0) {
|
|
198
|
+
log.error(`${resolvedPath} \uAC00 \uC774\uBBF8 \uC874\uC7AC\uD558\uACE0 \uBE44\uC5B4\uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
log.info("git clone \uC911...");
|
|
202
|
+
await cloneRepo(remoteUrl, resolvedPath);
|
|
203
|
+
log.success("clone \uC644\uB8CC");
|
|
204
|
+
const hookSrc = path3.join(resolvedPath, ".kb", "hooks", "pre-commit");
|
|
205
|
+
const hookDest = path3.join(resolvedPath, ".git", "hooks", "pre-commit");
|
|
206
|
+
if (fs4.existsSync(hookSrc)) {
|
|
207
|
+
await fs4.copy(hookSrc, hookDest);
|
|
208
|
+
await fs4.chmod(hookDest, 493);
|
|
209
|
+
log.step("pre-commit hook \uC124\uCE58");
|
|
210
|
+
} else {
|
|
211
|
+
log.warn("pre-commit hook \uC6D0\uBCF8\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 (.kb/hooks/pre-commit)");
|
|
212
|
+
}
|
|
213
|
+
await saveKbPath(resolvedPath);
|
|
214
|
+
log.success(`KB \uD569\uB958 \uC644\uB8CC: ${resolvedPath}`);
|
|
215
|
+
log.info("\uB2E4\uC74C \uB2E8\uACC4:");
|
|
216
|
+
log.step("cd ~/repos/{project} && kb wire claude (\uB610\uB294 gemini / codex)");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/commands/wire.ts
|
|
220
|
+
import { input as input3 } from "@inquirer/prompts";
|
|
221
|
+
|
|
222
|
+
// src/commands/wire-claude.ts
|
|
223
|
+
import fs7 from "fs-extra";
|
|
224
|
+
import path6 from "path";
|
|
225
|
+
|
|
226
|
+
// src/utils/symlink.ts
|
|
227
|
+
import fs5 from "fs-extra";
|
|
228
|
+
import path4 from "path";
|
|
229
|
+
async function createSymlink(target, linkPath) {
|
|
230
|
+
await fs5.ensureDir(path4.dirname(linkPath));
|
|
231
|
+
if (fs5.existsSync(linkPath)) {
|
|
232
|
+
const stat = await fs5.lstat(linkPath);
|
|
233
|
+
if (stat.isSymbolicLink()) {
|
|
234
|
+
const existing = await fs5.readlink(linkPath);
|
|
235
|
+
if (existing === target) {
|
|
236
|
+
log.step(`symlink \uC774\uBBF8 \uC874\uC7AC: ${path4.basename(linkPath)}`);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
await fs5.remove(linkPath);
|
|
240
|
+
} else {
|
|
241
|
+
log.warn(`${linkPath} \uC774 \uC77C\uBC18 \uD30C\uC77C\uB85C \uC874\uC7AC. symlink\uB85C \uAD50\uCCB4\uD569\uB2C8\uB2E4.`);
|
|
242
|
+
await fs5.remove(linkPath);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (!fs5.existsSync(target)) {
|
|
246
|
+
log.warn(`symlink \uB300\uC0C1 \uD30C\uC77C \uC5C6\uC74C: ${target} (\uB098\uC911\uC5D0 \uC0DD\uC131 \uD544\uC694)`);
|
|
247
|
+
}
|
|
248
|
+
await fs5.symlink(target, linkPath);
|
|
249
|
+
log.step(`symlink \uC0DD\uC131: ${path4.basename(linkPath)} \u2192 ${target}`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// src/utils/gitignore.ts
|
|
253
|
+
import fs6 from "fs-extra";
|
|
254
|
+
import path5 from "path";
|
|
255
|
+
async function ensureGitignorePatterns(dir, patterns) {
|
|
256
|
+
const gitignorePath = path5.join(dir, ".gitignore");
|
|
257
|
+
let content = "";
|
|
258
|
+
if (fs6.existsSync(gitignorePath)) {
|
|
259
|
+
content = await fs6.readFile(gitignorePath, "utf-8");
|
|
260
|
+
}
|
|
261
|
+
const lines = content.split("\n");
|
|
262
|
+
const toAdd = [];
|
|
263
|
+
for (const pattern of patterns) {
|
|
264
|
+
const trimmed = pattern.trim();
|
|
265
|
+
if (!lines.some((line) => line.trim() === trimmed)) {
|
|
266
|
+
toAdd.push(trimmed);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (toAdd.length === 0) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const separator = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
273
|
+
const section = separator + "\n# KB wiring (local-only)\n" + toAdd.join("\n") + "\n";
|
|
274
|
+
await fs6.appendFile(gitignorePath, section);
|
|
275
|
+
log.step(`.gitignore\uC5D0 ${toAdd.length}\uAC1C \uD328\uD134 \uCD94\uAC00`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// src/commands/wire-claude.ts
|
|
279
|
+
function buildProjectSection(ctx) {
|
|
280
|
+
let section = "\n## \uD504\uB85C\uC81D\uD2B8\n";
|
|
281
|
+
section += `- ${ctx.techStack}
|
|
282
|
+
`;
|
|
283
|
+
if (ctx.domains) section += `- \uB2F4\uB2F9: ${ctx.domains}
|
|
284
|
+
`;
|
|
285
|
+
section += `- \uD604\uC7AC repo: ${ctx.repoName}
|
|
286
|
+
`;
|
|
287
|
+
if (ctx.terms.length > 0) {
|
|
288
|
+
section += "\n## \uB3C4\uBA54\uC778 \uC6A9\uC5B4\n";
|
|
289
|
+
for (const { term, definition } of ctx.terms) {
|
|
290
|
+
section += `- ${term}: ${definition}
|
|
291
|
+
`;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return section;
|
|
295
|
+
}
|
|
296
|
+
function buildKbLocationContent(ctx) {
|
|
297
|
+
return `## KB \uC808\uB300 \uACBD\uB85C
|
|
298
|
+
LLM\uC740 \uD30C\uC77C \uC811\uADFC \uBC0F \uC258 \uC2A4\uD06C\uB9BD\uD2B8 \uC2E4\uD589 \uC2DC \uBC18\uB4DC\uC2DC \uC544\uB798\uC758 \uC808\uB300 \uACBD\uB85C\uB97C \uADF8\uB300\uB85C \uC0AC\uC6A9\uD560 \uAC83.
|
|
299
|
+
- KB \uB8E8\uD2B8: ${ctx.kbPath}
|
|
300
|
+
- \uC804\uCCB4 \uC778\uB371\uC2A4: ${ctx.kbPath}/kb-index.json
|
|
301
|
+
- \uB85C\uCEEC \uC778\uB371\uC2A4: ${ctx.kbPath}/.kb-index.local.json
|
|
302
|
+
- \uB85C\uCEEC \uC778\uB371\uC2A4 \uC7AC\uC0DD\uC131 \uC2A4\uD06C\uB9BD\uD2B8: ${ctx.kbPath}/.kb/scripts/rebuild-index.sh
|
|
303
|
+
`;
|
|
304
|
+
}
|
|
305
|
+
async function wireClaude(ctx) {
|
|
306
|
+
const rulesDir = path6.join(ctx.projectRoot, ".claude", "rules");
|
|
307
|
+
await fs7.ensureDir(rulesDir);
|
|
308
|
+
const claudeMdPath = path6.join(ctx.projectRoot, "CLAUDE.md");
|
|
309
|
+
const projectSection = buildProjectSection(ctx);
|
|
310
|
+
if (fs7.existsSync(claudeMdPath)) {
|
|
311
|
+
const existing = await fs7.readFile(claudeMdPath, "utf-8");
|
|
312
|
+
if (!existing.includes("## \uD504\uB85C\uC81D\uD2B8")) {
|
|
313
|
+
await fs7.appendFile(claudeMdPath, projectSection);
|
|
314
|
+
log.step("CLAUDE.md\uC5D0 \uD504\uB85C\uC81D\uD2B8 \uC815\uBCF4 \uCD94\uAC00");
|
|
315
|
+
} else {
|
|
316
|
+
log.step("CLAUDE.md\uC5D0 \uD504\uB85C\uC81D\uD2B8 \uC815\uBCF4 \uC774\uBBF8 \uC874\uC7AC \u2014 \uC2A4\uD0B5");
|
|
317
|
+
}
|
|
318
|
+
} else {
|
|
319
|
+
await fs7.writeFile(claudeMdPath, projectSection.trimStart());
|
|
320
|
+
log.step("CLAUDE.md \uC0DD\uC131");
|
|
321
|
+
}
|
|
322
|
+
const aiContextDir = path6.join(ctx.kbPath, "ai-context");
|
|
323
|
+
const symlinkTargets = [
|
|
324
|
+
{ name: "kb-rules.md", target: path6.join(aiContextDir, "kb-rules.md") },
|
|
325
|
+
{ name: "team-focus.md", target: path6.join(aiContextDir, "team-focus.md") }
|
|
326
|
+
];
|
|
327
|
+
if (fs7.existsSync(aiContextDir)) {
|
|
328
|
+
const files = await fs7.readdir(aiContextDir);
|
|
329
|
+
for (const file of files) {
|
|
330
|
+
if (file.endsWith("-overview.md")) {
|
|
331
|
+
symlinkTargets.push({
|
|
332
|
+
name: file,
|
|
333
|
+
target: path6.join(aiContextDir, file)
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
for (const { name, target } of symlinkTargets) {
|
|
339
|
+
await createSymlink(target, path6.join(rulesDir, name));
|
|
340
|
+
}
|
|
341
|
+
const locationPath = path6.join(rulesDir, "kb-location.md");
|
|
342
|
+
await fs7.writeFile(locationPath, buildKbLocationContent(ctx));
|
|
343
|
+
log.step("kb-location.md \uC0DD\uC131 (KB \uC808\uB300 \uACBD\uB85C)");
|
|
344
|
+
const settingsPath = path6.join(ctx.projectRoot, ".claude", "settings.json");
|
|
345
|
+
if (!fs7.existsSync(settingsPath)) {
|
|
346
|
+
await fs7.writeJson(settingsPath, {}, { spaces: 2 });
|
|
347
|
+
log.step(".claude/settings.json \uC0DD\uC131");
|
|
348
|
+
}
|
|
349
|
+
await ensureGitignorePatterns(ctx.projectRoot, [
|
|
350
|
+
".claude/rules/"
|
|
351
|
+
]);
|
|
352
|
+
log.success("Claude Code \uC5F0\uACB0 \uC644\uB8CC");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// src/commands/wire-gemini.ts
|
|
356
|
+
import fs8 from "fs-extra";
|
|
357
|
+
import path7 from "path";
|
|
358
|
+
function buildGeminiMd(ctx) {
|
|
359
|
+
let content = "@.gemini/kb-import.md\n";
|
|
360
|
+
content += "\n## \uD504\uB85C\uC81D\uD2B8\n";
|
|
361
|
+
content += `- ${ctx.techStack}
|
|
362
|
+
`;
|
|
363
|
+
if (ctx.domains) content += `- \uB2F4\uB2F9: ${ctx.domains}
|
|
364
|
+
`;
|
|
365
|
+
content += `- \uD604\uC7AC repo: ${ctx.repoName}
|
|
366
|
+
`;
|
|
367
|
+
if (ctx.terms.length > 0) {
|
|
368
|
+
content += "\n## \uB3C4\uBA54\uC778 \uC6A9\uC5B4\n";
|
|
369
|
+
for (const { term, definition } of ctx.terms) {
|
|
370
|
+
content += `- ${term}: ${definition}
|
|
371
|
+
`;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return content;
|
|
375
|
+
}
|
|
376
|
+
function buildKbImportMd(ctx) {
|
|
377
|
+
const aiContext = path7.join(ctx.kbPath, "ai-context");
|
|
378
|
+
let content = `## KB \uC808\uB300 \uACBD\uB85C
|
|
379
|
+
LLM\uC740 \uD30C\uC77C \uC811\uADFC \uBC0F \uC258 \uC2A4\uD06C\uB9BD\uD2B8 \uC2E4\uD589 \uC2DC \uBC18\uB4DC\uC2DC \uC544\uB798\uC758 \uC808\uB300 \uACBD\uB85C\uB97C \uADF8\uB300\uB85C \uC0AC\uC6A9\uD560 \uAC83.
|
|
380
|
+
- KB \uB8E8\uD2B8: ${ctx.kbPath}
|
|
381
|
+
- \uC804\uCCB4 \uC778\uB371\uC2A4: ${ctx.kbPath}/kb-index.json
|
|
382
|
+
- \uB85C\uCEEC \uC778\uB371\uC2A4: ${ctx.kbPath}/.kb-index.local.json
|
|
383
|
+
- \uB85C\uCEEC \uC778\uB371\uC2A4 \uC7AC\uC0DD\uC131 \uC2A4\uD06C\uB9BD\uD2B8: ${ctx.kbPath}/.kb/scripts/rebuild-index.sh
|
|
384
|
+
|
|
385
|
+
`;
|
|
386
|
+
content += `@${aiContext}/kb-rules.md
|
|
387
|
+
`;
|
|
388
|
+
content += `@${aiContext}/team-focus.md
|
|
389
|
+
`;
|
|
390
|
+
if (fs8.existsSync(aiContext)) {
|
|
391
|
+
const files = fs8.readdirSync(aiContext);
|
|
392
|
+
for (const file of files) {
|
|
393
|
+
if (file.endsWith("-overview.md")) {
|
|
394
|
+
content += `@${aiContext}/${file}
|
|
395
|
+
`;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return content;
|
|
400
|
+
}
|
|
401
|
+
async function wireGemini(ctx) {
|
|
402
|
+
const geminiDir = path7.join(ctx.projectRoot, ".gemini");
|
|
403
|
+
await fs8.ensureDir(geminiDir);
|
|
404
|
+
const geminiMdPath = path7.join(ctx.projectRoot, "GEMINI.md");
|
|
405
|
+
if (fs8.existsSync(geminiMdPath)) {
|
|
406
|
+
const existing = await fs8.readFile(geminiMdPath, "utf-8");
|
|
407
|
+
if (!existing.includes("## \uD504\uB85C\uC81D\uD2B8")) {
|
|
408
|
+
await fs8.appendFile(geminiMdPath, "\n" + buildGeminiMd(ctx));
|
|
409
|
+
log.step("GEMINI.md\uC5D0 \uD504\uB85C\uC81D\uD2B8 \uC815\uBCF4 \uCD94\uAC00");
|
|
410
|
+
} else {
|
|
411
|
+
log.step("GEMINI.md\uC5D0 \uD504\uB85C\uC81D\uD2B8 \uC815\uBCF4 \uC774\uBBF8 \uC874\uC7AC \u2014 \uC2A4\uD0B5");
|
|
412
|
+
}
|
|
413
|
+
} else {
|
|
414
|
+
await fs8.writeFile(geminiMdPath, buildGeminiMd(ctx));
|
|
415
|
+
log.step("GEMINI.md \uC0DD\uC131");
|
|
416
|
+
}
|
|
417
|
+
const importPath = path7.join(geminiDir, "kb-import.md");
|
|
418
|
+
await fs8.writeFile(importPath, buildKbImportMd(ctx));
|
|
419
|
+
log.step(".gemini/kb-import.md \uC0DD\uC131 (KB \uACBD\uB85C + @import)");
|
|
420
|
+
const settingsPath = path7.join(geminiDir, "settings.json");
|
|
421
|
+
if (!fs8.existsSync(settingsPath)) {
|
|
422
|
+
await fs8.writeJson(settingsPath, {}, { spaces: 2 });
|
|
423
|
+
log.step(".gemini/settings.json \uC0DD\uC131");
|
|
424
|
+
}
|
|
425
|
+
await ensureGitignorePatterns(ctx.projectRoot, [
|
|
426
|
+
".gemini/kb-import.md"
|
|
427
|
+
]);
|
|
428
|
+
log.success("Gemini CLI \uC5F0\uACB0 \uC644\uB8CC");
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// src/commands/wire-codex.ts
|
|
432
|
+
import fs9 from "fs-extra";
|
|
433
|
+
import path8 from "path";
|
|
434
|
+
function buildAgentsMd(ctx) {
|
|
435
|
+
let content = "# Project\n";
|
|
436
|
+
content += `- ${ctx.techStack}
|
|
437
|
+
`;
|
|
438
|
+
if (ctx.domains) content += `- \uB2F4\uB2F9: ${ctx.domains}
|
|
439
|
+
`;
|
|
440
|
+
content += `- \uD604\uC7AC repo: ${ctx.repoName}
|
|
441
|
+
`;
|
|
442
|
+
content += "\n# Knowledge Base\n";
|
|
443
|
+
content += "- \uC138\uC158 \uC2DC\uC791 \uC2DC \uBC18\uB4DC\uC2DC .codex/kb-directive.md \uD30C\uC77C\uC744 \uBA3C\uC800 \uC77D\uACE0 \uADF8 \uC548\uC758 \uACBD\uB85C \uC9C0\uC2DC\uB97C \uB530\uB97C \uAC83\n";
|
|
444
|
+
if (ctx.terms.length > 0) {
|
|
445
|
+
content += "\n# \uB3C4\uBA54\uC778 \uC6A9\uC5B4\n";
|
|
446
|
+
for (const { term, definition } of ctx.terms) {
|
|
447
|
+
content += `- ${term}: ${definition}
|
|
448
|
+
`;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return content;
|
|
452
|
+
}
|
|
453
|
+
function buildDirectiveMd(ctx) {
|
|
454
|
+
const aiContext = path8.join(ctx.kbPath, "ai-context");
|
|
455
|
+
return `## KB \uC791\uB3D9 \uC9C0\uC2DC
|
|
456
|
+
- \uC138\uC158 \uC2DC\uC791 \uC2DC \uBC18\uB4DC\uC2DC ${aiContext}/kb-rules.md \uB97C \uBA3C\uC800 \uC77D\uC744 \uAC83
|
|
457
|
+
|
|
458
|
+
## KB \uC808\uB300 \uACBD\uB85C
|
|
459
|
+
LLM\uC740 \uD30C\uC77C \uC811\uADFC \uBC0F \uC258 \uC2A4\uD06C\uB9BD\uD2B8 \uC2E4\uD589 \uC2DC \uBC18\uB4DC\uC2DC \uC544\uB798\uC758 \uC808\uB300 \uACBD\uB85C\uB97C \uADF8\uB300\uB85C \uC0AC\uC6A9\uD560 \uAC83.
|
|
460
|
+
- KB \uB8E8\uD2B8: ${ctx.kbPath}
|
|
461
|
+
- \uC804\uCCB4 \uC778\uB371\uC2A4: ${ctx.kbPath}/kb-index.json
|
|
462
|
+
- \uB85C\uCEEC \uC778\uB371\uC2A4: ${ctx.kbPath}/.kb-index.local.json
|
|
463
|
+
- \uB85C\uCEEC \uC778\uB371\uC2A4 \uC7AC\uC0DD\uC131 \uC2A4\uD06C\uB9BD\uD2B8: ${ctx.kbPath}/.kb/scripts/rebuild-index.sh
|
|
464
|
+
`;
|
|
465
|
+
}
|
|
466
|
+
async function wireCodex(ctx) {
|
|
467
|
+
const codexDir = path8.join(ctx.projectRoot, ".codex");
|
|
468
|
+
await fs9.ensureDir(codexDir);
|
|
469
|
+
const agentsMdPath = path8.join(ctx.projectRoot, "AGENTS.md");
|
|
470
|
+
if (fs9.existsSync(agentsMdPath)) {
|
|
471
|
+
const existing = await fs9.readFile(agentsMdPath, "utf-8");
|
|
472
|
+
if (!existing.includes("# Project")) {
|
|
473
|
+
await fs9.appendFile(agentsMdPath, "\n" + buildAgentsMd(ctx));
|
|
474
|
+
log.step("AGENTS.md\uC5D0 \uD504\uB85C\uC81D\uD2B8 \uC815\uBCF4 \uCD94\uAC00");
|
|
475
|
+
} else {
|
|
476
|
+
log.step("AGENTS.md\uC5D0 \uD504\uB85C\uC81D\uD2B8 \uC815\uBCF4 \uC774\uBBF8 \uC874\uC7AC \u2014 \uC2A4\uD0B5");
|
|
477
|
+
}
|
|
478
|
+
} else {
|
|
479
|
+
await fs9.writeFile(agentsMdPath, buildAgentsMd(ctx));
|
|
480
|
+
log.step("AGENTS.md \uC0DD\uC131");
|
|
481
|
+
}
|
|
482
|
+
const directivePath = path8.join(codexDir, "kb-directive.md");
|
|
483
|
+
await fs9.writeFile(directivePath, buildDirectiveMd(ctx));
|
|
484
|
+
log.step(".codex/kb-directive.md \uC0DD\uC131 (KB \uACBD\uB85C + \uC9C0\uC2DC)");
|
|
485
|
+
await ensureGitignorePatterns(ctx.projectRoot, [
|
|
486
|
+
".codex/kb-directive.md"
|
|
487
|
+
]);
|
|
488
|
+
log.success("Codex CLI \uC5F0\uACB0 \uC644\uB8CC");
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// src/commands/wire.ts
|
|
492
|
+
async function collectWireContext() {
|
|
493
|
+
let kbPath = getKbPath();
|
|
494
|
+
if (!kbPath) {
|
|
495
|
+
kbPath = await input3({
|
|
496
|
+
message: "KB \uACBD\uB85C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. KB \uC808\uB300 \uACBD\uB85C\uB97C \uC785\uB825\uD558\uC138\uC694"
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
if (!kbPath) {
|
|
500
|
+
log.error("KB \uACBD\uB85C\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. kb init \uB610\uB294 kb join\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
const projectRoot = process.cwd();
|
|
504
|
+
const repoName = projectRoot.split("/").pop() || "unknown";
|
|
505
|
+
const techStack = await input3({
|
|
506
|
+
message: "\uAE30\uC220 \uC2A4\uD0DD",
|
|
507
|
+
default: "Spring Boot + Kotlin, Gradle"
|
|
508
|
+
});
|
|
509
|
+
const domains = await input3({
|
|
510
|
+
message: "\uB2F4\uB2F9 \uB3C4\uBA54\uC778",
|
|
511
|
+
default: ""
|
|
512
|
+
});
|
|
513
|
+
const terms = [];
|
|
514
|
+
log.info("\uB3C4\uBA54\uC778 \uC6A9\uC5B4\uB97C \uC785\uB825\uD558\uC138\uC694 (\uBE48 \uC904\uB85C \uC885\uB8CC):");
|
|
515
|
+
while (true) {
|
|
516
|
+
const term = await input3({
|
|
517
|
+
message: "\uC6A9\uC5B4 (\uC5D4\uD130\uB85C \uC885\uB8CC)",
|
|
518
|
+
default: ""
|
|
519
|
+
});
|
|
520
|
+
if (!term) break;
|
|
521
|
+
const definition = await input3({
|
|
522
|
+
message: `${term}\uC758 \uC815\uC758`
|
|
523
|
+
});
|
|
524
|
+
terms.push({ term, definition });
|
|
525
|
+
}
|
|
526
|
+
return { kbPath, projectRoot, techStack, domains, repoName, terms };
|
|
527
|
+
}
|
|
528
|
+
async function wireCommand(tool) {
|
|
529
|
+
const validTools = ["claude", "gemini", "codex"];
|
|
530
|
+
if (!validTools.includes(tool)) {
|
|
531
|
+
log.error(`\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uB3C4\uAD6C: ${tool}. (claude, gemini, codex \uC911 \uC120\uD0DD)`);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
log.info(`\uD504\uB85C\uC81D\uD2B8\uC5D0 ${tool} \uC5F0\uACB0\uC744 \uC2DC\uC791\uD569\uB2C8\uB2E4.`);
|
|
535
|
+
const ctx = await collectWireContext();
|
|
536
|
+
if (!ctx) return;
|
|
537
|
+
switch (tool) {
|
|
538
|
+
case "claude":
|
|
539
|
+
await wireClaude(ctx);
|
|
540
|
+
break;
|
|
541
|
+
case "gemini":
|
|
542
|
+
await wireGemini(ctx);
|
|
543
|
+
break;
|
|
544
|
+
case "codex":
|
|
545
|
+
await wireCodex(ctx);
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// src/commands/doctor.ts
|
|
551
|
+
import fs10 from "fs-extra";
|
|
552
|
+
import path9 from "path";
|
|
553
|
+
async function doctorCommand() {
|
|
554
|
+
log.info("KB \uD658\uACBD \uC9C4\uB2E8\uC744 \uC2DC\uC791\uD569\uB2C8\uB2E4.\n");
|
|
555
|
+
const results = [];
|
|
556
|
+
const kbPath = getKbPath();
|
|
557
|
+
results.push({
|
|
558
|
+
name: `\uC124\uC815 \uD30C\uC77C (${CONFIG_PATH})`,
|
|
559
|
+
ok: !!kbPath,
|
|
560
|
+
detail: kbPath || "\uBBF8\uC124\uC815. kb init \uB610\uB294 kb join\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694."
|
|
561
|
+
});
|
|
562
|
+
if (!kbPath) {
|
|
563
|
+
printResults(results);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
const kbExists = fs10.existsSync(kbPath);
|
|
567
|
+
results.push({
|
|
568
|
+
name: "KB \uB514\uB809\uD1A0\uB9AC \uC811\uADFC",
|
|
569
|
+
ok: kbExists,
|
|
570
|
+
detail: kbExists ? kbPath : `${kbPath} \uACBD\uB85C\uAC00 \uC874\uC7AC\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.`
|
|
571
|
+
});
|
|
572
|
+
if (!kbExists) {
|
|
573
|
+
printResults(results);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
const kbRulesPath = path9.join(kbPath, "ai-context", "kb-rules.md");
|
|
577
|
+
const kbRulesExists = fs10.existsSync(kbRulesPath);
|
|
578
|
+
results.push({
|
|
579
|
+
name: "ai-context/kb-rules.md",
|
|
580
|
+
ok: kbRulesExists,
|
|
581
|
+
detail: kbRulesExists ? "\uC874\uC7AC" : "\uC5C6\uC74C. ai-context/ \uCD08\uAE30 \uD30C\uC77C\uC744 \uC791\uC131\uD558\uC138\uC694."
|
|
582
|
+
});
|
|
583
|
+
const indexPath = path9.join(kbPath, "kb-index.json");
|
|
584
|
+
const indexExists = fs10.existsSync(indexPath);
|
|
585
|
+
results.push({
|
|
586
|
+
name: "kb-index.json",
|
|
587
|
+
ok: indexExists,
|
|
588
|
+
detail: indexExists ? "\uC874\uC7AC" : "\uC5C6\uC74C. git push \uD6C4 CI\uAC00 \uC0DD\uC131\uD569\uB2C8\uB2E4."
|
|
589
|
+
});
|
|
590
|
+
const hookPath = path9.join(kbPath, ".git", "hooks", "pre-commit");
|
|
591
|
+
const hookExists = fs10.existsSync(hookPath);
|
|
592
|
+
results.push({
|
|
593
|
+
name: "pre-commit hook",
|
|
594
|
+
ok: hookExists,
|
|
595
|
+
detail: hookExists ? "\uC124\uCE58\uB428" : "\uBBF8\uC124\uCE58. kb join\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694."
|
|
596
|
+
});
|
|
597
|
+
const cwd = process.cwd();
|
|
598
|
+
if (cwd !== kbPath) {
|
|
599
|
+
const claudeRulesDir = path9.join(cwd, ".claude", "rules");
|
|
600
|
+
if (fs10.existsSync(claudeRulesDir)) {
|
|
601
|
+
const kbRulesLink = path9.join(claudeRulesDir, "kb-rules.md");
|
|
602
|
+
if (fs10.existsSync(kbRulesLink)) {
|
|
603
|
+
const stat = await fs10.lstat(kbRulesLink);
|
|
604
|
+
if (stat.isSymbolicLink()) {
|
|
605
|
+
const target = await fs10.readlink(kbRulesLink);
|
|
606
|
+
const targetValid = fs10.existsSync(target);
|
|
607
|
+
results.push({
|
|
608
|
+
name: "Claude symlink (kb-rules.md)",
|
|
609
|
+
ok: targetValid,
|
|
610
|
+
detail: targetValid ? `\u2192 ${target}` : `\uAE68\uC9C4 symlink \u2192 ${target}`
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
const locationFile = path9.join(claudeRulesDir, "kb-location.md");
|
|
615
|
+
results.push({
|
|
616
|
+
name: "Claude kb-location.md",
|
|
617
|
+
ok: fs10.existsSync(locationFile),
|
|
618
|
+
detail: fs10.existsSync(locationFile) ? "\uC874\uC7AC" : "\uC5C6\uC74C. kb wire claude\uB97C \uC2E4\uD589\uD558\uC138\uC694."
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
const geminiImport = path9.join(cwd, ".gemini", "kb-import.md");
|
|
622
|
+
if (fs10.existsSync(path9.join(cwd, "GEMINI.md"))) {
|
|
623
|
+
results.push({
|
|
624
|
+
name: "Gemini kb-import.md",
|
|
625
|
+
ok: fs10.existsSync(geminiImport),
|
|
626
|
+
detail: fs10.existsSync(geminiImport) ? "\uC874\uC7AC" : "\uC5C6\uC74C. kb wire gemini\uB97C \uC2E4\uD589\uD558\uC138\uC694."
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
const codexDirective = path9.join(cwd, ".codex", "kb-directive.md");
|
|
630
|
+
if (fs10.existsSync(path9.join(cwd, "AGENTS.md"))) {
|
|
631
|
+
results.push({
|
|
632
|
+
name: "Codex kb-directive.md",
|
|
633
|
+
ok: fs10.existsSync(codexDirective),
|
|
634
|
+
detail: fs10.existsSync(codexDirective) ? "\uC874\uC7AC" : "\uC5C6\uC74C. kb wire codex\uB97C \uC2E4\uD589\uD558\uC138\uC694."
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
printResults(results);
|
|
639
|
+
}
|
|
640
|
+
function printResults(results) {
|
|
641
|
+
let allOk = true;
|
|
642
|
+
for (const r of results) {
|
|
643
|
+
if (r.ok) {
|
|
644
|
+
log.success(`${r.name}: ${r.detail}`);
|
|
645
|
+
} else {
|
|
646
|
+
log.error(`${r.name}: ${r.detail}`);
|
|
647
|
+
allOk = false;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
console.log();
|
|
651
|
+
if (allOk) {
|
|
652
|
+
log.success("\uBAA8\uB4E0 \uAC80\uC99D \uD1B5\uACFC");
|
|
653
|
+
} else {
|
|
654
|
+
log.warn("\uC77C\uBD80 \uD56D\uBAA9\uC5D0 \uBB38\uC81C\uAC00 \uC788\uC2B5\uB2C8\uB2E4. \uC704 \uBA54\uC2DC\uC9C0\uB97C \uD655\uC778\uD558\uC138\uC694.");
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// src/commands/uninstall.ts
|
|
659
|
+
import { confirm } from "@inquirer/prompts";
|
|
660
|
+
import fs11 from "fs-extra";
|
|
661
|
+
import path10 from "path";
|
|
662
|
+
async function uninstallCommand() {
|
|
663
|
+
log.info("KB wiring \uC81C\uAC70\uB97C \uC2DC\uC791\uD569\uB2C8\uB2E4.\n");
|
|
664
|
+
const cwd = process.cwd();
|
|
665
|
+
let removed = 0;
|
|
666
|
+
const claudeRulesDir = path10.join(cwd, ".claude", "rules");
|
|
667
|
+
if (fs11.existsSync(claudeRulesDir)) {
|
|
668
|
+
const files = await fs11.readdir(claudeRulesDir);
|
|
669
|
+
for (const file of files) {
|
|
670
|
+
const filePath = path10.join(claudeRulesDir, file);
|
|
671
|
+
const stat = await fs11.lstat(filePath);
|
|
672
|
+
if (stat.isSymbolicLink()) {
|
|
673
|
+
await fs11.remove(filePath);
|
|
674
|
+
log.step(`symlink \uC0AD\uC81C: .claude/rules/${file}`);
|
|
675
|
+
removed++;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
const locationPath = path10.join(claudeRulesDir, "kb-location.md");
|
|
679
|
+
if (fs11.existsSync(locationPath)) {
|
|
680
|
+
await fs11.remove(locationPath);
|
|
681
|
+
log.step("\uC0AD\uC81C: .claude/rules/kb-location.md");
|
|
682
|
+
removed++;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
const geminiImport = path10.join(cwd, ".gemini", "kb-import.md");
|
|
686
|
+
if (fs11.existsSync(geminiImport)) {
|
|
687
|
+
await fs11.remove(geminiImport);
|
|
688
|
+
log.step("\uC0AD\uC81C: .gemini/kb-import.md");
|
|
689
|
+
removed++;
|
|
690
|
+
}
|
|
691
|
+
const codexDirective = path10.join(cwd, ".codex", "kb-directive.md");
|
|
692
|
+
if (fs11.existsSync(codexDirective)) {
|
|
693
|
+
await fs11.remove(codexDirective);
|
|
694
|
+
log.step("\uC0AD\uC81C: .codex/kb-directive.md");
|
|
695
|
+
removed++;
|
|
696
|
+
}
|
|
697
|
+
if (removed > 0) {
|
|
698
|
+
log.success(`\uD504\uB85C\uC81D\uD2B8 wiring ${removed}\uAC1C \uD30C\uC77C \uC81C\uAC70 \uC644\uB8CC`);
|
|
699
|
+
} else {
|
|
700
|
+
log.info("\uD604\uC7AC \uB514\uB809\uD1A0\uB9AC\uC5D0 \uC81C\uAC70\uD560 wiring \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
701
|
+
}
|
|
702
|
+
const trackedFiles = ["CLAUDE.md", "GEMINI.md", "AGENTS.md"].filter(
|
|
703
|
+
(f) => fs11.existsSync(path10.join(cwd, f))
|
|
704
|
+
);
|
|
705
|
+
if (trackedFiles.length > 0) {
|
|
706
|
+
log.warn(
|
|
707
|
+
`${trackedFiles.join(", ")}\uC758 \uD504\uB85C\uC81D\uD2B8 \uC139\uC158\uC740 \uC218\uB3D9\uC73C\uB85C \uC815\uB9AC\uD558\uC138\uC694 (\uC0AC\uC6A9\uC790 \uB0B4\uC6A9\uACFC \uC11E\uC5EC\uC788\uC744 \uC218 \uC788\uC74C).`
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
const kbPath = getKbPath();
|
|
711
|
+
const shouldRemoveConfig = await confirm({
|
|
712
|
+
message: `\uC124\uC815 \uD30C\uC77C\uC744 \uC0AD\uC81C\uD560\uAE4C\uC694? (${CONFIG_PATH})`,
|
|
713
|
+
default: true
|
|
714
|
+
});
|
|
715
|
+
if (shouldRemoveConfig) {
|
|
716
|
+
const didRemove = await removeConfig();
|
|
717
|
+
if (didRemove) {
|
|
718
|
+
log.step("\uC124\uC815 \uD30C\uC77C \uC0AD\uC81C \uC644\uB8CC");
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
if (kbPath && fs11.existsSync(kbPath)) {
|
|
722
|
+
const shouldRemoveKb = await confirm({
|
|
723
|
+
message: `KB \uB514\uB809\uD1A0\uB9AC\uB97C \uC0AD\uC81C\uD560\uAE4C\uC694? (${kbPath}) \u2014 \uD300 \uC9C0\uC2DD\uC774 \uBAA8\uB450 \uC0AD\uC81C\uB429\uB2C8\uB2E4!`,
|
|
724
|
+
default: false
|
|
725
|
+
});
|
|
726
|
+
if (shouldRemoveKb) {
|
|
727
|
+
await fs11.remove(kbPath);
|
|
728
|
+
log.step(`KB \uB514\uB809\uD1A0\uB9AC \uC0AD\uC81C: ${kbPath}`);
|
|
729
|
+
} else {
|
|
730
|
+
log.info(`KB \uB514\uB809\uD1A0\uB9AC \uC720\uC9C0: ${kbPath}`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
console.log();
|
|
734
|
+
log.success("uninstall \uC644\uB8CC");
|
|
735
|
+
log.info("npm uninstall -g kb-cli \uB85C CLI \uC790\uCCB4\uB3C4 \uC81C\uAC70\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// src/index.ts
|
|
739
|
+
var program = new Command();
|
|
740
|
+
program.name("kb").description("Team Knowledge Base harness CLI").version("0.1.0");
|
|
741
|
+
program.command("init").description("KB \uCD5C\uCD08 \uC0DD\uC131 (\uD300 \uB9AC\uB4DC)").action(initCommand);
|
|
742
|
+
program.command("join").description("\uAE30\uC874 KB\uC5D0 \uD569\uB958 (\uD300\uC6D0)").argument("<remote-url>", "Git remote URL").action(joinCommand);
|
|
743
|
+
program.command("wire").description("\uD504\uB85C\uC81D\uD2B8\uC5D0 LLM \uB3C4\uAD6C \uC5F0\uACB0").argument("<tool>", "claude | gemini | codex").action(wireCommand);
|
|
744
|
+
program.command("doctor").description("KB \uD658\uACBD \uBB34\uACB0\uC131 \uAC80\uC99D").action(doctorCommand);
|
|
745
|
+
program.command("uninstall").description("KB wiring \uC81C\uAC70 + \uC124\uC815 \uC815\uB9AC").action(uninstallCommand);
|
|
746
|
+
program.parse();
|
|
747
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/commands/init.ts","../src/utils/log.ts","../src/utils/template.ts","../src/utils/config.ts","../src/utils/git.ts","../src/commands/join.ts","../src/commands/wire.ts","../src/commands/wire-claude.ts","../src/utils/symlink.ts","../src/utils/gitignore.ts","../src/commands/wire-gemini.ts","../src/commands/wire-codex.ts","../src/commands/doctor.ts","../src/commands/uninstall.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { initCommand } from \"./commands/init.js\";\nimport { joinCommand } from \"./commands/join.js\";\nimport { wireCommand } from \"./commands/wire.js\";\nimport { doctorCommand } from \"./commands/doctor.js\";\nimport { uninstallCommand } from \"./commands/uninstall.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"kb\")\n .description(\"Team Knowledge Base harness CLI\")\n .version(\"0.1.0\");\n\nprogram\n .command(\"init\")\n .description(\"KB 최초 생성 (팀 리드)\")\n .action(initCommand);\n\nprogram\n .command(\"join\")\n .description(\"기존 KB에 합류 (팀원)\")\n .argument(\"<remote-url>\", \"Git remote URL\")\n .action(joinCommand);\n\nprogram\n .command(\"wire\")\n .description(\"프로젝트에 LLM 도구 연결\")\n .argument(\"<tool>\", \"claude | gemini | codex\")\n .action(wireCommand);\n\nprogram\n .command(\"doctor\")\n .description(\"KB 환경 무결성 검증\")\n .action(doctorCommand);\n\nprogram\n .command(\"uninstall\")\n .description(\"KB wiring 제거 + 설정 정리\")\n .action(uninstallCommand);\n\nprogram.parse();\n","import { input } from \"@inquirer/prompts\";\nimport fs from \"fs-extra\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { log } from \"../utils/log.js\";\nimport { renderTemplate } from \"../utils/template.js\";\nimport { saveKbPath } from \"../utils/config.js\";\nimport { initRepo } from \"../utils/git.js\";\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst TEMPLATES_DIR = path.join(__dirname, \"..\", \"..\", \"templates\", \"kb-structure\");\n\nconst KB_DIRS = [\n \"global\",\n \"rules\",\n \"analysis\",\n \"decisions\",\n \"troubleshoot\",\n \"drafts\",\n \"areas\",\n \"templates\",\n \"ai-context\",\n \"archive\",\n \"00-inbox\",\n \"99-personal\",\n \".kb/hooks\",\n \".kb/scripts\",\n \".kb/setup\",\n \".github/workflows\",\n];\n\nexport async function initCommand(): Promise<void> {\n log.info(\"KB 최초 생성을 시작합니다.\");\n\n const defaultPath = path.join(process.env.HOME || \"~\", \"team-kb\");\n const kbPath = await input({\n message: \"KB 경로\",\n default: defaultPath,\n });\n const resolvedPath = kbPath.startsWith(\"~\")\n ? kbPath.replace(\"~\", process.env.HOME || \"\")\n : path.resolve(kbPath);\n\n if (fs.existsSync(resolvedPath) && fs.readdirSync(resolvedPath).length > 0) {\n log.error(`${resolvedPath} 가 이미 존재하고 비어있지 않습니다.`);\n return;\n }\n\n const remoteUrl = await input({\n message: \"Git remote URL (없으면 엔터)\",\n default: \"\",\n });\n\n const today = new Date().toISOString().split(\"T\")[0];\n const vars = { date: today };\n\n // 1. Create directory structure\n log.info(\"폴더 구조 생성 중...\");\n for (const dir of KB_DIRS) {\n await fs.ensureDir(path.join(resolvedPath, dir));\n }\n log.success(\"폴더 구조 생성 완료\");\n\n // 2. Copy .gitignore\n const gitignoreSrc = path.join(TEMPLATES_DIR, \"gitignore\");\n await fs.copy(gitignoreSrc, path.join(resolvedPath, \".gitignore\"));\n log.step(\".gitignore 생성\");\n\n // 3. Copy and render note templates\n const noteTemplatesDir = path.join(TEMPLATES_DIR, \"note-templates\");\n const noteTemplates = await fs.readdir(noteTemplatesDir);\n for (const file of noteTemplates) {\n const content = await fs.readFile(path.join(noteTemplatesDir, file), \"utf-8\");\n const rendered = renderTemplate(content, vars);\n await fs.writeFile(path.join(resolvedPath, \"templates\", file), rendered);\n }\n log.step(\"노트 템플릿 생성\");\n\n // 4. Copy pre-commit hook\n const preCommitSrc = path.join(TEMPLATES_DIR, \"pre-commit\");\n const preCommitDest = path.join(resolvedPath, \".kb\", \"hooks\", \"pre-commit\");\n await fs.copy(preCommitSrc, preCommitDest);\n await fs.chmod(preCommitDest, 0o755);\n log.step(\"pre-commit hook 생성\");\n\n // 5. Copy rebuild-index.sh\n const rebuildSrc = path.join(TEMPLATES_DIR, \"rebuild-index.sh\");\n const rebuildDest = path.join(resolvedPath, \".kb\", \"scripts\", \"rebuild-index.sh\");\n await fs.copy(rebuildSrc, rebuildDest);\n await fs.chmod(rebuildDest, 0o755);\n log.step(\"rebuild-index.sh 생성\");\n\n // 6. Copy GitHub Actions workflows\n const healthSrc = path.join(TEMPLATES_DIR, \"kb-health.yml\");\n const coverageSrc = path.join(TEMPLATES_DIR, \"kb-coverage.yml\");\n await fs.copy(healthSrc, path.join(resolvedPath, \".github\", \"workflows\", \"kb-health.yml\"));\n await fs.copy(coverageSrc, path.join(resolvedPath, \".github\", \"workflows\", \"kb-coverage.yml\"));\n log.step(\"GitHub Actions 워크플로우 생성\");\n\n // 7. Create kb-index.json (empty)\n await fs.writeJson(path.join(resolvedPath, \"kb-index.json\"), []);\n log.step(\"kb-index.json 초기화 (빈 배열)\");\n\n // 8. Create ai-context files\n const kbRulesSrc = path.join(TEMPLATES_DIR, \"kb-rules.md\");\n const kbRulesContent = await fs.readFile(kbRulesSrc, \"utf-8\");\n await fs.writeFile(\n path.join(resolvedPath, \"ai-context\", \"kb-rules.md\"),\n renderTemplate(kbRulesContent, vars),\n );\n\n const teamFocusSrc = path.join(TEMPLATES_DIR, \"team-focus.md\");\n const teamFocusContent = await fs.readFile(teamFocusSrc, \"utf-8\");\n await fs.writeFile(\n path.join(resolvedPath, \"ai-context\", \"team-focus.md\"),\n renderTemplate(teamFocusContent, vars),\n );\n log.step(\"ai-context/ 파일 생성 (kb-rules.md, team-focus.md)\");\n\n // 9. Git init + remote\n await initRepo(resolvedPath, remoteUrl || undefined);\n log.step(\"git init\" + (remoteUrl ? ` + remote: ${remoteUrl}` : \"\"));\n\n // 10. Install pre-commit hook\n const gitHookDest = path.join(resolvedPath, \".git\", \"hooks\", \"pre-commit\");\n await fs.copy(preCommitDest, gitHookDest);\n await fs.chmod(gitHookDest, 0o755);\n log.step(\"pre-commit hook 설치 (.git/hooks/)\");\n\n // 11. Save config\n await saveKbPath(resolvedPath);\n\n // 12. Initial commit\n const git = (await import(\"simple-git\")).default(resolvedPath);\n await git.add(\".\");\n await git.commit(\"init: KB 구조 생성\");\n log.step(\"초기 커밋 완료\");\n\n log.success(`KB 생성 완료: ${resolvedPath}`);\n log.info(\"다음 단계:\");\n log.step(\"ai-context/ 오버뷰 초안 작성\");\n log.step(\"git push\");\n log.step(\"팀원에게 kb join 안내\");\n}\n","import pc from \"picocolors\";\n\nexport const log = {\n info: (msg: string) => console.log(pc.blue(\"ℹ\") + \" \" + msg),\n success: (msg: string) => console.log(pc.green(\"✔\") + \" \" + msg),\n warn: (msg: string) => console.log(pc.yellow(\"⚠\") + \" \" + msg),\n error: (msg: string) => console.error(pc.red(\"✖\") + \" \" + msg),\n step: (msg: string) => console.log(pc.gray(\" →\") + \" \" + msg),\n};\n","/**\n * Simple template variable replacement.\n * Replaces {{VAR_NAME}} with provided values.\n */\nexport function renderTemplate(\n template: string,\n vars: Record<string, string>,\n): string {\n let result = template;\n for (const [key, value] of Object.entries(vars)) {\n result = result.replaceAll(`{{${key}}}`, value);\n }\n return result;\n}\n","import fs from \"fs-extra\";\nimport path from \"node:path\";\nimport { log } from \"./log.js\";\n\nconst CONFIG_DIR = path.join(\n process.env.HOME || \"~\",\n \".config\",\n \"kb-cli\",\n);\nconst CONFIG_PATH = path.join(CONFIG_DIR, \"config.json\");\n\ninterface KbConfig {\n kbPath: string;\n}\n\n/**\n * Read KB path from config file.\n */\nexport function getKbPath(): string | undefined {\n try {\n if (fs.existsSync(CONFIG_PATH)) {\n const config: KbConfig = fs.readJsonSync(CONFIG_PATH);\n return config.kbPath;\n }\n } catch {\n // ignore parse errors\n }\n return undefined;\n}\n\n/**\n * Save KB path to config file.\n */\nexport async function saveKbPath(kbPath: string): Promise<void> {\n await fs.ensureDir(CONFIG_DIR);\n await fs.writeJson(CONFIG_PATH, { kbPath }, { spaces: 2 });\n log.step(`설정 저장: ${CONFIG_PATH}`);\n}\n\n/**\n * Remove config file.\n */\nexport async function removeConfig(): Promise<boolean> {\n if (fs.existsSync(CONFIG_PATH)) {\n await fs.remove(CONFIG_PATH);\n return true;\n }\n return false;\n}\n\nexport { CONFIG_PATH };\n","import simpleGit, { type SimpleGit } from \"simple-git\";\nimport fs from \"fs-extra\";\n\nexport function getGit(cwd?: string): SimpleGit {\n return simpleGit(cwd);\n}\n\nexport async function isGitRepo(dir: string): Promise<boolean> {\n try {\n const git = getGit(dir);\n return await git.checkIsRepo();\n } catch {\n return false;\n }\n}\n\nexport async function initRepo(\n dir: string,\n remote?: string,\n): Promise<void> {\n const git = getGit(dir);\n await git.init();\n if (remote) {\n await git.addRemote(\"origin\", remote);\n }\n}\n\nexport async function cloneRepo(\n url: string,\n dir: string,\n): Promise<void> {\n await fs.ensureDir(dir);\n const git = simpleGit();\n await git.clone(url, dir);\n}\n","import { input } from \"@inquirer/prompts\";\nimport fs from \"fs-extra\";\nimport path from \"node:path\";\nimport { log } from \"../utils/log.js\";\nimport { saveKbPath } from \"../utils/config.js\";\nimport { cloneRepo } from \"../utils/git.js\";\n\nexport async function joinCommand(remoteUrl: string): Promise<void> {\n log.info(\"기존 KB에 합류합니다.\");\n\n const defaultPath = path.join(process.env.HOME || \"~\", \"team-kb\");\n const kbPath = await input({\n message: \"로컬 경로\",\n default: defaultPath,\n });\n const resolvedPath = kbPath.startsWith(\"~\")\n ? kbPath.replace(\"~\", process.env.HOME || \"\")\n : path.resolve(kbPath);\n\n if (fs.existsSync(resolvedPath) && fs.readdirSync(resolvedPath).length > 0) {\n log.error(`${resolvedPath} 가 이미 존재하고 비어있지 않습니다.`);\n return;\n }\n\n // 1. Clone\n log.info(\"git clone 중...\");\n await cloneRepo(remoteUrl, resolvedPath);\n log.success(\"clone 완료\");\n\n // 2. Install pre-commit hook\n const hookSrc = path.join(resolvedPath, \".kb\", \"hooks\", \"pre-commit\");\n const hookDest = path.join(resolvedPath, \".git\", \"hooks\", \"pre-commit\");\n if (fs.existsSync(hookSrc)) {\n await fs.copy(hookSrc, hookDest);\n await fs.chmod(hookDest, 0o755);\n log.step(\"pre-commit hook 설치\");\n } else {\n log.warn(\"pre-commit hook 원본이 없습니다 (.kb/hooks/pre-commit)\");\n }\n\n // 3. Save config\n await saveKbPath(resolvedPath);\n\n log.success(`KB 합류 완료: ${resolvedPath}`);\n log.info(\"다음 단계:\");\n log.step(\"cd ~/repos/{project} && kb wire claude (또는 gemini / codex)\");\n}\n","import { input } from \"@inquirer/prompts\";\nimport { log } from \"../utils/log.js\";\nimport { getKbPath } from \"../utils/config.js\";\nimport { wireClaude } from \"./wire-claude.js\";\nimport { wireGemini } from \"./wire-gemini.js\";\nimport { wireCodex } from \"./wire-codex.js\";\n\nexport interface WireContext {\n /** Absolute path to the KB repo */\n kbPath: string;\n /** Current project root (cwd) */\n projectRoot: string;\n /** Tech stack (e.g., \"Spring Boot + Kotlin, Gradle\") */\n techStack: string;\n /** Domain areas (e.g., \"할인/쿠폰, 수수료\") */\n domains: string;\n /** Current repo name (e.g., \"coupon-api\") */\n repoName: string;\n /** Domain terms (key-value pairs) */\n terms: Array<{ term: string; definition: string }>;\n}\n\nasync function collectWireContext(): Promise<WireContext | null> {\n // Resolve KB path\n let kbPath = getKbPath();\n if (!kbPath) {\n kbPath = await input({\n message: \"KB 경로를 찾을 수 없습니다. KB 절대 경로를 입력하세요\",\n });\n }\n\n if (!kbPath) {\n log.error(\"KB 경로가 필요합니다. kb init 또는 kb join을 먼저 실행하세요.\");\n return null;\n }\n\n const projectRoot = process.cwd();\n const repoName =\n projectRoot.split(\"/\").pop() || \"unknown\";\n\n const techStack = await input({\n message: \"기술 스택\",\n default: \"Spring Boot + Kotlin, Gradle\",\n });\n\n const domains = await input({\n message: \"담당 도메인\",\n default: \"\",\n });\n\n // Collect domain terms\n const terms: Array<{ term: string; definition: string }> = [];\n log.info(\"도메인 용어를 입력하세요 (빈 줄로 종료):\");\n while (true) {\n const term = await input({\n message: \"용어 (엔터로 종료)\",\n default: \"\",\n });\n if (!term) break;\n const definition = await input({\n message: `${term}의 정의`,\n });\n terms.push({ term, definition });\n }\n\n return { kbPath, projectRoot, techStack, domains, repoName, terms };\n}\n\nexport async function wireCommand(tool: string): Promise<void> {\n const validTools = [\"claude\", \"gemini\", \"codex\"];\n if (!validTools.includes(tool)) {\n log.error(`지원하지 않는 도구: ${tool}. (claude, gemini, codex 중 선택)`);\n return;\n }\n\n log.info(`프로젝트에 ${tool} 연결을 시작합니다.`);\n\n const ctx = await collectWireContext();\n if (!ctx) return;\n\n switch (tool) {\n case \"claude\":\n await wireClaude(ctx);\n break;\n case \"gemini\":\n await wireGemini(ctx);\n break;\n case \"codex\":\n await wireCodex(ctx);\n break;\n }\n}\n","import fs from \"fs-extra\";\nimport path from \"node:path\";\nimport { log } from \"../utils/log.js\";\nimport { createSymlink } from \"../utils/symlink.js\";\nimport { ensureGitignorePatterns } from \"../utils/gitignore.js\";\nimport type { WireContext } from \"./wire.js\";\n\nfunction buildProjectSection(ctx: WireContext): string {\n let section = \"\\n## 프로젝트\\n\";\n section += `- ${ctx.techStack}\\n`;\n if (ctx.domains) section += `- 담당: ${ctx.domains}\\n`;\n section += `- 현재 repo: ${ctx.repoName}\\n`;\n\n if (ctx.terms.length > 0) {\n section += \"\\n## 도메인 용어\\n\";\n for (const { term, definition } of ctx.terms) {\n section += `- ${term}: ${definition}\\n`;\n }\n }\n\n return section;\n}\n\nfunction buildKbLocationContent(ctx: WireContext): string {\n return `## KB 절대 경로\nLLM은 파일 접근 및 쉘 스크립트 실행 시 반드시 아래의 절대 경로를 그대로 사용할 것.\n- KB 루트: ${ctx.kbPath}\n- 전체 인덱스: ${ctx.kbPath}/kb-index.json\n- 로컬 인덱스: ${ctx.kbPath}/.kb-index.local.json\n- 로컬 인덱스 재생성 스크립트: ${ctx.kbPath}/.kb/scripts/rebuild-index.sh\n`;\n}\n\nexport async function wireClaude(ctx: WireContext): Promise<void> {\n const rulesDir = path.join(ctx.projectRoot, \".claude\", \"rules\");\n await fs.ensureDir(rulesDir);\n\n // 1. CLAUDE.md — append project info (don't overwrite)\n const claudeMdPath = path.join(ctx.projectRoot, \"CLAUDE.md\");\n const projectSection = buildProjectSection(ctx);\n\n if (fs.existsSync(claudeMdPath)) {\n const existing = await fs.readFile(claudeMdPath, \"utf-8\");\n if (!existing.includes(\"## 프로젝트\")) {\n await fs.appendFile(claudeMdPath, projectSection);\n log.step(\"CLAUDE.md에 프로젝트 정보 추가\");\n } else {\n log.step(\"CLAUDE.md에 프로젝트 정보 이미 존재 — 스킵\");\n }\n } else {\n await fs.writeFile(claudeMdPath, projectSection.trimStart());\n log.step(\"CLAUDE.md 생성\");\n }\n\n // 2. Symlinks to KB ai-context files\n const aiContextDir = path.join(ctx.kbPath, \"ai-context\");\n const symlinkTargets = [\n { name: \"kb-rules.md\", target: path.join(aiContextDir, \"kb-rules.md\") },\n { name: \"team-focus.md\", target: path.join(aiContextDir, \"team-focus.md\") },\n ];\n\n // Also symlink any *-overview.md files\n if (fs.existsSync(aiContextDir)) {\n const files = await fs.readdir(aiContextDir);\n for (const file of files) {\n if (file.endsWith(\"-overview.md\")) {\n symlinkTargets.push({\n name: file,\n target: path.join(aiContextDir, file),\n });\n }\n }\n }\n\n for (const { name, target } of symlinkTargets) {\n await createSymlink(target, path.join(rulesDir, name));\n }\n\n // 3. kb-location.md — explicit paths (local-only)\n const locationPath = path.join(rulesDir, \"kb-location.md\");\n await fs.writeFile(locationPath, buildKbLocationContent(ctx));\n log.step(\"kb-location.md 생성 (KB 절대 경로)\");\n\n // 4. .claude/settings.json\n const settingsPath = path.join(ctx.projectRoot, \".claude\", \"settings.json\");\n if (!fs.existsSync(settingsPath)) {\n await fs.writeJson(settingsPath, {}, { spaces: 2 });\n log.step(\".claude/settings.json 생성\");\n }\n\n // 5. Update .gitignore\n await ensureGitignorePatterns(ctx.projectRoot, [\n \".claude/rules/\",\n ]);\n\n log.success(\"Claude Code 연결 완료\");\n}\n","import fs from \"fs-extra\";\nimport path from \"node:path\";\nimport { log } from \"./log.js\";\n\n/**\n * Create a symlink. If target doesn't exist, warn but still create.\n */\nexport async function createSymlink(\n target: string,\n linkPath: string,\n): Promise<void> {\n await fs.ensureDir(path.dirname(linkPath));\n\n if (fs.existsSync(linkPath)) {\n const stat = await fs.lstat(linkPath);\n if (stat.isSymbolicLink()) {\n const existing = await fs.readlink(linkPath);\n if (existing === target) {\n log.step(`symlink 이미 존재: ${path.basename(linkPath)}`);\n return;\n }\n await fs.remove(linkPath);\n } else {\n log.warn(`${linkPath} 이 일반 파일로 존재. symlink로 교체합니다.`);\n await fs.remove(linkPath);\n }\n }\n\n if (!fs.existsSync(target)) {\n log.warn(`symlink 대상 파일 없음: ${target} (나중에 생성 필요)`);\n }\n\n await fs.symlink(target, linkPath);\n log.step(`symlink 생성: ${path.basename(linkPath)} → ${target}`);\n}\n","import fs from \"fs-extra\";\nimport path from \"node:path\";\nimport { log } from \"./log.js\";\n\n/**\n * Ensure patterns exist in a .gitignore file.\n * Creates the file if it doesn't exist. Skips patterns already present.\n */\nexport async function ensureGitignorePatterns(\n dir: string,\n patterns: string[],\n): Promise<void> {\n const gitignorePath = path.join(dir, \".gitignore\");\n\n let content = \"\";\n if (fs.existsSync(gitignorePath)) {\n content = await fs.readFile(gitignorePath, \"utf-8\");\n }\n\n const lines = content.split(\"\\n\");\n const toAdd: string[] = [];\n\n for (const pattern of patterns) {\n const trimmed = pattern.trim();\n if (!lines.some((line) => line.trim() === trimmed)) {\n toAdd.push(trimmed);\n }\n }\n\n if (toAdd.length === 0) {\n return;\n }\n\n const separator = content.length > 0 && !content.endsWith(\"\\n\") ? \"\\n\" : \"\";\n const section =\n separator +\n \"\\n# KB wiring (local-only)\\n\" +\n toAdd.join(\"\\n\") +\n \"\\n\";\n\n await fs.appendFile(gitignorePath, section);\n log.step(`.gitignore에 ${toAdd.length}개 패턴 추가`);\n}\n","import fs from \"fs-extra\";\nimport path from \"node:path\";\nimport { log } from \"../utils/log.js\";\nimport { ensureGitignorePatterns } from \"../utils/gitignore.js\";\nimport type { WireContext } from \"./wire.js\";\n\nfunction buildGeminiMd(ctx: WireContext): string {\n let content = \"@.gemini/kb-import.md\\n\";\n content += \"\\n## 프로젝트\\n\";\n content += `- ${ctx.techStack}\\n`;\n if (ctx.domains) content += `- 담당: ${ctx.domains}\\n`;\n content += `- 현재 repo: ${ctx.repoName}\\n`;\n\n if (ctx.terms.length > 0) {\n content += \"\\n## 도메인 용어\\n\";\n for (const { term, definition } of ctx.terms) {\n content += `- ${term}: ${definition}\\n`;\n }\n }\n\n return content;\n}\n\nfunction buildKbImportMd(ctx: WireContext): string {\n const aiContext = path.join(ctx.kbPath, \"ai-context\");\n\n let content = `## KB 절대 경로\nLLM은 파일 접근 및 쉘 스크립트 실행 시 반드시 아래의 절대 경로를 그대로 사용할 것.\n- KB 루트: ${ctx.kbPath}\n- 전체 인덱스: ${ctx.kbPath}/kb-index.json\n- 로컬 인덱스: ${ctx.kbPath}/.kb-index.local.json\n- 로컬 인덱스 재생성 스크립트: ${ctx.kbPath}/.kb/scripts/rebuild-index.sh\n\n`;\n\n // @import lines\n content += `@${aiContext}/kb-rules.md\\n`;\n content += `@${aiContext}/team-focus.md\\n`;\n\n // Also import any *-overview.md files\n if (fs.existsSync(aiContext)) {\n const files = fs.readdirSync(aiContext);\n for (const file of files) {\n if (file.endsWith(\"-overview.md\")) {\n content += `@${aiContext}/${file}\\n`;\n }\n }\n }\n\n return content;\n}\n\nexport async function wireGemini(ctx: WireContext): Promise<void> {\n const geminiDir = path.join(ctx.projectRoot, \".gemini\");\n await fs.ensureDir(geminiDir);\n\n // 1. GEMINI.md (tracked — project info + @import)\n const geminiMdPath = path.join(ctx.projectRoot, \"GEMINI.md\");\n if (fs.existsSync(geminiMdPath)) {\n const existing = await fs.readFile(geminiMdPath, \"utf-8\");\n if (!existing.includes(\"## 프로젝트\")) {\n await fs.appendFile(geminiMdPath, \"\\n\" + buildGeminiMd(ctx));\n log.step(\"GEMINI.md에 프로젝트 정보 추가\");\n } else {\n log.step(\"GEMINI.md에 프로젝트 정보 이미 존재 — 스킵\");\n }\n } else {\n await fs.writeFile(geminiMdPath, buildGeminiMd(ctx));\n log.step(\"GEMINI.md 생성\");\n }\n\n // 2. .gemini/kb-import.md (local-only — absolute paths + @imports)\n const importPath = path.join(geminiDir, \"kb-import.md\");\n await fs.writeFile(importPath, buildKbImportMd(ctx));\n log.step(\".gemini/kb-import.md 생성 (KB 경로 + @import)\");\n\n // 3. .gemini/settings.json\n const settingsPath = path.join(geminiDir, \"settings.json\");\n if (!fs.existsSync(settingsPath)) {\n await fs.writeJson(settingsPath, {}, { spaces: 2 });\n log.step(\".gemini/settings.json 생성\");\n }\n\n // 4. Update .gitignore\n await ensureGitignorePatterns(ctx.projectRoot, [\n \".gemini/kb-import.md\",\n ]);\n\n log.success(\"Gemini CLI 연결 완료\");\n}\n","import fs from \"fs-extra\";\nimport path from \"node:path\";\nimport { log } from \"../utils/log.js\";\nimport { ensureGitignorePatterns } from \"../utils/gitignore.js\";\nimport type { WireContext } from \"./wire.js\";\n\nfunction buildAgentsMd(ctx: WireContext): string {\n let content = \"# Project\\n\";\n content += `- ${ctx.techStack}\\n`;\n if (ctx.domains) content += `- 담당: ${ctx.domains}\\n`;\n content += `- 현재 repo: ${ctx.repoName}\\n`;\n content += \"\\n# Knowledge Base\\n\";\n content += \"- 세션 시작 시 반드시 .codex/kb-directive.md 파일을 먼저 읽고 그 안의 경로 지시를 따를 것\\n\";\n\n if (ctx.terms.length > 0) {\n content += \"\\n# 도메인 용어\\n\";\n for (const { term, definition } of ctx.terms) {\n content += `- ${term}: ${definition}\\n`;\n }\n }\n\n return content;\n}\n\nfunction buildDirectiveMd(ctx: WireContext): string {\n const aiContext = path.join(ctx.kbPath, \"ai-context\");\n\n return `## KB 작동 지시\n- 세션 시작 시 반드시 ${aiContext}/kb-rules.md 를 먼저 읽을 것\n\n## KB 절대 경로\nLLM은 파일 접근 및 쉘 스크립트 실행 시 반드시 아래의 절대 경로를 그대로 사용할 것.\n- KB 루트: ${ctx.kbPath}\n- 전체 인덱스: ${ctx.kbPath}/kb-index.json\n- 로컬 인덱스: ${ctx.kbPath}/.kb-index.local.json\n- 로컬 인덱스 재생성 스크립트: ${ctx.kbPath}/.kb/scripts/rebuild-index.sh\n`;\n}\n\nexport async function wireCodex(ctx: WireContext): Promise<void> {\n const codexDir = path.join(ctx.projectRoot, \".codex\");\n await fs.ensureDir(codexDir);\n\n // 1. AGENTS.md (tracked — project info + directive reference)\n const agentsMdPath = path.join(ctx.projectRoot, \"AGENTS.md\");\n if (fs.existsSync(agentsMdPath)) {\n const existing = await fs.readFile(agentsMdPath, \"utf-8\");\n if (!existing.includes(\"# Project\")) {\n await fs.appendFile(agentsMdPath, \"\\n\" + buildAgentsMd(ctx));\n log.step(\"AGENTS.md에 프로젝트 정보 추가\");\n } else {\n log.step(\"AGENTS.md에 프로젝트 정보 이미 존재 — 스킵\");\n }\n } else {\n await fs.writeFile(agentsMdPath, buildAgentsMd(ctx));\n log.step(\"AGENTS.md 생성\");\n }\n\n // 2. .codex/kb-directive.md (local-only — absolute paths)\n const directivePath = path.join(codexDir, \"kb-directive.md\");\n await fs.writeFile(directivePath, buildDirectiveMd(ctx));\n log.step(\".codex/kb-directive.md 생성 (KB 경로 + 지시)\");\n\n // 3. Update .gitignore\n await ensureGitignorePatterns(ctx.projectRoot, [\n \".codex/kb-directive.md\",\n ]);\n\n log.success(\"Codex CLI 연결 완료\");\n}\n","import fs from \"fs-extra\";\nimport path from \"node:path\";\nimport { log } from \"../utils/log.js\";\nimport { getKbPath, CONFIG_PATH } from \"../utils/config.js\";\n\ninterface CheckResult {\n name: string;\n ok: boolean;\n detail: string;\n}\n\nexport async function doctorCommand(): Promise<void> {\n log.info(\"KB 환경 진단을 시작합니다.\\n\");\n const results: CheckResult[] = [];\n\n // 1. KB config\n const kbPath = getKbPath();\n results.push({\n name: `설정 파일 (${CONFIG_PATH})`,\n ok: !!kbPath,\n detail: kbPath || \"미설정. kb init 또는 kb join을 먼저 실행하세요.\",\n });\n\n if (!kbPath) {\n printResults(results);\n return;\n }\n\n // 2. KB directory exists and is accessible\n const kbExists = fs.existsSync(kbPath);\n results.push({\n name: \"KB 디렉토리 접근\",\n ok: kbExists,\n detail: kbExists ? kbPath : `${kbPath} 경로가 존재하지 않습니다.`,\n });\n\n if (!kbExists) {\n printResults(results);\n return;\n }\n\n // 3. ai-context/kb-rules.md exists\n const kbRulesPath = path.join(kbPath, \"ai-context\", \"kb-rules.md\");\n const kbRulesExists = fs.existsSync(kbRulesPath);\n results.push({\n name: \"ai-context/kb-rules.md\",\n ok: kbRulesExists,\n detail: kbRulesExists ? \"존재\" : \"없음. ai-context/ 초기 파일을 작성하세요.\",\n });\n\n // 4. kb-index.json exists\n const indexPath = path.join(kbPath, \"kb-index.json\");\n const indexExists = fs.existsSync(indexPath);\n results.push({\n name: \"kb-index.json\",\n ok: indexExists,\n detail: indexExists ? \"존재\" : \"없음. git push 후 CI가 생성합니다.\",\n });\n\n // 5. pre-commit hook installed\n const hookPath = path.join(kbPath, \".git\", \"hooks\", \"pre-commit\");\n const hookExists = fs.existsSync(hookPath);\n results.push({\n name: \"pre-commit hook\",\n ok: hookExists,\n detail: hookExists ? \"설치됨\" : \"미설치. kb join을 다시 실행하세요.\",\n });\n\n // 6. Check project wiring (if in a project directory)\n const cwd = process.cwd();\n if (cwd !== kbPath) {\n // Claude\n const claudeRulesDir = path.join(cwd, \".claude\", \"rules\");\n if (fs.existsSync(claudeRulesDir)) {\n const kbRulesLink = path.join(claudeRulesDir, \"kb-rules.md\");\n if (fs.existsSync(kbRulesLink)) {\n const stat = await fs.lstat(kbRulesLink);\n if (stat.isSymbolicLink()) {\n const target = await fs.readlink(kbRulesLink);\n const targetValid = fs.existsSync(target);\n results.push({\n name: \"Claude symlink (kb-rules.md)\",\n ok: targetValid,\n detail: targetValid ? `→ ${target}` : `깨진 symlink → ${target}`,\n });\n }\n }\n\n const locationFile = path.join(claudeRulesDir, \"kb-location.md\");\n results.push({\n name: \"Claude kb-location.md\",\n ok: fs.existsSync(locationFile),\n detail: fs.existsSync(locationFile) ? \"존재\" : \"없음. kb wire claude를 실행하세요.\",\n });\n }\n\n // Gemini\n const geminiImport = path.join(cwd, \".gemini\", \"kb-import.md\");\n if (fs.existsSync(path.join(cwd, \"GEMINI.md\"))) {\n results.push({\n name: \"Gemini kb-import.md\",\n ok: fs.existsSync(geminiImport),\n detail: fs.existsSync(geminiImport) ? \"존재\" : \"없음. kb wire gemini를 실행하세요.\",\n });\n }\n\n // Codex\n const codexDirective = path.join(cwd, \".codex\", \"kb-directive.md\");\n if (fs.existsSync(path.join(cwd, \"AGENTS.md\"))) {\n results.push({\n name: \"Codex kb-directive.md\",\n ok: fs.existsSync(codexDirective),\n detail: fs.existsSync(codexDirective) ? \"존재\" : \"없음. kb wire codex를 실행하세요.\",\n });\n }\n }\n\n printResults(results);\n}\n\nfunction printResults(results: CheckResult[]): void {\n let allOk = true;\n for (const r of results) {\n if (r.ok) {\n log.success(`${r.name}: ${r.detail}`);\n } else {\n log.error(`${r.name}: ${r.detail}`);\n allOk = false;\n }\n }\n\n console.log();\n if (allOk) {\n log.success(\"모든 검증 통과\");\n } else {\n log.warn(\"일부 항목에 문제가 있습니다. 위 메시지를 확인하세요.\");\n }\n}\n","import { confirm } from \"@inquirer/prompts\";\nimport fs from \"fs-extra\";\nimport path from \"node:path\";\nimport { log } from \"../utils/log.js\";\nimport { getKbPath, removeConfig, CONFIG_PATH } from \"../utils/config.js\";\n\nexport async function uninstallCommand(): Promise<void> {\n log.info(\"KB wiring 제거를 시작합니다.\\n\");\n\n const cwd = process.cwd();\n let removed = 0;\n\n // 1. Remove Claude wiring (local-only files only)\n const claudeRulesDir = path.join(cwd, \".claude\", \"rules\");\n if (fs.existsSync(claudeRulesDir)) {\n // Remove symlinks (KB-related only)\n const files = await fs.readdir(claudeRulesDir);\n for (const file of files) {\n const filePath = path.join(claudeRulesDir, file);\n const stat = await fs.lstat(filePath);\n if (stat.isSymbolicLink()) {\n await fs.remove(filePath);\n log.step(`symlink 삭제: .claude/rules/${file}`);\n removed++;\n }\n }\n\n // Remove kb-location.md\n const locationPath = path.join(claudeRulesDir, \"kb-location.md\");\n if (fs.existsSync(locationPath)) {\n await fs.remove(locationPath);\n log.step(\"삭제: .claude/rules/kb-location.md\");\n removed++;\n }\n }\n\n // 2. Remove Gemini wiring (local-only file only)\n const geminiImport = path.join(cwd, \".gemini\", \"kb-import.md\");\n if (fs.existsSync(geminiImport)) {\n await fs.remove(geminiImport);\n log.step(\"삭제: .gemini/kb-import.md\");\n removed++;\n }\n\n // 3. Remove Codex wiring (local-only file only)\n const codexDirective = path.join(cwd, \".codex\", \"kb-directive.md\");\n if (fs.existsSync(codexDirective)) {\n await fs.remove(codexDirective);\n log.step(\"삭제: .codex/kb-directive.md\");\n removed++;\n }\n\n if (removed > 0) {\n log.success(`프로젝트 wiring ${removed}개 파일 제거 완료`);\n } else {\n log.info(\"현재 디렉토리에 제거할 wiring 파일이 없습니다.\");\n }\n\n // 4. Tracked files (CLAUDE.md, GEMINI.md, AGENTS.md) — don't touch\n const trackedFiles = [\"CLAUDE.md\", \"GEMINI.md\", \"AGENTS.md\"].filter(\n (f) => fs.existsSync(path.join(cwd, f)),\n );\n if (trackedFiles.length > 0) {\n log.warn(\n `${trackedFiles.join(\", \")}의 프로젝트 섹션은 수동으로 정리하세요 (사용자 내용과 섞여있을 수 있음).`,\n );\n }\n\n // 5. Remove config file\n const kbPath = getKbPath();\n const shouldRemoveConfig = await confirm({\n message: `설정 파일을 삭제할까요? (${CONFIG_PATH})`,\n default: true,\n });\n\n if (shouldRemoveConfig) {\n const didRemove = await removeConfig();\n if (didRemove) {\n log.step(\"설정 파일 삭제 완료\");\n }\n }\n\n // 6. KB directory — ask but warn\n if (kbPath && fs.existsSync(kbPath)) {\n const shouldRemoveKb = await confirm({\n message: `KB 디렉토리를 삭제할까요? (${kbPath}) — 팀 지식이 모두 삭제됩니다!`,\n default: false,\n });\n\n if (shouldRemoveKb) {\n await fs.remove(kbPath);\n log.step(`KB 디렉토리 삭제: ${kbPath}`);\n } else {\n log.info(`KB 디렉토리 유지: ${kbPath}`);\n }\n }\n\n console.log();\n log.success(\"uninstall 완료\");\n log.info(\"npm uninstall -g kb-cli 로 CLI 자체도 제거할 수 있습니다.\");\n}\n"],"mappings":"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","names":["fs","path","fs","fs","path","fs","input","fs","path","path","input","fs","input","fs","path","fs","path","fs","path","fs","path","path","fs","path","fs","fs","path","path","fs","fs","path","path","fs","input","fs","path","fs","path","fs","path","path","fs"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@01b/team-kb",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Team Knowledge Base harness CLI - scaffold, wire, and manage KB for LLM-assisted development",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"kb": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsup",
|
|
11
|
+
"dev": "tsup --watch",
|
|
12
|
+
"test": "vitest",
|
|
13
|
+
"lint": "tsc --noEmit"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@inquirer/prompts": "^7.0.0",
|
|
20
|
+
"commander": "^13.0.0",
|
|
21
|
+
"fs-extra": "^11.0.0",
|
|
22
|
+
"picocolors": "^1.0.0",
|
|
23
|
+
"simple-git": "^3.0.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/fs-extra": "^11.0.0",
|
|
27
|
+
"@types/node": "^20.0.0",
|
|
28
|
+
"tsup": "^8.0.0",
|
|
29
|
+
"typescript": "^5.0.0",
|
|
30
|
+
"vitest": "^2.0.0"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"templates"
|
|
35
|
+
],
|
|
36
|
+
"license": "MIT"
|
|
37
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
name: KB Coverage Report
|
|
2
|
+
on:
|
|
3
|
+
schedule:
|
|
4
|
+
- cron: '0 9 * * 1'
|
|
5
|
+
|
|
6
|
+
jobs:
|
|
7
|
+
coverage:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
steps:
|
|
10
|
+
- uses: actions/checkout@v4
|
|
11
|
+
|
|
12
|
+
- name: 폴더별 노트 수
|
|
13
|
+
run: |
|
|
14
|
+
echo "=== 폴더별 노트 수 ==="
|
|
15
|
+
for dir in global rules analysis decisions troubleshoot drafts areas; do
|
|
16
|
+
COUNT=$(find "./$dir" -name '*.md' 2>/dev/null | wc -l)
|
|
17
|
+
echo " $dir: ${COUNT}개"
|
|
18
|
+
done
|
|
19
|
+
|
|
20
|
+
- name: 태그별 노트 수
|
|
21
|
+
run: |
|
|
22
|
+
echo "=== 태그별 노트 수 ==="
|
|
23
|
+
grep -roh 'tags:.*' --include='*.md' . | \
|
|
24
|
+
sed 's/tags://;s/\[//;s/\]//;s/,/\n/g' | \
|
|
25
|
+
sed 's/^ *//;s/ *$//' | sort | uniq -c | sort -rn
|
|
26
|
+
|
|
27
|
+
- name: Stale 노트 탐지 (90일 이상 미갱신)
|
|
28
|
+
run: |
|
|
29
|
+
echo "=== Stale 후보 (90일+ 미갱신) ==="
|
|
30
|
+
THRESHOLD=$(date -d '90 days ago' +%Y-%m-%d 2>/dev/null || date -v-90d +%Y-%m-%d)
|
|
31
|
+
for f in $(find . -name '*.md' \
|
|
32
|
+
-not -path './.kb/*' -not -path './templates/*' \
|
|
33
|
+
-not -path './99-personal/*' -not -path './00-inbox/*' \
|
|
34
|
+
-not -path './.github/*' -not -path './archive/*'); do
|
|
35
|
+
UPDATED=$(grep -m1 'updated:' "$f" 2>/dev/null | sed 's/updated: *//' | tr -d ' ')
|
|
36
|
+
if [ -n "$UPDATED" ] && [ "$UPDATED" \< "$THRESHOLD" ]; then
|
|
37
|
+
echo " STALE: $f (updated: $UPDATED)"
|
|
38
|
+
fi
|
|
39
|
+
done
|
|
40
|
+
|
|
41
|
+
- name: ai-context/ 오버뷰 신선도 체크 (30일)
|
|
42
|
+
run: |
|
|
43
|
+
echo "=== 오버뷰 신선도 ==="
|
|
44
|
+
THRESHOLD=$(date -d '30 days ago' +%Y-%m-%d 2>/dev/null || date -v-30d +%Y-%m-%d)
|
|
45
|
+
for f in $(find ./ai-context -name '*.md' 2>/dev/null); do
|
|
46
|
+
UPDATED=$(grep -m1 'updated:' "$f" 2>/dev/null | sed 's/updated: *//' | tr -d ' ')
|
|
47
|
+
if [ -n "$UPDATED" ] && [ "$UPDATED" \< "$THRESHOLD" ]; then
|
|
48
|
+
echo " WARNING: $f 오버뷰가 30일 이상 미갱신 (updated: $UPDATED)"
|
|
49
|
+
fi
|
|
50
|
+
done
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
name: KB Health Check
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
schedule:
|
|
6
|
+
- cron: '0 9 * * 1'
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
group: kb-index-update
|
|
10
|
+
cancel-in-progress: true
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
validate:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: frontmatter 검증
|
|
19
|
+
run: |
|
|
20
|
+
ERRORS=0
|
|
21
|
+
for f in $(find . -name '*.md' \
|
|
22
|
+
-not -path './.kb/*' \
|
|
23
|
+
-not -path './templates/*' \
|
|
24
|
+
-not -path './99-personal/*' \
|
|
25
|
+
-not -path './00-inbox/*' \
|
|
26
|
+
-not -path './.github/*'); do
|
|
27
|
+
if ! head -1 "$f" | grep -q '^---'; then
|
|
28
|
+
echo "MISSING frontmatter: $f"; ERRORS=$((ERRORS+1))
|
|
29
|
+
fi
|
|
30
|
+
if ! grep -q 'tldr:' "$f"; then
|
|
31
|
+
echo "MISSING tldr: $f"; ERRORS=$((ERRORS+1))
|
|
32
|
+
fi
|
|
33
|
+
done
|
|
34
|
+
if [ $ERRORS -gt 0 ]; then exit 1; fi
|
|
35
|
+
|
|
36
|
+
- name: 민감 정보 검사
|
|
37
|
+
run: |
|
|
38
|
+
if grep -rn 'password\|secret\|jdbc:\|api[_-]key\|token=' --include='*.md' \
|
|
39
|
+
--exclude-dir=.kb --exclude-dir=templates .; then
|
|
40
|
+
echo "ERROR: 민감 정보 포함"; exit 1
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
- name: 빈 노트 탐지
|
|
44
|
+
run: |
|
|
45
|
+
find . -name '*.md' -not -path './.kb/*' -not -path './templates/*' \
|
|
46
|
+
-size -50c -exec echo "거의 빈 파일: {}" \;
|
|
47
|
+
|
|
48
|
+
- name: kb-index.json 갱신
|
|
49
|
+
run: |
|
|
50
|
+
echo "[" > kb-index.json
|
|
51
|
+
FIRST=true
|
|
52
|
+
for f in $(find . -name '*.md' \
|
|
53
|
+
-not -path './.kb/*' -not -path './templates/*' \
|
|
54
|
+
-not -path './99-personal/*' -not -path './00-inbox/*' \
|
|
55
|
+
-not -path './.github/*' -not -name 'README.md'); do
|
|
56
|
+
TAGS=$(grep -m1 'tags:' "$f" 2>/dev/null | sed 's/tags: *//')
|
|
57
|
+
TLDR=$(grep -m1 'tldr:' "$f" 2>/dev/null | sed 's/tldr: *//')
|
|
58
|
+
UPDATED=$(grep -m1 'updated:' "$f" 2>/dev/null | sed 's/updated: *//')
|
|
59
|
+
if [ "$FIRST" = true ]; then FIRST=false; else echo "," >> kb-index.json; fi
|
|
60
|
+
printf ' {"path":"%s","tags":%s,"tldr":%s,"updated":"%s"}' \
|
|
61
|
+
"$f" "${TAGS:-[]}" "${TLDR:-\"\"}" "${UPDATED:-unknown}" >> kb-index.json
|
|
62
|
+
done
|
|
63
|
+
echo "" >> kb-index.json
|
|
64
|
+
echo "]" >> kb-index.json
|
|
65
|
+
|
|
66
|
+
- name: kb-index.json 커밋
|
|
67
|
+
run: |
|
|
68
|
+
git config user.name "kb-bot"
|
|
69
|
+
git config user.email "kb-bot@noreply"
|
|
70
|
+
git add kb-index.json
|
|
71
|
+
git diff --cached --quiet || (git commit -m "chore: update kb-index.json" && git pull --rebase origin main && git push)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
tags: [kb-rules]
|
|
3
|
+
tldr: "LLM이 KB를 어떻게 검색/기록/참조할지 정의하는 팀 공통 규칙"
|
|
4
|
+
updated: {{date}}
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## KB 검색 규칙
|
|
8
|
+
- KB 절대 경로는 이 세션에 자동 로딩된 KB 경로 설정을 참조할 것
|
|
9
|
+
- 검색 시 인덱스를 먼저 읽어 관련 노트를 특정할 것
|
|
10
|
+
- kb-index.json을 읽기 (CI가 관리하는 정본)
|
|
11
|
+
- .kb-index.local.json이 존재하면 함께 읽기 (로컬 최신 상태)
|
|
12
|
+
- 두 인덱스의 항목을 합집합(Union)으로 병합하여 검색
|
|
13
|
+
- 인덱스의 tags, tldr로 관련성을 판단하고, 필요한 노트만 전체 읽기
|
|
14
|
+
- 현재 repo 관련 노트 우선: `repo:` 태그 또는 repo 태그 없는 공통 노트
|
|
15
|
+
|
|
16
|
+
## KB 기록 규칙
|
|
17
|
+
- 코딩/분석 중 새로운 비즈니스 룰, 코드 패턴, 장애 원인을 발견하면 KB에 기록 제안
|
|
18
|
+
- 기록 전에 반드시 KB 내 관련 노트를 먼저 검색할 것 (중복 방지)
|
|
19
|
+
- 유사한 노트가 있으면 새로 만들지 말고 기존 노트에 추가할 것
|
|
20
|
+
- 저장 전 사용자에게 경로/파일명을 보여주고 확인받을 것
|
|
21
|
+
- 폴더 선택 기준:
|
|
22
|
+
- 비즈니스 룰 → rules/
|
|
23
|
+
- 코드 분석 → analysis/
|
|
24
|
+
- 의사결정 → decisions/
|
|
25
|
+
- 장애 대응 → troubleshoot/
|
|
26
|
+
- 확실하지 않으면 → drafts/
|
|
27
|
+
- frontmatter 필수: tags, tldr, created, updated
|
|
28
|
+
- 템플릿(templates/)의 형식을 따를 것
|
|
29
|
+
- 노트를 생성/수정/이동한 직후, rebuild-index.sh를 실행하여 로컬 인덱스(.kb-index.local.json)를 최신화할 것
|
|
30
|
+
|
|
31
|
+
## KB 갭 인식
|
|
32
|
+
- KB 검색 결과 관련 노트가 없으면 사용자에게 알릴 것
|
|
33
|
+
- 코딩 중 해당 주제에 대한 정보를 파악하게 되면 KB 기록을 제안할 것
|
|
34
|
+
- 사용자가 "KB에서 찾아줘" 또는 "KB에 기록해줘"라고 말하면 해당 동작을 즉시 수행할 것
|
|
35
|
+
|
|
36
|
+
## KB 신뢰도
|
|
37
|
+
- global/: 전사 규칙, 최우선 적용
|
|
38
|
+
- rules/, decisions/: 팀 검증됨, 신뢰 가능
|
|
39
|
+
- analysis/, troubleshoot/: 분석 결과, 대체로 신뢰 가능
|
|
40
|
+
- drafts/: 미검증, 참고만
|
|
41
|
+
- archive/: 비활성, 최신 정보가 아닐 수 있음
|
|
42
|
+
|
|
43
|
+
## KB 보안
|
|
44
|
+
- KB에 credential, 접속 정보, API 키를 절대 포함하지 말 것
|
|
45
|
+
|
|
46
|
+
## KB 예외 처리
|
|
47
|
+
- git commit/push 시 pre-commit hook이 실패하면 재시도하지 말 것
|
|
48
|
+
- hook 실패 원인(민감정보 포함, frontmatter 누락 등)을 사용자에게 보고하고 수정 지시를 기다릴 것
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# KB pre-commit hook: frontmatter validation + secret detection
|
|
3
|
+
ERRORS=0
|
|
4
|
+
for f in $(git diff --cached --name-only --diff-filter=ACM -- '*.md'); do
|
|
5
|
+
case "$f" in .kb/*|templates/*|99-personal/*|00-inbox/*) continue;; esac
|
|
6
|
+
|
|
7
|
+
if ! head -1 "$f" | grep -q '^---'; then
|
|
8
|
+
echo "ERROR: $f — frontmatter 없음"; ERRORS=$((ERRORS+1))
|
|
9
|
+
fi
|
|
10
|
+
if ! grep -q 'tldr:' "$f"; then
|
|
11
|
+
echo "ERROR: $f — tldr 필드 없음"; ERRORS=$((ERRORS+1))
|
|
12
|
+
fi
|
|
13
|
+
if ! grep -q 'tags:' "$f"; then
|
|
14
|
+
echo "ERROR: $f — tags 필드 없음"; ERRORS=$((ERRORS+1))
|
|
15
|
+
fi
|
|
16
|
+
done
|
|
17
|
+
|
|
18
|
+
# Secret detection — gitleaks first, regex fallback
|
|
19
|
+
if command -v gitleaks &> /dev/null; then
|
|
20
|
+
gitleaks protect --staged --no-banner
|
|
21
|
+
if [ $? -ne 0 ]; then
|
|
22
|
+
echo "ERROR: gitleaks가 민감 정보를 탐지했습니다."; ERRORS=$((ERRORS+1))
|
|
23
|
+
fi
|
|
24
|
+
else
|
|
25
|
+
if git diff --cached --name-only --diff-filter=ACM -- '*.md' | xargs grep -l 'password\|secret\|jdbc:\|api[_-]key\|token=' 2>/dev/null; then
|
|
26
|
+
echo "ERROR: 민감 정보가 포함된 파일이 있습니다."; ERRORS=$((ERRORS+1))
|
|
27
|
+
echo "TIP: brew install gitleaks 로 더 정밀한 탐지를 사용할 수 있습니다."
|
|
28
|
+
fi
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
if [ $ERRORS -gt 0 ]; then exit 1; fi
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Rebuild local KB index (.kb-index.local.json)
|
|
3
|
+
KB_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
4
|
+
cd "$KB_ROOT"
|
|
5
|
+
|
|
6
|
+
LOCAL_INDEX=".kb-index.local.json"
|
|
7
|
+
echo "[" > "$LOCAL_INDEX"
|
|
8
|
+
FIRST=true
|
|
9
|
+
for f in $(find . -name '*.md' \
|
|
10
|
+
-not -path './.kb/*' -not -path './templates/*' \
|
|
11
|
+
-not -path './99-personal/*' -not -path './00-inbox/*' \
|
|
12
|
+
-not -path './.github/*' -not -name 'README.md'); do
|
|
13
|
+
TAGS=$(grep -m1 'tags:' "$f" 2>/dev/null | sed 's/tags: *//')
|
|
14
|
+
TLDR=$(grep -m1 'tldr:' "$f" 2>/dev/null | sed 's/tldr: *//')
|
|
15
|
+
UPDATED=$(grep -m1 'updated:' "$f" 2>/dev/null | sed 's/updated: *//')
|
|
16
|
+
if [ "$FIRST" = true ]; then FIRST=false; else echo "," >> "$LOCAL_INDEX"; fi
|
|
17
|
+
printf ' {"path":"%s","tags":%s,"tldr":%s,"updated":"%s"}' \
|
|
18
|
+
"$f" "${TAGS:-[]}" "${TLDR:-\"\"}" "${UPDATED:-unknown}" >> "$LOCAL_INDEX"
|
|
19
|
+
done
|
|
20
|
+
echo "" >> "$LOCAL_INDEX"
|
|
21
|
+
echo "]" >> "$LOCAL_INDEX"
|
|
22
|
+
echo ".kb-index.local.json 재생성 완료 ($(grep -c '"path"' "$LOCAL_INDEX")개 노트)"
|