@team-semicolon/semo-cli 4.12.0 → 4.15.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.
- package/dist/commands/commitments.js +25 -3
- package/dist/commands/commitments.test.d.ts +9 -0
- package/dist/commands/commitments.test.js +223 -0
- package/dist/commands/context.js +23 -3
- package/dist/commands/harness.d.ts +8 -0
- package/dist/commands/harness.js +412 -0
- package/dist/commands/incubator.d.ts +10 -0
- package/dist/commands/incubator.js +517 -0
- package/dist/commands/service.d.ts +10 -0
- package/dist/commands/service.js +283 -0
- package/dist/commands/sessions.js +156 -0
- package/dist/commands/skill-sync.d.ts +2 -1
- package/dist/commands/skill-sync.js +88 -23
- package/dist/commands/skill-sync.test.js +78 -45
- package/dist/database.d.ts +2 -1
- package/dist/database.js +109 -27
- package/dist/global-cache.js +26 -14
- package/dist/index.js +578 -522
- package/dist/kb.d.ts +4 -4
- package/dist/kb.js +211 -101
- package/dist/semo-workspace.js +51 -0
- package/dist/service-migrate.d.ts +114 -0
- package/dist/service-migrate.js +457 -0
- package/dist/templates/harness/commit-msg +1 -0
- package/dist/templates/harness/commitlint.config.js +11 -0
- package/dist/templates/harness/eslint.config.mjs +17 -0
- package/dist/templates/harness/pr-quality-gate.yml +19 -0
- package/dist/templates/harness/pre-commit +1 -0
- package/dist/templates/harness/pre-push +1 -0
- package/dist/templates/harness/prettierignore +5 -0
- package/dist/templates/harness/prettierrc.json +7 -0
- package/package.json +8 -4
|
@@ -86,6 +86,9 @@ function registerCommitmentsCommands(program) {
|
|
|
86
86
|
JSON.stringify(steps),
|
|
87
87
|
]);
|
|
88
88
|
console.log(chalk_1.default.green(`✔ commitment created: ${id}`));
|
|
89
|
+
if (!options.sourceType) {
|
|
90
|
+
console.log(chalk_1.default.yellow(`⚠ source-type 미지정: 자동 검증 불가. --source-type 권장.`));
|
|
91
|
+
}
|
|
89
92
|
console.log(JSON.stringify({ id, bot_id: options.botId, title: options.title, deadline_at: deadlineAt }));
|
|
90
93
|
}
|
|
91
94
|
catch (err) {
|
|
@@ -239,7 +242,8 @@ function registerCommitmentsCommands(program) {
|
|
|
239
242
|
}
|
|
240
243
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
241
244
|
params.push(parseInt(options.limit));
|
|
242
|
-
const result = await pool.query(`SELECT id, bot_id, status, title,
|
|
245
|
+
const result = await pool.query(`SELECT id, bot_id, status, title, description, source_type, source_ref,
|
|
246
|
+
deadline_at::text, steps,
|
|
243
247
|
last_heartbeat_at::text, created_at::text, completed_at::text, metadata
|
|
244
248
|
FROM semo.bot_commitments
|
|
245
249
|
${where}
|
|
@@ -290,6 +294,8 @@ function registerCommitmentsCommands(program) {
|
|
|
290
294
|
.command("watch")
|
|
291
295
|
.description("워치독 — 활성 약속의 health 상태 조회")
|
|
292
296
|
.option("--format <type>", "출력 형식 (table|json)", "table")
|
|
297
|
+
.option("--bot-id <id>", "특정 봇만 필터")
|
|
298
|
+
.option("--exclude-bot <id>", "특정 봇 제외")
|
|
293
299
|
.action(async (options) => {
|
|
294
300
|
const connected = await (0, database_1.isDbConnected)();
|
|
295
301
|
if (!connected) {
|
|
@@ -299,17 +305,33 @@ function registerCommitmentsCommands(program) {
|
|
|
299
305
|
}
|
|
300
306
|
try {
|
|
301
307
|
const pool = (0, database_1.getPool)();
|
|
302
|
-
const
|
|
308
|
+
const conditions = [];
|
|
309
|
+
const params = [];
|
|
310
|
+
let paramIdx = 1;
|
|
311
|
+
if (options.botId) {
|
|
312
|
+
conditions.push(`bot_id = $${paramIdx++}`);
|
|
313
|
+
params.push(options.botId);
|
|
314
|
+
}
|
|
315
|
+
if (options.excludeBot) {
|
|
316
|
+
conditions.push(`bot_id != $${paramIdx++}`);
|
|
317
|
+
params.push(options.excludeBot);
|
|
318
|
+
}
|
|
319
|
+
const whereClause = conditions.length > 0
|
|
320
|
+
? `WHERE ${conditions.join(" AND ")}`
|
|
321
|
+
: "";
|
|
322
|
+
const result = await pool.query(`SELECT id, bot_id, status, title, description, source_type, source_ref,
|
|
323
|
+
deadline_at::text,
|
|
303
324
|
health, minutes_since_heartbeat, minutes_overdue,
|
|
304
325
|
steps, last_heartbeat_at::text, created_at::text, metadata
|
|
305
326
|
FROM semo.v_active_commitments
|
|
327
|
+
${whereClause}
|
|
306
328
|
ORDER BY
|
|
307
329
|
CASE health
|
|
308
330
|
WHEN 'overdue' THEN 0
|
|
309
331
|
WHEN 'stale' THEN 1
|
|
310
332
|
ELSE 2
|
|
311
333
|
END,
|
|
312
|
-
deadline_at ASC NULLS LAST
|
|
334
|
+
deadline_at ASC NULLS LAST`, params);
|
|
313
335
|
if (options.format === "json") {
|
|
314
336
|
const summary = {
|
|
315
337
|
total: result.rows.length,
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* commitments 테스트 — bot_commitments CRUD + 워치독 뷰 검증
|
|
4
|
+
*
|
|
5
|
+
* 실행: npx ts-node packages/cli/src/commands/commitments.test.ts
|
|
6
|
+
*
|
|
7
|
+
* 실제 DB 연결 필요 (semo.bot_commitments 테이블).
|
|
8
|
+
* 테스트 데이터는 bot_id='__test__' 접두사로 생성하며 종료 시 정리.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
const database_1 = require("../database");
|
|
12
|
+
const TEST_BOT = "__test__";
|
|
13
|
+
let passed = 0;
|
|
14
|
+
let failed = 0;
|
|
15
|
+
const createdIds = [];
|
|
16
|
+
function assert(condition, message) {
|
|
17
|
+
if (condition) {
|
|
18
|
+
passed++;
|
|
19
|
+
console.log(` ✅ ${message}`);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
failed++;
|
|
23
|
+
console.log(` ❌ ${message}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function makeId() {
|
|
27
|
+
const rand = Math.random().toString(36).slice(2, 6);
|
|
28
|
+
return `cmt-${TEST_BOT}-${Date.now()}-${rand}`;
|
|
29
|
+
}
|
|
30
|
+
async function cleanup() {
|
|
31
|
+
try {
|
|
32
|
+
const pool = (0, database_1.getPool)();
|
|
33
|
+
await pool.query(`DELETE FROM semo.bot_commitments WHERE bot_id = $1`, [TEST_BOT]);
|
|
34
|
+
}
|
|
35
|
+
catch { /* ignore */ }
|
|
36
|
+
}
|
|
37
|
+
async function run() {
|
|
38
|
+
console.log("\n🧪 commitments.test.ts\n");
|
|
39
|
+
const connected = await (0, database_1.isDbConnected)();
|
|
40
|
+
if (!connected) {
|
|
41
|
+
console.log("❌ DB 연결 실패 — 테스트 스킵");
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
const pool = (0, database_1.getPool)();
|
|
45
|
+
// 사전 정리
|
|
46
|
+
await cleanup();
|
|
47
|
+
// ── Case 1: CREATE — 기본 생성 ─────────────────────────────────────────────
|
|
48
|
+
console.log("Case 1: 기본 생성");
|
|
49
|
+
const id1 = makeId();
|
|
50
|
+
createdIds.push(id1);
|
|
51
|
+
{
|
|
52
|
+
await pool.query(`INSERT INTO semo.bot_commitments (id, bot_id, status, title, deadline_at)
|
|
53
|
+
VALUES ($1, $2, 'pending', '테스트 약속 1', NOW() + INTERVAL '15 minutes')`, [id1, TEST_BOT]);
|
|
54
|
+
const r = await pool.query(`SELECT * FROM semo.bot_commitments WHERE id = $1`, [id1]);
|
|
55
|
+
assert(r.rows.length === 1, "INSERT 성공");
|
|
56
|
+
assert(r.rows[0].status === "pending", "초기 상태 = pending");
|
|
57
|
+
assert(r.rows[0].bot_id === TEST_BOT, `bot_id = ${TEST_BOT}`);
|
|
58
|
+
assert(r.rows[0].completed_at === null, "completed_at = null (아직 완료 전)");
|
|
59
|
+
}
|
|
60
|
+
// ── Case 2: CREATE — steps JSON ────────────────────────────────────────────
|
|
61
|
+
console.log("Case 2: steps JSON 저장");
|
|
62
|
+
const id2 = makeId();
|
|
63
|
+
createdIds.push(id2);
|
|
64
|
+
{
|
|
65
|
+
const steps = [
|
|
66
|
+
{ label: "Typography", done: false },
|
|
67
|
+
{ label: "Layout", done: false },
|
|
68
|
+
{ label: "Motion", done: false },
|
|
69
|
+
];
|
|
70
|
+
await pool.query(`INSERT INTO semo.bot_commitments (id, bot_id, status, title, steps)
|
|
71
|
+
VALUES ($1, $2, 'pending', 'Steps 테스트', $3)`, [id2, TEST_BOT, JSON.stringify(steps)]);
|
|
72
|
+
const r = await pool.query(`SELECT steps FROM semo.bot_commitments WHERE id = $1`, [id2]);
|
|
73
|
+
const savedSteps = r.rows[0].steps;
|
|
74
|
+
assert(Array.isArray(savedSteps), "steps는 배열");
|
|
75
|
+
assert(savedSteps.length === 3, `steps 3개 (got ${savedSteps.length})`);
|
|
76
|
+
assert(savedSteps[0].label === "Typography", "첫 step label 일치");
|
|
77
|
+
assert(savedSteps.every((s) => !s.done), "모든 step done=false");
|
|
78
|
+
}
|
|
79
|
+
// ── Case 3: UPDATE — heartbeat ─────────────────────────────────────────────
|
|
80
|
+
console.log("Case 3: heartbeat 갱신");
|
|
81
|
+
{
|
|
82
|
+
await pool.query(`UPDATE semo.bot_commitments
|
|
83
|
+
SET last_heartbeat_at = NOW(),
|
|
84
|
+
status = CASE WHEN status = 'pending' THEN 'active' ELSE status END
|
|
85
|
+
WHERE id = $1`, [id1]);
|
|
86
|
+
const r = await pool.query(`SELECT status, last_heartbeat_at FROM semo.bot_commitments WHERE id = $1`, [id1]);
|
|
87
|
+
assert(r.rows[0].status === "active", "heartbeat → status = active");
|
|
88
|
+
assert(r.rows[0].last_heartbeat_at !== null, "last_heartbeat_at 채워짐");
|
|
89
|
+
}
|
|
90
|
+
// ── Case 4: UPDATE — step-done ─────────────────────────────────────────────
|
|
91
|
+
console.log("Case 4: step 완료 처리");
|
|
92
|
+
{
|
|
93
|
+
await pool.query(`UPDATE semo.bot_commitments
|
|
94
|
+
SET steps = (
|
|
95
|
+
SELECT jsonb_agg(
|
|
96
|
+
CASE
|
|
97
|
+
WHEN elem->>'label' = $2 THEN jsonb_set(elem, '{done}', 'true')
|
|
98
|
+
ELSE elem
|
|
99
|
+
END
|
|
100
|
+
)
|
|
101
|
+
FROM jsonb_array_elements(steps) AS elem
|
|
102
|
+
)
|
|
103
|
+
WHERE id = $1`, [id2, "Typography"]);
|
|
104
|
+
const r = await pool.query(`SELECT steps FROM semo.bot_commitments WHERE id = $1`, [id2]);
|
|
105
|
+
const steps = r.rows[0].steps;
|
|
106
|
+
const typo = steps.find((s) => s.label === "Typography");
|
|
107
|
+
const layout = steps.find((s) => s.label === "Layout");
|
|
108
|
+
assert(typo.done === true, "Typography done=true");
|
|
109
|
+
assert(layout.done === false, "Layout 여전히 done=false");
|
|
110
|
+
}
|
|
111
|
+
// ── Case 5: DONE — completed_at 트리거 ────────────────────────────────────
|
|
112
|
+
console.log("Case 5: done → completed_at 자동 채움");
|
|
113
|
+
{
|
|
114
|
+
await pool.query(`UPDATE semo.bot_commitments SET status = 'done' WHERE id = $1`, [id1]);
|
|
115
|
+
const r = await pool.query(`SELECT status, completed_at FROM semo.bot_commitments WHERE id = $1`, [id1]);
|
|
116
|
+
assert(r.rows[0].status === "done", "status = done");
|
|
117
|
+
assert(r.rows[0].completed_at !== null, "completed_at 자동 채워짐 (트리거)");
|
|
118
|
+
}
|
|
119
|
+
// ── Case 6: FAIL — metadata.fail_reason ────────────────────────────────────
|
|
120
|
+
console.log("Case 6: fail + reason 저장");
|
|
121
|
+
const id3 = makeId();
|
|
122
|
+
createdIds.push(id3);
|
|
123
|
+
{
|
|
124
|
+
await pool.query(`INSERT INTO semo.bot_commitments (id, bot_id, status, title)
|
|
125
|
+
VALUES ($1, $2, 'active', '실패 테스트')`, [id3, TEST_BOT]);
|
|
126
|
+
await pool.query(`UPDATE semo.bot_commitments
|
|
127
|
+
SET status = 'failed', metadata = metadata || jsonb_build_object('fail_reason', $2::text)
|
|
128
|
+
WHERE id = $1`, [id3, "빌드 실패"]);
|
|
129
|
+
const r = await pool.query(`SELECT status, completed_at, metadata FROM semo.bot_commitments WHERE id = $1`, [id3]);
|
|
130
|
+
assert(r.rows[0].status === "failed", "status = failed");
|
|
131
|
+
assert(r.rows[0].completed_at !== null, "completed_at 자동 채워짐");
|
|
132
|
+
assert(r.rows[0].metadata.fail_reason === "빌드 실패", "metadata.fail_reason 저장됨");
|
|
133
|
+
}
|
|
134
|
+
// ── Case 7: updated_at 트리거 ──────────────────────────────────────────────
|
|
135
|
+
console.log("Case 7: updated_at 자동 갱신");
|
|
136
|
+
{
|
|
137
|
+
const before = await pool.query(`SELECT updated_at FROM semo.bot_commitments WHERE id = $1`, [id2]);
|
|
138
|
+
// 최소 1ms 대기
|
|
139
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
140
|
+
await pool.query(`UPDATE semo.bot_commitments SET title = 'Updated title' WHERE id = $1`, [id2]);
|
|
141
|
+
const after = await pool.query(`SELECT updated_at FROM semo.bot_commitments WHERE id = $1`, [id2]);
|
|
142
|
+
const t1 = new Date(before.rows[0].updated_at).getTime();
|
|
143
|
+
const t2 = new Date(after.rows[0].updated_at).getTime();
|
|
144
|
+
assert(t2 > t1, `updated_at 갱신됨 (${t2 - t1}ms 차이)`);
|
|
145
|
+
}
|
|
146
|
+
// ── Case 8: v_active_commitments 뷰 ───────────────────────────────────────
|
|
147
|
+
console.log("Case 8: v_active_commitments 뷰");
|
|
148
|
+
const id4 = makeId();
|
|
149
|
+
createdIds.push(id4);
|
|
150
|
+
{
|
|
151
|
+
// overdue commitment 생성 (deadline 과거)
|
|
152
|
+
await pool.query(`INSERT INTO semo.bot_commitments (id, bot_id, status, title, deadline_at)
|
|
153
|
+
VALUES ($1, $2, 'active', 'Overdue 테스트', NOW() - INTERVAL '10 minutes')`, [id4, TEST_BOT]);
|
|
154
|
+
const r = await pool.query(`SELECT health, minutes_overdue FROM semo.v_active_commitments WHERE id = $1`, [id4]);
|
|
155
|
+
assert(r.rows.length === 1, "뷰에서 조회 가능");
|
|
156
|
+
assert(r.rows[0].health === "overdue", `health = overdue (got ${r.rows[0].health})`);
|
|
157
|
+
assert(r.rows[0].minutes_overdue > 0, `minutes_overdue > 0 (got ${r.rows[0].minutes_overdue})`);
|
|
158
|
+
}
|
|
159
|
+
// ── Case 9: stale 감지 ─────────────────────────────────────────────────────
|
|
160
|
+
console.log("Case 9: stale heartbeat 감지");
|
|
161
|
+
const id5 = makeId();
|
|
162
|
+
createdIds.push(id5);
|
|
163
|
+
{
|
|
164
|
+
await pool.query(`INSERT INTO semo.bot_commitments (id, bot_id, status, title, last_heartbeat_at)
|
|
165
|
+
VALUES ($1, $2, 'active', 'Stale 테스트', NOW() - INTERVAL '45 minutes')`, [id5, TEST_BOT]);
|
|
166
|
+
const r = await pool.query(`SELECT health, minutes_since_heartbeat FROM semo.v_active_commitments WHERE id = $1`, [id5]);
|
|
167
|
+
assert(r.rows[0].health === "stale", `health = stale (got ${r.rows[0].health})`);
|
|
168
|
+
assert(r.rows[0].minutes_since_heartbeat >= 44, `heartbeat 45분+ 전`);
|
|
169
|
+
}
|
|
170
|
+
// ── Case 10: on-track ──────────────────────────────────────────────────────
|
|
171
|
+
console.log("Case 10: on-track (정상)");
|
|
172
|
+
const id6 = makeId();
|
|
173
|
+
createdIds.push(id6);
|
|
174
|
+
{
|
|
175
|
+
await pool.query(`INSERT INTO semo.bot_commitments (id, bot_id, status, title, deadline_at, last_heartbeat_at)
|
|
176
|
+
VALUES ($1, $2, 'active', 'On-track 테스트', NOW() + INTERVAL '30 minutes', NOW())`, [id6, TEST_BOT]);
|
|
177
|
+
const r = await pool.query(`SELECT health FROM semo.v_active_commitments WHERE id = $1`, [id6]);
|
|
178
|
+
assert(r.rows[0].health === "on-track", `health = on-track (got ${r.rows[0].health})`);
|
|
179
|
+
}
|
|
180
|
+
// ── Case 11: 완료된 건은 뷰에서 제외 ──────────────────────────────────────
|
|
181
|
+
console.log("Case 11: 완료된 commitment는 뷰에서 제외");
|
|
182
|
+
{
|
|
183
|
+
const r = await pool.query(`SELECT * FROM semo.v_active_commitments WHERE id = $1`, [id1] // Case 5에서 done 처리됨
|
|
184
|
+
);
|
|
185
|
+
assert(r.rows.length === 0, "done 상태는 v_active_commitments에서 제외");
|
|
186
|
+
}
|
|
187
|
+
// ── Case 12: 인덱스 사용 확인 ─────────────────────────────────────────────
|
|
188
|
+
console.log("Case 12: 인덱스 존재 확인");
|
|
189
|
+
{
|
|
190
|
+
const r = await pool.query(`SELECT indexname FROM pg_indexes WHERE tablename = 'bot_commitments' AND schemaname = 'semo' ORDER BY indexname`);
|
|
191
|
+
const names = r.rows.map((row) => row.indexname);
|
|
192
|
+
assert(names.includes("idx_commitments_active"), "idx_commitments_active 존재");
|
|
193
|
+
assert(names.includes("idx_commitments_bot"), "idx_commitments_bot 존재");
|
|
194
|
+
}
|
|
195
|
+
// ── Case 13: watch --bot-id 필터 ─────────────────────────────────────────
|
|
196
|
+
console.log("Case 13: watch --bot-id 필터");
|
|
197
|
+
{
|
|
198
|
+
// __test__ 봇의 활성 커밋먼트만 조회 (id4=overdue, id5=stale, id6=on-track)
|
|
199
|
+
const r = await pool.query(`SELECT id FROM semo.v_active_commitments WHERE bot_id = $1`, [TEST_BOT]);
|
|
200
|
+
assert(r.rows.length >= 3, `--bot-id 필터: __test__ 항목 3건+ (got ${r.rows.length})`);
|
|
201
|
+
assert(r.rows.every((row) => row.id.startsWith("cmt-__test__")), "모두 __test__ 봇 소유");
|
|
202
|
+
}
|
|
203
|
+
// ── Case 14: watch --exclude-bot 필터 ──────────────────────────────────
|
|
204
|
+
console.log("Case 14: watch --exclude-bot 필터");
|
|
205
|
+
{
|
|
206
|
+
const r = await pool.query(`SELECT id, bot_id FROM semo.v_active_commitments WHERE bot_id != $1`, [TEST_BOT]);
|
|
207
|
+
const hasTestBot = r.rows.some((row) => row.bot_id === TEST_BOT);
|
|
208
|
+
assert(!hasTestBot, "--exclude-bot 필터: __test__ 봇 제외됨");
|
|
209
|
+
}
|
|
210
|
+
// ── 정리 ──────────────────────────────────────────────────────────────────
|
|
211
|
+
await cleanup();
|
|
212
|
+
// ── 결과 ──────────────────────────────────────────────────────────────────
|
|
213
|
+
console.log(`\n${"─".repeat(40)}`);
|
|
214
|
+
console.log(`총 ${passed + failed}개 테스트: ✅ ${passed} passed, ❌ ${failed} failed\n`);
|
|
215
|
+
await (0, database_1.closeConnection)();
|
|
216
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
217
|
+
}
|
|
218
|
+
run().catch(async (err) => {
|
|
219
|
+
console.error("테스트 런타임 에러:", err);
|
|
220
|
+
await cleanup();
|
|
221
|
+
await (0, database_1.closeConnection)();
|
|
222
|
+
process.exit(1);
|
|
223
|
+
});
|
package/dist/commands/context.js
CHANGED
|
@@ -53,7 +53,8 @@ const path = __importStar(require("path"));
|
|
|
53
53
|
const os = __importStar(require("os"));
|
|
54
54
|
const database_1 = require("../database");
|
|
55
55
|
const kb_1 = require("../kb");
|
|
56
|
-
// [v4.
|
|
56
|
+
// [v4.7.0] syncSkillsToDB 복원 — 워크스페이스 → DB 동기화 경로 재활성화
|
|
57
|
+
const skill_sync_1 = require("./skill-sync");
|
|
57
58
|
const global_cache_1 = require("../global-cache");
|
|
58
59
|
const semo_workspace_1 = require("../semo-workspace");
|
|
59
60
|
// ============================================================
|
|
@@ -211,8 +212,27 @@ function registerContextCommands(program) {
|
|
|
211
212
|
// [v4.2.0] KB→md 파일 생성 제거 — semo CLI kb 명령어로 대체
|
|
212
213
|
// 기존 memory/*.md (team, projects, decisions, infra, process, bots, ontology) 파일은
|
|
213
214
|
// semo CLI가 실시간 DB 조회로 대체합니다.
|
|
214
|
-
// [v4.
|
|
215
|
-
//
|
|
215
|
+
// [v4.7.0] 워크스페이스 → DB 스킬 동기화 복원
|
|
216
|
+
// v4.4.0에서 제거했으나, 워크스페이스 스킬이 DB에 미반영되는 문제 발생.
|
|
217
|
+
// --no-skills 플래그로 스킵 가능.
|
|
218
|
+
if (options.skills !== false) {
|
|
219
|
+
spinner.text = "스킬 동기화 (워크스페이스 → DB)...";
|
|
220
|
+
try {
|
|
221
|
+
const client = await pool.connect();
|
|
222
|
+
try {
|
|
223
|
+
const skillResult = await (0, skill_sync_1.syncSkillsToDB)(client, pool);
|
|
224
|
+
if (skillResult.total > 0) {
|
|
225
|
+
console.log(chalk_1.default.green(` ✓ 스킬 DB 동기화: ${skillResult.total}개 스킬 upsert`));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
client.release();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch (skillErr) {
|
|
233
|
+
console.log(chalk_1.default.yellow(` ⚠ 스킬 DB 동기화 실패 (비치명적): ${skillErr}`));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
216
236
|
// DB → 글로벌 캐시 (skills/commands/agents → ~/.claude/)
|
|
217
237
|
if (options.globalCache !== false) {
|
|
218
238
|
spinner.text = "글로벌 캐시 동기화 (skills/commands/agents)...";
|