sentix 2.0.1
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/LICENSE +21 -0
- package/README.md +627 -0
- package/bin/sentix.js +116 -0
- package/package.json +37 -0
- package/src/CLAUDE.md +26 -0
- package/src/commands/CLAUDE.md +29 -0
- package/src/commands/context.js +227 -0
- package/src/commands/doctor.js +213 -0
- package/src/commands/evolve.js +203 -0
- package/src/commands/feature.js +327 -0
- package/src/commands/init.js +467 -0
- package/src/commands/metrics.js +170 -0
- package/src/commands/plugin.js +111 -0
- package/src/commands/run.js +303 -0
- package/src/commands/safety.js +163 -0
- package/src/commands/status.js +149 -0
- package/src/commands/ticket.js +362 -0
- package/src/commands/update.js +143 -0
- package/src/commands/version.js +218 -0
- package/src/context.js +104 -0
- package/src/dev-server.js +154 -0
- package/src/lib/agent-loop.js +110 -0
- package/src/lib/api-client.js +213 -0
- package/src/lib/changelog.js +110 -0
- package/src/lib/pipeline.js +218 -0
- package/src/lib/provider.js +129 -0
- package/src/lib/safety.js +146 -0
- package/src/lib/semver.js +40 -0
- package/src/lib/similarity.js +58 -0
- package/src/lib/ticket-index.js +137 -0
- package/src/lib/tools.js +142 -0
- package/src/lib/verify-gates.js +254 -0
- package/src/plugins/auto-version.js +89 -0
- package/src/plugins/logger.js +55 -0
- package/src/registry.js +63 -0
- package/src/version.js +15 -0
package/bin/sentix.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* sentix — Autonomous multi-agent DevSecOps pipeline CLI
|
|
5
|
+
*
|
|
6
|
+
* Entry point + plugin loader.
|
|
7
|
+
* Loading order: src/commands/ → src/plugins/ → .sentix/plugins/ (project-local)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readdir } from 'node:fs/promises';
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
|
+
import { resolve, dirname } from 'node:path';
|
|
13
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
14
|
+
import { getAllCommands, getCommand, runHooks } from '../src/registry.js';
|
|
15
|
+
import { createContext } from '../src/context.js';
|
|
16
|
+
import { VERSION } from '../src/version.js';
|
|
17
|
+
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = dirname(__filename);
|
|
20
|
+
const srcDir = resolve(__dirname, '..', 'src');
|
|
21
|
+
|
|
22
|
+
// ── Load built-in commands ──────────────────────────────
|
|
23
|
+
|
|
24
|
+
async function loadModules(dir) {
|
|
25
|
+
if (!existsSync(dir)) return;
|
|
26
|
+
const files = await readdir(dir);
|
|
27
|
+
for (const file of files.sort()) {
|
|
28
|
+
if (file.endsWith('.js')) {
|
|
29
|
+
await import(pathToFileURL(resolve(dir, file)).href);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── Load project-local plugins ──────────────────────────
|
|
35
|
+
|
|
36
|
+
async function loadProjectPlugins(cwd) {
|
|
37
|
+
const pluginDir = resolve(cwd, '.sentix', 'plugins');
|
|
38
|
+
if (!existsSync(pluginDir)) return;
|
|
39
|
+
const files = await readdir(pluginDir);
|
|
40
|
+
for (const file of files.sort()) {
|
|
41
|
+
if (file.endsWith('.js')) {
|
|
42
|
+
await import(pathToFileURL(resolve(pluginDir, file)).href);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── Help ─────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
function showHelp() {
|
|
50
|
+
console.log(`
|
|
51
|
+
sentix v${VERSION} — Autonomous multi-agent DevSecOps pipeline
|
|
52
|
+
|
|
53
|
+
Usage: sentix <command> [args...]
|
|
54
|
+
|
|
55
|
+
Commands:`);
|
|
56
|
+
|
|
57
|
+
const cmds = getAllCommands();
|
|
58
|
+
const maxLen = Math.max(...[...cmds.keys()].map(k => k.length));
|
|
59
|
+
for (const [name, cmd] of cmds) {
|
|
60
|
+
console.log(` ${name.padEnd(maxLen + 2)} ${cmd.description}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log(`
|
|
64
|
+
Run 'sentix <command> --help' for details on a specific command.
|
|
65
|
+
`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Main ─────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
async function main() {
|
|
71
|
+
// Load in order: commands → built-in plugins → project plugins
|
|
72
|
+
await loadModules(resolve(srcDir, 'commands'));
|
|
73
|
+
await loadModules(resolve(srcDir, 'plugins'));
|
|
74
|
+
|
|
75
|
+
const cwd = process.cwd();
|
|
76
|
+
await loadProjectPlugins(cwd);
|
|
77
|
+
|
|
78
|
+
const [commandName, ...args] = process.argv.slice(2);
|
|
79
|
+
|
|
80
|
+
if (!commandName || commandName === '--help' || commandName === '-h') {
|
|
81
|
+
showHelp();
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (commandName === '--version' || commandName === '-v') {
|
|
86
|
+
console.log(`sentix v${VERSION}`);
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const cmd = getCommand(commandName);
|
|
91
|
+
if (!cmd) {
|
|
92
|
+
console.error(`Unknown command: ${commandName}`);
|
|
93
|
+
console.error(`Run 'sentix --help' to see available commands.`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Per-command --help
|
|
98
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
99
|
+
console.log(`\n${cmd.description}\n`);
|
|
100
|
+
console.log(`Usage: ${cmd.usage}\n`);
|
|
101
|
+
process.exit(0);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const ctx = createContext(cwd);
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
await runHooks('before:command', { command: commandName, args, ctx });
|
|
108
|
+
await cmd.run(args.filter(a => a !== '--help' && a !== '-h'), ctx);
|
|
109
|
+
await runHooks('after:command', { command: commandName, args, ctx });
|
|
110
|
+
} catch (err) {
|
|
111
|
+
ctx.error(err.message);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sentix",
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "Autonomous multi-agent DevSecOps pipeline CLI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"sentix": "./bin/sentix.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "node src/dev-server.js",
|
|
11
|
+
"start": "node bin/sentix.js",
|
|
12
|
+
"test": "node --test __tests__/*.test.js",
|
|
13
|
+
"doctor": "node bin/sentix.js doctor"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18.0.0"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"bin/",
|
|
20
|
+
"src/"
|
|
21
|
+
],
|
|
22
|
+
"keywords": [
|
|
23
|
+
"devops",
|
|
24
|
+
"devsecops",
|
|
25
|
+
"pipeline",
|
|
26
|
+
"autonomous",
|
|
27
|
+
"multi-agent",
|
|
28
|
+
"claude",
|
|
29
|
+
"ai"
|
|
30
|
+
],
|
|
31
|
+
"author": "JANUS",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/kgg1226/sentix.git"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/CLAUDE.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# src/ — Sentix Core
|
|
2
|
+
|
|
3
|
+
ESM 모듈 (`"type": "module"`). 외부 의존성 제로 — Node.js 내장 모듈만 사용.
|
|
4
|
+
|
|
5
|
+
## 로딩 순서
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
bin/sentix.js
|
|
9
|
+
→ src/commands/*.js (내장 명령어)
|
|
10
|
+
→ src/plugins/*.js (내장 플러그인)
|
|
11
|
+
→ .sentix/plugins/*.js (프로젝트 로컬 플러그인)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## 핵심 모듈
|
|
15
|
+
|
|
16
|
+
| 파일 | 역할 |
|
|
17
|
+
|------|------|
|
|
18
|
+
| `registry.js` | 명령어/훅 등록 (`registerCommand`, `registerHook`) |
|
|
19
|
+
| `context.js` | 명령어에 주입되는 `ctx` 객체 (fs 헬퍼, 로깅) |
|
|
20
|
+
| `version.js` | `package.json`에서 버전 읽기 |
|
|
21
|
+
| `dev-server.js` | 개발용 HTTP API 서버 (`:4400`) |
|
|
22
|
+
|
|
23
|
+
## 새 모듈 추가 시
|
|
24
|
+
|
|
25
|
+
- `commands/` 또는 `plugins/` 에 `.js` 파일을 넣으면 자동 로드
|
|
26
|
+
- `registerCommand()` 또는 `registerHook()`으로 등록
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# src/commands/ — CLI 명령어
|
|
2
|
+
|
|
3
|
+
## 명령어 추가법
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
import { registerCommand } from '../registry.js';
|
|
7
|
+
|
|
8
|
+
registerCommand('name', {
|
|
9
|
+
description: '명령어 설명',
|
|
10
|
+
usage: 'sentix name [args]',
|
|
11
|
+
async run(args, ctx) {
|
|
12
|
+
// args: string[], ctx: context object
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 기존 명령어
|
|
18
|
+
|
|
19
|
+
| 파일 | 명령어 | 설명 |
|
|
20
|
+
|------|--------|------|
|
|
21
|
+
| `init.js` | `sentix init` | 프로젝트 초기화 |
|
|
22
|
+
| `run.js` | `sentix run "요청"` | Governor 파이프라인 실행 |
|
|
23
|
+
| `doctor.js` | `sentix doctor` | 설치 상태 진단 |
|
|
24
|
+
| `status.js` | `sentix status` | Governor 상태 조회 |
|
|
25
|
+
| `metrics.js` | `sentix metrics` | 에이전트 메트릭 분석 |
|
|
26
|
+
| `update.js` | `sentix update` | 프레임워크 업데이트 |
|
|
27
|
+
| `plugin.js` | `sentix plugin` | 플러그인 관리 |
|
|
28
|
+
|
|
29
|
+
파일은 알파벳순으로 자동 로드됨.
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sentix context — 연동 프로젝트의 컨텍스트를 가져온다
|
|
3
|
+
*
|
|
4
|
+
* registry.md에 등록된 프로젝트의 INTERFACE.md, README.md 등을
|
|
5
|
+
* 로컬(../) 또는 GitHub API로 가져와 tasks/context/에 캐시한다.
|
|
6
|
+
*
|
|
7
|
+
* 사용법:
|
|
8
|
+
* sentix context # 전체 프로젝트 컨텍스트 동기화
|
|
9
|
+
* sentix context asset-manager # 특정 프로젝트만
|
|
10
|
+
* sentix context asset-manager --full # src/ 스키마까지 포함
|
|
11
|
+
* sentix context --list # 등록된 프로젝트 목록만 출력
|
|
12
|
+
* sentix context --clean # 캐시 삭제
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { registerCommand } from '../registry.js';
|
|
16
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
17
|
+
import { resolve } from 'node:path';
|
|
18
|
+
import { execSync } from 'node:child_process';
|
|
19
|
+
|
|
20
|
+
registerCommand('context', {
|
|
21
|
+
description: 'Fetch cross-project context from registry',
|
|
22
|
+
usage: 'sentix context [project] [--full] [--list] [--clean]',
|
|
23
|
+
|
|
24
|
+
async run(args, ctx) {
|
|
25
|
+
const listOnly = args.includes('--list');
|
|
26
|
+
const fullMode = args.includes('--full');
|
|
27
|
+
const cleanMode = args.includes('--clean');
|
|
28
|
+
const target = args.find(a => !a.startsWith('--'));
|
|
29
|
+
|
|
30
|
+
// ── registry.md 파싱 ────────────────────────────
|
|
31
|
+
if (!ctx.exists('registry.md')) {
|
|
32
|
+
ctx.error('registry.md not found. Run: sentix init');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const registry = await ctx.readFile('registry.md');
|
|
37
|
+
const projects = parseRegistry(registry);
|
|
38
|
+
|
|
39
|
+
if (projects.length === 0) {
|
|
40
|
+
ctx.warn('No projects registered in registry.md');
|
|
41
|
+
ctx.log('Add projects to the table in registry.md');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── --list ──────────────────────────────────────
|
|
46
|
+
if (listOnly) {
|
|
47
|
+
ctx.log('=== Registered Projects ===\n');
|
|
48
|
+
for (const p of projects) {
|
|
49
|
+
const status = checkProjectAccess(p, ctx.cwd);
|
|
50
|
+
ctx.log(` ${status.icon} ${p.name.padEnd(20)} ${status.label}`);
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── --clean ─────────────────────────────────────
|
|
56
|
+
if (cleanMode) {
|
|
57
|
+
if (ctx.exists('tasks/context')) {
|
|
58
|
+
const { rmSync } = await import('node:fs');
|
|
59
|
+
rmSync(resolve(ctx.cwd, 'tasks/context'), { recursive: true, force: true });
|
|
60
|
+
ctx.success('Cleared tasks/context/');
|
|
61
|
+
} else {
|
|
62
|
+
ctx.log('tasks/context/ does not exist');
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── 대상 프로젝트 필터링 ────────────────────────
|
|
68
|
+
const targets = target
|
|
69
|
+
? projects.filter(p => p.name === target)
|
|
70
|
+
: projects;
|
|
71
|
+
|
|
72
|
+
if (target && targets.length === 0) {
|
|
73
|
+
ctx.error(`Project "${target}" not found in registry.md`);
|
|
74
|
+
ctx.log('Registered: ' + projects.map(p => p.name).join(', '));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
ctx.log(`=== Cross-Project Context Sync ===\n`);
|
|
79
|
+
|
|
80
|
+
let synced = 0;
|
|
81
|
+
let failed = 0;
|
|
82
|
+
|
|
83
|
+
for (const project of targets) {
|
|
84
|
+
ctx.log(`--- ${project.name} ---`);
|
|
85
|
+
|
|
86
|
+
const access = checkProjectAccess(project, ctx.cwd);
|
|
87
|
+
const contextDir = `tasks/context/${project.name}`;
|
|
88
|
+
|
|
89
|
+
if (access.type === 'local') {
|
|
90
|
+
// ── 로컬 파일시스템 ───────────────────────
|
|
91
|
+
ctx.log(` Source: ${access.path} (local)`);
|
|
92
|
+
synced += await syncLocal(project, access.path, contextDir, fullMode, ctx);
|
|
93
|
+
} else if (access.type === 'github') {
|
|
94
|
+
// ── GitHub API ────────────────────────────
|
|
95
|
+
ctx.log(` Source: github.com/kgg1226/${project.name}`);
|
|
96
|
+
synced += await syncGitHub(project, contextDir, fullMode, ctx);
|
|
97
|
+
} else {
|
|
98
|
+
ctx.warn(` Cannot access ${project.name} — not found locally or on GitHub`);
|
|
99
|
+
failed++;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
ctx.log('');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── 요약 ────────────────────────────────────────
|
|
106
|
+
ctx.log('=== Summary ===');
|
|
107
|
+
ctx.log(` Synced: ${synced} file(s)`);
|
|
108
|
+
if (failed > 0) ctx.warn(` Failed: ${failed} project(s)`);
|
|
109
|
+
if (synced > 0) {
|
|
110
|
+
ctx.success(`Context cached in tasks/context/`);
|
|
111
|
+
ctx.log(' Claude Code can now read these files for cross-project reference.');
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// ── registry.md 파서 ──────────────────────────────────
|
|
117
|
+
function parseRegistry(content) {
|
|
118
|
+
const projects = [];
|
|
119
|
+
const lines = content.split('\n');
|
|
120
|
+
|
|
121
|
+
for (const line of lines) {
|
|
122
|
+
// 테이블 행 파싱: | name | path | condition |
|
|
123
|
+
const match = line.match(/^\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*([^|]*?)\s*\|$/);
|
|
124
|
+
if (!match) continue;
|
|
125
|
+
|
|
126
|
+
const [, name, path, condition] = match;
|
|
127
|
+
// 헤더 행, 구분선 건너뛰기
|
|
128
|
+
if (name === '프로젝트' || name === '---' || name.includes('---')) continue;
|
|
129
|
+
|
|
130
|
+
projects.push({
|
|
131
|
+
name: name.trim(),
|
|
132
|
+
path: path.trim(),
|
|
133
|
+
condition: condition.trim(),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return projects;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── 프로젝트 접근 가능성 확인 ─────────────────────────
|
|
141
|
+
function checkProjectAccess(project, cwd) {
|
|
142
|
+
// 1. 로컬 경로 확인
|
|
143
|
+
const localPath = resolve(cwd, project.path);
|
|
144
|
+
if (existsSync(resolve(localPath, 'INTERFACE.md')) || existsSync(resolve(localPath, 'README.md'))) {
|
|
145
|
+
return { type: 'local', path: localPath, icon: '●', label: `local (${project.path})` };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 2. GitHub 접근 가능성 (gh CLI 또는 curl)
|
|
149
|
+
try {
|
|
150
|
+
const result = execSync(
|
|
151
|
+
`curl -sf -o /dev/null -w "%{http_code}" "https://api.github.com/repos/kgg1226/${project.name}"`,
|
|
152
|
+
{ encoding: 'utf-8', timeout: 5000, stdio: 'pipe' }
|
|
153
|
+
).trim();
|
|
154
|
+
if (result === '200') {
|
|
155
|
+
return { type: 'github', icon: '○', label: 'github (remote)' };
|
|
156
|
+
}
|
|
157
|
+
} catch {
|
|
158
|
+
// GitHub 접근 불가
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { type: 'none', icon: '✗', label: 'not accessible' };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── 로컬 동기화 ──────────────────────────────────────
|
|
165
|
+
async function syncLocal(project, sourcePath, contextDir, fullMode, ctx) {
|
|
166
|
+
let count = 0;
|
|
167
|
+
|
|
168
|
+
const files = [
|
|
169
|
+
{ src: 'INTERFACE.md', label: 'INTERFACE.md (API contract)' },
|
|
170
|
+
{ src: 'README.md', label: 'README.md' },
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
if (fullMode) {
|
|
174
|
+
// --full: 주요 설정 파일도 포함
|
|
175
|
+
const extraFiles = [
|
|
176
|
+
'package.json',
|
|
177
|
+
'.sentix/config.toml',
|
|
178
|
+
'tasks/lessons.md',
|
|
179
|
+
];
|
|
180
|
+
for (const f of extraFiles) {
|
|
181
|
+
files.push({ src: f, label: f });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for (const { src, label } of files) {
|
|
186
|
+
const srcPath = resolve(sourcePath, src);
|
|
187
|
+
if (existsSync(srcPath)) {
|
|
188
|
+
const content = readFileSync(srcPath, 'utf-8');
|
|
189
|
+
await ctx.writeFile(`${contextDir}/${src}`, content);
|
|
190
|
+
ctx.success(` ${label}`);
|
|
191
|
+
count++;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return count;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── GitHub 동기화 ────────────────────────────────────
|
|
199
|
+
async function syncGitHub(project, contextDir, fullMode, ctx) {
|
|
200
|
+
let count = 0;
|
|
201
|
+
|
|
202
|
+
const files = ['INTERFACE.md', 'README.md'];
|
|
203
|
+
if (fullMode) {
|
|
204
|
+
files.push('package.json', '.sentix/config.toml', 'tasks/lessons.md');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
for (const file of files) {
|
|
208
|
+
try {
|
|
209
|
+
const url = `https://raw.githubusercontent.com/kgg1226/${project.name}/main/${file}`;
|
|
210
|
+
const content = execSync(`curl -sf "${url}"`, {
|
|
211
|
+
encoding: 'utf-8',
|
|
212
|
+
timeout: 10000,
|
|
213
|
+
stdio: 'pipe',
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
if (content) {
|
|
217
|
+
await ctx.writeFile(`${contextDir}/${file}`, content);
|
|
218
|
+
ctx.success(` ${file}`);
|
|
219
|
+
count++;
|
|
220
|
+
}
|
|
221
|
+
} catch {
|
|
222
|
+
// File not found on GitHub — skip silently
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return count;
|
|
227
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sentix doctor — 설치 상태 진단
|
|
3
|
+
*
|
|
4
|
+
* CLAUDE.md, tasks/, Claude Code, git, deprecated 파일 등을 확인.
|
|
5
|
+
* Exits with code 1 if issues are found.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
import { registerCommand } from '../registry.js';
|
|
10
|
+
import { isConfigured as isSafetyConfigured } from '../lib/safety.js';
|
|
11
|
+
import { getRuntimeMode, loadProvider } from '../lib/provider.js';
|
|
12
|
+
|
|
13
|
+
registerCommand('doctor', {
|
|
14
|
+
description: 'Diagnose Sentix installation health',
|
|
15
|
+
usage: 'sentix doctor',
|
|
16
|
+
|
|
17
|
+
async run(_args, ctx) {
|
|
18
|
+
ctx.log('=== Sentix Doctor ===\n');
|
|
19
|
+
|
|
20
|
+
let issues = 0;
|
|
21
|
+
|
|
22
|
+
// ── Required files ──────────────────────────────
|
|
23
|
+
const required = [
|
|
24
|
+
{ path: 'CLAUDE.md', label: 'CLAUDE.md (execution doc)' },
|
|
25
|
+
{ path: 'FRAMEWORK.md', label: 'FRAMEWORK.md (design doc)' },
|
|
26
|
+
{ path: '.sentix/config.toml', label: '.sentix/config.toml' },
|
|
27
|
+
{ path: '.sentix/rules/hard-rules.md', label: '.sentix/rules/hard-rules.md' },
|
|
28
|
+
{ path: 'tasks/lessons.md', label: 'tasks/lessons.md' },
|
|
29
|
+
{ path: 'tasks/patterns.md', label: 'tasks/patterns.md' },
|
|
30
|
+
{ path: 'tasks/predictions.md', label: 'tasks/predictions.md' },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
for (const { path, label } of required) {
|
|
34
|
+
if (ctx.exists(path)) {
|
|
35
|
+
ctx.success(label);
|
|
36
|
+
} else {
|
|
37
|
+
ctx.error(`${label} — MISSING`);
|
|
38
|
+
issues++;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── tasks/ structure ────────────────────────────
|
|
43
|
+
const taskDirs = ['tasks/tickets'];
|
|
44
|
+
for (const dir of taskDirs) {
|
|
45
|
+
if (ctx.exists(dir)) {
|
|
46
|
+
ctx.success(`${dir}/`);
|
|
47
|
+
} else {
|
|
48
|
+
ctx.warn(`${dir}/ — missing (will be created on first run)`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── Docs (lazy loading) ─────────────────────────
|
|
53
|
+
ctx.log('\n--- Context Docs ---\n');
|
|
54
|
+
|
|
55
|
+
const docs = [
|
|
56
|
+
{ path: 'docs/governor-sop.md', label: 'docs/governor-sop.md (SOP details)' },
|
|
57
|
+
{ path: 'docs/agent-scopes.md', label: 'docs/agent-scopes.md (agent file scopes)' },
|
|
58
|
+
{ path: 'docs/severity.md', label: 'docs/severity.md (severity logic)' },
|
|
59
|
+
{ path: 'docs/architecture.md', label: 'docs/architecture.md (Mermaid diagrams)' },
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
for (const { path, label } of docs) {
|
|
63
|
+
if (ctx.exists(path)) {
|
|
64
|
+
ctx.success(label);
|
|
65
|
+
} else {
|
|
66
|
+
ctx.warn(`${label} — missing (run: sentix update)`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Ticket index
|
|
71
|
+
if (ctx.exists('tasks/tickets/index.json')) {
|
|
72
|
+
ctx.success('tasks/tickets/index.json (ticket index)');
|
|
73
|
+
} else {
|
|
74
|
+
ctx.warn('tasks/tickets/index.json — missing (create with: sentix ticket create)');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// CHANGELOG
|
|
78
|
+
if (ctx.exists('CHANGELOG.md')) {
|
|
79
|
+
ctx.success('CHANGELOG.md');
|
|
80
|
+
} else {
|
|
81
|
+
ctx.warn('CHANGELOG.md — missing (create with: sentix version bump)');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Deprecated files ────────────────────────────
|
|
85
|
+
ctx.log('\n--- Deprecated Files ---\n');
|
|
86
|
+
|
|
87
|
+
const deprecated = [
|
|
88
|
+
'AGENTS.md',
|
|
89
|
+
'DESIGN.md',
|
|
90
|
+
'PATTERN-ENGINE.md',
|
|
91
|
+
'VISUAL-PERCEPTION.md',
|
|
92
|
+
'LEARNING-PIPELINE.md',
|
|
93
|
+
'SELF-EVOLUTION.md',
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
let hasDeprecated = false;
|
|
97
|
+
for (const file of deprecated) {
|
|
98
|
+
if (ctx.exists(file)) {
|
|
99
|
+
ctx.warn(`${file} — deprecated (content merged into FRAMEWORK.md)`);
|
|
100
|
+
hasDeprecated = true;
|
|
101
|
+
issues++;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!hasDeprecated) {
|
|
106
|
+
ctx.success('No deprecated files found');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── CLAUDE.md references check ──────────────────
|
|
110
|
+
if (ctx.exists('CLAUDE.md')) {
|
|
111
|
+
const claude = await ctx.readFile('CLAUDE.md');
|
|
112
|
+
if (claude.includes('AGENTS.md')) {
|
|
113
|
+
ctx.warn('CLAUDE.md references AGENTS.md — should be FRAMEWORK.md');
|
|
114
|
+
issues++;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Multi-project files ─────────────────────────
|
|
119
|
+
ctx.log('\n--- Multi-Project ---\n');
|
|
120
|
+
|
|
121
|
+
if (ctx.exists('INTERFACE.md')) {
|
|
122
|
+
ctx.success('INTERFACE.md (API contract)');
|
|
123
|
+
} else {
|
|
124
|
+
ctx.warn('INTERFACE.md — not found (needed for multi-project cross-reference)');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (ctx.exists('registry.md')) {
|
|
128
|
+
ctx.success('registry.md (project registry)');
|
|
129
|
+
} else {
|
|
130
|
+
ctx.warn('registry.md — not found (needed for multi-project cascade)');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── Safety word ───────────────────────────────
|
|
134
|
+
ctx.log('\n--- Security ---\n');
|
|
135
|
+
|
|
136
|
+
const hasSafety = await isSafetyConfigured(ctx);
|
|
137
|
+
if (hasSafety) {
|
|
138
|
+
ctx.success('Safety word: configured (.sentix/safety.toml)');
|
|
139
|
+
} else {
|
|
140
|
+
ctx.warn('Safety word: NOT configured — LLM injection defense disabled');
|
|
141
|
+
ctx.log(' Fix: sentix safety set <안전어>');
|
|
142
|
+
// issue 카운트하지 않음 — CI/CD 환경에서는 safety word 미설정이 정상
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── Runtime mode ──────────────────────────────
|
|
146
|
+
ctx.log('\n--- Runtime ---\n');
|
|
147
|
+
|
|
148
|
+
const runtimeMode = await getRuntimeMode(ctx);
|
|
149
|
+
ctx.success(`Mode: ${runtimeMode}`);
|
|
150
|
+
|
|
151
|
+
if (runtimeMode === 'engine') {
|
|
152
|
+
try {
|
|
153
|
+
const provider = await loadProvider(ctx);
|
|
154
|
+
ctx.success(`Provider: ${provider.name} (${provider.type})`);
|
|
155
|
+
if (provider.api.api_key) {
|
|
156
|
+
ctx.success(`API key: ${provider.api.api_key_env} (set)`);
|
|
157
|
+
} else if (provider.api.api_key_env) {
|
|
158
|
+
ctx.error(`API key: ${provider.api.api_key_env} (NOT SET — engine mode requires this)`);
|
|
159
|
+
issues++;
|
|
160
|
+
}
|
|
161
|
+
} catch (err) {
|
|
162
|
+
ctx.error(`Provider: ${err.message}`);
|
|
163
|
+
issues++;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── External tools ──────────────────────────────
|
|
168
|
+
ctx.log('\n--- External Tools ---\n');
|
|
169
|
+
|
|
170
|
+
// Git
|
|
171
|
+
const git = spawnSync('git', ['--version'], { encoding: 'utf-8', stdio: 'pipe' });
|
|
172
|
+
if (git.status === 0) {
|
|
173
|
+
ctx.success(`git: ${git.stdout.trim()}`);
|
|
174
|
+
} else {
|
|
175
|
+
ctx.error('git: not found');
|
|
176
|
+
issues++;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Node.js
|
|
180
|
+
const node = spawnSync('node', ['--version'], { encoding: 'utf-8', stdio: 'pipe' });
|
|
181
|
+
if (node.status === 0) {
|
|
182
|
+
const ver = node.stdout.trim();
|
|
183
|
+
const major = parseInt(ver.replace('v', ''));
|
|
184
|
+
if (major >= 18) {
|
|
185
|
+
ctx.success(`node: ${ver}`);
|
|
186
|
+
} else {
|
|
187
|
+
ctx.warn(`node: ${ver} (18+ recommended)`);
|
|
188
|
+
issues++;
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
ctx.error('node: not found');
|
|
192
|
+
issues++;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Claude Code
|
|
196
|
+
const claude = spawnSync('claude', ['--version'], { encoding: 'utf-8', stdio: 'pipe' });
|
|
197
|
+
if (claude.status === 0) {
|
|
198
|
+
ctx.success('claude: installed');
|
|
199
|
+
} else {
|
|
200
|
+
ctx.warn('claude: not found (needed for sentix run)');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── Summary ─────────────────────────────────────
|
|
204
|
+
ctx.log('');
|
|
205
|
+
if (issues === 0) {
|
|
206
|
+
ctx.success('All checks passed!');
|
|
207
|
+
} else {
|
|
208
|
+
ctx.warn(`${issues} issue(s) found. Run 'sentix init' to fix missing files.`);
|
|
209
|
+
process.exitCode = 1;
|
|
210
|
+
}
|
|
211
|
+
ctx.log('');
|
|
212
|
+
},
|
|
213
|
+
});
|