triflux 8.12.2 → 8.12.5

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/bin/triflux.mjs CHANGED
@@ -1030,6 +1030,20 @@ async function cmdDoctor(options = {}) {
1030
1030
  warn("Gemini 쿼터 캐시 재생성 실패");
1031
1031
  }
1032
1032
  }
1033
+ try {
1034
+ const { buildAll } = await import("../scripts/cache-warmup.mjs");
1035
+ const warmupSummary = buildAll({ cwd: process.cwd(), force: true });
1036
+ if (warmupSummary.ok) {
1037
+ report.actions.push({ type: "rebuild", name: "warmup-caches", status: "ok", built: warmupSummary.built });
1038
+ ok("Phase 1 웜업 캐시 재생성됨");
1039
+ } else {
1040
+ report.actions.push({ type: "rebuild", name: "warmup-caches", status: "failed" });
1041
+ warn("Phase 1 웜업 캐시 재생성 실패");
1042
+ }
1043
+ } catch {
1044
+ report.actions.push({ type: "rebuild", name: "warmup-caches", status: "failed" });
1045
+ warn("Phase 1 웜업 캐시 재생성 실패");
1046
+ }
1033
1047
  console.log(`\n ${LINE}`);
1034
1048
  console.log(` ${GREEN_BRIGHT}${BOLD}✓ 캐시 초기화 + 재생성 완료${RESET}\n`);
1035
1049
  report.status = report.actions.some((action) => action.status === "failed") ? "issues" : "ok";
@@ -1083,6 +1097,19 @@ async function cmdDoctor(options = {}) {
1083
1097
  } catch { try { unlinkSync(fp); cleaned++; ok(`손상된 캐시 정리: ${name}`); } catch {} }
1084
1098
  }
1085
1099
  if (cleaned === 0) info("에러 캐시 없음");
1100
+ try {
1101
+ const { fixCaches } = await import("../scripts/cache-doctor.mjs");
1102
+ const cacheRepair = await fixCaches({ cwd: process.cwd() });
1103
+ if (cacheRepair.fixed.length > 0 && cacheRepair.ok) {
1104
+ ok(`웜업 캐시 자동 복구: ${cacheRepair.fixed.join(", ")}`);
1105
+ } else if (cacheRepair.fixed.length > 0) {
1106
+ warn(`웜업 캐시 자동 복구 실패: ${cacheRepair.fixed.join(", ")}`);
1107
+ } else {
1108
+ info("웜업 캐시: 이미 정상 상태");
1109
+ }
1110
+ } catch {
1111
+ warn("웜업 캐시 자동 복구 실패");
1112
+ }
1086
1113
  console.log(`\n ${LINE}`);
1087
1114
  info("수정 완료 — 아래 진단 결과를 확인하세요");
1088
1115
  console.log("");
@@ -1257,6 +1284,43 @@ async function cmdDoctor(options = {}) {
1257
1284
  info(`수동: node ${join(PKG_ROOT, "scripts", "mcp-check.mjs")}`);
1258
1285
  }
1259
1286
 
1287
+ // 9.5. Phase 1 웜업 캐시
1288
+ section("Warmup Cache");
1289
+ try {
1290
+ const { verifyCaches } = await import("../scripts/cache-doctor.mjs");
1291
+ const cacheVerification = verifyCaches({ cwd: process.cwd() });
1292
+ const brokenCaches = cacheVerification.results.filter((result) => result.status !== "ok");
1293
+
1294
+ addDoctorCheck(report, {
1295
+ name: "warmup-cache",
1296
+ status: cacheVerification.ok ? "ok" : "issues",
1297
+ files: cacheVerification.results.map((result) => ({
1298
+ target: result.target,
1299
+ status: result.status,
1300
+ path: result.file,
1301
+ })),
1302
+ ...(cacheVerification.ok ? {} : { fix: "tfx doctor --fix" }),
1303
+ });
1304
+
1305
+ if (brokenCaches.length === 0) {
1306
+ ok("4개 웜업 캐시 정상");
1307
+ } else {
1308
+ warn(`${brokenCaches.length}개 웜업 캐시 이슈 발견`);
1309
+ for (const entry of brokenCaches) {
1310
+ info(`${entry.target}: ${entry.status}`);
1311
+ }
1312
+ if (!fix) issues += brokenCaches.length;
1313
+ }
1314
+ } catch (error) {
1315
+ addDoctorCheck(report, {
1316
+ name: "warmup-cache",
1317
+ status: "invalid",
1318
+ fix: "node scripts/cache-doctor.mjs --fix",
1319
+ });
1320
+ warn(`웜업 캐시 검사 실패: ${error.message}`);
1321
+ issues++;
1322
+ }
1323
+
1260
1324
  // 10. CLI 이슈 트래커
