relayax-cli 0.1.994 → 0.1.996

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.
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerCheckUpdate = registerCheckUpdate;
4
4
  const version_check_js_1 = require("../lib/version-check.js");
5
+ const slug_js_1 = require("../lib/slug.js");
6
+ const config_js_1 = require("../lib/config.js");
5
7
  function registerCheckUpdate(program) {
6
8
  program
7
9
  .command('check-update [slug]')
@@ -24,7 +26,28 @@ function registerCheckUpdate(program) {
24
26
  }
25
27
  // Team version check
26
28
  if (slug) {
27
- const teamResult = await (0, version_check_js_1.checkTeamVersion)(slug, force);
29
+ // Resolve to scoped slug
30
+ let scopedSlug;
31
+ if ((0, slug_js_1.isScopedSlug)(slug)) {
32
+ scopedSlug = slug;
33
+ }
34
+ else {
35
+ const installed = (0, config_js_1.loadInstalled)();
36
+ const found = (0, slug_js_1.findInstalledByName)(installed, slug);
37
+ if (found) {
38
+ scopedSlug = found;
39
+ }
40
+ else {
41
+ try {
42
+ const parsed = await (0, slug_js_1.resolveSlug)(slug);
43
+ scopedSlug = parsed.full;
44
+ }
45
+ catch {
46
+ scopedSlug = slug; // fallback to original
47
+ }
48
+ }
49
+ }
50
+ const teamResult = await (0, version_check_js_1.checkTeamVersion)(scopedSlug, force);
28
51
  if (teamResult) {
29
52
  if (quiet) {
30
53
  const byAuthor = teamResult.author ? ` ${teamResult.author}` : '';
@@ -8,6 +8,7 @@ const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const ai_tools_js_1 = require("../lib/ai-tools.js");
10
10
  const command_adapter_js_1 = require("../lib/command-adapter.js");
11
+ const config_js_1 = require("../lib/config.js");
11
12
  // eslint-disable-next-line @typescript-eslint/no-var-requires
12
13
  const pkg = require('../../package.json');
13
14
  const VALID_TEAM_DIRS = ['skills', 'agents', 'rules', 'commands'];
@@ -123,8 +124,17 @@ function registerInit(program) {
123
124
  // ── 1. 글로벌 User 커맨드 설치 ──
124
125
  let globalStatus = 'already';
125
126
  if (opts.update || !hasGlobalUserCommands()) {
126
- installGlobalUserCommands();
127
+ const { commands } = installGlobalUserCommands();
127
128
  globalStatus = opts.update ? 'updated' : 'installed';
129
+ // Register relay-core in installed.json
130
+ const installed = (0, config_js_1.loadInstalled)();
131
+ installed['relay-core'] = {
132
+ version: pkg.version,
133
+ installed_at: new Date().toISOString(),
134
+ files: commands.map((c) => (0, command_adapter_js_1.getGlobalCommandPath)(c)),
135
+ type: 'system',
136
+ };
137
+ (0, config_js_1.saveInstalled)(installed);
128
138
  }
129
139
  // ── 2. 로컬 Builder 커맨드 (팀 프로젝트인 경우) ──
130
140
  const localResults = [];
@@ -9,22 +9,26 @@ const path_1 = __importDefault(require("path"));
9
9
  const api_js_1 = require("../lib/api.js");
10
10
  const storage_js_1 = require("../lib/storage.js");
11
11
  const config_js_1 = require("../lib/config.js");
12
+ const slug_js_1 = require("../lib/slug.js");
12
13
  function registerInstall(program) {
13
14
  program
14
15
  .command('install <slug>')
15
16
  .description('에이전트 팀 패키지를 .relay/teams/에 다운로드합니다')
16
- .action(async (slug) => {
17
+ .action(async (slugInput) => {
17
18
  const json = program.opts().json ?? false;
18
19
  const projectPath = process.cwd();
19
- const teamDir = path_1.default.join(projectPath, '.relay', 'teams', slug);
20
20
  const tempDir = (0, storage_js_1.makeTempDir)();
21
21
  try {
22
+ // 0. Resolve scoped slug
23
+ const parsed = await (0, slug_js_1.resolveSlug)(slugInput);
24
+ const slug = parsed.full;
22
25
  // 1. Fetch team metadata
23
26
  const team = await (0, api_js_1.fetchTeamInfo)(slug);
27
+ const teamDir = path_1.default.join(projectPath, '.relay', 'teams', parsed.owner, parsed.name);
24
28
  // 2. Visibility check
25
29
  const visibility = team.visibility ?? 'public';
26
30
  if (visibility === 'login-only' || visibility === 'invite-only') {
27
- const token = (0, config_js_1.loadToken)();
31
+ const token = await (0, config_js_1.getValidToken)();
28
32
  if (!token) {
29
33
  console.error(JSON.stringify({
30
34
  error: 'LOGIN_REQUIRED',
@@ -119,7 +123,7 @@ function registerInstall(program) {
119
123
  console.log(` \x1b[90m└${'─'.repeat(44)}┘\x1b[0m`);
120
124
  }
121
125
  // Follow prompt (only when logged in)
122
- const token = (0, config_js_1.loadToken)();
126
+ const token = await (0, config_js_1.getValidToken)();
123
127
  if (authorUsername && token) {
124
128
  try {
125
129
  const { confirm } = await import('@inquirer/prompts');
@@ -43,6 +43,9 @@ 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
+ const refresh_token = url.searchParams.get('refresh_token') ?? undefined;
47
+ const expires_at_raw = url.searchParams.get('expires_at');
48
+ const expires_at = expires_at_raw ? Number(expires_at_raw) : undefined;
46
49
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
47
50
  res.end(`<!DOCTYPE html>
48
51
  <html><head><title>RelayAX</title></head>
@@ -55,7 +58,7 @@ function waitForToken(port) {
55
58
  </body></html>`);
56
59
  server.close();
57
60
  if (token) {
58
- resolve(token);
61
+ resolve({ token, refresh_token, expires_at });
59
62
  }
60
63
  else {
61
64
  reject(new Error('토큰이 전달되지 않았습니다'));
@@ -97,14 +100,19 @@ function registerLogin(program) {
97
100
  .action(async (opts) => {
98
101
  const json = program.opts().json ?? false;
99
102
  (0, config_js_1.ensureGlobalRelayDir)();
100
- let token = opts.token;
101
- if (!token) {
103
+ let accessToken = opts.token;
104
+ let refreshToken;
105
+ let expiresAt;
106
+ if (!accessToken) {
102
107
  try {
103
108
  const port = await findAvailablePort();
104
109
  const loginUrl = `${config_js_1.API_URL}/auth/cli-login?port=${port}`;
105
110
  console.error('브라우저에서 로그인을 진행합니다...');
106
111
  openBrowser(loginUrl);
107
- token = await waitForToken(port);
112
+ const loginResult = await waitForToken(port);
113
+ accessToken = loginResult.token;
114
+ refreshToken = loginResult.refresh_token;
115
+ expiresAt = loginResult.expires_at;
108
116
  }
109
117
  catch (err) {
110
118
  const msg = err instanceof Error ? err.message : '로그인 실패';
@@ -112,8 +120,12 @@ function registerLogin(program) {
112
120
  process.exit(1);
113
121
  }
114
122
  }
115
- const user = await verifyToken(token);
116
- (0, config_js_1.saveToken)(token);
123
+ const user = await verifyToken(accessToken);
124
+ (0, config_js_1.saveTokenData)({
125
+ access_token: accessToken,
126
+ ...(refreshToken ? { refresh_token: refreshToken } : {}),
127
+ ...(expiresAt ? { expires_at: expiresAt } : {}),
128
+ });
117
129
  const result = {
118
130
  status: 'ok',
119
131
  message: '로그인 성공',
@@ -379,7 +379,7 @@ function registerPublish(program) {
379
379
  process.exit(1);
380
380
  }
381
381
  // Get token (checked before tarball creation)
382
- const token = opts.token ?? process.env.RELAY_TOKEN ?? (0, config_js_1.loadToken)();
382
+ const token = opts.token ?? process.env.RELAY_TOKEN ?? await (0, config_js_1.getValidToken)();
383
383
  if (!token) {
384
384
  console.error(JSON.stringify({
385
385
  error: 'NO_TOKEN',
@@ -31,7 +31,7 @@ function registerStatus(program) {
31
31
  const json = program.opts().json ?? false;
32
32
  const projectPath = process.cwd();
33
33
  // 1. 로그인 상태
34
- const token = (0, config_js_1.loadToken)();
34
+ const token = await (0, config_js_1.getValidToken)();
35
35
  let username;
36
36
  if (token) {
37
37
  username = await resolveUsername(token);
@@ -3,15 +3,25 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerUninstall = registerUninstall;
4
4
  const config_js_1 = require("../lib/config.js");
5
5
  const installer_js_1 = require("../lib/installer.js");
6
+ const slug_js_1 = require("../lib/slug.js");
6
7
  function registerUninstall(program) {
7
8
  program
8
9
  .command('uninstall <slug>')
9
10
  .description('에이전트 팀 제거')
10
- .action((slug) => {
11
+ .action((slugInput) => {
11
12
  const json = program.opts().json ?? false;
12
13
  const installed = (0, config_js_1.loadInstalled)();
14
+ // Resolve slug from installed.json
15
+ let slug;
16
+ if ((0, slug_js_1.isScopedSlug)(slugInput)) {
17
+ slug = slugInput;
18
+ }
19
+ else {
20
+ const found = (0, slug_js_1.findInstalledByName)(installed, slugInput);
21
+ slug = found ?? slugInput;
22
+ }
13
23
  if (!installed[slug]) {
14
- const msg = { error: 'NOT_INSTALLED', message: `'${slug}'는 설치되어 있지 않습니다.` };
24
+ const msg = { error: 'NOT_INSTALLED', message: `'${slugInput}'는 설치되어 있지 않습니다.` };
15
25
  if (json) {
16
26
  console.error(JSON.stringify(msg));
17
27
  }
@@ -5,19 +5,35 @@ const api_js_1 = require("../lib/api.js");
5
5
  const storage_js_1 = require("../lib/storage.js");
6
6
  const installer_js_1 = require("../lib/installer.js");
7
7
  const config_js_1 = require("../lib/config.js");
8
+ const slug_js_1 = require("../lib/slug.js");
8
9
  function registerUpdate(program) {
9
10
  program
10
11
  .command('update <slug>')
11
12
  .description('설치된 에이전트 팀을 최신 버전으로 업데이트합니다')
12
13
  .option('--path <install_path>', '설치 경로 지정 (기본: ./.claude)')
13
14
  .option('--code <code>', '초대 코드 (invite-only 팀 업데이트 시 필요)')
14
- .action(async (slug, opts) => {
15
+ .action(async (slugInput, opts) => {
15
16
  const json = program.opts().json ?? false;
16
17
  const installPath = (0, config_js_1.getInstallPath)(opts.path);
17
18
  const tempDir = (0, storage_js_1.makeTempDir)();
18
19
  try {
19
- // Check installed.json for current version
20
+ // Resolve scoped slug (try installed.json first for offline, then server)
20
21
  const installed = (0, config_js_1.loadInstalled)();
22
+ let slug;
23
+ if ((0, slug_js_1.isScopedSlug)(slugInput)) {
24
+ slug = slugInput;
25
+ }
26
+ else {
27
+ const found = (0, slug_js_1.findInstalledByName)(installed, slugInput);
28
+ if (found) {
29
+ slug = found;
30
+ }
31
+ else {
32
+ const parsed = await (0, slug_js_1.resolveSlug)(slugInput);
33
+ slug = parsed.full;
34
+ }
35
+ }
36
+ // Check installed.json for current version
21
37
  const currentEntry = installed[slug];
22
38
  const currentVersion = currentEntry?.version ?? null;
23
39
  // Fetch latest team metadata
@@ -35,7 +51,7 @@ function registerUpdate(program) {
35
51
  // Visibility check
36
52
  const visibility = team.visibility ?? 'public';
37
53
  if (visibility === 'login-only') {
38
- const token = (0, config_js_1.loadToken)();
54
+ const token = await (0, config_js_1.getValidToken)();
39
55
  if (!token) {
40
56
  console.error('이 팀은 로그인이 필요합니다. `relay login`을 먼저 실행하세요.');
41
57
  process.exit(1);
package/dist/lib/api.d.ts CHANGED
@@ -8,4 +8,10 @@ export interface TeamVersionInfo {
8
8
  }
9
9
  export declare function fetchTeamVersions(slug: string): Promise<TeamVersionInfo[]>;
10
10
  export declare function reportInstall(slug: string): Promise<void>;
11
+ export interface ResolvedSlug {
12
+ owner: string;
13
+ name: string;
14
+ full: string;
15
+ }
16
+ export declare function resolveSlugFromServer(name: string): Promise<ResolvedSlug[]>;
11
17
  export declare function followBuilder(username: string): Promise<void>;
package/dist/lib/api.js CHANGED
@@ -4,6 +4,7 @@ exports.fetchTeamInfo = fetchTeamInfo;
4
4
  exports.searchTeams = searchTeams;
5
5
  exports.fetchTeamVersions = fetchTeamVersions;
6
6
  exports.reportInstall = reportInstall;
7
+ exports.resolveSlugFromServer = resolveSlugFromServer;
7
8
  exports.followBuilder = followBuilder;
8
9
  const config_js_1 = require("./config.js");
9
10
  async function fetchTeamInfo(slug) {
@@ -43,8 +44,17 @@ async function reportInstall(slug) {
43
44
  // non-critical: ignore errors
44
45
  });
45
46
  }
47
+ async function resolveSlugFromServer(name) {
48
+ const url = `${config_js_1.API_URL}/api/registry/resolve?name=${encodeURIComponent(name)}`;
49
+ const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
50
+ if (!res.ok) {
51
+ throw new Error(`slug resolve 실패 (${res.status})`);
52
+ }
53
+ const data = (await res.json());
54
+ return data.results;
55
+ }
46
56
  async function followBuilder(username) {
47
- const token = (0, config_js_1.loadToken)();
57
+ const token = await (0, config_js_1.getValidToken)();
48
58
  const headers = {
49
59
  'Content-Type': 'application/json',
50
60
  };
@@ -66,7 +66,7 @@ exports.USER_COMMANDS = [
66
66
  - 팀 이름과 설명
67
67
  - 제공하는 커맨드 목록
68
68
  - 왜 이 팀이 지금 프로젝트에 맞는지 설명
69
- 4. 관심 있는 팀이 있다면 \`/relay-install <slug>\`로 바로 설치할 수 있다고 안내합니다.
69
+ 4. 관심 있는 팀이 있다면 \`/relay-install <@author/slug>\`로 바로 설치할 수 있다고 안내합니다.
70
70
 
71
71
  ## 예시
72
72
 
@@ -74,7 +74,7 @@ exports.USER_COMMANDS = [
74
74
  → relay search 콘텐츠 실행
75
75
  → 결과 해석: "contents-team이 카드뉴스, PDF, PPT를 만들 수 있어요"
76
76
  → 프로젝트 맥락 기반 추천
77
- → "/relay-install contents-team으로 설치할 수 있어요"`,
77
+ → "/relay-install @example/contents-team으로 설치할 수 있어요"`,
78
78
  },
79
79
  {
80
80
  id: 'relay-install',
@@ -90,7 +90,7 @@ exports.USER_COMMANDS = [
90
90
  - 업데이트 여부와 관계없이 설치를 계속 진행합니다.
91
91
 
92
92
  ### 1. 팀 패키지 다운로드
93
- - \`relay install <slug>\` 명령어를 실행합니다.
93
+ - \`relay install <@author/slug>\` 명령어를 실행합니다.
94
94
  - 패키지가 \`.relay/teams/<slug>/\`에 다운로드됩니다.
95
95
 
96
96
  ### 2. 패키지 내용 확인
@@ -115,7 +115,7 @@ exports.USER_COMMANDS = [
115
115
  - **env**: 환경변수 확인 → required이면 설정 안내, optional이면 알림
116
116
  - **mcp**: MCP 서버 설정 — 에이전트의 MCP 설정에 추가 안내
117
117
  - **runtime**: Node.js/Python 버전 확인
118
- - **teams**: 의존하는 다른 팀 → \`relay install <team>\`으로 재귀 설치
118
+ - **teams**: 의존하는 다른 팀 → \`relay install <@author/team>\`으로 재귀 설치
119
119
  ${LOGIN_JIT_GUIDE}
120
120
 
121
121
  ### 5. 완료 안내
@@ -124,9 +124,9 @@ ${LOGIN_JIT_GUIDE}
124
124
 
125
125
  ## 예시
126
126
 
127
- 사용자: /relay-install contents-team
128
- → relay install contents-team 실행 (패키지 다운로드)
129
- → .relay/teams/contents-team/ 내용 확인
127
+ 사용자: /relay-install @example/contents-team
128
+ → relay install @example/contents-team 실행 (패키지 다운로드)
129
+ → .relay/teams/@example/contents-team/ 내용 확인
130
130
  → commands/cardnews.md → .claude/commands/cardnews.md 복사
131
131
  → skills/pdf-gen.md → .claude/skills/pdf-gen.md 복사
132
132
  → requires 확인: ✓ playwright 설치됨, ✓ sharp 설치됨
@@ -151,7 +151,7 @@ ${LOGIN_JIT_GUIDE}
151
151
  사용자: /relay-list
152
152
  → relay list --json 실행
153
153
  → "2개 팀이 설치되어 있어요:"
154
- → " contents-team v1.2.0 (2일 전 설치) - /cardnews, /pdf-report"
154
+ → " @example/contents-team v1.2.0 (2일 전 설치) - /cardnews, /pdf-report"
155
155
  → " qa-team v0.5.1 (1주 전 설치) - /qa, /qa-only"`,
156
156
  },
157
157
  {
@@ -167,14 +167,14 @@ ${LOGIN_JIT_GUIDE}
167
167
  - 업데이트 여부와 관계없이 팀 업데이트를 계속 진행합니다.
168
168
 
169
169
  ### 특정 팀 업데이트
170
- - 사용자가 팀 이름을 지정한 경우: \`relay update <slug> --json\` 실행
170
+ - 사용자가 팀 이름을 지정한 경우: \`relay update <@author/slug> --json\` 실행
171
171
  - 업데이트 결과를 보여줍니다 (이전 버전 → 새 버전)
172
172
 
173
173
  ### 전체 업데이트 확인
174
174
  - 팀 이름을 지정하지 않은 경우:
175
175
  1. \`relay outdated --json\`으로 업데이트 가능한 팀 목록을 확인합니다.
176
176
  2. 업데이트 가능한 팀이 있으면 목록을 보여주고 어떤 팀을 업데이트할지 물어봅니다.
177
- 3. 선택된 팀에 대해 \`relay update <slug> --json\`을 실행합니다.
177
+ 3. 선택된 팀에 대해 \`relay update <@author/slug> --json\`을 실행합니다.
178
178
  4. 모두 최신이면 "모든 팀이 최신 버전입니다"라고 안내합니다.
179
179
 
180
180
  ## 예시
@@ -182,10 +182,10 @@ ${LOGIN_JIT_GUIDE}
182
182
  사용자: /relay-update
183
183
  → relay outdated --json 실행
184
184
  → "1개 팀 업데이트 가능:"
185
- → " contents-team: v1.2.0 → v1.3.0"
185
+ → " @example/contents-team: v1.2.0 → v1.3.0"
186
186
  → "업데이트할까요?"
187
- → relay update contents-team --json 실행
188
- → "✓ contents-team v1.3.0으로 업데이트 완료"`,
187
+ → relay update @example/contents-team --json 실행
188
+ → "✓ @example/contents-team v1.3.0으로 업데이트 완료"`,
189
189
  },
190
190
  {
191
191
  id: 'relay-uninstall',
@@ -194,15 +194,15 @@ ${LOGIN_JIT_GUIDE}
194
194
 
195
195
  ## 실행 방법
196
196
 
197
- 1. \`relay uninstall <slug> --json\` 명령어를 실행합니다.
197
+ 1. \`relay uninstall <@author/slug> --json\` 명령어를 실행합니다.
198
198
  2. 삭제 결과를 보여줍니다 (팀 이름, 제거된 파일 수).
199
199
  3. 삭제 완료 후 남아있는 팀 목록을 간단히 안내합니다.
200
200
 
201
201
  ## 예시
202
202
 
203
- 사용자: /relay-uninstall contents-team
204
- → relay uninstall contents-team --json 실행
205
- → "✓ contents-team 삭제 완료 (12개 파일 제거)"`,
203
+ 사용자: /relay-uninstall @example/contents-team
204
+ → relay uninstall @example/contents-team --json 실행
205
+ → "✓ @example/contents-team 삭제 완료 (12개 파일 제거)"`,
206
206
  },
207
207
  ];
208
208
  // ─── Builder Commands (로컬 설치) ───
@@ -293,7 +293,7 @@ requires:
293
293
  - filesystem
294
294
  - network
295
295
  teams:
296
- - contents-team
296
+ - @example/contents-team
297
297
  \`\`\`
298
298
 
299
299
  ### 4. 포트폴리오 생성 (슬롯 기반)
@@ -11,9 +11,29 @@ export declare function getInstallPath(override?: string): string;
11
11
  export declare function ensureGlobalRelayDir(): void;
12
12
  /** cwd/.relay/ — 프로젝트 로컬 (installed.json, teams/) */
13
13
  export declare function ensureProjectRelayDir(): void;
14
+ export interface TokenData {
15
+ access_token: string;
16
+ refresh_token?: string;
17
+ expires_at?: number;
18
+ }
19
+ export declare function loadTokenData(): TokenData | undefined;
14
20
  export declare function loadToken(): string | undefined;
21
+ export declare function saveTokenData(data: TokenData): void;
15
22
  export declare function saveToken(token: string): void;
16
- /** 프로젝트 로컬 installed.json 읽기 */
23
+ /**
24
+ * 유효한 access_token을 반환한다.
25
+ * 1. 저장된 토큰이 없으면 undefined
26
+ * 2. expires_at이 아직 유효하면 access_token 반환
27
+ * 3. 만료되었으면 refresh_token으로 갱신 시도
28
+ * 4. 갱신 실패 시 undefined (재로그인 필요)
29
+ */
30
+ export declare function getValidToken(): Promise<string | undefined>;
31
+ /** 프로젝트 로컬 installed.json 읽기 (unscoped 키 자동 마이그레이션) */
17
32
  export declare function loadInstalled(): InstalledRegistry;
33
+ /**
34
+ * 비동기 마이그레이션: unscoped 키를 서버 resolve하여 scoped 키로 변환.
35
+ * install/update 등 비동기 커맨드에서 호출.
36
+ */
37
+ export declare function migrateInstalled(): Promise<void>;
18
38
  /** 프로젝트 로컬 installed.json 쓰기 */
19
39
  export declare function saveInstalled(registry: InstalledRegistry): void;
@@ -7,14 +7,19 @@ exports.API_URL = void 0;
7
7
  exports.getInstallPath = getInstallPath;
8
8
  exports.ensureGlobalRelayDir = ensureGlobalRelayDir;
9
9
  exports.ensureProjectRelayDir = ensureProjectRelayDir;
10
+ exports.loadTokenData = loadTokenData;
10
11
  exports.loadToken = loadToken;
12
+ exports.saveTokenData = saveTokenData;
11
13
  exports.saveToken = saveToken;
14
+ exports.getValidToken = getValidToken;
12
15
  exports.loadInstalled = loadInstalled;
16
+ exports.migrateInstalled = migrateInstalled;
13
17
  exports.saveInstalled = saveInstalled;
14
18
  const fs_1 = __importDefault(require("fs"));
15
19
  const path_1 = __importDefault(require("path"));
16
20
  const os_1 = __importDefault(require("os"));
17
21
  const ai_tools_js_1 = require("./ai-tools.js");
22
+ const slug_js_1 = require("./slug.js");
18
23
  exports.API_URL = 'https://www.relayax.com';
19
24
  const GLOBAL_RELAY_DIR = path_1.default.join(os_1.default.homedir(), '.relay');
20
25
  /**
@@ -50,22 +55,71 @@ function ensureProjectRelayDir() {
50
55
  fs_1.default.mkdirSync(dir, { recursive: true });
51
56
  }
52
57
  }
53
- function loadToken() {
58
+ function loadTokenData() {
54
59
  const tokenFile = path_1.default.join(GLOBAL_RELAY_DIR, 'token');
55
60
  if (!fs_1.default.existsSync(tokenFile))
56
61
  return undefined;
57
62
  try {
58
- return fs_1.default.readFileSync(tokenFile, 'utf-8').trim() || undefined;
63
+ const raw = fs_1.default.readFileSync(tokenFile, 'utf-8').trim();
64
+ if (!raw)
65
+ return undefined;
66
+ // JSON 형식 (새 포맷)
67
+ if (raw.startsWith('{')) {
68
+ return JSON.parse(raw);
69
+ }
70
+ // plain text (기존 포맷) — 호환성 유지
71
+ return { access_token: raw };
59
72
  }
60
73
  catch {
61
74
  return undefined;
62
75
  }
63
76
  }
77
+ function loadToken() {
78
+ return loadTokenData()?.access_token;
79
+ }
80
+ function saveTokenData(data) {
81
+ ensureGlobalRelayDir();
82
+ fs_1.default.writeFileSync(path_1.default.join(GLOBAL_RELAY_DIR, 'token'), JSON.stringify(data));
83
+ }
64
84
  function saveToken(token) {
65
85
  ensureGlobalRelayDir();
66
- fs_1.default.writeFileSync(path_1.default.join(GLOBAL_RELAY_DIR, 'token'), token);
86
+ fs_1.default.writeFileSync(path_1.default.join(GLOBAL_RELAY_DIR, 'token'), JSON.stringify({ access_token: token }));
87
+ }
88
+ /**
89
+ * 유효한 access_token을 반환한다.
90
+ * 1. 저장된 토큰이 없으면 undefined
91
+ * 2. expires_at이 아직 유효하면 access_token 반환
92
+ * 3. 만료되었으면 refresh_token으로 갱신 시도
93
+ * 4. 갱신 실패 시 undefined (재로그인 필요)
94
+ */
95
+ async function getValidToken() {
96
+ const data = loadTokenData();
97
+ if (!data)
98
+ return undefined;
99
+ // expires_at이 없거나 아직 유효하면 (30초 여유) 그대로 사용
100
+ if (!data.expires_at || data.expires_at > Date.now() / 1000 + 30) {
101
+ return data.access_token;
102
+ }
103
+ // 만료됨 — refresh 시도
104
+ if (!data.refresh_token)
105
+ return undefined;
106
+ try {
107
+ const res = await fetch(`${exports.API_URL}/api/auth/refresh`, {
108
+ method: 'POST',
109
+ headers: { 'Content-Type': 'application/json' },
110
+ body: JSON.stringify({ refresh_token: data.refresh_token }),
111
+ });
112
+ if (!res.ok)
113
+ return undefined;
114
+ const refreshed = (await res.json());
115
+ saveTokenData(refreshed);
116
+ return refreshed.access_token;
117
+ }
118
+ catch {
119
+ return undefined;
120
+ }
67
121
  }
68
- /** 프로젝트 로컬 installed.json 읽기 */
122
+ /** 프로젝트 로컬 installed.json 읽기 (unscoped 키 자동 마이그레이션) */
69
123
  function loadInstalled() {
70
124
  const file = path_1.default.join(process.cwd(), '.relay', 'installed.json');
71
125
  if (!fs_1.default.existsSync(file)) {
@@ -73,12 +127,65 @@ function loadInstalled() {
73
127
  }
74
128
  try {
75
129
  const raw = fs_1.default.readFileSync(file, 'utf-8');
76
- return JSON.parse(raw);
130
+ const registry = JSON.parse(raw);
131
+ return migrateInstalledKeys(registry);
77
132
  }
78
133
  catch {
79
134
  return {};
80
135
  }
81
136
  }
137
+ /**
138
+ * unscoped 키를 감지하여 서버 resolve 없이 가능한 마이그레이션을 수행한다.
139
+ * 서버 resolve가 필요한 경우는 마이그레이션 보류 (다음 기회에 재시도).
140
+ */
141
+ function migrateInstalledKeys(registry) {
142
+ const unscopedKeys = Object.keys(registry).filter((k) => !(0, slug_js_1.isScopedSlug)(k) && k !== 'relay-core');
143
+ if (unscopedKeys.length === 0)
144
+ return registry;
145
+ // 비동기 서버 resolve 없이는 owner를 알 수 없으므로,
146
+ // loadInstalled는 동기 함수 → 마이그레이션은 비동기 migrateInstalled()로 별도 호출
147
+ return registry;
148
+ }
149
+ /**
150
+ * 비동기 마이그레이션: unscoped 키를 서버 resolve하여 scoped 키로 변환.
151
+ * install/update 등 비동기 커맨드에서 호출.
152
+ */
153
+ async function migrateInstalled() {
154
+ const { resolveSlugFromServer } = await import('./api.js');
155
+ const registry = loadInstalled();
156
+ const teamsDir = path_1.default.join(process.cwd(), '.relay', 'teams');
157
+ let changed = false;
158
+ for (const key of Object.keys(registry)) {
159
+ if ((0, slug_js_1.isScopedSlug)(key) || key === 'relay-core')
160
+ continue;
161
+ try {
162
+ const results = await resolveSlugFromServer(key);
163
+ if (results.length !== 1)
164
+ continue;
165
+ const { owner, name } = results[0];
166
+ const scopedKey = `@${owner}/${name}`;
167
+ // installed.json 키 변환
168
+ registry[scopedKey] = registry[key];
169
+ delete registry[key];
170
+ // 디렉토리 이동
171
+ const oldDir = path_1.default.join(teamsDir, key);
172
+ const newDir = path_1.default.join(teamsDir, owner, name);
173
+ if (fs_1.default.existsSync(oldDir)) {
174
+ fs_1.default.mkdirSync(path_1.default.dirname(newDir), { recursive: true });
175
+ fs_1.default.renameSync(oldDir, newDir);
176
+ // files 배열 업데이트
177
+ registry[scopedKey].files = registry[scopedKey].files.map((f) => f.replace(`/teams/${key}`, `/teams/${owner}/${name}`));
178
+ }
179
+ changed = true;
180
+ }
181
+ catch {
182
+ // 네트워크 오류 등 — 다음 기회에 재시도
183
+ }
184
+ }
185
+ if (changed) {
186
+ saveInstalled(registry);
187
+ }
188
+ }
82
189
  /** 프로젝트 로컬 installed.json 쓰기 */
83
190
  function saveInstalled(registry) {
84
191
  ensureProjectRelayDir();
@@ -25,19 +25,16 @@ ${PREAMBLE_END}`;
25
25
  function injectPreamble(filePath, slug) {
26
26
  const content = fs_1.default.readFileSync(filePath, 'utf-8');
27
27
  const preamble = generatePreamble(slug);
28
- // Replace existing preamble or prepend
29
28
  const startIdx = content.indexOf(PREAMBLE_START);
30
29
  const endIdx = content.indexOf(PREAMBLE_END);
31
30
  let newContent;
32
31
  if (startIdx !== -1 && endIdx !== -1) {
33
- // Replace existing
34
32
  newContent =
35
33
  content.slice(0, startIdx) +
36
34
  preamble +
37
35
  content.slice(endIdx + PREAMBLE_END.length);
38
36
  }
39
37
  else {
40
- // Prepend
41
38
  newContent = preamble + '\n\n' + content;
42
39
  }
43
40
  fs_1.default.writeFileSync(filePath, newContent);
@@ -0,0 +1,24 @@
1
+ export interface ParsedSlug {
2
+ owner: string;
3
+ name: string;
4
+ full: string;
5
+ }
6
+ /**
7
+ * Scoped slug(`@owner/name`)를 동기적으로 파싱한다.
8
+ * 단순 slug는 파싱할 수 없으므로 null을 반환한다.
9
+ */
10
+ export declare function parseSlug(input: string): ParsedSlug | null;
11
+ /** input이 scoped slug인지 확인 */
12
+ export declare function isScopedSlug(input: string): boolean;
13
+ /** input이 단순 slug인지 확인 */
14
+ export declare function isSimpleSlug(input: string): boolean;
15
+ /**
16
+ * Scoped 또는 단순 slug를 받아 ParsedSlug를 반환한다.
17
+ * 단순 slug는 서버에 resolve를 요청한다.
18
+ */
19
+ export declare function resolveSlug(input: string): Promise<ParsedSlug>;
20
+ /**
21
+ * installed.json에서 단순 slug로 매칭되는 scoped 키를 찾는다.
22
+ * 네트워크 없이 로컬에서만 동작.
23
+ */
24
+ export declare function findInstalledByName(installed: Record<string, unknown>, name: string): string | null;
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseSlug = parseSlug;
4
+ exports.isScopedSlug = isScopedSlug;
5
+ exports.isSimpleSlug = isSimpleSlug;
6
+ exports.resolveSlug = resolveSlug;
7
+ exports.findInstalledByName = findInstalledByName;
8
+ const api_js_1 = require("./api.js");
9
+ const SCOPED_SLUG_RE = /^@([a-z0-9](?:[a-z0-9-]*[a-z0-9])?)\/([a-z0-9](?:[a-z0-9-]*[a-z0-9])?)$/;
10
+ const SIMPLE_SLUG_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
11
+ /**
12
+ * Scoped slug(`@owner/name`)를 동기적으로 파싱한다.
13
+ * 단순 slug는 파싱할 수 없으므로 null을 반환한다.
14
+ */
15
+ function parseSlug(input) {
16
+ const m = input.match(SCOPED_SLUG_RE);
17
+ if (!m)
18
+ return null;
19
+ return { owner: m[1], name: m[2], full: input };
20
+ }
21
+ /** input이 scoped slug인지 확인 */
22
+ function isScopedSlug(input) {
23
+ return SCOPED_SLUG_RE.test(input);
24
+ }
25
+ /** input이 단순 slug인지 확인 */
26
+ function isSimpleSlug(input) {
27
+ return SIMPLE_SLUG_RE.test(input);
28
+ }
29
+ /**
30
+ * Scoped 또는 단순 slug를 받아 ParsedSlug를 반환한다.
31
+ * 단순 slug는 서버에 resolve를 요청한다.
32
+ */
33
+ async function resolveSlug(input) {
34
+ // scoped slug면 바로 파싱
35
+ const parsed = parseSlug(input);
36
+ if (parsed)
37
+ return parsed;
38
+ // 단순 slug인지 검증
39
+ if (!isSimpleSlug(input)) {
40
+ throw new Error(`잘못된 slug 형식입니다: '${input}'. @owner/name 또는 name 형태로 입력하세요.`);
41
+ }
42
+ // 서버에 resolve 요청
43
+ const results = await (0, api_js_1.resolveSlugFromServer)(input);
44
+ if (results.length === 0) {
45
+ throw new Error(`'${input}' 팀을 찾을 수 없습니다.`);
46
+ }
47
+ if (results.length === 1) {
48
+ const r = results[0];
49
+ return { owner: r.owner, name: r.name, full: r.full };
50
+ }
51
+ // 여러 개 매칭
52
+ const list = results.map((r) => ` ${r.full}`).join('\n');
53
+ throw new Error(`'${input}'에 해당하는 팀이 여러 개입니다. 전체 slug를 지정해주세요:\n${list}`);
54
+ }
55
+ /**
56
+ * installed.json에서 단순 slug로 매칭되는 scoped 키를 찾는다.
57
+ * 네트워크 없이 로컬에서만 동작.
58
+ */
59
+ function findInstalledByName(installed, name) {
60
+ // 정확한 키가 있으면 그대로 (하위 호환)
61
+ if (name in installed)
62
+ return name;
63
+ // scoped 키 중 name 부분이 매칭되는 것 찾기
64
+ const matches = Object.keys(installed).filter((key) => {
65
+ const parsed = parseSlug(key);
66
+ return parsed && parsed.name === name;
67
+ });
68
+ if (matches.length === 1)
69
+ return matches[0];
70
+ return null;
71
+ }
@@ -6,7 +6,6 @@ exports.checkAllTeams = checkAllTeams;
6
6
  const config_js_1 = require("./config.js");
7
7
  const api_js_1 = require("./api.js");
8
8
  const update_cache_js_1 = require("./update-cache.js");
9
- const device_hash_js_1 = require("./device-hash.js");
10
9
  // eslint-disable-next-line @typescript-eslint/no-var-requires
11
10
  const pkg = require('../../package.json');
12
11
  async function checkCliVersion(force) {
@@ -37,15 +36,12 @@ async function checkTeamVersion(slug, force) {
37
36
  const entry = installed[slug];
38
37
  if (!entry?.version)
39
38
  return null;
39
+ // system 타입(relay-core)은 CLI 버전 체크로 대체
40
+ if (entry.type === 'system') {
41
+ return null;
42
+ }
40
43
  const team = await (0, api_js_1.fetchTeamInfo)(slug);
41
44
  (0, update_cache_js_1.updateCacheTimestamp)(slug);
42
- // Send usage ping (fire-and-forget)
43
- fetch(`${config_js_1.API_URL}/api/registry/${slug}/ping`, {
44
- method: 'POST',
45
- headers: { 'Content-Type': 'application/json' },
46
- body: JSON.stringify({ device_hash: (0, device_hash_js_1.getDeviceHash)() }),
47
- signal: AbortSignal.timeout(3000),
48
- }).catch(() => { });
49
45
  if (team.version !== entry.version) {
50
46
  return {
51
47
  type: 'team',
package/dist/types.d.ts CHANGED
@@ -4,10 +4,12 @@ export interface InstalledTeam {
4
4
  files: string[];
5
5
  type?: 'team' | 'system';
6
6
  }
7
+ /** 키는 scoped slug 포맷: "@owner/name" */
7
8
  export interface InstalledRegistry {
8
- [slug: string]: InstalledTeam;
9
+ [scopedSlug: string]: InstalledTeam;
9
10
  }
10
11
  export interface TeamRegistryInfo {
12
+ /** scoped slug 포맷: "@owner/name" */
11
13
  slug: string;
12
14
  name: string;
13
15
  description?: string;
@@ -17,11 +19,9 @@ export interface TeamRegistryInfo {
17
19
  name: string;
18
20
  description: string;
19
21
  }[];
20
- components: {
21
- agents: number;
22
- rules: number;
23
- skills: number;
24
- };
22
+ component_agents: number;
23
+ component_rules: number;
24
+ component_skills: number;
25
25
  tags?: string[];
26
26
  install_count?: number;
27
27
  requires?: Record<string, unknown>;
@@ -39,6 +39,7 @@ export interface TeamRegistryInfo {
39
39
  } | null;
40
40
  }
41
41
  export interface SearchResult {
42
+ /** scoped slug 포맷: "@owner/name" */
42
43
  slug: string;
43
44
  name: string;
44
45
  description: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relayax-cli",
3
- "version": "0.1.994",
3
+ "version": "0.1.996",
4
4
  "description": "RelayAX Agent Team Marketplace CLI - Install and manage agent teams",
5
5
  "main": "dist/index.js",
6
6
  "bin": {