@taejung3852/project-scaffold 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 (35) hide show
  1. package/.hooks/convention-check.sh +79 -0
  2. package/.hooks/devlog-auto.sh +55 -0
  3. package/.obsidian-template/appearance.json +6 -0
  4. package/.obsidian-template/core-plugins.json +20 -0
  5. package/.obsidian-template/graph.json +22 -0
  6. package/.obsidian-template/snippets/folder-colors.css +55 -0
  7. package/AGENT.md +55 -0
  8. package/README.kr.md +247 -0
  9. package/README.md +247 -0
  10. package/SOUL.md +54 -0
  11. package/THIRD_PARTY_NOTICES.md +14 -0
  12. package/bin/project-scaffold.js +122 -0
  13. package/lib/agents-registry.js +197 -0
  14. package/lib/agents.js +203 -0
  15. package/lib/install.js +342 -0
  16. package/package.json +33 -0
  17. package/skills/brief/SKILL.md +63 -0
  18. package/skills/brief/agents/openai.yaml +4 -0
  19. package/skills/brief/references/dashboard.md +140 -0
  20. package/skills/brief/references/handoff.md +130 -0
  21. package/skills/brief/references/report.md +163 -0
  22. package/skills/review/SKILL.md +123 -0
  23. package/skills/review/agents/openai.yaml +4 -0
  24. package/skills/wiki/SKILL.md +73 -0
  25. package/skills/wiki/agents/openai.yaml +4 -0
  26. package/skills/wiki/references/ambiguity-check.md +16 -0
  27. package/skills/wiki/references/capture.md +119 -0
  28. package/skills/wiki/references/clear-path.md +82 -0
  29. package/skills/wiki/references/devlog.md +111 -0
  30. package/skills/wiki/references/ingest.md +118 -0
  31. package/skills/wiki/references/lint.md +83 -0
  32. package/skills/wiki/references/query.md +81 -0
  33. package/skills/wiki/references/setup.md +397 -0
  34. package/skills/wiki/references/unclear-path.md +51 -0
  35. package/templates/gitignore +14 -0
