relayax-cli 0.3.65 → 0.3.67

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.
@@ -1,13 +1,9 @@
1
1
  import { Command } from 'commander';
2
- /**
3
- * 글로벌 User 커맨드를 감지된 모든 에이전트 CLI에 설치한다.
4
- * ~/{skillsDir}/commands/relay/ 에 설치.
5
- * 기존 파일 중 현재 커맨드 목록에 없는 것은 제거한다.
6
- */
7
2
  export declare function installGlobalUserCommands(): {
8
3
  installed: boolean;
9
4
  commands: string[];
10
5
  tools: string[];
6
+ removed: string[];
11
7
  };
12
8
  /**
13
9
  * 글로벌 User 커맨드가 이미 설치되어 있는지 확인한다.
@@ -36,10 +36,15 @@ function showWelcome() {
36
36
  ' 에이전트 CLI에 relay 커맨드를 연결합니다.',
37
37
  '',
38
38
  ' \x1b[2mUser 커맨드 (글로벌)\x1b[0m',
39
- ' /relay-install 에이전트 탐색 & 설치',
40
- ' /relay-status 설치 현황 & Space',
39
+ ' /relay-explore 에이전트 탐색 & 추천',
40
+ ' /relay-create 에이전트 생성 & 배포',
41
+ ' /relay-status 설치 현황 & Organization',
41
42
  ' /relay-uninstall 에이전트 삭제',
42
43
  '',
44
+ ' \x1b[2mCLI 명령어\x1b[0m',
45
+ ' relay install 에이전트 설치 (CLI 한 줄 완결)',
46
+ ' relay publish 재배포 (--patch/--minor/--major)',
47
+ '',
43
48
  ];
44
49
  console.log(lines.join('\n'));
45
50
  }
@@ -65,20 +70,28 @@ async function selectToolsInteractively(detectedIds) {
65
70
  * ~/{skillsDir}/commands/relay/ 에 설치.
66
71
  * 기존 파일 중 현재 커맨드 목록에 없는 것은 제거한다.
67
72
  */
73
+ /** 제거된 레거시 커맨드 → 대체 안내 매핑 */
74
+ const LEGACY_COMMANDS = {
75
+ 'relay-install': 'relay install (CLI) 또는 /relay-explore',
76
+ 'relay-publish': 'relay publish --patch (CLI) 또는 /relay-create',
77
+ };
68
78
  function installGlobalUserCommands() {
69
79
  const globalCLIs = (0, ai_tools_js_1.detectGlobalCLIs)();
70
80
  const currentIds = new Set(command_adapter_js_1.USER_COMMANDS.map((c) => c.id));
71
81
  const commands = [];
72
82
  const tools = [];
73
- // 감지된 CLI가 없으면 설치하지 않음 (사용자가 --tools로 지정하거나 CLI를 먼저 설치해야 함)
83
+ const removed = [];
74
84
  const targetDirs = globalCLIs.map((t) => ({ name: t.name, dir: (0, command_adapter_js_1.getGlobalCommandDirForTool)(t.skillsDir), getPath: (id) => (0, command_adapter_js_1.getGlobalCommandPathForTool)(t.skillsDir, id) }));
75
85
  for (const target of targetDirs) {
76
86
  fs_1.default.mkdirSync(target.dir, { recursive: true });
77
- // 기존 파일 중 현재 목록에 없는 것 제거
87
+ // 기존 파일 중 현재 목록에 없는 것 제거 + 레거시 안내
78
88
  for (const file of fs_1.default.readdirSync(target.dir)) {
79
89
  const id = file.replace(/\.md$/, '');
80
90
  if (!currentIds.has(id)) {
81
91
  fs_1.default.unlinkSync(path_1.default.join(target.dir, file));
92
+ if (LEGACY_COMMANDS[id] && !removed.includes(id)) {
93
+ removed.push(id);
94
+ }
82
95
  }
83
96
  }
84
97
  // 현재 커맨드 설치 (덮어쓰기)
@@ -87,11 +100,10 @@ function installGlobalUserCommands() {
87
100
  }
88
101
  tools.push(target.name);
89
102
  }
90
- // commands 목록은 한 번만
91
103
  for (const cmd of command_adapter_js_1.USER_COMMANDS) {
92
104
  commands.push(cmd.id);
93
105
  }
94
- return { installed: true, commands, tools };
106
+ return { installed: true, commands, tools, removed };
95
107
  }