1261
1325
  section("CLI Issues");
1262
1326
  const issuesFile = join(CLAUDE_DIR, "cache", "cli-issues.jsonl");
@@ -19,9 +19,10 @@ export class CodexBackend {
19
19
  */
20
20
  buildArgs(prompt, resultFile, opts = {}) {
21
21
  const modelFlag = opts.model ? ` --model '${opts.model}'` : "";
22
+ const cwdFlag = opts.cwd ? ` --cwd '${opts.cwd}'` : "";
22
23
  // Codex 0.117.0+: config.toml에 sandbox 설정이 있으면 CLI 플래그 중복 불가
23
24
  // --dangerously-bypass-approvals-and-sandbox 대신 exec 서브커맨드만 사용 (config가 sandbox 관리)
24
- return `codex exec ${prompt} --output-last-message '${resultFile}' --color never${modelFlag}`;
25
+ return `codex exec ${prompt} --output-last-message '${resultFile}' --color never${modelFlag}${cwdFlag}`;
25
26
  }
26
27
 
27
28
  env() { return {}; }
@@ -40,7 +40,7 @@ function renderTmuxInstallHelp() {
40
40
  export { parseTeamArgs };
41
41
 
42
42
  export async function teamStart(args = []) {
43
- const { agents, lead, layout, teammateMode, task: rawTask, assigns, autoAttach, progressive, timeoutSec, verbose, dashboard, dashboardLayout, dashboardSize, dashboardAnchor, mcpProfile, model } = parseTeamArgs(args);
43
+ const { agents, lead, layout, teammateMode, task: rawTask, assigns, autoAttach, progressive, timeoutSec, verbose, dashboard, dashboardLayout, dashboardSize, dashboardAnchor, mcpProfile, model, cwd } = parseTeamArgs(args);
44
44
  // --assign 사용 시 task를 자동 생성
45
45
  const task = rawTask || (assigns.length > 0 ? assigns.map(a => a.prompt).join(" + ") : "");
46
46
  if (!task) return printStartUsage();
@@ -84,7 +84,7 @@ export async function teamStart(args = []) {
84
84
  const state = effectiveMode === "in-process"
85
85
  ? await startInProcessTeam({ sessionId, task, lead, agents, subtasks, hubUrl })
86
86
  : effectiveMode === "headless"
87
- ? await startHeadlessTeam({ sessionId, task, lead, agents, subtasks, layout, assigns, autoAttach, progressive, timeoutSec, verbose, dashboard, dashboardLayout, dashboardSize, dashboardAnchor, mcpProfile, model })
87
+ ? await startHeadlessTeam({ sessionId, task, lead, agents, subtasks, layout, assigns, autoAttach, progressive, timeoutSec, verbose, dashboard, dashboardLayout, dashboardSize, dashboardAnchor, mcpProfile, model, cwd })
88
88
  : effectiveMode === "wt"
89
89
  ? await startWtTeam({ sessionId, task, lead, agents, subtasks, layout, hubUrl })
90
90
  : await startMuxTeam({ sessionId, task, lead, agents, subtasks, layout, hubUrl, teammateMode: effectiveMode });
@@ -1,3 +1,4 @@
1
+ import { resolve } from "node:path";
1
2
  import { normalizeLayout, normalizeTeammateMode } from "../../services/runtime-mode.mjs";
2
3
  import { parseDashboardLayout } from "../../../dashboard-layout.mjs";
3
4
  import { parseDashboardAnchor } from "../../../dashboard-anchor.mjs";
@@ -54,6 +55,7 @@ export function parseTeamArgs(args = []) {
54
55
  let dashboardAnchor = "window";
55
56
  let mcpProfile = "";
56
57
  let model = "";
58
+ let cwd = "";
57
59
 
58
60
  for (let index = 0; index < args.length; index += 1) {
59
61
  const current = args[index];
@@ -92,6 +94,13 @@ export function parseTeamArgs(args = []) {
92
94
  mcpProfile = args[++index].trim();
93
95
  } else if ((current === "--model" || current === "-m") && args[index + 1]) {
94
96
  model = args[++index].trim();
97
+ } else if (current === "--cwd" && args[index + 1]) {
98
+ let p = args[++index].trim();
99
+ // MSYS/Git Bash 드라이브 문자 변환: /c/... → C:/...
100
+ if (process.platform === "win32" && /^\/[a-zA-Z]\//.test(p)) {
101
+ p = p[1].toUpperCase() + ":" + p.slice(2);
102
+ }
103
+ cwd = resolve(p);
95
104
  } else if (current.startsWith("-")) {
96
105
  console.warn(` ⚠ 미인식 플래그 무시: ${current}`);
97
106
  } else {
@@ -116,5 +125,6 @@ export function parseTeamArgs(args = []) {
116
125
  dashboardAnchor,
117
126
  mcpProfile,
118
127
  model,
128
+ cwd,
119
129
  };
120
130
  }
@@ -204,7 +204,7 @@ function loadAvailableServersFromSearchEngineCache(cwd = process.cwd()) {
204
204
 
205
205
  try {
206
206
  const parsed = JSON.parse(readFileSync(cacheFile, 'utf8'));
207
- if (!Array.isArray(parsed?.engines)) return [];
207
+ if (!Array.isArray(parsed?.engines)) return null;
208
208
  return parsed.engines
209
209
  .filter((engine) => engine?.status === 'available')
210
210
  .map((engine) => (typeof engine?.name === 'string' ? engine.name.trim() : ''))
@@ -647,16 +647,13 @@ export class DelegatorMcpWorker {
647
647
  }
648
648
 
649
649
  _getMcpPolicyOptions(args) {
650
- const cachedAvailableServers = loadAvailableServersFromSearchEngineCache(args.cwd || this.cwd);
651
650
  return {
652
651
  agentType: args.agentType || 'executor',
653
652
  requestedProfile: args.mcpProfile || 'auto',
654
653
  searchTool: args.searchTool,
655
654
  workerIndex: Number.isInteger(args.workerIndex) ? args.workerIndex : undefined,
656
655
  taskText: withContext(String(args.prompt ?? ''), args.contextFile),
657
- availableServers: Array.isArray(args.availableServers)
658
- ? args.availableServers
659
- : cachedAvailableServers ?? undefined,
656
+ availableServers: Array.isArray(args.availableServers) ? args.availableServers : undefined,
660
657
  };
661
658
  }
662
659
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "8.12.2",
3
+ "version": "8.12.5",
4
4
  "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,401 +1,30 @@
1
1
  #!/usr/bin/env node
2
- // scripts/cache-buildup.mjs — Phase 1 캐시 빌드업
3
- // setup.mjs에서 백그라운드 스폰. CWD의 .omc/cache/ 및 .omc/state/에 기록.
4
-
5
- import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
6
- import { execSync } from "node:child_process";
7
- import { join, basename } from "node:path";
8
- import { homedir } from "node:os";
9
-
10
- import { readPreflightCache } from "./preflight-cache.mjs";
11
- import { SEARCH_SERVER_ORDER, MCP_SERVER_DOMAIN_TAGS } from "./lib/mcp-server-catalog.mjs";
12
-
13
- const CWD = process.cwd();
14
- const CACHE_DIR = join(CWD, ".omc", "cache");
15
- const STATE_DIR = join(CWD, ".omc", "state");
16
- const HOME = homedir();
17
-
18
- function ensureDirs() {
19
- mkdirSync(CACHE_DIR, { recursive: true });
20
- mkdirSync(STATE_DIR, { recursive: true });
21
- }
22
-
23
- function writeJSON(path, data) {
24
- writeFileSync(path, JSON.stringify(data, null, 2) + "\n");
25
- }
26
-
27
- // ── 1. Codex 스킬 스캔 (Task D: 일반화된 발견) ──
28
-
29
- const ROLE_KEYWORDS = {
30
- plan: ["plan", "계획", "decompos", "strategy", "설계"],
31
- auto: ["autonomous", "자율", "auto-execute", "autopilot", "full auto"],
32
- persist: ["loop", "반복", "completion", "persist", "until", "끝까지"],
33
- investigate: ["investigate", "research", "조사", "분석", "analysis"],
34
- review: ["review", "리뷰", "inspect", "검수"],
2
+ // scripts/cache-buildup.mjs — legacy wrapper for cache-warmup
3
+
4
+ import { fileURLToPath } from "node:url";
5
+ import {
6
+ buildAll,
7
+ checkSearchEngines,
8
+ extractProjectMeta,
9
+ formatBuildSummary,
10
+ probeTierEnvironment,
11
+ scanCodexSkills,
12
+ } from "./cache-warmup.mjs";
13
+
14
+ export {
15
+ buildAll,
16
+ checkSearchEngines,
17
+ extractProjectMeta,
18
+ probeTierEnvironment,
19
+ scanCodexSkills,
35
20
  };
36
21
 
37
- function classifyRole(description) {
38
- const lower = (description || "").toLowerCase();
39
- for (const [role, keywords] of Object.entries(ROLE_KEYWORDS)) {
40
- if (keywords.some((kw) => lower.includes(kw))) return role;
41
- }
42
- return "general";
43
- }
44
-
45
- function parseSkillFrontmatter(content) {
46
- const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
47
- if (!match) return null;
48
-
49
- const fm = match[1];
50
- const name = fm.match(/^name:\s*(.+)$/m)?.[1]?.trim() || null;
51
- const desc = fm.match(/^description:\s*(.+)$/m)?.[1]?.trim() || null;
52
- return name ? { name, description: desc } : null;
53
- }
54
-
55
- export function scanCodexSkills() {
56
- const codexSkillsDir = join(HOME, ".codex", "skills");
57
- const skills = [];
58
-
59
- // 1) OMX/커스텀 스킬 (~/.codex/skills/*)
60
- if (existsSync(codexSkillsDir)) {
61
- for (const entry of readdirSync(codexSkillsDir, { withFileTypes: true })) {
62
- if (!entry.isDirectory()) continue;
63
- const skillMd = join(codexSkillsDir, entry.name, "SKILL.md");
64
- if (!existsSync(skillMd)) continue;
65
-
66
- try {
67
- const content = readFileSync(skillMd, "utf8");
68
- const fm = parseSkillFrontmatter(content);
69
- const name = fm?.name || entry.name;
70
- const description = fm?.description || "";
71
- skills.push({
72
- name,
73
- role: classifyRole(description),
74
- description: description.slice(0, 200),
75
- source: "custom",
76
- path: skillMd,
77
- });
78
- } catch {
79
- skills.push({ name: entry.name, role: "general", description: "", source: "custom", path: skillMd });
80
- }
81
- }
82
- }
83
-
84
- // 2) OpenAI 빌트인 스킬 (codex에 내장된 /명령들)
85
- const BUILTIN_SKILLS = [
86
- { name: "web-clone", role: "general", description: "Clone and analyze web pages" },
87
- { name: "help", role: "general", description: "Show available commands" },
88
- { name: "note", role: "general", description: "Save notes during session" },
89
- { name: "worker", role: "auto", description: "Spawn background worker for tasks" },
90
- ];
91
-
92
- for (const builtin of BUILTIN_SKILLS) {
93
- // 이미 커스텀으로 오버라이드된 경우 스킵
94
- if (skills.some((s) => s.name === builtin.name)) continue;
95
- // 실제 디렉토리 존재 확인
96
- if (existsSync(join(codexSkillsDir, builtin.name))) continue;
97
- skills.push({ ...builtin, source: "builtin" });
98
- }
99
-
100
- return {
101
- scanned_at: new Date().toISOString(),
102
- codex_skills_dir: codexSkillsDir,
103
- total: skills.length,
104
- skills,
105
- };
106
- }
107
-
108
- // ── 2. Tier 환경 프로브 ──
109
-
110
- function probeCliVersion(command, runExec = execSync) {
111
- try {
112
- runExec(command, { stdio: "ignore", timeout: 3000, windowsHide: true });
113
- return true;
114
- } catch {
115
- return false;
116
- }
117
- }
118
-
119
- function resolveCachedCliCheck(preflightCheck, command, runExec = execSync) {
120
- if (typeof preflightCheck?.ok === "boolean") {
121
- return preflightCheck.ok;
122
- }
123
- return probeCliVersion(command, runExec);
124
- }
125
-
126
- export function probeTierEnvironment({
127
- readPreflight = readPreflightCache,
128
- runExec = execSync,
129
- platform = process.platform,
130
- } = {}) {
131
- const preflight = readPreflight();
132
- const checks = {
133
- psmux: false,
134
- hub: false,
135
- codex: resolveCachedCliCheck(preflight?.codex, "codex --version", runExec),
136
- gemini: resolveCachedCliCheck(preflight?.gemini, "gemini --version", runExec),
137
- wt: false,
138
- };
139
-
140
- // psmux
141
- try {
142
- runExec("psmux --version", { stdio: "ignore", timeout: 3000, windowsHide: true });
143
- checks.psmux = true;
144
- } catch {}
145
-
146
- // hub (preflight에서 가져오거나 직접 확인)
147
- if (preflight?.hub?.ok) {
148
- checks.hub = true;
149
- } else {
150
- try {
151
- runExec("curl -sf http://127.0.0.1:27888/status", { stdio: "ignore", timeout: 2000, windowsHide: true });
152
- checks.hub = true;
153
- } catch {}
154
- }
155
-
156
- // Windows Terminal
157
- if (platform === "win32") {
158
- try {
159
- runExec("where wt.exe", { stdio: "ignore", timeout: 2000, windowsHide: true });
160
- checks.wt = true;
161
- } catch {}
162
- }
163
-
164
- // Tier 분류
165
- let tier = "minimal"; // claude-only
166
- if (checks.codex || checks.gemini) tier = "standard"; // multi-cli
167
- if (checks.psmux && checks.hub && (checks.codex || checks.gemini)) tier = "full"; // orchestration-ready
168
-
169
- const agents = [];
170
- agents.push("claude");
171
- if (checks.codex) agents.push("codex");
172
- if (checks.gemini) agents.push("gemini");
173
-
174
- return {
175
- probed_at: new Date().toISOString(),
176
- tier,
177
- checks,
178
- available_agents: agents,
179
- codex_plan: preflight?.codex_plan || { plan: "unknown" },
180
- };
181
- }
182
-
183
- // ── 3. 프로젝트 메타 추출 ──
184
-
185
- export function extractProjectMeta() {
186
- let name = basename(CWD);
187
- let description = "";
188
- let lang = "unknown";
189
- let testCmd = null;
190
- let isGit = false;
191
-
192
- // git
193
- try {
194
- const toplevel = execSync("git rev-parse --show-toplevel", {
195
- encoding: "utf8", timeout: 3000, windowsHide: true, stdio: ["pipe", "pipe", "ignore"],
196
- }).trim();
197
- name = basename(toplevel);
198
- isGit = true;
199
- } catch {}
200
-
201
- // package.json
202
- const pkgPath = join(CWD, "package.json");
203
- if (existsSync(pkgPath)) {
204
- try {
205
- const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
206
- description = pkg.description || "";
207
- testCmd = pkg.scripts?.test || null;
208
- lang = "JavaScript/ESM (Node.js)";
209
- } catch {}
210
- } else if (existsSync(join(CWD, "pyproject.toml")) || existsSync(join(CWD, "setup.py"))) {
211
- lang = "Python";
212
- } else if (existsSync(join(CWD, "Cargo.toml"))) {
213
- lang = "Rust";
214
- } else if (existsSync(join(CWD, "go.mod"))) {
215
- lang = "Go";
216
- }
217
-
218
- return {
219
- extracted_at: new Date().toISOString(),
220
- name,
221
- description: description.slice(0, 300),
222
- lang,
223
- test_cmd: testCmd,
224
- is_git: isGit,
225
- };
226
- }
227
-
228
- // ── 4. 검색 엔진 확인 ──
229
-
230
- function loadMcpInventory() {
231
- const invPath = join(HOME, ".claude", "cache", "mcp-inventory.json");
232
- try {
233
- return JSON.parse(readFileSync(invPath, "utf8"));
234
- } catch {
235
- return null;
236
- }
237
- }
238
-
239
- function loadMcpConfigs() {
240
- const servers = {};
241
- // Claude 설정 파일들에서 MCP 서버 읽기
242
- const configPaths = [
243
- join(HOME, ".claude", "settings.json"),
244
- join(HOME, ".claude", "settings.local.json"),
245
- join(CWD, ".mcp.json"),
246
- join(CWD, ".claude", ".mcp.json"),
247
- ];
248
-
249
- for (const cfgPath of configPaths) {
250
- try {
251
- const data = JSON.parse(readFileSync(cfgPath, "utf8"));
252
- const mcpServers = data.mcpServers || {};
253
- for (const [name, config] of Object.entries(mcpServers)) {
254
- servers[name] = { configured: true, source: basename(cfgPath), ...config };
255
- }
256
- } catch {}
257
- }
258
- return servers;
259
- }
260
-
261
- export function checkSearchEngines() {
262
- const inventory = loadMcpInventory();
263
- const configuredServers = loadMcpConfigs();
264
- const engines = [];
265
-
266
- // 알려진 검색 서버 (우선순위순)
267
- const KNOWN_SEARCH_SERVERS = [...SEARCH_SERVER_ORDER, "context7"];
268
-
269
- // MCP 인벤토리에서 검색 가능 서버 탐지
270
- const allServerNames = new Set(KNOWN_SEARCH_SERVERS);
271
-
272
- // 인벤토리의 Claude MCP 서버들도 확인
273
- if (inventory) {
274
- for (const scope of ["codex", "gemini", "claude"]) {
275
- const scopeData = inventory[scope];
276
- if (!scopeData?.servers) continue;
277
- for (const srv of scopeData.servers) {
278
- const tags = srv.domain_tags || MCP_SERVER_DOMAIN_TAGS[srv.name] || [];
279
- if (tags.includes("search") || tags.includes("web") || tags.includes("research")) {
280
- allServerNames.add(srv.name);
281
- }
282
- }
283
- }
284
- }
285
-
286
- // 설정된 서버도 추가
287
- for (const name of Object.keys(configuredServers)) {
288
- const tags = MCP_SERVER_DOMAIN_TAGS[name] || [];
289
- if (tags.includes("search") || tags.includes("web") || KNOWN_SEARCH_SERVERS.includes(name)) {
290
- allServerNames.add(name);
291
- }
292
- }
293
-
294
- // 각 검색 서버의 상태 판정
295
- for (const name of allServerNames) {
296
- const configured = !!configuredServers[name];
297
- const tags = MCP_SERVER_DOMAIN_TAGS[name] || [];
298
-
299
- // 인벤토리에서 실제 활성 상태 확인
300
- let inventoryStatus = null;
301
- if (inventory) {
302
- for (const scope of ["codex", "gemini", "claude"]) {
303
- const srv = inventory[scope]?.servers?.find((s) => s.name === name);
304
- if (srv) {
305
- inventoryStatus = { scope, status: srv.status, tool_count: srv.tool_count };
306
- break;
307
- }
308
- }
309
- }
310
-
311
- let status = "unavailable";
312
- if (inventoryStatus?.status === "enabled" || inventoryStatus?.status === "configured" || inventoryStatus?.status === "available") {
313
- status = "available";
314
- } else if (configured) {
315
- status = "configured"; // 설정은 있지만 인벤토리 미확인
316
- }
317
-
318
- engines.push({
319
- name,
320
- status,
321
- domain_tags: tags,
322
- configured,
323
- inventory: inventoryStatus,
324
- source: configuredServers[name]?.source || null,
325
- });
326
- }
327
-
328
- // 우선순위순 정렬: SEARCH_SERVER_ORDER 먼저, 나머지 알파벳순
329
- engines.sort((a, b) => {
330
- const ai = SEARCH_SERVER_ORDER.indexOf(a.name);
331
- const bi = SEARCH_SERVER_ORDER.indexOf(b.name);
332
- if (ai !== -1 && bi !== -1) return ai - bi;
333
- if (ai !== -1) return -1;
334
- if (bi !== -1) return 1;
335
- return a.name.localeCompare(b.name);
336
- });
337
-
338
- const available = engines.filter((e) => e.status === "available");
339
- const primary = available[0]?.name || null;
340
-
341
- return {
342
- checked_at: new Date().toISOString(),
343
- primary_engine: primary,
344
- available_count: available.length,
345
- total_count: engines.length,
346
- engines,
347
- };
348
- }
349
-
350
- // ── 메인 실행 ──
351
-
352
- function main() {
353
- ensureDirs();
354
-
355
- const results = {};
356
-
357
- try {
358
- const skills = scanCodexSkills();
359
- writeJSON(join(CACHE_DIR, "codex-skills.json"), skills);
360
- results.codex_skills = { ok: true, count: skills.total };
361
- } catch (e) {
362
- results.codex_skills = { ok: false, error: e.message };
363
- }
364
-
365
- try {
366
- const tier = probeTierEnvironment();
367
- writeJSON(join(STATE_DIR, "tier-environment.json"), tier);
368
- results.tier = { ok: true, tier: tier.tier };
369
- } catch (e) {
370
- results.tier = { ok: false, error: e.message };
371
- }
372
-
373
- try {
374
- const meta = extractProjectMeta();
375
- writeJSON(join(CACHE_DIR, "project-meta.json"), meta);
376
- results.project_meta = { ok: true, name: meta.name };
377
- } catch (e) {
378
- results.project_meta = { ok: false, error: e.message };
379
- }
380
-
381
- try {
382
- const search = checkSearchEngines();
383
- writeJSON(join(STATE_DIR, "search-engines.json"), search);
384
- results.search_engines = { ok: true, primary: search.primary_engine, available: search.available_count };
385
- } catch (e) {
386
- results.search_engines = { ok: false, error: e.message };
387
- }
388
-
389
- // 간결 stdout (hook/로그용)
390
- const ok = Object.values(results).every((r) => r.ok);
391
- const summary = ok ? "cache-buildup: ok" : "cache-buildup: partial";
392
- const details = [];
393
- if (results.codex_skills?.ok) details.push(`skills:${results.codex_skills.count}`);
394
- if (results.tier?.ok) details.push(`tier:${results.tier.tier}`);
395
- if (results.search_engines?.ok) details.push(`search:${results.search_engines.primary || "none"}(${results.search_engines.available})`);
396
- console.log(details.length ? `${summary} (${details.join(", ")})` : summary);
22
+ async function main() {
23
+ const summary = buildAll({ force: process.argv.includes("--force") });
24
+ console.log(formatBuildSummary(summary, { label: "cache-buildup" }));
25
+ if (!summary.ok) process.exitCode = 1;
397
26
  }
398
27
 
399
- if (process.argv[1]?.endsWith("cache-buildup.mjs")) {
400
- main();
28
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
29
+ await main();
401
30
  }