@team-semicolon/semo-cli 4.1.4 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
  /**
3
- * semo context — DB ↔ .claude/memory/ 동기화
3
+ * semo context — 스킬/캐시/크론잡 동기화
4
4
  *
5
- * sync: Core DB → .claude/memory/*.md (KB domains, bot_status, ontology, projects)
6
- * push: .claude/memory/decisions.md → DB (semo.knowledge_base WHERE domain='decision')
5
+ * sync: DB → 글로벌 캐시 (skills/commands/agents) + 스킬 DB 동기화 + 크론잡
6
+ * push: .claude/memory/<domain>.md → DB (deprecated kb_upsert MCP 도구로 대체)
7
+ *
8
+ * [v4.2.0] KB→md 파일 생성 제거 — semo-kb MCP 서버로 통일
7
9
  */
8
10
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
11
  if (k2 === undefined) k2 = k;
@@ -42,24 +44,29 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
42
44
  return (mod && mod.__esModule) ? mod : { "default": mod };
43
45
  };
44
46
  Object.defineProperty(exports, "__esModule", { value: true });
47
+ exports.syncCronJobs = syncCronJobs;
45
48
  exports.registerContextCommands = registerContextCommands;
46
49
  const chalk_1 = __importDefault(require("chalk"));
47
50
  const ora_1 = __importDefault(require("ora"));
48
51
  const fs = __importStar(require("fs"));
49
52
  const path = __importStar(require("path"));
53
+ const os = __importStar(require("os"));
50
54
  const database_1 = require("../database");
51
55
  const kb_1 = require("../kb");
56
+ const skill_sync_1 = require("./skill-sync");
57
+ const global_cache_1 = require("../global-cache");
52
58
  // ============================================================
53
59
  // Memory file mapping
54
60
  // ============================================================
55
61
  const MEMORY_DIR = ".claude/memory";
56
62
  // --out-dir 로 override 가능 (OpenClaw 봇 workspace 경로 지원)
63
+ // 기본값: ~/.claude/memory/ (글로벌 — 모든 프로젝트에서 공유)
57
64
  function resolveMemoryDir(outDir) {
58
65
  if (outDir) {
59
66
  // 절대경로 또는 ~ 경로 처리
60
67
  return outDir.replace(/^~/, require("os").homedir());
61
68
  }
62
- return path.join(process.cwd(), MEMORY_DIR);
69
+ return path.join(require("os").homedir(), MEMORY_DIR);
63
70
  }
