@wooojin/forgen 0.4.7 → 0.4.9
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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +40 -0
- package/assets/dev-guide/be/README.md +226 -0
- package/assets/dev-guide/be/adapters/build-agents-md.sh +63 -0
- package/assets/dev-guide/be/principles/common.md +433 -0
- package/assets/dev-guide/be/principles/go.md +469 -0
- package/assets/dev-guide/be/principles/node.md +388 -0
- package/assets/dev-guide/be/skills/go/be-build/SKILL.md +262 -0
- package/assets/dev-guide/be/skills/go/be-perf/SKILL.md +308 -0
- package/assets/dev-guide/be/skills/go/be-review/SKILL.md +119 -0
- package/assets/dev-guide/be/skills/go/be-security/SKILL.md +362 -0
- package/assets/dev-guide/be/skills/node/be-build/SKILL.md +239 -0
- package/assets/dev-guide/be/skills/node/be-perf/SKILL.md +272 -0
- package/assets/dev-guide/be/skills/node/be-review/SKILL.md +118 -0
- package/assets/dev-guide/be/skills/node/be-security/SKILL.md +355 -0
- package/assets/dev-guide/be/sources/12factor/INDEX.md +53 -0
- package/assets/dev-guide/be/sources/api-design/INDEX.md +56 -0
- package/assets/dev-guide/be/sources/ddia/INDEX.md +55 -0
- package/assets/dev-guide/be/sources/go-runtime/INDEX.md +62 -0
- package/assets/dev-guide/be/sources/node-runtime/INDEX.md +60 -0
- package/assets/dev-guide/be/sources/otel/INDEX.md +53 -0
- package/assets/dev-guide/be/sources/owasp-api/INDEX.md +52 -0
- package/assets/dev-guide/be/sources/postgres/INDEX.md +55 -0
- package/assets/dev-guide/be/sources/sre-book/INDEX.md +48 -0
- package/assets/dev-guide/fe/README.md +197 -0
- package/assets/dev-guide/fe/adapters/build-agents-md.sh +63 -0
- package/assets/dev-guide/fe/adapters/refresh.sh +68 -0
- package/assets/dev-guide/fe/principles/common.md +160 -0
- package/assets/dev-guide/fe/principles/react.md +183 -0
- package/assets/dev-guide/fe/principles/vue.md +196 -0
- package/assets/dev-guide/fe/skills/react/fe-build/SKILL.md +139 -0
- package/assets/dev-guide/fe/skills/react/fe-perf/SKILL.md +179 -0
- package/assets/dev-guide/fe/skills/react/fe-review/SKILL.md +141 -0
- package/assets/dev-guide/fe/skills/vue/fe-build/SKILL.md +148 -0
- package/assets/dev-guide/fe/skills/vue/fe-perf/SKILL.md +163 -0
- package/assets/dev-guide/fe/skills/vue/fe-review/SKILL.md +136 -0
- package/assets/dev-guide/fe/sources/a11y-dx/INDEX.md +41 -0
- package/assets/dev-guide/fe/sources/a11y-dx/chrome-devtools-memory.md +150 -0
- package/assets/dev-guide/fe/sources/a11y-dx/chrome-devtools-performance.md +99 -0
- package/assets/dev-guide/fe/sources/a11y-dx/lighthouse-audits.md +146 -0
- package/assets/dev-guide/fe/sources/a11y-dx/react-devtools-profiler.md +128 -0
- package/assets/dev-guide/fe/sources/a11y-dx/wcag22-new-criteria.md +174 -0
- package/assets/dev-guide/fe/sources/perf/01-core-web-vitals.md +58 -0
- package/assets/dev-guide/fe/sources/perf/02-inp.md +83 -0
- package/assets/dev-guide/fe/sources/perf/03-lcp-cls.md +130 -0
- package/assets/dev-guide/fe/sources/perf/04-speculation-rules.md +148 -0
- package/assets/dev-guide/fe/sources/perf/05-view-transitions.md +153 -0
- package/assets/dev-guide/fe/sources/perf/06-nextjs-caching.md +188 -0
- package/assets/dev-guide/fe/sources/perf/07-server-components.md +181 -0
- package/assets/dev-guide/fe/sources/perf/08-ppr.md +133 -0
- package/assets/dev-guide/fe/sources/perf/09-nextjs-image.md +200 -0
- package/assets/dev-guide/fe/sources/perf/10-optimize-lcp.md +201 -0
- package/assets/dev-guide/fe/sources/perf/INDEX.md +88 -0
- package/assets/dev-guide/fe/sources/react/INDEX.md +41 -0
- package/assets/dev-guide/fe/sources/react/keeping-components-pure.md +135 -0
- package/assets/dev-guide/fe/sources/react/no-effect-patterns.md +183 -0
- package/assets/dev-guide/fe/sources/react/react-compiler.md +182 -0
- package/assets/dev-guide/fe/sources/react/server-components.md +194 -0
- package/assets/dev-guide/fe/sources/react/server-functions.md +192 -0
- package/assets/dev-guide/fe/sources/react/suspense.md +218 -0
- package/assets/dev-guide/fe/sources/react/use-action-state.md +123 -0
- package/assets/dev-guide/fe/sources/react/use-form-status.md +158 -0
- package/assets/dev-guide/fe/sources/react/use-hook.md +153 -0
- package/assets/dev-guide/fe/sources/react/use-optimistic.md +194 -0
- package/assets/dev-guide/fe/sources/toss-ff/INDEX.md +58 -0
- package/assets/dev-guide/fe/sources/toss-ff/cohesion-code-directory.md +79 -0
- package/assets/dev-guide/fe/sources/toss-ff/cohesion-form-fields.md +110 -0
- package/assets/dev-guide/fe/sources/toss-ff/cohesion-magic-number.md +47 -0
- package/assets/dev-guide/fe/sources/toss-ff/coupling-item-edit-modal.md +124 -0
- package/assets/dev-guide/fe/sources/toss-ff/coupling-use-bottom-sheet.md +57 -0
- package/assets/dev-guide/fe/sources/toss-ff/coupling-use-page-state.md +71 -0
- package/assets/dev-guide/fe/sources/toss-ff/overview-4-principles.md +77 -0
- package/assets/dev-guide/fe/sources/toss-ff/predictability-hidden-logic.md +59 -0
- package/assets/dev-guide/fe/sources/toss-ff/predictability-http.md +77 -0
- package/assets/dev-guide/fe/sources/toss-ff/predictability-use-user.md +110 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-comparison-order.md +52 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-condition-name.md +64 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-login-start-page.md +183 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-magic-number.md +53 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-submit-button.md +73 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-ternary-operator.md +38 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-use-page-state.md +77 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-user-policy.md +98 -0
- package/assets/dev-guide/fe/sources/vue/INDEX.md +17 -0
- package/assets/dev-guide/fe/sources/vue/composition-api.md +251 -0
- package/assets/dev-guide/fe/sources/vue/nuxt-data-fetching.md +232 -0
- package/assets/dev-guide/fe/sources/vue/pinia-state-management.md +134 -0
- package/assets/dev-guide/fe/sources/vue/reactivity-pitfalls.md +261 -0
- package/assets/dev-guide/fe/sources/vue/style-guide-priority-a.md +117 -0
- package/assets/dev-guide/fe/sources/vue/style-guide-priority-b.md +231 -0
- package/assets/dev-guide/fe/sources/vue/style-guide-priority-c.md +86 -0
- package/assets/dev-guide/fe/sources/vue/style-guide-priority-d.md +72 -0
- package/dist/checks/self-score-deflation.js +6 -4
- package/dist/cli.js +47 -2
- package/dist/core/auto-compound-runner.js +6 -2
- package/dist/core/dashboard-cli.d.ts +12 -0
- package/dist/core/dashboard-cli.js +226 -0
- package/dist/core/dashboard.js +2 -2
- package/dist/core/dev-guide-injector.d.ts +26 -0
- package/dist/core/dev-guide-injector.js +137 -0
- package/dist/core/doctor.d.ts +10 -0
- package/dist/core/doctor.js +49 -8
- package/dist/core/harness.js +8 -2
- package/dist/core/init.js +53 -0
- package/dist/core/inspect-cli.js +4 -4
- package/dist/core/lifecycle-classifier.d.ts +23 -0
- package/dist/core/lifecycle-classifier.js +104 -0
- package/dist/core/migrate-evidence-host.js +1 -1
- package/dist/core/notify.js +7 -0
- package/dist/core/observability-backfill.d.ts +31 -0
- package/dist/core/observability-backfill.js +178 -0
- package/dist/core/observability-store.d.ts +58 -0
- package/dist/core/observability-store.js +195 -0
- package/dist/core/paths.d.ts +16 -2
- package/dist/core/paths.js +16 -2
- package/dist/core/session-store.d.ts +12 -1
- package/dist/core/session-store.js +77 -1
- package/dist/core/spawn.d.ts +17 -0
- package/dist/core/spawn.js +191 -8
- package/dist/core/statusline-cli.js +34 -1
- package/dist/core/v1-bootstrap.d.ts +7 -0
- package/dist/core/v1-bootstrap.js +28 -6
- package/dist/engine/compound-extractor.js +40 -1
- package/dist/engine/compound-loop.js +6 -0
- package/dist/engine/compound-retire.d.ts +20 -0
- package/dist/engine/compound-retire.js +85 -0
- package/dist/engine/learn-cli.js +2 -2
- package/dist/engine/lifecycle/bypass-detector.js +3 -2
- package/dist/engine/lifecycle/meta-reclassifier.js +1 -1
- package/dist/engine/lifecycle/signals.js +2 -2
- package/dist/engine/lifecycle/trigger-t1-correction.js +1 -1
- package/dist/engine/solution-candidate.js +1 -1
- package/dist/engine/solution-outcomes.js +1 -1
- package/dist/engine/solution-quarantine.js +1 -1
- package/dist/engine/solution-weakness.js +8 -2
- package/dist/forge/cli.js +1 -1
- package/dist/hooks/context-guard.js +25 -1
- package/dist/hooks/keyword-detector.js +1 -1
- package/dist/hooks/post-tool-use.js +48 -0
- package/dist/hooks/secret-filter.js +2 -2
- package/dist/hooks/shared/hook-response.js +1 -1
- package/dist/hooks/shared/hook-timing.js +3 -3
- package/dist/hooks/solution-injector.js +94 -1
- package/dist/hooks/stop-guard.js +3 -3
- package/dist/host/install-claude.d.ts +6 -2
- package/dist/host/install-claude.js +74 -2
- package/dist/host/install-codex.d.ts +4 -0
- package/dist/host/install-codex.js +72 -1
- package/dist/host/install-orchestrator.js +1 -0
- package/dist/mcp/tools.js +1 -1
- package/dist/preset/facet-catalog.js +2 -2
- package/dist/renderer/rule-renderer.js +7 -7
- package/dist/store/compound-usage-store.js +1 -1
- package/dist/store/implicit-feedback-store.js +2 -2
- package/dist/store/profile-store.d.ts +11 -0
- package/dist/store/profile-store.js +23 -0
- package/package.json +6 -6
- package/plugin.json +1 -1
- package/scripts/postinstall.js +134 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen — Dashboard CLI (P3)
|
|
3
|
+
*
|
|
4
|
+
* `fgx status [--watch] [--json] [--interval N]`
|
|
5
|
+
* ANSI box-drawing 기반 상태 대시보드. TUI 라이브러리 없음.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'node:fs';
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
import { getUsageStats } from './usage-telemetry.js';
|
|
10
|
+
import { classifySolutions } from './lifecycle-classifier.js';
|
|
11
|
+
import { STATE_DIR } from './paths.js';
|
|
12
|
+
import { FORGEN_HOME } from './paths.js';
|
|
13
|
+
// ── ANSI ──────────────────────────────────────────────────────────────────────
|
|
14
|
+
const isTTY = process.stdout.isTTY;
|
|
15
|
+
const C = {
|
|
16
|
+
reset: isTTY ? '\x1b[0m' : '',
|
|
17
|
+
bold: isTTY ? '\x1b[1m' : '',
|
|
18
|
+
dim: isTTY ? '\x1b[2m' : '',
|
|
19
|
+
cyan: isTTY ? '\x1b[36m' : '',
|
|
20
|
+
yellow: isTTY ? '\x1b[33m' : '',
|
|
21
|
+
green: isTTY ? '\x1b[32m' : '',
|
|
22
|
+
red: isTTY ? '\x1b[31m' : '',
|
|
23
|
+
};
|
|
24
|
+
const BOX_WIDTH = 66;
|
|
25
|
+
function bar(pct, width = 10) {
|
|
26
|
+
const filled = Math.round(pct * width);
|
|
27
|
+
const empty = width - filled;
|
|
28
|
+
return '█'.repeat(Math.max(0, filled)) + '░'.repeat(Math.max(0, empty));
|
|
29
|
+
}
|
|
30
|
+
// 런타임에 정규식 구성 — biome 의 noControlCharactersInRegex 우회.
|
|
31
|
+
// ESC (0x1B) 는 ANSI escape 의 시작이지만 literal regex 에는 못 박힘.
|
|
32
|
+
const ANSI_STRIP_RE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g');
|
|
33
|
+
function boxLine(content) {
|
|
34
|
+
const plain = content.replace(ANSI_STRIP_RE, '');
|
|
35
|
+
const padding = BOX_WIDTH - 2 - plain.length;
|
|
36
|
+
return `│ ${content}${' '.repeat(Math.max(0, padding))} │`;
|
|
37
|
+
}
|
|
38
|
+
function boxEmpty() {
|
|
39
|
+
return `│${' '.repeat(BOX_WIDTH - 2)}│`;
|
|
40
|
+
}
|
|
41
|
+
function boxTop() {
|
|
42
|
+
return `┌─ ${C.bold}${C.cyan}forgen status${C.reset}` +
|
|
43
|
+
' ' + '─'.repeat(BOX_WIDTH - 17) + '┐';
|
|
44
|
+
}
|
|
45
|
+
function boxBottom() {
|
|
46
|
+
return `└${'─'.repeat(BOX_WIDTH - 2)}┘`;
|
|
47
|
+
}
|
|
48
|
+
function collectData() {
|
|
49
|
+
const now = new Date();
|
|
50
|
+
// usage
|
|
51
|
+
const usage = (() => {
|
|
52
|
+
try {
|
|
53
|
+
return getUsageStats();
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return { hour5: { claude: 0, codex: 0, total: 0 }, week: { claude: 0, codex: 0, total: 0 } };
|
|
57
|
+
}
|
|
58
|
+
})();
|
|
59
|
+
// today extracted: me/solutions/*.md mtime이 오늘인 것 (또는 last-extraction.json)
|
|
60
|
+
const todayExtracted = (() => {
|
|
61
|
+
try {
|
|
62
|
+
const lastExtPath = path.join(STATE_DIR, 'last-extraction.json');
|
|
63
|
+
if (fs.existsSync(lastExtPath)) {
|
|
64
|
+
const data = JSON.parse(fs.readFileSync(lastExtPath, 'utf-8'));
|
|
65
|
+
const ts = data.ts ?? 0;
|
|
66
|
+
const isToday = new Date(ts).toDateString() === now.toDateString();
|
|
67
|
+
if (isToday && typeof data.count === 'number')
|
|
68
|
+
return data.count;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch { /* ignore */ }
|
|
72
|
+
// fallback: count solutions modified today
|
|
73
|
+
try {
|
|
74
|
+
const solutionsDir = path.join(FORGEN_HOME, 'me', 'solutions');
|
|
75
|
+
if (!fs.existsSync(solutionsDir))
|
|
76
|
+
return 0;
|
|
77
|
+
const files = fs.readdirSync(solutionsDir).filter(f => f.endsWith('.md'));
|
|
78
|
+
const todayStr = now.toDateString();
|
|
79
|
+
return files.filter(f => {
|
|
80
|
+
try {
|
|
81
|
+
const stat = fs.statSync(path.join(solutionsDir, f));
|
|
82
|
+
return new Date(stat.mtimeMs).toDateString() === todayStr;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}).length;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
})();
|
|
93
|
+
// lifecycle
|
|
94
|
+
const classified = (() => {
|
|
95
|
+
try {
|
|
96
|
+
return classifySolutions();
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
})();
|
|
102
|
+
const counts = { hot: 0, warm: 0, cold: 0, dead: 0, new: 0 };
|
|
103
|
+
for (const c of classified)
|
|
104
|
+
counts[c.lifecycle]++;
|
|
105
|
+
const topHot = classified
|
|
106
|
+
.filter(c => c.lifecycle === 'hot')
|
|
107
|
+
.sort((a, b) => b.acted_90d - a.acted_90d)
|
|
108
|
+
.slice(0, 5)
|
|
109
|
+
.map(c => ({
|
|
110
|
+
id: c.solutionId,
|
|
111
|
+
surfaced: c.surfaced_90d,
|
|
112
|
+
acted: c.acted_90d,
|
|
113
|
+
rate: c.hitRate ?? 0,
|
|
114
|
+
}));
|
|
115
|
+
// rate-limit misses (7d)
|
|
116
|
+
const rateLimitMisses7d = (() => {
|
|
117
|
+
try {
|
|
118
|
+
const missPath = path.join(STATE_DIR, 'rate-limit-misses.jsonl');
|
|
119
|
+
if (!fs.existsSync(missPath))
|
|
120
|
+
return 0;
|
|
121
|
+
const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
|
122
|
+
const lines = fs.readFileSync(missPath, 'utf-8').split('\n').filter(Boolean);
|
|
123
|
+
return lines.filter(l => {
|
|
124
|
+
try {
|
|
125
|
+
const obj = JSON.parse(l);
|
|
126
|
+
return typeof obj.ts === 'number' && obj.ts >= cutoff;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}).length;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
})();
|
|
137
|
+
return {
|
|
138
|
+
timestamp: now.toISOString(),
|
|
139
|
+
usage,
|
|
140
|
+
todayExtracted,
|
|
141
|
+
solutions: {
|
|
142
|
+
total: classified.length,
|
|
143
|
+
...counts,
|
|
144
|
+
topHot,
|
|
145
|
+
classified,
|
|
146
|
+
},
|
|
147
|
+
rateLimitMisses7d,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
// ── Render ─────────────────────────────────────────────────────────────────────
|
|
151
|
+
function renderTTY(data) {
|
|
152
|
+
const lines = [];
|
|
153
|
+
lines.push(boxTop());
|
|
154
|
+
lines.push(boxEmpty());
|
|
155
|
+
// Usage
|
|
156
|
+
lines.push(boxLine(`${C.bold}Usage${C.reset}`));
|
|
157
|
+
const h5total = data.usage.hour5.total;
|
|
158
|
+
const h5pct = Math.min(1, h5total / Math.max(h5total + 10, 50));
|
|
159
|
+
const h5bar = bar(h5pct);
|
|
160
|
+
lines.push(boxLine(` 5h window: ${C.yellow}${h5bar}${C.reset} (${h5total} tool calls)`));
|
|
161
|
+
const wktotal = data.usage.week.total;
|
|
162
|
+
const wkpct = Math.min(1, wktotal / Math.max(wktotal + 10, 100));
|
|
163
|
+
const wkbar = bar(wkpct);
|
|
164
|
+
lines.push(boxLine(` weekly: ${C.yellow}${wkbar}${C.reset} (${wktotal} tool calls)`));
|
|
165
|
+
lines.push(boxEmpty());
|
|
166
|
+
// Today compound
|
|
167
|
+
lines.push(boxLine(`${C.bold}Today's compound${C.reset}`));
|
|
168
|
+
lines.push(boxLine(` extracted: ${data.todayExtracted} solutions`));
|
|
169
|
+
lines.push(boxEmpty());
|
|
170
|
+
// Solutions
|
|
171
|
+
lines.push(boxLine(`${C.bold}Solutions${C.reset} (${data.solutions.total} total)`));
|
|
172
|
+
const lifecycleLine = ` ${C.red}🔥 hot:${C.reset} ${data.solutions.hot}` +
|
|
173
|
+
` ${C.yellow}🟡 warm:${C.reset} ${data.solutions.warm}` +
|
|
174
|
+
` ${C.cyan}🥶 cold:${C.reset} ${data.solutions.cold}` +
|
|
175
|
+
` ${C.dim}💀 dead:${C.reset} ${data.solutions.dead}` +
|
|
176
|
+
` ${C.green}🌱 new:${C.reset} ${data.solutions.new}`;
|
|
177
|
+
lines.push(boxLine(lifecycleLine));
|
|
178
|
+
lines.push(boxEmpty());
|
|
179
|
+
// Top 5 hot
|
|
180
|
+
if (data.solutions.topHot.length > 0) {
|
|
181
|
+
lines.push(boxLine(`${C.bold}Top ${data.solutions.topHot.length} hot (90d)${C.reset}`));
|
|
182
|
+
for (const h of data.solutions.topHot) {
|
|
183
|
+
const pctStr = `${Math.round(h.rate * 100)}%`;
|
|
184
|
+
const idShort = h.id.length > 36 ? h.id.slice(0, 33) + '...' : h.id;
|
|
185
|
+
lines.push(boxLine(` · ${C.yellow}${idShort}${C.reset} surf=${h.surfaced} acted=${h.acted} (${pctStr})`));
|
|
186
|
+
}
|
|
187
|
+
lines.push(boxEmpty());
|
|
188
|
+
}
|
|
189
|
+
// Rate-limit
|
|
190
|
+
lines.push(boxLine(`${C.bold}Rate-limit${C.reset}`));
|
|
191
|
+
lines.push(boxLine(` misses (7d): ${data.rateLimitMisses7d}`));
|
|
192
|
+
lines.push(boxEmpty());
|
|
193
|
+
// Last update
|
|
194
|
+
lines.push(boxLine(`${C.dim}Last update: ${data.timestamp}${C.reset}`));
|
|
195
|
+
lines.push(boxBottom());
|
|
196
|
+
return lines.join('\n');
|
|
197
|
+
}
|
|
198
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
199
|
+
export async function runDashboard(opts) {
|
|
200
|
+
const intervalSec = opts.intervalSec ?? 5;
|
|
201
|
+
const render = () => {
|
|
202
|
+
const data = collectData();
|
|
203
|
+
if (opts.json) {
|
|
204
|
+
// Strip classified array (verbose) from JSON output for cleaner schema
|
|
205
|
+
const { solutions: { classified: _c, ...solutionStats }, ...rest } = data;
|
|
206
|
+
process.stdout.write(JSON.stringify({ ...rest, solutions: solutionStats }, null, 2) + '\n');
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const output = renderTTY(data);
|
|
210
|
+
if (opts.watch) {
|
|
211
|
+
process.stdout.write('\x1b[2J\x1b[H'); // clear screen
|
|
212
|
+
}
|
|
213
|
+
process.stdout.write(output + '\n');
|
|
214
|
+
};
|
|
215
|
+
render();
|
|
216
|
+
if (opts.watch) {
|
|
217
|
+
const interval = setInterval(render, intervalSec * 1000);
|
|
218
|
+
process.on('SIGINT', () => {
|
|
219
|
+
clearInterval(interval);
|
|
220
|
+
process.stdout.write('\n');
|
|
221
|
+
process.exit(0);
|
|
222
|
+
});
|
|
223
|
+
// Keep process alive
|
|
224
|
+
await new Promise(() => { });
|
|
225
|
+
}
|
|
226
|
+
}
|
package/dist/core/dashboard.js
CHANGED
|
@@ -40,13 +40,13 @@ function dim(s) { return `${DIM}${s}${RESET}`; }
|
|
|
40
40
|
function cyan(s) { return `${CYAN}${s}${RESET}`; }
|
|
41
41
|
// ── Box-drawing table helpers ──
|
|
42
42
|
function tableRow(cols, widths) {
|
|
43
|
-
return
|
|
43
|
+
return ` │ ${cols.map((c, i) => c.padEnd(widths[i])).join(' │ ')} │`;
|
|
44
44
|
}
|
|
45
45
|
function tableSep(widths, top = false, bottom = false) {
|
|
46
46
|
const left = top ? '┌' : bottom ? '└' : '├';
|
|
47
47
|
const mid = top ? '┬' : bottom ? '┴' : '┼';
|
|
48
48
|
const right = top ? '┐' : bottom ? '┘' : '┤';
|
|
49
|
-
return
|
|
49
|
+
return ` ${left}${widths.map(w => '─'.repeat(w + 2)).join(mid)}${right}`;
|
|
50
50
|
}
|
|
51
51
|
// ── Data Collection Functions ──
|
|
52
52
|
/** Read all .md files in a directory and return their frontmatter. */
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dev-guide-injector — fgx init 시 dev-guide principles 를
|
|
3
|
+
* Claude (.claude/rules/dev-guide-principles.md) + Codex (AGENTS.md managed block)
|
|
4
|
+
* 양쪽에 stack-aware 자동 inject.
|
|
5
|
+
*/
|
|
6
|
+
export interface DetectedStack {
|
|
7
|
+
side: 'fe' | 'be';
|
|
8
|
+
stack: 'react' | 'vue' | 'node' | 'go';
|
|
9
|
+
principlesFiles: string[];
|
|
10
|
+
}
|
|
11
|
+
export interface InjectResult {
|
|
12
|
+
stack: DetectedStack | null;
|
|
13
|
+
claudeRulePath: string;
|
|
14
|
+
claudeRuleWritten: boolean;
|
|
15
|
+
agentsMdPath: string;
|
|
16
|
+
agentsMdInjected: boolean;
|
|
17
|
+
bytesWritten: number;
|
|
18
|
+
}
|
|
19
|
+
export interface InjectOptions {
|
|
20
|
+
cwd: string;
|
|
21
|
+
pkgRoot: string;
|
|
22
|
+
dryRun?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare function detectStack(cwd: string): DetectedStack | null;
|
|
25
|
+
export declare function readPrinciples(pkgRoot: string, side: 'fe' | 'be', stack: string): string;
|
|
26
|
+
export declare function injectDevGuidePrinciples(opts: InjectOptions): InjectResult;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dev-guide-injector — fgx init 시 dev-guide principles 를
|
|
3
|
+
* Claude (.claude/rules/dev-guide-principles.md) + Codex (AGENTS.md managed block)
|
|
4
|
+
* 양쪽에 stack-aware 자동 inject.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'node:fs';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
// ── 상수 ──────────────────────────────────────────────────────────────────────
|
|
9
|
+
const AGENTS_MD_BEGIN = '<!-- >>> forgen-managed-rules -->';
|
|
10
|
+
const AGENTS_MD_END = '<!-- <<< forgen-managed-rules -->';
|
|
11
|
+
// ── detectStack ───────────────────────────────────────────────────────────────
|
|
12
|
+
export function detectStack(cwd) {
|
|
13
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
14
|
+
if (fs.existsSync(pkgPath)) {
|
|
15
|
+
try {
|
|
16
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
17
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies, ...pkg.peerDependencies };
|
|
18
|
+
if ('react' in deps || 'next' in deps) {
|
|
19
|
+
return { side: 'fe', stack: 'react', principlesFiles: ['common.md', 'react.md'] };
|
|
20
|
+
}
|
|
21
|
+
if ('vue' in deps || 'nuxt' in deps) {
|
|
22
|
+
return { side: 'fe', stack: 'vue', principlesFiles: ['common.md', 'vue.md'] };
|
|
23
|
+
}
|
|
24
|
+
return { side: 'be', stack: 'node', principlesFiles: ['common.md', 'node.md'] };
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// JSON parse 실패 — fall through to go.mod check
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (fs.existsSync(path.join(cwd, 'go.mod'))) {
|
|
31
|
+
return { side: 'be', stack: 'go', principlesFiles: ['common.md', 'go.md'] };
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
// ── readPrinciples ────────────────────────────────────────────────────────────
|
|
36
|
+
export function readPrinciples(pkgRoot, side, stack) {
|
|
37
|
+
const principlesDir = path.join(pkgRoot, 'assets', 'dev-guide', side, 'principles');
|
|
38
|
+
const commonContent = fs.readFileSync(path.join(principlesDir, 'common.md'), 'utf-8');
|
|
39
|
+
const stackContent = fs.readFileSync(path.join(principlesDir, `${stack}.md`), 'utf-8');
|
|
40
|
+
return [
|
|
41
|
+
`<!-- forgen dev-guide principles (auto-generated, do not edit) -->`,
|
|
42
|
+
`<!-- source: assets/dev-guide/${side}/principles/common.md + ${stack}.md -->`,
|
|
43
|
+
``,
|
|
44
|
+
`# common (${side})`,
|
|
45
|
+
commonContent.trimEnd(),
|
|
46
|
+
``,
|
|
47
|
+
`---`,
|
|
48
|
+
``,
|
|
49
|
+
`# ${stack}`,
|
|
50
|
+
stackContent.trimEnd(),
|
|
51
|
+
``,
|
|
52
|
+
].join('\n');
|
|
53
|
+
}
|
|
54
|
+
// ── injectDevGuidePrinciples ──────────────────────────────────────────────────
|
|
55
|
+
export function injectDevGuidePrinciples(opts) {
|
|
56
|
+
const { cwd, pkgRoot, dryRun = false } = opts;
|
|
57
|
+
const claudeRulePath = path.join(cwd, '.claude', 'rules', 'dev-guide-principles.md');
|
|
58
|
+
const agentsMdPath = path.join(cwd, 'AGENTS.md');
|
|
59
|
+
const emptyResult = {
|
|
60
|
+
stack: null,
|
|
61
|
+
claudeRulePath,
|
|
62
|
+
claudeRuleWritten: false,
|
|
63
|
+
agentsMdPath,
|
|
64
|
+
agentsMdInjected: false,
|
|
65
|
+
bytesWritten: 0,
|
|
66
|
+
};
|
|
67
|
+
const detected = detectStack(cwd);
|
|
68
|
+
if (!detected)
|
|
69
|
+
return emptyResult;
|
|
70
|
+
const body = readPrinciples(pkgRoot, detected.side, detected.stack);
|
|
71
|
+
// ── Claude side ──
|
|
72
|
+
const claudeRuleWritten = writeClaudeRule({ claudeRulePath, body, dryRun });
|
|
73
|
+
// ── Codex side ──
|
|
74
|
+
const agentsMdInjected = upsertAgentsMd({ agentsMdPath, body, dryRun });
|
|
75
|
+
return {
|
|
76
|
+
stack: detected,
|
|
77
|
+
claudeRulePath,
|
|
78
|
+
claudeRuleWritten,
|
|
79
|
+
agentsMdPath,
|
|
80
|
+
agentsMdInjected,
|
|
81
|
+
bytesWritten: dryRun ? 0 : body.length,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
85
|
+
function writeClaudeRule(opts) {
|
|
86
|
+
const { claudeRulePath, body, dryRun } = opts;
|
|
87
|
+
const existing = fs.existsSync(claudeRulePath)
|
|
88
|
+
? fs.readFileSync(claudeRulePath, 'utf-8')
|
|
89
|
+
: null;
|
|
90
|
+
if (existing === body)
|
|
91
|
+
return false;
|
|
92
|
+
if (!dryRun) {
|
|
93
|
+
fs.mkdirSync(path.dirname(claudeRulePath), { recursive: true });
|
|
94
|
+
fs.writeFileSync(claudeRulePath, body, 'utf-8');
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
function escapeRegex(s) {
|
|
99
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
100
|
+
}
|
|
101
|
+
function buildDevGuideBlock(body) {
|
|
102
|
+
return [AGENTS_MD_BEGIN, body.trimEnd(), AGENTS_MD_END].join('\n');
|
|
103
|
+
}
|
|
104
|
+
function upsertAgentsMd(opts) {
|
|
105
|
+
const { agentsMdPath, body, dryRun } = opts;
|
|
106
|
+
const block = buildDevGuideBlock(body);
|
|
107
|
+
const current = fs.existsSync(agentsMdPath) ? fs.readFileSync(agentsMdPath, 'utf-8') : '';
|
|
108
|
+
const reMarker = new RegExp(`${escapeRegex(AGENTS_MD_BEGIN)}[\\s\\S]*?${escapeRegex(AGENTS_MD_END)}`);
|
|
109
|
+
const hasBlock = reMarker.test(current);
|
|
110
|
+
let newContent;
|
|
111
|
+
if (hasBlock) {
|
|
112
|
+
newContent = current.replace(reMarker, block);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
const beginIdx = current.indexOf(AGENTS_MD_BEGIN);
|
|
116
|
+
const endIdx = current.indexOf(AGENTS_MD_END);
|
|
117
|
+
if (beginIdx !== -1 && endIdx === -1) {
|
|
118
|
+
// self-heal: begin 만 있고 end 손상
|
|
119
|
+
newContent = `${current.slice(0, beginIdx).replace(/\s+$/, '')}\n\n${block}\n`;
|
|
120
|
+
}
|
|
121
|
+
else if (current.length === 0) {
|
|
122
|
+
// 신규 파일
|
|
123
|
+
newContent = `# Agent Instructions (forgen-managed)\n\n${block}\n`;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
// 기존 파일에 append
|
|
127
|
+
newContent = `${current.replace(/\s+$/, '')}\n\n${block}\n`;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (newContent === current)
|
|
131
|
+
return false;
|
|
132
|
+
if (!dryRun) {
|
|
133
|
+
fs.mkdirSync(path.dirname(agentsMdPath), { recursive: true });
|
|
134
|
+
fs.writeFileSync(agentsMdPath, newContent, 'utf-8');
|
|
135
|
+
}
|
|
136
|
+
return true;
|
|
137
|
+
}
|
package/dist/core/doctor.d.ts
CHANGED
|
@@ -2,5 +2,15 @@ export interface DoctorOptions {
|
|
|
2
2
|
/** When true, delete stale session-scoped state files instead of just
|
|
3
3
|
* reporting bloat. Triggered by `forgen doctor --prune-state`. */
|
|
4
4
|
pruneState?: boolean;
|
|
5
|
+
/**
|
|
6
|
+
* When true, auto-fix recoverable failures (e.g. missing plugin cache /
|
|
7
|
+
* stale installPath) by running `npm run build` + `node scripts/postinstall.js`
|
|
8
|
+
* inside the forgen install directory. Triggered by `forgen doctor --repair`.
|
|
9
|
+
*
|
|
10
|
+
* v0.4.8 (E3) — 이전엔 안내문 ("Fix: npm run build && node scripts/postinstall.js")
|
|
11
|
+
* 만 출력했고 사용자가 직접 실행해야 했음. fail-open: repair 실패해도
|
|
12
|
+
* doctor 흐름은 정상 종료.
|
|
13
|
+
*/
|
|
14
|
+
repair?: boolean;
|
|
5
15
|
}
|
|
6
16
|
export declare function runDoctor(opts?: DoctorOptions): Promise<void>;
|
package/dist/core/doctor.js
CHANGED
|
@@ -2,7 +2,8 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import * as os from 'node:os';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import { execFileSync } from 'node:child_process';
|
|
5
|
-
import {
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { FORGEN_HOME, LAB_DIR, ME_BEHAVIOR, ME_DIR, ME_SOLUTIONS, ME_RULES, ME_SKILLS, PACKS_DIR, SESSIONS_DIR, STATE_DIR, V1_SESSIONS_DIR } from './paths.js';
|
|
6
7
|
import { getTimingStats } from '../hooks/shared/hook-timing.js';
|
|
7
8
|
import { countSessionScopedFiles, pruneState } from './state-gc.js';
|
|
8
9
|
import { summarizeAllByHost } from '../store/host-mismatch.js';
|
|
@@ -87,6 +88,29 @@ function renderCodexParity() {
|
|
|
87
88
|
console.log(` ✓ Codex parity green (last run: ${timeStr},${version})`);
|
|
88
89
|
}
|
|
89
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* v0.4.8 (E3): plugin cache / installPath 진단이 실패했을 때 자동 복구.
|
|
93
|
+
* forgen 패키지 디렉토리에서 `npm run build` + postinstall 을 차례로 실행.
|
|
94
|
+
* 실패해도 doctor 자체는 계속 진행 (fail-open).
|
|
95
|
+
*/
|
|
96
|
+
function attemptPluginRepair() {
|
|
97
|
+
try {
|
|
98
|
+
// forgen 패키지 루트 = 현재 파일에서 dist/core/doctor.js 위치 → pkgRoot.
|
|
99
|
+
// dev (src/) 와 prod (dist/) 양쪽 모두 path.resolve(...,'..','..') 로 도달.
|
|
100
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
101
|
+
const pkgRoot = path.resolve(here, '..', '..');
|
|
102
|
+
console.log(`\n [Repair] forgen 패키지 자가복구 시도 — ${pkgRoot}`);
|
|
103
|
+
execFileSync('npm', ['run', 'build'], { cwd: pkgRoot, stdio: 'inherit' });
|
|
104
|
+
execFileSync('node', ['scripts/postinstall.js'], { cwd: pkgRoot, stdio: 'inherit' });
|
|
105
|
+
console.log(' [Repair] 완료. 진단 재실행 권장: forgen doctor');
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
console.warn(` [Repair] 실패: ${e instanceof Error ? e.message : String(e)}`);
|
|
110
|
+
console.warn(' [Repair] 수동 복구: cd <forgen pkgRoot> && npm run build && node scripts/postinstall.js');
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
90
114
|
export async function runDoctor(opts = {}) {
|
|
91
115
|
failedChecks = [];
|
|
92
116
|
console.log('\n Forgen — Diagnostics\n');
|
|
@@ -114,7 +138,9 @@ export async function runDoctor(opts = {}) {
|
|
|
114
138
|
});
|
|
115
139
|
forgenPluginCacheOk = versions.length > 0;
|
|
116
140
|
}
|
|
117
|
-
check('forgen plugin cache', forgenPluginCacheOk,
|
|
141
|
+
check('forgen plugin cache', forgenPluginCacheOk, opts.repair
|
|
142
|
+
? 'Hook execution requires plugin cache. Attempting auto-repair (--repair)…'
|
|
143
|
+
: 'Hook execution requires plugin cache. Fix: npm run build && node scripts/postinstall.js (or rerun with --repair)');
|
|
118
144
|
// installed_plugins.json 정합성 확인
|
|
119
145
|
const installedPluginsPath = path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json');
|
|
120
146
|
let pluginRegistered = false;
|
|
@@ -129,7 +155,15 @@ export async function runDoctor(opts = {}) {
|
|
|
129
155
|
}
|
|
130
156
|
catch { /* ignore */ }
|
|
131
157
|
}
|
|
132
|
-
check('forgen plugin registered & installPath exists', pluginRegistered,
|
|
158
|
+
check('forgen plugin registered & installPath exists', pluginRegistered, opts.repair
|
|
159
|
+
? 'Plugin registered but installPath missing on disk. Attempting auto-repair (--repair)…'
|
|
160
|
+
: 'Plugin registered but installPath missing on disk. Fix: npm run build && node scripts/postinstall.js (or rerun with --repair)');
|
|
161
|
+
// v0.4.8 (E3): plugin cache 또는 installPath 가 깨졌고 --repair 가 켜져
|
|
162
|
+
// 있으면 build + postinstall 자동 실행. doctor 진단 자체는 계속 진행하여
|
|
163
|
+
// 사용자가 다른 health 항목도 한 번에 확인 가능.
|
|
164
|
+
if (opts.repair && (!forgenPluginCacheOk || !pluginRegistered)) {
|
|
165
|
+
attemptPluginRepair();
|
|
166
|
+
}
|
|
133
167
|
console.log();
|
|
134
168
|
section('Directories');
|
|
135
169
|
check('~/.forgen/', exists(FORGEN_HOME));
|
|
@@ -138,7 +172,8 @@ export async function runDoctor(opts = {}) {
|
|
|
138
172
|
check('~/.forgen/me/behavior/', exists(ME_BEHAVIOR));
|
|
139
173
|
check('~/.forgen/me/rules/', exists(ME_RULES));
|
|
140
174
|
check('~/.forgen/packs/', exists(PACKS_DIR));
|
|
141
|
-
check('~/.forgen/sessions/', exists(SESSIONS_DIR));
|
|
175
|
+
check('~/.forgen/sessions/ (session logs)', exists(SESSIONS_DIR));
|
|
176
|
+
check('~/.forgen/state/sessions/ (v1 effective state)', exists(V1_SESSIONS_DIR));
|
|
142
177
|
// R9-IA5: warn if a user dropped rule files at ~/.forgen/rules/ by mistake.
|
|
143
178
|
// That path is NOT loaded — personal rules live at ~/.forgen/me/rules/.
|
|
144
179
|
const legacyRulesPath = path.join(FORGEN_HOME, 'rules');
|
|
@@ -194,10 +229,16 @@ export async function runDoctor(opts = {}) {
|
|
|
194
229
|
}
|
|
195
230
|
console.log();
|
|
196
231
|
console.log(' [Log Locations]');
|
|
197
|
-
console.log(` Session logs:
|
|
232
|
+
console.log(` Session logs: ${SESSIONS_DIR}`);
|
|
198
233
|
if (exists(SESSIONS_DIR)) {
|
|
199
234
|
const sessionCount = fs.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith('.json')).length;
|
|
200
|
-
console.log(` Saved sessions:
|
|
235
|
+
console.log(` Saved sessions: ${sessionCount}`);
|
|
236
|
+
}
|
|
237
|
+
// v0.4.8 (A3): v1 effective state directory 도 가시화 — 두 dir 책임 다름.
|
|
238
|
+
console.log(` V1 effective state: ${V1_SESSIONS_DIR}`);
|
|
239
|
+
if (exists(V1_SESSIONS_DIR)) {
|
|
240
|
+
const stateCount = fs.readdirSync(V1_SESSIONS_DIR).filter((f) => f.endsWith('.json')).length;
|
|
241
|
+
console.log(` V1 state count: ${stateCount}`);
|
|
201
242
|
}
|
|
202
243
|
console.log(` Claude Code sessions: ${CLAUDE_PROJECTS_DIR}`);
|
|
203
244
|
console.log();
|
|
@@ -244,7 +285,7 @@ export async function runDoctor(opts = {}) {
|
|
|
244
285
|
}
|
|
245
286
|
else {
|
|
246
287
|
console.log(' Hook Count p50ms p95ms max ms');
|
|
247
|
-
console.log(
|
|
288
|
+
console.log(` ${'-'.repeat(56)}`);
|
|
248
289
|
for (const s of timingStats) {
|
|
249
290
|
const hook = s.hook.padEnd(22);
|
|
250
291
|
const count = String(s.count).padStart(5);
|
|
@@ -499,7 +540,7 @@ export async function runDoctor(opts = {}) {
|
|
|
499
540
|
for (const f of failedChecks) {
|
|
500
541
|
if (!bySection.has(f.section))
|
|
501
542
|
bySection.set(f.section, []);
|
|
502
|
-
bySection.get(f.section)
|
|
543
|
+
bySection.get(f.section)?.push(f);
|
|
503
544
|
}
|
|
504
545
|
for (const [sec, items] of bySection) {
|
|
505
546
|
console.log(` [${sec}]`);
|
package/dist/core/harness.js
CHANGED
|
@@ -165,7 +165,7 @@ function ensureCompoundMemory(cwd) {
|
|
|
165
165
|
const content = fs.readFileSync(memoryMdPath, 'utf-8');
|
|
166
166
|
if (content.includes('compound-index.md'))
|
|
167
167
|
return;
|
|
168
|
-
fs.writeFileSync(memoryMdPath, content.trimEnd()
|
|
168
|
+
fs.writeFileSync(memoryMdPath, `${content.trimEnd()}\n${compoundPointer}\n`);
|
|
169
169
|
}
|
|
170
170
|
const indexPath = path.join(memoryDir, 'compound-index.md');
|
|
171
171
|
const solutionsDir = ME_SOLUTIONS;
|
|
@@ -262,7 +262,7 @@ function migrateToForgen() {
|
|
|
262
262
|
catch (e) {
|
|
263
263
|
log.debug(`migrateToForgen: ${legacyHome} 파일 복사 중 오류`, e);
|
|
264
264
|
}
|
|
265
|
-
const backupPath = legacyHome
|
|
265
|
+
const backupPath = `${legacyHome}.bak`;
|
|
266
266
|
try {
|
|
267
267
|
if (!fs.existsSync(backupPath)) {
|
|
268
268
|
fs.renameSync(legacyHome, backupPath);
|
|
@@ -348,6 +348,12 @@ export async function prepareHarness(cwd, options = {}) {
|
|
|
348
348
|
if (v1Result.legacyBackupPath) {
|
|
349
349
|
log.debug(`v1: 레거시 프로필 백업 완료 → ${v1Result.legacyBackupPath}`);
|
|
350
350
|
}
|
|
351
|
+
if (v1Result.corruptProfileBackupPath) {
|
|
352
|
+
// v0.4.8 — corrupt profile auto-repair 결과는 debug 가 아닌 user-visible warning.
|
|
353
|
+
// 사용자가 onboarding 으로 보내지는 이유를 알 수 있도록.
|
|
354
|
+
console.warn(` ⚠ forgen: profile.json 이 깨져 있어 옆에 백업해두고 onboarding 으로 보냅니다.`);
|
|
355
|
+
console.warn(` ⚠ backup: ${v1Result.corruptProfileBackupPath}`);
|
|
356
|
+
}
|
|
351
357
|
if (v1Result.session) {
|
|
352
358
|
const { session } = v1Result;
|
|
353
359
|
log.debug(`v1 세션 시작: ${session.quality_pack}/${session.autonomy_pack}, trust=${session.effective_trust_policy}`);
|
package/dist/core/init.js
CHANGED
|
@@ -9,6 +9,7 @@ import * as path from 'node:path';
|
|
|
9
9
|
import { profileExists } from '../store/profile-store.js';
|
|
10
10
|
import { ensureV1Directories } from './v1-bootstrap.js';
|
|
11
11
|
import { initializeForgenHome } from './init-cli.js';
|
|
12
|
+
import { injectDevGuidePrinciples } from './dev-guide-injector.js';
|
|
12
13
|
// ── CLI 핸들러 ──
|
|
13
14
|
export async function handleInit(_args) {
|
|
14
15
|
const cwd = process.cwd();
|
|
@@ -19,6 +20,21 @@ export async function handleInit(_args) {
|
|
|
19
20
|
// 프로젝트 .claude/rules 디렉토리 생성
|
|
20
21
|
const rulesDir = path.join(cwd, '.claude', 'rules');
|
|
21
22
|
fs.mkdirSync(rulesDir, { recursive: true });
|
|
23
|
+
// dev-guide principles 자동 inject — profile 존재 여부와 무관한 stateless 작업.
|
|
24
|
+
// profileExists() early-return / onboarding 흐름 어디든 동일하게 통과해야 하므로
|
|
25
|
+
// 두 분기 앞쪽에 배치 (기존 위치는 unreachable 경로였음).
|
|
26
|
+
const pkgRoot = path.resolve(import.meta.dirname ?? __dirname, '..', '..');
|
|
27
|
+
const injectResult = injectDevGuidePrinciples({ cwd, pkgRoot });
|
|
28
|
+
if (injectResult.stack) {
|
|
29
|
+
const { side, stack } = injectResult.stack;
|
|
30
|
+
const stackLabel = side === 'fe'
|
|
31
|
+
? (stack === 'react' ? 'React/Next' : 'Vue/Nuxt')
|
|
32
|
+
: (stack === 'go' ? 'Go' : 'Node.js');
|
|
33
|
+
console.log(` 💡 ${stackLabel} 프로젝트 감지됨. dev-guide principles 자동 inject:`);
|
|
34
|
+
console.log(` ${injectResult.claudeRulePath} (Claude)`);
|
|
35
|
+
console.log(` ${injectResult.agentsMdPath} (Codex, managed block)`);
|
|
36
|
+
suggestStackSkills(cwd);
|
|
37
|
+
}
|
|
22
38
|
// v0.4.1 (2026-04-24): starter-pack 프로비저닝 — 격리 홈 / 신규 FORGEN_HOME
|
|
23
39
|
// 에서 "신규 사용자 첫날 가치" 가 0이 되는 결함 해소. npm install-g 시의
|
|
24
40
|
// postinstall 이 하던 starter 배포를 런타임에서도 보장.
|
|
@@ -52,3 +68,40 @@ export async function handleInit(_args) {
|
|
|
52
68
|
console.log(' forgen inspect profile View your profile');
|
|
53
69
|
console.log(' forgen doctor Check system health\n');
|
|
54
70
|
}
|
|
71
|
+
// 감지된 스택을 알리고, fgx 번들에 포함된 dev-guide 스킬을 안내.
|
|
72
|
+
// 실제 설치는 install-claude.ts 가 ~/.claude/skills/ 에 처리.
|
|
73
|
+
function suggestStackSkills(cwd) {
|
|
74
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
75
|
+
const goModPath = path.join(cwd, 'go.mod');
|
|
76
|
+
let stack = null;
|
|
77
|
+
let skills = [];
|
|
78
|
+
if (fs.existsSync(pkgPath)) {
|
|
79
|
+
try {
|
|
80
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
81
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies, ...pkg.peerDependencies };
|
|
82
|
+
if ('react' in deps || 'next' in deps) {
|
|
83
|
+
stack = 'React/Next';
|
|
84
|
+
skills = ['fe-build-react', 'fe-review-react', 'fe-perf-react'];
|
|
85
|
+
}
|
|
86
|
+
else if ('vue' in deps || 'nuxt' in deps) {
|
|
87
|
+
stack = 'Vue/Nuxt';
|
|
88
|
+
skills = ['fe-build-vue', 'fe-review-vue', 'fe-perf-vue'];
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
stack = 'Node.js';
|
|
92
|
+
skills = ['be-build-node', 'be-review-node', 'be-perf-node', 'be-security-node'];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch { /* ignore */ }
|
|
96
|
+
}
|
|
97
|
+
else if (fs.existsSync(goModPath)) {
|
|
98
|
+
stack = 'Go';
|
|
99
|
+
skills = ['be-build-go', 'be-review-go', 'be-perf-go', 'be-security-go'];
|
|
100
|
+
}
|
|
101
|
+
if (!stack)
|
|
102
|
+
return;
|
|
103
|
+
console.log(` 💡 ${stack} 프로젝트로 감지됨. fgx 번들 dev-guide 스킬 사용 가능:`);
|
|
104
|
+
for (const s of skills)
|
|
105
|
+
console.log(` /${s}`);
|
|
106
|
+
console.log('');
|
|
107
|
+
}
|