geobuke-code 0.2.6 → 0.2.8

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/README.md CHANGED
@@ -103,8 +103,8 @@ phase-protocol/계획 → /plan(SubTask) → 【게이트: 구현 직전 케이
103
103
  | **업데이트 필요 시** | (PreToolUse·SessionStart) | hook 구버전(②) 또는 신버전 출시(①)면 갱신 안내. PreToolUse는 세션당 1회(`systemMessage` 비차단), SessionStart는 진입 시 표시. `gbc status`는 캐시만 갱신하고 안내는 **표시하지 않는다**(명시 진단 명령). 게이트 통과/차단 동작은 불변 |
104
104
 
105
105
  > 세션 진입 알림만 끄려면 `GBC_NO_SESSION_HINT=1`. 매 대화 종료(Stop) defer 리마인드만 끄려면 `gbc defer mute`(영속, 해제 `unmute` · 스킬 `/gbc-mute`) — 진입 알림은 남는다. 업데이트 안내만 끄려면 `GBC_NO_UPDATE_NOTICE=1`.
106
- > 프로젝트 hook이 구식이거나(SessionStart 누락·옛 명령) 새 버전이 나오면 gbc가 감지해 `gbc init --yes` 재실행 또는 `npm i -g geobuke-code@latest`를 안내한다. PreToolUse 경로로도 알리므로 "설치만 하고 init " 경우에도 도달한다.
107
- > **업데이트 안내(①)는 네트워크를 게이트 핫패스에 들이지 않는다**: `~/.gbc/version-check.json` 캐시만 비교하고, 갱신은 SessionStart·`gbc status`에서만 짧은 타임아웃으로. 조회 실패는 조용히 무시(fail-silent)되어 게이트 결정에 영향이 없다.
106
+ > 프로젝트 hook이 구식이거나(SessionStart 누락·옛 명령) 새 버전이 나오면 gbc가 감지해 **`gbc update`**(전역 최신 + 현재 프로젝트 재init 한방) 또는 수동 `npm i -g geobuke-code@latest → gbc init --yes`를 안내한다. 안내는 **이미 hook이 등록된 프로젝트**(=한 번이라도 `gbc init`을코호트)에만 도달한다 — 전혀 init하지 않은 프로젝트엔 실행할 hook이 없어 구조적으로 알릴 수 없다(gbc는 전역 hook을 깔지 않는다).
107
+ > **업데이트 안내(①)는 네트워크를 게이트 핫패스에 들이지 않는다**: `~/.gbc/version-check.json` 캐시만 비교하고, 갱신 fetch는 SessionStart·`gbc status`에서만 짧은 타임아웃(1.5s)으로. SessionStart는 캐시가 stale이면 **표시 전에 갱신**해 신버전이 그 세션에 바로 뜬다(1세션 지연 없음). 조회 실패는 조용히 무시(fail-silent)되어 게이트 결정에 영향이 없다. 캐시 TTL 24h.
108
108
 
109
109
  ### 시나리오 도출 루프 (수기 입력 불필요)
110
110
 
@@ -135,6 +135,7 @@ phase-protocol/계획 → /plan(SubTask) → 【게이트: 구현 직전 케이
135
135
  | 명령 | 설명 |
136
136
  |---|---|
137
137
  | `gbc init` | hook + `/gate` · `/gbc-mute` 스킬 설치 |
138
+ | `gbc update` | 전역 최신 설치(`npm i -g …@latest`) + 현재 프로젝트 재init 한방. `--dry-run`으로 실행 명령만 미리보기 |
138
139
  | `gbc status` | 게이트 상태 + 로드된 명세 + Stop 리마인드 음소거 여부 |
139
140
  | `gbc defer add "<케이스>"` | 케이스를 명시적으로 미루기 (→ open) |
140
141
  | `gbc defer list` | 미룬 항목 목록 (상태: 미해결/진행중/해결) |
package/dist/cli.js CHANGED
@@ -3,6 +3,7 @@
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { dirname, join } from "node:path";
5
5
  import { homedir } from "node:os";
6
+ import { spawnSync } from "node:child_process";
6
7
  import { mkdirSync, existsSync, readFileSync, writeFileSync, copyFileSync, } from "node:fs";
