@team-semicolon/semo-cli 4.3.0 → 4.4.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.
@@ -0,0 +1,366 @@
1
+ "use strict";
2
+ /**
3
+ * Declarative Workspace Audit Runner
4
+ *
5
+ * bot_workspace_standard 테이블에서 규칙을 로드하고,
6
+ * bot_status에서 봇 목록을 가져와 동적으로 TC를 생성·실행.
7
+ *
8
+ * 로컬 스크립트 의존성 없음 — DB가 SoT.
9
+ */
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __importStar = (this && this.__importStar) || (function () {
27
+ var ownKeys = function(o) {
28
+ ownKeys = Object.getOwnPropertyNames || function (o) {
29
+ var ar = [];
30
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
+ return ar;
32
+ };
33
+ return ownKeys(o);
34
+ };
35
+ return function (mod) {
36
+ if (mod && mod.__esModule) return mod;
37
+ var result = {};
38
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
+ __setModuleDefault(result, mod);
40
+ return result;
41
+ };
42
+ })();
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ exports.runDeclarativeWorkspaceAudit = runDeclarativeWorkspaceAudit;
45
+ const fs = __importStar(require("fs"));
46
+ const path = __importStar(require("path"));
47
+ const os = __importStar(require("os"));
48
+ // ============================================================
49
+ // Rule Checkers
50
+ // ============================================================
51
+ function checkExistence(fullPath) {
52
+ try {
53
+ const lstat = fs.lstatSync(fullPath);
54
+ return {
55
+ exists: true,
56
+ isSymlink: lstat.isSymbolicLink(),
57
+ isDir: lstat.isDirectory() || (lstat.isSymbolicLink() && fs.statSync(fullPath).isDirectory()),
58
+ };
59
+ }
60
+ catch {
61
+ return { exists: false, isSymlink: false, isDir: false };
62
+ }
63
+ }
64
+ function checkGlob(wsPath, pattern) {
65
+ // Simple glob matching for common patterns
66
+ const matches = [];
67
+ if (pattern === "*/.git") {
68
+ // Check subdirectories for .git
69
+ try {
70
+ for (const entry of fs.readdirSync(wsPath)) {
71
+ const sub = path.join(wsPath, entry);
72
+ if (fs.statSync(sub).isDirectory() && entry !== ".git") {
73
+ if (fs.existsSync(path.join(sub, ".git"))) {
74
+ matches.push(entry);
75
+ }
76
+ }
77
+ }
78
+ }
79
+ catch { /* */ }
80
+ }
81
+ else if (pattern === "node_modules") {
82
+ try {
83
+ const find = (dir, depth) => {
84
+ if (depth > 3)
85
+ return;
86
+ for (const entry of fs.readdirSync(dir)) {
87
+ const full = path.join(dir, entry);
88
+ if (entry === "node_modules" && fs.statSync(full).isDirectory()) {
89
+ matches.push(path.relative(wsPath, full));
90
+ }
91
+ else if (fs.statSync(full).isDirectory() && !entry.startsWith(".")) {
92
+ find(full, depth + 1);
93
+ }
94
+ }
95
+ };
96
+ find(wsPath, 0);
97
+ }
98
+ catch { /* */ }
99
+ }
100
+ else if (pattern.startsWith("*.")) {
101
+ // Glob for file extensions in root
102
+ const ext = pattern.slice(1); // ".ovpn", ".pem", etc.
103
+ try {
104
+ for (const entry of fs.readdirSync(wsPath)) {
105
+ if (entry.endsWith(ext) && fs.statSync(path.join(wsPath, entry)).isFile()) {
106
+ matches.push(entry);
107
+ }
108
+ }
109
+ }
110
+ catch { /* */ }
111
+ }
112
+ return matches;
113
+ }
114
+ function checkSymlinkTarget(fullPath, expectedTarget) {
115
+ if (!expectedTarget)
116
+ return { ok: true, actual: null };
117
+ const resolved = expectedTarget.replace("$HOME", os.homedir());
118
+ try {
119
+ const actual = fs.readlinkSync(fullPath);
120
+ return { ok: actual === resolved, actual };
121
+ }
122
+ catch {
123
+ return { ok: false, actual: null };
124
+ }
125
+ }
126
+ function checkContentRules(fullPath, rules) {
127
+ const details = [];
128
+ let allPassed = true;
129
+ try {
130
+ const content = fs.readFileSync(fullPath, "utf-8");
131
+ const lines = content.split("\n");
132
+ // max_lines
133
+ if (rules.max_lines !== undefined) {
134
+ if (lines.length > rules.max_lines) {
135
+ details.push(`줄 수 ${lines.length} > ${rules.max_lines}`);
136
+ allPassed = false;
137
+ }
138
+ }
139
+ // required_sections (grep for headings)
140
+ if (rules.required_sections) {
141
+ for (const section of rules.required_sections) {
142
+ const found = content.toLowerCase().includes(section.toLowerCase());
143
+ if (!found) {
144
+ details.push(`섹션 미발견: ${section}`);
145
+ allPassed = false;
146
+ }
147
+ }
148
+ }
149
+ // required_patterns (regex)
150
+ if (rules.required_patterns) {
151
+ for (const pat of rules.required_patterns) {
152
+ const regex = new RegExp(pat, "i");
153
+ if (!regex.test(content)) {
154
+ details.push(`패턴 미발견: ${pat}`);
155
+ allPassed = false;
156
+ }
157
+ }
158
+ }
159
+ // forbidden_patterns (should NOT match)
160
+ if (rules.forbidden_patterns) {
161
+ for (const pat of rules.forbidden_patterns) {
162
+ const regex = new RegExp(pat);
163
+ if (regex.test(content)) {
164
+ details.push(`금지 패턴 탐지: ${pat}`);
165
+ allPassed = false;
166
+ }
167
+ }
168
+ }
169
+ }
170
+ catch {
171
+ details.push("파일 읽기 실패");
172
+ allPassed = false;
173
+ }
174
+ return { passed: allPassed, details };
175
+ }
176
+ // ============================================================
177
+ // Main Runner
178
+ // ============================================================
179
+ function checkRule(wsPath, botId, rule) {
180
+ const results = [];
181
+ const label = (msg) => `${botId}/${rule.path_pattern}: ${msg}`;
182
+ const caseId = `${botId}/${rule.path_pattern}`;
183
+ if (rule.entry_type === "glob") {
184
+ // Glob: check for forbidden matches
185
+ const matches = checkGlob(wsPath, rule.path_pattern);
186
+ if (rule.level === "forbidden") {
187
+ if (matches.length === 0) {
188
+ results.push({
189
+ type: "case",
190
+ id: caseId,
191
+ status: "pass",
192
+ label: label("없음"),
193
+ });
194
+ }
195
+ else {
196
+ for (const m of matches) {
197
+ results.push({
198
+ type: "case",
199
+ id: `${botId}/${m}`,
200
+ status: rule.severity === "error" ? "fail" : "warn",
201
+ label: label(`금지 항목 탐지: ${m}`),
202
+ detail: rule.description || undefined,
203
+ });
204
+ }
205
+ }
206
+ }
207
+ return results;
208
+ }
209
+ const fullPath = path.join(wsPath, rule.path_pattern);
210
+ const { exists, isSymlink, isDir } = checkExistence(fullPath);
211
+ // Required
212
+ if (rule.level === "required") {
213
+ if (!exists) {
214
+ results.push({
215
+ type: "case",
216
+ id: caseId,
217
+ status: "fail",
218
+ label: label("없음"),
219
+ detail: rule.description || undefined,
220
+ });
221
+ return results;
222
+ }
223
+ // Type check
224
+ if (rule.entry_type === "symlink" && !isSymlink) {
225
+ results.push({
226
+ type: "case",
227
+ id: caseId,
228
+ status: "fail",
229
+ label: label("심링크 아님"),
230
+ });
231
+ return results;
232
+ }
233
+ if (rule.entry_type === "dir" && !isDir) {
234
+ results.push({
235
+ type: "case",
236
+ id: caseId,
237
+ status: "fail",
238
+ label: label("디렉토리 아님"),
239
+ });
240
+ return results;
241
+ }
242
+ // Symlink target check
243
+ if (rule.entry_type === "symlink" && rule.symlink_target) {
244
+ const { ok, actual } = checkSymlinkTarget(fullPath, rule.symlink_target);
245
+ if (!ok) {
246
+ results.push({
247
+ type: "case",
248
+ id: caseId,
249
+ status: "fail",
250
+ label: label(`심링크 타겟 불일치 (actual: ${actual})`),
251
+ });
252
+ return results;
253
+ }
254
+ }
255
+ // Content rules
256
+ if (rule.content_rules && rule.entry_type === "file") {
257
+ const { passed, details } = checkContentRules(fullPath, rule.content_rules);
258
+ if (!passed) {
259
+ results.push({
260
+ type: "case",
261
+ id: caseId,
262
+ status: rule.severity === "error" ? "fail" : "warn",
263
+ label: label(details.join("; ")),
264
+ });
265
+ return results;
266
+ }
267
+ }
268
+ // All checks passed
269
+ results.push({
270
+ type: "case",
271
+ id: caseId,
272
+ status: "pass",
273
+ label: label("OK"),
274
+ });
275
+ }
276
+ // Optional — only warn if content rules fail
277
+ if (rule.level === "optional") {
278
+ if (!exists) {
279
+ results.push({
280
+ type: "case",
281
+ id: caseId,
282
+ status: "warn",
283
+ label: label("없음 (선택)"),
284
+ });
285
+ }
286
+ else {
287
+ results.push({
288
+ type: "case",
289
+ id: caseId,
290
+ status: "pass",
291
+ label: label("OK"),
292
+ });
293
+ }
294
+ }
295
+ // Forbidden
296
+ if (rule.level === "forbidden") {
297
+ if (exists) {
298
+ results.push({
299
+ type: "case",
300
+ id: caseId,
301
+ status: rule.severity === "error" ? "fail" : "warn",
302
+ label: label("금지 항목 존재"),
303
+ detail: rule.description || undefined,
304
+ });
305
+ }
306
+ else {
307
+ results.push({
308
+ type: "case",
309
+ id: caseId,
310
+ status: "pass",
311
+ label: label("없음"),
312
+ });
313
+ }
314
+ }
315
+ return results;
316
+ }
317
+ async function runDeclarativeWorkspaceAudit(pool) {
318
+ // 1. Load rules from DB
319
+ const { rows: rules } = await pool.query(`SELECT path_pattern, entry_type, level, severity, category,
320
+ bot_scope, bot_ids, symlink_target, content_rules, description
321
+ FROM semo.bot_workspace_standard
322
+ WHERE spec_version = '2.0'
323
+ ORDER BY category, level DESC, path_pattern`);
324
+ // 2. Load bot list from DB
325
+ const { rows: bots } = await pool.query(`SELECT DISTINCT bot_id FROM semo.bot_status WHERE bot_id != 'shared' ORDER BY bot_id`);
326
+ const results = [];
327
+ // 3. Bot × Rule matrix
328
+ for (const bot of bots) {
329
+ const wsPath = path.join(os.homedir(), `.openclaw-${bot.bot_id}`, "workspace");
330
+ // Check workspace exists
331
+ if (!fs.existsSync(wsPath)) {
332
+ results.push({
333
+ type: "case",
334
+ id: `${bot.bot_id}/workspace`,
335
+ status: "fail",
336
+ label: `${bot.bot_id}: 워크스페이스 없음 (${wsPath})`,
337
+ });
338
+ continue;
339
+ }
340
+ for (const rule of rules) {
341
+ // bot_scope filter
342
+ if (rule.bot_scope === "include" &&
343
+ !rule.bot_ids.includes(bot.bot_id)) {
344
+ continue;
345
+ }
346
+ if (rule.bot_scope === "exclude" &&
347
+ rule.bot_ids.includes(bot.bot_id)) {
348
+ continue;
349
+ }
350
+ const ruleResults = checkRule(wsPath, bot.bot_id, rule);
351
+ results.push(...ruleResults);
352
+ }
353
+ }
354
+ // 4. Summary
355
+ let pass = 0, fail = 0, warn = 0;
356
+ for (const r of results) {
357
+ if (r.status === "pass")
358
+ pass++;
359
+ else if (r.status === "fail")
360
+ fail++;
361
+ else if (r.status === "warn")
362
+ warn++;
363
+ }
364
+ results.push({ type: "summary", pass, fail, warn });
365
+ return results;
366
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-semicolon/semo-cli",
3
- "version": "4.3.0",
3
+ "version": "4.4.0",
4
4
  "description": "SEMO CLI - AI Agent Orchestration Framework Installer",
5
5
  "main": "dist/index.js",
6
6
  "bin": {