relayax-cli 0.2.23 → 0.2.24
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.
- package/README.md +1 -1
- package/dist/commands/check-update.js +5 -13
- package/dist/commands/deploy-record.d.ts +2 -0
- package/dist/commands/deploy-record.js +93 -0
- package/dist/commands/init.d.ts +8 -0
- package/dist/commands/init.js +1 -0
- package/dist/commands/install.js +26 -18
- package/dist/commands/list.js +40 -14
- package/dist/commands/login.d.ts +5 -0
- package/dist/commands/login.js +29 -13
- package/dist/commands/publish.js +13 -0
- package/dist/commands/uninstall.js +57 -11
- package/dist/commands/update.js +9 -8
- package/dist/index.js +2 -0
- package/dist/lib/command-adapter.d.ts +0 -2
- package/dist/lib/command-adapter.js +110 -61
- package/dist/lib/config.d.ts +10 -6
- package/dist/lib/config.js +28 -59
- package/dist/lib/installer.d.ts +5 -0
- package/dist/lib/installer.js +28 -2
- package/dist/lib/preamble.d.ts +4 -2
- package/dist/lib/preamble.js +26 -4
- package/dist/lib/slug.d.ts +0 -5
- package/dist/lib/slug.js +0 -18
- package/dist/types.d.ts +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.registerCheckUpdate = registerCheckUpdate;
|
|
4
4
|
const version_check_js_1 = require("../lib/version-check.js");
|
|
5
5
|
const slug_js_1 = require("../lib/slug.js");
|
|
6
|
-
const config_js_1 = require("../lib/config.js");
|
|
7
6
|
function registerCheckUpdate(program) {
|
|
8
7
|
program
|
|
9
8
|
.command('check-update [slug]')
|
|
@@ -32,19 +31,12 @@ function registerCheckUpdate(program) {
|
|
|
32
31
|
scopedSlug = slug;
|
|
33
32
|
}
|
|
34
33
|
else {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
scopedSlug = found;
|
|
34
|
+
try {
|
|
35
|
+
const parsed = await (0, slug_js_1.resolveSlug)(slug);
|
|
36
|
+
scopedSlug = parsed.full;
|
|
39
37
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const parsed = await (0, slug_js_1.resolveSlug)(slug);
|
|
43
|
-
scopedSlug = parsed.full;
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
scopedSlug = slug; // fallback to original
|
|
47
|
-
}
|
|
38
|
+
catch {
|
|
39
|
+
scopedSlug = slug;
|
|
48
40
|
}
|
|
49
41
|
}
|
|
50
42
|
const teamResult = await (0, version_check_js_1.checkTeamVersion)(scopedSlug, force);
|
|
@@ -0,0 +1,93 @@
|
|
|
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.registerDeployRecord = registerDeployRecord;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const config_js_1 = require("../lib/config.js");
|
|
9
|
+
const slug_js_1 = require("../lib/slug.js");
|
|
10
|
+
function registerDeployRecord(program) {
|
|
11
|
+
program
|
|
12
|
+
.command('deploy-record <slug>')
|
|
13
|
+
.description('에이전트가 배치한 파일 정보를 installed.json에 기록합니다')
|
|
14
|
+
.requiredOption('--scope <scope>', '배치 범위 (global 또는 local)')
|
|
15
|
+
.option('--files <paths...>', '배치된 파일 경로 목록')
|
|
16
|
+
.action((slugInput, opts) => {
|
|
17
|
+
const json = program.opts().json ?? false;
|
|
18
|
+
const scope = opts.scope;
|
|
19
|
+
if (scope !== 'global' && scope !== 'local') {
|
|
20
|
+
const msg = { error: 'INVALID_SCOPE', message: '--scope는 global 또는 local이어야 합니다.' };
|
|
21
|
+
if (json) {
|
|
22
|
+
console.error(JSON.stringify(msg));
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
console.error(`\x1b[31m오류:\x1b[0m ${msg.message}`);
|
|
26
|
+
}
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
const files = opts.files ?? [];
|
|
30
|
+
// Resolve absolute paths
|
|
31
|
+
const resolvedFiles = files.map((f) => f.startsWith('/') || f.startsWith('~')
|
|
32
|
+
? f
|
|
33
|
+
: path_1.default.resolve(f));
|
|
34
|
+
// Find the team in the appropriate registry
|
|
35
|
+
const localRegistry = (0, config_js_1.loadInstalled)();
|
|
36
|
+
const globalRegistry = (0, config_js_1.loadGlobalInstalled)();
|
|
37
|
+
// Resolve slug — check both registries for short name match
|
|
38
|
+
let slug;
|
|
39
|
+
if ((0, slug_js_1.isScopedSlug)(slugInput)) {
|
|
40
|
+
slug = slugInput;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
const allKeys = [...Object.keys(localRegistry), ...Object.keys(globalRegistry)];
|
|
44
|
+
const match = allKeys.find((key) => {
|
|
45
|
+
const parsed = (0, slug_js_1.parseSlug)(key);
|
|
46
|
+
return parsed && parsed.name === slugInput;
|
|
47
|
+
});
|
|
48
|
+
slug = match ?? slugInput;
|
|
49
|
+
}
|
|
50
|
+
// Check if team exists in either registry
|
|
51
|
+
const entry = localRegistry[slug] ?? globalRegistry[slug];
|
|
52
|
+
if (!entry) {
|
|
53
|
+
const msg = { error: 'NOT_INSTALLED', message: `'${slugInput}'는 설치되어 있지 않습니다.` };
|
|
54
|
+
if (json) {
|
|
55
|
+
console.error(JSON.stringify(msg));
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
console.error(`\x1b[31m오류:\x1b[0m ${msg.message}`);
|
|
59
|
+
}
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
// Update deploy info
|
|
63
|
+
entry.deploy_scope = scope;
|
|
64
|
+
entry.deployed_files = resolvedFiles;
|
|
65
|
+
// Save to the correct registry based on scope
|
|
66
|
+
if (scope === 'global') {
|
|
67
|
+
globalRegistry[slug] = entry;
|
|
68
|
+
(0, config_js_1.saveGlobalInstalled)(globalRegistry);
|
|
69
|
+
// Also update local registry if entry exists there
|
|
70
|
+
if (localRegistry[slug]) {
|
|
71
|
+
localRegistry[slug] = entry;
|
|
72
|
+
(0, config_js_1.saveInstalled)(localRegistry);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
localRegistry[slug] = entry;
|
|
77
|
+
(0, config_js_1.saveInstalled)(localRegistry);
|
|
78
|
+
}
|
|
79
|
+
const result = {
|
|
80
|
+
status: 'ok',
|
|
81
|
+
slug,
|
|
82
|
+
deploy_scope: scope,
|
|
83
|
+
deployed_files: resolvedFiles.length,
|
|
84
|
+
};
|
|
85
|
+
if (json) {
|
|
86
|
+
console.log(JSON.stringify(result));
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const scopeLabel = scope === 'global' ? '글로벌' : '로컬';
|
|
90
|
+
console.log(`\x1b[32m✓ ${slug} 배치 정보 기록 완료\x1b[0m (${scopeLabel}, ${resolvedFiles.length}개 파일)`);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
/**
|
|
3
|
+
* 글로벌 User 커맨드를 ~/.claude/commands/relay/에 설치한다.
|
|
4
|
+
* 기존 파일 중 현재 커맨드 목록에 없는 것은 제거한다.
|
|
5
|
+
*/
|
|
6
|
+
export declare function installGlobalUserCommands(): {
|
|
7
|
+
installed: boolean;
|
|
8
|
+
commands: string[];
|
|
9
|
+
};
|
|
2
10
|
/**
|
|
3
11
|
* 글로벌 User 커맨드가 이미 설치되어 있는지 확인한다.
|
|
4
12
|
*/
|
package/dist/commands/init.js
CHANGED
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.installGlobalUserCommands = installGlobalUserCommands;
|
|
6
7
|
exports.hasGlobalUserCommands = hasGlobalUserCommands;
|
|
7
8
|
exports.registerInit = registerInit;
|
|
8
9
|
const fs_1 = __importDefault(require("fs"));
|
package/dist/commands/install.js
CHANGED
|
@@ -38,14 +38,12 @@ function registerInstall(program) {
|
|
|
38
38
|
const json = program.opts().json ?? false;
|
|
39
39
|
const projectPath = process.cwd();
|
|
40
40
|
const tempDir = (0, storage_js_1.makeTempDir)();
|
|
41
|
+
// Auto-init: 글로벌 커맨드가 없으면 자동 설치
|
|
41
42
|
if (!(0, init_js_1.hasGlobalUserCommands)()) {
|
|
42
43
|
if (!json) {
|
|
43
|
-
console.error('\x1b[33m
|
|
44
|
+
console.error('\x1b[33m⚙ 글로벌 커맨드를 자동 설치합니다...\x1b[0m');
|
|
44
45
|
}
|
|
45
|
-
|
|
46
|
-
console.error(JSON.stringify({ error: 'NOT_INITIALIZED', message: 'relay init을 먼저 실행하세요.' }));
|
|
47
|
-
}
|
|
48
|
-
process.exit(1);
|
|
46
|
+
(0, init_js_1.installGlobalUserCommands)();
|
|
49
47
|
}
|
|
50
48
|
try {
|
|
51
49
|
// 0. @spaces/{spaceSlug}/{teamSlug} 형식 감지 및 파싱
|
|
@@ -128,23 +126,33 @@ function registerInstall(program) {
|
|
|
128
126
|
}
|
|
129
127
|
}
|
|
130
128
|
const teamDir = path_1.default.join(projectPath, '.relay', 'teams', parsed.owner, parsed.name);
|
|
131
|
-
// 2. Visibility check
|
|
129
|
+
// 2. Visibility check + auto-login
|
|
132
130
|
const visibility = team.visibility ?? 'public';
|
|
133
131
|
if (visibility === 'private') {
|
|
134
|
-
|
|
132
|
+
let token = await (0, config_js_1.getValidToken)();
|
|
135
133
|
if (!token) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
134
|
+
const isTTY = Boolean(process.stdin.isTTY);
|
|
135
|
+
if (isTTY && !json) {
|
|
136
|
+
// Auto-login: TTY 환경에서 자동으로 login 플로우 트리거
|
|
137
|
+
console.error('\x1b[33m⚙ 이 팀은 로그인이 필요합니다. 로그인을 시작합니다...\x1b[0m');
|
|
138
|
+
const { runLogin } = await import('./login.js');
|
|
139
|
+
await runLogin();
|
|
140
|
+
token = await (0, config_js_1.getValidToken)();
|
|
143
141
|
}
|
|
144
|
-
|
|
145
|
-
|
|
142
|
+
if (!token) {
|
|
143
|
+
if (json) {
|
|
144
|
+
console.error(JSON.stringify({
|
|
145
|
+
error: 'LOGIN_REQUIRED',
|
|
146
|
+
visibility,
|
|
147
|
+
slug,
|
|
148
|
+
message: '이 팀은 로그인이 필요합니다. relay login을 먼저 실행하세요.',
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
console.error('\x1b[31m이 팀은 로그인이 필요합니다. relay login 을 먼저 실행하세요.\x1b[0m');
|
|
153
|
+
}
|
|
154
|
+
process.exit(1);
|
|
146
155
|
}
|
|
147
|
-
process.exit(1);
|
|
148
156
|
}
|
|
149
157
|
}
|
|
150
158
|
// 3. Download package
|
|
@@ -252,7 +260,7 @@ function registerInstall(program) {
|
|
|
252
260
|
else {
|
|
253
261
|
console.log(`\n\x1b[33m💡 설치 완료! AI 에이전트에서 사용할 수 있습니다.\x1b[0m`);
|
|
254
262
|
}
|
|
255
|
-
console.log('\n 에이전트가 /relay-install로 환경을
|
|
263
|
+
console.log('\n \x1b[90m에이전트가 /relay-install로 환경을 구성합니다.\x1b[0m');
|
|
256
264
|
}
|
|
257
265
|
}
|
|
258
266
|
catch (err) {
|
package/dist/commands/list.js
CHANGED
|
@@ -68,27 +68,53 @@ function registerList(program) {
|
|
|
68
68
|
}
|
|
69
69
|
return;
|
|
70
70
|
}
|
|
71
|
-
// 기본 동작:
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
71
|
+
// 기본 동작: 글로벌 + 로컬 통합 목록
|
|
72
|
+
const { global: globalInstalled, local: localInstalled } = (0, config_js_1.loadMergedInstalled)();
|
|
73
|
+
const allEntries = [];
|
|
74
|
+
const seen = new Set();
|
|
75
|
+
// 글로벌 먼저
|
|
76
|
+
for (const [slug, info] of Object.entries(globalInstalled)) {
|
|
77
|
+
allEntries.push({
|
|
78
|
+
slug,
|
|
79
|
+
version: info.version,
|
|
80
|
+
installed_at: info.installed_at,
|
|
81
|
+
scope: 'global',
|
|
82
|
+
deploy_scope: info.deploy_scope,
|
|
83
|
+
space_slug: info.space_slug,
|
|
84
|
+
});
|
|
85
|
+
seen.add(slug);
|
|
86
|
+
}
|
|
87
|
+
// 로컬 (글로벌과 중복되지 않는 것만)
|
|
88
|
+
for (const [slug, info] of Object.entries(localInstalled)) {
|
|
89
|
+
if (seen.has(slug))
|
|
90
|
+
continue;
|
|
91
|
+
allEntries.push({
|
|
92
|
+
slug,
|
|
93
|
+
version: info.version,
|
|
94
|
+
installed_at: info.installed_at,
|
|
95
|
+
scope: 'local',
|
|
96
|
+
deploy_scope: info.deploy_scope,
|
|
97
|
+
space_slug: info.space_slug,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
80
100
|
if (json) {
|
|
81
|
-
console.log(JSON.stringify({ installed:
|
|
101
|
+
console.log(JSON.stringify({ installed: allEntries }));
|
|
82
102
|
}
|
|
83
103
|
else {
|
|
84
|
-
if (
|
|
104
|
+
if (allEntries.length === 0) {
|
|
85
105
|
console.log('\n설치된 팀이 없습니다. `relay install <slug>`로 설치하세요.');
|
|
86
106
|
return;
|
|
87
107
|
}
|
|
88
|
-
console.log(`\n설치된 팀 (${
|
|
89
|
-
for (const item of
|
|
108
|
+
console.log(`\n설치된 팀 (${allEntries.length}개):\n`);
|
|
109
|
+
for (const item of allEntries) {
|
|
90
110
|
const date = new Date(item.installed_at).toLocaleDateString('ko-KR');
|
|
91
|
-
|
|
111
|
+
const scopeLabel = item.deploy_scope === 'global'
|
|
112
|
+
? '\x1b[32m글로벌\x1b[0m'
|
|
113
|
+
: item.deploy_scope === 'local'
|
|
114
|
+
? '\x1b[33m로컬\x1b[0m'
|
|
115
|
+
: '\x1b[90m미배치\x1b[0m';
|
|
116
|
+
const spaceLabel = item.space_slug ? ` \x1b[90m[Space: ${item.space_slug}]\x1b[0m` : '';
|
|
117
|
+
console.log(` \x1b[36m${item.slug}\x1b[0m v${item.version} ${scopeLabel} (${date})${spaceLabel}`);
|
|
92
118
|
}
|
|
93
119
|
}
|
|
94
120
|
});
|
package/dist/commands/login.d.ts
CHANGED
package/dist/commands/login.js
CHANGED
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runLogin = runLogin;
|
|
6
7
|
exports.registerLogin = registerLogin;
|
|
7
8
|
const http_1 = __importDefault(require("http"));
|
|
8
9
|
const readline_1 = __importDefault(require("readline"));
|
|
@@ -38,9 +39,6 @@ async function verifyToken(token) {
|
|
|
38
39
|
return null;
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
|
-
function parseFormBody(body) {
|
|
42
|
-
return new URLSearchParams(body);
|
|
43
|
-
}
|
|
44
42
|
function collectBody(req) {
|
|
45
43
|
return new Promise((resolve) => {
|
|
46
44
|
const chunks = [];
|
|
@@ -65,16 +63,9 @@ function waitForToken(port) {
|
|
|
65
63
|
}, 5 * 60 * 1000);
|
|
66
64
|
const server = http_1.default.createServer(async (req, res) => {
|
|
67
65
|
const url = new URL(req.url ?? '/', `http://localhost:${port}`);
|
|
68
|
-
if (url.pathname === '/callback') {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (req.method === 'POST') {
|
|
72
|
-
const body = await collectBody(req);
|
|
73
|
-
params = parseFormBody(body);
|
|
74
|
-
}
|
|
75
|
-
else {
|
|
76
|
-
params = url.searchParams;
|
|
77
|
-
}
|
|
66
|
+
if (url.pathname === '/callback' && req.method === 'POST') {
|
|
67
|
+
const body = await collectBody(req);
|
|
68
|
+
const params = new URLSearchParams(body);
|
|
78
69
|
const token = params.get('token');
|
|
79
70
|
const refresh_token = params.get('refresh_token') ?? undefined;
|
|
80
71
|
const expires_at_raw = params.get('expires_at');
|
|
@@ -210,6 +201,31 @@ async function selectProvider(json) {
|
|
|
210
201
|
return 'email';
|
|
211
202
|
return 'github';
|
|
212
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* 대화형 로그인 플로우 실행 (auto-login에서 호출).
|
|
206
|
+
* 로그인 성공 시 토큰 저장, 실패 시 throw.
|
|
207
|
+
*/
|
|
208
|
+
async function runLogin() {
|
|
209
|
+
(0, config_js_1.ensureGlobalRelayDir)();
|
|
210
|
+
const provider = await selectProvider(false);
|
|
211
|
+
let loginResult;
|
|
212
|
+
if (provider === 'email') {
|
|
213
|
+
loginResult = await loginWithEmail(false);
|
|
214
|
+
}
|
|
215
|
+
else if (provider === 'kakao') {
|
|
216
|
+
loginResult = await loginWithBrowser('kakao', false);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
loginResult = await loginWithBrowser('github', false);
|
|
220
|
+
}
|
|
221
|
+
await verifyToken(loginResult.token);
|
|
222
|
+
(0, config_js_1.saveTokenData)({
|
|
223
|
+
access_token: loginResult.token,
|
|
224
|
+
...(loginResult.refresh_token ? { refresh_token: loginResult.refresh_token } : {}),
|
|
225
|
+
...(loginResult.expires_at ? { expires_at: loginResult.expires_at } : {}),
|
|
226
|
+
});
|
|
227
|
+
console.log(`\x1b[32m✓ 로그인 완료\x1b[0m`);
|
|
228
|
+
}
|
|
213
229
|
function registerLogin(program) {
|
|
214
230
|
program
|
|
215
231
|
.command('login')
|
package/dist/commands/publish.js
CHANGED
|
@@ -646,6 +646,19 @@ function registerPublish(program) {
|
|
|
646
646
|
console.log(` \x1b[90m└${'─'.repeat(44)}┘\x1b[0m`);
|
|
647
647
|
console.log(`\n \x1b[90m명함 수정: \x1b[36mwww.relayax.com/dashboard/profile\x1b[0m`);
|
|
648
648
|
}
|
|
649
|
+
// Show shareable onboarding guide as a plain copyable block
|
|
650
|
+
if (isTTY) {
|
|
651
|
+
const guideLines = [
|
|
652
|
+
'npm install -g relayax-cli',
|
|
653
|
+
'relay login',
|
|
654
|
+
`relay install ${result.slug}`,
|
|
655
|
+
];
|
|
656
|
+
console.log(`\n \x1b[90m공유용 온보딩 가이드:\x1b[0m\n`);
|
|
657
|
+
console.log('```');
|
|
658
|
+
guideLines.forEach((line) => console.log(line));
|
|
659
|
+
console.log('```');
|
|
660
|
+
console.log(`\n \x1b[90m위 블록을 복사하여 팀원에게 공유하세요\x1b[0m`);
|
|
661
|
+
}
|
|
649
662
|
}
|
|
650
663
|
}
|
|
651
664
|
catch (err) {
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.registerUninstall = registerUninstall;
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
4
9
|
const config_js_1 = require("../lib/config.js");
|
|
5
10
|
const installer_js_1 = require("../lib/installer.js");
|
|
6
11
|
const slug_js_1 = require("../lib/slug.js");
|
|
@@ -10,17 +15,24 @@ function registerUninstall(program) {
|
|
|
10
15
|
.description('에이전트 팀 제거')
|
|
11
16
|
.action((slugInput) => {
|
|
12
17
|
const json = program.opts().json ?? false;
|
|
13
|
-
const
|
|
14
|
-
|
|
18
|
+
const localInstalled = (0, config_js_1.loadInstalled)();
|
|
19
|
+
const globalInstalled = (0, config_js_1.loadGlobalInstalled)();
|
|
20
|
+
// Resolve slug — support short names like "cardnews-team"
|
|
15
21
|
let slug;
|
|
16
22
|
if ((0, slug_js_1.isScopedSlug)(slugInput)) {
|
|
17
23
|
slug = slugInput;
|
|
18
24
|
}
|
|
19
25
|
else {
|
|
20
|
-
const
|
|
21
|
-
|
|
26
|
+
const allKeys = [...Object.keys(localInstalled), ...Object.keys(globalInstalled)];
|
|
27
|
+
const match = allKeys.find((key) => {
|
|
28
|
+
const parsed = (0, slug_js_1.parseSlug)(key);
|
|
29
|
+
return parsed && parsed.name === slugInput;
|
|
30
|
+
});
|
|
31
|
+
slug = match ?? slugInput;
|
|
22
32
|
}
|
|
23
|
-
|
|
33
|
+
const localEntry = localInstalled[slug];
|
|
34
|
+
const globalEntry = globalInstalled[slug];
|
|
35
|
+
if (!localEntry && !globalEntry) {
|
|
24
36
|
const msg = { error: 'NOT_INSTALLED', message: `'${slugInput}'는 설치되어 있지 않습니다.` };
|
|
25
37
|
if (json) {
|
|
26
38
|
console.error(JSON.stringify(msg));
|
|
@@ -30,21 +42,55 @@ function registerUninstall(program) {
|
|
|
30
42
|
}
|
|
31
43
|
process.exit(1);
|
|
32
44
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
45
|
+
let totalRemoved = 0;
|
|
46
|
+
// Remove from local registry
|
|
47
|
+
if (localEntry) {
|
|
48
|
+
const removed = (0, installer_js_1.uninstallTeam)(localEntry.files);
|
|
49
|
+
totalRemoved += removed.length;
|
|
50
|
+
// Remove deployed files
|
|
51
|
+
if (localEntry.deployed_files && localEntry.deployed_files.length > 0) {
|
|
52
|
+
const deployedRemoved = (0, installer_js_1.uninstallTeam)(localEntry.deployed_files);
|
|
53
|
+
totalRemoved += deployedRemoved.length;
|
|
54
|
+
// Clean empty parent directories
|
|
55
|
+
const boundary = path_1.default.join(process.cwd(), '.claude');
|
|
56
|
+
for (const f of deployedRemoved) {
|
|
57
|
+
(0, installer_js_1.cleanEmptyParents)(f, boundary);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
delete localInstalled[slug];
|
|
61
|
+
(0, config_js_1.saveInstalled)(localInstalled);
|
|
62
|
+
}
|
|
63
|
+
// Remove from global registry
|
|
64
|
+
if (globalEntry) {
|
|
65
|
+
// Only remove files if not already handled by local entry
|
|
66
|
+
if (!localEntry) {
|
|
67
|
+
const removed = (0, installer_js_1.uninstallTeam)(globalEntry.files);
|
|
68
|
+
totalRemoved += removed.length;
|
|
69
|
+
}
|
|
70
|
+
// Remove globally deployed files
|
|
71
|
+
if (globalEntry.deployed_files && globalEntry.deployed_files.length > 0) {
|
|
72
|
+
const deployedRemoved = (0, installer_js_1.uninstallTeam)(globalEntry.deployed_files);
|
|
73
|
+
totalRemoved += deployedRemoved.length;
|
|
74
|
+
// Clean empty parent directories
|
|
75
|
+
const boundary = path_1.default.join(os_1.default.homedir(), '.claude');
|
|
76
|
+
for (const f of deployedRemoved) {
|
|
77
|
+
(0, installer_js_1.cleanEmptyParents)(f, boundary);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
delete globalInstalled[slug];
|
|
81
|
+
(0, config_js_1.saveGlobalInstalled)(globalInstalled);
|
|
82
|
+
}
|
|
37
83
|
const result = {
|
|
38
84
|
status: 'ok',
|
|
39
85
|
team: slug,
|
|
40
|
-
files_removed:
|
|
86
|
+
files_removed: totalRemoved,
|
|
41
87
|
};
|
|
42
88
|
if (json) {
|
|
43
89
|
console.log(JSON.stringify(result));
|
|
44
90
|
}
|
|
45
91
|
else {
|
|
46
92
|
console.log(`\n\x1b[32m✓ ${slug} 제거 완료\x1b[0m`);
|
|
47
|
-
console.log(` 삭제된 파일: ${
|
|
93
|
+
console.log(` 삭제된 파일: ${totalRemoved}개`);
|
|
48
94
|
}
|
|
49
95
|
});
|
|
50
96
|
}
|
package/dist/commands/update.js
CHANGED
|
@@ -26,14 +26,8 @@ function registerUpdate(program) {
|
|
|
26
26
|
slug = slugInput;
|
|
27
27
|
}
|
|
28
28
|
else {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
slug = found;
|
|
32
|
-
}
|
|
33
|
-
else {
|
|
34
|
-
const parsed = await (0, slug_js_1.resolveSlug)(slugInput);
|
|
35
|
-
slug = parsed.full;
|
|
36
|
-
}
|
|
29
|
+
const parsed = await (0, slug_js_1.resolveSlug)(slugInput);
|
|
30
|
+
slug = parsed.full;
|
|
37
31
|
}
|
|
38
32
|
// Check installed.json for current version
|
|
39
33
|
const currentEntry = installed[slug];
|
|
@@ -68,11 +62,17 @@ function registerUpdate(program) {
|
|
|
68
62
|
(0, preamble_js_1.injectPreambleToTeam)(extractDir, slug);
|
|
69
63
|
// Copy files to install_path
|
|
70
64
|
const files = (0, installer_js_1.installTeam)(extractDir, installPath);
|
|
65
|
+
// Preserve deploy info but clear deployed_files (agent needs to re-deploy)
|
|
66
|
+
const previousDeployScope = currentEntry?.deploy_scope;
|
|
67
|
+
const hadDeployedFiles = (currentEntry?.deployed_files?.length ?? 0) > 0;
|
|
71
68
|
// Update installed.json with new version
|
|
72
69
|
installed[slug] = {
|
|
73
70
|
version: latestVersion,
|
|
74
71
|
installed_at: new Date().toISOString(),
|
|
75
72
|
files,
|
|
73
|
+
// Keep deploy_scope so agent knows where to re-deploy
|
|
74
|
+
...(previousDeployScope ? { deploy_scope: previousDeployScope } : {}),
|
|
75
|
+
// Clear deployed_files — agent must re-deploy and call deploy-record
|
|
76
76
|
};
|
|
77
77
|
(0, config_js_1.saveInstalled)(installed);
|
|
78
78
|
// Report install (non-blocking)
|
|
@@ -84,6 +84,7 @@ function registerUpdate(program) {
|
|
|
84
84
|
version: latestVersion,
|
|
85
85
|
files_installed: files.length,
|
|
86
86
|
install_path: installPath,
|
|
87
|
+
...(hadDeployedFiles ? { needs_redeploy: true, previous_deploy_scope: previousDeployScope } : {}),
|
|
87
88
|
};
|
|
88
89
|
if (json) {
|
|
89
90
|
console.log(JSON.stringify(result));
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,7 @@ const follow_js_1 = require("./commands/follow.js");
|
|
|
18
18
|
const changelog_js_1 = require("./commands/changelog.js");
|
|
19
19
|
const join_js_1 = require("./commands/join.js");
|
|
20
20
|
const spaces_js_1 = require("./commands/spaces.js");
|
|
21
|
+
const deploy_record_js_1 = require("./commands/deploy-record.js");
|
|
21
22
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
22
23
|
const pkg = require('../package.json');
|
|
23
24
|
const program = new commander_1.Command();
|
|
@@ -42,4 +43,5 @@ program
|
|
|
42
43
|
(0, changelog_js_1.registerChangelog)(program);
|
|
43
44
|
(0, join_js_1.registerJoin)(program);
|
|
44
45
|
(0, spaces_js_1.registerSpaces)(program);
|
|
46
|
+
(0, deploy_record_js_1.registerDeployRecord)(program);
|
|
45
47
|
program.parse();
|
|
@@ -29,5 +29,3 @@ export declare function getGlobalCommandDir(): string;
|
|
|
29
29
|
export declare function formatCommandFile(content: CommandContent): string;
|
|
30
30
|
export declare const USER_COMMANDS: CommandContent[];
|
|
31
31
|
export declare const BUILDER_COMMANDS: CommandContent[];
|
|
32
|
-
/** 하위 호환 — 기존 코드에서 RELAY_COMMANDS를 참조하는 경우 */
|
|
33
|
-
export declare const RELAY_COMMANDS: CommandContent[];
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.BUILDER_COMMANDS = exports.USER_COMMANDS = void 0;
|
|
7
7
|
exports.createAdapter = createAdapter;
|
|
8
8
|
exports.getGlobalCommandPath = getGlobalCommandPath;
|
|
9
9
|
exports.getGlobalCommandDir = getGlobalCommandDir;
|
|
@@ -20,9 +20,7 @@ function createAdapter(tool) {
|
|
|
20
20
|
getFilePath(commandId) {
|
|
21
21
|
return path_1.default.join(tool.skillsDir, 'commands', 'relay', `${commandId}.md`);
|
|
22
22
|
},
|
|
23
|
-
formatFile
|
|
24
|
-
return `---\ndescription: ${content.description}\n---\n\n${content.body}\n`;
|
|
25
|
-
},
|
|
23
|
+
formatFile: formatCommandFile,
|
|
26
24
|
};
|
|
27
25
|
}
|
|
28
26
|
/**
|
|
@@ -101,44 +99,46 @@ exports.USER_COMMANDS = [
|
|
|
101
99
|
|
|
102
100
|
## 실행 방법
|
|
103
101
|
|
|
104
|
-
###
|
|
105
|
-
|
|
106
|
-
-
|
|
107
|
-
-
|
|
108
|
-
- 업데이트 여부와 관계없이 설치를 계속 진행합니다.
|
|
109
|
-
|
|
110
|
-
### 1. 팀 패키지 다운로드
|
|
111
|
-
- Public 마켓 팀: \`relay install <@author/slug>\` 명령어를 실행합니다.
|
|
112
|
-
- Space 팀: \`relay install @spaces/<space-slug>/<team-slug>\` 명령어를 실행합니다.
|
|
102
|
+
### 1. 패키지 다운로드
|
|
103
|
+
\`relay install <@author/slug> --json\` 명령어를 실행합니다.
|
|
104
|
+
- Public 마켓 팀: \`relay install <@author/slug> --json\`
|
|
105
|
+
- Space 팀: \`relay install @spaces/<space-slug>/<team-slug> --json\`
|
|
113
106
|
- Space 가입이 필요하면: \`relay join <space-slug> --code <invite-code>\` 를 먼저 실행합니다.
|
|
114
|
-
- 또는
|
|
115
|
-
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
-
|
|
122
|
-
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
- Claude Code
|
|
136
|
-
- Claude Code
|
|
137
|
-
-
|
|
138
|
-
-
|
|
107
|
+
- 또는 \`--join-code <code>\`로 가입+설치를 한번에 할 수 있습니다.
|
|
108
|
+
- CLI가 init과 login을 자동으로 처리합니다 (사용자가 별도 실행할 필요 없음).
|
|
109
|
+
- JSON 출력에서 \`install_path\` (패키지 경로)를 확인합니다.
|
|
110
|
+
|
|
111
|
+
### 2. 배치 범위 선택
|
|
112
|
+
사용자에게 설치 범위를 물어봅니다:
|
|
113
|
+
|
|
114
|
+
- **글로벌** (\`~/.claude/\`): 모든 프로젝트에서 사용 가능
|
|
115
|
+
- **로컬** (현재 프로젝트 \`.claude/\`): 이 프로젝트에서만 사용
|
|
116
|
+
|
|
117
|
+
판단 기준:
|
|
118
|
+
- 범용 도구 (카드뉴스, PDF 생성 등): 글로벌 추천
|
|
119
|
+
- 프로젝트 전용 팀: 로컬 추천
|
|
120
|
+
- Space 비공개 팀: 로컬 추천
|
|
121
|
+
|
|
122
|
+
사용자가 별도 지정하지 않으면 글로벌로 진행합니다.
|
|
123
|
+
|
|
124
|
+
### 3. 에이전트 환경에 맞게 배치
|
|
125
|
+
다운로드된 패키지(\`install_path\`)에서 파일을 읽고 선택된 범위에 배치합니다:
|
|
126
|
+
- Claude Code 글로벌: \`<install_path>/commands/\` → \`~/.claude/commands/\`에 복사
|
|
127
|
+
- Claude Code 글로벌: \`<install_path>/skills/\` → \`~/.claude/skills/\`에 복사
|
|
128
|
+
- Claude Code 로컬: \`<install_path>/commands/\` → \`.claude/commands/\`에 복사
|
|
129
|
+
- Claude Code 로컬: \`<install_path>/skills/\` → \`.claude/skills/\`에 복사
|
|
130
|
+
- agents/, rules/ 파일도 같은 방식으로 배치합니다.
|
|
131
|
+
- **충돌 확인**: 같은 이름의 파일이 이미 있으면 사용자에게 덮어쓸지 물어봅니다.
|
|
132
|
+
|
|
133
|
+
### 4. 배치 정보 기록 (필수)
|
|
134
|
+
배치 완료 후 반드시 \`relay deploy-record\`를 실행하여 배치 정보를 기록합니다:
|
|
135
|
+
\`\`\`
|
|
136
|
+
relay deploy-record <slug> --scope <global|local> --files <배치된_파일1> <배치된_파일2> ...
|
|
137
|
+
\`\`\`
|
|
138
|
+
이 정보는 \`relay uninstall\` 시 배치된 파일까지 정리하는 데 사용됩니다.
|
|
139
139
|
|
|
140
140
|
### 5. Requirements 확인 및 설치
|
|
141
|
-
|
|
141
|
+
\`<install_path>/relay.yaml\`의 \`requires\` 섹션을 읽고 처리합니다:
|
|
142
142
|
- **cli**: \`which <name>\`으로 확인 → 없으면 install 명령 실행 또는 안내
|
|
143
143
|
- **npm**: \`npm list <package>\`로 확인 → 없으면 \`npm install\`
|
|
144
144
|
- **env**: 환경변수 확인 → required이면 설정 안내, optional이면 알림
|
|
@@ -155,21 +155,20 @@ ${BUSINESS_CARD_FORMAT}
|
|
|
155
155
|
- 거절하면: 건너뜁니다
|
|
156
156
|
- "바로 사용해볼까요?" 제안
|
|
157
157
|
|
|
158
|
+
### 7. 업데이트 확인 (설치 완료 후)
|
|
159
|
+
- \`relay check-update\` 명령어를 실행합니다.
|
|
160
|
+
- CLI 업데이트가 있으면 안내합니다: "relay v{new} available. Run: npm update -g relayax-cli"
|
|
161
|
+
- 다른 팀 업데이트가 있으면 안내합니다.
|
|
162
|
+
|
|
158
163
|
## 예시
|
|
159
164
|
|
|
160
165
|
사용자: /relay-install @example/contents-team
|
|
161
|
-
→ relay install @example/contents-team 실행 (패키지 다운로드)
|
|
162
|
-
→
|
|
163
|
-
→
|
|
164
|
-
→
|
|
166
|
+
→ relay install @example/contents-team --json 실행 (패키지 다운로드)
|
|
167
|
+
→ 사용자에게 "글로벌 vs 로컬" 선택 질문 → 글로벌
|
|
168
|
+
→ .relay/teams/ 내용을 ~/.claude/에 배치
|
|
169
|
+
→ relay deploy-record @example/contents-team --scope global --files ~/.claude/commands/cardnews.md ...
|
|
165
170
|
→ requires 확인: ✓ playwright 설치됨, ✓ sharp 설치됨
|
|
166
|
-
→ "✓ 설치 완료! /cardnews를 사용해볼까요?"
|
|
167
|
-
|
|
168
|
-
사용자: /relay-install @spaces/bobusan/pm-bot
|
|
169
|
-
→ relay install @spaces/bobusan/pm-bot 실행
|
|
170
|
-
→ Space 멤버 확인 → 정상
|
|
171
|
-
→ 패키지 다운로드 및 배치
|
|
172
|
-
→ "✓ 설치 완료!"`,
|
|
171
|
+
→ "✓ 설치 완료! /cardnews를 사용해볼까요?"`,
|
|
173
172
|
},
|
|
174
173
|
{
|
|
175
174
|
id: 'relay-list',
|
|
@@ -218,6 +217,9 @@ ${BUSINESS_CARD_FORMAT}
|
|
|
218
217
|
### 특정 팀 업데이트
|
|
219
218
|
- 사용자가 팀 이름을 지정한 경우: \`relay update <@author/slug> --json\` 실행
|
|
220
219
|
- 업데이트 결과를 보여줍니다 (이전 버전 → 새 버전)
|
|
220
|
+
- **재배치 필요 확인**: JSON 출력에 \`needs_redeploy: true\`가 있으면:
|
|
221
|
+
1. \`previous_deploy_scope\`를 참고하여 같은 범위(글로벌/로컬)로 파일을 다시 배치합니다.
|
|
222
|
+
2. 배치 후 \`relay deploy-record <slug> --scope <scope> --files <...>\`를 실행하여 기록합니다.
|
|
221
223
|
${BUSINESS_CARD_FORMAT}
|
|
222
224
|
|
|
223
225
|
### 전체 업데이트 확인
|
|
@@ -235,18 +237,50 @@ ${BUSINESS_CARD_FORMAT}
|
|
|
235
237
|
→ " @example/contents-team: v1.2.0 → v1.3.0"
|
|
236
238
|
→ "업데이트할까요?"
|
|
237
239
|
→ relay update @example/contents-team --json 실행
|
|
240
|
+
→ needs_redeploy: true → 글로벌로 재배치
|
|
241
|
+
→ relay deploy-record @example/contents-team --scope global --files ...
|
|
238
242
|
→ "✓ @example/contents-team v1.3.0으로 업데이트 완료"`,
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
id: 'relay-spaces',
|
|
246
|
+
description: '내 Space 목록을 확인합니다',
|
|
247
|
+
body: `사용자의 Space 목록을 조회하고 보여줍니다.
|
|
248
|
+
|
|
249
|
+
## 실행 방법
|
|
250
|
+
|
|
251
|
+
1. \`relay spaces --json\` 명령어를 실행합니다.
|
|
252
|
+
2. 결과를 사용자에게 보기 좋게 정리합니다:
|
|
253
|
+
- 개인 스페이스
|
|
254
|
+
- 팀 스페이스 (이름, 역할, 설명)
|
|
255
|
+
3. Space가 있으면 관련 활용법을 안내합니다:
|
|
256
|
+
- 팀 목록 보기: \`relay list --space <slug>\`
|
|
257
|
+
- 비공개 팀 설치: \`relay install @spaces/<slug>/<team>\`
|
|
258
|
+
- Space 관리: www.relayax.com/spaces/<slug>
|
|
259
|
+
${LOGIN_JIT_GUIDE}
|
|
260
|
+
|
|
261
|
+
## 예시
|
|
262
|
+
|
|
263
|
+
사용자: /relay-spaces
|
|
264
|
+
→ relay spaces --json 실행
|
|
265
|
+
→ "2개 Space가 있어요:"
|
|
266
|
+
→ " bobusan — 보부산 (소유자)"
|
|
267
|
+
→ " design-lab — 디자인 랩 (멤버)"
|
|
268
|
+
→ "💡 Space 팀 보기: relay list --space bobusan"`,
|
|
239
269
|
},
|
|
240
270
|
{
|
|
241
271
|
id: 'relay-uninstall',
|
|
242
272
|
description: '설치된 에이전트 팀을 삭제합니다',
|
|
243
|
-
body: `설치된 에이전트 팀을
|
|
273
|
+
body: `설치된 에이전트 팀을 제거합니다. CLI가 패키지와 배치된 파일을 모두 정리합니다.
|
|
244
274
|
|
|
245
275
|
## 실행 방법
|
|
246
276
|
|
|
247
277
|
1. \`relay uninstall <@author/slug> --json\` 명령어를 실행합니다.
|
|
248
|
-
2.
|
|
249
|
-
|
|
278
|
+
2. CLI가 자동으로 처리하는 것:
|
|
279
|
+
- \`.relay/teams/\` 패키지 삭제
|
|
280
|
+
- \`deployed_files\`에 기록된 배치 파일 삭제 (\`~/.claude/\` 또는 \`.claude/\`)
|
|
281
|
+
- 빈 상위 디렉토리 정리
|
|
282
|
+
- installed.json에서 항목 제거 (글로벌/로컬 양쪽)
|
|
283
|
+
3. 삭제 결과를 보여줍니다 (팀 이름, 제거된 파일 수).
|
|
250
284
|
|
|
251
285
|
## 예시
|
|
252
286
|
|
|
@@ -265,9 +299,9 @@ exports.BUILDER_COMMANDS = [
|
|
|
265
299
|
## 실행 단계
|
|
266
300
|
|
|
267
301
|
### 1. 인증 확인 (가장 먼저)
|
|
268
|
-
- \`
|
|
269
|
-
- 미인증이면 즉시
|
|
270
|
-
-
|
|
302
|
+
- \`relay status --json\` 명령어를 실행하여 로그인 상태를 확인합니다.
|
|
303
|
+
- 미인증이면 즉시 \`relay login\`을 실행합니다.
|
|
304
|
+
- 로그인 완료 후 다음 단계로 진행합니다.
|
|
271
305
|
${LOGIN_JIT_GUIDE}
|
|
272
306
|
|
|
273
307
|
### 2. 팀 구조 분석
|
|
@@ -363,8 +397,9 @@ requires:
|
|
|
363
397
|
- 각 스킬의 SKILL.md, 에이전트 설정, 커맨드 문서를 분석하여 팀의 파이프라인 흐름을 추론합니다.
|
|
364
398
|
|
|
365
399
|
#### 5-2. GUIDE.html 생성
|
|
366
|
-
-
|
|
367
|
-
-
|
|
400
|
+
- 팀의 핵심 기능, 시작 방법, 파이프라인 흐름, Q&A를 포함하는 단일 HTML 가이드를 생성합니다.
|
|
401
|
+
- 디자인: 깔끔한 단일 페이지, 시스템 폰트, 최대 1200px 너비, 라이트 테마.
|
|
402
|
+
- 5-1에서 분석한 팀 소스 정보를 기반으로 콘텐츠를 구성합니다.
|
|
368
403
|
- 파이프라인이 없는 단순한 팀은 시작 방법 + 기능 설명 + Q&A만 포함합니다.
|
|
369
404
|
|
|
370
405
|
#### 5-3. 미리보기 + 컨펌
|
|
@@ -419,6 +454,20 @@ portfolio:
|
|
|
419
454
|
### 9. 배포
|
|
420
455
|
- \`relay publish\` 명령어를 실행합니다.
|
|
421
456
|
- 배포 결과와 마켓플레이스 URL을 보여줍니다.
|
|
457
|
+
|
|
458
|
+
### 10. 공유용 온보딩 가이드 제공
|
|
459
|
+
- \`relay publish\` 출력 끝에 코드블록 형태의 온보딩 가이드가 포함됩니다.
|
|
460
|
+
- 이 코드블록을 사용자에게 그대로 보여줍니다.
|
|
461
|
+
- 출력에 코드블록이 없으면 아래 형태로 직접 생성합니다:
|
|
462
|
+
|
|
463
|
+
\\\`\\\`\\\`
|
|
464
|
+
npm install -g relayax-cli
|
|
465
|
+
relay login
|
|
466
|
+
relay install <slug>
|
|
467
|
+
\\\`\\\`\\\`
|
|
468
|
+
|
|
469
|
+
- \`<slug>\`는 배포된 팀의 실제 슬러그로 치환합니다.
|
|
470
|
+
- "이 블록을 팀원에게 공유하면 바로 설치할 수 있습니다"라고 안내합니다.
|
|
422
471
|
${BUSINESS_CARD_FORMAT}
|
|
423
472
|
|
|
424
473
|
## 예시
|
|
@@ -432,8 +481,8 @@ ${BUSINESS_CARD_FORMAT}
|
|
|
432
481
|
→ GUIDE.html 생성 → 브라우저에서 미리보기 → 빌더 컨펌
|
|
433
482
|
→ GUIDE.html 스크린샷 → gallery 첫 번째 이미지로 등록
|
|
434
483
|
→ relay publish 실행
|
|
435
|
-
→ "배포 완료! URL: https://relayax.com/teams/my-team"
|
|
484
|
+
→ "배포 완료! URL: https://relayax.com/teams/my-team"
|
|
485
|
+
→ 온보딩 가이드 코드블록 표시
|
|
486
|
+
→ "이 블록을 팀원에게 공유하면 바로 설치할 수 있습니다"`,
|
|
436
487
|
},
|
|
437
488
|
];
|
|
438
|
-
/** 하위 호환 — 기존 코드에서 RELAY_COMMANDS를 참조하는 경우 */
|
|
439
|
-
exports.RELAY_COMMANDS = [...exports.USER_COMMANDS, ...exports.BUILDER_COMMANDS];
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -28,12 +28,16 @@ export declare function saveToken(token: string): void;
|
|
|
28
28
|
* 4. 갱신 실패 시 undefined (재로그인 필요)
|
|
29
29
|
*/
|
|
30
30
|
export declare function getValidToken(): Promise<string | undefined>;
|
|
31
|
-
/** 프로젝트 로컬 installed.json 읽기
|
|
31
|
+
/** 프로젝트 로컬 installed.json 읽기 */
|
|
32
32
|
export declare function loadInstalled(): InstalledRegistry;
|
|
33
|
-
/**
|
|
34
|
-
* 비동기 마이그레이션: unscoped 키를 서버 resolve하여 scoped 키로 변환.
|
|
35
|
-
* install/update 등 비동기 커맨드에서 호출.
|
|
36
|
-
*/
|
|
37
|
-
export declare function migrateInstalled(): Promise<void>;
|
|
38
33
|
/** 프로젝트 로컬 installed.json 쓰기 */
|
|
39
34
|
export declare function saveInstalled(registry: InstalledRegistry): void;
|
|
35
|
+
/** 글로벌 installed.json 읽기 (~/.relay/installed.json) */
|
|
36
|
+
export declare function loadGlobalInstalled(): InstalledRegistry;
|
|
37
|
+
/** 글로벌 installed.json 쓰기 (~/.relay/installed.json) */
|
|
38
|
+
export declare function saveGlobalInstalled(registry: InstalledRegistry): void;
|
|
39
|
+
/** 글로벌 + 로컬 레지스트리 병합 뷰 */
|
|
40
|
+
export declare function loadMergedInstalled(): {
|
|
41
|
+
global: InstalledRegistry;
|
|
42
|
+
local: InstalledRegistry;
|
|
43
|
+
};
|
package/dist/lib/config.js
CHANGED
|
@@ -13,13 +13,14 @@ exports.saveTokenData = saveTokenData;
|
|
|
13
13
|
exports.saveToken = saveToken;
|
|
14
14
|
exports.getValidToken = getValidToken;
|
|
15
15
|
exports.loadInstalled = loadInstalled;
|
|
16
|
-
exports.migrateInstalled = migrateInstalled;
|
|
17
16
|
exports.saveInstalled = saveInstalled;
|
|
17
|
+
exports.loadGlobalInstalled = loadGlobalInstalled;
|
|
18
|
+
exports.saveGlobalInstalled = saveGlobalInstalled;
|
|
19
|
+
exports.loadMergedInstalled = loadMergedInstalled;
|
|
18
20
|
const fs_1 = __importDefault(require("fs"));
|
|
19
21
|
const path_1 = __importDefault(require("path"));
|
|
20
22
|
const os_1 = __importDefault(require("os"));
|
|
21
23
|
const ai_tools_js_1 = require("./ai-tools.js");
|
|
22
|
-
const slug_js_1 = require("./slug.js");
|
|
23
24
|
exports.API_URL = 'https://www.relayax.com';
|
|
24
25
|
const GLOBAL_RELAY_DIR = path_1.default.join(os_1.default.homedir(), '.relay');
|
|
25
26
|
/**
|
|
@@ -63,7 +64,6 @@ function loadTokenData() {
|
|
|
63
64
|
const raw = fs_1.default.readFileSync(tokenFile, 'utf-8').trim();
|
|
64
65
|
if (!raw)
|
|
65
66
|
return undefined;
|
|
66
|
-
// JSON 형식 (새 포맷)
|
|
67
67
|
if (raw.startsWith('{')) {
|
|
68
68
|
return JSON.parse(raw);
|
|
69
69
|
}
|
|
@@ -121,76 +121,45 @@ async function getValidToken() {
|
|
|
121
121
|
return undefined;
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
|
-
/** 프로젝트 로컬 installed.json 읽기
|
|
124
|
+
/** 프로젝트 로컬 installed.json 읽기 */
|
|
125
125
|
function loadInstalled() {
|
|
126
126
|
const file = path_1.default.join(process.cwd(), '.relay', 'installed.json');
|
|
127
127
|
if (!fs_1.default.existsSync(file)) {
|
|
128
128
|
return {};
|
|
129
129
|
}
|
|
130
130
|
try {
|
|
131
|
-
|
|
132
|
-
const registry = JSON.parse(raw);
|
|
133
|
-
return migrateInstalledKeys(registry);
|
|
131
|
+
return JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
|
|
134
132
|
}
|
|
135
133
|
catch {
|
|
136
134
|
return {};
|
|
137
135
|
}
|
|
138
136
|
}
|
|
139
|
-
/**
|
|
140
|
-
* unscoped 키를 감지하여 서버 resolve 없이 가능한 마이그레이션을 수행한다.
|
|
141
|
-
* 서버 resolve가 필요한 경우는 마이그레이션 보류 (다음 기회에 재시도).
|
|
142
|
-
*/
|
|
143
|
-
function migrateInstalledKeys(registry) {
|
|
144
|
-
const unscopedKeys = Object.keys(registry).filter((k) => !(0, slug_js_1.isScopedSlug)(k) && k !== 'relay-core');
|
|
145
|
-
if (unscopedKeys.length === 0)
|
|
146
|
-
return registry;
|
|
147
|
-
// 비동기 서버 resolve 없이는 owner를 알 수 없으므로,
|
|
148
|
-
// loadInstalled는 동기 함수 → 마이그레이션은 비동기 migrateInstalled()로 별도 호출
|
|
149
|
-
return registry;
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* 비동기 마이그레이션: unscoped 키를 서버 resolve하여 scoped 키로 변환.
|
|
153
|
-
* install/update 등 비동기 커맨드에서 호출.
|
|
154
|
-
*/
|
|
155
|
-
async function migrateInstalled() {
|
|
156
|
-
const { resolveSlugFromServer } = await import('./api.js');
|
|
157
|
-
const registry = loadInstalled();
|
|
158
|
-
const teamsDir = path_1.default.join(process.cwd(), '.relay', 'teams');
|
|
159
|
-
let changed = false;
|
|
160
|
-
for (const key of Object.keys(registry)) {
|
|
161
|
-
if ((0, slug_js_1.isScopedSlug)(key) || key === 'relay-core')
|
|
162
|
-
continue;
|
|
163
|
-
try {
|
|
164
|
-
const results = await resolveSlugFromServer(key);
|
|
165
|
-
if (results.length !== 1)
|
|
166
|
-
continue;
|
|
167
|
-
const { owner, name } = results[0];
|
|
168
|
-
const scopedKey = `@${owner}/${name}`;
|
|
169
|
-
// installed.json 키 변환
|
|
170
|
-
registry[scopedKey] = registry[key];
|
|
171
|
-
delete registry[key];
|
|
172
|
-
// 디렉토리 이동
|
|
173
|
-
const oldDir = path_1.default.join(teamsDir, key);
|
|
174
|
-
const newDir = path_1.default.join(teamsDir, owner, name);
|
|
175
|
-
if (fs_1.default.existsSync(oldDir)) {
|
|
176
|
-
fs_1.default.mkdirSync(path_1.default.dirname(newDir), { recursive: true });
|
|
177
|
-
fs_1.default.renameSync(oldDir, newDir);
|
|
178
|
-
// files 배열 업데이트
|
|
179
|
-
registry[scopedKey].files = registry[scopedKey].files.map((f) => f.replace(`/teams/${key}`, `/teams/${owner}/${name}`));
|
|
180
|
-
}
|
|
181
|
-
changed = true;
|
|
182
|
-
}
|
|
183
|
-
catch {
|
|
184
|
-
// 네트워크 오류 등 — 다음 기회에 재시도
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
if (changed) {
|
|
188
|
-
saveInstalled(registry);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
137
|
/** 프로젝트 로컬 installed.json 쓰기 */
|
|
192
138
|
function saveInstalled(registry) {
|
|
193
139
|
ensureProjectRelayDir();
|
|
194
140
|
const file = path_1.default.join(process.cwd(), '.relay', 'installed.json');
|
|
195
141
|
fs_1.default.writeFileSync(file, JSON.stringify(registry, null, 2));
|
|
196
142
|
}
|
|
143
|
+
// ─── 글로벌 레지스트리 ───
|
|
144
|
+
/** 글로벌 installed.json 읽기 (~/.relay/installed.json) */
|
|
145
|
+
function loadGlobalInstalled() {
|
|
146
|
+
const file = path_1.default.join(GLOBAL_RELAY_DIR, 'installed.json');
|
|
147
|
+
if (!fs_1.default.existsSync(file))
|
|
148
|
+
return {};
|
|
149
|
+
try {
|
|
150
|
+
return JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return {};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/** 글로벌 installed.json 쓰기 (~/.relay/installed.json) */
|
|
157
|
+
function saveGlobalInstalled(registry) {
|
|
158
|
+
ensureGlobalRelayDir();
|
|
159
|
+
const file = path_1.default.join(GLOBAL_RELAY_DIR, 'installed.json');
|
|
160
|
+
fs_1.default.writeFileSync(file, JSON.stringify(registry, null, 2));
|
|
161
|
+
}
|
|
162
|
+
/** 글로벌 + 로컬 레지스트리 병합 뷰 */
|
|
163
|
+
function loadMergedInstalled() {
|
|
164
|
+
return { global: loadGlobalInstalled(), local: loadInstalled() };
|
|
165
|
+
}
|
package/dist/lib/installer.d.ts
CHANGED
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
export declare function installTeam(extractedDir: string, installPath: string): string[];
|
|
2
2
|
export declare function uninstallTeam(files: string[]): string[];
|
|
3
|
+
/**
|
|
4
|
+
* 빈 상위 디렉토리를 boundary까지 정리한다.
|
|
5
|
+
* 예: /home/.claude/skills/cardnews/ 가 비었으면 삭제, /home/.claude/skills/는 유지
|
|
6
|
+
*/
|
|
7
|
+
export declare function cleanEmptyParents(filePath: string, boundary: string): void;
|
package/dist/lib/installer.js
CHANGED
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.installTeam = installTeam;
|
|
7
7
|
exports.uninstallTeam = uninstallTeam;
|
|
8
|
+
exports.cleanEmptyParents = cleanEmptyParents;
|
|
8
9
|
const fs_1 = __importDefault(require("fs"));
|
|
9
10
|
const path_1 = __importDefault(require("path"));
|
|
10
11
|
const COPY_DIRS = ['skills', 'agents', 'rules', 'commands'];
|
|
@@ -39,10 +40,16 @@ function uninstallTeam(files) {
|
|
|
39
40
|
const removed = [];
|
|
40
41
|
for (const file of files) {
|
|
41
42
|
try {
|
|
42
|
-
if (fs_1.default.existsSync(file))
|
|
43
|
+
if (!fs_1.default.existsSync(file))
|
|
44
|
+
continue;
|
|
45
|
+
const stat = fs_1.default.statSync(file);
|
|
46
|
+
if (stat.isDirectory()) {
|
|
47
|
+
fs_1.default.rmSync(file, { recursive: true, force: true });
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
43
50
|
fs_1.default.unlinkSync(file);
|
|
44
|
-
removed.push(file);
|
|
45
51
|
}
|
|
52
|
+
removed.push(file);
|
|
46
53
|
}
|
|
47
54
|
catch {
|
|
48
55
|
// best-effort removal
|
|
@@ -50,3 +57,22 @@ function uninstallTeam(files) {
|
|
|
50
57
|
}
|
|
51
58
|
return removed;
|
|
52
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* 빈 상위 디렉토리를 boundary까지 정리한다.
|
|
62
|
+
* 예: /home/.claude/skills/cardnews/ 가 비었으면 삭제, /home/.claude/skills/는 유지
|
|
63
|
+
*/
|
|
64
|
+
function cleanEmptyParents(filePath, boundary) {
|
|
65
|
+
let dir = path_1.default.dirname(filePath);
|
|
66
|
+
while (dir.length > boundary.length && dir.startsWith(boundary)) {
|
|
67
|
+
try {
|
|
68
|
+
const entries = fs_1.default.readdirSync(dir);
|
|
69
|
+
if (entries.length > 0)
|
|
70
|
+
break;
|
|
71
|
+
fs_1.default.rmdirSync(dir);
|
|
72
|
+
dir = path_1.default.dirname(dir);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
package/dist/lib/preamble.d.ts
CHANGED
|
@@ -5,7 +5,9 @@ export declare function generatePreamble(slug: string): string;
|
|
|
5
5
|
*/
|
|
6
6
|
export declare function injectPreamble(filePath: string, slug: string): void;
|
|
7
7
|
/**
|
|
8
|
-
* 팀의
|
|
9
|
-
*
|
|
8
|
+
* 팀의 사용자 진입점 파일에 preamble을 주입한다.
|
|
9
|
+
* - 루트 SKILL.md
|
|
10
|
+
* - user-invocable: true인 서브 스킬 SKILL.md
|
|
11
|
+
* - commands/*.md
|
|
10
12
|
*/
|
|
11
13
|
export declare function injectPreambleToTeam(teamDir: string, slug: string): number;
|
package/dist/lib/preamble.js
CHANGED
|
@@ -54,18 +54,40 @@ function injectPreamble(filePath, slug) {
|
|
|
54
54
|
fs_1.default.writeFileSync(filePath, cleaned);
|
|
55
55
|
}
|
|
56
56
|
/**
|
|
57
|
-
* 팀의
|
|
58
|
-
*
|
|
57
|
+
* 팀의 사용자 진입점 파일에 preamble을 주입한다.
|
|
58
|
+
* - 루트 SKILL.md
|
|
59
|
+
* - user-invocable: true인 서브 스킬 SKILL.md
|
|
60
|
+
* - commands/*.md
|
|
59
61
|
*/
|
|
60
62
|
function injectPreambleToTeam(teamDir, slug) {
|
|
61
63
|
let count = 0;
|
|
62
|
-
// 1. 루트 SKILL.md
|
|
64
|
+
// 1. 루트 SKILL.md
|
|
63
65
|
const rootSkill = path_1.default.join(teamDir, 'SKILL.md');
|
|
64
66
|
if (fs_1.default.existsSync(rootSkill)) {
|
|
65
67
|
injectPreamble(rootSkill, slug);
|
|
66
68
|
count++;
|
|
67
69
|
}
|
|
68
|
-
// 2.
|
|
70
|
+
// 2. user-invocable 서브 스킬 SKILL.md만 (skills/**/SKILL.md)
|
|
71
|
+
const skillsDir = path_1.default.join(teamDir, 'skills');
|
|
72
|
+
if (fs_1.default.existsSync(skillsDir)) {
|
|
73
|
+
function walkSkills(dir) {
|
|
74
|
+
for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
75
|
+
const fullPath = path_1.default.join(dir, entry.name);
|
|
76
|
+
if (entry.isDirectory()) {
|
|
77
|
+
walkSkills(fullPath);
|
|
78
|
+
}
|
|
79
|
+
else if (entry.name === 'SKILL.md') {
|
|
80
|
+
const content = fs_1.default.readFileSync(fullPath, 'utf-8');
|
|
81
|
+
if (/user-invocable:\s*true/i.test(content)) {
|
|
82
|
+
injectPreamble(fullPath, slug);
|
|
83
|
+
count++;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
walkSkills(skillsDir);
|
|
89
|
+
}
|
|
90
|
+
// 3. commands/*.md
|
|
69
91
|
const commandsDir = path_1.default.join(teamDir, 'commands');
|
|
70
92
|
if (fs_1.default.existsSync(commandsDir)) {
|
|
71
93
|
for (const entry of fs_1.default.readdirSync(commandsDir, { withFileTypes: true })) {
|
package/dist/lib/slug.d.ts
CHANGED
|
@@ -17,8 +17,3 @@ export declare function isSimpleSlug(input: string): boolean;
|
|
|
17
17
|
* 단순 slug는 서버에 resolve를 요청한다.
|
|
18
18
|
*/
|
|
19
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;
|
package/dist/lib/slug.js
CHANGED
|
@@ -4,7 +4,6 @@ exports.parseSlug = parseSlug;
|
|
|
4
4
|
exports.isScopedSlug = isScopedSlug;
|
|
5
5
|
exports.isSimpleSlug = isSimpleSlug;
|
|
6
6
|
exports.resolveSlug = resolveSlug;
|
|
7
|
-
exports.findInstalledByName = findInstalledByName;
|
|
8
7
|
const api_js_1 = require("./api.js");
|
|
9
8
|
const SCOPED_SLUG_RE = /^@([a-z0-9](?:[a-z0-9-]*[a-z0-9])?)\/([a-z0-9](?:[a-z0-9-]*[a-z0-9])?)$/;
|
|
10
9
|
const SIMPLE_SLUG_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
@@ -52,20 +51,3 @@ async function resolveSlug(input) {
|
|
|
52
51
|
const list = results.map((r) => ` ${r.full}`).join('\n');
|
|
53
52
|
throw new Error(`'${input}'에 해당하는 팀이 여러 개입니다. 전체 slug를 지정해주세요:\n${list}`);
|
|
54
53
|
}
|
|
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
|
-
}
|
package/dist/types.d.ts
CHANGED
|
@@ -10,6 +10,10 @@ export interface InstalledTeam {
|
|
|
10
10
|
type?: 'team' | 'system';
|
|
11
11
|
/** Space 소속 팀인 경우 Space slug */
|
|
12
12
|
space_slug?: string;
|
|
13
|
+
/** 배치 범위 — 에이전트가 relay deploy-record로 기록 */
|
|
14
|
+
deploy_scope?: 'global' | 'local';
|
|
15
|
+
/** 배치된 파일 절대경로 목록 — 에이전트가 relay deploy-record로 기록 */
|
|
16
|
+
deployed_files?: string[];
|
|
13
17
|
}
|
|
14
18
|
/** 키는 scoped slug 포맷: "@owner/name" */
|
|
15
19
|
export interface InstalledRegistry {
|