96
108
  /**
97
109
  * 글로벌 User 커맨드가 이미 설치되어 있는지 확인한다.
@@ -149,10 +161,12 @@ function registerInit(program) {
149
161
  // ── 1. 글로벌 User 커맨드 설치 ──
150
162
  let globalStatus = 'already';
151
163
  let globalTools = [];
164
+ let removedCommands = [];
152
165
  {
153
166
  const result = installGlobalUserCommands();
154
167
  globalStatus = hasGlobalUserCommands() ? 'updated' : 'installed';
155
168
  globalTools = result.tools;
169
+ removedCommands = result.removed;
156
170
  // Register relay-core in installed.json
157
171
  const installed = (0, config_js_1.loadInstalled)();
158
172
  installed['relay-core'] = {
@@ -245,6 +259,14 @@ function registerInit(program) {
245
259
  }
246
260
  else {
247
261
  console.log(`\n\x1b[32m✓ relay 초기화 완료\x1b[0m\n`);
262
+ // 레거시 커맨드 마이그레이션 안내
263
+ if (removedCommands.length > 0) {
264
+ console.log(` \x1b[33m⚠ 변경된 커맨드:\x1b[0m`);
265
+ for (const id of removedCommands) {
266
+ console.log(` \x1b[31m✗ /${id}\x1b[0m → ${LEGACY_COMMANDS[id]}`);
267
+ }
268
+ console.log();
269
+ }
248
270
  // 글로벌
249
271
  {
250
272
  const toolNames = globalTools.length > 0 ? globalTools.join(', ') : '(감지된 CLI 없음)';
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.registerInstall = registerInstall;
7
7
  const fs_1 = __importDefault(require("fs"));
8
+ const os_1 = __importDefault(require("os"));
8
9
  const path_1 = __importDefault(require("path"));
9
10
  const api_js_1 = require("../lib/api.js");
10
11
  const storage_js_1 = require("../lib/storage.js");
@@ -15,11 +16,15 @@ const init_js_1 = require("./init.js");
15
16
  const paths_js_1 = require("../lib/paths.js");
16
17
  const error_report_js_1 = require("../lib/error-report.js");
17
18
  const step_tracker_js_1 = require("../lib/step-tracker.js");
19
+ const installer_js_1 = require("../lib/installer.js");
18
20
  function registerInstall(program) {
19
21
  program
20
22
  .command('install <slug>')
21
23
  .description('에이전트 패키지를 .relay/agents/에 다운로드합니다')
22
24
  .option('--join-code <code>', '초대 코드 (Organization 에이전트 설치 시 자동 가입)')
25
+ .option('--code <code>', '접근 코드 (private 에이전트 설치 시)')
26
+ .option('--global', '글로벌 설치 (홈 디렉토리)')
27
+ .option('--local', '로컬 설치 (프로젝트 디렉토리)')
23
28
  .option('--project <dir>', '프로젝트 루트 경로 (기본: cwd, 환경변수: RELAY_PROJECT_PATH)')
24
29
  .action(async (slugInput, _opts) => {
25
30
  const json = program.opts().json ?? false;
@@ -45,6 +50,39 @@ function registerInstall(program) {
45
50
  const actualSlugInput = versionMatch ? versionMatch[1] : slugInput;
46
51
  parsed = await (0, slug_js_1.resolveSlug)(actualSlugInput);
47
52
  slug = parsed.full;
53
+ // Helper: ensure a valid token exists, triggering auto-login in TTY if needed.
54
+ // Returns the token string or null if login failed / not available.
55
+ async function ensureToken() {
56
+ let token = await (0, config_js_1.getValidToken)();
57
+ if (!token) {
58
+ const isTTY = Boolean(process.stdin.isTTY);
59
+ if (isTTY && !json) {
60
+ console.error('\x1b[33m⚙ 이 에이전트는 로그인이 필요합니다. 로그인을 시작합니다...\x1b[0m');
61
+ const { runLogin } = await import('./login.js');
62
+ await runLogin();
63
+ token = await (0, config_js_1.getValidToken)();
64
+ }
65
+ }
66
+ return token ?? null;
67
+ }
68
+ // Pre-fetch auto-login: --join-code and --code always require auth.
69
+ if (_opts.joinCode || _opts.code) {
70
+ const token = await ensureToken();
71
+ if (!token) {
72
+ if (json) {
73
+ console.error(JSON.stringify({
74
+ error: 'LOGIN_REQUIRED',
75
+ slug,
76
+ message: '이 에이전트는 로그인이 필요합니다. relay login을 먼저 실행하세요.',
77
+ fix: 'relay login 실행 후 재시도하세요.',
78
+ }));
79
+ }
80
+ else {
81
+ console.error('\x1b[31m이 에이전트는 로그인이 필요합니다. relay login 을 먼저 실행하세요.\x1b[0m');
82
+ }
83
+ process.exit(1);
84
+ }
85
+ }
48
86
  try {
49
87
  agent = await (0, api_js_1.fetchAgentInfo)(slug);
50
88
  }
@@ -64,15 +102,63 @@ function registerInstall(program) {
64
102
  }
65
103
  }
66
104
  catch { /* ignore parse errors */ }