package/lib/agents.js ADDED
@@ -0,0 +1,203 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+ import readline from "node:readline/promises";
5
+ import {
6
+ agentRegistry,
7
+ detectAgents,
8
+ getAgent,
9
+ normalizeAgentId,
10
+ registrySource,
11
+ supportedAgentIds,
12
+ uniqueProjectSkillDirs,
13
+ } from "./agents-registry.js";
14
+
15
+ export const supportedAgents = supportedAgentIds;
16
+
17
+ function writeFileSafely(file, content, force, managedFiles) {
18
+ if (fs.existsSync(file) && !force) return false;
19
+ fs.mkdirSync(path.dirname(file), { recursive: true });
20
+ fs.writeFileSync(file, content);
21
+ managedFiles?.add(file);
22
+ return true;
23
+ }
24
+
25
+ function existingEntry(file) {
26
+ try {
27
+ return fs.lstatSync(file);
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ function linkSkill(skillDir, link, force, managedFiles) {
34
+ fs.mkdirSync(path.dirname(link), { recursive: true });
35
+ const existing = existingEntry(link);
36
+ if (existing) {
37
+ if (!existing.isSymbolicLink() && !force) return false;
38
+ fs.rmSync(link, { recursive: true, force: true });
39
+ }
40
+ const relativeTarget = path.relative(path.dirname(link), skillDir) || ".";
41
+ fs.symlinkSync(relativeTarget, link, "dir");
42
+ managedFiles?.add(link);
43
+ return true;
44
+ }
45
+
46
+ function skillEntries(target) {
47
+ const root = path.join(target, "skills");
48
+ if (!fs.existsSync(root)) throw new Error("skills/가 없습니다. init 또는 add를 먼저 실행하세요.");
49
+ return fs
50
+ .readdirSync(root, { withFileTypes: true })
51
+ .filter((entry) => entry.isDirectory() && fs.existsSync(path.join(root, entry.name, "SKILL.md")))
52
+ .map((entry) => ({ name: entry.name, dir: path.join(root, entry.name) }));
53
+ }
54
+
55
+ function setupSkillDirectory(target, folder, force, managedFiles) {
56
+ if (folder === "skills") return;
57
+ for (const skill of skillEntries(target)) {
58
+ linkSkill(skill.dir, path.join(target, folder, skill.name), force, managedFiles);
59
+ }
60
+ }
61
+
62
+ function setupAgentsWrapper(target, force, managedFiles) {
63
+ writeFileSafely(path.join(target, "AGENTS.md"), "@AGENT.md\n", force, managedFiles);
64
+ }
65
+
66
+ function setupHermesContext(target, force, managedFiles) {
67
+ writeFileSafely(path.join(target, ".hermes.md"), "@AGENT.md\n", force, managedFiles);
68
+ writeFileSafely(
69
+ path.join(target, "cli-config.yaml"),
70
+ "terminal:\n docker_mount_cwd_to_workspace: true\n",
71
+ force,
72
+ managedFiles,
73
+ );
74
+ console.log(" ℹ️ Hermes 전역 external_dirs 등록은 환경별 설정이므로 doctor 안내를 확인하세요.");
75
+ }
76
+
77
+ function ensureAdapterGitignore(target, agents, managedFiles) {
78
+ const file = path.join(target, ".gitignore");
79
+ const existing = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
80
+ const existingLines = new Set(existing.split("\n"));
81
+ const additions = uniqueProjectSkillDirs(agents)
82
+ .filter((directory) => directory !== "skills")
83
+ .map((directory) => `${directory.replaceAll("\\", "/").replace(/\/$/, "")}/`)
84
+ .filter((directory) => !existingLines.has(directory));
85
+ if (additions.length === 0) return;
86
+
87
+ const prefix = existing.length > 0 ? `${existing.trimEnd()}\n\n` : "";
88
+ fs.writeFileSync(file, `${prefix}# project-scaffold agent adapters\n${additions.join("\n")}\n`);
89
+ managedFiles?.add(file);
90
+ }
91
+
92
+ export function normalizeAgents(values) {
93
+ const normalized = values.map((value) => value.trim().toLowerCase()).filter(Boolean);
94
+ const expanded = normalized.includes("all") || normalized.includes("*") ? supportedAgentIds : normalized;
95
+ const unique = [...new Set(expanded.map(normalizeAgentId))];
96
+ const unknown = unique.filter((agent) => !getAgent(agent));
97
+ if (unknown.length > 0) {
98
+ throw new Error(`지원하지 않는 에이전트: ${unknown.join(", ")}\n'project-scaffold agents'로 전체 목록을 확인하세요.`);
99
+ }
100
+ return unique;
101
+ }
102
+
103
+ export function registryLines() {
104
+ const groups = new Map();
105
+ for (const agent of Object.values(agentRegistry)) {
106
+ const ids = groups.get(agent.projectSkillsDir) ?? [];
107
+ ids.push(agent.id);
108
+ groups.set(agent.projectSkillsDir, ids);
109
+ }
110
+ const lines = [
111
+ `지원 에이전트 ${supportedAgentIds.length}개 — skills@${registrySource.version} 기준`,
112
+ "",
113
+ ];
114
+ for (const [directory, ids] of [...groups.entries()].sort(([a], [b]) => a.localeCompare(b))) {
115
+ lines.push(`${directory}: ${ids.sort().join(", ")}`);
116
+ }
117
+ return lines;
118
+ }
119
+
120
+ export async function chooseAgents(values, yes = false, target = process.cwd()) {
121
+ if (values.length > 0) return normalizeAgents(values);
122
+
123
+ const detected = detectAgents({ target });
124
+ const defaults = detected.length > 0 ? detected : ["universal"];
125
+ if (yes || !process.stdin.isTTY) return defaults;
126
+
127
+ if (detected.length > 0) {
128
+ console.log(`\n감지된 에이전트: ${detected.join(", ")}`);
129
+ } else {
130
+ console.log("\n자동 감지된 에이전트가 없습니다.");
131
+ console.log("추천: claude-code, codex, cursor, gemini-cli, github-copilot, opencode, windsurf, universal");
132
+ }
133
+ console.log("쉼표로 복수 선택할 수 있습니다. 전체 목록은 'list', 모두 설치는 'all'입니다.");
134
+
135
+ const terminal = readline.createInterface({ input: process.stdin, output: process.stdout });
136
+ let answer = await terminal.question(`선택 [${defaults.join(",")}]: `);
137
+ if (answer.trim().toLowerCase() === "list") {
138
+ console.log(`\n${registryLines().join("\n")}\n`);
139
+ answer = await terminal.question(`선택 [${defaults.join(",")}]: `);
140
+ }
141
+ terminal.close();
142
+ return normalizeAgents((answer || defaults.join(",")).split(","));
143
+ }
144
+
145
+ export function configureAgents({ target, agents, force = false, managedFiles = new Set() }) {
146
+ const normalized = normalizeAgents(agents);
147
+ for (const directory of uniqueProjectSkillDirs(normalized)) {
148
+ console.log(` → ${directory}`);
149
+ setupSkillDirectory(target, directory, force, managedFiles);
150
+ }
151
+
152
+ setupAgentsWrapper(target, force, managedFiles);
153
+ if (normalized.includes("claude-code")) {
154
+ writeFileSafely(path.join(target, "CLAUDE.md"), "@AGENT.md\n", force, managedFiles);
155
+ }
156
+ if (normalized.includes("hermes-agent")) setupHermesContext(target, force, managedFiles);
157
+ ensureAdapterGitignore(target, normalized, managedFiles);
158
+ return managedFiles;
159
+ }
160
+
161
+ export function agentChecks(target, agents) {
162
+ const normalized = normalizeAgents(agents);
163
+ const checks = uniqueProjectSkillDirs(normalized).map((directory) => ({
164
+ label: `skills 경로 ${directory}`,
165
+ ok: fs.existsSync(path.join(target, directory)),
166
+ }));
167
+ checks.push({ label: "AGENTS.md wrapper", ok: fs.existsSync(path.join(target, "AGENTS.md")) });
168
+ if (normalized.includes("claude-code")) {
169
+ checks.push({ label: "CLAUDE.md wrapper", ok: fs.existsSync(path.join(target, "CLAUDE.md")) });
170
+ }
171
+ if (normalized.includes("hermes-agent")) {
172
+ checks.push({ label: "Hermes context", ok: fs.existsSync(path.join(target, ".hermes.md")) });
173
+ }
174
+ return checks;
175
+ }
176
+
177
+ export async function addAgents(options) {
178
+ const target = path.resolve(options.target ?? process.cwd());
179
+ if (!fs.existsSync(path.join(target, ".git"))) throw new Error(`${target}은 Git 저장소가 아닙니다.`);
180
+ const agents = await chooseAgents(options.agents ?? [], options.yes, target);
181
+ const manifestFile = path.join(target, ".project-scaffold", "manifest.json");
182
+ let manifest = {};
183
+ if (fs.existsSync(manifestFile)) {
184
+ try {
185
+ manifest = JSON.parse(fs.readFileSync(manifestFile, "utf8"));
186
+ } catch {
187
+ throw new Error(`설치 manifest가 손상되었습니다: ${manifestFile}`);
188
+ }
189
+ }
190
+ const managedFiles = configureAgents({ target, agents, force: options.force });
191
+ manifest.agents = [...new Set([...(manifest.agents ?? []).map(normalizeAgentId), ...agents])];
192
+ manifest.agentRegistry = registrySource;
193
+ manifest.updatedAt = new Date().toISOString();
194
+ manifest.managedFiles = [
195
+ ...new Set([
196
+ ...(manifest.managedFiles ?? []),
197
+ ...[...managedFiles].map((file) => path.relative(target, file)),
198
+ ]),
199
+ ].sort();
200
+ fs.mkdirSync(path.dirname(manifestFile), { recursive: true });
201
+ fs.writeFileSync(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`);
202
+ console.log(`\n✅ 에이전트 설정 완료: ${agents.join(", ")}`);
203
+ }
package/lib/install.js ADDED
@@ -0,0 +1,342 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+ import { fileURLToPath } from "node:url";
6
+ import { agentChecks, chooseAgents, configureAgents } from "./agents.js";
7
+ import { registrySource } from "./agents-registry.js";
8
+
9
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10
+ const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
11
+ const scaffoldDirectories = [".hooks", ".obsidian-template"];
12
+ const scaffoldFiles = ["AGENT.md", "SOUL.md"];
13
+ const wikiDirectories = ["conventions", "decisions", "devlog", "meetings", "synthesis", "sources"];
14
+ const rawDirectories = ["meetings", "decisions", "dev-logs", "ideas"];
15
+ const legacySkillNames = [
16
+ "setup",
17
+ "capture",
18
+ "devlog",
19
+ "ingest",
20
+ "query",
21
+ "code-lint",
22
+ "wiki-lint",
23
+ "dashboard",
24
+ "curate",
25
+ "report",
26
+ "help",
27
+ "handoff",
28
+ ];
29
+ const legacyEvolutionFiles = [
30
+ "skills/.usage.json",
31
+ "scripts/skill_usage.py",
32
+ "scripts/curator.py",
33
+ "scripts/prompt_builder.py",
34
+ "scripts/skill_manager.py",
35
+ ];
36
+ const currentSkillNames = ["wiki", "brief", "review"];
37
+
38
+ function relative(target, file) {
39
+ return path.relative(target, file);
40
+ }
41
+
42
+ function copyTree(source, destination, force, target, managedFiles) {
43
+ if (!fs.existsSync(source)) return;
44
+ fs.mkdirSync(destination, { recursive: true });
45
+ for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
46
+ const sourcePath = path.join(source, entry.name);
47
+ const destinationPath = path.join(destination, entry.name);
48
+ if (entry.isSymbolicLink()) continue;
49
+ if (entry.isDirectory()) {
50
+ copyTree(sourcePath, destinationPath, force, target, managedFiles);
51
+ } else if (entry.isFile() && (force || !fs.existsSync(destinationPath))) {
52
+ fs.copyFileSync(sourcePath, destinationPath);
53
+ managedFiles.add(relative(target, destinationPath));
54
+ }
55
+ }
56
+ }
57
+
58
+ function copyFile(source, destination, force, target, managedFiles) {
59
+ if (!fs.existsSync(source) || (fs.existsSync(destination) && !force)) return;
60
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
61
+ fs.copyFileSync(source, destination);
62
+ managedFiles.add(relative(target, destination));
63
+ }
64
+
65
+ function copySkills(target, force, managedFiles) {
66
+ const sourceRoot = path.join(packageRoot, "skills");
67
+ const destinationRoot = path.join(target, "skills");
68
+ for (const entry of fs.readdirSync(sourceRoot, { withFileTypes: true })) {
69
+ if (!entry.isDirectory()) continue;
70
+ const source = path.join(sourceRoot, entry.name);
71
+ if (!fs.existsSync(path.join(source, "SKILL.md"))) continue;
72
+ copyTree(source, path.join(destinationRoot, entry.name), force, target, managedFiles);
73
+ }
74
+ }
75
+
76
+ function activeLegacySkills(target) {
77
+ return legacySkillNames.filter((name) => fs.existsSync(path.join(target, "skills", name, "SKILL.md")));
78
+ }
79
+
80
+ function pathExistsWithoutFollowing(file) {
81
+ try {
82
+ fs.lstatSync(file);
83
+ return true;
84
+ } catch {
85
+ return false;
86
+ }
87
+ }
88
+
89
+ function legacyArtifacts(target) {
90
+ const files = [...legacyEvolutionFiles];
91
+ for (const name of currentSkillNames) {
92
+ files.push(`.cursor/rules/${name}.mdc`, `.continue/prompts/${name}.md`);
93
+ }
94
+ for (const name of legacySkillNames) {
95
+ files.push(
96
+ `skills/${name}/SKILL.md`,
97
+ `.claude/skills/${name}`,
98
+ `.agents/skills/${name}`,
99
+ `.windsurf/skills/${name}`,
100
+ `.hermes/skills/${name}`,
101
+ `.cursor/rules/${name}.mdc`,
102
+ `.continue/prompts/${name}.md`,
103
+ );
104
+ }
105
+ return files.filter((file) => pathExistsWithoutFollowing(path.join(target, file)));
106
+ }
107
+
108
+ function removeLegacySkillState(target, force) {
109
+ const active = activeLegacySkills(target);
110
+ if (active.length > 0 && !force) {
111
+ console.log(` ⚠️ 이전 스킬 감지: ${active.join(", ")}`);
112
+ console.log(" 3스킬 구조로 마이그레이션하려면 --force를 사용하세요.");
113
+ return;
114
+ }
115
+ if (!force) return;
116
+
117
+ for (const name of legacySkillNames) {
118
+ fs.rmSync(path.join(target, "skills", name), { recursive: true, force: true });
119
+ for (const folder of [".claude/skills", ".agents/skills", ".windsurf/skills", ".hermes/skills"]) {
120
+ fs.rmSync(path.join(target, folder, name), { recursive: true, force: true });
121
+ }
122
+ fs.rmSync(path.join(target, ".cursor", "rules", `${name}.mdc`), { force: true });
123
+ fs.rmSync(path.join(target, ".continue", "prompts", `${name}.md`), { force: true });
124
+ }
125
+ for (const file of legacyEvolutionFiles) fs.rmSync(path.join(target, file), { force: true });
126
+
127
+ const aiderConfig = path.join(target, ".aider.conf.yml");
128
+ if (fs.existsSync(aiderConfig)) {
129
+ const legacyLines = new Set(legacySkillNames.map((name) => `skills/${name}/SKILL.md`));
130
+ const cleaned = fs
131
+ .readFileSync(aiderConfig, "utf8")
132
+ .split("\n")
133
+ .filter((line) => !legacyLines.has(line.replace(/^\s*-\s*/, "").trim()))
134
+ .join("\n");
135
+ fs.writeFileSync(aiderConfig, cleaned);
136
+ }
137
+ if (active.length > 0) console.log(` ✅ 이전 스킬 ${active.length}개와 자가 진화 상태 제거`);
138
+ }
139
+
140
+ function removeDeprecatedAdapterState(target, force) {
141
+ if (!force) return;
142
+ for (const name of currentSkillNames) {
143
+ fs.rmSync(path.join(target, ".cursor", "rules", `${name}.mdc`), { force: true });
144
+ fs.rmSync(path.join(target, ".continue", "prompts", `${name}.md`), { force: true });
145
+ }
146
+ }
147
+
148
+ function mergeGitignore(target, managedFiles) {
149
+ const source = path.join(packageRoot, "templates", "gitignore");
150
+ const destination = path.join(target, ".gitignore");
151
+ const existing = fs.existsSync(destination) ? fs.readFileSync(destination, "utf8") : "";
152
+ const sourceLines = fs.readFileSync(source, "utf8").split("\n");
153
+ const additions = sourceLines.filter((line) => line && !line.startsWith("#") && !existing.split("\n").includes(line));
154
+ if (!fs.existsSync(destination)) {
155
+ fs.writeFileSync(destination, fs.readFileSync(source));
156
+ managedFiles.add(".gitignore");
157
+ } else if (additions.length > 0) {
158
+ fs.appendFileSync(destination, `\n# project-scaffold\n${additions.join("\n")}\n`);
159
+ managedFiles.add(".gitignore");
160
+ }
161
+ }
162
+
163
+ function writeIfMissing(file, content, target, managedFiles) {
164
+ if (fs.existsSync(file)) return;
165
+ fs.mkdirSync(path.dirname(file), { recursive: true });
166
+ fs.writeFileSync(file, content);
167
+ managedFiles.add(relative(target, file));
168
+ }
169
+
170
+ function createKnowledgeStructure(target, managedFiles) {
171
+ const today = new Date().toISOString().slice(0, 10);
172
+ for (const directory of wikiDirectories) {
173
+ const keep = path.join(target, "wiki", directory, ".gitkeep");
174
+ writeIfMissing(keep, "", target, managedFiles);
175
+ }
176
+ for (const directory of rawDirectories) {
177
+ const keep = path.join(target, "raw", directory, ".gitkeep");
178
+ writeIfMissing(keep, "", target, managedFiles);
179
+ }
180
+ writeIfMissing(
181
+ path.join(target, "wiki", "index.md"),
182
+ `---\ntitle: Wiki Index\nupdated: ${today}\n---\n\n# Wiki Index\n\n> /wiki를 실행하면 현재 상태를 보고 초기 설정을 추천합니다.\n\n## 통계\n\n| 항목 | 수 |\n|---|---|\n| 전체 페이지 | 0 |\n| Conventions | 0 |\n| Sources | 0 |\n\n마지막 업데이트: ${today}\n`,
183
+ target,
184
+ managedFiles,
185
+ );
186
+ writeIfMissing(
187
+ path.join(target, "wiki", "log.md"),
188
+ `---\ntitle: Wiki Log\nupdated: ${today}\n---\n\n# Wiki Operation Log\n\n> 모든 ingest·query·lint 오퍼레이션이 여기에 기록됩니다.\n\n---\n`,
189
+ target,
190
+ managedFiles,
191
+ );
192
+ writeIfMissing(
193
+ path.join(target, "wiki", "dashboard.md"),
194
+ `---\ntitle: Dashboard\nupdated: ${today}\n---\n\n# 프로젝트 대시보드\n\n> /brief를 실행해 현황판 갱신을 선택하세요.\n\n## 📋 오늘 할 일\n\n- [ ] /wiki를 실행하여 프로젝트 초기 설정\n\n## ⚠️ 주의 필요\n\n- wiki/conventions/ 비어있음 → /wiki 초기 설정 필요\n`,
195
+ target,
196
+ managedFiles,
197
+ );
198
+ copyTree(path.join(packageRoot, ".obsidian-template"), path.join(target, "wiki", ".obsidian"), false, target, managedFiles);
199
+ }
200
+
201
+ function installHooks(target, force, managedFiles) {
202
+ const hooks = [
203
+ ["convention-check.sh", "pre-commit"],
204
+ ["devlog-auto.sh", "post-commit"],
205
+ ];
206
+ for (const [sourceName, destinationName] of hooks) {
207
+ const destination = path.join(target, ".git", "hooks", destinationName);
208
+ if (fs.existsSync(destination) && !force) {
209
+ console.log(` ⏭ 기존 ${destinationName} hook 보존`);
210
+ continue;
211
+ }
212
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
213
+ fs.copyFileSync(path.join(packageRoot, ".hooks", sourceName), destination);
214
+ fs.chmodSync(destination, 0o755);
215
+ managedFiles.add(relative(target, destination));
216
+ }
217
+ }
218
+
219
+ function commandExists(command) {
220
+ return spawnSync(command, ["--help"], { stdio: "ignore" }).status === 0;
221
+ }
222
+
223
+ function setupGraphify(enabled) {
224
+ if (!enabled) return "skipped";
225
+ if (!commandExists("graphify")) {
226
+ console.log(" ⚠️ Graphify 미설치: pip install graphifyy && graphify install");
227
+ return "not-installed";
228
+ }
229
+ const result = spawnSync("graphify", ["install"], { stdio: "inherit" });
230
+ return result.status === 0 ? "installed" : "failed";
231
+ }
232
+
233
+ function writeManifest(target, manifest) {
234
+ const file = path.join(target, ".project-scaffold", "manifest.json");
235
+ fs.mkdirSync(path.dirname(file), { recursive: true });
236
+ fs.writeFileSync(file, `${JSON.stringify(manifest, null, 2)}\n`);
237
+ }
238
+
239
+ export async function install(options) {
240
+ const target = path.resolve(options.target ?? process.cwd());
241
+ if (!fs.existsSync(target)) throw new Error(`대상 경로가 없습니다: ${target}`);
242
+ if (!fs.existsSync(path.join(target, ".git"))) throw new Error(`${target}은 Git 저장소가 아닙니다.`);
243
+ if (target === packageRoot) throw new Error("project-scaffold 저장소 자체에는 설치할 수 없습니다. 다른 Git 저장소를 지정하세요.");
244
+
245
+ const agents = await chooseAgents(options.agents ?? [], options.yes, target);
246
+ const managedFiles = new Set();
247
+
248
+ console.log(`🚀 project-scaffold ${options.mode}\n 대상: ${target}\n 에이전트: ${agents.join(", ")}`);
249
+ console.log("\n📦 scaffold 파일 설치");
250
+ removeLegacySkillState(target, options.force);
251
+ removeDeprecatedAdapterState(target, options.force);
252
+ copySkills(target, options.force, managedFiles);
253
+ for (const directory of scaffoldDirectories) {
254
+ copyTree(path.join(packageRoot, directory), path.join(target, directory), options.force, target, managedFiles);
255
+ }
256
+ for (const file of scaffoldFiles) {
257
+ copyFile(path.join(packageRoot, file), path.join(target, file), options.force, target, managedFiles);
258
+ }
259
+ mergeGitignore(target, managedFiles);
260
+
261
+ console.log("📚 LLM Wiki 구조 생성");
262
+ createKnowledgeStructure(target, managedFiles);
263
+
264
+ if (options.hooks) {
265
+ console.log("🔗 Git hook 설치");
266
+ installHooks(target, options.force, managedFiles);
267
+ }
268
+
269
+ console.log("🤖 에이전트 어댑터 설정");
270
+ const adapterFiles = configureAgents({ target, agents, force: options.force, managedFiles: new Set() });
271
+
272
+ console.log("🧠 Graphify 확인");
273
+ const graphify = setupGraphify(options.graphify);
274
+ const allManagedFiles = new Set(managedFiles);
275
+ for (const file of adapterFiles) allManagedFiles.add(relative(target, file));
276
+ for (const checkPath of ["CLAUDE.md", "AGENTS.md", ".cursorrules", ".hermes.md", "cli-config.yaml", ".aider.conf.yml"]) {
277
+ if (fs.existsSync(path.join(target, checkPath))) allManagedFiles.add(checkPath);
278
+ }
279
+ writeManifest(target, {
280
+ schemaVersion: 1,
281
+ package: packageJson.name,
282
+ version: packageJson.version,
283
+ mode: options.mode,
284
+ installedAt: new Date().toISOString(),
285
+ agents,
286
+ agentRegistry: registrySource,
287
+ features: { hooks: options.hooks, graphify },
288
+ managedFiles: [...allManagedFiles].sort(),
289
+ });
290
+
291
+ console.log("\n✅ 설치 완료");
292
+ console.log(` 다음 단계: ${agents[0]}에서 프로젝트를 열고 /wiki를 실행하세요.`);
293
+ console.log(" 상태 검사: npx @taejung3852/project-scaffold@latest doctor");
294
+ }
295
+
296
+ function loadManifest(target) {
297
+ const file = path.join(target, ".project-scaffold", "manifest.json");
298
+ if (!fs.existsSync(file)) return null;
299
+ try {
300
+ return JSON.parse(fs.readFileSync(file, "utf8"));
301
+ } catch {
302
+ return null;
303
+ }
304
+ }
305
+
306
+ export async function doctor(options) {
307
+ const target = path.resolve(options.target ?? process.cwd());
308
+ const manifest = loadManifest(target);
309
+ const checks = [
310
+ { label: "Git 저장소", ok: fs.existsSync(path.join(target, ".git")) },
311
+ { label: "설치 manifest", ok: manifest !== null },
312
+ {
313
+ label: `agent registry skills@${registrySource.version}`,
314
+ ok: manifest?.agentRegistry?.version === registrySource.version,
315
+ },
316
+ { label: "AGENT.md", ok: fs.existsSync(path.join(target, "AGENT.md")) },
317
+ { label: "skills/", ok: fs.existsSync(path.join(target, "skills")) },
318
+ { label: "legacy 스킬·자가 진화 상태 없음", ok: legacyArtifacts(target).length === 0 },
319
+ { label: "wiki/index.md", ok: fs.existsSync(path.join(target, "wiki", "index.md")) },
320
+ { label: "raw/", ok: fs.existsSync(path.join(target, "raw")) },
321
+ ];
322
+ if (manifest?.features?.hooks) {
323
+ checks.push(
324
+ { label: "pre-commit hook", ok: fs.existsSync(path.join(target, ".git", "hooks", "pre-commit")) },
325
+ { label: "post-commit hook", ok: fs.existsSync(path.join(target, ".git", "hooks", "post-commit")) },
326
+ );
327
+ }
328
+ checks.push(...agentChecks(target, manifest?.agents ?? []));
329
+ const result = { ok: checks.every((check) => check.ok), target, version: manifest?.version ?? null, checks };
330
+
331
+ if (options.json) {
332
+ console.log(JSON.stringify(result, null, 2));
333
+ } else {
334
+ console.log(`project-scaffold doctor\n대상: ${target}\n`);
335
+ for (const check of checks) console.log(`${check.ok ? "✅" : "❌"} ${check.label}`);
336
+ if (manifest?.agents?.includes("hermes-agent")) {
337
+ console.log("ℹ️ Hermes는 ~/.hermes/config.yaml의 skills.external_dirs에 프로젝트 skills/ 경로를 등록해야 합니다.");
338
+ }
339
+ console.log(result.ok ? "\n✅ 설치 상태가 정상입니다." : "\n❌ 누락된 항목을 확인하세요.");
340
+ }
341
+ return result;
342
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@taejung3852/project-scaffold",
3
+ "version": "0.1.0",
4
+ "description": "LLM Wiki-based project context scaffold for AI coding agents",
5
+ "type": "module",
6
+ "bin": {
7
+ "project-scaffold": "bin/project-scaffold.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "lib/",
12
+ "templates/",
13
+ "skills/",
14
+ ".hooks/",
15
+ ".obsidian-template/",
16
+ "AGENT.md",
17
+ "SOUL.md",
18
+ "THIRD_PARTY_NOTICES.md"
19
+ ],
20
+ "scripts": {
21
+ "test": "node --test",
22
+ "check": "node --check bin/project-scaffold.js && node --check lib/install.js && node --check lib/agents.js && node --check lib/agents-registry.js",
23
+ "pack:check": "npm pack --dry-run"
24
+ },
25
+ "engines": {
26
+ "node": ">=20"
27
+ },
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/taejung3852/project-scaffold.git"
32
+ }
33
+ }
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: brief
3
+ description: "프로젝트 현황을 사람이 따라갈 수 있는 브리핑으로 정리한다. 현재 목표, 변경점, 결정 필요 항목, 다음 행동을 30초·5분·상세 깊이로 설명하고 dashboard, 회의·스프린트·ADR report, 세션 handoff를 생성한다. 진행 상황 파악, 대시보드 갱신, 보고서 작성, 세션 인계가 필요할 때 사용."
4
+ ---
5
+
6
+ # /brief — 사람 중심 프로젝트 브리핑
7
+
8
+ 항상 사람이 현재 상황과 자신의 결정 지점을 빠르게 파악하도록 구성한다.
9
+
10
+ ## 사용자 호출 UX
11
+
12
+ 자연어 요청에서 목적이 명확하면 묻지 말고 바로 적절한 작업을 수행한다.
13
+
14
+ - “지금 어디까지 했어?” → 기본 현황 브리핑
15
+ - “회의 내용 공유용으로 정리해줘” → report
16
+ - “다음 세션에서 이어가게 해줘” → handoff
17
+
18
+ 사용자가 **`/brief`만 단독으로 호출하면**, git 상태·최근 devlog·대화 흐름을 먼저 확인하고 가장 적절한 항목을 추천한다. 내부 모드명 대신 사람이 이해하는 행동으로 묻는다.
19
+
20
+ ```text
21
+ 지금 맥락상 "현재 상태와 다음 우선순위 확인"이 가장 적절해 보여요.
22
+
23
+ 1. 현재 상태를 30초로 보기 ← 추천
24
+ 2. 최근 변화까지 5분으로 보기
25
+ 3. 프로젝트 현황판 갱신하기
26
+ 4. 회의·ADR·스프린트 문서 만들기
27
+ 5. 다음 세션용 인계 남기기
28
+
29
+ 어떤 걸 할까요? 번호나 원하는 내용을 말해줘도 됩니다.
30
+ ```
31
+
32
+ 추천 근거는 한 줄만 말한다. 사용자가 번호 대신 자연어로 답해도 의도를 다시 해석한다. 이미 목적이 분명한 요청에는 선택 메뉴를 보여주지 않는다.
33
+
34
+ ## 기본 브리핑
35
+
36
+ 별도 모드가 없으면 다음 순서로 답한다.
37
+
38
+ 1. **Now** — 지금 목표와 현재 작업
39
+ 2. **Why** — 이 작업을 하는 이유
40
+ 3. **Since last visit** — 최근 코드·문서·결정 변화
41
+ 4. **Decision queue** — 사용자가 판단해야 할 항목
42
+ 5. **Next** — 가장 가까운 다음 한 단계
43
+ 6. **Sources** — 관련 wiki·devlog·코드 경로
44
+
45
+ 기본 깊이는 30초 분량이다. 사용자가 더 알고 싶어 하면 5분 또는 상세 보기로 확장한다. 진행률은 구현 근거가 있을 때만 숫자로 표시하고, 미구현 계획은 0%로 둔다.
46
+
47
+ ## 내부 모드 라우팅
48
+
49
+ | 모드 | 대표 의도 | 읽을 참조 |
50
+ |---|---|---|
51
+ | `dashboard` | 현황판 갱신, TODO·마일스톤 관리, terminal/web 렌더 | `references/dashboard.md` |
52
+ | `report` | 회의록, 인터뷰, ADR, 스프린트 요약 | `references/report.md` |
53
+ | `handoff` | 세션 저장·복원·목록·정리 | `references/handoff.md` |
54
+
55
+ 사용자가 프로젝트 현황, 최근 변경, 다음 우선순위를 물으면 명시적 호출이 없어도 기본 브리핑으로 처리한다.
56
+
57
+ ## 공통 원칙
58
+
59
+ - 완료된 구현과 미래 계획을 분리한다.
60
+ - 요약만 제공하고 끝내지 말고 사용자가 결정해야 할 지점을 드러낸다.
61
+ - 원본과 해석을 구분하고 관련 파일로 이동할 수 있게 한다.
62
+ - 같은 내용을 여러 산출물에 복제하지 말고 기존 PR·커밋·wiki 경로를 참조한다.
63
+ - 민감 정보는 handoff와 report에 저장하기 전에 검열한다.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Project Brief"
3
+ short_description: "프로젝트 현황과 결정 지점을 사람 중심으로 브리핑"
4
+ default_prompt: "Use $brief to explain the current project state at the right depth."