relayax-cli 0.1.93 → 0.1.94

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.
@@ -31,15 +31,16 @@ function registerCreate(program) {
31
31
  .action(async (name) => {
32
32
  const json = program.opts().json ?? false;
33
33
  const projectPath = process.cwd();
34
- const relayYamlPath = path_1.default.join(projectPath, 'relay.yaml');
34
+ const relayDir = path_1.default.join(projectPath, '.relay');
35
+ const relayYamlPath = path_1.default.join(relayDir, 'relay.yaml');
35
36
  const isTTY = Boolean(process.stdin.isTTY) && !json;
36
- // 1. relay.yaml 이미 존재하면 에러
37
+ // 1. .relay/relay.yaml 이미 존재하면 에러
37
38
  if (fs_1.default.existsSync(relayYamlPath)) {
38
39
  if (json) {
39
- console.error(JSON.stringify({ error: 'ALREADY_EXISTS', message: 'relay.yaml이 이미 존재합니다.' }));
40
+ console.error(JSON.stringify({ error: 'ALREADY_EXISTS', message: '.relay/relay.yaml이 이미 존재합니다.' }));
40
41
  }
41
42
  else {
42
- console.error('relay.yaml이 이미 존재합니다. 기존 팀 프로젝트에서는 `relay init`을 사용하세요.');
43
+ console.error('.relay/relay.yaml이 이미 존재합니다. 기존 팀 프로젝트에서는 `relay init`을 사용하세요.');
43
44
  }
44
45
  process.exit(1);
45
46
  }
@@ -69,7 +70,8 @@ function registerCreate(program) {
69
70
  ],
70
71
  });
71
72
  }