64
71
  const KB_DOMAIN_MAP = {
65
72
  team: "team.md",
@@ -75,78 +82,82 @@ function ensureMemoryDir(resolvedDir) {
75
82
  fs.mkdirSync(resolvedDir, { recursive: true });
76
83
  return resolvedDir;
77
84
  }
78
- function kbEntriesToMarkdown(domain, entries) {
79
- if (entries.length === 0) {
80
- return `# ${domain}\n\n_No entries._\n`;
81
- }
82
- const lines = [`# ${domain}\n`, `> 자동 생성: semo context sync (${new Date().toISOString()})\n`];
83
- for (const entry of entries) {
84
- lines.push(`\n## ${entry.key}\n`);
85
- lines.push(entry.content);
86
- if (entry.metadata && Object.keys(entry.metadata).length > 0) {
87
- lines.push(`\n_metadata: ${JSON.stringify(entry.metadata)}_`);
88
- }
89
- lines.push("");
90
- }
91
- return lines.join("\n");
92
- }
93
- function botStatusToMarkdown(rows) {
94
- if (rows.length === 0) {
95
- return "# Bots\n\n_No bot status data._\n";
85
+ function parseCronJobsFile(filePath) {
86
+ try {
87
+ const content = fs.readFileSync(filePath, "utf-8");
88
+ const data = JSON.parse(content);
89
+ const jobs = data.jobs || [];
90
+ return jobs.map((job) => ({
91
+ jobId: job.jobId || job.id,
92
+ name: job.name || "",
93
+ schedule: job.schedule || {},
94
+ enabled: job.enabled !== false,
95
+ lastRun: job.lastRun || null,
96
+ nextRun: job.nextRun || null,
97
+ sessionTarget: job.sessionTarget || "main",
98
+ payload: job.payload || null,
99
+ }));
96
100
  }
97
- const lines = [
98
- "# Bots\n",
99
- `> 자동 생성: semo context sync (${new Date().toISOString()})\n`,
100
- "| Bot | 이름 | 역할 | Status | Last Active | Sessions |",
101
- "|-----|------|------|--------|-------------|----------|",
102
- ];
103
- for (const bot of rows) {
104
- const status = bot.status === "online" ? "🟢 online" : "🔴 offline";
105
- const lastActive = bot.last_active ? new Date(bot.last_active).toLocaleString("ko-KR") : "-";
106
- const displayName = [bot.emoji, bot.name].filter(Boolean).join(" ") || bot.bot_id;
107
- lines.push(`| ${bot.bot_id} | ${displayName} | ${bot.role || "-"} | ${status} | ${lastActive} | ${bot.session_count} |`);
101
+ catch {
102
+ return [];
108
103
  }
109
- return lines.join("\n") + "\n";
110
104
  }
111
- function ontologyToMarkdown(domains) {
112
- if (domains.length === 0) {
113
- return "# Ontology\n\n_No ontology domains._\n";
105
+ function getOpenClawBotIds() {
106
+ const homeDir = os.homedir();
107
+ try {
108
+ return fs.readdirSync(homeDir)
109
+ .filter(f => f.startsWith(".openclaw-"))
110
+ .map(f => f.replace(".openclaw-", ""));
114
111
  }
115
- const lines = [
116
- "# Ontology\n",
117
- `> 자동 생성: semo context sync (${new Date().toISOString()})\n`,
118
- ];
119
- for (const d of domains) {
120
- lines.push(`\n## ${d.domain} (v${d.version})\n`);
121
- if (d.description)
122
- lines.push(`${d.description}\n`);
123
- lines.push("```json");
124
- lines.push(JSON.stringify(d.schema, null, 2));
125
- lines.push("```\n");
112
+ catch {
113
+ return [];
126
114
  }
127
- return lines.join("\n");
128
115
  }
129
- async function fetchBotStatus(pool) {
116
+ /**
117
+ * Collect cron jobs from all ~/.openclaw-* directories and upsert to semo.bot_cron_jobs.
118
+ * Uses DELETE + INSERT per bot (same pattern as sync-agent).
119
+ */
120
+ async function syncCronJobs(pool) {
121
+ const homeDir = os.homedir();
122
+ const botIds = getOpenClawBotIds();
123
+ let totalJobs = 0;
124
+ let syncedBots = 0;
130
125
  const client = await pool.connect();
131
126
  try {
132
- const result = await client.query(`
133
- SELECT bot_id, name, emoji, role, status, last_active::text, session_count
134
- FROM semo.bot_status
135
- ORDER BY bot_id
136
- `);
137
- return result.rows;
138
- }
139
- catch {
140
- return [];
127
+ for (const botId of botIds) {
128
+ const cronPath = path.join(homeDir, `.openclaw-${botId}`, "cron", "jobs.json");
129
+ const jobs = parseCronJobsFile(cronPath);
130
+ // Always delete old entries (handles removed jobs)
131
+ await client.query("DELETE FROM semo.bot_cron_jobs WHERE bot_id = $1", [botId]);
132
+ for (const job of jobs) {
133
+ await client.query(`INSERT INTO semo.bot_cron_jobs
134
+ (bot_id, job_id, name, schedule, enabled, last_run, next_run, session_target, payload, synced_at)
135
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW())`, [
136
+ botId,
137
+ job.jobId,
138
+ job.name,
139
+ JSON.stringify(job.schedule),
140
+ job.enabled,
141
+ job.lastRun,
142
+ job.nextRun,
143
+ job.sessionTarget,
144
+ job.payload ? JSON.stringify(job.payload) : null,
145
+ ]);
146
+ }
147
+ totalJobs += jobs.length;
148
+ if (jobs.length > 0)
149
+ syncedBots++;
150
+ }
141
151
  }
142
152
  finally {
143
153
  client.release();
144
154
  }
155
+ return { bots: syncedBots, jobs: totalJobs };
145
156
  }
146
157
  // ============================================================
147
158
  // Markdown → KBEntry parser (for push)
148
159
  // ============================================================
149
- function parseDecisionsMarkdown(content) {
160
+ function parseMarkdownSections(content, domain) {
150
161
  const entries = [];
151
162
  // Split by h2 sections
152
163
  const sections = content.split(/\n##\s+/);
@@ -159,7 +170,7 @@ function parseDecisionsMarkdown(content) {
159
170
  const body = section.substring(firstNewline + 1).trim();
160
171
  if (key && body) {
161
172
  entries.push({
162
- domain: "decision",
173
+ domain,
163
174
  key,
164
175
  content: body,
165
176
  created_by: "claude-context-push",
@@ -168,22 +179,21 @@ function parseDecisionsMarkdown(content) {
168
179
  }
169
180
  return entries;
170
181
  }
182
+ // [v4.2.0] digestToMarkdown 제거 — MCP kb_digest로 대체
171
183
  // ============================================================
172
184
  // Commands
173
185
  // ============================================================
174
186
  function registerContextCommands(program) {
175
187
  const ctxCmd = program
176
188
  .command("context")
177
- .description("Core DB .claude/memory/ 컨텍스트 동기화");
189
+ .description("스킬/캐시/크론잡 동기화 (KB는 semo-kb MCP 서버)");
178
190
  // ── semo context sync ──────────────────────────────────────
179
191
  ctxCmd
180
192
  .command("sync")
181
- .description("Core DB .claude/memory/ 파일 생성")
182
- .option("--bot <name>", " ID (bot_status 필터)")
183
- .option("--domain <name>", "특정 KB 도메인만")
184
- .option("--no-bots", "bot_status 동기화 건너뜀")
185
- .option("--no-ontology", "ontology 동기화 건너뜀")
186
- .option("--out-dir <path>", "메모리 파일 출력 경로 (기본: .claude/memory/). OpenClaw 봇 workspace 지원용")
193
+ .description("스킬/에이전트/캐시 동기화 + 크론잡 (KB는 semo-kb MCP 서버 사용)")
194
+ .option("--no-skills", "스킬 파일 DB 동기화 건너뜀")
195
+ .option("--out-dir <path>", "캐시 파일 출력 경로 (기본: .claude/memory/)")
196
+ .option("--no-global-cache", "글로벌 캐시(skills/commands/agents) 동기화 건너뜀")
187
197
  .action(async (options) => {
188
198
  const spinner = (0, ora_1.default)("context sync 시작...").start();
189
199
  const connected = await (0, database_1.isDbConnected)();
@@ -194,40 +204,75 @@ function registerContextCommands(program) {
194
204
  }
195
205
  const pool = (0, database_1.getPool)();
196
206
  const memDir = ensureMemoryDir(resolveMemoryDir(options.outDir));
197
- let written = 0;
198
207
  try {
199
- // 1. KB domains memory/*.md
200
- const domains = options.domain ? [options.domain] : Object.keys(KB_DOMAIN_MAP);
201
- for (const domain of domains) {
202
- spinner.text = `KB 동기화: ${domain}...`;
208
+ // [v4.2.0] KB→md 파일 생성 제거 — MCP kb_search/kb_list/kb_bot_status/kb_ontology로 대체
209
+ // 기존 memory/*.md (team, projects, decisions, infra, process, bots, ontology) 파일은
210
+ // semo-kb MCP 서버가 실시간 DB 조회로 대체합니다.
211
+ // 1. 스킬 파일 skill_definitions DB 동기화
212
+ if (options.skills !== false) {
213
+ const semoSystemDir = path.join(process.cwd(), "semo-system");
214
+ if (fs.existsSync(semoSystemDir)) {
215
+ spinner.text = "skills 동기화...";
216
+ const client = await pool.connect();
217
+ try {
218
+ await client.query("BEGIN");
219
+ await (0, skill_sync_1.syncSkillsToDB)(client, semoSystemDir);
220
+ await client.query("COMMIT");
221
+ }
222
+ catch {
223
+ await client.query("ROLLBACK").catch(() => { });
224
+ // 스킬 동기화 실패는 무시 — context sync의 핵심은 memory/ 파일
225
+ }
226
+ finally {
227
+ client.release();
228
+ }
229
+ }
230
+ }
231
+ // 2. DB → 글로벌 캐시 (skills/commands/agents → ~/.claude/)
232
+ if (options.globalCache !== false) {
233
+ spinner.text = "글로벌 캐시 동기화 (skills/commands/agents)...";
203
234
  try {
204
- const { shared } = await (0, kb_1.kbList)(pool, { domain, limit: 1000 });
205
- const filename = KB_DOMAIN_MAP[domain] || `${domain}.md`;
206
- const content = kbEntriesToMarkdown(domain, shared);
207
- fs.writeFileSync(path.join(memDir, filename), content);
208
- written++;
235
+ const cacheResult = await (0, global_cache_1.syncGlobalCache)();
236
+ console.log(chalk_1.default.green(` ✓ 글로벌 캐시: skills(${cacheResult.skills}) commands(${cacheResult.commands}) agents(${cacheResult.agents})`));
209
237
  }
210
- catch {
211
- // domain may not exist skip silently
238
+ catch (cacheErr) {
239
+ // DB 실패 기존 파일 유지 (비치명적)
240
+ console.log(chalk_1.default.yellow(` ⚠ 글로벌 캐시 동기화 실패 (기존 파일 유지): ${cacheErr}`));
212
241
  }
213
242
  }
214
- // 2. bot_statusmemory/bots.md
215
- if (options.bots !== false) {
216
- spinner.text = "bot_status 동기화...";
217
- const botRows = await fetchBotStatus(pool);
218
- const botsContent = botStatusToMarkdown(botRows);
219
- fs.writeFileSync(path.join(memDir, "bots.md"), botsContent);
220
- written++;
243
+ // 3. 크론잡 동기화 (local DB)
244
+ try {
245
+ spinner.text = "크론잡 동기화...";
246
+ const cronResult = await syncCronJobs(pool);
247
+ if (cronResult.jobs > 0) {
248
+ console.log(chalk_1.default.green(` ✓ 크론잡: ${cronResult.bots}개 봇, ${cronResult.jobs}개 잡 동기화`));
249
+ }
250
+ }
251
+ catch {
252
+ // 크론잡 동기화 실패는 비치명적
253
+ }
254
+ // [v4.2.0] KB Digest 제거 — MCP kb_digest로 대체
255
+ // 4. MCP 서버 자동 등록 (.claude/settings.json)
256
+ try {
257
+ const semoRoot = process.cwd();
258
+ const settingsPath = path.join(memDir, "..", "settings.json");
259
+ if (fs.existsSync(settingsPath)) {
260
+ const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
261
+ if (!settings.mcpServers?.["semo-kb"]) {
262
+ settings.mcpServers = settings.mcpServers || {};
263
+ settings.mcpServers["semo-kb"] = {
264
+ command: "node",
265
+ args: [path.join(semoRoot, "packages/mcp-kb/dist/index.js")],
266
+ };
267
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
268
+ console.log(chalk_1.default.green(" ✓ semo-kb MCP 서버 자동 등록"));
269
+ }
270
+ }
221
271
  }
222
- // 3. ontology → memory/ontology.md
223
- if (options.ontology !== false) {
224
- spinner.text = "ontology 동기화...";
225
- const domains2 = await (0, kb_1.ontoList)(pool);
226
- const ontoContent = ontologyToMarkdown(domains2);
227
- fs.writeFileSync(path.join(memDir, "ontology.md"), ontoContent);
228
- written++;
272
+ catch {
273
+ // MCP 자동 등록 실패는 비치명적
229
274
  }
230
- spinner.succeed(`context sync 완료 — ${written}개 파일 업데이트`);
275
+ spinner.succeed("context sync 완료 — 스킬/캐시/크론잡 동기화");
231
276
  console.log(chalk_1.default.gray(` 저장 위치: ${memDir}`));
232
277
  }
233
278
  catch (err) {
@@ -240,28 +285,35 @@ function registerContextCommands(program) {
240
285
  // ── semo context push ──────────────────────────────────────
241
286
  ctxCmd
242
287
  .command("push")
243
- .description(".claude/memory/decisions.md → Core DB (semo.knowledge_base)")
244
- .option("--domain <name>", "push할 도메인 (기본: decision)", "decision")
288
+ .description(".claude/memory/<domain>.md → Core DB (semo.knowledge_base)")
289
+ .option("--domain <name>", "push할 도메인 (쉼표 구분 가능, 기본: decision)", "decision")
245
290
  .option("--dry-run", "실제 push 없이 변경사항만 미리보기")
246
291
  .option("--out-dir <path>", "메모리 파일 경로 (기본: .claude/memory/). OpenClaw 봇 workspace 지원용")
247
292
  .action(async (options) => {
293
+ console.log(chalk_1.default.yellow("⚠️ [deprecated] context push는 kb_upsert MCP 도구로 대체 예정입니다."));
294
+ console.log(chalk_1.default.yellow(" 봇/세션에서는 semo-kb MCP 서버의 kb_upsert 도구를 직접 사용하세요.\n"));
295
+ const domains = options.domain.split(",").map((d) => d.trim()).filter(Boolean);
248
296
  const memDir = resolveMemoryDir(options.outDir);
249
- const filename = KB_DOMAIN_MAP[options.domain] || `${options.domain}.md`;
250
- const filePath = path.join(memDir, filename);
251
- if (!fs.existsSync(filePath)) {
252
- console.log(chalk_1.default.red(`\n❌ 파일 없음: ${MEMORY_DIR}/${filename}`));
253
- console.log(chalk_1.default.gray(" semo context sync 먼저 실행하세요."));
254
- process.exit(1);
297
+ // 도메인별 엔트리 수집
298
+ const allEntries = [];
299
+ for (const domain of domains) {
300
+ const filename = KB_DOMAIN_MAP[domain] || `${domain}.md`;
301
+ const filePath = path.join(memDir, filename);
302
+ if (!fs.existsSync(filePath)) {
303
+ console.log(chalk_1.default.yellow(`⚠️ 파일 없음 (건너뜀): ${MEMORY_DIR}/${filename}`));
304
+ continue;
305
+ }
306
+ const content = fs.readFileSync(filePath, "utf-8");
307
+ const entries = parseMarkdownSections(content, domain);
308
+ allEntries.push(...entries);
255
309
  }
256
- const content = fs.readFileSync(filePath, "utf-8");
257
- const entries = parseDecisionsMarkdown(content);
258
- if (entries.length === 0) {
310
+ if (allEntries.length === 0) {
259
311
  console.log(chalk_1.default.yellow("⚠️ push할 항목이 없습니다."));
260
312
  return;
261
313
  }
262
- console.log(chalk_1.default.cyan(`\n📤 context push: ${options.domain} (${entries.length}건)\n`));
314
+ console.log(chalk_1.default.cyan(`\n📤 context push: ${domains.join(", ")} (${allEntries.length}건)\n`));
263
315
  if (options.dryRun) {
264
- for (const e of entries) {
316
+ for (const e of allEntries) {
265
317
  console.log(chalk_1.default.gray(` [dry-run] ${e.domain}/${e.key}`));
266
318
  }
267
319
  return;
@@ -277,14 +329,41 @@ function registerContextCommands(program) {
277
329
  let upserted = 0;
278
330
  const errors = [];
279
331
  try {
332
+ // Domain validation: check all domains against ontology
333
+ const ontologyResult = await client.query("SELECT domain FROM semo.ontology");
334
+ const knownDomains = new Set(ontologyResult.rows.map((r) => r.domain));
335
+ const validEntries = [];
336
+ for (const entry of allEntries) {
337
+ if (knownDomains.has(entry.domain)) {
338
+ validEntries.push(entry);
339
+ }
340
+ else {
341
+ errors.push(`${entry.domain}/${entry.key}: 미등록 도메인 '${entry.domain}' (등록된 도메인: ${Array.from(knownDomains).join(', ')})`);
342
+ }
343
+ }
344
+ if (validEntries.length === 0 && errors.length > 0) {
345
+ spinner.fail("모든 엔트리가 도메인 검증에 실패했습니다.");
346
+ errors.forEach(e => console.log(chalk_1.default.red(` ❌ ${e}`)));
347
+ client.release();
348
+ await (0, database_1.closeConnection)();
349
+ return;
350
+ }
351
+ // Generate embeddings for all valid entries
352
+ spinner.text = "임베딩 생성 중...";
353
+ const texts = validEntries.map(e => `${e.key}: ${e.content}`);
354
+ const embeddings = await (0, kb_1.generateEmbeddings)(texts);
280
355
  await client.query("BEGIN");
281
- for (const entry of entries) {
356
+ for (let i = 0; i < validEntries.length; i++) {
357
+ const entry = validEntries[i];
282
358
  try {
283
- await client.query(`INSERT INTO semo.knowledge_base (domain, key, content, metadata, created_by)
284
- VALUES ($1, $2, $3, $4, $5)
359
+ const embedding = embeddings[i];
360
+ const embeddingStr = embedding ? `[${embedding.join(",")}]` : null;
361
+ await client.query(`INSERT INTO semo.knowledge_base (domain, key, content, metadata, created_by, embedding)
362
+ VALUES ($1, $2, $3, $4, $5, $6::vector)
285
363
  ON CONFLICT (domain, key) DO UPDATE SET
286
364
  content = EXCLUDED.content,
287
- metadata = EXCLUDED.metadata`, [entry.domain, entry.key, entry.content, JSON.stringify(entry.metadata || {}), entry.created_by]);
365
+ metadata = EXCLUDED.metadata,
366
+ embedding = COALESCE(EXCLUDED.embedding, semo.knowledge_base.embedding)`, [entry.domain, entry.key, entry.content, JSON.stringify(entry.metadata || {}), entry.created_by, embeddingStr]);
288
367
  upserted++;
289
368
  }
290
369
  catch (err) {
@@ -292,7 +371,7 @@ function registerContextCommands(program) {
292
371
  }
293
372
  }
294
373
  await client.query("COMMIT");
295
- spinner.succeed(`push 완료: ${upserted}건 업서트`);
374
+ spinner.succeed(`push 완료: ${upserted}건 업서트 (임베딩 ${process.env.OPENAI_API_KEY ? '생성됨' : '건너뜀'})`);
296
375
  if (errors.length > 0) {
297
376
  errors.forEach(e => console.log(chalk_1.default.red(` ❌ ${e}`)));
298
377
  }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * semo db — 데이터베이스 관리
3
+ *
4
+ * semo db migrate — 마이그레이션 실행
5
+ * semo db migrate --status — 적용된 마이그레이션 목록
6
+ * semo db migrate --dry-run — 미적용 마이그레이션 미리보기
7
+ */
8
+ import { Command } from "commander";
9
+ export declare function registerDbCommands(program: Command): void;
@@ -0,0 +1,189 @@
1
+ "use strict";
2
+ /**
3
+ * semo db — 데이터베이스 관리
4
+ *
5
+ * semo db migrate — 마이그레이션 실행
6
+ * semo db migrate --status — 적용된 마이그레이션 목록
7
+ * semo db migrate --dry-run — 미적용 마이그레이션 미리보기
8
+ */
9
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ var desc = Object.getOwnPropertyDescriptor(m, k);
12
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
+ }
15
+ Object.defineProperty(o, k2, desc);
16
+ }) : (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ o[k2] = m[k];
19
+ }));
20
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
22
+ }) : function(o, v) {
23
+ o["default"] = v;
24
+ });
25
+ var __importStar = (this && this.__importStar) || (function () {
26
+ var ownKeys = function(o) {
27
+ ownKeys = Object.getOwnPropertyNames || function (o) {
28
+ var ar = [];
29
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
30
+ return ar;
31
+ };
32
+ return ownKeys(o);
33
+ };
34
+ return function (mod) {
35
+ if (mod && mod.__esModule) return mod;
36
+ var result = {};
37
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
38
+ __setModuleDefault(result, mod);
39
+ return result;
40
+ };
41
+ })();
42
+ var __importDefault = (this && this.__importDefault) || function (mod) {
43
+ return (mod && mod.__esModule) ? mod : { "default": mod };
44
+ };
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.registerDbCommands = registerDbCommands;
47
+ const chalk_1 = __importDefault(require("chalk"));
48
+ const ora_1 = __importDefault(require("ora"));
49
+ const fs = __importStar(require("fs"));
50
+ const path = __importStar(require("path"));
51
+ const database_1 = require("../database");
52
+ // ============================================================
53
+ // Migration runner
54
+ // ============================================================
55
+ const MIGRATIONS_DIR = path.resolve(__dirname, "..", "..", "migrations");
56
+ /**
57
+ * schema_migrations 테이블이 없으면 생성
58
+ */
59
+ async function ensureMigrationsTable() {
60
+ const pool = (0, database_1.getPool)();
61
+ await pool.query(`
62
+ CREATE TABLE IF NOT EXISTS semo.schema_migrations (
63
+ version TEXT PRIMARY KEY,
64
+ applied_at TIMESTAMPTZ DEFAULT NOW()
65
+ );
66
+ `);
67
+ }
68
+ /**
69
+ * 이미 적용된 마이그레이션 버전 목록
70
+ */
71
+ async function getAppliedMigrations() {
72
+ const pool = (0, database_1.getPool)();
73
+ const { rows } = await pool.query(`SELECT version, applied_at::text FROM semo.schema_migrations ORDER BY version`);
74
+ return rows;
75
+ }
76
+ /**
77
+ * migrations/ 디렉터리에서 SQL 파일 목록 (정렬)
78
+ */
79
+ function getMigrationFiles() {
80
+ if (!fs.existsSync(MIGRATIONS_DIR))
81
+ return [];
82
+ return fs
83
+ .readdirSync(MIGRATIONS_DIR)
84
+ .filter((f) => f.endsWith(".sql"))
85
+ .sort();
86
+ }
87
+ /**
88
+ * 단일 마이그레이션 실행 (트랜잭션)
89
+ */
90
+ async function runMigration(filename) {
91
+ const pool = (0, database_1.getPool)();
92
+ const client = await pool.connect();
93
+ const filePath = path.join(MIGRATIONS_DIR, filename);
94
+ const sql = fs.readFileSync(filePath, "utf-8");
95
+ const version = filename.replace(/\.sql$/, "");
96
+ try {
97
+ await client.query("BEGIN");
98
+ await client.query(sql);
99
+ await client.query(`INSERT INTO semo.schema_migrations (version) VALUES ($1)`, [version]);
100
+ await client.query("COMMIT");
101
+ }
102
+ catch (err) {
103
+ await client.query("ROLLBACK");
104
+ throw err;
105
+ }
106
+ finally {
107
+ client.release();
108
+ }
109
+ }
110
+ // ============================================================
111
+ // Command registration
112
+ // ============================================================
113
+ function registerDbCommands(program) {
114
+ const dbCmd = program
115
+ .command("db")
116
+ .description("데이터베이스 관리");
117
+ dbCmd
118
+ .command("migrate")
119
+ .description("마이그레이션 실행")
120
+ .option("--status", "적용된 마이그레이션 목록만 표시")
121
+ .option("--dry-run", "미적용 마이그레이션 미리보기 (실행하지 않음)")
122
+ .action(async (options) => {
123
+ const connected = await (0, database_1.isDbConnected)();
124
+ if (!connected) {
125
+ console.error(chalk_1.default.red("DB 연결 실패"));
126
+ await (0, database_1.closeConnection)();
127
+ process.exit(1);
128
+ }
129
+ try {
130
+ await ensureMigrationsTable();
131
+ const applied = await getAppliedMigrations();
132
+ const appliedSet = new Set(applied.map((r) => r.version));
133
+ const allFiles = getMigrationFiles();
134
+ // --status: 적용 상태만 출력
135
+ if (options.status) {
136
+ if (applied.length === 0) {
137
+ console.log(chalk_1.default.yellow("적용된 마이그레이션 없음"));
138
+ }
139
+ else {
140
+ console.log(chalk_1.default.cyan("적용된 마이그레이션:"));
141
+ for (const r of applied) {
142
+ console.log(` ${chalk_1.default.green("✓")} ${r.version} ${chalk_1.default.gray(r.applied_at)}`);
143
+ }
144
+ }
145
+ const pending = allFiles.filter((f) => !appliedSet.has(f.replace(/\.sql$/, "")));
146
+ if (pending.length > 0) {
147
+ console.log(chalk_1.default.yellow(`\n미적용: ${pending.length}개`));
148
+ for (const f of pending) {
149
+ console.log(` ${chalk_1.default.yellow("○")} ${f}`);
150
+ }
151
+ }
152
+ await (0, database_1.closeConnection)();
153
+ return;
154
+ }
155
+ // 미적용 마이그레이션 필터
156
+ const pending = allFiles.filter((f) => !appliedSet.has(f.replace(/\.sql$/, "")));
157
+ if (pending.length === 0) {
158
+ console.log(chalk_1.default.green("모든 마이그레이션이 적용됨"));
159
+ await (0, database_1.closeConnection)();
160
+ return;
161
+ }
162
+ // --dry-run: 미리보기만
163
+ if (options.dryRun) {
164
+ console.log(chalk_1.default.cyan(`미적용 마이그레이션 ${pending.length}개:`));
165
+ for (const f of pending) {
166
+ console.log(` ${chalk_1.default.yellow("○")} ${f}`);
167
+ }
168
+ await (0, database_1.closeConnection)();
169
+ return;
170
+ }
171
+ // 실행
172
+ const spinner = (0, ora_1.default)(`마이그레이션 실행 중 (${pending.length}개)`).start();
173
+ for (const file of pending) {
174
+ spinner.text = `적용 중: ${file}`;
175
+ await runMigration(file);
176
+ spinner.text = `${chalk_1.default.green("✓")} ${file}`;
177
+ console.log(` ${chalk_1.default.green("✓")} ${file}`);
178
+ }
179
+ spinner.succeed(`마이그레이션 완료 (${pending.length}개 적용)`);
180
+ }
181
+ catch (err) {
182
+ console.error(chalk_1.default.red(`마이그레이션 실패: ${err}`));
183
+ process.exit(1);
184
+ }
185
+ finally {
186
+ await (0, database_1.closeConnection)();
187
+ }
188
+ });
189
+ }
@@ -1,11 +1,10 @@
1
1
  /**
2
2
  * semo get <resource> — 세션 중 실시간 DB 쿼리
3
3
  *
4
- * semo get projects [--active]
4
+ * semo get projects [--active] [--format table|json|md]
5
5
  * semo get bots [--status online|offline]
6
6
  * semo get kb [--domain <d>] [--key <k>] [--search <text>]
7
7
  * semo get ontology [--domain <d>]
8
- * semo get tasks [--project <p>] [--status <s>]
9
8
  * semo get sessions [--bot <n>]
10
9
  */
11
10
  import { Command } from "commander";