7
8
  import { runPreToolUse, runStop, runSessionStart } from "./hook.js";
8
9
  import { loadPlanSpec, computeSpecHash, addSpecCase, readSpecCases, clearSpec } from "./spec.js";
@@ -10,7 +11,7 @@ import { loadState, resetGate } from "./state.js";
10
11
  import { addDefer, loadDefers, resolveDefer, startDefer, reopenDefer } from "./defer.js";
11
12
  import { isStopHintMuted, setStopHintMuted } from "./config.js";
12
13
  import { selectedTransport } from "./judge.js";
13
- import { buildPreCommand, normalizeHooks, ensureSessionStartHook } from "./install.js";
14
+ import { buildPreCommand, normalizeHooks, ensureSessionStartHook, DEV_PLACEHOLDER } from "./install.js";
14
15
  import { isCacheStale, readVersionCache, refreshVersionCache, } from "./version.js";
15
16
  import { logEvent, parseEvents, computeMetrics } from "./metrics.js";
16
17
  const CLI_PATH = fileURLToPath(import.meta.url);
@@ -29,8 +30,8 @@ const PKG_VERSION = readPkgVersion();
29
30
  function hasApiKey() {
30
31
  return existsSync(join(homedir(), ".gbc", "api-key"));
31
32
  }