67
- // Private agent: show purchase info + relay access hint
68
- if (errorVisibility === 'private' || purchaseInfo) {
105
+ // Task 2.1: --join-code provided and not yet a member → join org then retry
106
+ if (_opts.joinCode && membershipStatus !== 'member') {
107
+ if (!json) {
108
+ console.error('\x1b[33m⚙ 초대 코드로 Organization에 가입합니다...\x1b[0m');
109
+ }
110
+ const { joinOrg } = await import('./join.js');
111
+ await joinOrg(parsed.owner, _opts.joinCode);
112
+ agent = await (0, api_js_1.fetchAgentInfo)(slug);
113
+ }
114
+ // Task 2.2: --code provided and agent is private → claim access then retry
115
+ else if (_opts.code && (errorVisibility === 'private' || purchaseInfo)) {
116
+ if (!json) {
117
+ console.error('\x1b[33m⚙ 접근 코드로 에이전트 접근 권한을 요청합니다...\x1b[0m');
118
+ }
119
+ const token = await (0, config_js_1.getValidToken)();
120
+ if (!token) {
121
+ if (json) {
122
+ console.error(JSON.stringify({
123
+ error: 'LOGIN_REQUIRED',
124
+ slug,
125
+ message: '이 에이전트는 로그인이 필요합니다. relay login을 먼저 실행하세요.',
126
+ fix: 'relay login 실행 후 재시도하세요.',
127
+ }));
128
+ }
129
+ else {
130
+ console.error('\x1b[31m이 에이전트는 로그인이 필요합니다. relay login 을 먼저 실행하세요.\x1b[0m');
131
+ }
132
+ process.exit(1);
133
+ }
134
+ const claimRes = await fetch(`${config_js_1.API_URL}/api/agents/${slug}/claim-access`, {
135
+ method: 'POST',
136
+ headers: {
137
+ 'Content-Type': 'application/json',
138
+ Authorization: `Bearer ${token}`,
139
+ },
140
+ body: JSON.stringify({ code: _opts.code }),
141
+ signal: AbortSignal.timeout(10000),
142
+ });
143
+ if (!claimRes.ok) {
144
+ const claimBody = (await claimRes.json().catch(() => ({})));
145
+ const claimErrCode = claimBody.error ?? String(claimRes.status);
146
+ if (claimErrCode === 'INVALID_LINK')
147
+ throw new Error('초대 링크가 유효하지 않거나 만료되었습니다.');
148
+ throw new Error(claimBody.message ?? `접근 권한 요청 실패 (${claimRes.status})`);
149
+ }
150
+ agent = await (0, api_js_1.fetchAgentInfo)(slug);
151
+ }
152
+ // No code provided: show appropriate error messages
153
+ else if (errorVisibility === 'private' || purchaseInfo) {
154
+ // Private agent: show purchase info + relay access hint
69
155
  if (json) {
70
156
  console.error(JSON.stringify({
71
157
  error: 'ACCESS_REQUIRED',
72
158
  message: '이 에이전트는 접근 권한이 필요합니다.',
73
159
  slug,
74
160
  purchase_info: purchaseInfo ?? null,
75
- fix: '접근 링크 코드가 있으면: relay access <slug> --code <코드>',
161
+ fix: '접근 링크 코드가 있으면: relay install ' + slugInput + ' --code <코드>',
76
162
  }));
77
163
  }
78
164
  else {
@@ -83,18 +169,18 @@ function registerInstall(program) {
83
169
  if (purchaseInfo?.url) {
84
170
  console.error(` \x1b[36m${purchaseInfo.url}\x1b[0m`);
85
171
  }
86
- console.error(`\n\x1b[33m접근 링크 코드가 있으면: relay access ${slugInput} --code <코드>\x1b[0m`);
172
+ console.error(`\n\x1b[33m접근 링크 코드가 있으면: relay install ${slugInput} --code <코드>\x1b[0m`);
87
173
  }
88
174
  process.exit(1);
89
175
  }
90
- if (membershipStatus === 'member') {
176
+ else if (membershipStatus === 'member') {
91
177
  // Member but no access to this specific agent
92
178
  if (json) {
93
179
  console.error(JSON.stringify({
94
180
  error: 'NO_ACCESS',
95
181
  message: '이 에이전트에 대한 접근 권한이 없습니다.',
96
182
  slug,
97
- fix: '이 에이전트의 접근 링크 코드가 있으면 `relay access ' + slugInput + ' --code <코드>`로 접근 권한을 얻으세요. 없으면 에이전트 제작자에게 문의하세요.',
183
+ fix: '이 에이전트의 접근 링크 코드가 있으면 `relay install ' + slugInput + ' --code <코드>`로 접근 권한을 얻으세요. 없으면 에이전트 제작자에게 문의하세요.',
98
184
  }));
99
185
  }
100
186
  else {
@@ -108,12 +194,12 @@ function registerInstall(program) {
108
194
  error: 'ACCESS_REQUIRED',
109
195
  message: '이 에이전트는 접근 권한이 필요합니다.',
110
196
  slug,
111
- fix: '초대 코드가 있으면 `relay join <org-slug> --code <코드>`로 가입하세요.',
197
+ fix: '초대 코드가 있으면 `relay install ' + slugInput + ' --join-code <코드>`로 가입하세요.',
112
198
  }));
113
199
  }
114
200
  else {
115
201
  console.error('\x1b[31m이 에이전트는 접근 권한이 필요합니다.\x1b[0m');
116
- console.error('\x1b[33m초대 코드가 있으면 `relay join <org-slug> --code <코드>`로 가입하세요.\x1b[0m');
202
+ console.error('\x1b[33m초대 코드가 있으면 `relay install ' + slugInput + ' --join-code <코드>`로 가입하세요.\x1b[0m');
117
203
  }
118
204
  process.exit(1);
119
205
  }
@@ -126,35 +212,32 @@ function registerInstall(program) {
126
212
  throw new Error('에이전트 정보를 가져오지 못했습니다.');
127
213
  // Re-bind as non-optional so TypeScript tracks the narrowing through nested scopes
128
214
  let resolvedAgent = agent;
129
- const agentDir = path_1.default.join(projectPath, '.relay', 'agents', parsed.owner, parsed.name);
130
- // 2. Visibility check + auto-login
215
+ // Scope 자동결정: --global/--local 플래그 > agent_type 기반
216
+ const scope = _opts.global ? 'global'
217
+ : _opts.local ? 'local'
218
+ : resolvedAgent.type === 'passive' ? 'local'
219
+ : 'global';
220
+ const agentDir = scope === 'global'
221
+ ? path_1.default.join(os_1.default.homedir(), '.relay', 'agents', parsed.owner, parsed.name)
222
+ : path_1.default.join(projectPath, '.relay', 'agents', parsed.owner, parsed.name);
223
+ // 2. Visibility check + auto-login (internal agents always require a token)
131
224
  const visibility = resolvedAgent.visibility ?? 'public';
132
225
  if (visibility === 'internal') {
133
- let token = await (0, config_js_1.getValidToken)();
226
+ const token = await ensureToken();
134
227
  if (!token) {
135
- const isTTY = Boolean(process.stdin.isTTY);
136
- if (isTTY && !json) {
137
- // Auto-login: TTY 환경에서 자동으로 login 플로우 트리거
138
- console.error('\x1b[33m⚙ 이 에이전트는 로그인이 필요합니다. 로그인을 시작합니다...\x1b[0m');
139
- const { runLogin } = await import('./login.js');
140
- await runLogin();
141
- token = await (0, config_js_1.getValidToken)();
228
+ if (json) {
229
+ console.error(JSON.stringify({
230
+ error: 'LOGIN_REQUIRED',
231
+ visibility,
232
+ slug,
233
+ message: '이 에이전트는 로그인이 필요합니다. relay login을 먼저 실행하세요.',
234
+ fix: 'relay login 실행 후 재시도하세요.',
235
+ }));
142
236
  }
143
- if (!token) {
144
- if (json) {
145
- console.error(JSON.stringify({
146
- error: 'LOGIN_REQUIRED',
147
- visibility,
148
- slug,
149
- message: '이 에이전트는 로그인이 필요합니다. relay login을 먼저 실행하세요.',
150
- fix: 'relay login 실행 후 재시도하세요.',
151
- }));
152
- }
153
- else {
154
- console.error('\x1b[31m이 에이전트는 로그인이 필요합니다. relay login 을 먼저 실행하세요.\x1b[0m');
155
- }
156
- process.exit(1);
237
+ else {
238
+ console.error('\x1b[31m이 에이전트는 로그인이 필요합니다. relay login 을 먼저 실행하세요.\x1b[0m');
157
239
  }
240
+ process.exit(1);
158
241
  }
159
242
  }
160
243
  // 3. Download package (retry once if signed URL expired)
@@ -184,7 +267,13 @@ function registerInstall(program) {
184
267
  await (0, storage_js_1.extractPackage)(tarPath, agentDir);
185
268
  // 4.5. Inject preamble (update check) into SKILL.md and commands
186
269
  (0, preamble_js_1.injectPreambleToAgent)(agentDir, slug);
187
- // 5. Count extracted files
270
+ // 5. Deploy symlinks to detected AI tool directories
271
+ const deploy = (0, installer_js_1.deploySymlinks)(agentDir, scope, projectPath);
272
+ for (const w of deploy.warnings) {
273
+ if (!json)
274
+ console.error(`\x1b[33m${w}\x1b[0m`);
275
+ }
276
+ // 6. Count extracted files
188
277
  function countFiles(dir) {
189
278
  let count = 0;
190
279
  if (!fs_1.default.existsSync(dir))
@@ -200,16 +289,26 @@ function registerInstall(program) {
200
289
  return count;
201
290
  }
202
291
  const fileCount = countFiles(agentDir);
203
- // 6. Record in installed.json
204
- const installed = (0, config_js_1.loadInstalled)();
205
- installed[slug] = {
292
+ // 7. Record in installed.json (scope-aware)
293
+ const installRecord = {
206
294
  agent_id: resolvedAgent.id,
207
295
  version: resolvedAgent.version,
208
296
  installed_at: new Date().toISOString(),
209
297
  files: [agentDir],
298
+ deploy_scope: scope,
299
+ deployed_symlinks: deploy.symlinks,
210
300
  };
211
- (0, config_js_1.saveInstalled)(installed);
212
- // 7. Report install + usage ping (non-blocking, agent_id 기반)
301
+ if (scope === 'global') {
302
+ const globalInstalled = (0, config_js_1.loadGlobalInstalled)();
303
+ globalInstalled[slug] = installRecord;
304
+ (0, config_js_1.saveGlobalInstalled)(globalInstalled);
305
+ }
306
+ else {
307
+ const installed = (0, config_js_1.loadInstalled)();
308
+ installed[slug] = installRecord;
309
+ (0, config_js_1.saveInstalled)(installed);
310
+ }
311
+ // 8. Report install + usage ping (non-blocking, agent_id 기반)
213
312
  await (0, api_js_1.reportInstall)(resolvedAgent.id, slug, resolvedAgent.version);
214
313
  (0, api_js_1.sendUsagePing)(resolvedAgent.id, slug, resolvedAgent.version);
215
314
  const result = {
@@ -220,6 +319,8 @@ function registerInstall(program) {
220
319
  commands: resolvedAgent.commands,
221
320
  files: fileCount,
222
321
  install_path: agentDir,
322
+ scope,
323
+ symlinks: deploy.symlinks,
223
324
  author: resolvedAgent.author ? {
224
325
  username: resolvedAgent.author.username,
225
326
  display_name: resolvedAgent.author.display_name ?? null,
@@ -233,9 +334,11 @@ function registerInstall(program) {
233
334
  else {
234
335
  const authorUsername = resolvedAgent.author?.username;
235
336
  const authorSuffix = authorUsername ? ` \x1b[90mby @${authorUsername}\x1b[0m` : '';
236
- console.log(`\n\x1b[32m✓ ${resolvedAgent.name} 다운로드 완료\x1b[0m v${resolvedAgent.version}${authorSuffix}`);
337
+ const scopeLabel = scope === 'global' ? '글로벌' : '로컬';
338
+ console.log(`\n\x1b[32m✓ ${resolvedAgent.name} 설치 완료\x1b[0m v${resolvedAgent.version}${authorSuffix}`);
237
339
  console.log(` 위치: \x1b[36m${agentDir}\x1b[0m`);
238
- console.log(` 파일: ${fileCount}개`);
340
+ console.log(` 범위: ${scopeLabel}`);
341
+ console.log(` 파일: ${fileCount}개, symlink: ${deploy.symlinks.length}개`);
239
342
  if (resolvedAgent.commands.length > 0) {
240
343
  console.log('\n 포함된 커맨드:');
241
344
  for (const cmd of resolvedAgent.commands) {
@@ -256,7 +359,9 @@ function registerInstall(program) {
256
359
  else {
257
360
  console.log(`\n\x1b[33m💡 설치 완료! AI 에이전트에서 사용할 수 있습니다.\x1b[0m`);
258
361
  }
259
- console.log('\n \x1b[90m에이전트가 /relay-install로 환경을 구성합니다.\x1b[0m');
362
+ // Requires check
363
+ const requiresResults = (0, installer_js_1.checkRequires)(agentDir);
364
+ (0, installer_js_1.printRequiresCheck)(requiresResults);
260
365
  }
261
366
  }
262
367
  catch (err) {