jutell 0.3.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/LICENSE +21 -0
- package/README.md +11 -0
- package/assets/default-config.json +26 -0
- package/assets/local-admin/assets/index-CVml-p-C.css +1 -0
- package/assets/local-admin/assets/index-Gxd8X8ii.js +60 -0
- package/assets/local-admin/index.html +14 -0
- package/assets/local-admin-server.js +1391 -0
- package/assets/mcp-server/config/bridge-config.js +72 -0
- package/assets/mcp-server/index.js +42 -0
- package/assets/mcp-server/tools/bridge-tools.js +64 -0
- package/assets/mcp-server/tools/catalog.js +25 -0
- package/assets/mcp-server/tools/usage-counters.js +97 -0
- package/assets/skill/SKILL.md +135 -0
- package/assets/skill/references/explained-diff-format.md +117 -0
- package/assets/skill/references/feature-registry.md +29 -0
- package/assets/skill/references/glossary-ko.md +302 -0
- package/assets/skill/references/report-format.md +176 -0
- package/assets/skill/references/risk-level-guide.md +85 -0
- package/assets/templates/request-builder/BUG_REPORT_REQUEST.md +100 -0
- package/assets/templates/request-builder/CODE_REVIEW_REQUEST.md +93 -0
- package/assets/templates/request-builder/DESIGN_REQUEST.md +111 -0
- package/assets/templates/request-builder/FEATURE_REQUEST.md +93 -0
- package/assets/templates/request-builder/MANUAL_EDIT_GUIDE.md +95 -0
- package/assets/templates/request-builder/NEXT_AGENT_HANDOFF.md +106 -0
- package/assets/templates/request-builder/PROJECT_PLANNING_REQUEST.md +106 -0
- package/assets/templates/request-builder/README.md +48 -0
- package/assets/version.json +6 -0
- package/dist/cli.js +82 -0
- package/dist/commands/dashboard.js +81 -0
- package/dist/commands/default.js +103 -0
- package/dist/commands/lifecycle.js +166 -0
- package/dist/commands/migrate.js +159 -0
- package/dist/commands/provider.js +135 -0
- package/dist/commands/session/add-work.js +43 -0
- package/dist/commands/session/create-page.js +53 -0
- package/dist/commands/session/finish-session.js +28 -0
- package/dist/commands/session/index.js +76 -0
- package/dist/commands/session/move-page.js +37 -0
- package/dist/commands/session/new-session.js +24 -0
- package/dist/commands/session/operator-storage.js +126 -0
- package/dist/commands/session/prompt.js +77 -0
- package/dist/commands/session/storage-command.js +74 -0
- package/dist/commands/session/storage.js +212 -0
- package/dist/commands/session/types.js +1 -0
- package/dist/commands/status.js +208 -0
- package/dist/commands/upgrade.js +113 -0
- package/dist/commands/use.js +180 -0
- package/dist/compat.js +5 -0
- package/dist/config/managed.js +257 -0
- package/dist/config/paths.js +100 -0
- package/dist/index.js +4 -0
- package/dist/installer/agents.js +42 -0
- package/dist/installer/claude.js +160 -0
- package/dist/installer/config.js +45 -0
- package/dist/installer/opencode.js +237 -0
- package/dist/installer/providers.js +15 -0
- package/dist/installer/skill.js +94 -0
- package/dist/output/format.js +187 -0
- package/dist/process/mcpProbe.js +122 -0
- package/dist/process/system.js +34 -0
- package/dist/types.js +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { stdin as input } from 'node:process';
|
|
2
|
+
import { askText } from './prompt.js';
|
|
3
|
+
import { OPERATOR_STORAGE_ERROR, readOperatorStoragePath, ensureOperatorStorageDir, writeOperatorStoragePath, hasOperatorStorageConfig, removeOperatorStorageConfig, } from './operator-storage.js';
|
|
4
|
+
function requireInteractiveConfirm() {
|
|
5
|
+
if (!input.isTTY) {
|
|
6
|
+
throw new Error('비대화형 실행에서는 `--yes`를 함께 지정해야 합니다. 확인 단계를 건너뜁니다.');
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export async function storageCommand(paths, options, io, args) {
|
|
10
|
+
const sub = args[0] ?? '';
|
|
11
|
+
if (sub === 'set')
|
|
12
|
+
return storageSetCommand(paths, options, io, args[1]);
|
|
13
|
+
if (sub === 'reset')
|
|
14
|
+
return storageResetCommand(paths, options, io);
|
|
15
|
+
return storageStatusCommand(paths, io);
|
|
16
|
+
}
|
|
17
|
+
async function storageStatusCommand(paths, io) {
|
|
18
|
+
try {
|
|
19
|
+
const custom = await readOperatorStoragePath(paths.targetRoot);
|
|
20
|
+
if (custom === undefined) {
|
|
21
|
+
io.write('Session 저장 위치: 기본 저장 (사용 가능)');
|
|
22
|
+
io.write('별도 설정이 없으면 `.jutell-local/`의 기본 저장 위치를 사용합니다.');
|
|
23
|
+
io.write('바꾸려면 `jutell session storage set`을 실행하세요.');
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
io.write('Session 저장 위치: 운영자 지정 저장 (사용 가능)');
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
io.write('Session 저장 위치: 운영자 지정 저장 (사용 불가능)');
|
|
31
|
+
io.write(OPERATOR_STORAGE_ERROR);
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function storageSetCommand(paths, options, io, explicitPath) {
|
|
36
|
+
let candidate = explicitPath?.trim() ?? '';
|
|
37
|
+
if (!candidate) {
|
|
38
|
+
candidate = (await askText('Session 저장 위치 (절대 경로):'))?.trim() ?? '';
|
|
39
|
+
}
|
|
40
|
+
if (!candidate) {
|
|
41
|
+
throw new Error('저장 위치를 입력해야 합니다. 예: `jutell session storage set <절대 경로>`');
|
|
42
|
+
}
|
|
43
|
+
await ensureOperatorStorageDir(candidate);
|
|
44
|
+
if (!options.yes) {
|
|
45
|
+
requireInteractiveConfirm();
|
|
46
|
+
const ok = await io.ask('지정한 위치에 Session을 저장하도록 설정합니다. 계속하시겠습니까?', false);
|
|
47
|
+
if (!ok) {
|
|
48
|
+
io.write('설정을 변경하지 않았습니다.');
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
await writeOperatorStoragePath(paths.targetRoot, candidate);
|
|
53
|
+
io.write('운영자 Session 저장 위치를 설정했습니다.');
|
|
54
|
+
io.write('이제 모든 Session 기록은 지정한 로컬 위치에만 저장됩니다.');
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
async function storageResetCommand(paths, options, io) {
|
|
58
|
+
if (!(await hasOperatorStorageConfig(paths.targetRoot))) {
|
|
59
|
+
io.write('설정된 운영자 Session 저장 위치가 없습니다.');
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
if (!options.yes) {
|
|
63
|
+
requireInteractiveConfirm();
|
|
64
|
+
const ok = await io.ask('운영자 Session 저장 위치 설정만 제거합니다. Session 기록 파일은 지우지 않습니다. 계속하시겠습니까?', false);
|
|
65
|
+
if (!ok) {
|
|
66
|
+
io.write('설정을 제거하지 않았습니다.');
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
await removeOperatorStorageConfig(paths.targetRoot);
|
|
71
|
+
io.write('운영자 Session 저장 위치 설정을 제거했습니다.');
|
|
72
|
+
io.write('다시 기본 로컬 저장 위치를 사용합니다.');
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export const SESSION_FOLDER_BASE = 'collaboration-sessions';
|
|
4
|
+
export const SESSION_META_FILE = 'session.json';
|
|
5
|
+
export const SESSION_SUMMARY_FILE = 'SESSION_SUMMARY.md';
|
|
6
|
+
export function sessionRoot(dataRoot) {
|
|
7
|
+
return path.join(dataRoot, SESSION_FOLDER_BASE);
|
|
8
|
+
}
|
|
9
|
+
export function pad2(n) {
|
|
10
|
+
return String(n).padStart(2, '0');
|
|
11
|
+
}
|
|
12
|
+
export function todayStamp(now = new Date()) {
|
|
13
|
+
const p = pad2;
|
|
14
|
+
return `${now.getFullYear()}-${p(now.getMonth() + 1)}-${p(now.getDate())}`;
|
|
15
|
+
}
|
|
16
|
+
// 레거시 단일 파일 기록: .jutell-local/collaboration-sessions/YYYY-MM-DD-session-NN.md
|
|
17
|
+
// 이동·변환·삭제하지 않으며 참고 안내에만 사용한다.
|
|
18
|
+
export function listLegacyFlatFiles(sessionRootDir, stamp) {
|
|
19
|
+
return fs.readdir(sessionRootDir).then((entries) => entries
|
|
20
|
+
.filter((entry) => new RegExp(`^${stamp}-session-\\d+\\.md$`).test(entry))
|
|
21
|
+
.sort()).catch(() => []);
|
|
22
|
+
}
|
|
23
|
+
export function isValidPageFile(file) {
|
|
24
|
+
return /^page-\d+-.+\.md$/.test(file);
|
|
25
|
+
}
|
|
26
|
+
// date 폴더 안 Page 파일 목록을 page-번호 순서로 정렬한 {number, file}
|
|
27
|
+
export async function listPageFiles(dir) {
|
|
28
|
+
let entries = [];
|
|
29
|
+
try {
|
|
30
|
+
entries = await fs.readdir(dir);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
const pages = entries
|
|
36
|
+
.filter(isValidPageFile)
|
|
37
|
+
.map((file) => {
|
|
38
|
+
const match = /^page-(\d+)-(.+)\.md$/.exec(file);
|
|
39
|
+
return match ? { number: Number(match[1]), file, slug: match[2] } : null;
|
|
40
|
+
})
|
|
41
|
+
.filter((entry) => entry !== null);
|
|
42
|
+
pages.sort((a, b) => a.number - b.number);
|
|
43
|
+
return pages;
|
|
44
|
+
}
|
|
45
|
+
// 오늘 Session이 상태로 있는지 (session.json 존재)
|
|
46
|
+
export async function sessionExists(dir) {
|
|
47
|
+
try {
|
|
48
|
+
await fs.access(path.join(dir, SESSION_META_FILE));
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export async function readSessionMeta(dir) {
|
|
56
|
+
const file = path.join(dir, SESSION_META_FILE);
|
|
57
|
+
let raw;
|
|
58
|
+
try {
|
|
59
|
+
raw = await fs.readFile(file, 'utf8');
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(raw);
|
|
66
|
+
if (typeof parsed.date !== 'string')
|
|
67
|
+
throw new Error('date 누락');
|
|
68
|
+
const pages = Array.isArray(parsed.pages) ? parsed.pages : [];
|
|
69
|
+
return {
|
|
70
|
+
date: parsed.date,
|
|
71
|
+
status: parsed.status === 'finished' ? 'finished' : 'active',
|
|
72
|
+
currentPage: typeof parsed.currentPage === 'number' ? parsed.currentPage : null,
|
|
73
|
+
pages: pages.filter((page) => typeof page === 'object' &&
|
|
74
|
+
typeof page.number === 'number' &&
|
|
75
|
+
typeof page.agent === 'string' &&
|
|
76
|
+
typeof page.role === 'string' &&
|
|
77
|
+
typeof page.title === 'string' &&
|
|
78
|
+
typeof page.file === 'string'),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
throw new Error(`session.json을 읽지 못했습니다(손상). 파일을 지우지 않고 중단합니다: ${file}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// 원자적 저장: 임시 파일에 쓴 뒤 이름을 바꾼다. 손상된 기존 파일은 덮어쓰지 않는다.
|
|
86
|
+
export async function saveSessionMeta(dir, meta) {
|
|
87
|
+
await fs.mkdir(dir, { recursive: true });
|
|
88
|
+
const file = path.join(dir, SESSION_META_FILE);
|
|
89
|
+
const existing = await readSessionMeta(dir);
|
|
90
|
+
if (existing && existing.date !== meta.date) {
|
|
91
|
+
throw new Error(`session.json 날짜가 다릅니다. 병합하지 않습니다: ${file}`);
|
|
92
|
+
}
|
|
93
|
+
const tmp = path.join(dir, `${SESSION_META_FILE}.tmp`);
|
|
94
|
+
await fs.writeFile(tmp, `${JSON.stringify(meta, null, 2)}\n`, 'utf8');
|
|
95
|
+
await fs.rename(tmp, file);
|
|
96
|
+
}
|
|
97
|
+
export function createInitialMeta(date) {
|
|
98
|
+
return { date, status: 'active', currentPage: null, pages: [] };
|
|
99
|
+
}
|
|
100
|
+
export function slugify(value) {
|
|
101
|
+
return value
|
|
102
|
+
.toLowerCase()
|
|
103
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
104
|
+
.replace(/^-+|-+$/g, '');
|
|
105
|
+
}
|
|
106
|
+
export function pageFileName(page) {
|
|
107
|
+
const agent = slugify(page.agent) || 'agent';
|
|
108
|
+
const title = slugify(page.title);
|
|
109
|
+
return `page-${pad2(page.number)}-${agent}${title ? `-${title}` : ''}.md`;
|
|
110
|
+
}
|
|
111
|
+
export function pageLabel(page) {
|
|
112
|
+
return `Page ${pad2(page.number)} — ${page.agent} / ${page.title}`;
|
|
113
|
+
}
|
|
114
|
+
export function parseFileNameTitle(file) {
|
|
115
|
+
// page-01-opencode-jutell-report-style.md → 부분 타이틀(slug)만 복원
|
|
116
|
+
const match = /^page-\d+-(.+?)\.md$/.exec(file);
|
|
117
|
+
return match ? match[1] : file;
|
|
118
|
+
}
|
|
119
|
+
// Page 파일 안의 작업 번호 목록 (중복 없이 최대값 + 1 반환)
|
|
120
|
+
export function nextWorkNumber(content) {
|
|
121
|
+
const numbers = Array.from(content.matchAll(/^## 작업 (\d+)\s*$/gm), (match) => Number(match[1]));
|
|
122
|
+
return (numbers.length > 0 ? Math.max(...numbers) : 0) + 1;
|
|
123
|
+
}
|
|
124
|
+
// Page 01을 처음 만들었을 때 같이 넣는 작업 01 블록
|
|
125
|
+
export function workTemplate(workNumber) {
|
|
126
|
+
const p = pad2(workNumber);
|
|
127
|
+
return [
|
|
128
|
+
`## 작업 ${p}`,
|
|
129
|
+
'',
|
|
130
|
+
'### ChatGPT',
|
|
131
|
+
'',
|
|
132
|
+
'-',
|
|
133
|
+
'',
|
|
134
|
+
'### Agent 답변',
|
|
135
|
+
'',
|
|
136
|
+
'-',
|
|
137
|
+
'',
|
|
138
|
+
'### 내 피드백',
|
|
139
|
+
'',
|
|
140
|
+
'좋았던 점',
|
|
141
|
+
'',
|
|
142
|
+
'-',
|
|
143
|
+
'',
|
|
144
|
+
'불편했던 점',
|
|
145
|
+
'',
|
|
146
|
+
'-',
|
|
147
|
+
'',
|
|
148
|
+
'나라면 이렇게 말할 것 같다',
|
|
149
|
+
'',
|
|
150
|
+
'-',
|
|
151
|
+
'',
|
|
152
|
+
'JuTell 개선 아이디어',
|
|
153
|
+
'',
|
|
154
|
+
'-',
|
|
155
|
+
'',
|
|
156
|
+
'오늘의 발견',
|
|
157
|
+
'',
|
|
158
|
+
'-',
|
|
159
|
+
].join('\n');
|
|
160
|
+
}
|
|
161
|
+
export function pageTemplate(page, date) {
|
|
162
|
+
return [
|
|
163
|
+
`# ${pageLabel(page)}`,
|
|
164
|
+
'',
|
|
165
|
+
`- Date: ${date}`,
|
|
166
|
+
`- Agent: ${page.agent}`,
|
|
167
|
+
`- 역할: ${page.role}`,
|
|
168
|
+
'- 상태: 진행 중',
|
|
169
|
+
'',
|
|
170
|
+
'---',
|
|
171
|
+
'',
|
|
172
|
+
workTemplate(1),
|
|
173
|
+
'',
|
|
174
|
+
].join('\n');
|
|
175
|
+
}
|
|
176
|
+
export function summaryTemplate(date, pages) {
|
|
177
|
+
const pageLines = pages.length > 0 ? pages.map((page) => `- ${pageLabel(page)}`) : ['- (Page 없음)'];
|
|
178
|
+
return [
|
|
179
|
+
'# Session Summary',
|
|
180
|
+
'',
|
|
181
|
+
`Date: ${date}`,
|
|
182
|
+
'',
|
|
183
|
+
'## 오늘 Page',
|
|
184
|
+
'',
|
|
185
|
+
...pageLines,
|
|
186
|
+
'',
|
|
187
|
+
'## 오늘 가장 좋았던 점',
|
|
188
|
+
'',
|
|
189
|
+
'-',
|
|
190
|
+
'',
|
|
191
|
+
'## 오늘 가장 불편했던 점',
|
|
192
|
+
'',
|
|
193
|
+
'-',
|
|
194
|
+
'',
|
|
195
|
+
'## 오늘의 핵심 발견',
|
|
196
|
+
'',
|
|
197
|
+
'-',
|
|
198
|
+
'',
|
|
199
|
+
'## Agent별 차이',
|
|
200
|
+
'',
|
|
201
|
+
'-',
|
|
202
|
+
'',
|
|
203
|
+
'## JuTell에 반영할 후보',
|
|
204
|
+
'',
|
|
205
|
+
'-',
|
|
206
|
+
'',
|
|
207
|
+
'## 다음 Session에서 가장 먼저 할 일',
|
|
208
|
+
'',
|
|
209
|
+
'-',
|
|
210
|
+
'',
|
|
211
|
+
].join('\n');
|
|
212
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import net from 'node:net';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { assets, codexScopedPaths, packageRoot, safeLocation } from '../config/paths.js';
|
|
5
|
+
import { claudeDetected, codexDetected, nodeMajorVersion, operatingSystem } from '../process/system.js';
|
|
6
|
+
import { exists, parseSkillVersion, readBridgeConfig, readCodexRegistration, readText, readVersionInfo, FEATURE_IDS } from '../config/managed.js';
|
|
7
|
+
import { setupCommand } from './lifecycle.js';
|
|
8
|
+
import { hasJuTellAgentsBlock } from '../installer/agents.js';
|
|
9
|
+
import { opencodeDetected, readOpenCodeRegistration } from '../installer/opencode.js';
|
|
10
|
+
import { readClaudeRegistration } from '../installer/claude.js';
|
|
11
|
+
import { probeMcpServer } from '../process/mcpProbe.js';
|
|
12
|
+
async function processAlive(pid) {
|
|
13
|
+
try {
|
|
14
|
+
process.kill(pid, 0);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function adminState(paths) {
|
|
22
|
+
const marker = path.join(paths.dataRoot, 'dashboard.json');
|
|
23
|
+
const raw = await readText(marker);
|
|
24
|
+
if (!raw)
|
|
25
|
+
return '중지됨';
|
|
26
|
+
try {
|
|
27
|
+
const value = JSON.parse(raw);
|
|
28
|
+
if (typeof value.pid === 'number' && await processAlive(value.pid))
|
|
29
|
+
return '실행 중';
|
|
30
|
+
await fs.rm(marker, { force: true });
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return '확인 필요';
|
|
34
|
+
}
|
|
35
|
+
return '중지됨';
|
|
36
|
+
}
|
|
37
|
+
export async function getStatus(paths) {
|
|
38
|
+
const config = await readBridgeConfig(paths);
|
|
39
|
+
// Codex only reads MCP servers from its global config, so status always
|
|
40
|
+
// checks that real file regardless of the invocation's scope (see
|
|
41
|
+
// codexScopedPaths) — otherwise a project-scope status could report
|
|
42
|
+
// "등록됨" for a file Codex never actually consumes.
|
|
43
|
+
const registration = await readCodexRegistration(codexScopedPaths(paths), packageRoot(), config.config.mcp?.enabled === true);
|
|
44
|
+
const versions = await readVersionInfo();
|
|
45
|
+
const skillInstalled = await exists(path.join(paths.skillRoot, 'SKILL.md'));
|
|
46
|
+
const installedSkillText = await readText(path.join(paths.skillRoot, 'SKILL.md'));
|
|
47
|
+
const agentsManaged = await hasJuTellAgentsBlock(paths.targetRoot);
|
|
48
|
+
const opencode = await readOpenCodeRegistration(paths, packageRoot(), false);
|
|
49
|
+
const claude = await readClaudeRegistration(paths, packageRoot(), false);
|
|
50
|
+
const codexPreparation = registration.conflict ? 'error' : !registration.registered ? 'not_registered' : registration.enabled ? 'enabled' : 'registered';
|
|
51
|
+
const opencodePreparation = opencode.conflict ? 'conflict' : !opencode.registered ? 'not_registered' : opencode.enabled ? 'enabled' : 'registered';
|
|
52
|
+
const claudePreparation = claude.conflict ? 'conflict' : !claude.registered ? 'not_registered' : claude.enabled ? 'enabled' : 'registered';
|
|
53
|
+
const anyProviderRegistered = registration.registered || opencode.registered || claude.registered;
|
|
54
|
+
const anyProviderEnabled = registration.enabled || opencode.enabled || claude.enabled;
|
|
55
|
+
const warnings = [];
|
|
56
|
+
if (!config.valid)
|
|
57
|
+
warnings.push('설정 파일을 읽지 못해 balanced 기본값을 사용 중입니다.');
|
|
58
|
+
if (registration.conflict)
|
|
59
|
+
warnings.push('같은 이름의 관리되지 않는 Codex MCP 설정이 있어 자동 변경하지 않았습니다.');
|
|
60
|
+
if (opencode.conflict)
|
|
61
|
+
warnings.push('OpenCode 설정에 같은 이름의 관리되지 않는 MCP 항목이 있어 자동 변경하지 않았습니다.');
|
|
62
|
+
if (registration.bothRegistered)
|
|
63
|
+
warnings.push('Codex에 canonical jutell과 legacy beginner_bridge MCP가 모두 있습니다. 자동 정리하지 않았습니다. 이전 항목 제거는 추후 안전한 마이그레이션에서 안내합니다.');
|
|
64
|
+
if (opencode.bothRegistered)
|
|
65
|
+
warnings.push('OpenCode에 canonical jutell과 legacy beginner_bridge MCP가 모두 있습니다. 자동 정리하지 않았습니다. 이전 항목 제거는 추후 안전한 마이그레이션에서 안내합니다.');
|
|
66
|
+
if (registration.legacyRegistered && !registration.canonicalRegistered)
|
|
67
|
+
warnings.push('Codex에 이전 beginner_bridge 항목만 있습니다. jutell use codex 를 실행하면 보존하면서 새 jutell 항목을 추가합니다.');
|
|
68
|
+
if (opencode.legacyRegistered && !opencode.canonicalRegistered)
|
|
69
|
+
warnings.push('OpenCode에 이전 beginner_bridge 항목만 있습니다. jutell use opencode 를 실행하면 보존하면서 새 jutell 항목을 추가합니다.');
|
|
70
|
+
if (config.config.mcp?.enabled && !anyProviderRegistered)
|
|
71
|
+
warnings.push('MCP 연결 정책은 켜져 있지만 Codex·OpenCode·Claude Code 어느 Provider에도 JuTell MCP가 등록되지 않았습니다. jutell use <agent> 를 실행해 주세요.');
|
|
72
|
+
if (config.config.mcp?.enabled && anyProviderRegistered && !anyProviderEnabled)
|
|
73
|
+
warnings.push('연결 정책(.jutell.json)은 켜져 있지만 새 세션에서 자동 시작할 활성 Provider 항목이 없습니다. jutell use <agent> 를 실행해 주세요.');
|
|
74
|
+
if (!config.config.mcp?.enabled && anyProviderEnabled)
|
|
75
|
+
warnings.push('연결 정책(.jutell.json)은 꺼져 있지만 Provider 자동 시작은 켜져 있습니다. 일치시키려면 jutell use <agent> 또는 jutell off을 실행해 주세요.');
|
|
76
|
+
return {
|
|
77
|
+
cliVersion: versions.cli,
|
|
78
|
+
skillVersion: skillInstalled ? (parseSkillVersion(installedSkillText) ?? versions.skill) : '설치되지 않음',
|
|
79
|
+
mcpVersion: versions.mcp,
|
|
80
|
+
adminVersion: versions.admin,
|
|
81
|
+
installationScope: paths.scope,
|
|
82
|
+
configExists: config.exists,
|
|
83
|
+
configValid: config.valid,
|
|
84
|
+
codexDetected: codexDetected(),
|
|
85
|
+
opencodeDetected: opencodeDetected(),
|
|
86
|
+
claudeDetected: claudeDetected(),
|
|
87
|
+
skillInstalled,
|
|
88
|
+
agentsManaged,
|
|
89
|
+
mcpRegistered: anyProviderRegistered,
|
|
90
|
+
mcpEnabled: config.config.mcp?.enabled === true,
|
|
91
|
+
codexPreparation,
|
|
92
|
+
opencodePreparation,
|
|
93
|
+
claudePreparation,
|
|
94
|
+
anyProviderRegistered,
|
|
95
|
+
anyProviderEnabled,
|
|
96
|
+
actualConnection: 'not_checked',
|
|
97
|
+
opencode: { registered: opencode.registered, conflict: opencode.conflict, enabled: opencode.enabled },
|
|
98
|
+
claude: { registered: claude.registered, conflict: claude.conflict, enabled: claude.enabled },
|
|
99
|
+
profile: config.config.profile,
|
|
100
|
+
activeFeatureCount: Object.values(config.config.features).filter(Boolean).length,
|
|
101
|
+
configLocation: config.source === 'legacy' ? '기존 설정(.beginner-bridge.json)' : safeLocation(paths.scope, 'config'),
|
|
102
|
+
localAdmin: await adminState(paths),
|
|
103
|
+
usageCountersEnabled: config.config.usageMeasurement?.localCountersEnabled === true,
|
|
104
|
+
telemetry: '비활성화',
|
|
105
|
+
externalTransmission: '없음',
|
|
106
|
+
warnings,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export async function statusCommand(paths, options, io) {
|
|
110
|
+
const status = await getStatus(paths);
|
|
111
|
+
if (options.json) {
|
|
112
|
+
io.write(JSON.stringify(status, null, 2));
|
|
113
|
+
return status;
|
|
114
|
+
}
|
|
115
|
+
const codexLabel = { not_registered: '미등록', registered: '등록됨', enabled: '활성화됨', error: '오류' }[status.codexPreparation];
|
|
116
|
+
const opencodeLabel = { not_registered: '미등록', registered: '등록됨', enabled: '활성화됨', conflict: '충돌' }[status.opencodePreparation];
|
|
117
|
+
const claudeLabel = { not_registered: '미등록', registered: '등록됨', enabled: '활성화됨', conflict: '충돌' }[status.claudePreparation];
|
|
118
|
+
const actual = { not_checked: '확인하지 않음', success: '마지막 확인 성공', failure: '마지막 확인 실패' }[status.actualConnection];
|
|
119
|
+
const codexDetectedLabel = status.codexDetected ? '감지됨' : '미감지';
|
|
120
|
+
const opencodeDetectedLabel = status.opencodeDetected ? '감지됨' : status.opencode.registered ? '설정 있음(명령 미감지)' : '미감지';
|
|
121
|
+
const claudeDetectedLabel = status.claudeDetected ? '감지됨' : status.claude.registered ? '설정 있음(명령 미감지)' : '미감지';
|
|
122
|
+
const claudeScopeNote = status.installationScope === 'global' ? '사용자 전역(user)' : '현재 프로젝트(local)';
|
|
123
|
+
io.write(`JuTell 상태\n\nCLI: ${status.cliVersion}\nSkill: ${status.skillInstalled ? '설치됨' : '설치되지 않음'}\nAGENTS.md: ${status.agentsManaged ? 'JuTell 블록 있음' : 'JuTell 블록 없음'}\nJuTell 연결 정책: ${status.mcpEnabled ? '켜짐' : '꺼짐'}\nCodex MCP (전역, Codex는 프로젝트 설정을 읽지 않음): ${codexLabel}${status.codexDetected ? ` (명령 ${codexDetectedLabel})` : ''}\nOpenCode MCP: ${opencodeLabel}${status.opencode.enabled ? ' (새 세션 자동 시작 켜짐)' : ''}${status.opencodeDetected ? '' : ` (명령 ${opencodeDetectedLabel})`}\nClaude Code MCP (${claudeScopeNote}): ${claudeLabel}${status.claudeDetected ? '' : ` (명령 ${claudeDetectedLabel})`}\nMCP 서버 응답: ${actual}\n현재 Agent 세션 적용: 직접 확인 필요\nProfile: ${status.profile}\n활성 Feature: ${status.activeFeatureCount}개\n설치 범위 (설정·Skill·AGENTS.md): ${status.installationScope === 'global' ? '사용자 전역' : '현재 프로젝트'}\n로컬 관리자: ${status.localAdmin}\n로컬 사용량 카운터: ${status.usageCountersEnabled ? '켜짐' : '꺼짐'}\nTelemetry: ${status.telemetry}\n외부 전송: ${status.externalTransmission}\n설정 위치: ${status.configLocation}`);
|
|
124
|
+
for (const warning of status.warnings)
|
|
125
|
+
io.write(`주의: ${warning}`);
|
|
126
|
+
return status;
|
|
127
|
+
}
|
|
128
|
+
async function portAvailable() {
|
|
129
|
+
return new Promise((resolve) => {
|
|
130
|
+
const server = net.createServer();
|
|
131
|
+
server.once('error', () => resolve(false));
|
|
132
|
+
server.listen(0, '127.0.0.1', () => server.close(() => resolve(true)));
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
async function writeCheck(paths) {
|
|
136
|
+
const directory = paths.dataRoot;
|
|
137
|
+
const file = path.join(directory, `.doctor-${process.pid}.tmp`);
|
|
138
|
+
try {
|
|
139
|
+
await fs.mkdir(directory, { recursive: true });
|
|
140
|
+
await fs.writeFile(file, 'ok', 'utf8');
|
|
141
|
+
await fs.rm(file, { force: true });
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
await fs.rm(file, { force: true });
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
export async function getDoctorResults(paths) {
|
|
150
|
+
const config = await readBridgeConfig(paths);
|
|
151
|
+
const codexPaths = codexScopedPaths(paths);
|
|
152
|
+
const registration = await readCodexRegistration(codexPaths, packageRoot(), config.config.mcp?.enabled === true);
|
|
153
|
+
const opencode = await readOpenCodeRegistration(paths, packageRoot(), false);
|
|
154
|
+
const claude = await readClaudeRegistration(paths, packageRoot(), false);
|
|
155
|
+
const skillFile = path.join(paths.skillRoot, 'SKILL.md');
|
|
156
|
+
const skillText = await readText(skillFile);
|
|
157
|
+
const mcpEntry = path.join(assets().mcpServer, 'index.js');
|
|
158
|
+
const adminEntry = path.join(assets().localAdmin, 'index.html');
|
|
159
|
+
const mcpText = await readText(mcpEntry) ?? '';
|
|
160
|
+
const checks = [];
|
|
161
|
+
checks.push({ name: 'Node 버전', status: nodeMajorVersion() >= 18 ? '정상' : '오류', detail: `현재 Node ${process.versions.node}` });
|
|
162
|
+
checks.push({ name: '운영체제', status: ['Windows', 'macOS', 'Linux'].includes(operatingSystem()) ? '정상' : '직접 확인 필요', detail: operatingSystem() });
|
|
163
|
+
const anyProviderDetectedOrConfigured = codexDetected() || opencodeDetected() || claudeDetected() || await exists(codexPaths.codexConfigFile) || await exists(paths.opencodeConfigFile) || claude.exists;
|
|
164
|
+
checks.push({ name: 'AI Agent Provider 설치 또는 설정', status: anyProviderDetectedOrConfigured ? '정상' : '직접 확인 필요', detail: anyProviderDetectedOrConfigured ? 'Provider 명령 또는 설정을 확인했습니다.' : 'Provider 명령과 설정을 모두 자동 확인하지 못했습니다.' });
|
|
165
|
+
checks.push({ name: 'OpenCode 명령 감지', status: opencodeDetected() ? '정상' : '직접 확인 필요', detail: opencodeDetected() ? 'opencode --version 명령을 확인했습니다.' : 'opencode 명령을 자동 확인하지 못했습니다. PATH·shell 실행 권한을 확인해 주세요.' });
|
|
166
|
+
checks.push({ name: 'Skill 파일', status: skillText?.includes('name: beginner-bridge') ? '정상' : '오류', detail: skillText ? 'Skill 파일을 확인했습니다.' : 'Skill 파일이 없습니다.' });
|
|
167
|
+
const skillVersion = parseSkillVersion(skillText);
|
|
168
|
+
const sourceSkillText = await readText(path.join(assets().skill, 'SKILL.md'));
|
|
169
|
+
const sourceVersion = parseSkillVersion(sourceSkillText);
|
|
170
|
+
const skillMatch = Boolean(skillText && sourceSkillText && skillText === sourceSkillText);
|
|
171
|
+
checks.push({ name: 'Skill 버전', status: skillMatch ? '정상' : skillVersion ? '주의' : '직접 확인 필요', detail: skillMatch ? `스킬 버전 ${sourceVersion ?? '(버전 기록 없음)'} (설치 사본 일치).` : skillVersion ? `설치 사본 버전 ${skillVersion}이 원본과 다릅니다. jutell use <agent> 로 재설치하세요.` : '설치된 Skill에 버전 기록이 없습니다.' });
|
|
172
|
+
checks.push({ name: '현재 Agent 세션 적용', status: '직접 확인 필요', detail: '실제로 Skill과 규칙을 읽었는지 여부는 해당 Agent 세션에서 직접 확인해야 합니다.' });
|
|
173
|
+
checks.push({ name: 'MCP 빌드 파일', status: await exists(mcpEntry) ? '정상' : '오류', detail: await exists(mcpEntry) ? '패키지에 포함되어 있습니다.' : 'MCP 빌드 파일이 없습니다.' });
|
|
174
|
+
checks.push({ name: 'Codex MCP', status: registration.conflict ? '오류' : registration.registered ? '정상' : '주의', detail: registration.conflict ? '관리되지 않는 동일 이름 설정이 있습니다.' : registration.registered ? `JuTell 관리 블록을 확인했습니다. 새 세션 자동 시작: ${registration.enabled ? '켜짐' : '꺼짐'}.` : '등록되지 않았습니다.' });
|
|
175
|
+
checks.push({ name: 'OpenCode MCP', status: opencode.conflict ? '오류' : opencode.registered ? (opencode.enabled ? '정상' : '주의') : '주의', detail: opencode.conflict ? '관리되지 않는 동일 이름 항목이 있습니다.' : opencode.registered ? `JuTell 관리 블록을 확인했습니다. 새 세션 자동 시작: ${opencode.enabled ? '켜짐' : '꺼짐'}.` : 'OpenCode MCP가 등록되지 않았습니다.' });
|
|
176
|
+
checks.push({ name: 'Claude Code MCP', status: claude.registered ? '정상' : '주의', detail: claude.registered ? `${claude.claudeScope} 범위(${claude.claudeScope === 'user' ? '사용자 전역' : '현재 프로젝트'})에 등록되어 있습니다.` : 'Claude Code MCP가 등록되지 않았습니다.' });
|
|
177
|
+
checks.push({ name: config.source === 'legacy' ? '.beginner-bridge.json' : '.jutell.json', status: config.valid ? '정상' : '오류', detail: config.exists ? (config.valid ? (config.source === 'legacy' ? '이전 설정 파일을 읽었습니다. 새 .jutell.json이 없으면 사용합니다.' : '설정 형식을 확인했습니다.') : '설정이 올바르지 않아 기본값을 사용합니다.') : '없으면 기본 설정을 사용합니다.' });
|
|
178
|
+
const featuresValid = Object.keys(config.config.features).every((id) => FEATURE_IDS.includes(id));
|
|
179
|
+
const limitsValid = config.config.limits.maxMainFiles >= 1 && config.config.limits.maxMainFiles <= 10 && config.config.limits.maxGlossaryTerms >= 0 && config.config.limits.maxGlossaryTerms <= 10 && config.config.limits.compactReportMaxSentences >= 4 && config.config.limits.compactReportMaxSentences <= 30;
|
|
180
|
+
checks.push({ name: '공식 Feature ID', status: featuresValid ? '정상' : '오류', detail: featuresValid ? '현재 공식 ID만 확인했습니다.' : '지원하지 않는 Feature ID가 있습니다.' });
|
|
181
|
+
checks.push({ name: 'limits', status: limitsValid ? '정상' : '오류', detail: limitsValid ? '허용 범위를 확인했습니다.' : '허용 범위를 벗어난 값이 있습니다.' });
|
|
182
|
+
checks.push({ name: '로컬 관리자 빌드', status: await exists(adminEntry) ? '정상' : '오류', detail: await exists(adminEntry) ? '관리자 화면 파일을 확인했습니다.' : '관리자 화면 파일이 없습니다.' });
|
|
183
|
+
checks.push({ name: '포트 사용 가능 여부', status: await portAvailable() ? '정상' : '주의', detail: '127.0.0.1의 임시 포트를 확인했습니다.' });
|
|
184
|
+
checks.push({ name: '쓰기 권한', status: await writeCheck(paths) ? '정상' : '오류', detail: '로컬 상태 폴더에 임시 파일을 만들고 삭제했습니다.' });
|
|
185
|
+
const backupExists = await exists(`${codexPaths.codexConfigFile}.previous`);
|
|
186
|
+
checks.push({ name: '설정 백업 상태', status: !await exists(codexPaths.codexConfigFile) || backupExists ? '정상' : '주의', detail: !await exists(codexPaths.codexConfigFile) ? '아직 Provider 설정을 변경하지 않았습니다.' : backupExists ? '이전 설정 백업을 확인했습니다.' : '기존 Provider 설정 백업이 없습니다.' });
|
|
187
|
+
const externalCode = /(?:https?:\/\/(?!127\.0\.0\.1)|https?\.request|net\.connect)/i.test(mcpText);
|
|
188
|
+
checks.push({ name: '외부 전송 코드', status: externalCode ? '오류' : '정상', detail: externalCode ? 'MCP 빌드에서 외부 네트워크 관련 코드를 찾았습니다.' : 'MCP 빌드에 외부 전송 패턴이 없습니다.' });
|
|
189
|
+
const probe = await probeMcpServer(mcpEntry);
|
|
190
|
+
checks.push({ name: 'MCP 서버 실제 연결 (Stdio)', status: probe.ok ? '정상' : '오류', detail: probe.ok ? `${probe.serverName || 'JuTell'} 서버가 응답하고 ${probe.toolCount}개 도구를 제공합니다.` : `서버 응답 실패: ${probe.error ?? '알 수 없는 오류'}` });
|
|
191
|
+
return checks;
|
|
192
|
+
}
|
|
193
|
+
export async function doctorCommand(paths, options, io) {
|
|
194
|
+
let checks = await getDoctorResults(paths);
|
|
195
|
+
const fixableError = checks.some((check) => check.status === '오류' && check.name !== 'MCP 서버 실제 연결 (Stdio)');
|
|
196
|
+
if (options.fix && fixableError) {
|
|
197
|
+
await setupCommand(paths, { ...options, yes: true }, io);
|
|
198
|
+
checks = await getDoctorResults(paths);
|
|
199
|
+
}
|
|
200
|
+
if (options.json) {
|
|
201
|
+
io.write(JSON.stringify(checks, null, 2));
|
|
202
|
+
return checks;
|
|
203
|
+
}
|
|
204
|
+
io.write('JuTell 점검 결과');
|
|
205
|
+
for (const check of checks)
|
|
206
|
+
io.write(`${check.status} ${check.name}: ${check.detail}`);
|
|
207
|
+
return checks;
|
|
208
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { assets, codexScopedPaths, packageRoot } from '../config/paths.js';
|
|
2
|
+
import { readBridgeConfig, snapshot, restore } from '../config/managed.js';
|
|
3
|
+
import { ensureBridgeConfig } from '../installer/config.js';
|
|
4
|
+
import { installSkill, recordSkillFiles, removeAddedSkillFiles } from '../installer/skill.js';
|
|
5
|
+
import { agentsFile, ensureJuTellAgentsBlock } from '../installer/agents.js';
|
|
6
|
+
import { readCodexRegistration, registerMcp } from '../config/managed.js';
|
|
7
|
+
import { readOpenCodeRegistration, registerOpenCodeMcp } from '../installer/opencode.js';
|
|
8
|
+
import { readClaudeRegistration, registerClaudeMcp } from '../installer/claude.js';
|
|
9
|
+
export async function upgradeCommand(paths, options, io) {
|
|
10
|
+
const snapshots = [
|
|
11
|
+
await snapshot(paths.configFile),
|
|
12
|
+
await snapshot(paths.legacyConfigFile),
|
|
13
|
+
await snapshot(codexScopedPaths(paths).codexConfigFile),
|
|
14
|
+
await snapshot(paths.opencodeConfigFile),
|
|
15
|
+
await snapshot(paths.claudeConfigFile),
|
|
16
|
+
];
|
|
17
|
+
if (paths.scope === 'project')
|
|
18
|
+
snapshots.push(await snapshot(agentsFile(paths.targetRoot)));
|
|
19
|
+
let changedSkill = [];
|
|
20
|
+
try {
|
|
21
|
+
// 1. Config — preserve profile, create canonical from legacy if needed (READ LEGACY, WRITE CANONICAL)
|
|
22
|
+
const beforeConfig = await readBridgeConfig(paths);
|
|
23
|
+
const ensured = await ensureBridgeConfig(paths, undefined);
|
|
24
|
+
const configCreated = !beforeConfig.exists && ensured.created;
|
|
25
|
+
const migratedConfig = beforeConfig.source === 'legacy' && ensured.config;
|
|
26
|
+
// 2. Skill — refresh to current assets
|
|
27
|
+
const skillResult = await installSkill(assets().skill, paths.skillRoot);
|
|
28
|
+
changedSkill = skillResult.changed;
|
|
29
|
+
// 3. AGENTS block — refresh
|
|
30
|
+
if (paths.scope === 'project')
|
|
31
|
+
await ensureJuTellAgentsBlock(paths.targetRoot);
|
|
32
|
+
// 4. Provider MCP — refresh canonical command/path, repair drift, keep legacy + unrelated
|
|
33
|
+
const mcpEnabled = ensured.config.mcp?.enabled === true;
|
|
34
|
+
const codexPaths = codexScopedPaths(paths);
|
|
35
|
+
const codexReg = await readCodexRegistration(codexPaths, packageRoot(), mcpEnabled);
|
|
36
|
+
const opencodeReg = await readOpenCodeRegistration(paths, packageRoot(), mcpEnabled);
|
|
37
|
+
const claudeReg = await readClaudeRegistration(paths, packageRoot(), mcpEnabled);
|
|
38
|
+
let codexRefreshed = false;
|
|
39
|
+
let opencodeRefreshed = false;
|
|
40
|
+
let claudeRefreshed = false;
|
|
41
|
+
// Codex: if any JuTell registration (canonical or legacy heuristic) exists, ensure canonical block is fresh
|
|
42
|
+
if (codexReg.registered || codexReg.legacyRegistered || codexReg.canonicalRegistered) {
|
|
43
|
+
// Force refresh by rewriting canonical block with current packageRoot/command
|
|
44
|
+
// Use registerMcp's forced path: remove canonical block then add fresh
|
|
45
|
+
const before = await readCodexRegistration(codexPaths, packageRoot(), mcpEnabled);
|
|
46
|
+
// registerMcp will handle backup + rewrite; we force by temporarily clearing enabled flag check via direct write if needed
|
|
47
|
+
// Simpler: call registerMcp — it will rewrite if command drift is detected via heuristic? Our heuristic already treats unmarked as registered,
|
|
48
|
+
// but registerMcp's early return checks canonicalRegistered + enabled ===. To force refresh when command is stale, we bypass by
|
|
49
|
+
// using internal write: if canonical block exists but command doesn't match current packageRoot, force.
|
|
50
|
+
const currentContent = before.content;
|
|
51
|
+
const expected = packageRoot();
|
|
52
|
+
const needsRefresh = !currentContent.includes(expected) && (before.canonicalRegistered || before.legacyRegistered);
|
|
53
|
+
if (needsRefresh || before.canonicalRegistered) {
|
|
54
|
+
await registerMcp(codexPaths, packageRoot(), mcpEnabled);
|
|
55
|
+
codexRefreshed = true;
|
|
56
|
+
}
|
|
57
|
+
else if (before.legacyRegistered && !before.canonicalRegistered) {
|
|
58
|
+
// legacy-only -> create canonical (READ LEGACY, WRITE CANONICAL, keep legacy)
|
|
59
|
+
await registerMcp(codexPaths, packageRoot(), mcpEnabled);
|
|
60
|
+
codexRefreshed = true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// OpenCode: if registered, refresh via registerOpenCodeMcp (serializeWithManaged uses current packageRoot)
|
|
64
|
+
if (opencodeReg.registered || opencodeReg.canonicalRegistered || opencodeReg.legacyRegistered) {
|
|
65
|
+
await registerOpenCodeMcp(paths, packageRoot(), mcpEnabled);
|
|
66
|
+
opencodeRefreshed = true;
|
|
67
|
+
}
|
|
68
|
+
// Claude: if registered, ensure command matches current (registerClaudeMcp already checks commandMatches)
|
|
69
|
+
if (claudeReg.registered) {
|
|
70
|
+
await registerClaudeMcp(paths, packageRoot(), mcpEnabled);
|
|
71
|
+
claudeRefreshed = true;
|
|
72
|
+
}
|
|
73
|
+
await recordSkillFiles(paths, changedSkill);
|
|
74
|
+
// 5. Report — what changed / preserved / legacy / next
|
|
75
|
+
const parts = ['JuTell 업그레이드가 끝났습니다.'];
|
|
76
|
+
if (migratedConfig)
|
|
77
|
+
parts.push('- 설정: 이전 .beginner-bridge.json을 읽어 .jutell.json을 만들었습니다. 이전 파일은 보존했습니다.');
|
|
78
|
+
else if (configCreated)
|
|
79
|
+
parts.push('- 설정: 새 .jutell.json을 만들었습니다.');
|
|
80
|
+
else
|
|
81
|
+
parts.push('- 설정: 기존 .jutell.json을 유지했습니다.');
|
|
82
|
+
if (skillResult.conflicts.length)
|
|
83
|
+
parts.push(`- Skill: 기존 파일을 덮어쓰지 않았습니다 (${skillResult.conflicts.join(', ')}).`);
|
|
84
|
+
else if (changedSkill.length)
|
|
85
|
+
parts.push('- Skill: 최신 Skill로 새로고침했습니다.');
|
|
86
|
+
else
|
|
87
|
+
parts.push('- Skill: 최신 상태를 유지했습니다.');
|
|
88
|
+
parts.push('- AGENTS.md: JuTell 블록을 확인/새로고침했습니다.');
|
|
89
|
+
if (codexRefreshed || opencodeRefreshed || claudeRefreshed) {
|
|
90
|
+
const refreshed = [
|
|
91
|
+
codexRefreshed ? 'Codex' : null,
|
|
92
|
+
opencodeRefreshed ? 'OpenCode' : null,
|
|
93
|
+
claudeRefreshed ? 'Claude Code' : null,
|
|
94
|
+
].filter(Boolean).join(', ');
|
|
95
|
+
parts.push(`- MCP: ${refreshed} canonical jutell 블록을 현재 패키지 경로로 새로고침했습니다. 관련 없는 설정은 보존했습니다.`);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
parts.push('- MCP: 등록된 Provider가 없어 MCP를 새로고침하지 않았습니다. jutell use <provider>로 연결하세요.');
|
|
99
|
+
}
|
|
100
|
+
const anyLegacy = codexReg.legacyRegistered || opencodeReg.legacyRegistered || beforeConfig.source === 'legacy';
|
|
101
|
+
if (anyLegacy)
|
|
102
|
+
parts.push('- 레거시: 이전 beginner_bridge 상태는 그대로 두었습니다. 정리하려면 jutell migrate --clean 을 실행하세요.');
|
|
103
|
+
parts.push('다음: jutell status / jutell doctor 로 확인하세요.');
|
|
104
|
+
io.write(parts.join('\n'));
|
|
105
|
+
return { cancelled: false };
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
for (const s of snapshots)
|
|
109
|
+
await restore(s);
|
|
110
|
+
await removeAddedSkillFiles(paths.skillRoot, changedSkill);
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|