32
- function stopCommand() {
33
- return `node "${CLI_PATH}" hook stop`;
33
+ function stopCommand(hookPath) {
34
+ return `node "${hookPath}" hook stop`;
34
35
  }
35
36
  function nowIso() {
36
37
  try {
@@ -64,6 +65,10 @@ function nowStamp() {
64
65
  async function cmdInit(args) {
65
66
  const cwd = process.cwd();
66
67
  const yes = args.includes("--yes") || args.includes("-y");
68
+ // --dev: hook 명령에 절대경로(CLI_PATH) 대신 ${CLAUDE_PROJECT_DIR} placeholder를 굽는다.
69
+ // geobuke-code 자기 repo 도그푸딩 전용(dist 위치가 옮겨다녀도 안 깨짐). 기본(false)은 절대경로.
70
+ const dev = args.includes("--dev");
71
+ const hookPath = dev ? DEV_PLACEHOLDER : CLI_PATH;
67
72
  const claudeDir = join(cwd, ".claude");
68
73
  const settingsPath = join(claudeDir, "settings.json");
69
74
  // 설치 대상 스킬들(제품소스 skills/<name>/SKILL.md → .claude/skills/<name>/SKILL.md).
@@ -75,7 +80,7 @@ async function cmdInit(args) {
75
80
  1) ${settingsPath} 에 PreToolUse(Edit|Write) + Stop + SessionStart hook 추가 (머지·멱등)
76
81
  - 기존 settings.json 있으면 백업: settings.json.bak-<시각>
77
82
  2) ${join(claudeDir, "skills")} 에 ${skillNames.map((n) => `/${n}`).join(", ")} 스킬 설치
78
- 3) hook 명령: ${buildPreCommand(CLI_PATH)}
83
+ 3) hook 명령: ${buildPreCommand(hookPath)}${dev ? " (--dev: ${CLAUDE_PROJECT_DIR} placeholder)" : ""}
79
84
  ${hasApiKey()
80
85
  ? " (~/.gbc/api-key 감지됨 → 빠른 haiku API 경로로 동작)"
81
86
  : " (~/.gbc/api-key 없음 → claude -p 폴백. 빠른 경로 원하면 키 파일 생성)"}
@@ -104,7 +109,7 @@ ${hasApiKey()
104
109
  if (!serialized.includes("hook pre-tool-use")) {
105
110
  (hooks.PreToolUse ??= []).push({
106
111
  matcher: "Edit|Write|MultiEdit",
107
- hooks: [{ type: "command", command: buildPreCommand(CLI_PATH) }],
112
+ hooks: [{ type: "command", command: buildPreCommand(hookPath) }],
108
113
  });
109
114
  console.log(` + PreToolUse hook 추가`);
110
115
  }
@@ -118,7 +123,7 @@ ${hasApiKey()
118
123
  // Stop (멱등)
119
124
  if (!serialized.includes("hook stop")) {
120
125
  (hooks.Stop ??= []).push({
121
- hooks: [{ type: "command", command: stopCommand() }],
126
+ hooks: [{ type: "command", command: stopCommand(hookPath) }],
122
127
  });
123
128
  console.log(` + Stop hook 추가`);
124
129
  }
@@ -126,7 +131,7 @@ ${hasApiKey()
126
131
  console.log(` = Stop hook 이미 존재 (skip)`);
127
132
  }
128
133
  // SessionStart (멱등) — 세션 진입(startup|resume) 시 미해결 defer 알림
129
- if (ensureSessionStartHook(settings, CLI_PATH)) {
134
+ if (ensureSessionStartHook(settings, hookPath)) {
130
135
  console.log(` + SessionStart hook 추가`);
131
136
  }
132
137
  else {
@@ -329,11 +334,50 @@ function cmdMetrics(args) {
329
334
  게이트 리셋 ${m.m1.resets} · 통과후 churn ${m.m1.churnAfterPass}
330
335
  ⚠️ ${m.m1.note}`);
331
336
  }
337
+ // ---------- gbc update ----------
338
+ /**
339
+ * 전역 최신 설치 + (현재 프로젝트면) 재init을 한 번에. 자동 silent 업데이트가 아니라 명시 명령 —
340
+ * 사용자가 nag를 보고 'gbc update' 한 줄로 갱신한다(매번 두 명령 외울 필요 제거).
341
+ * ★재init은 '새로 깔린' 바이너리를 fresh spawn해야 신규 스킬·hook이 반영된다(현재 실행 중인 건 구버전).
342
+ */
343
+ function cmdUpdate(args) {
344
+ const cwd = process.cwd();
345
+ const dry = args.includes("--dry-run");
346
+ const isProject = existsSync(join(cwd, ".gbc"));
347
+ const steps = ["npm i -g geobuke-code@latest", ...(isProject ? ["gbc init --yes"] : [])];
348
+ if (dry) {
349
+ console.log("🐢 gbc update — 실행 예정(--dry-run):");
350
+ steps.forEach((s) => console.log(` $ ${s}`));
351
+ if (!isProject)
352
+ console.log(" (현재 폴더에 .gbc 없음 → init 생략. 프로젝트에서 'gbc init --yes' 실행)");
353
+ return;
354
+ }
355
+ console.log(`🐢 gbc update — 전역 최신 설치${isProject ? " + 현재 프로젝트 재init" : ""}`);
356
+ // 1) 전역 최신 설치. shell:true + 고정 명령 문자열(사용자 입력 없음 → 인젝션 무관, 크로스플랫폼).
357
+ const r1 = spawnSync("npm i -g geobuke-code@latest", { stdio: "inherit", shell: true });
358
+ if (r1.status !== 0) {
359
+ console.error("❌ 전역 설치 실패. 권한 문제면 관리자 권한(Windows)·sudo 또는 수동 'npm i -g geobuke-code@latest'.");
360
+ process.exit(1);
361
+ }
362
+ // 2) gbc 프로젝트면 재init — 신규 스킬(gbc-mute 등)·최신 hook 반영.
363
+ if (isProject) {
364
+ const r2 = spawnSync("gbc init --yes", { stdio: "inherit", shell: true, cwd });
365
+ if (r2.status !== 0) {
366
+ console.error("⚠️ 전역 설치는 됐으나 'gbc init --yes' 실패 — 프로젝트에서 수동 실행하세요.");
367
+ process.exit(1);
368
+ }
369
+ }
370
+ else {
371
+ console.log("ℹ️ 현재 폴더는 gbc 프로젝트 아님(.gbc 없음) → 각 프로젝트에서 'gbc init --yes' 실행하세요.");
372
+ }
373
+ console.log("✅ gbc update 완료.");
374
+ }
332
375
  function usage() {
333
376
  console.log(`🐢 gbc — 거북이코드 구현-전 게이트
334
377
 
335
378
  사용:
336
- gbc init [--yes] 프로젝트에 hook + /gate 스킬 설치
379
+ gbc init [--yes] 프로젝트에 hook + /gate · /gbc-mute 스킬 설치
380
+ gbc update [--dry-run] 전역 최신 설치 + 현재 프로젝트 재init (한방 갱신)
337
381
  gbc status 게이트 상태 + 로드된 명세 확인
338
382
  gbc defer add "<케이스>" 케이스를 명시적으로 미루기 (→ open)
339
383
  gbc defer list 미룬 항목 목록 (상태: 미해결/진행중/해결)
@@ -367,6 +411,8 @@ async function main() {
367
411
  break;
368
412
  case "init":
369
413
  return cmdInit(rest);
414
+ case "update":
415
+ return cmdUpdate(rest);
370
416
  case "status":
371
417
  return cmdStatus();
372
418
  case "defer":
package/dist/hook.js CHANGED
@@ -326,8 +326,20 @@ export async function runSessionStart(ctx) {
326
326
  }
327
327
  }
328
328
  }
329
+ // ①신버전 안내 — 캐시가 stale이면 '표시 전에' 먼저 갱신(1.5s 상한, fail-silent)해 이번 세션에
330
+ // 즉시 반영한다(표시-후-갱신의 '다음 세션 지연' 제거). SessionStart는 게이트를 막지 않으므로
331
+ // 짧은 네트워크가 안전(advisor 승인). 갱신은 24h TTL당 1회만 발생.
332
+ try {
333
+ if (ctx?.cliPath && process.env.GBC_NO_UPDATE_NOTICE !== "1" && isCacheStale(readVersionCache())) {
334
+ await refreshVersionCache();
335
+ }
336
+ }
337
+ catch {
338
+ /* 갱신 실패는 무시(fail-silent) */
339
+ }
329
340
  // 업데이트 안내(staleness + version) — SessionStart 보유 코호트(0.2.3+)용. 세션 식별자가 없어
330
341
  // 항상 표시되므로 dedup 대신 GBC_NO_UPDATE_NOTICE opt-out에 맡긴다(buildUpdateNotice 내부).
342
+ // 위에서 갱신된 캐시를 읽으므로 신버전이 뜨는 그 세션에 즉시 표시된다.
331
343
  try {
332
344
  if (ctx?.cliPath) {
333
345
  const notice = buildUpdateNotice(readProjectSettings(cwd), ctx.cliPath, ctx.version ?? "");
@@ -340,15 +352,5 @@ export async function runSessionStart(ctx) {
340
352
  }
341
353
  if (parts.length > 0)
342
354
  process.stdout.write(parts.join("\n"));
343
- // 버전 캐시 갱신은 '표시 후'에만(이번 출력은 캐시값 기준, 갱신은 다음 세션용). SessionStart는
344
- // 게이트를 막지 않으므로 짧은 타임아웃 네트워크가 안전(advisor 승인). 실패는 조용히 무시.
345
- try {
346
- if (ctx?.cliPath && process.env.GBC_NO_UPDATE_NOTICE !== "1" && isCacheStale(readVersionCache())) {
347
- await refreshVersionCache();
348
- }
349
- }
350
- catch {
351
- /* 갱신 실패는 무시(fail-silent) */
352
- }
353
355
  process.exit(0);
354
356
  }
package/dist/install.js CHANGED
@@ -1,6 +1,22 @@
1
1
  // gbc init 설치 로직 (순수함수 — cli.ts main() 부작용 없이 단위테스트 가능).
2
2
  // 키 주입은 셸이 아니라 gbc 코드(judge.ts resolveApiKey)가 처리한다 → hook 명령은
3
3
  // 셸 무관 순수 형태라 native Windows(cmd.exe)/bash/zsh/Mac에서 동일하게 동작한다.
4
+ /**
5
+ * dev(도그푸딩) 설치용 hook 경로 placeholder. `gbc init --dev`가 절대경로(CLI_PATH) 대신 이걸
6
+ * 구워, geobuke-code 자기 repo처럼 dist 위치가 옮겨다니는 클론에서도 hook이 깨지지 않게 한다
7
+ * (CC 런타임이 ${CLAUDE_PROJECT_DIR}를 프로젝트 루트로 치환). npm 전역·외부 4곳 도그푸딩은 절대경로
8
+ * 유지(기본동작 불변) — 이 placeholder는 명시 opt-in일 때만 쓰인다.
9
+ */
10
+ export const DEV_PLACEHOLDER = "${CLAUDE_PROJECT_DIR}/dist/cli.js";
11
+ /**
12
+ * PreToolUse hook의 *정식* 명령 집합(절대경로 + dev placeholder). stale/normalize 판정의 공통 기준.
13
+ * read-time(hasStalePreToolUse)은 런타임 cliPath=절대경로뿐이라 이 repo가 dev인지 모른다 → 두 정식
14
+ * 형태 중 하나면 stale 아님으로 봐야 placeholder를 구식으로 오판하지 않는다. substring이 아니라
15
+ * 완전일치 집합이라, 서브명령명이 바뀌면 placeholder 형태도 함께 갱신돼 진짜 구식 감지는 유지된다.
16
+ */
17
+ function canonicalPreCommands(cliPath) {
18
+ return [buildPreCommand(cliPath), buildPreCommand(DEV_PLACEHOLDER)];
19
+ }
4
20
  /**
5
21
  * PreToolUse hook 명령 생성 — 셸 무관 순수 명령.
6
22
  * `node "<cliPath>" hook pre-tool-use` 형태만 생성한다. 키 주입(셸 prefix)·셸 확장 없음.
@@ -19,12 +35,14 @@ export function buildPreCommand(cliPath) {
19
35
  * settings를 제자리 수정하고 변경 건수를 반환한다(멱등: 이미 표준이면 0건).
20
36
  */
21
37
  export function normalizeHooks(settings, cliPath) {
22
- const target = buildPreCommand(cliPath);
38
+ const canon = canonicalPreCommands(cliPath);
23
39
  let changed = 0;
24
40
  for (const entry of settings.hooks?.PreToolUse ?? []) {
25
41
  for (const h of entry.hooks ?? []) {
26
- if (h.command.includes("hook pre-tool-use") && h.command !== target) {
27
- h.command = target;
42
+ // 이미 정식(절대 or placeholder)이면 건드리지 않는다 dev placeholder를 절대경로로 덮어
43
+ // 도그푸딩 설치를 깨뜨리지 않게. 진짜 구식(옛 bash 키주입 등)만 절대경로로 교체.
44
+ if (h.command.includes("hook pre-tool-use") && !canon.includes(h.command)) {
45
+ h.command = buildPreCommand(cliPath);
28
46
  changed++;
29
47
  }
30
48
  }
@@ -40,10 +58,12 @@ export function buildSessionStartCommand(cliPath) {
40
58
  * 감지부만 떼어낸 비파괴 술어 — ②init-staleness 안내가 settings를 수정하지 않고 판단하게 한다.
41
59
  */
42
60
  export function hasStalePreToolUse(settings, cliPath) {
43
- const target = buildPreCommand(cliPath);
61
+ const canon = canonicalPreCommands(cliPath);
44
62
  for (const entry of settings.hooks?.PreToolUse ?? []) {
45
63
  for (const h of entry.hooks ?? []) {
46
- if (h.command.includes("hook pre-tool-use") && h.command !== target)
64
+ // dev placeholder도 정식이므로 stale 아님 — 절대경로 런타임에서 placeholder를 구식으로 오판해
65
+ // 'gbc init' 재실행을 헛권하던 false-positive 차단(B-잔여 #3의 실제 증상).
66
+ if (h.command.includes("hook pre-tool-use") && !canon.includes(h.command))
47
67
  return true;
48
68
  }
49
69
  }
package/dist/version.js CHANGED
@@ -67,7 +67,7 @@ export function buildVersionNotice(current, cache) {
67
67
  if (compareVersions(current, cache.latest) >= 0)
68
68
  return "";
69
69
  return (`🐢 거북이코드 신버전 ${cache.latest} 사용 가능(현재 ${current}). ` +
70
- `갱신: npm i -g geobuke-code@latest → 각 프로젝트서 gbc init --yes`);
70
+ `갱신: 'gbc update'(전역 최신 + 현재 프로젝트 재init) 또는 수동 'npm i -g geobuke-code@latest → gbc init --yes'`);
71
71
  }
72
72
  /**
73
73
  * npm 레지스트리에서 최신 버전을 받아 캐시에 쓴다(짧은 타임아웃, 비차단·fail-silent).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "geobuke-code",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "거북이코드 — 구현 직전 강제 게이트. Claude Code PreToolUse hook으로 코드 변경 전 계획 케이스 누락·시나리오 미지정을 차단한다.",
5
5
  "type": "module",
6
6
  "bin": {