relayax-cli 0.1.1 → 0.1.3
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/init.js +102 -46
- package/dist/commands/install.js +7 -5
- package/dist/commands/login.js +5 -13
- package/dist/commands/publish.js +81 -67
- package/dist/lib/api.js +3 -6
- package/dist/lib/config.d.ts +8 -9
- package/dist/lib/config.js +25 -49
- package/dist/types.d.ts +0 -5
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -6,71 +6,127 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.registerInit = registerInit;
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
|
-
const os_1 = __importDefault(require("os"));
|
|
10
9
|
const readline_1 = __importDefault(require("readline"));
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
const DIRS = ['skills', 'commands', 'agents', 'rules'];
|
|
11
|
+
function prompt(rl, question, defaultVal) {
|
|
12
|
+
const hint = defaultVal ? ` (${defaultVal})` : '';
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
rl.question(`${question}${hint}: `, (answer) => {
|
|
15
|
+
resolve(answer.trim() || defaultVal || '');
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function slugify(str) {
|
|
20
|
+
return str
|
|
21
|
+
.toLowerCase()
|
|
22
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
23
|
+
.replace(/-+/g, '-')
|
|
24
|
+
.replace(/^-|-$/g, '');
|
|
25
|
+
}
|
|
26
|
+
function toYaml(data) {
|
|
27
|
+
const lines = [
|
|
28
|
+
`name: "${data.name}"`,
|
|
29
|
+
`slug: "${data.slug}"`,
|
|
30
|
+
`description: "${data.description}"`,
|
|
31
|
+
`version: "${data.version}"`,
|
|
32
|
+
];
|
|
33
|
+
if (data.tags.length > 0) {
|
|
34
|
+
lines.push('tags:');
|
|
35
|
+
for (const tag of data.tags) {
|
|
36
|
+
lines.push(` - "${tag}"`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
lines.push('tags: []');
|
|
41
|
+
}
|
|
42
|
+
return lines.join('\n') + '\n';
|
|
19
43
|
}
|
|
20
44
|
function registerInit(program) {
|
|
21
45
|
program
|
|
22
46
|
.command('init')
|
|
23
|
-
.description('
|
|
24
|
-
.option('--
|
|
25
|
-
.option('--
|
|
47
|
+
.description('현재 디렉토리를 RelayAX 팀 패키지로 초기화합니다')
|
|
48
|
+
.option('--name <name>', '팀 이름')
|
|
49
|
+
.option('--slug <slug>', 'URL 슬러그')
|
|
50
|
+
.option('--description <desc>', '한 줄 설명')
|
|
26
51
|
.action(async (opts) => {
|
|
27
52
|
const pretty = program.opts().pretty ?? false;
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
if
|
|
31
|
-
|
|
53
|
+
const cwd = process.cwd();
|
|
54
|
+
const relayYamlPath = path_1.default.join(cwd, 'relay.yaml');
|
|
55
|
+
// Check if already initialized
|
|
56
|
+
if (fs_1.default.existsSync(relayYamlPath)) {
|
|
57
|
+
if (pretty) {
|
|
58
|
+
console.log('\x1b[33m이미 초기화되어 있습니다.\x1b[0m relay.yaml이 존재합니다.');
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
console.log(JSON.stringify({ status: 'already_initialized', path: relayYamlPath }));
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
32
64
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
65
|
+
// Detect existing directories
|
|
66
|
+
const existing = DIRS.filter((d) => fs_1.default.existsSync(path_1.default.join(cwd, d)));
|
|
67
|
+
const missing = DIRS.filter((d) => !fs_1.default.existsSync(path_1.default.join(cwd, d)));
|
|
68
|
+
const dirName = path_1.default.basename(cwd);
|
|
69
|
+
const autoSlug = slugify(dirName);
|
|
70
|
+
let name;
|
|
71
|
+
let slug;
|
|
72
|
+
let description;
|
|
73
|
+
let tags = [];
|
|
74
|
+
if (opts.name && opts.slug && opts.description) {
|
|
75
|
+
// Non-interactive
|
|
76
|
+
name = opts.name;
|
|
77
|
+
slug = opts.slug;
|
|
78
|
+
description = opts.description;
|
|
36
79
|
}
|
|
37
80
|
else {
|
|
38
|
-
const rl = readline_1.default.createInterface({
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
console.log(' 3) 직접 입력');
|
|
46
|
-
const choice = (await prompt(rl, '\n선택 [1]: ')).trim() || '1';
|
|
47
|
-
if (choice === '3') {
|
|
48
|
-
install_path = (await prompt(rl, '경로 입력: ')).trim();
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
install_path = PRESET_PATHS[choice] ?? PRESET_PATHS['1'];
|
|
81
|
+
const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stderr });
|
|
82
|
+
if (pretty) {
|
|
83
|
+
console.log('\n\x1b[1mRelayAX 팀 패키지 초기화\x1b[0m\n');
|
|
84
|
+
if (existing.length > 0) {
|
|
85
|
+
console.log(` 감지된 디렉토리: \x1b[36m${existing.join(', ')}\x1b[0m`);
|
|
86
|
+
}
|
|
87
|
+
console.log('');
|
|
52
88
|
}
|
|
89
|
+
name = opts.name ?? await prompt(rl, '팀 이름', dirName);
|
|
90
|
+
slug = opts.slug ?? await prompt(rl, '슬러그', autoSlug);
|
|
91
|
+
description = opts.description ?? await prompt(rl, '한 줄 설명');
|
|
92
|
+
const tagsInput = await prompt(rl, '태그 (쉼표 구분)', '');
|
|
93
|
+
tags = tagsInput ? tagsInput.split(',').map((t) => t.trim()).filter(Boolean) : [];
|
|
53
94
|
rl.close();
|
|
54
95
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
96
|
+
if (!description) {
|
|
97
|
+
console.error(JSON.stringify({ error: 'MISSING_DESCRIPTION', message: 'description은 필수입니다' }));
|
|
98
|
+
process.exit(1);
|
|
58
99
|
}
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
100
|
+
// Create missing directories
|
|
101
|
+
const created = [];
|
|
102
|
+
for (const dir of missing) {
|
|
103
|
+
fs_1.default.mkdirSync(path_1.default.join(cwd, dir), { recursive: true });
|
|
104
|
+
// Add .gitkeep so empty dirs are tracked
|
|
105
|
+
fs_1.default.writeFileSync(path_1.default.join(cwd, dir, '.gitkeep'), '');
|
|
106
|
+
created.push(dir);
|
|
62
107
|
}
|
|
63
|
-
|
|
64
|
-
|
|
108
|
+
// Write relay.yaml
|
|
109
|
+
const yamlData = { name, slug, description, version: '1.0.0', tags };
|
|
110
|
+
fs_1.default.writeFileSync(relayYamlPath, toYaml(yamlData));
|
|
65
111
|
const result = {
|
|
66
112
|
status: 'ok',
|
|
67
|
-
|
|
68
|
-
|
|
113
|
+
slug,
|
|
114
|
+
name,
|
|
115
|
+
description,
|
|
116
|
+
existing_dirs: existing,
|
|
117
|
+
created_dirs: created,
|
|
69
118
|
};
|
|
70
119
|
if (pretty) {
|
|
71
|
-
console.log(
|
|
72
|
-
console.log(`
|
|
73
|
-
console.log(`
|
|
120
|
+
console.log(`\n\x1b[32m✓ 초기화 완료\x1b[0m\n`);
|
|
121
|
+
console.log(` 팀: \x1b[36m${name}\x1b[0m (${slug})`);
|
|
122
|
+
console.log(` 설명: ${description}`);
|
|
123
|
+
if (created.length > 0) {
|
|
124
|
+
console.log(` 생성됨: \x1b[33m${created.join(', ')}\x1b[0m`);
|
|
125
|
+
}
|
|
126
|
+
console.log(` 설정: \x1b[36mrelay.yaml\x1b[0m`);
|
|
127
|
+
console.log('\n 다음 단계:');
|
|
128
|
+
console.log(' 1. skills/, commands/ 등에 파일 추가');
|
|
129
|
+
console.log(' 2. \x1b[36mrelay publish\x1b[0m 로 마켓에 배포');
|
|
74
130
|
}
|
|
75
131
|
else {
|
|
76
132
|
console.log(JSON.stringify(result));
|
package/dist/commands/install.js
CHANGED
|
@@ -8,10 +8,11 @@ const config_js_1 = require("../lib/config.js");
|
|
|
8
8
|
function registerInstall(program) {
|
|
9
9
|
program
|
|
10
10
|
.command('install <slug>')
|
|
11
|
-
.description('에이전트 팀 설치')
|
|
12
|
-
.
|
|
11
|
+
.description('에이전트 팀 설치 (현재 디렉토리의 .claude/에 설치)')
|
|
12
|
+
.option('--path <install_path>', '설치 경로 지정 (기본: ./.claude)')
|
|
13
|
+
.action(async (slug, opts) => {
|
|
13
14
|
const pretty = program.opts().pretty ?? false;
|
|
14
|
-
const
|
|
15
|
+
const installPath = (0, config_js_1.getInstallPath)(opts.path);
|
|
15
16
|
const tempDir = (0, storage_js_1.makeTempDir)();
|
|
16
17
|
try {
|
|
17
18
|
// 1. Fetch team metadata
|
|
@@ -22,7 +23,7 @@ function registerInstall(program) {
|
|
|
22
23
|
const extractDir = `${tempDir}/extracted`;
|
|
23
24
|
await (0, storage_js_1.extractPackage)(tarPath, extractDir);
|
|
24
25
|
// 4. Copy files to install_path
|
|
25
|
-
const files = (0, installer_js_1.installTeam)(extractDir,
|
|
26
|
+
const files = (0, installer_js_1.installTeam)(extractDir, installPath);
|
|
26
27
|
// 5. Record in installed.json
|
|
27
28
|
const installed = (0, config_js_1.loadInstalled)();
|
|
28
29
|
installed[slug] = {
|
|
@@ -40,10 +41,11 @@ function registerInstall(program) {
|
|
|
40
41
|
version: team.version,
|
|
41
42
|
commands: team.commands,
|
|
42
43
|
files_installed: files.length,
|
|
44
|
+
install_path: installPath,
|
|
43
45
|
};
|
|
44
46
|
if (pretty) {
|
|
45
47
|
console.log(`\n\x1b[32m✓ ${team.name} 설치 완료\x1b[0m v${team.version}`);
|
|
46
|
-
console.log(` 설치 위치: \x1b[36m${
|
|
48
|
+
console.log(` 설치 위치: \x1b[36m${installPath}\x1b[0m`);
|
|
47
49
|
console.log(` 파일 수: ${files.length}개`);
|
|
48
50
|
if (team.commands.length > 0) {
|
|
49
51
|
console.log('\n 사용 가능한 커맨드:');
|
package/dist/commands/login.js
CHANGED
|
@@ -24,9 +24,9 @@ function openBrowser(url) {
|
|
|
24
24
|
// ignore browser open errors
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
-
async function verifyToken(
|
|
27
|
+
async function verifyToken(token) {
|
|
28
28
|
try {
|
|
29
|
-
const res = await fetch(`${
|
|
29
|
+
const res = await fetch(`${config_js_1.API_URL}/api/auth/me`, {
|
|
30
30
|
headers: { Authorization: `Bearer ${token}` },
|
|
31
31
|
});
|
|
32
32
|
if (!res.ok)
|
|
@@ -43,7 +43,6 @@ function waitForToken(port) {
|
|
|
43
43
|
const url = new URL(req.url ?? '/', `http://localhost:${port}`);
|
|
44
44
|
if (url.pathname === '/callback') {
|
|
45
45
|
const token = url.searchParams.get('token');
|
|
46
|
-
// Send a nice HTML response that auto-closes
|
|
47
46
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
48
47
|
res.end(`<!DOCTYPE html>
|
|
49
48
|
<html><head><title>RelayAX</title></head>
|
|
@@ -98,13 +97,11 @@ function registerLogin(program) {
|
|
|
98
97
|
.action(async (opts) => {
|
|
99
98
|
const pretty = program.opts().pretty ?? false;
|
|
100
99
|
(0, config_js_1.ensureRelayDir)();
|
|
101
|
-
const config = (0, config_js_1.loadConfig)();
|
|
102
|
-
const apiUrl = config?.api_url ?? 'https://relayax.com';
|
|
103
100
|
let token = opts.token;
|
|
104
101
|
if (!token) {
|
|
105
102
|
try {
|
|
106
103
|
const port = await findAvailablePort();
|
|
107
|
-
const loginUrl = `${
|
|
104
|
+
const loginUrl = `${config_js_1.API_URL}/auth/cli-login?port=${port}`;
|
|
108
105
|
console.error('브라우저에서 로그인을 진행합니다...');
|
|
109
106
|
openBrowser(loginUrl);
|
|
110
107
|
token = await waitForToken(port);
|
|
@@ -115,13 +112,8 @@ function registerLogin(program) {
|
|
|
115
112
|
process.exit(1);
|
|
116
113
|
}
|
|
117
114
|
}
|
|
118
|
-
const user = await verifyToken(
|
|
119
|
-
|
|
120
|
-
install_path: config?.install_path ?? `${process.env.HOME}/.claude`,
|
|
121
|
-
api_url: apiUrl,
|
|
122
|
-
token,
|
|
123
|
-
};
|
|
124
|
-
(0, config_js_1.saveConfig)(updatedConfig);
|
|
115
|
+
const user = await verifyToken(token);
|
|
116
|
+
(0, config_js_1.saveToken)(token);
|
|
125
117
|
const result = {
|
|
126
118
|
status: 'ok',
|
|
127
119
|
message: '로그인 성공',
|
package/dist/commands/publish.js
CHANGED
|
@@ -7,16 +7,44 @@ exports.registerPublish = registerPublish;
|
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const os_1 = __importDefault(require("os"));
|
|
10
|
-
const readline_1 = require("readline");
|
|
11
10
|
const tar_1 = require("tar");
|
|
12
11
|
const config_js_1 = require("../lib/config.js");
|
|
13
12
|
const VALID_DIRS = ['skills', 'agents', 'rules', 'commands'];
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
.
|
|
13
|
+
function parseRelayYaml(content) {
|
|
14
|
+
const result = {};
|
|
15
|
+
const tags = [];
|
|
16
|
+
let inTags = false;
|
|
17
|
+
for (const line of content.split('\n')) {
|
|
18
|
+
const trimmed = line.trim();
|
|
19
|
+
if (inTags) {
|
|
20
|
+
if (trimmed.startsWith('- ')) {
|
|
21
|
+
tags.push(trimmed.slice(2).replace(/^["']|["']$/g, ''));
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
inTags = false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (trimmed === 'tags: []') {
|
|
29
|
+
result.tags = [];
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (trimmed === 'tags:') {
|
|
33
|
+
inTags = true;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const match = trimmed.match(/^(\w+):\s*["']?(.+?)["']?$/);
|
|
37
|
+
if (match) {
|
|
38
|
+
result[match[1]] = match[2];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
name: String(result.name ?? ''),
|
|
43
|
+
slug: String(result.slug ?? ''),
|
|
44
|
+
description: String(result.description ?? ''),
|
|
45
|
+
version: String(result.version ?? '1.0.0'),
|
|
46
|
+
tags,
|
|
47
|
+
};
|
|
20
48
|
}
|
|
21
49
|
function detectCommands(teamDir) {
|
|
22
50
|
const cmdDir = path_1.default.join(teamDir, 'commands');
|
|
@@ -30,7 +58,6 @@ function detectCommands(teamDir) {
|
|
|
30
58
|
try {
|
|
31
59
|
const content = fs_1.default.readFileSync(path_1.default.join(cmdDir, file), 'utf-8');
|
|
32
60
|
const lines = content.split('\n').map((l) => l.trim()).filter(Boolean);
|
|
33
|
-
// Check frontmatter for description
|
|
34
61
|
if (lines[0] === '---') {
|
|
35
62
|
const endIdx = lines.indexOf('---', 1);
|
|
36
63
|
if (endIdx > 0) {
|
|
@@ -41,7 +68,6 @@ function detectCommands(teamDir) {
|
|
|
41
68
|
}
|
|
42
69
|
}
|
|
43
70
|
else if (lines[0]) {
|
|
44
|
-
// Use first line, strip leading # if heading
|
|
45
71
|
description = lines[0].replace(/^#+\s*/, '');
|
|
46
72
|
}
|
|
47
73
|
}
|
|
@@ -58,34 +84,24 @@ function countDir(teamDir, dirName) {
|
|
|
58
84
|
return 0;
|
|
59
85
|
return fs_1.default.readdirSync(dirPath).filter((f) => !f.startsWith('.')).length;
|
|
60
86
|
}
|
|
61
|
-
async function prompt(question, defaultVal) {
|
|
62
|
-
const rl = (0, readline_1.createInterface)({ input: process.stdin, output: process.stderr });
|
|
63
|
-
const hint = defaultVal ? ` (기본값: ${defaultVal})` : '';
|
|
64
|
-
return new Promise((resolve) => {
|
|
65
|
-
rl.question(`${question}${hint}: `, (answer) => {
|
|
66
|
-
rl.close();
|
|
67
|
-
resolve(answer.trim() || defaultVal || '');
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
87
|
async function createTarball(teamDir) {
|
|
72
88
|
const tmpFile = path_1.default.join(os_1.default.tmpdir(), `relay-publish-${Date.now()}.tar.gz`);
|
|
73
|
-
|
|
74
|
-
const
|
|
89
|
+
// Only include valid dirs that exist
|
|
90
|
+
const dirsToInclude = VALID_DIRS.filter((d) => fs_1.default.existsSync(path_1.default.join(teamDir, d)));
|
|
75
91
|
await (0, tar_1.create)({
|
|
76
92
|
gzip: true,
|
|
77
93
|
file: tmpFile,
|
|
78
|
-
cwd:
|
|
79
|
-
}, [
|
|
94
|
+
cwd: teamDir,
|
|
95
|
+
}, [...dirsToInclude]);
|
|
80
96
|
return tmpFile;
|
|
81
97
|
}
|
|
82
|
-
async function publishToApi(
|
|
98
|
+
async function publishToApi(token, tarPath, metadata) {
|
|
83
99
|
const fileBuffer = fs_1.default.readFileSync(tarPath);
|
|
84
100
|
const blob = new Blob([fileBuffer], { type: 'application/gzip' });
|
|
85
101
|
const form = new FormData();
|
|
86
102
|
form.append('package', blob, `${metadata.slug}-${metadata.version}.tar.gz`);
|
|
87
103
|
form.append('metadata', JSON.stringify(metadata));
|
|
88
|
-
const res = await fetch(`${
|
|
104
|
+
const res = await fetch(`${config_js_1.API_URL}/api/publish`, {
|
|
89
105
|
method: 'POST',
|
|
90
106
|
headers: { Authorization: `Bearer ${token}` },
|
|
91
107
|
body: form,
|
|
@@ -99,73 +115,71 @@ async function publishToApi(apiUrl, token, tarPath, metadata) {
|
|
|
99
115
|
}
|
|
100
116
|
function registerPublish(program) {
|
|
101
117
|
program
|
|
102
|
-
.command('publish
|
|
103
|
-
.description('
|
|
104
|
-
.option('--
|
|
105
|
-
.
|
|
106
|
-
.option('--tag <tag>', '태그 (여러 번 사용 가능)', (val, prev) => [...prev, val], [])
|
|
107
|
-
.option('--token <token>', 'Supabase 인증 토큰')
|
|
108
|
-
.option('--slug <slug>', 'URL용 슬러그 (기본: 디렉토리명)')
|
|
109
|
-
.option('--version <ver>', '버전 (기본: 1.0.0)', '1.0.0')
|
|
110
|
-
.action(async (dir, opts) => {
|
|
118
|
+
.command('publish')
|
|
119
|
+
.description('현재 팀 패키지를 마켓플레이스에 배포합니다 (relay.yaml 필요)')
|
|
120
|
+
.option('--token <token>', '인증 토큰')
|
|
121
|
+
.action(async (opts) => {
|
|
111
122
|
const pretty = program.opts().pretty ?? false;
|
|
112
|
-
const teamDir =
|
|
113
|
-
|
|
114
|
-
|
|
123
|
+
const teamDir = process.cwd();
|
|
124
|
+
const relayYamlPath = path_1.default.join(teamDir, 'relay.yaml');
|
|
125
|
+
// Check relay.yaml exists
|
|
126
|
+
if (!fs_1.default.existsSync(relayYamlPath)) {
|
|
127
|
+
console.error(JSON.stringify({
|
|
128
|
+
error: 'NOT_INITIALIZED',
|
|
129
|
+
message: 'relay.yaml이 없습니다. 먼저 `relay init`을 실행하세요.',
|
|
130
|
+
}));
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
// Parse relay.yaml
|
|
134
|
+
const yamlContent = fs_1.default.readFileSync(relayYamlPath, 'utf-8');
|
|
135
|
+
const config = parseRelayYaml(yamlContent);
|
|
136
|
+
if (!config.slug || !config.name || !config.description) {
|
|
137
|
+
console.error(JSON.stringify({
|
|
138
|
+
error: 'INVALID_CONFIG',
|
|
139
|
+
message: 'relay.yaml에 name, slug, description이 필요합니다.',
|
|
140
|
+
}));
|
|
115
141
|
process.exit(1);
|
|
116
142
|
}
|
|
117
143
|
// Validate structure
|
|
118
|
-
const hasDirs = VALID_DIRS.some((d) =>
|
|
144
|
+
const hasDirs = VALID_DIRS.some((d) => {
|
|
145
|
+
const dirPath = path_1.default.join(teamDir, d);
|
|
146
|
+
if (!fs_1.default.existsSync(dirPath))
|
|
147
|
+
return false;
|
|
148
|
+
return fs_1.default.readdirSync(dirPath).filter((f) => !f.startsWith('.')).length > 0;
|
|
149
|
+
});
|
|
119
150
|
if (!hasDirs) {
|
|
120
151
|
console.error(JSON.stringify({
|
|
121
|
-
error: '
|
|
122
|
-
message:
|
|
152
|
+
error: 'EMPTY_PACKAGE',
|
|
153
|
+
message: 'skills/, agents/, rules/, commands/ 중 하나 이상에 파일이 있어야 합니다.',
|
|
123
154
|
}));
|
|
124
155
|
process.exit(1);
|
|
125
156
|
}
|
|
126
157
|
// Get token
|
|
127
|
-
const
|
|
128
|
-
const token = opts.token ?? process.env.RELAY_TOKEN ?? config.token;
|
|
158
|
+
const token = opts.token ?? process.env.RELAY_TOKEN ?? (0, config_js_1.loadToken)();
|
|
129
159
|
if (!token) {
|
|
130
160
|
console.error(JSON.stringify({
|
|
131
161
|
error: 'NO_TOKEN',
|
|
132
|
-
message: '
|
|
162
|
+
message: '인증이 필요합니다. `relay login`을 먼저 실행하세요.',
|
|
133
163
|
}));
|
|
134
164
|
process.exit(1);
|
|
135
165
|
}
|
|
136
|
-
// Auto-detect
|
|
137
|
-
const autoSlug = slugify(path_1.default.basename(teamDir));
|
|
138
166
|
const detectedCommands = detectCommands(teamDir);
|
|
139
167
|
const components = {
|
|
140
168
|
agents: countDir(teamDir, 'agents'),
|
|
141
169
|
rules: countDir(teamDir, 'rules'),
|
|
142
170
|
skills: countDir(teamDir, 'skills'),
|
|
143
171
|
};
|
|
144
|
-
// Gather metadata (prompt if not provided)
|
|
145
|
-
const isInteractive = process.stdin.isTTY;
|
|
146
|
-
const slug = opts.slug ?? (isInteractive ? await prompt('슬러그', autoSlug) : autoSlug);
|
|
147
|
-
const name = opts.name ?? (isInteractive ? await prompt('팀 이름', path_1.default.basename(teamDir)) : path_1.default.basename(teamDir));
|
|
148
|
-
const description = opts.description ?? (isInteractive ? await prompt('한 줄 설명') : '');
|
|
149
|
-
let tags = opts.tag;
|
|
150
|
-
if (tags.length === 0 && isInteractive) {
|
|
151
|
-
const tagsInput = await prompt('태그 (쉼표 구분)', '');
|
|
152
|
-
tags = tagsInput ? tagsInput.split(',').map((t) => t.trim()).filter(Boolean) : [];
|
|
153
|
-
}
|
|
154
|
-
if (!description) {
|
|
155
|
-
console.error(JSON.stringify({ error: 'MISSING_DESCRIPTION', message: 'description은 필수입니다' }));
|
|
156
|
-
process.exit(1);
|
|
157
|
-
}
|
|
158
172
|
const metadata = {
|
|
159
|
-
slug,
|
|
160
|
-
name,
|
|
161
|
-
description,
|
|
162
|
-
tags,
|
|
173
|
+
slug: config.slug,
|
|
174
|
+
name: config.name,
|
|
175
|
+
description: config.description,
|
|
176
|
+
tags: config.tags,
|
|
163
177
|
commands: detectedCommands,
|
|
164
178
|
components,
|
|
165
|
-
version:
|
|
179
|
+
version: config.version,
|
|
166
180
|
};
|
|
167
181
|
if (pretty) {
|
|
168
|
-
console.error(`패키지 생성
|
|
182
|
+
console.error(`패키지 생성 중... (${config.name} v${config.version})`);
|
|
169
183
|
}
|
|
170
184
|
let tarPath = null;
|
|
171
185
|
try {
|
|
@@ -173,9 +187,9 @@ function registerPublish(program) {
|
|
|
173
187
|
if (pretty) {
|
|
174
188
|
console.error(`업로드 중...`);
|
|
175
189
|
}
|
|
176
|
-
const result = await publishToApi(
|
|
190
|
+
const result = await publishToApi(token, tarPath, metadata);
|
|
177
191
|
if (pretty) {
|
|
178
|
-
console.log(`\n\x1b[32m✓ ${name} 배포 완료\x1b[0m v${result.version}`);
|
|
192
|
+
console.log(`\n\x1b[32m✓ ${config.name} 배포 완료\x1b[0m v${result.version}`);
|
|
179
193
|
console.log(` 슬러그: \x1b[36m${result.slug}\x1b[0m`);
|
|
180
194
|
console.log(` URL: \x1b[36m${result.url}\x1b[0m`);
|
|
181
195
|
}
|
package/dist/lib/api.js
CHANGED
|
@@ -5,8 +5,7 @@ exports.searchTeams = searchTeams;
|
|
|
5
5
|
exports.reportInstall = reportInstall;
|
|
6
6
|
const config_js_1 = require("./config.js");
|
|
7
7
|
async function fetchTeamInfo(slug) {
|
|
8
|
-
const
|
|
9
|
-
const url = `${config.api_url}/api/registry/${slug}`;
|
|
8
|
+
const url = `${config_js_1.API_URL}/api/registry/${slug}`;
|
|
10
9
|
const res = await fetch(url);
|
|
11
10
|
if (!res.ok) {
|
|
12
11
|
const body = await res.text();
|
|
@@ -15,11 +14,10 @@ async function fetchTeamInfo(slug) {
|
|
|
15
14
|
return res.json();
|
|
16
15
|
}
|
|
17
16
|
async function searchTeams(query, tag) {
|
|
18
|
-
const config = (0, config_js_1.ensureConfig)();
|
|
19
17
|
const params = new URLSearchParams({ q: query });
|
|
20
18
|
if (tag)
|
|
21
19
|
params.set('tag', tag);
|
|
22
|
-
const url = `${
|
|
20
|
+
const url = `${config_js_1.API_URL}/api/registry/search?${params.toString()}`;
|
|
23
21
|
const res = await fetch(url);
|
|
24
22
|
if (!res.ok) {
|
|
25
23
|
const body = await res.text();
|
|
@@ -29,8 +27,7 @@ async function searchTeams(query, tag) {
|
|
|
29
27
|
return data.results;
|
|
30
28
|
}
|
|
31
29
|
async function reportInstall(slug) {
|
|
32
|
-
const
|
|
33
|
-
const url = `${config.api_url}/api/registry/${slug}/install`;
|
|
30
|
+
const url = `${config_js_1.API_URL}/api/registry/${slug}/install`;
|
|
34
31
|
await fetch(url, { method: 'POST' }).catch(() => {
|
|
35
32
|
// non-critical: ignore errors
|
|
36
33
|
});
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare
|
|
3
|
-
export declare function ensureRelayDir(): void;
|
|
4
|
-
export declare function loadConfig(): RelayConfig | null;
|
|
5
|
-
export declare function saveConfig(config: RelayConfig): void;
|
|
6
|
-
export declare function requireConfig(): RelayConfig;
|
|
1
|
+
import type { InstalledRegistry } from '../types.js';
|
|
2
|
+
export declare const API_URL = "https://relayax.com";
|
|
7
3
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* 현재 디렉토리의 .claude/ 를 기본 설치 경로로 사용한다.
|
|
5
|
+
* --path 옵션으로 오버라이드 가능.
|
|
10
6
|
*/
|
|
11
|
-
export declare function
|
|
7
|
+
export declare function getInstallPath(override?: string): string;
|
|
8
|
+
export declare function ensureRelayDir(): void;
|
|
9
|
+
export declare function loadToken(): string | undefined;
|
|
10
|
+
export declare function saveToken(token: string): void;
|
|
12
11
|
export declare function loadInstalled(): InstalledRegistry;
|
|
13
12
|
export declare function saveInstalled(registry: InstalledRegistry): void;
|
package/dist/lib/config.js
CHANGED
|
@@ -3,75 +3,51 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.API_URL = void 0;
|
|
7
|
+
exports.getInstallPath = getInstallPath;
|
|
7
8
|
exports.ensureRelayDir = ensureRelayDir;
|
|
8
|
-
exports.
|
|
9
|
-
exports.
|
|
10
|
-
exports.requireConfig = requireConfig;
|
|
11
|
-
exports.ensureConfig = ensureConfig;
|
|
9
|
+
exports.loadToken = loadToken;
|
|
10
|
+
exports.saveToken = saveToken;
|
|
12
11
|
exports.loadInstalled = loadInstalled;
|
|
13
12
|
exports.saveInstalled = saveInstalled;
|
|
14
13
|
const fs_1 = __importDefault(require("fs"));
|
|
15
14
|
const path_1 = __importDefault(require("path"));
|
|
16
15
|
const os_1 = __importDefault(require("os"));
|
|
17
|
-
|
|
18
|
-
const DEFAULT_INSTALL_PATH = path_1.default.join(os_1.default.homedir(), '.claude');
|
|
16
|
+
exports.API_URL = 'https://relayax.com';
|
|
19
17
|
const RELAY_DIR = path_1.default.join(os_1.default.homedir(), '.relay');
|
|
20
|
-
const CONFIG_FILE = path_1.default.join(RELAY_DIR, 'config.json');
|
|
21
18
|
const INSTALLED_FILE = path_1.default.join(RELAY_DIR, 'installed.json');
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
/**
|
|
20
|
+
* 현재 디렉토리의 .claude/ 를 기본 설치 경로로 사용한다.
|
|
21
|
+
* --path 옵션으로 오버라이드 가능.
|
|
22
|
+
*/
|
|
23
|
+
function getInstallPath(override) {
|
|
24
|
+
if (override) {
|
|
25
|
+
const resolved = override.startsWith('~')
|
|
26
|
+
? path_1.default.join(os_1.default.homedir(), override.slice(1))
|
|
27
|
+
: path_1.default.resolve(override);
|
|
28
|
+
return resolved;
|
|
29
|
+
}
|
|
30
|
+
return path_1.default.join(process.cwd(), '.claude');
|
|
24
31
|
}
|
|
25
32
|
function ensureRelayDir() {
|
|
26
33
|
if (!fs_1.default.existsSync(RELAY_DIR)) {
|
|
27
34
|
fs_1.default.mkdirSync(RELAY_DIR, { recursive: true });
|
|
28
35
|
}
|
|
29
36
|
}
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
37
|
+
function loadToken() {
|
|
38
|
+
const tokenFile = path_1.default.join(RELAY_DIR, 'token');
|
|
39
|
+
if (!fs_1.default.existsSync(tokenFile))
|
|
40
|
+
return undefined;
|
|
34
41
|
try {
|
|
35
|
-
|
|
36
|
-
return JSON.parse(raw);
|
|
42
|
+
return fs_1.default.readFileSync(tokenFile, 'utf-8').trim() || undefined;
|
|
37
43
|
}
|
|
38
44
|
catch {
|
|
39
|
-
return
|
|
45
|
+
return undefined;
|
|
40
46
|
}
|
|
41
47
|
}
|
|
42
|
-
function
|
|
48
|
+
function saveToken(token) {
|
|
43
49
|
ensureRelayDir();
|
|
44
|
-
fs_1.default.writeFileSync(
|
|
45
|
-
}
|
|
46
|
-
function requireConfig() {
|
|
47
|
-
const config = loadConfig();
|
|
48
|
-
if (!config) {
|
|
49
|
-
console.error(JSON.stringify({
|
|
50
|
-
error: 'NOT_INITIALIZED',
|
|
51
|
-
message: 'relay가 초기화되지 않았습니다. `relay init`을 먼저 실행하세요.',
|
|
52
|
-
}));
|
|
53
|
-
process.exit(1);
|
|
54
|
-
}
|
|
55
|
-
return config;
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* npx 첫 실행처럼 config가 없을 때 기본값으로 자동 초기화한다.
|
|
59
|
-
* 이미 config가 있으면 그대로 반환한다.
|
|
60
|
-
*/
|
|
61
|
-
function ensureConfig() {
|
|
62
|
-
const existing = loadConfig();
|
|
63
|
-
if (existing)
|
|
64
|
-
return existing;
|
|
65
|
-
const config = {
|
|
66
|
-
install_path: DEFAULT_INSTALL_PATH,
|
|
67
|
-
api_url: DEFAULT_API_URL,
|
|
68
|
-
};
|
|
69
|
-
if (!fs_1.default.existsSync(config.install_path)) {
|
|
70
|
-
fs_1.default.mkdirSync(config.install_path, { recursive: true });
|
|
71
|
-
}
|
|
72
|
-
saveConfig(config);
|
|
73
|
-
console.error(`relay 초기 설정 완료 (${config.install_path}에 설치)`);
|
|
74
|
-
return config;
|
|
50
|
+
fs_1.default.writeFileSync(path_1.default.join(RELAY_DIR, 'token'), token);
|
|
75
51
|
}
|
|
76
52
|
function loadInstalled() {
|
|
77
53
|
if (!fs_1.default.existsSync(INSTALLED_FILE)) {
|
package/dist/types.d.ts
CHANGED