relayax-cli 0.3.64 → 0.3.66

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.
@@ -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) {
@@ -17,7 +17,7 @@ const version_check_js_1 = require("../lib/version-check.js");
17
17
  const paths_js_1 = require("../lib/paths.js");
18
18
  const error_report_js_1 = require("../lib/error-report.js");
19
19
  const step_tracker_js_1 = require("../lib/step-tracker.js");
20
- const index_js_1 = require("../prompts/index.js");
20
+ // GUIDE_INSTRUCTION removed — share text now uses npx install command directly
21
21
  // eslint-disable-next-line @typescript-eslint/no-var-requires
22
22
  const cliPkg = require('../../package.json');
23
23
  const VALID_DIRS = ['skills', 'agents', 'rules', 'commands', 'bin'];
@@ -49,6 +49,7 @@ function parseRelayYaml(content) {
49
49
  visibility,
50
50
  type,
51
51
  source: raw.source ? String(raw.source) : undefined,
52
+ org_slug: raw.org_slug ? String(raw.org_slug) : undefined,
52
53
  };
53
54
  }
54
55
  function detectCommands(agentDir) {
@@ -289,7 +290,11 @@ function registerPublish(program) {
289
290
  .description('현재 에이전트 패키지를 Space에 배포합니다 (relay.yaml 필요)')
290
291
  .option('--token <token>', '인증 토큰')
291
292
  .option('--space <slug>', '배포할 Space 지정')
293
+ .option('--org <slug>', 'Organization slug 지정')
292
294
  .option('--version <version>', '배포 버전 지정 (relay.yaml 업데이트)')
295
+ .option('--patch', 'patch 버전 범프')
296
+ .option('--minor', 'minor 버전 범프')
297
+ .option('--major', 'major 버전 범프')
293
298
  .option('--project <dir>', '프로젝트 루트 경로 (기본: cwd, 환경변수: RELAY_PROJECT_PATH)')
294
299
  .action(async (opts) => {
295
300
  const json = program.opts().json ?? false;
@@ -305,22 +310,6 @@ function registerPublish(program) {
305
310
  console.error(`\n\x1b[33m⚠ relay v${cliUpdate.latest}이 있습니다\x1b[0m (현재 v${cliUpdate.current})`);
306
311
  console.error(' 최신 버전에서는 설치자에게 자동 업데이트 알림이 지원됩니다.');
307
312
  console.error(` 업데이트: \x1b[36mnpm update -g relayax-cli\x1b[0m\n`);
308
- try {
309
- const { confirm } = await import('@inquirer/prompts');
310
- const shouldContinue = await confirm({
311
- message: '현재 버전으로 계속 배포할까요?',
312
- default: true,
313
- });
314
- if (!shouldContinue) {
315
- (0, error_report_js_1.reportCliError)('publish', 'CANCELLED_CLI_UPDATE', `current:${cliUpdate.current} latest:${cliUpdate.latest}`);
316
- console.error('\n배포를 취소했습니다. CLI를 업데이트한 후 다시 시도하세요.');
317
- process.exit(0);
318
- }
319
- console.error('');
320
- }
321
- catch {
322
- // non-interactive fallback: continue
323
- }
324
313
  }
325
314
  }
326
315
  // Check .relay/relay.yaml exists
@@ -399,13 +388,34 @@ function registerPublish(program) {
399
388
  }));
400
389
  process.exit(1);
401
390
  }
402
- // Version bump: --version flag takes priority
391
+ // Version bump: --version flag takes priority, then --patch/--minor/--major
392
+ const hasBumpFlag = Boolean(opts.patch || opts.minor || opts.major);
403
393
  if (opts.version) {
404
394
  config.version = opts.version;
405
395
  const yamlData = js_yaml_1.default.load(fs_1.default.readFileSync(relayYamlPath, 'utf-8'));
406
396
  yamlData.version = opts.version;
407
397
  fs_1.default.writeFileSync(relayYamlPath, js_yaml_1.default.dump(yamlData, { lineWidth: 120 }), 'utf-8');
408
398
  }
