relayax-cli 0.1.2 → 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.
@@ -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 config_js_1 = require("../lib/config.js");
12
- const DEFAULT_API_URL = 'https://relayax.com';
13
- const PRESET_PATHS = {
14
- '1': path_1.default.join(os_1.default.homedir(), '.claude'),
15
- '2': path_1.default.join(os_1.default.homedir(), '.gemini'),
16
- };
17
- function prompt(rl, question) {
18
- return new Promise((resolve) => rl.question(question, resolve));
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('relayax 초기화 설치 경로 설정')
24
- .option('--path <install_path>', '설치 경로 직접 지정')
25
- .option('--api-url <url>', `API URL (기본값: ${DEFAULT_API_URL})`)
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 api_url = opts.apiUrl ?? DEFAULT_API_URL;
29
- let install_path;
30
- if (opts.path) {
31
- install_path = opts.path;
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
- else if (!pretty) {
34
- // non-pretty / agent mode: use default
35
- install_path = PRESET_PATHS['1'];
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
- input: process.stdin,
40
- output: process.stdout,
41
- });
42
- console.log('\n설치 경로를 선택하세요:');
43
- console.log(' 1) ~/.claude/ (Claude Code) [기본값]');
44
- console.log(' 2) ~/.gemini/ (Gemini CLI)');
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
- // Resolve ~ in custom paths
56
- if (install_path.startsWith('~')) {
57
- install_path = path_1.default.join(os_1.default.homedir(), install_path.slice(1));
96
+ if (!description) {
97
+ console.error(JSON.stringify({ error: 'MISSING_DESCRIPTION', message: 'description은 필수입니다' }));
98
+ process.exit(1);
58
99
  }
59
- // Ensure install path exists
60
- if (!fs_1.default.existsSync(install_path)) {
61
- fs_1.default.mkdirSync(install_path, { recursive: true });
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
- (0, config_js_1.ensureRelayDir)();
64
- (0, config_js_1.saveConfig)({ install_path, api_url });
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
- install_path,
68
- api_url,
113
+ slug,
114
+ name,
115
+ description,
116
+ existing_dirs: existing,
117
+ created_dirs: created,
69
118
  };
70
119
  if (pretty) {
71
- console.log('\n\x1b[32m✓ relay 초기화 완료\x1b[0m');
72
- console.log(` 설치 경로: \x1b[36m${install_path}\x1b[0m`);
73
- console.log(` API URL: \x1b[36m${api_url}\x1b[0m`);
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));
@@ -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 slugify(str) {
15
- return str
16
- .toLowerCase()
17
- .replace(/[^a-z0-9-]/g, '-')
18
- .replace(/-+/g, '-')
19
- .replace(/^-|-$/g, '');
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
- const parent = path_1.default.dirname(teamDir);
74
- const dirName = path_1.default.basename(teamDir);
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: parent,
79
- }, [dirName]);
94
+ cwd: teamDir,
95
+ }, [...dirsToInclude]);
80
96
  return tmpFile;
81
97
  }
