relayax-cli 0.2.40 → 0.3.41
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/diff.d.ts +2 -0
- package/dist/commands/diff.js +72 -0
- package/dist/commands/install.js +73 -205
- package/dist/commands/join.d.ts +3 -2
- package/dist/commands/join.js +18 -69
- package/dist/commands/list.js +18 -21
- package/dist/commands/orgs.d.ts +10 -0
- package/dist/commands/orgs.js +128 -0
- package/dist/commands/package.d.ts +2 -0
- package/dist/commands/package.js +287 -0
- package/dist/commands/publish.js +63 -58
- package/dist/commands/search.js +1 -1
- package/dist/commands/spaces.d.ts +0 -1
- package/dist/commands/versions.d.ts +2 -0
- package/dist/commands/versions.js +44 -0
- package/dist/index.js +8 -2
- package/dist/lib/api.d.ts +7 -6
- package/dist/lib/api.js +14 -29
- package/dist/lib/command-adapter.js +147 -51
- package/dist/lib/config.d.ts +9 -4
- package/dist/lib/config.js +105 -23
- package/dist/lib/guide.d.ts +13 -5
- package/dist/lib/guide.js +142 -50
- package/dist/lib/preamble.js +3 -4
- package/dist/lib/slug.d.ts +0 -1
- package/dist/lib/slug.js +3 -7
- package/dist/types.d.ts +2 -2
- package/package.json +1 -1
package/dist/commands/list.js
CHANGED
|
@@ -2,30 +2,27 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.registerList = registerList;
|
|
4
4
|
const config_js_1 = require("../lib/config.js");
|
|
5
|
-
async function
|
|
6
|
-
const res = await fetch(`${config_js_1.API_URL}/api/
|
|
5
|
+
async function fetchOrgTeamList(orgSlug, token) {
|
|
6
|
+
const res = await fetch(`${config_js_1.API_URL}/api/orgs/${orgSlug}/teams`, {
|
|
7
7
|
headers: { Authorization: `Bearer ${token}` },
|
|
8
8
|
signal: AbortSignal.timeout(8000),
|
|
9
9
|
});
|
|
10
10
|
if (!res.ok) {
|
|
11
11
|
const body = await res.text();
|
|
12
|
-
throw new Error(`
|
|
12
|
+
throw new Error(`Org 팀 목록 조회 실패 (${res.status}): ${body}`);
|
|
13
13
|
}
|
|
14
|
-
|
|
15
|
-
if (Array.isArray(data))
|
|
16
|
-
return data;
|
|
17
|
-
return data.teams ?? [];
|
|
14
|
+
return (await res.json());
|
|
18
15
|
}
|
|
19
16
|
function registerList(program) {
|
|
20
17
|
program
|
|
21
18
|
.command('list')
|
|
22
19
|
.description('설치된 에이전트 팀 목록')
|
|
23
|
-
.option('--
|
|
20
|
+
.option('--org <slug>', 'Organization 팀 목록 조회')
|
|
24
21
|
.action(async (opts) => {
|
|
25
22
|
const json = program.opts().json ?? false;
|
|
26
|
-
// --
|
|
27
|
-
if (opts.
|
|
28
|
-
const
|
|
23
|
+
// --org 옵션: Org 팀 목록
|
|
24
|
+
if (opts.org) {
|
|
25
|
+
const orgSlug = opts.org;
|
|
29
26
|
const token = await (0, config_js_1.getValidToken)();
|
|
30
27
|
if (!token) {
|
|
31
28
|
if (json) {
|
|
@@ -38,23 +35,23 @@ function registerList(program) {
|
|
|
38
35
|
process.exit(1);
|
|
39
36
|
}
|
|
40
37
|
try {
|
|
41
|
-
const teams = await
|
|
38
|
+
const teams = await fetchOrgTeamList(orgSlug, token);
|
|
42
39
|
if (json) {
|
|
43
|
-
console.log(JSON.stringify({
|
|
40
|
+
console.log(JSON.stringify({ org: orgSlug, teams }));
|
|
44
41
|
return;
|
|
45
42
|
}
|
|
46
43
|
if (teams.length === 0) {
|
|
47
|
-
console.log(`\n
|
|
44
|
+
console.log(`\n@${orgSlug} Organization에 팀이 없습니다.`);
|
|
48
45
|
return;
|
|
49
46
|
}
|
|
50
|
-
console.log(`\n\x1b[1m
|
|
47
|
+
console.log(`\n\x1b[1m@${orgSlug} 팀 목록\x1b[0m (${teams.length}개):\n`);
|
|
51
48
|
for (const t of teams) {
|
|
52
49
|
const desc = t.description
|
|
53
50
|
? ` \x1b[90m${t.description.length > 50 ? t.description.slice(0, 50) + '...' : t.description}\x1b[0m`
|
|
54
51
|
: '';
|
|
55
|
-
console.log(` \x1b[36m
|
|
52
|
+
console.log(` \x1b[36m@${t.owner}/${t.slug}\x1b[0m \x1b[1m${t.name}\x1b[0m${desc}`);
|
|
56
53
|
}
|
|
57
|
-
console.log(`\n\x1b[33m
|
|
54
|
+
console.log(`\n\x1b[33m 설치: relay install @${orgSlug}/<팀슬러그>\x1b[0m`);
|
|
58
55
|
}
|
|
59
56
|
catch (err) {
|
|
60
57
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -80,7 +77,7 @@ function registerList(program) {
|
|
|
80
77
|
installed_at: info.installed_at,
|
|
81
78
|
scope: 'global',
|
|
82
79
|
deploy_scope: info.deploy_scope,
|
|
83
|
-
|
|
80
|
+
org_slug: info.org_slug,
|
|
84
81
|
});
|
|
85
82
|
seen.add(slug);
|
|
86
83
|
}
|
|
@@ -94,7 +91,7 @@ function registerList(program) {
|
|
|
94
91
|
installed_at: info.installed_at,
|
|
95
92
|
scope: 'local',
|
|
96
93
|
deploy_scope: info.deploy_scope,
|
|
97
|
-
|
|
94
|
+
org_slug: info.org_slug,
|
|
98
95
|
});
|
|
99
96
|
}
|
|
100
97
|
if (json) {
|
|
@@ -113,8 +110,8 @@ function registerList(program) {
|
|
|
113
110
|
: item.deploy_scope === 'local'
|
|
114
111
|
? '\x1b[33m로컬\x1b[0m'
|
|
115
112
|
: '\x1b[90m미배치\x1b[0m';
|
|
116
|
-
const
|
|
117
|
-
console.log(` \x1b[36m${item.slug}\x1b[0m v${item.version} ${scopeLabel} (${date})${
|
|
113
|
+
const orgLabel = item.org_slug ? ` \x1b[90m[Org: ${item.org_slug}]\x1b[0m` : '';
|
|
114
|
+
console.log(` \x1b[36m${item.slug}\x1b[0m v${item.version} ${scopeLabel} (${date})${orgLabel}`);
|
|
118
115
|
}
|
|
119
116
|
}
|
|
120
117
|
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
export interface OrgInfo {
|
|
3
|
+
id: string;
|
|
4
|
+
slug: string;
|
|
5
|
+
name: string;
|
|
6
|
+
description: string | null;
|
|
7
|
+
role: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function fetchMyOrgs(token: string): Promise<OrgInfo[]>;
|
|
10
|
+
export declare function registerOrgs(program: Command): void;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fetchMyOrgs = fetchMyOrgs;
|
|
4
|
+
exports.registerOrgs = registerOrgs;
|
|
5
|
+
const config_js_1 = require("../lib/config.js");
|
|
6
|
+
async function fetchMyOrgs(token) {
|
|
7
|
+
const res = await fetch(`${config_js_1.API_URL}/api/orgs`, {
|
|
8
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
9
|
+
signal: AbortSignal.timeout(8000),
|
|
10
|
+
});
|
|
11
|
+
if (!res.ok) {
|
|
12
|
+
throw new Error(`Organization 목록 조회 실패 (${res.status})`);
|
|
13
|
+
}
|
|
14
|
+
return (await res.json());
|
|
15
|
+
}
|
|
16
|
+
function registerOrgs(program) {
|
|
17
|
+
const orgsCmd = program
|
|
18
|
+
.command('orgs')
|
|
19
|
+
.description('Organization 관련 명령어');
|
|
20
|
+
orgsCmd
|
|
21
|
+
.command('list')
|
|
22
|
+
.description('내 Organization 목록을 확인합니다')
|
|
23
|
+
.action(async () => {
|
|
24
|
+
const json = program.opts().json ?? false;
|
|
25
|
+
const token = await (0, config_js_1.getValidToken)();
|
|
26
|
+
if (!token) {
|
|
27
|
+
if (json) {
|
|
28
|
+
console.error(JSON.stringify({ error: 'LOGIN_REQUIRED', message: '로그인이 필요합니다.', fix: 'relay login 실행 후 재시도하세요.' }));
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
console.error('\x1b[31m오류: 로그인이 필요합니다.\x1b[0m');
|
|
32
|
+
console.error(' relay login을 먼저 실행하세요.');
|
|
33
|
+
}
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const orgs = await fetchMyOrgs(token);
|
|
38
|
+
if (json) {
|
|
39
|
+
console.log(JSON.stringify({ orgs }));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (orgs.length === 0) {
|
|
43
|
+
console.log('\nOrganization이 없습니다.');
|
|
44
|
+
console.log('\x1b[33m Organization을 만들려면: relay orgs create "이름"\x1b[0m');
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
console.log(`\n\x1b[1m내 Organization\x1b[0m (${orgs.length}개):\n`);
|
|
48
|
+
for (const o of orgs) {
|
|
49
|
+
const role = o.role === 'owner' ? '\x1b[33m오너\x1b[0m'
|
|
50
|
+
: o.role === 'admin' ? '\x1b[36m관리자\x1b[0m'
|
|
51
|
+
: o.role === 'builder' ? '\x1b[36m빌더\x1b[0m'
|
|
52
|
+
: '\x1b[90m멤버\x1b[0m';
|
|
53
|
+
const desc = o.description
|
|
54
|
+
? ` \x1b[90m${o.description.length > 40 ? o.description.slice(0, 40) + '...' : o.description}\x1b[0m`
|
|
55
|
+
: '';
|
|
56
|
+
console.log(` \x1b[36m@${o.slug}\x1b[0m \x1b[1m${o.name}\x1b[0m ${role}${desc}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
62
|
+
if (json) {
|
|
63
|
+
console.error(JSON.stringify({ error: 'FETCH_FAILED', message, fix: '네트워크 연결을 확인하거나 잠시 후 재시도하세요.' }));
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
console.error(`\x1b[31m오류: ${message}\x1b[0m`);
|
|
67
|
+
}
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
orgsCmd
|
|
72
|
+
.command('create <name>')
|
|
73
|
+
.description('새 Organization을 생성합니다')
|
|
74
|
+
.option('--slug <slug>', 'URL slug (미지정 시 이름에서 자동 생성)')
|
|
75
|
+
.action(async (name, opts) => {
|
|
76
|
+
const json = program.opts().json ?? false;
|
|
77
|
+
const token = await (0, config_js_1.getValidToken)();
|
|
78
|
+
if (!token) {
|
|
79
|
+
if (json) {
|
|
80
|
+
console.error(JSON.stringify({ error: 'LOGIN_REQUIRED', message: '로그인이 필요합니다.', fix: 'relay login 실행 후 재시도하세요.' }));
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
console.error('\x1b[31m오류: 로그인이 필요합니다.\x1b[0m');
|
|
84
|
+
}
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
const slug = opts.slug ?? name
|
|
88
|
+
.toLowerCase()
|
|
89
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
90
|
+
.replace(/[\s]+/g, '-')
|
|
91
|
+
.replace(/-+/g, '-')
|
|
92
|
+
.replace(/^-|-$/g, '')
|
|
93
|
+
.slice(0, 50);
|
|
94
|
+
try {
|
|
95
|
+
const res = await fetch(`${config_js_1.API_URL}/api/orgs`, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
headers: {
|
|
98
|
+
'Content-Type': 'application/json',
|
|
99
|
+
Authorization: `Bearer ${token}`,
|
|
100
|
+
},
|
|
101
|
+
body: JSON.stringify({ name, slug }),
|
|
102
|
+
});
|
|
103
|
+
if (!res.ok) {
|
|
104
|
+
const body = await res.json().catch(() => ({ message: `${res.status}` }));
|
|
105
|
+
throw new Error(body.message ?? `Organization 생성 실패 (${res.status})`);
|
|
106
|
+
}
|
|
107
|
+
const org = await res.json();
|
|
108
|
+
if (json) {
|
|
109
|
+
console.log(JSON.stringify({ status: 'created', org }));
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
console.log(`\x1b[32m✅ Organization "${org.name}" (@${org.slug}) 생성 완료\x1b[0m`);
|
|
113
|
+
console.log(`\n\x1b[33m 팀 배포: relay publish --org ${org.slug}\x1b[0m`);
|
|
114
|
+
console.log(`\x1b[33m 멤버 초대: www.relayax.com/orgs/${org.slug}/members\x1b[0m`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
119
|
+
if (json) {
|
|
120
|
+
console.error(JSON.stringify({ error: 'CREATE_FAILED', message }));
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
console.error(`\x1b[31m오류: ${message}\x1b[0m`);
|
|
124
|
+
}
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerPackage = registerPackage;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
10
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
11
|
+
const ai_tools_js_1 = require("../lib/ai-tools.js");
|
|
12
|
+
const SYNC_DIRS = ['skills', 'commands', 'agents', 'rules'];
|
|
13
|
+
const EXCLUDE_SUBDIRS = ['relay']; // relay CLI 전용 하위 디렉토리 제외
|
|
14
|
+
// ─── Helpers ───
|
|
15
|
+
function fileHash(filePath) {
|
|
16
|
+
const content = fs_1.default.readFileSync(filePath);
|
|
17
|
+
return crypto_1.default.createHash('md5').update(content).digest('hex');
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 디렉토리를 재귀 탐색하여 파일 목록을 반환한다.
|
|
21
|
+
* baseDir 기준 상대 경로 + 해시.
|
|
22
|
+
*/
|
|
23
|
+
function scanDir(baseDir, subDir) {
|
|
24
|
+
const fullDir = path_1.default.join(baseDir, subDir);
|
|
25
|
+
if (!fs_1.default.existsSync(fullDir))
|
|
26
|
+
return [];
|
|
27
|
+
const entries = [];
|
|
28
|
+
function walk(dir) {
|
|
29
|
+
for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
30
|
+
if (entry.name.startsWith('.'))
|
|
31
|
+
continue;
|
|
32
|
+
const fullPath = path_1.default.join(dir, entry.name);
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
walk(fullPath);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const relPath = path_1.default.relative(baseDir, fullPath);
|
|
38
|
+
entries.push({ relPath, hash: fileHash(fullPath) });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
walk(fullDir);
|
|
43
|
+
return entries;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 소스 디렉토리(예: .claude/)에서 배포 가능한 콘텐츠를 스캔한다.
|
|
47
|
+
* relay/ 하위 디렉토리는 제외.
|
|
48
|
+
*/
|
|
49
|
+
function scanSource(projectPath, tool) {
|
|
50
|
+
const sourceBase = path_1.default.join(projectPath, tool.skillsDir);
|
|
51
|
+
const files = [];
|
|
52
|
+
const summary = {};
|
|
53
|
+
for (const dir of SYNC_DIRS) {
|
|
54
|
+
const fullDir = path_1.default.join(sourceBase, dir);
|
|
55
|
+
if (!fs_1.default.existsSync(fullDir))
|
|
56
|
+
continue;
|
|
57
|
+
// 제외 대상 필터링 (예: commands/relay/)
|
|
58
|
+
const dirEntries = fs_1.default.readdirSync(fullDir, { withFileTypes: true });
|
|
59
|
+
let count = 0;
|
|
60
|
+
for (const entry of dirEntries) {
|
|
61
|
+
if (entry.name.startsWith('.'))
|
|
62
|
+
continue;
|
|
63
|
+
if (entry.isDirectory() && EXCLUDE_SUBDIRS.includes(entry.name))
|
|
64
|
+
continue;
|
|
65
|
+
const entryPath = path_1.default.join(fullDir, entry.name);
|
|
66
|
+
if (entry.isDirectory()) {
|
|
67
|
+
// 하위 파일 재귀 탐색
|
|
68
|
+
const subFiles = scanDir(sourceBase, path_1.default.join(dir, entry.name));
|
|
69
|
+
// relPath를 sourceBase 기준 → SYNC_DIRS 기준으로 유지
|
|
70
|
+
files.push(...subFiles);
|
|
71
|
+
count += subFiles.length > 0 ? 1 : 0; // 디렉토리 단위로 카운트
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
const relPath = path_1.default.relative(sourceBase, entryPath);
|
|
75
|
+
files.push({ relPath, hash: fileHash(entryPath) });
|
|
76
|
+
count++;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (count > 0)
|
|
80
|
+
summary[dir] = count;
|
|
81
|
+
}
|
|
82
|
+
return { tool, files, summary };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* .relay/ 디렉토리의 현재 콘텐츠를 스캔한다.
|
|
86
|
+
*/
|
|
87
|
+
function scanRelay(relayDir) {
|
|
88
|
+
const files = [];
|
|
89
|
+
for (const dir of SYNC_DIRS) {
|
|
90
|
+
files.push(...scanDir(relayDir, dir));
|
|
91
|
+
}
|
|
92
|
+
return files;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 소스와 .relay/를 비교하여 diff를 생성한다.
|
|
96
|
+
*/
|
|
97
|
+
function computeDiff(sourceFiles, relayFiles) {
|
|
98
|
+
const relayMap = new Map(relayFiles.map((f) => [f.relPath, f.hash]));
|
|
99
|
+
const sourceMap = new Map(sourceFiles.map((f) => [f.relPath, f.hash]));
|
|
100
|
+
const diff = [];
|
|
101
|
+
// 소스에 있는 파일
|
|
102
|
+
for (const [relPath, hash] of sourceMap) {
|
|
103
|
+
const relayHash = relayMap.get(relPath);
|
|
104
|
+
if (!relayHash) {
|
|
105
|
+
diff.push({ relPath, status: 'added' });
|
|
106
|
+
}
|
|
107
|
+
else if (relayHash !== hash) {
|
|
108
|
+
diff.push({ relPath, status: 'modified' });
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
diff.push({ relPath, status: 'unchanged' });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// .relay/에만 있는 파일 (소스에서 삭제됨)
|
|
115
|
+
for (const [relPath] of relayMap) {
|
|
116
|
+
if (!sourceMap.has(relPath)) {
|
|
117
|
+
diff.push({ relPath, status: 'deleted' });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return diff.sort((a, b) => a.relPath.localeCompare(b.relPath));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* 소스에서 .relay/로 파일을 동기화한다.
|
|
124
|
+
*/
|
|
125
|
+
function syncToRelay(sourceBase, relayDir, diff) {
|
|
126
|
+
for (const entry of diff) {
|
|
127
|
+
const sourcePath = path_1.default.join(sourceBase, entry.relPath);
|
|
128
|
+
const relayPath = path_1.default.join(relayDir, entry.relPath);
|
|
129
|
+
if (entry.status === 'added' || entry.status === 'modified') {
|
|
130
|
+
fs_1.default.mkdirSync(path_1.default.dirname(relayPath), { recursive: true });
|
|
131
|
+
fs_1.default.copyFileSync(sourcePath, relayPath);
|
|
132
|
+
}
|
|
133
|
+
else if (entry.status === 'deleted') {
|
|
134
|
+
if (fs_1.default.existsSync(relayPath)) {
|
|
135
|
+
fs_1.default.unlinkSync(relayPath);
|
|
136
|
+
// 빈 디렉토리 정리
|
|
137
|
+
const parentDir = path_1.default.dirname(relayPath);
|
|
138
|
+
try {
|
|
139
|
+
const remaining = fs_1.default.readdirSync(parentDir).filter((f) => !f.startsWith('.'));
|
|
140
|
+
if (remaining.length === 0)
|
|
141
|
+
fs_1.default.rmdirSync(parentDir);
|
|
142
|
+
}
|
|
143
|
+
catch { /* ignore */ }
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// ─── Command ───
|
|
149
|
+
function registerPackage(program) {
|
|
150
|
+
program
|
|
151
|
+
.command('package')
|
|
152
|
+
.description('소스 디렉토리에서 .relay/로 콘텐츠를 동기화합니다')
|
|
153
|
+
.option('--source <dir>', '소스 디렉토리 지정 (예: .claude)')
|
|
154
|
+
.option('--sync', '변경사항을 .relay/에 즉시 반영', false)
|
|
155
|
+
.option('--init', '최초 패키징: 소스 감지 → .relay/ 초기화', false)
|
|
156
|
+
.action(async (opts) => {
|
|
157
|
+
const json = program.opts().json ?? false;
|
|
158
|
+
const projectPath = process.cwd();
|
|
159
|
+
const relayDir = path_1.default.join(projectPath, '.relay');
|
|
160
|
+
const relayYamlPath = path_1.default.join(relayDir, 'relay.yaml');
|
|
161
|
+
// ─── 최초 패키징 (--init) ───
|
|
162
|
+
if (opts.init || !fs_1.default.existsSync(relayYamlPath)) {
|
|
163
|
+
const detected = (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
|
|
164
|
+
// 각 도구의 콘텐츠 스캔
|
|
165
|
+
const scans = detected
|
|
166
|
+
.map((tool) => scanSource(projectPath, tool))
|
|
167
|
+
.filter((s) => s.files.length > 0);
|
|
168
|
+
if (json) {
|
|
169
|
+
console.log(JSON.stringify({
|
|
170
|
+
status: 'init_required',
|
|
171
|
+
detected: scans.map((s) => ({
|
|
172
|
+
source: s.tool.skillsDir,
|
|
173
|
+
name: s.tool.name,
|
|
174
|
+
summary: s.summary,
|
|
175
|
+
fileCount: s.files.length,
|
|
176
|
+
})),
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
if (scans.length === 0) {
|
|
181
|
+
console.error('배포 가능한 에이전트 콘텐츠를 찾지 못했습니다.');
|
|
182
|
+
console.error('skills/, commands/, agents/, rules/ 중 하나를 만들어주세요.');
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
console.error('\n프로젝트에서 발견된 에이전트 콘텐츠:\n');
|
|
186
|
+
for (const scan of scans) {
|
|
187
|
+
const parts = Object.entries(scan.summary)
|
|
188
|
+
.map(([dir, count]) => `${dir} ${count}개`)
|
|
189
|
+
.join(', ');
|
|
190
|
+
console.error(` 📁 ${scan.tool.skillsDir}/ — ${parts}`);
|
|
191
|
+
}
|
|
192
|
+
console.error('');
|
|
193
|
+
}
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
// ─── 재패키징 (source 기반 동기화) ───
|
|
197
|
+
const yamlContent = fs_1.default.readFileSync(relayYamlPath, 'utf-8');
|
|
198
|
+
const config = js_yaml_1.default.load(yamlContent);
|
|
199
|
+
const source = opts.source ?? config.source;
|
|
200
|
+
if (!source) {
|
|
201
|
+
if (json) {
|
|
202
|
+
console.log(JSON.stringify({
|
|
203
|
+
status: 'no_source',
|
|
204
|
+
message: 'relay.yaml에 source 필드가 없습니다. --source 옵션으로 지정하거나 relay.yaml에 source를 추가하세요.',
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
console.error('relay.yaml에 source 필드가 없습니다.');
|
|
209
|
+
console.error('--source <dir> 옵션으로 지정하거나 relay.yaml에 source를 추가하세요.');
|
|
210
|
+
}
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
// 소스 디렉토리 존재 확인
|
|
214
|
+
const sourceBase = path_1.default.join(projectPath, source);
|
|
215
|
+
if (!fs_1.default.existsSync(sourceBase)) {
|
|
216
|
+
const msg = `소스 디렉토리 '${source}'를 찾을 수 없습니다.`;
|
|
217
|
+
if (json) {
|
|
218
|
+
console.log(JSON.stringify({ error: 'SOURCE_NOT_FOUND', message: msg }));
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
console.error(msg);
|
|
222
|
+
}
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
// 소스에서 해당 도구 찾기
|
|
226
|
+
const allTools = (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
|
|
227
|
+
const tool = allTools.find((t) => t.skillsDir === source);
|
|
228
|
+
const toolName = tool?.name ?? source;
|
|
229
|
+
// diff 계산
|
|
230
|
+
const sourceScan = tool
|
|
231
|
+
? scanSource(projectPath, tool)
|
|
232
|
+
: { tool: { name: source, value: source, skillsDir: source }, files: [], summary: {} };
|
|
233
|
+
// tool이 없으면 직접 스캔
|
|
234
|
+
if (!tool) {
|
|
235
|
+
for (const dir of SYNC_DIRS) {
|
|
236
|
+
const files = scanDir(sourceBase, dir);
|
|
237
|
+
sourceScan.files.push(...files);
|
|
238
|
+
if (files.length > 0)
|
|
239
|
+
sourceScan.summary[dir] = files.length;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const relayFiles = scanRelay(relayDir);
|
|
243
|
+
const diff = computeDiff(sourceScan.files, relayFiles);
|
|
244
|
+
const summary = {
|
|
245
|
+
added: diff.filter((d) => d.status === 'added').length,
|
|
246
|
+
modified: diff.filter((d) => d.status === 'modified').length,
|
|
247
|
+
deleted: diff.filter((d) => d.status === 'deleted').length,
|
|
248
|
+
unchanged: diff.filter((d) => d.status === 'unchanged').length,
|
|
249
|
+
};
|
|
250
|
+
const hasChanges = summary.added + summary.modified + summary.deleted > 0;
|
|
251
|
+
// --sync: 즉시 동기화
|
|
252
|
+
if (opts.sync && hasChanges) {
|
|
253
|
+
syncToRelay(sourceBase, relayDir, diff);
|
|
254
|
+
}
|
|
255
|
+
const result = {
|
|
256
|
+
source,
|
|
257
|
+
sourceName: toolName,
|
|
258
|
+
synced: opts.sync === true && hasChanges,
|
|
259
|
+
diff: diff.filter((d) => d.status !== 'unchanged'),
|
|
260
|
+
summary,
|
|
261
|
+
};
|
|
262
|
+
if (json) {
|
|
263
|
+
console.log(JSON.stringify(result));
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
if (!hasChanges) {
|
|
267
|
+
console.error(`✓ 소스(${source})와 .relay/가 동기화 상태입니다.`);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
console.error(`\n📦 소스 동기화 (${source}/ → .relay/)\n`);
|
|
271
|
+
for (const entry of diff) {
|
|
272
|
+
if (entry.status === 'unchanged')
|
|
273
|
+
continue;
|
|
274
|
+
const icon = entry.status === 'added' ? ' 신규' : entry.status === 'modified' ? ' 변경' : ' 삭제';
|
|
275
|
+
console.error(`${icon}: ${entry.relPath}`);
|
|
276
|
+
}
|
|
277
|
+
console.error('');
|
|
278
|
+
console.error(` 합계: 신규 ${summary.added}, 변경 ${summary.modified}, 삭제 ${summary.deleted}, 유지 ${summary.unchanged}`);
|
|
279
|
+
if (opts.sync) {
|
|
280
|
+
console.error(`\n✓ .relay/에 반영 완료`);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
console.error(`\n반영하려면: relay package --sync`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
}
|