399
+ else if (hasBumpFlag) {
400
+ const [major, minor, patch] = config.version.split('.').map(Number);
401
+ let newVersion;
402
+ if (opts.major) {
403
+ newVersion = `${major + 1}.0.0`;
404
+ }
405
+ else if (opts.minor) {
406
+ newVersion = `${major}.${minor + 1}.0`;
407
+ }
408
+ else {
409
+ newVersion = `${major}.${minor}.${patch + 1}`;
410
+ }
411
+ config.version = newVersion;
412
+ const yamlData = js_yaml_1.default.load(fs_1.default.readFileSync(relayYamlPath, 'utf-8'));
413
+ yamlData.version = newVersion;
414
+ fs_1.default.writeFileSync(relayYamlPath, js_yaml_1.default.dump(yamlData, { lineWidth: 120 }), 'utf-8');
415
+ if (!json) {
416
+ console.error(` → relay.yaml에 version: ${newVersion} 저장됨\n`);
417
+ }
418
+ }
409
419
  else if (isTTY) {
410
420
  const { select: promptVersion } = await import('@inquirer/prompts');
411
421
  const [major, minor, patch] = config.version.split('.').map(Number);
@@ -462,26 +472,31 @@ function registerPublish(program) {
462
472
  try {
463
473
  const { fetchMyOrgs } = await import('./orgs.js');
464
474
  const orgs = await fetchMyOrgs(token);
465
- // --space flag (legacy alias for --org): resolve Org by slug
466
- if (opts.space) {
467
- const matched = orgs.find((o) => o.slug === opts.space);
475
+ // Determine explicit org slug: --org > --space (legacy) > relay.yaml org_slug
476
+ const explicitOrgSlug = opts.org ?? opts.space ?? config.org_slug;
477
+ // --org / --space / relay.yaml org_slug: resolve Org by slug
478
+ if (explicitOrgSlug) {
479
+ const matched = orgs.find((o) => o.slug === explicitOrgSlug);
468
480
  if (matched) {
469
481
  selectedOrgId = matched.id;
470
482
  selectedOrgSlug = matched.slug;
483
+ if (!json && (opts.org || config.org_slug)) {
484
+ console.error(`\x1b[2m Organization: ${matched.name} (${matched.slug})\x1b[0m\n`);
485
+ }
471
486
  }
472
487
  else {
473
488
  if (json) {
474
489
  console.error(JSON.stringify({
475
490
  error: 'INVALID_ORG',
476
- message: `Organization '${opts.space}'를 찾을 수 없습니다.`,
491
+ message: `Organization '${explicitOrgSlug}'를 찾을 수 없습니다.`,
477
492
  fix: `사용 가능한 Org: ${orgs.map((o) => o.slug).join(', ')}`,
478
493
  options: orgs.map((o) => ({ value: o.slug, label: `${o.name} (${o.slug})` })),
479
494
  }));
480
495
  }
481
496
  else {
482
- console.error(`Organization '${opts.space}'를 찾을 수 없습니다.`);
497
+ console.error(`Organization '${explicitOrgSlug}'를 찾을 수 없습니다.`);
483
498
  }
484
- (0, error_report_js_1.reportCliError)('publish', 'INVALID_ORG', `org:${opts.space}`);
499
+ (0, error_report_js_1.reportCliError)('publish', 'INVALID_ORG', `org:${explicitOrgSlug}`);
485
500
  process.exit(1);
486
501
  }
487
502
  }
@@ -581,7 +596,8 @@ function registerPublish(program) {
581
596
  }
582
597
  }
583
598
  // Confirm visibility before publish (재배포 시 변경 기회 제공)
584
- if (isTTY) {
599
+ // Skip when a bump flag is present and visibility is already set in relay.yaml
600
+ if (isTTY && !hasBumpFlag) {
585
601
  const { select: promptConfirmVis } = await import('@inquirer/prompts');
586
602
  const visLabelMap = {
587
603
  public: '공개',
@@ -701,22 +717,24 @@ function registerPublish(program) {
701
717
  if (isTTY) {
702
718
  const detailSlug = result.slug.startsWith('@') ? result.slug.slice(1) : result.slug;
703
719
  const accessCode = result.access_code;
704
- const guideUrl = accessCode
705
- ? `https://relayax.com/api/registry/${detailSlug}/guide.md?code=${accessCode}`
706
- : `https://relayax.com/api/registry/${detailSlug}/guide.md`;
707
- console.log(`\n \x1b[90m주변인에게 공유하세요:\x1b[0m\n`);
708
- console.log('```');
709
- console.log(index_js_1.GUIDE_INSTRUCTION);
710
- console.log(guideUrl);
711
- console.log('```');
712
- const installCmd = accessCode
713
- ? `/relay-install ${result.slug} --code ${accessCode}`
714
- : `/relay-install ${result.slug}`;
715
- console.log(`\n \x1b[90mrelay CLI 사용자용:\x1b[0m\n`);
716
- console.log('```');
717
- console.log(installCmd);
718
- console.log('```');
719
- console.log(`\n \x1b[90m상세페이지: \x1b[36mrelayax.com/@${detailSlug}\x1b[0m`);
720
+ // Primary: npx 설치 명령어 한 줄
721
+ const visibility = config.visibility ?? 'public';
722
+ let installCmd;
723
+ if (visibility === 'internal' && accessCode) {
724
+ installCmd = `npx relayax-cli install ${result.slug} --join-code ${accessCode}`;
725
+ }
726
+ else if (visibility === 'private' && accessCode) {
727
+ installCmd = `npx relayax-cli install ${result.slug} --code ${accessCode}`;
728
+ }
729
+ else {
730
+ installCmd = `npx relayax-cli install ${result.slug}`;
731
+ }
732
+ console.log(`\n \x1b[90m공유하세요:\x1b[0m`);
733
+ console.log(` ┌${'─'.repeat(installCmd.length + 2)}┐`);
734
+ console.log(` │ ${installCmd} │`);
735
+ console.log(` └${'─'.repeat(installCmd.length + 2)}┘`);
736
+ // Secondary: 에이전트 소개 페이지
737
+ console.log(`\n \x1b[90m에이전트 소개: \x1b[36mhttps://relayax.com/@${detailSlug}\x1b[0m`);
720
738
  }
721
739
  }
722
740
  }
@@ -67,11 +67,19 @@ function registerUninstall(program) {
67
67
  if (localEntry) {
68
68
  const removed = (0, installer_js_1.uninstallAgent)(localEntry.files);
69
69
  totalRemoved += removed.length;
70
- // Remove deployed files
70
+ // Remove deployed symlinks (new)
71
+ if (localEntry.deployed_symlinks && localEntry.deployed_symlinks.length > 0) {
72
+ const symlinkRemoved = (0, installer_js_1.removeSymlinks)(localEntry.deployed_symlinks);
73
+ totalRemoved += symlinkRemoved.length;
74
+ const boundary = inferBoundary(localEntry.deployed_symlinks, (0, paths_js_1.resolveProjectPath)(_opts.project));
75
+ for (const f of symlinkRemoved) {
76
+ (0, installer_js_1.cleanEmptyParents)(f, boundary);
77
+ }
78
+ }
79
+ // Remove deployed files (legacy)
71
80
  if (localEntry.deployed_files && localEntry.deployed_files.length > 0) {
72
81
  const deployedRemoved = (0, installer_js_1.uninstallAgent)(localEntry.deployed_files);
73
82
  totalRemoved += deployedRemoved.length;
74
- // Clean empty parent directories
75
83
  const boundary = inferBoundary(localEntry.deployed_files, (0, paths_js_1.resolveProjectPath)(_opts.project));
76
84
  for (const f of deployedRemoved) {
77
85
  (0, installer_js_1.cleanEmptyParents)(f, boundary);
@@ -87,11 +95,19 @@ function registerUninstall(program) {
87
95
  const removed = (0, installer_js_1.uninstallAgent)(globalEntry.files);
88
96
  totalRemoved += removed.length;
89
97
  }
90
- // Remove globally deployed files
98
+ // Remove deployed symlinks (new)
99
+ if (globalEntry.deployed_symlinks && globalEntry.deployed_symlinks.length > 0) {
100
+ const symlinkRemoved = (0, installer_js_1.removeSymlinks)(globalEntry.deployed_symlinks);
101
+ totalRemoved += symlinkRemoved.length;
102
+ const boundary = inferBoundary(globalEntry.deployed_symlinks, os_1.default.homedir());
103
+ for (const f of symlinkRemoved) {
104
+ (0, installer_js_1.cleanEmptyParents)(f, boundary);
105
+ }
106
+ }
107
+ // Remove globally deployed files (legacy)
91
108
  if (globalEntry.deployed_files && globalEntry.deployed_files.length > 0) {
92
109
  const deployedRemoved = (0, installer_js_1.uninstallAgent)(globalEntry.deployed_files);
93
110
  totalRemoved += deployedRemoved.length;
94
- // Clean empty parent directories
95
111
  const boundary = inferBoundary(globalEntry.deployed_files, os_1.default.homedir());
96
112
  for (const f of deployedRemoved) {
97
113
  (0, installer_js_1.cleanEmptyParents)(f, boundary);