@wooojin/forgen 0.4.8 → 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/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/cli.js +42 -0
- package/dist/core/dashboard-cli.d.ts +12 -0
- package/dist/core/dashboard-cli.js +226 -0
- package/dist/core/dev-guide-injector.d.ts +26 -0
- package/dist/core/dev-guide-injector.js +137 -0
- package/dist/core/init.js +53 -0
- package/dist/core/lifecycle-classifier.d.ts +23 -0
- package/dist/core/lifecycle-classifier.js +104 -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/session-store.js +4 -0
- package/dist/core/spawn.d.ts +17 -0
- package/dist/core/spawn.js +179 -2
- package/dist/core/statusline-cli.js +34 -1
- package/dist/engine/compound-extractor.js +39 -0
- 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/hooks/context-guard.js +25 -1
- package/dist/hooks/post-tool-use.js +48 -0
- package/dist/hooks/solution-injector.js +93 -0
- 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 +71 -0
- package/dist/host/install-orchestrator.js +1 -0
- package/package.json +6 -6
- package/plugin.json +1 -1
- package/scripts/postinstall.js +134 -0
|
@@ -30,6 +30,9 @@ import { STATE_DIR } from '../core/paths.js';
|
|
|
30
30
|
import { recordHookTiming } from './shared/hook-timing.js';
|
|
31
31
|
import { appendPending, flushAccept } from '../engine/solution-outcomes.js';
|
|
32
32
|
import { appendImplicitFeedback } from '../store/implicit-feedback-store.js';
|
|
33
|
+
import { emitSolutionEvent, querySurfacedWithin } from '../core/observability-store.js';
|
|
34
|
+
import { parseSolutionV3 } from '../engine/solution-format.js';
|
|
35
|
+
import { ME_SOLUTIONS } from '../core/paths.js';
|
|
33
36
|
const MAX_SOLUTIONS_PER_SESSION = 10;
|
|
34
37
|
/**
|
|
35
38
|
* Minimum relevance thresholds by fitness state (2026-04-21 gate sweep).
|
|
@@ -252,6 +255,56 @@ function backfillCacheTagsOnDisk(cachePath, allMatched) {
|
|
|
252
255
|
}
|
|
253
256
|
}
|
|
254
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* 직전 5분 내 surfaced 된 솔루션이 현재 프롬프트와 키워드 매칭 시 acted_on emit.
|
|
260
|
+
* tags/identifiers 가 없는 솔루션은 skip. fail-open.
|
|
261
|
+
*/
|
|
262
|
+
async function detectActOnFromPriorSurface(sessionId, promptLower) {
|
|
263
|
+
try {
|
|
264
|
+
const recentSurfaces = querySurfacedWithin(sessionId, 5);
|
|
265
|
+
if (recentSurfaces.length === 0)
|
|
266
|
+
return;
|
|
267
|
+
const seen = new Set();
|
|
268
|
+
for (const surf of recentSurfaces) {
|
|
269
|
+
if (seen.has(surf.solutionId))
|
|
270
|
+
continue;
|
|
271
|
+
seen.add(surf.solutionId);
|
|
272
|
+
// 솔루션 파일 로드 (ME_SOLUTIONS 기준)
|
|
273
|
+
const filePath = path.join(ME_SOLUTIONS, `${surf.solutionId}.md`);
|
|
274
|
+
if (!fs.existsSync(filePath))
|
|
275
|
+
continue;
|
|
276
|
+
let raw;
|
|
277
|
+
try {
|
|
278
|
+
raw = fs.readFileSync(filePath, 'utf-8');
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
const sol = parseSolutionV3(raw);
|
|
284
|
+
if (!sol)
|
|
285
|
+
continue;
|
|
286
|
+
const tags = sol.frontmatter.tags ?? [];
|
|
287
|
+
const identifiers = sol.frontmatter.identifiers ?? [];
|
|
288
|
+
if (tags.length === 0 && identifiers.length === 0)
|
|
289
|
+
continue;
|
|
290
|
+
const hit = tags.some(t => promptLower.includes(t.toLowerCase()))
|
|
291
|
+
|| identifiers.some(id => promptLower.includes(id.toLowerCase()));
|
|
292
|
+
if (!hit)
|
|
293
|
+
continue;
|
|
294
|
+
emitSolutionEvent({
|
|
295
|
+
sessionId,
|
|
296
|
+
solutionId: surf.solutionId,
|
|
297
|
+
eventType: 'acted_on',
|
|
298
|
+
signalSource: 'prompt-keyword',
|
|
299
|
+
signalScore: 0.20,
|
|
300
|
+
meta: { surface_ts: surf.ts },
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
catch (e) {
|
|
305
|
+
log.debug('detectActOnFromPriorSurface 실패', e);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
255
308
|
async function main() {
|
|
256
309
|
const _hookStart = Date.now();
|
|
257
310
|
try {
|
|
@@ -265,6 +318,8 @@ async function main() {
|
|
|
265
318
|
return;
|
|
266
319
|
}
|
|
267
320
|
const sessionId = input.session_id ?? 'default';
|
|
321
|
+
// Observability P2: 직전 surfaced 솔루션과 현재 프롬프트 키워드 매칭 → acted_on emit
|
|
322
|
+
await detectActOnFromPriorSurface(sessionId, input.prompt.toLowerCase());
|
|
268
323
|
// v1: 교정 감지 → correction-record 호출 유도 hint
|
|
269
324
|
const correctionPatterns = /하지\s*마|그렇게\s*말고|앞으로는|이렇게\s*해|stop\s+doing|don'?t\s+do|always\s+do|never\s+do|아니\s*그게\s*아니라/i;
|
|
270
325
|
if (correctionPatterns.test(input.prompt)) {
|
|
@@ -294,6 +349,25 @@ async function main() {
|
|
|
294
349
|
// 다시 매칭되면 그 정보로 cache의 missing tags를 채울 수 있다.
|
|
295
350
|
// matches는 새 주입 후보 (이미 injected는 제외).
|
|
296
351
|
const allMatched = matchSolutions(input.prompt, scope, cwd);
|
|
352
|
+
// Observability P1: matched emit — top-5 후보 각각 기록
|
|
353
|
+
try {
|
|
354
|
+
for (const candidate of allMatched.slice(0, 5)) {
|
|
355
|
+
emitSolutionEvent({
|
|
356
|
+
sessionId,
|
|
357
|
+
solutionId: candidate.name,
|
|
358
|
+
eventType: 'matched',
|
|
359
|
+
signalSource: 'matcher',
|
|
360
|
+
signalScore: candidate.relevance,
|
|
361
|
+
meta: {
|
|
362
|
+
matchedTags: candidate.matchedTags,
|
|
363
|
+
matchedIdentifiers: candidate.matchedIdentifiers,
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
catch (e) {
|
|
369
|
+
log.debug('matched emit 실패', e);
|
|
370
|
+
}
|
|
297
371
|
const matches = allMatched.filter(m => !injected.has(m.name));
|
|
298
372
|
// T3: emit a ranking-decision record for offline analysis. Fail-open —
|
|
299
373
|
// the logger swallows any error so this never blocks hook approval.
|
|
@@ -566,6 +640,25 @@ async function main() {
|
|
|
566
640
|
catch (e) {
|
|
567
641
|
log.debug('recommendation_surfaced emit 실패', e);
|
|
568
642
|
}
|
|
643
|
+
// Observability P1: surfaced emit — approveWithContext 직전 effectiveToInject 각각 기록
|
|
644
|
+
try {
|
|
645
|
+
for (const sol of effectiveToInject) {
|
|
646
|
+
emitSolutionEvent({
|
|
647
|
+
sessionId,
|
|
648
|
+
solutionId: sol.name,
|
|
649
|
+
eventType: 'surfaced',
|
|
650
|
+
signalSource: 'hook-prepend',
|
|
651
|
+
signalScore: sol.relevance,
|
|
652
|
+
meta: {
|
|
653
|
+
surfaceChars: fullInjection.length,
|
|
654
|
+
injectionMode: 'context-prepend',
|
|
655
|
+
},
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
catch (e) {
|
|
660
|
+
log.debug('surfaced emit 실패', e);
|
|
661
|
+
}
|
|
569
662
|
// H1: 사용자 UI 에 recall hit 1줄 노출. additionalContext 는 모델 전용이라
|
|
570
663
|
// v0.4.0 에서 8,000+ 주입이 발생했는데도 사용자는 0건을 봤다. systemMessage
|
|
571
664
|
// 로 "N개 솔루션 참조" 를 surface → 사용자가 어떤 축적 지식이 붙었는지 인식.
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Claude InstallPlan — feat/codex-support Phase 1 (P1-2)
|
|
3
3
|
*
|
|
4
|
-
* `npm install` postinstall.js 의 *Claude 측*
|
|
4
|
+
* `npm install` postinstall.js 의 *Claude 측* 5 작업을 module 로 분리.
|
|
5
5
|
* `forgen install claude` CLI 가 호출 + (P1-6 에서) postinstall.js 도 위임.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* 5 작업:
|
|
8
8
|
* 1. Plugin cache: ~/.claude/plugins/cache/forgen-local/forgen/<ver>/ 작성 + installed_plugins.json 등록
|
|
9
9
|
* 2. Slash commands: ~/.claude/commands/forgen/*.md 생성 (forgen-managed marker)
|
|
10
10
|
* 3. Settings hooks injection: ~/.claude/settings.json 의 hooks 머지 (forgen entry idempotent)
|
|
11
11
|
* 4. MCP register: ~/.claude.json 에 mcpServers.forgen-compound 추가
|
|
12
|
+
* 5. Dev-guide skills: ~/.claude/skills/forgen-<stack>-<skill>/ 설치 (forgen-managed only)
|
|
12
13
|
*
|
|
13
14
|
* 사용자 비-forgen 자산 보존 + 재실행 idempotent.
|
|
14
15
|
*/
|
|
@@ -31,5 +32,8 @@ export interface ClaudeInstallResult {
|
|
|
31
32
|
hooksInjected: number;
|
|
32
33
|
mcpRegistered: boolean;
|
|
33
34
|
mcpAlreadyPresent: boolean;
|
|
35
|
+
skillsPath: string;
|
|
36
|
+
skillsInstalled: number;
|
|
37
|
+
skillsRemoved: number;
|
|
34
38
|
}
|
|
35
39
|
export declare function planClaudeInstall(opts: ClaudeInstallOptions): ClaudeInstallResult;
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Claude InstallPlan — feat/codex-support Phase 1 (P1-2)
|
|
3
3
|
*
|
|
4
|
-
* `npm install` postinstall.js 의 *Claude 측*
|
|
4
|
+
* `npm install` postinstall.js 의 *Claude 측* 5 작업을 module 로 분리.
|
|
5
5
|
* `forgen install claude` CLI 가 호출 + (P1-6 에서) postinstall.js 도 위임.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* 5 작업:
|
|
8
8
|
* 1. Plugin cache: ~/.claude/plugins/cache/forgen-local/forgen/<ver>/ 작성 + installed_plugins.json 등록
|
|
9
9
|
* 2. Slash commands: ~/.claude/commands/forgen/*.md 생성 (forgen-managed marker)
|
|
10
10
|
* 3. Settings hooks injection: ~/.claude/settings.json 의 hooks 머지 (forgen entry idempotent)
|
|
11
11
|
* 4. MCP register: ~/.claude.json 에 mcpServers.forgen-compound 추가
|
|
12
|
+
* 5. Dev-guide skills: ~/.claude/skills/forgen-<stack>-<skill>/ 설치 (forgen-managed only)
|
|
12
13
|
*
|
|
13
14
|
* 사용자 비-forgen 자산 보존 + 재실행 idempotent.
|
|
14
15
|
*/
|
|
@@ -234,6 +235,72 @@ function registerMcpInClaudeJson(opts) {
|
|
|
234
235
|
fs.writeFileSync(claudeJsonPath, `${JSON.stringify(claudeJson, null, 2)}\n`);
|
|
235
236
|
return { registered: !alreadyPresent, alreadyPresent };
|
|
236
237
|
}
|
|
238
|
+
// ── 5. Dev-guide skills ────────────────────────────────────────────────
|
|
239
|
+
const FORGEN_SKILL_PREFIX = 'forgen-';
|
|
240
|
+
function installDevGuideSkills(opts) {
|
|
241
|
+
const { pkgRoot, skillsDir, dryRun } = opts;
|
|
242
|
+
const devGuideRoot = path.join(pkgRoot, 'assets', 'dev-guide');
|
|
243
|
+
// Collect all SKILL.md entries: assets/dev-guide/{tier}/skills/{stack}/{skill}/SKILL.md
|
|
244
|
+
const entries = [];
|
|
245
|
+
if (fs.existsSync(devGuideRoot)) {
|
|
246
|
+
for (const tier of fs.readdirSync(devGuideRoot)) {
|
|
247
|
+
const skillsBase = path.join(devGuideRoot, tier, 'skills');
|
|
248
|
+
if (!fs.existsSync(skillsBase))
|
|
249
|
+
continue;
|
|
250
|
+
for (const stack of fs.readdirSync(skillsBase)) {
|
|
251
|
+
const stackDir = path.join(skillsBase, stack);
|
|
252
|
+
if (!fs.statSync(stackDir).isDirectory())
|
|
253
|
+
continue;
|
|
254
|
+
for (const skill of fs.readdirSync(stackDir)) {
|
|
255
|
+
const skillMd = path.join(stackDir, skill, 'SKILL.md');
|
|
256
|
+
if (fs.existsSync(skillMd)) {
|
|
257
|
+
entries.push({ name: `${FORGEN_SKILL_PREFIX}${stack}-${skill}`, src: skillMd });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (dryRun) {
|
|
264
|
+
return { skillsPath: skillsDir, skillsInstalled: entries.length, skillsRemoved: 0 };
|
|
265
|
+
}
|
|
266
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
267
|
+
// Remove stale forgen-* skill dirs (idempotent re-install, do not touch user's own skills)
|
|
268
|
+
let removed = 0;
|
|
269
|
+
for (const entry of fs.readdirSync(skillsDir)) {
|
|
270
|
+
if (!entry.startsWith(FORGEN_SKILL_PREFIX))
|
|
271
|
+
continue;
|
|
272
|
+
const fullPath = path.join(skillsDir, entry);
|
|
273
|
+
if (fs.statSync(fullPath).isDirectory()) {
|
|
274
|
+
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
275
|
+
removed += 1;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Install each skill via symlink → cpSync fallback (mirrors plugin cache pattern)
|
|
279
|
+
let installed = 0;
|
|
280
|
+
for (const { name, src } of entries) {
|
|
281
|
+
const targetDir = path.join(skillsDir, name);
|
|
282
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
283
|
+
const targetFile = path.join(targetDir, 'SKILL.md');
|
|
284
|
+
let linked = false;
|
|
285
|
+
let symlinkErr = null;
|
|
286
|
+
try {
|
|
287
|
+
fs.symlinkSync(src, targetFile, 'file');
|
|
288
|
+
linked = true;
|
|
289
|
+
}
|
|
290
|
+
catch (e) {
|
|
291
|
+
symlinkErr = e;
|
|
292
|
+
}
|
|
293
|
+
if (!linked && symlinkErr) {
|
|
294
|
+
const code = symlinkErr.code ?? 'UNKNOWN';
|
|
295
|
+
process.stderr.write(`[forgen] symlink ${src} → ${targetFile} failed (${code}); falling back to copyFile.\n`);
|
|
296
|
+
}
|
|
297
|
+
if (!linked) {
|
|
298
|
+
fs.copyFileSync(src, targetFile);
|
|
299
|
+
}
|
|
300
|
+
installed += 1;
|
|
301
|
+
}
|
|
302
|
+
return { skillsPath: skillsDir, skillsInstalled: installed, skillsRemoved: removed };
|
|
303
|
+
}
|
|
237
304
|
// ── public ─────────────────────────────────────────────────────────────
|
|
238
305
|
export function planClaudeInstall(opts) {
|
|
239
306
|
if (!opts.pkgRoot || !fs.existsSync(opts.pkgRoot)) {
|
|
@@ -249,12 +316,14 @@ export function planClaudeInstall(opts) {
|
|
|
249
316
|
const slashCommandsDir = path.join(claudeDir, 'commands', 'forgen');
|
|
250
317
|
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
251
318
|
const claudeJsonPath = path.join(homeDir, '.claude.json');
|
|
319
|
+
const skillsDir = path.join(claudeDir, 'skills');
|
|
252
320
|
const pluginCacheWritten = writePluginCache({ pkgRoot: opts.pkgRoot, cacheDir, pluginsDir, version, dryRun });
|
|
253
321
|
const slashCommandsCount = writeSlashCommands({ pkgRoot: opts.pkgRoot, targetDir: slashCommandsDir, dryRun });
|
|
254
322
|
const hooksInjected = injectHooksIntoSettings({ pkgRoot: opts.pkgRoot, settingsPath, dryRun });
|
|
255
323
|
const mcp = registerMcp
|
|
256
324
|
? registerMcpInClaudeJson({ pkgRoot: opts.pkgRoot, claudeJsonPath, dryRun })
|
|
257
325
|
: { registered: false, alreadyPresent: false };
|
|
326
|
+
const skills = installDevGuideSkills({ pkgRoot: opts.pkgRoot, skillsDir, dryRun });
|
|
258
327
|
return {
|
|
259
328
|
homeDir,
|
|
260
329
|
pluginCachePath: cacheDir,
|
|
@@ -265,5 +334,8 @@ export function planClaudeInstall(opts) {
|
|
|
265
334
|
hooksInjected,
|
|
266
335
|
mcpRegistered: mcp.registered,
|
|
267
336
|
mcpAlreadyPresent: mcp.alreadyPresent,
|
|
337
|
+
skillsPath: skills.skillsPath,
|
|
338
|
+
skillsInstalled: skills.skillsInstalled,
|
|
339
|
+
skillsRemoved: skills.skillsRemoved,
|
|
268
340
|
};
|
|
269
341
|
}
|
|
@@ -40,5 +40,9 @@ export interface CodexInstallResult {
|
|
|
40
40
|
/** P3-3: AGENTS.md (cwd) 에 forgen rule block 인젝션 여부 */
|
|
41
41
|
agentsMdPath: string;
|
|
42
42
|
agentsMdInjected: boolean;
|
|
43
|
+
/** v0.4.9: dev-guide skills (~/.codex/skills) 설치 결과 */
|
|
44
|
+
devGuideSkillsPath: string;
|
|
45
|
+
devGuideSkillsInstalled: number;
|
|
46
|
+
devGuideSkillsRemoved: number;
|
|
43
47
|
}
|
|
44
48
|
export declare function planCodexInstall(opts: CodexInstallOptions): CodexInstallResult;
|
|
@@ -150,6 +150,12 @@ export function planCodexInstall(opts) {
|
|
|
150
150
|
// pkgRoot 의 git repo root 의 AGENTS.md, 또는 explicit override.
|
|
151
151
|
const agentsMdPath = opts.agentsMdPath ?? resolveAgentsMdPath(opts.pkgRoot);
|
|
152
152
|
const agentsResult = upsertForgenRulesInAgentsMd({ agentsMdPath, pkgRoot: opts.pkgRoot, dryRun: opts.dryRun ?? false });
|
|
153
|
+
// 8) v0.4.9: dev-guide skills → ~/.codex/skills/forgen-<stack>-<skill>/SKILL.md
|
|
154
|
+
const devGuideResult = installDevGuideSkillsToCodex({
|
|
155
|
+
pkgRoot: opts.pkgRoot,
|
|
156
|
+
codexHome,
|
|
157
|
+
dryRun: opts.dryRun ?? false,
|
|
158
|
+
});
|
|
153
159
|
return {
|
|
154
160
|
codexHome,
|
|
155
161
|
hooksPath,
|
|
@@ -163,8 +169,73 @@ export function planCodexInstall(opts) {
|
|
|
163
169
|
skillsPath,
|
|
164
170
|
agentsMdPath,
|
|
165
171
|
agentsMdInjected: agentsResult.injected,
|
|
172
|
+
devGuideSkillsPath: devGuideResult.devGuideSkillsPath,
|
|
173
|
+
devGuideSkillsInstalled: devGuideResult.devGuideSkillsInstalled,
|
|
174
|
+
devGuideSkillsRemoved: devGuideResult.devGuideSkillsRemoved,
|
|
166
175
|
};
|
|
167
176
|
}
|
|
177
|
+
// ── v0.4.9: dev-guide skills → ~/.codex/skills ────────────────────────
|
|
178
|
+
// dev-guide prefix pattern: forgen-<stack>-<skill> (e.g. forgen-react-fe-build)
|
|
179
|
+
// 반드시 stack 이 react|vue|node|go 인 것만 매칭 — forgen 자체 commands 보존
|
|
180
|
+
const DEV_GUIDE_SKILL_PATTERN = /^forgen-(react|vue|node|go)-/;
|
|
181
|
+
function installDevGuideSkillsToCodex(opts) {
|
|
182
|
+
const devGuideRoot = path.join(opts.pkgRoot, 'assets', 'dev-guide');
|
|
183
|
+
const codexSkillsDir = path.join(opts.codexHome, 'skills');
|
|
184
|
+
if (!fs.existsSync(devGuideRoot)) {
|
|
185
|
+
return { devGuideSkillsPath: codexSkillsDir, devGuideSkillsInstalled: 0, devGuideSkillsRemoved: 0 };
|
|
186
|
+
}
|
|
187
|
+
// Collect entries: assets/dev-guide/{tier}/skills/{stack}/{skill}/SKILL.md
|
|
188
|
+
const entries = [];
|
|
189
|
+
for (const tier of fs.readdirSync(devGuideRoot)) {
|
|
190
|
+
const skillsBase = path.join(devGuideRoot, tier, 'skills');
|
|
191
|
+
if (!fs.existsSync(skillsBase))
|
|
192
|
+
continue;
|
|
193
|
+
for (const stack of fs.readdirSync(skillsBase)) {
|
|
194
|
+
const stackDir = path.join(skillsBase, stack);
|
|
195
|
+
if (!fs.statSync(stackDir).isDirectory())
|
|
196
|
+
continue;
|
|
197
|
+
for (const skill of fs.readdirSync(stackDir)) {
|
|
198
|
+
const skillMd = path.join(stackDir, skill, 'SKILL.md');
|
|
199
|
+
if (fs.existsSync(skillMd)) {
|
|
200
|
+
entries.push({ name: `forgen-${stack}-${skill}`, src: skillMd });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (opts.dryRun) {
|
|
206
|
+
return { devGuideSkillsPath: codexSkillsDir, devGuideSkillsInstalled: entries.length, devGuideSkillsRemoved: 0 };
|
|
207
|
+
}
|
|
208
|
+
fs.mkdirSync(codexSkillsDir, { recursive: true });
|
|
209
|
+
// Stale cleanup: dev-guide pattern 만 정리 (forgen 자체 commands 보존)
|
|
210
|
+
let removed = 0;
|
|
211
|
+
for (const entry of fs.readdirSync(codexSkillsDir)) {
|
|
212
|
+
if (DEV_GUIDE_SKILL_PATTERN.test(entry)) {
|
|
213
|
+
try {
|
|
214
|
+
fs.rmSync(path.join(codexSkillsDir, entry), { recursive: true, force: true });
|
|
215
|
+
removed++;
|
|
216
|
+
}
|
|
217
|
+
catch { /* best-effort */ }
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Install via symlink → copyFileSync fallback
|
|
221
|
+
let installed = 0;
|
|
222
|
+
for (const { name, src } of entries) {
|
|
223
|
+
const dstDir = path.join(codexSkillsDir, name);
|
|
224
|
+
fs.mkdirSync(dstDir, { recursive: true });
|
|
225
|
+
const dst = path.join(dstDir, 'SKILL.md');
|
|
226
|
+
let linked = false;
|
|
227
|
+
try {
|
|
228
|
+
fs.symlinkSync(src, dst, 'file');
|
|
229
|
+
linked = true;
|
|
230
|
+
}
|
|
231
|
+
catch { /* fallback */ }
|
|
232
|
+
if (!linked) {
|
|
233
|
+
fs.copyFileSync(src, dst);
|
|
234
|
+
}
|
|
235
|
+
installed++;
|
|
236
|
+
}
|
|
237
|
+
return { devGuideSkillsPath: codexSkillsDir, devGuideSkillsInstalled: installed, devGuideSkillsRemoved: removed };
|
|
238
|
+
}
|
|
168
239
|
// ── P3-3: Codex skills install ────────────────────────────────────────
|
|
169
240
|
function installCodexSkills(opts) {
|
|
170
241
|
const { sourceDir, targetDir, dryRun } = opts;
|
|
@@ -109,6 +109,7 @@ export function renderResult(result, dryRun) {
|
|
|
109
109
|
lines.push(` slash commands: ${result.claude.slashCommandsCount} → ${result.claude.slashCommandsPath}`);
|
|
110
110
|
lines.push(` settings.json hooks: ${result.claude.hooksInjected}`);
|
|
111
111
|
lines.push(` MCP: ${result.claude.mcpAlreadyPresent ? 'already present' : (result.claude.mcpRegistered ? 'registered' : 'skipped')}`);
|
|
112
|
+
lines.push(` skills: ${result.claude.skillsInstalled ?? 0} installed → ${result.claude.skillsPath ?? ''}`);
|
|
112
113
|
}
|
|
113
114
|
if (result.codex) {
|
|
114
115
|
lines.push('');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wooojin/forgen",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.9",
|
|
4
4
|
"preferGlobal": true,
|
|
5
5
|
"main": "dist/lib.js",
|
|
6
6
|
"types": "./dist/lib.d.ts",
|
|
@@ -47,19 +47,19 @@
|
|
|
47
47
|
],
|
|
48
48
|
"repository": {
|
|
49
49
|
"type": "git",
|
|
50
|
-
"url": "https://github.com/forgen-team/forgen.git"
|
|
50
|
+
"url": "git+https://github.com/forgen-team/forgen.git"
|
|
51
51
|
},
|
|
52
52
|
"engines": {
|
|
53
|
-
"node": ">=
|
|
53
|
+
"node": ">=22.0.0"
|
|
54
54
|
},
|
|
55
55
|
"type": "module",
|
|
56
56
|
"workspaces": [
|
|
57
57
|
"packages/*"
|
|
58
58
|
],
|
|
59
59
|
"bin": {
|
|
60
|
-
"forgen": "
|
|
61
|
-
"fgx": "
|
|
62
|
-
"forgen-mcp": "
|
|
60
|
+
"forgen": "dist/cli.js",
|
|
61
|
+
"fgx": "dist/fgx.js",
|
|
62
|
+
"forgen-mcp": "dist/mcp/server.js"
|
|
63
63
|
},
|
|
64
64
|
"files": [
|
|
65
65
|
"dist/",
|
package/plugin.json
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -746,6 +746,125 @@ function cleanLegacyMcpFromSettings(settings) {
|
|
|
746
746
|
}
|
|
747
747
|
}
|
|
748
748
|
|
|
749
|
+
/**
|
|
750
|
+
* dev-guide 스킬 14개 자동 설치 — assets/dev-guide/{fe,be}/skills/{stack}/{skill}/SKILL.md
|
|
751
|
+
* → ~/.claude/skills/forgen-<stack>-<skill>/SKILL.md
|
|
752
|
+
*
|
|
753
|
+
* forgen- prefix 로 사용자 own skills 와 격리. idempotent 재실행 시 forgen-* 만 정리 후 재설치.
|
|
754
|
+
*/
|
|
755
|
+
function installDevGuideSkills(home) {
|
|
756
|
+
const devGuideRoot = join(PKG_ROOT, 'assets', 'dev-guide');
|
|
757
|
+
if (!existsSync(devGuideRoot)) {
|
|
758
|
+
console.log('[forgen] dev-guide assets not found — skipping skill install');
|
|
759
|
+
return 0;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const userSkillsDir = join(home, '.claude', 'skills');
|
|
763
|
+
mkdirSync(userSkillsDir, { recursive: true });
|
|
764
|
+
|
|
765
|
+
// 1. 기존 forgen-* 디렉토리 정리 (사용자 own 보존)
|
|
766
|
+
let removed = 0;
|
|
767
|
+
for (const entry of readdirSync(userSkillsDir)) {
|
|
768
|
+
if (entry.startsWith('forgen-')) {
|
|
769
|
+
try { rmSync(join(userSkillsDir, entry), { recursive: true, force: true }); removed++; } catch { /* best-effort */ }
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// 2. assets/dev-guide/{side}/skills/{stack}/{skill}/SKILL.md 수집
|
|
774
|
+
let installed = 0;
|
|
775
|
+
for (const side of ['fe', 'be']) {
|
|
776
|
+
const sideSkillsDir = join(devGuideRoot, side, 'skills');
|
|
777
|
+
if (!existsSync(sideSkillsDir)) continue;
|
|
778
|
+
for (const stack of readdirSync(sideSkillsDir)) {
|
|
779
|
+
const stackDir = join(sideSkillsDir, stack);
|
|
780
|
+
if (!statSync(stackDir).isDirectory()) continue;
|
|
781
|
+
for (const skill of readdirSync(stackDir)) {
|
|
782
|
+
const skillSrc = join(stackDir, skill);
|
|
783
|
+
const skillFile = join(skillSrc, 'SKILL.md');
|
|
784
|
+
if (!existsSync(skillFile)) continue;
|
|
785
|
+
|
|
786
|
+
// 대상: ~/.claude/skills/forgen-<stack>-<skill>/SKILL.md
|
|
787
|
+
const dstDir = join(userSkillsDir, `forgen-${stack}-${skill}`);
|
|
788
|
+
mkdirSync(dstDir, { recursive: true });
|
|
789
|
+
const dst = join(dstDir, 'SKILL.md');
|
|
790
|
+
|
|
791
|
+
// symlink → fallback cpSync
|
|
792
|
+
try {
|
|
793
|
+
if (existsSync(dst)) rmSync(dst);
|
|
794
|
+
symlinkSync(skillFile, dst, 'file');
|
|
795
|
+
} catch {
|
|
796
|
+
cpSync(skillFile, dst);
|
|
797
|
+
}
|
|
798
|
+
installed++;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// 3. sudo 케이스 ownership 회복
|
|
804
|
+
fixOwnership(userSkillsDir);
|
|
805
|
+
|
|
806
|
+
console.log(`[forgen] dev-guide skills: ${installed} installed${removed > 0 ? ` (${removed} stale removed)` : ''}`);
|
|
807
|
+
return installed;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* v0.4.9: dev-guide 14 skills → ~/.codex/skills/forgen-<stack>-<skill>/SKILL.md
|
|
812
|
+
* forgen 자체 commands(forgen-compound 등) 와 prefix 겹치지 않도록
|
|
813
|
+
* DEV_GUIDE_SKILL_PATTERN(/^forgen-(react|vue|node|go)-/) 으로 stale 정리.
|
|
814
|
+
*/
|
|
815
|
+
const DEV_GUIDE_SKILL_PATTERN = /^forgen-(react|vue|node|go)-/;
|
|
816
|
+
|
|
817
|
+
function installDevGuideSkillsToCodex(home) {
|
|
818
|
+
const devGuideRoot = join(PKG_ROOT, 'assets', 'dev-guide');
|
|
819
|
+
if (!existsSync(devGuideRoot)) {
|
|
820
|
+
console.log('[forgen] dev-guide assets not found — codex skill install skipped');
|
|
821
|
+
return 0;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const codexSkillsDir = join(home, '.codex', 'skills');
|
|
825
|
+
mkdirSync(codexSkillsDir, { recursive: true });
|
|
826
|
+
|
|
827
|
+
// 1. stale forgen-<stack>-<skill> 정리 (forgen 자체 commands 보존)
|
|
828
|
+
let removed = 0;
|
|
829
|
+
for (const entry of readdirSync(codexSkillsDir)) {
|
|
830
|
+
if (DEV_GUIDE_SKILL_PATTERN.test(entry)) {
|
|
831
|
+
try { rmSync(join(codexSkillsDir, entry), { recursive: true, force: true }); removed++; } catch { /* best-effort */ }
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// 2. assets/dev-guide/{tier}/skills/{stack}/{skill}/SKILL.md 수집
|
|
836
|
+
let installed = 0;
|
|
837
|
+
for (const tier of readdirSync(devGuideRoot)) {
|
|
838
|
+
const sideSkillsDir = join(devGuideRoot, tier, 'skills');
|
|
839
|
+
if (!existsSync(sideSkillsDir)) continue;
|
|
840
|
+
for (const stack of readdirSync(sideSkillsDir)) {
|
|
841
|
+
const stackDir = join(sideSkillsDir, stack);
|
|
842
|
+
if (!statSync(stackDir).isDirectory()) continue;
|
|
843
|
+
for (const skill of readdirSync(stackDir)) {
|
|
844
|
+
const skillFile = join(stackDir, skill, 'SKILL.md');
|
|
845
|
+
if (!existsSync(skillFile)) continue;
|
|
846
|
+
|
|
847
|
+
const dstDir = join(codexSkillsDir, `forgen-${stack}-${skill}`);
|
|
848
|
+
mkdirSync(dstDir, { recursive: true });
|
|
849
|
+
const dst = join(dstDir, 'SKILL.md');
|
|
850
|
+
|
|
851
|
+
// symlink → fallback cpSync
|
|
852
|
+
try {
|
|
853
|
+
if (existsSync(dst)) rmSync(dst);
|
|
854
|
+
symlinkSync(skillFile, dst, 'file');
|
|
855
|
+
} catch {
|
|
856
|
+
cpSync(skillFile, dst);
|
|
857
|
+
}
|
|
858
|
+
installed++;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
fixOwnership(codexSkillsDir);
|
|
864
|
+
console.log(`[forgen] codex dev-guide skills: ${installed} installed${removed > 0 ? ` (${removed} stale removed)` : ''}`);
|
|
865
|
+
return installed;
|
|
866
|
+
}
|
|
867
|
+
|
|
749
868
|
// ── Main ──
|
|
750
869
|
|
|
751
870
|
/**
|
|
@@ -927,6 +1046,21 @@ async function main() {
|
|
|
927
1046
|
console.error(`[forgen] starter pack failed: ${err?.message ?? err}`);
|
|
928
1047
|
}
|
|
929
1048
|
|
|
1049
|
+
// ── 8b. dev-guide 스킬 자동 설치 (claude) ──
|
|
1050
|
+
try {
|
|
1051
|
+
installDevGuideSkills(HOME);
|
|
1052
|
+
} catch (e) {
|
|
1053
|
+
// postinstall 의 "fail-open" 원칙 — npm install 깨뜨리지 않음
|
|
1054
|
+
console.log(`[forgen] dev-guide skills 설치 스킵 (${e?.message ?? e})`);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// ── 8c. dev-guide 스킬 자동 설치 (codex) ──
|
|
1058
|
+
try {
|
|
1059
|
+
installDevGuideSkillsToCodex(HOME);
|
|
1060
|
+
} catch (e) {
|
|
1061
|
+
console.log(`[forgen] codex dev-guide skills 설치 스킵 (${e?.message ?? e})`);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
930
1064
|
// sudo 실행 시 파일 소유권을 실제 유저로 변경
|
|
931
1065
|
fixOwnership(join(HOME, '.claude'), join(HOME, '.forgen'));
|
|
932
1066
|
|