72
- // 3. relay.yaml 생성
73
+ // 3. .relay/relay.yaml 생성
74
+ fs_1.default.mkdirSync(relayDir, { recursive: true });
73
75
  const yamlData = {
74
76
  name,
75
77
  slug: defaultSlug,
@@ -80,7 +82,7 @@ function registerCreate(program) {
80
82
  };
81
83
  fs_1.default.writeFileSync(relayYamlPath, js_yaml_1.default.dump(yamlData, { lineWidth: 120 }), 'utf-8');
82
84
  // 4. 디렉토리 구조 생성
83
- const createdDirs = [];
85
+ const createdDirs = ['.relay'];
84
86
  for (const dir of DEFAULT_DIRS) {
85
87
  const dirPath = path_1.default.join(projectPath, dir);
86
88
  if (!fs_1.default.existsSync(dirPath)) {
@@ -118,7 +120,7 @@ function registerCreate(program) {
118
120
  }
119
121
  else {
120
122
  console.log(`\n\x1b[32m✓ ${name} 팀 프로젝트 생성 완료\x1b[0m\n`);
121
- console.log(` relay.yaml 생성됨`);
123
+ console.log(` .relay/relay.yaml 생성됨`);
122
124
  if (createdDirs.length > 0) {
123
125
  console.log(` 디렉토리 생성: ${createdDirs.join(', ')}`);
124
126
  }
@@ -77,10 +77,10 @@ function hasGlobalUserCommands() {
77
77
  return command_adapter_js_1.USER_COMMANDS.every((cmd) => fs_1.default.existsSync((0, command_adapter_js_1.getGlobalCommandPath)(cmd.id)));
78
78
  }
79
79
  /**
80
- * 팀 프로젝트인지 감지한다 (relay.yaml 또는 팀 디렉토리 구조).
80
+ * 팀 프로젝트인지 감지한다 (.relay/relay.yaml 또는 팀 디렉토리 구조).
81
81
  */
82
82
  function isTeamProject(projectPath) {
83
- if (fs_1.default.existsSync(path_1.default.join(projectPath, 'relay.yaml'))) {
83
+ if (fs_1.default.existsSync(path_1.default.join(projectPath, '.relay', 'relay.yaml'))) {
84
84
  return true;
85
85
  }
86
86
  return VALID_TEAM_DIRS.some((d) => {
@@ -90,6 +90,80 @@ function isTeamProject(projectPath) {
90
90
  return fs_1.default.readdirSync(dirPath).filter((f) => !f.startsWith('.')).length > 0;
91
91
  });
92
92
  }
93
+ /** 레거시 User 커맨드 ID (이전에 로컬에 설치되던 것) */
94
+ const LEGACY_LOCAL_COMMAND_IDS = ['relay-explore', 'relay-install', 'relay-publish'];
95
+ /**
96
+ * 레거시 구조를 감지하고 마이그레이션한다.
97
+ * - relay.yaml (루트) → .relay/relay.yaml
98
+ * - portfolio/ (루트) → .relay/portfolio/
99
+ * - 로컬 레거시 슬래시 커맨드 제거 (글로벌로 이동되므로)
100
+ */
101
+ function detectLegacy(projectPath) {
102
+ const details = [];
103
+ if (fs_1.default.existsSync(path_1.default.join(projectPath, 'relay.yaml'))) {
104
+ details.push('relay.yaml → .relay/relay.yaml');
105
+ }
106
+ const legacyPortfolio = path_1.default.join(projectPath, 'portfolio');
107
+ if (fs_1.default.existsSync(legacyPortfolio) && fs_1.default.statSync(legacyPortfolio).isDirectory()) {
108
+ const hasImages = fs_1.default.readdirSync(legacyPortfolio).some((f) => ['.png', '.jpg', '.jpeg', '.webp'].some((ext) => f.toLowerCase().endsWith(ext)));
109
+ if (hasImages) {
110
+ details.push('portfolio/ → .relay/portfolio/');
111
+ }
112
+ }
113
+ // 로컬에 레거시 슬래시 커맨드가 있는지 확인
114
+ const detected = (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
115
+ for (const tool of detected) {
116
+ const cmdDir = path_1.default.join(projectPath, tool.skillsDir, 'commands', 'relay');
117
+ if (!fs_1.default.existsSync(cmdDir))
118
+ continue;
119
+ for (const cmdId of LEGACY_LOCAL_COMMAND_IDS) {
120
+ if (fs_1.default.existsSync(path_1.default.join(cmdDir, `${cmdId}.md`))) {
121
+ details.push(`${tool.skillsDir}/commands/relay/${cmdId}.md 제거 (글로벌로 이동)`);
122
+ break; // 한 번만 표시
123
+ }
124
+ }
125
+ }
126
+ return { hasLegacy: details.length > 0, details };
127
+ }
128
+ function runMigration(projectPath) {
129
+ const result = { relayYaml: false, portfolio: false, localCommandsCleaned: 0 };
130
+ const relayDir = path_1.default.join(projectPath, '.relay');
131
+ // 1. relay.yaml 이동
132
+ const legacyYaml = path_1.default.join(projectPath, 'relay.yaml');
133
+ const newYaml = path_1.default.join(relayDir, 'relay.yaml');
134
+ if (fs_1.default.existsSync(legacyYaml) && !fs_1.default.existsSync(newYaml)) {
135
+ fs_1.default.mkdirSync(relayDir, { recursive: true });
136
+ fs_1.default.renameSync(legacyYaml, newYaml);
137
+ result.relayYaml = true;
138
+ }
139
+ // 2. portfolio/ 이동
140
+ const legacyPortfolio = path_1.default.join(projectPath, 'portfolio');
141
+ const newPortfolio = path_1.default.join(relayDir, 'portfolio');
142
+ if (fs_1.default.existsSync(legacyPortfolio) && fs_1.default.statSync(legacyPortfolio).isDirectory() && !fs_1.default.existsSync(newPortfolio)) {
143
+ fs_1.default.renameSync(legacyPortfolio, newPortfolio);
144
+ result.portfolio = true;
145
+ }
146
+ // 3. 로컬 레거시 슬래시 커맨드 제거
147
+ const detected = (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
148
+ for (const tool of detected) {
149
+ const cmdDir = path_1.default.join(projectPath, tool.skillsDir, 'commands', 'relay');
150
+ if (!fs_1.default.existsSync(cmdDir))
151
+ continue;
152
+ for (const cmdId of LEGACY_LOCAL_COMMAND_IDS) {
153
+ const cmdPath = path_1.default.join(cmdDir, `${cmdId}.md`);
154
+ if (fs_1.default.existsSync(cmdPath)) {
155
+ fs_1.default.unlinkSync(cmdPath);
156
+ result.localCommandsCleaned++;
157
+ }
158
+ }
159
+ // 디렉토리가 비었으면 삭제
160
+ const remaining = fs_1.default.readdirSync(cmdDir);
161
+ if (remaining.length === 0) {
162
+ fs_1.default.rmdirSync(cmdDir);
163
+ }
164
+ }
165
+ return result;
166
+ }
93
167
  function registerInit(program) {
94
168
  program
95
169
  .command('init')
@@ -99,6 +173,42 @@ function registerInit(program) {
99
173
  .action(async (opts) => {
100
174
  const json = program.opts().json ?? false;
101
175
  const projectPath = process.cwd();
176
+ // ── 0. 레거시 마이그레이션 ──
177
+ const legacy = detectLegacy(projectPath);
178
+ let migrated = false;
179
+ if (legacy.hasLegacy) {
180
+ if (json) {
181
+ // JSON 모드: 자동 마이그레이션
182
+ const migrationResult = runMigration(projectPath);
183
+ migrated = migrationResult.relayYaml || migrationResult.portfolio || migrationResult.localCommandsCleaned > 0;
184
+ }
185
+ else if (process.stdin.isTTY) {
186
+ console.log('\n \x1b[33m⚠ 레거시 구조 감지\x1b[0m\n');
187
+ for (const d of legacy.details) {
188
+ console.log(` ${d}`);
189
+ }
190
+ console.log();
191
+ const { confirm } = await import('@inquirer/prompts');
192
+ const doMigrate = await confirm({ message: '마이그레이션할까요?', default: true });
193
+ if (doMigrate) {
194
+ const migrationResult = runMigration(projectPath);
195
+ migrated = true;
196
+ console.log(`\n \x1b[32m✓ 마이그레이션 완료\x1b[0m`);
197
+ if (migrationResult.relayYaml)
198
+ console.log(' relay.yaml → .relay/relay.yaml');
199
+ if (migrationResult.portfolio)
200
+ console.log(' portfolio/ → .relay/portfolio/');
201
+ if (migrationResult.localCommandsCleaned > 0)
202
+ console.log(` 레거시 로컬 커맨드 ${migrationResult.localCommandsCleaned}개 제거`);
203
+ console.log();
204
+ }
205
+ }
206
+ else {
207
+ // 비TTY, 비JSON: 자동 마이그레이션
208
+ const migrationResult = runMigration(projectPath);
209
+ migrated = migrationResult.relayYaml || migrationResult.portfolio || migrationResult.localCommandsCleaned > 0;
210
+ }
211
+ }
102
212
  const detected = (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
103
213
  const detectedIds = new Set(detected.map((t) => t.value));
104
214
  const isBuilder = isTeamProject(projectPath);
@@ -183,6 +293,7 @@ function registerInit(program) {
183
293
  console.log(JSON.stringify({
184
294
  status: 'ok',
185
295
  mode: isBuilder ? 'builder' : 'user',
296
+ migrated,
186
297
  global: {
187
298
  status: globalStatus,
188
299
  path: (0, command_adapter_js_1.getGlobalCommandDir)(),
@@ -12,6 +12,10 @@ const tar_1 = require("tar");
12
12
  const config_js_1 = require("../lib/config.js");
13
13
  const VALID_DIRS = ['skills', 'agents', 'rules', 'commands'];
14
14
  const IMAGE_EXTS = ['.png', '.jpg', '.jpeg', '.webp'];
15
+ /** 개별 포트폴리오 이미지 최대 크기 (2 MB) */
16
+ const MAX_IMAGE_SIZE = 2 * 1024 * 1024;
17
+ /** 전체 업로드 최대 크기 (10 MB) */
18
+ const MAX_TOTAL_UPLOAD_SIZE = 10 * 1024 * 1024;
15
19
  function parseRelayYaml(content) {
16
20
  const raw = js_yaml_1.default.load(content) ?? {};
17
21
  const tags = Array.isArray(raw.tags)
@@ -93,15 +97,15 @@ function collectPortfolio(teamDir, yamlPortfolio) {
93
97
  return fs_1.default.existsSync(absPath);
94
98
  });
95
99
  }
96
- // Auto-scan ./portfolio/
97
- const portfolioDir = path_1.default.join(teamDir, 'portfolio');
100
+ // Auto-scan .relay/portfolio/
101
+ const portfolioDir = path_1.default.join(teamDir, '.relay', 'portfolio');
98
102
  if (!fs_1.default.existsSync(portfolioDir))
99
103
  return [];
100
104
  const files = fs_1.default.readdirSync(portfolioDir)
101
105
  .filter((f) => IMAGE_EXTS.some((ext) => f.toLowerCase().endsWith(ext)))
102
106
  .sort();
103
107
  return files.map((f) => ({
104
- path: path_1.default.join('portfolio', f),
108
+ path: path_1.default.join('.relay', 'portfolio', f),
105
109
  title: path_1.default.basename(f, path_1.default.extname(f)).replace(/[-_]/g, ' '),
106
110
  }));
107
111
  }
@@ -140,13 +144,23 @@ async function publishToApi(token, tarPath, metadata, teamDir, portfolioEntries)
140
144
  const form = new FormData();
141
145
  form.append('package', blob, `${metadata.slug}-${metadata.version}.tar.gz`);
142
146
  form.append('metadata', JSON.stringify(metadata));
143
- // Attach portfolio images
147
+ // Attach portfolio images (with size validation)
144
148
  if (portfolioEntries.length > 0) {
145
149
  const portfolioMeta = [];
150
+ let totalImageSize = 0;
146
151
  for (let i = 0; i < portfolioEntries.length; i++) {
147
152
  const entry = portfolioEntries[i];
148
153
  const absPath = path_1.default.resolve(teamDir, entry.path);
149
154
  const imgBuffer = fs_1.default.readFileSync(absPath);
155
+ if (imgBuffer.length > MAX_IMAGE_SIZE) {
156
+ const sizeMB = (imgBuffer.length / 1024 / 1024).toFixed(1);
157
+ throw new Error(`포트폴리오 이미지 '${path_1.default.basename(entry.path)}'이(가) 너무 큽니다 (${sizeMB}MB). 최대 ${MAX_IMAGE_SIZE / 1024 / 1024}MB까지 허용됩니다.`);
158
+ }
159
+ totalImageSize += imgBuffer.length;
160
+ if (totalImageSize > MAX_TOTAL_UPLOAD_SIZE) {
161
+ const totalMB = (totalImageSize / 1024 / 1024).toFixed(1);
162
+ throw new Error(`포트폴리오 이미지 총 크기가 너무 큽니다 (${totalMB}MB). 최대 ${MAX_TOTAL_UPLOAD_SIZE / 1024 / 1024}MB까지 허용됩니다.`);
163
+ }
150
164
  const ext = path_1.default.extname(entry.path).slice(1) || 'png';
151
165
  const mimeType = ext === 'jpg' || ext === 'jpeg' ? 'image/jpeg' : ext === 'webp' ? 'image/webp' : 'image/png';
152
166
  const imgBlob = new Blob([imgBuffer], { type: mimeType });
@@ -179,14 +193,15 @@ function registerPublish(program) {
179
193
  .action(async (opts) => {
180
194
  const json = program.opts().json ?? false;
181
195
  const teamDir = process.cwd();
182
- const relayYamlPath = path_1.default.join(teamDir, 'relay.yaml');
196
+ const relayDir = path_1.default.join(teamDir, '.relay');
197
+ const relayYamlPath = path_1.default.join(relayDir, 'relay.yaml');
183
198
  const isTTY = Boolean(process.stdin.isTTY) && !json;
184
- // Check relay.yaml exists
199
+ // Check .relay/relay.yaml exists
185
200
  if (!fs_1.default.existsSync(relayYamlPath)) {
186
201
  if (!isTTY) {
187
202
  console.error(JSON.stringify({
188
203
  error: 'NOT_INITIALIZED',
189
- message: 'relay.yaml이 없습니다. 먼저 `relay init`을 실행하세요.',
204
+ message: '.relay/relay.yaml이 없습니다. 먼저 `relay create`를 실행하세요.',
190
205
  }));
191
206
  process.exit(1);
192
207
  }
@@ -195,7 +210,7 @@ function registerPublish(program) {
195
210
  const dirName = path_1.default.basename(teamDir);
196
211
  const defaultSlug = dirName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
197
212
  console.error('\n\x1b[36m릴레이 팀 패키지를 초기화합니다.\x1b[0m');
198
- console.error('relay.yaml을 생성하기 위해 몇 가지 정보를 입력해주세요.\n');
213
+ console.error('.relay/relay.yaml을 생성하기 위해 몇 가지 정보를 입력해주세요.\n');
199
214
  const name = await promptInput({
200
215
  message: '팀 이름:',
201
216
  default: dirName,
@@ -222,7 +237,7 @@ function registerPublish(program) {
222
237
  });
223
238
  console.error('\n\x1b[2m💡 프로필에 연락처를 설정하면 설치 시 명함이 전달됩니다: relayax.com/dashboard/edit\x1b[0m');
224
239
  if (visibility === 'invite-only') {
225
- console.error('\x1b[2m💡 invite-only 팀은 `relay invite <slug> add @username`으로 사용자를 초대하세요.\x1b[0m');
240
+ console.error('\x1b[2m💡 invite-only 팀은 대시보드에서 사용자를 초대하세요: relayax.com/dashboard\x1b[0m');
226
241
  }
227
242
  console.error('');
228
243
  const tags = tagsRaw
@@ -237,8 +252,9 @@ function registerPublish(program) {
237
252
  tags,
238
253
  visibility,
239
254
  };
255
+ fs_1.default.mkdirSync(relayDir, { recursive: true });
240
256
  fs_1.default.writeFileSync(relayYamlPath, js_yaml_1.default.dump(yamlData, { lineWidth: 120 }), 'utf-8');
241
- console.error(`\n\x1b[32m✓ relay.yaml이 생성되었습니다.\x1b[0m\n`);
257
+ console.error(`\n\x1b[32m✓ .relay/relay.yaml이 생성되었습니다.\x1b[0m\n`);
242
258
  }
243
259
  // Parse relay.yaml
244
260
  const yamlContent = fs_1.default.readFileSync(relayYamlPath, 'utf-8');
@@ -48,7 +48,7 @@ function registerStatus(program) {
48
48
  hasLocal = command_adapter_js_1.BUILDER_COMMANDS.some((cmd) => fs_1.default.existsSync(path_1.default.join(localDir, `${cmd.id}.md`)));
49
49
  }
50
50
  // 3. 팀 정보
51
- const relayYamlPath = path_1.default.join(projectPath, 'relay.yaml');
51
+ const relayYamlPath = path_1.default.join(projectPath, '.relay', 'relay.yaml');
52
52
  let team = null;
53
53
  if (fs_1.default.existsSync(relayYamlPath)) {
54
54
  try {
@@ -88,7 +88,7 @@ exports.USER_COMMANDS = [
88
88
  - 설치 결과를 확인합니다 (설치된 파일 수, 사용 가능한 커맨드 목록).
89
89
 
90
90
  ### 2. 의존성 확인 및 설치
91
- 설치된 팀의 relay.yaml에 \`requires\` 섹션이 있으면 각 항목을 확인하고 처리합니다:
91
+ 설치된 팀의 .relay/relay.yaml에 \`requires\` 섹션이 있으면 각 항목을 확인하고 처리합니다:
92
92
 
93
93
  - **cli**: \`which <name>\`으로 확인 → 없으면 install 명령 실행 또는 안내
94
94
  - **npm**: \`npm list <package>\`로 확인 → 없으면 \`npm install\`
@@ -206,7 +206,7 @@ ${LOGIN_JIT_GUIDE}
206
206
  ### 2. 팀 구조 분석
207
207
  - skills/, agents/, rules/, commands/ 디렉토리를 탐색합니다.
208
208
  - 각 파일의 이름과 description을 추출합니다.
209
- - relay.yaml이 있으면 읽고, 없으면 사용자에게 팀 정보(name, slug, description, tags)를 물어보고 생성합니다.
209
+ - .relay/relay.yaml이 있으면 읽고, 없으면 사용자에게 팀 정보(name, slug, description, tags)를 물어보고 생성합니다.
210
210
 
211
211
  ### 3. 포트폴리오 생성
212
212
 
@@ -219,14 +219,14 @@ ${LOGIN_JIT_GUIDE}
219
219
  - Rules 목록
220
220
  - 비시각적 팀의 경우 기술 스택이나 데이터 종류 등 추가 정보
221
221
  - 생성된 HTML을 Playwright로 스크린샷 캡처합니다. (gstack 또는 webapp-testing 스킬 활용)
222
- - 결과 PNG를 ./portfolio/team-overview.png에 저장합니다.
222
+ - 결과 PNG를 ./.relay/portfolio/team-overview.png에 저장합니다.
223
223
 
224
224
  #### Layer 2: 결과물 쇼케이스 (선택)
225
- - output/, results/, examples/, portfolio/ 디렉토리를 스캔합니다.
225
+ - output/, results/, examples/ 디렉토리를 스캔합니다.
226
226
  - 발견된 결과물(PNG, JPG, HTML, PDF)을 사용자에게 보여줍니다.
227
227
  - HTML 파일은 Playwright 스크린샷으로 변환합니다.
228
228
  - 사용자가 포트폴리오에 포함할 항목을 선택합니다.
229
- - 선택된 이미지를 ./portfolio/에 저장합니다.
229
+ - 선택된 이미지를 ./.relay/portfolio/에 저장합니다.
230
230
 
231
231
  ### 4. 메타데이터 생성
232
232
  - description: skills 내용 기반으로 자동 생성합니다.
@@ -234,8 +234,8 @@ ${LOGIN_JIT_GUIDE}
234
234
  - tags: 팀 특성에 맞는 태그를 추천합니다.
235
235
  - 사용자에게 확인: "이대로 배포할까요?"
236
236
 
237
- ### 5. relay.yaml 업데이트
238
- - 생성/수정된 메타데이터와 포트폴리오 경로를 relay.yaml에 반영합니다.
237
+ ### 5. .relay/relay.yaml 업데이트
238
+ - 생성/수정된 메타데이터와 포트폴리오 경로를 .relay/relay.yaml에 반영합니다.
239
239
 
240
240
  ### 6. 배포
241
241
  - \`relay publish\` 명령어를 실행합니다.
@@ -247,7 +247,7 @@ ${LOGIN_JIT_GUIDE}
247
247
  → 팀 구조 분석: skills 3개, commands 5개, agents 2개
248
248
  → Layer 1: 팀 구성 시각화 HTML 생성 → 스크린샷 캡처
249
249
  → Layer 2: output/ 스캔 → "카드뉴스 예시.png, PDF 보고서.png 발견. 포트폴리오에 포함할까요?"
250
- → 사용자 확인 후 relay.yaml 업데이트
250
+ → 사용자 확인 후 .relay/relay.yaml 업데이트
251
251
  → relay publish 실행
252
252
  → "배포 완료! URL: https://relayax.com/teams/contents-team"`,
253
253
  },
@@ -0,0 +1,75 @@
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.API_URL = void 0;
7
+ exports.getInstallPath = getInstallPath;
8
+ exports.ensureRelayDir = ensureRelayDir;
9
+ exports.loadToken = loadToken;
10
+ exports.saveToken = saveToken;
11
+ exports.loadInstalled = loadInstalled;
12
+ exports.saveInstalled = saveInstalled;
13
+ const fs_1 = __importDefault(require("fs"));
14
+ const path_1 = __importDefault(require("path"));
15
+ const os_1 = __importDefault(require("os"));
16
+ const ai_tools_js_1 = require("./ai-tools.js");
17
+ exports.API_URL = 'https://relayax.com';
18
+ const RELAY_DIR = path_1.default.join(os_1.default.homedir(), '.relay');
19
+ const INSTALLED_FILE = path_1.default.join(RELAY_DIR, 'installed.json');
20
+ /**
21
+ * 설치 경로를 결정한다.
22
+ * 1. --path 옵션이 있으면 그대로 사용
23
+ * 2. 에이전트 CLI 자동 감지 → 감지된 경로 사용
24
+ * 3. 감지 안 되면 현재 디렉토리에 직접 설치
25
+ */
26
+ function getInstallPath(override) {
27
+ if (override) {
28
+ const resolved = override.startsWith('~')
29
+ ? path_1.default.join(os_1.default.homedir(), override.slice(1))
30
+ : path_1.default.resolve(override);
31
+ return resolved;
32
+ }
33
+ const cwd = process.cwd();
34
+ const detected = (0, ai_tools_js_1.detectAgentCLIs)(cwd);
35
+ if (detected.length >= 1) {
36
+ return path_1.default.join(cwd, detected[0].skillsDir);
37
+ }
38
+ return cwd;
39
+ }
40
+ function ensureRelayDir() {
41
+ if (!fs_1.default.existsSync(RELAY_DIR)) {
42
+ fs_1.default.mkdirSync(RELAY_DIR, { recursive: true });
43
+ }
44
+ }
45
+ function loadToken() {
46
+ const tokenFile = path_1.default.join(RELAY_DIR, 'token');
47
+ if (!fs_1.default.existsSync(tokenFile))
48
+ return undefined;
49
+ try {
50
+ return fs_1.default.readFileSync(tokenFile, 'utf-8').trim() || undefined;
51
+ }
52
+ catch {
53
+ return undefined;
54
+ }
55
+ }
56
+ function saveToken(token) {
57
+ ensureRelayDir();
58
+ fs_1.default.writeFileSync(path_1.default.join(RELAY_DIR, 'token'), token);
59
+ }
60
+ function loadInstalled() {
61
+ if (!fs_1.default.existsSync(INSTALLED_FILE)) {
62
+ return {};
63
+ }
64
+ try {
65
+ const raw = fs_1.default.readFileSync(INSTALLED_FILE, 'utf-8');
66
+ return JSON.parse(raw);
67
+ }
68
+ catch {
69
+ return {};
70
+ }
71
+ }
72
+ function saveInstalled(registry) {
73
+ ensureRelayDir();
74
+ fs_1.default.writeFileSync(INSTALLED_FILE, JSON.stringify(registry, null, 2));
75
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relayax-cli",
3
- "version": "0.1.93",
3
+ "version": "0.1.94",
4
4
  "description": "RelayAX Agent Team Marketplace CLI - Install and manage agent teams",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -35,6 +35,7 @@
35
35
  "dependencies": {
36
36
  "@inquirer/prompts": "^8.3.2",
37
37
  "commander": "^13.1.0",
38
+ "form-data": "^4.0.5",
38
39
  "js-yaml": "^4.1.1",
39
40
  "tar": "^7.4.0"
40
41
  },