82
- async function publishToApi(apiUrl, token, tarPath, metadata) {
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(`${apiUrl}/api/publish`, {
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,27 +115,42 @@ async function publishToApi(apiUrl, token, tarPath, metadata) {
99
115
  }
100
116
  function registerPublish(program) {
101
117
  program
102
- .command('publish [dir]')
103
- .description('에이전트 팀을 마켓플레이스에 배포합니다')
104
- .option('--name <name>', ' 표시명')
105
- .option('--description <desc>', '한 줄 설명')
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 = path_1.default.resolve(dir ?? process.cwd());
113
- if (!fs_1.default.existsSync(teamDir) || !fs_1.default.statSync(teamDir).isDirectory()) {
114
- console.error(JSON.stringify({ error: 'INVALID_DIR', message: `디렉토리를 찾을 수 없습니다: ${teamDir}` }));
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) => fs_1.default.existsSync(path_1.default.join(teamDir, 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: 'INVALID_STRUCTURE',
122
- message: `팀 디렉토리에는 skills/, agents/, rules/, commands/ 중 하나 이상이 있어야 합니다`,
152
+ error: 'EMPTY_PACKAGE',
153
+ message: 'skills/, agents/, rules/, commands/ 중 하나 이상에 파일이 있어야 합니다.',
123
154
  }));
124
155
  process.exit(1);
125
156
  }
@@ -128,43 +159,27 @@ function registerPublish(program) {
128
159
  if (!token) {
129
160
  console.error(JSON.stringify({
130
161
  error: 'NO_TOKEN',
131
- message: '인증 토큰이 필요합니다. --token 플래그, RELAY_TOKEN 환경변수, 또는 `relay login`을 사용하세요.',
162
+ message: '인증이 필요합니다. `relay login`을 먼저 실행하세요.',
132
163
  }));
133
164
  process.exit(1);
134
165
  }
135
- // Auto-detect
136
- const autoSlug = slugify(path_1.default.basename(teamDir));
137
166
  const detectedCommands = detectCommands(teamDir);
138
167
  const components = {
139
168
  agents: countDir(teamDir, 'agents'),
140
169
  rules: countDir(teamDir, 'rules'),
141
170
  skills: countDir(teamDir, 'skills'),
142
171
  };
143
- // Gather metadata (prompt if not provided)
144
- const isInteractive = process.stdin.isTTY;
145
- const slug = opts.slug ?? (isInteractive ? await prompt('슬러그', autoSlug) : autoSlug);
146
- const name = opts.name ?? (isInteractive ? await prompt('팀 이름', path_1.default.basename(teamDir)) : path_1.default.basename(teamDir));
147
- const description = opts.description ?? (isInteractive ? await prompt('한 줄 설명') : '');
148
- let tags = opts.tag;
149
- if (tags.length === 0 && isInteractive) {
150
- const tagsInput = await prompt('태그 (쉼표 구분)', '');
151
- tags = tagsInput ? tagsInput.split(',').map((t) => t.trim()).filter(Boolean) : [];
152
- }
153
- if (!description) {
154
- console.error(JSON.stringify({ error: 'MISSING_DESCRIPTION', message: 'description은 필수입니다' }));
155
- process.exit(1);
156
- }
157
172
  const metadata = {
158
- slug,
159
- name,
160
- description,
161
- tags,
173
+ slug: config.slug,
174
+ name: config.name,
175
+ description: config.description,
176
+ tags: config.tags,
162
177
  commands: detectedCommands,
163
178
  components,
164
- version: opts.version,
179
+ version: config.version,
165
180
  };
166
181
  if (pretty) {
167
- console.error(`패키지 생성 중...`);
182
+ console.error(`패키지 생성 중... (${config.name} v${config.version})`);
168
183
  }
169
184
  let tarPath = null;
170
185
  try {
@@ -172,9 +187,9 @@ function registerPublish(program) {
172
187
  if (pretty) {
173
188
  console.error(`업로드 중...`);
174
189
  }
175
- const result = await publishToApi(config_js_1.API_URL, token, tarPath, metadata);
190
+ const result = await publishToApi(token, tarPath, metadata);
176
191
  if (pretty) {
177
- console.log(`\n\x1b[32m✓ ${name} 배포 완료\x1b[0m v${result.version}`);
192
+ console.log(`\n\x1b[32m✓ ${config.name} 배포 완료\x1b[0m v${result.version}`);
178
193
  console.log(` 슬러그: \x1b[36m${result.slug}\x1b[0m`);
179
194
  console.log(` URL: \x1b[36m${result.url}\x1b[0m`);
180
195
  }
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const commander_1 = require("commander");
5
+ const init_js_1 = require("./commands/init.js");
5
6
  const search_js_1 = require("./commands/search.js");
6
7
  const install_js_1 = require("./commands/install.js");
7
8
  const list_js_1 = require("./commands/list.js");
@@ -16,6 +17,7 @@ program
16
17
  .description('RelayAX Agent Team Marketplace CLI')
17
18
  .version(pkg.version)
18
19
  .option('--pretty', '인간 친화적 출력 (기본값: JSON)');
20
+ (0, init_js_1.registerInit)(program);
19
21
  (0, search_js_1.registerSearch)(program);
20
22
  (0, install_js_1.registerInstall)(program);
21
23
  (0, list_js_1.registerList)(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relayax-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "RelayAX Agent Team Marketplace CLI - Install and manage agent teams",
5
5
  "main": "dist/index.js",
6
6
  "bin": {