relayax-cli 0.2.22 → 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 +34 -18
- package/dist/commands/publish.js +65 -0
- package/dist/commands/spaces.d.ts +11 -0
- package/dist/commands/spaces.js +77 -0
- package/dist/commands/uninstall.js +57 -11
- package/dist/commands/update.js +9 -8
- package/dist/index.js +4 -0
- package/dist/lib/command-adapter.d.ts +0 -2
- package/dist/lib/command-adapter.js +122 -63
- 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
|
@@ -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
|
@@ -17,6 +17,8 @@ const check_update_js_1 = require("./commands/check-update.js");
|
|
|
17
17
|
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
|
+
const spaces_js_1 = require("./commands/spaces.js");
|
|
21
|
+
const deploy_record_js_1 = require("./commands/deploy-record.js");
|
|
20
22
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
21
23
|
const pkg = require('../package.json');
|
|
22
24
|
const program = new commander_1.Command();
|
|
@@ -40,4 +42,6 @@ program
|
|
|
40
42
|
(0, follow_js_1.registerFollow)(program);
|
|
41
43
|
(0, changelog_js_1.registerChangelog)(program);
|
|
42
44
|
(0, join_js_1.registerJoin)(program);
|
|
45
|
+
(0, spaces_js_1.registerSpaces)(program);
|
|
46
|
+
(0, deploy_record_js_1.registerDeployRecord)(program);
|
|
43
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. 미리보기 + 컨펌
|
|
@@ -391,7 +426,17 @@ requires:
|
|
|
391
426
|
- tags: 팀 특성에 맞는 태그를 추천합니다.
|
|
392
427
|
- 사용자에게 확인: "이대로 배포할까요?"
|
|
393
428
|
|
|
394
|
-
### 7.
|
|
429
|
+
### 7. 공개 범위 확인 (필수)
|
|
430
|
+
- .relay/relay.yaml에 \`visibility\`가 반드시 설정되어 있어야 합니다.
|
|
431
|
+
- 설정되어 있지 않으면 빌더에게 반드시 물어봅니다:
|
|
432
|
+
- "공개 (마켓플레이스에 누구나 검색·설치 가능)" vs "비공개 (Space 멤버만 접근)"
|
|
433
|
+
- 선택 결과를 relay.yaml에 저장합니다.
|
|
434
|
+
- 이미 설정되어 있으면 현재 값을 확인합니다:
|
|
435
|
+
- 공개인 경우: "⚠ 이 팀은 **공개**로 설정되어 있어 마켓플레이스에 노출됩니다. 맞나요?"
|
|
436
|
+
- 비공개인 경우: "이 팀은 **비공개**로 설정되어 Space 멤버만 접근 가능합니다."
|
|
437
|
+
- 빌더가 변경을 원하면 relay.yaml을 업데이트합니다.
|
|
438
|
+
|
|
439
|
+
### 8. .relay/relay.yaml 업데이트
|
|
395
440
|
- 메타데이터, requires, 포트폴리오 슬롯을 .relay/relay.yaml에 반영합니다.
|
|
396
441
|
|
|
397
442
|
\`\`\`yaml
|
|
@@ -406,9 +451,23 @@ portfolio:
|
|
|
406
451
|
title: "카드뉴스 예시"
|
|
407
452
|
\`\`\`
|
|
408
453
|
|
|
409
|
-
###
|
|
454
|
+
### 9. 배포
|
|
410
455
|
- \`relay publish\` 명령어를 실행합니다.
|
|
411
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
|
+
- "이 블록을 팀원에게 공유하면 바로 설치할 수 있습니다"라고 안내합니다.
|
|
412
471
|
${BUSINESS_CARD_FORMAT}
|
|
413
472
|
|
|
414
473
|
## 예시
|
|
@@ -422,8 +481,8 @@ ${BUSINESS_CARD_FORMAT}
|
|
|
422
481
|
→ GUIDE.html 생성 → 브라우저에서 미리보기 → 빌더 컨펌
|
|
423
482
|
→ GUIDE.html 스크린샷 → gallery 첫 번째 이미지로 등록
|
|
424
483
|
→ relay publish 실행
|
|
425
|
-
→ "배포 완료! URL: https://relayax.com/teams/my-team"
|
|
484
|
+
→ "배포 완료! URL: https://relayax.com/teams/my-team"
|
|
485
|
+
→ 온보딩 가이드 코드블록 표시
|
|
486
|
+
→ "이 블록을 팀원에게 공유하면 바로 설치할 수 있습니다"`,
|
|
426
487
|
},
|
|
427
488
|
];
|
|
428
|
-
/** 하위 호환 — 기존 코드에서 RELAY_COMMANDS를 참조하는 경우 */
|
|
429
|
-
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
|
+
}
|