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
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 = [];
|
|
@@ -59,24 +57,22 @@ const SUCCESS_HTML = `<!DOCTYPE html>
|
|
|
59
57
|
</body></html>`;
|
|
60
58
|
function waitForToken(port) {
|
|
61
59
|
return new Promise((resolve, reject) => {
|
|
60
|
+
const timeout = setTimeout(() => {
|
|
61
|
+
server.close();
|
|
62
|
+
reject(new Error('로그인 시간이 초과되었습니다 (5분)'));
|
|
63
|
+
}, 5 * 60 * 1000);
|
|
62
64
|
const server = http_1.default.createServer(async (req, res) => {
|
|
63
65
|
const url = new URL(req.url ?? '/', `http://localhost:${port}`);
|
|
64
|
-
if (url.pathname === '/callback') {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (req.method === 'POST') {
|
|
68
|
-
const body = await collectBody(req);
|
|
69
|
-
params = parseFormBody(body);
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
params = url.searchParams;
|
|
73
|
-
}
|
|
66
|
+
if (url.pathname === '/callback' && req.method === 'POST') {
|
|
67
|
+
const body = await collectBody(req);
|
|
68
|
+
const params = new URLSearchParams(body);
|
|
74
69
|
const token = params.get('token');
|
|
75
70
|
const refresh_token = params.get('refresh_token') ?? undefined;
|
|
76
71
|
const expires_at_raw = params.get('expires_at');
|
|
77
72
|
const expires_at = expires_at_raw ? Number(expires_at_raw) : undefined;
|
|
78
73
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
79
74
|
res.end(SUCCESS_HTML);
|
|
75
|
+
clearTimeout(timeout);
|
|
80
76
|
server.close();
|
|
81
77
|
if (token) {
|
|
82
78
|
resolve({ token, refresh_token, expires_at });
|
|
@@ -91,11 +87,6 @@ function waitForToken(port) {
|
|
|
91
87
|
}
|
|
92
88
|
});
|
|
93
89
|
server.listen(port, '127.0.0.1');
|
|
94
|
-
// Timeout after 5 minutes
|
|
95
|
-
setTimeout(() => {
|
|
96
|
-
server.close();
|
|
97
|
-
reject(new Error('로그인 시간이 초과되었습니다 (5분)'));
|
|
98
|
-
}, 5 * 60 * 1000);
|
|
99
90
|
});
|
|
100
91
|
}
|
|
101
92
|
function findAvailablePort() {
|
|
@@ -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
|
@@ -481,6 +481,58 @@ function registerPublish(program) {
|
|
|
481
481
|
}));
|
|
482
482
|
process.exit(1);
|
|
483
483
|
}
|
|
484
|
+
// Visibility validation: must be explicitly set
|
|
485
|
+
if (!config.visibility) {
|
|
486
|
+
if (isTTY) {
|
|
487
|
+
const { select: promptSelect } = await import('@inquirer/prompts');
|
|
488
|
+
console.error('\n\x1b[33m⚠ relay.yaml에 visibility가 설정되지 않았습니다.\x1b[0m');
|
|
489
|
+
// Show user's spaces to help decide
|
|
490
|
+
try {
|
|
491
|
+
const { fetchMySpaces } = await import('./spaces.js');
|
|
492
|
+
const spaceToken = opts.token ?? process.env.RELAY_TOKEN ?? await (0, config_js_1.getValidToken)();
|
|
493
|
+
if (spaceToken) {
|
|
494
|
+
const spaces = await fetchMySpaces(spaceToken);
|
|
495
|
+
const nonPersonal = spaces.filter((s) => !s.is_personal);
|
|
496
|
+
if (nonPersonal.length > 0) {
|
|
497
|
+
console.error(`\n 내 Space:`);
|
|
498
|
+
for (const s of nonPersonal) {
|
|
499
|
+
console.error(` \x1b[36m${s.slug}\x1b[0m — ${s.name}`);
|
|
500
|
+
}
|
|
501
|
+
console.error(`\n 비공개로 설정하면 위 Space 멤버만 접근할 수 있습니다.\n`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
catch {
|
|
506
|
+
// Space 조회 실패는 무시 — visibility 선택은 계속 진행
|
|
507
|
+
}
|
|
508
|
+
config.visibility = await promptSelect({
|
|
509
|
+
message: '공개 범위를 선택하세요:',
|
|
510
|
+
choices: [
|
|
511
|
+
{ name: '공개 — 마켓플레이스에 누구나 검색·설치', value: 'public' },
|
|
512
|
+
{ name: '비공개 — Space 멤버만 접근', value: 'private' },
|
|
513
|
+
],
|
|
514
|
+
});
|
|
515
|
+
// Save back to relay.yaml
|
|
516
|
+
const yamlData = js_yaml_1.default.load(yamlContent);
|
|
517
|
+
yamlData.visibility = config.visibility;
|
|
518
|
+
fs_1.default.writeFileSync(relayYamlPath, js_yaml_1.default.dump(yamlData, { lineWidth: 120 }), 'utf-8');
|
|
519
|
+
console.error(` → relay.yaml에 visibility: ${config.visibility} 저장됨\n`);
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
console.error(JSON.stringify({
|
|
523
|
+
error: 'MISSING_VISIBILITY',
|
|
524
|
+
message: 'relay.yaml에 visibility (public 또는 private)를 설정해주세요.',
|
|
525
|
+
}));
|
|
526
|
+
process.exit(1);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
// Confirm visibility before publish
|
|
530
|
+
if (isTTY) {
|
|
531
|
+
const visLabel = config.visibility === 'public'
|
|
532
|
+
? '\x1b[32m공개\x1b[0m (마켓플레이스에 누구나 검색·설치)'
|
|
533
|
+
: '\x1b[33m비공개\x1b[0m (Space 멤버만 접근)';
|
|
534
|
+
console.error(`공개 범위: ${visLabel}`);
|
|
535
|
+
}
|
|
484
536
|
// Profile hint
|
|
485
537
|
if (isTTY) {
|
|
486
538
|
console.error('💡 프로필에 연락처를 설정하면 설치 시 명함이 전달됩니다: www.relayax.com/dashboard/profile');
|
|
@@ -594,6 +646,19 @@ function registerPublish(program) {
|
|
|
594
646
|
console.log(` \x1b[90m└${'─'.repeat(44)}┘\x1b[0m`);
|
|
595
647
|
console.log(`\n \x1b[90m명함 수정: \x1b[36mwww.relayax.com/dashboard/profile\x1b[0m`);
|
|
596
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
|
+
}
|
|
597
662
|
}
|
|
598
663
|
}
|
|
599
664
|
catch (err) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
export interface SpaceInfo {
|
|
3
|
+
id: string;
|
|
4
|
+
slug: string;
|
|
5
|
+
name: string;
|
|
6
|
+
description: string | null;
|
|
7
|
+
is_personal: boolean;
|
|
8
|
+
role: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function fetchMySpaces(token: string): Promise<SpaceInfo[]>;
|
|
11
|
+
export declare function registerSpaces(program: Command): void;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fetchMySpaces = fetchMySpaces;
|
|
4
|
+
exports.registerSpaces = registerSpaces;
|
|
5
|
+
const config_js_1 = require("../lib/config.js");
|
|
6
|
+
async function fetchMySpaces(token) {
|
|
7
|
+
const res = await fetch(`${config_js_1.API_URL}/api/spaces`, {
|
|
8
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
9
|
+
signal: AbortSignal.timeout(8000),
|
|
10
|
+
});
|
|
11
|
+
if (!res.ok) {
|
|
12
|
+
throw new Error(`Space 목록 조회 실패 (${res.status})`);
|
|
13
|
+
}
|
|
14
|
+
return (await res.json());
|
|
15
|
+
}
|
|
16
|
+
function registerSpaces(program) {
|
|
17
|
+
program
|
|
18
|
+
.command('spaces')
|
|
19
|
+
.description('내 Space 목록을 확인합니다')
|
|
20
|
+
.action(async () => {
|
|
21
|
+
const json = program.opts().json ?? false;
|
|
22
|
+
const token = await (0, config_js_1.getValidToken)();
|
|
23
|
+
if (!token) {
|
|
24
|
+
if (json) {
|
|
25
|
+
console.error(JSON.stringify({ error: 'LOGIN_REQUIRED', message: '로그인이 필요합니다. relay login을 먼저 실행하세요.' }));
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
console.error('\x1b[31m오류: 로그인이 필요합니다.\x1b[0m');
|
|
29
|
+
console.error(' relay login을 먼저 실행하세요.');
|
|
30
|
+
}
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const spaces = await fetchMySpaces(token);
|
|
35
|
+
if (json) {
|
|
36
|
+
console.log(JSON.stringify({ spaces }));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const personal = spaces.find((s) => s.is_personal);
|
|
40
|
+
const others = spaces.filter((s) => !s.is_personal);
|
|
41
|
+
if (personal) {
|
|
42
|
+
console.log(`\n\x1b[90m개인 스페이스:\x1b[0m`);
|
|
43
|
+
console.log(` \x1b[36m${personal.slug}\x1b[0m \x1b[1m${personal.name}\x1b[0m`);
|
|
44
|
+
}
|
|
45
|
+
if (others.length > 0) {
|
|
46
|
+
console.log(`\n\x1b[1m내 Space\x1b[0m (${others.length}개):\n`);
|
|
47
|
+
for (const s of others) {
|
|
48
|
+
const role = s.role === 'owner' ? '\x1b[33m소유자\x1b[0m'
|
|
49
|
+
: s.role === 'admin' ? '\x1b[36m관리자\x1b[0m'
|
|
50
|
+
: '\x1b[90m멤버\x1b[0m';
|
|
51
|
+
const desc = s.description
|
|
52
|
+
? ` \x1b[90m${s.description.length > 40 ? s.description.slice(0, 40) + '...' : s.description}\x1b[0m`
|
|
53
|
+
: '';
|
|
54
|
+
console.log(` \x1b[36m${s.slug}\x1b[0m \x1b[1m${s.name}\x1b[0m ${role}${desc}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!personal && others.length === 0) {
|
|
58
|
+
console.log('\nSpace가 없습니다.');
|
|
59
|
+
console.log('\x1b[33m💡 Space를 만들려면: www.relayax.com/spaces/new\x1b[0m');
|
|
60
|
+
}
|
|
61
|
+
if (others.length > 0) {
|
|
62
|
+
console.log(`\n\x1b[33m💡 Space 팀 목록: relay list --space <slug>\x1b[0m`);
|
|
63
|
+
console.log(`\x1b[33m💡 비공개 배포: relay.yaml에 visibility: private 설정 후 relay publish\x1b[0m`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
68
|
+
if (json) {
|
|
69
|
+
console.error(JSON.stringify({ error: 'FETCH_FAILED', message }));
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
console.error(`\x1b[31m오류: ${message}\x1b[0m`);
|
|
73
|
+
}
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|