relayax-cli 0.4.34 → 0.4.35
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/dist/commands/publish.js
CHANGED
|
@@ -16,6 +16,7 @@ const error_report_js_1 = require("../lib/error-report.js");
|
|
|
16
16
|
const step_tracker_js_1 = require("../lib/step-tracker.js");
|
|
17
17
|
const git_operations_js_1 = require("../lib/git-operations.js");
|
|
18
18
|
const setup_command_js_1 = require("../lib/setup-command.js");
|
|
19
|
+
const requires_suggest_js_1 = require("../lib/requires-suggest.js");
|
|
19
20
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
20
21
|
const cliPkg = require('../../package.json');
|
|
21
22
|
const VALID_DIRS = ['skills', 'agents', 'rules', 'commands', 'bin'];
|
|
@@ -681,6 +682,28 @@ function registerPublish(program) {
|
|
|
681
682
|
console.error(` → relay.yaml에 visibility: ${config.visibility} 저장됨 (${visLabelMap[config.visibility]})\n`);
|
|
682
683
|
}
|
|
683
684
|
}
|
|
685
|
+
// ── Requires 자동 감지 + 제안 ──
|
|
686
|
+
if (isTTY && !json) {
|
|
687
|
+
const suggestions = (0, requires_suggest_js_1.suggestRequires)(relayDir, config.requires);
|
|
688
|
+
if (suggestions.length > 0) {
|
|
689
|
+
console.error('\n\x1b[33m⚡ requires에 빠진 항목이 감지되었습니다:\x1b[0m');
|
|
690
|
+
for (const line of (0, requires_suggest_js_1.formatSuggestions)(suggestions)) {
|
|
691
|
+
console.error(line);
|
|
692
|
+
}
|
|
693
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
694
|
+
const addThem = await confirm({
|
|
695
|
+
message: 'requires에 추가할까요?',
|
|
696
|
+
default: true,
|
|
697
|
+
});
|
|
698
|
+
if (addThem) {
|
|
699
|
+
config.requires = (0, requires_suggest_js_1.mergeIntoRequires)(config.requires ?? {}, suggestions);
|
|
700
|
+
const yamlData = js_yaml_1.default.load(fs_1.default.readFileSync(relayYamlPath, 'utf-8'));
|
|
701
|
+
yamlData.requires = config.requires;
|
|
702
|
+
fs_1.default.writeFileSync(relayYamlPath, js_yaml_1.default.dump(yamlData, { lineWidth: 120 }), 'utf-8');
|
|
703
|
+
console.error(' → relay.yaml에 requires 업데이트됨\n');
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
684
707
|
// Generate setup command BEFORE detectCommands so it's included in metadata
|
|
685
708
|
{
|
|
686
709
|
const commandsDir = path_1.default.join(relayDir, 'commands');
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Requires } from '../commands/publish.js';
|
|
2
|
+
interface Suggestion {
|
|
3
|
+
category: 'env' | 'cli' | 'pip' | 'runtime';
|
|
4
|
+
name: string;
|
|
5
|
+
source: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
setup_hint?: string;
|
|
8
|
+
install?: string;
|
|
9
|
+
required?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* .relay/ 디렉토리를 스캔하여 requires에 빠진 항목을 제안한다.
|
|
13
|
+
*/
|
|
14
|
+
export declare function suggestRequires(relayDir: string, currentRequires?: Requires): Suggestion[];
|
|
15
|
+
/**
|
|
16
|
+
* 제안 항목을 사람이 읽기 좋은 형태로 포맷한다.
|
|
17
|
+
*/
|
|
18
|
+
export declare function formatSuggestions(suggestions: Suggestion[]): string[];
|
|
19
|
+
/**
|
|
20
|
+
* 제안 항목을 requires 객체에 병합한다.
|
|
21
|
+
*/
|
|
22
|
+
export declare function mergeIntoRequires(requires: Requires, suggestions: Suggestion[]): Requires;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,295 @@
|
|
|
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.suggestRequires = suggestRequires;
|
|
7
|
+
exports.formatSuggestions = formatSuggestions;
|
|
8
|
+
exports.mergeIntoRequires = mergeIntoRequires;
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
// Python stdlib — 이 목록에 없는 import는 pip 패키지 후보
|
|
12
|
+
const PYTHON_STDLIB = new Set([
|
|
13
|
+
'abc', 'argparse', 'ast', 'asyncio', 'base64', 'bisect', 'calendar',
|
|
14
|
+
'cmath', 'codecs', 'collections', 'concurrent', 'contextlib', 'copy',
|
|
15
|
+
'csv', 'ctypes', 'dataclasses', 'datetime', 'decimal', 'difflib',
|
|
16
|
+
'email', 'enum', 'errno', 'filecmp', 'fnmatch', 'fractions', 'ftplib',
|
|
17
|
+
'functools', 'gc', 'gettext', 'glob', 'gzip', 'hashlib', 'heapq',
|
|
18
|
+
'hmac', 'html', 'http', 'imaplib', 'importlib', 'inspect', 'io',
|
|
19
|
+
'itertools', 'json', 'keyword', 'linecache', 'locale', 'logging',
|
|
20
|
+
'lzma', 'math', 'mimetypes', 'multiprocessing', 'numbers', 'operator',
|
|
21
|
+
'os', 'pathlib', 'pickle', 'platform', 'plistlib', 'pprint',
|
|
22
|
+
'profile', 'pstats', 'queue', 'random', 're', 'readline',
|
|
23
|
+
'reprlib', 'resource', 'rlcompleter', 'sched', 'secrets', 'select',
|
|
24
|
+
'shelve', 'shlex', 'shutil', 'signal', 'site', 'smtplib', 'socket',
|
|
25
|
+
'socketserver', 'sqlite3', 'ssl', 'stat', 'statistics', 'string',
|
|
26
|
+
'struct', 'subprocess', 'sys', 'sysconfig', 'syslog', 'tempfile',
|
|
27
|
+
'textwrap', 'threading', 'time', 'timeit', 'token', 'tokenize',
|
|
28
|
+
'tomllib', 'trace', 'traceback', 'tracemalloc', 'types', 'typing',
|
|
29
|
+
'unicodedata', 'unittest', 'urllib', 'uuid', 'venv', 'warnings',
|
|
30
|
+
'wave', 'weakref', 'webbrowser', 'xml', 'xmlrpc', 'zipfile', 'zipimport', 'zlib',
|
|
31
|
+
// 자주 쓰이는 서브모듈
|
|
32
|
+
'os.path', 'collections.abc', 'concurrent.futures', 'urllib.parse',
|
|
33
|
+
'http.client', 'http.server', 'email.mime',
|
|
34
|
+
]);
|
|
35
|
+
// pip 패키지명 → import 이름 매핑 (다른 경우만)
|
|
36
|
+
const PIP_IMPORT_MAP = {
|
|
37
|
+
'Pillow': 'PIL',
|
|
38
|
+
'google-genai': 'google',
|
|
39
|
+
'scikit-learn': 'sklearn',
|
|
40
|
+
'python-dotenv': 'dotenv',
|
|
41
|
+
'beautifulsoup4': 'bs4',
|
|
42
|
+
'opencv-python': 'cv2',
|
|
43
|
+
'pyyaml': 'yaml',
|
|
44
|
+
};
|
|
45
|
+
// import 이름 → pip 패키지명 역매핑
|
|
46
|
+
const IMPORT_TO_PIP = {};
|
|
47
|
+
for (const [pip, imp] of Object.entries(PIP_IMPORT_MAP)) {
|
|
48
|
+
IMPORT_TO_PIP[imp] = pip;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* .relay/ 디렉토리를 스캔하여 requires에 빠진 항목을 제안한다.
|
|
52
|
+
*/
|
|
53
|
+
function suggestRequires(relayDir, currentRequires) {
|
|
54
|
+
const suggestions = [];
|
|
55
|
+
const existing = normalizeExisting(currentRequires);
|
|
56
|
+
// 모든 스크립트 파일 수집
|
|
57
|
+
const scripts = findScripts(relayDir);
|
|
58
|
+
for (const scriptPath of scripts) {
|
|
59
|
+
const content = fs_1.default.readFileSync(scriptPath, 'utf-8');
|
|
60
|
+
const relName = path_1.default.relative(relayDir, scriptPath);
|
|
61
|
+
const ext = path_1.default.extname(scriptPath);
|
|
62
|
+
// Python 스크립트
|
|
63
|
+
if (ext === '.py') {
|
|
64
|
+
// 환경변수 감지: os.environ.get("VAR") / os.environ["VAR"] / os.getenv("VAR")
|
|
65
|
+
const envPattern = /os\.environ\.get\(\s*['"](\w+)['"]/g;
|
|
66
|
+
const envPattern2 = /os\.environ\[['"](\w+)['"]\]/g;
|
|
67
|
+
const envPattern3 = /os\.getenv\(\s*['"](\w+)['"]/g;
|
|
68
|
+
for (const pattern of [envPattern, envPattern2, envPattern3]) {
|
|
69
|
+
let match;
|
|
70
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
71
|
+
const varName = match[1];
|
|
72
|
+
if (!existing.envNames.has(varName)) {
|
|
73
|
+
suggestions.push({ category: 'env', name: varName, source: relName });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// pip 패키지 감지: import xxx / from xxx import
|
|
78
|
+
const importPattern = /^(?:import|from)\s+(\w+)/gm;
|
|
79
|
+
let match;
|
|
80
|
+
while ((match = importPattern.exec(content)) !== null) {
|
|
81
|
+
const moduleName = match[1];
|
|
82
|
+
if (PYTHON_STDLIB.has(moduleName))
|
|
83
|
+
continue;
|
|
84
|
+
const pipName = IMPORT_TO_PIP[moduleName] ?? moduleName;
|
|
85
|
+
if (!existing.pipNames.has(pipName) && !existing.pipNames.has(moduleName)) {
|
|
86
|
+
suggestions.push({ category: 'pip', name: pipName, source: relName });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// shebang → python3 런타임
|
|
90
|
+
if (content.startsWith('#!/') && content.includes('python')) {
|
|
91
|
+
if (!existing.hasRuntime.python) {
|
|
92
|
+
suggestions.push({ category: 'runtime', name: 'python', source: relName });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Node/TS 스크립트
|
|
97
|
+
if (ext === '.js' || ext === '.ts' || ext === '.mjs') {
|
|
98
|
+
// 환경변수 감지: process.env.VAR / process.env['VAR']
|
|
99
|
+
const envPattern = /process\.env\.(\w+)/g;
|
|
100
|
+
const envPattern2 = /process\.env\[['"](\w+)['"]\]/g;
|
|
101
|
+
for (const pattern of [envPattern, envPattern2]) {
|
|
102
|
+
let match;
|
|
103
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
104
|
+
const varName = match[1];
|
|
105
|
+
if (['NODE_ENV', 'PATH', 'HOME', 'PWD'].includes(varName))
|
|
106
|
+
continue;
|
|
107
|
+
if (!existing.envNames.has(varName)) {
|
|
108
|
+
suggestions.push({ category: 'env', name: varName, source: relName });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// setup-* 스킬 감지: 관련 env가 requires에 없으면 제안
|
|
115
|
+
const skillsDir = path_1.default.join(relayDir, 'skills');
|
|
116
|
+
if (fs_1.default.existsSync(skillsDir)) {
|
|
117
|
+
for (const entry of fs_1.default.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
118
|
+
if (entry.isDirectory() && entry.name.startsWith('setup-')) {
|
|
119
|
+
const setupName = entry.name; // e.g., setup-kling
|
|
120
|
+
const skillMd = path_1.default.join(skillsDir, setupName, 'SKILL.md');
|
|
121
|
+
if (fs_1.default.existsSync(skillMd)) {
|
|
122
|
+
const skillContent = fs_1.default.readFileSync(skillMd, 'utf-8');
|
|
123
|
+
// SKILL.md에서 환경변수 참조 찾기
|
|
124
|
+
const envRefs = /[A-Z][A-Z0-9_]{2,}/g;
|
|
125
|
+
let match;
|
|
126
|
+
while ((match = envRefs.exec(skillContent)) !== null) {
|
|
127
|
+
const varName = match[0];
|
|
128
|
+
if (['SKILL', 'WHEN', 'THEN', 'SHALL', 'MUST', 'TODO', 'NOTE', 'IMPORTANT', 'WARNING', 'ERROR', 'JSON', 'API', 'URL', 'HTTP', 'HTTPS', 'MCP', 'CLI', 'SDK', 'README', 'YAML'].includes(varName))
|
|
129
|
+
continue;
|
|
130
|
+
if (varName.endsWith('_KEY') || varName.endsWith('_TOKEN') || varName.endsWith('_SECRET') || varName.endsWith('_SESSION')) {
|
|
131
|
+
if (!existing.envNames.has(varName)) {
|
|
132
|
+
suggestions.push({
|
|
133
|
+
category: 'env',
|
|
134
|
+
name: varName,
|
|
135
|
+
source: `skills/${setupName}/SKILL.md`,
|
|
136
|
+
setup_hint: `/${setupName} 스킬을 실행하세요`,
|
|
137
|
+
required: false,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// CLI 도구 감지: subprocess.run(['cmd', ...]) / execSync('cmd ...')
|
|
147
|
+
for (const scriptPath of scripts) {
|
|
148
|
+
const content = fs_1.default.readFileSync(scriptPath, 'utf-8');
|
|
149
|
+
const relName = path_1.default.relative(relayDir, scriptPath);
|
|
150
|
+
// Python subprocess
|
|
151
|
+
const subprocessPattern = /subprocess\.(?:run|Popen|call)\(\s*\[?\s*['"](\w+)['"]/g;
|
|
152
|
+
let match;
|
|
153
|
+
while ((match = subprocessPattern.exec(content)) !== null) {
|
|
154
|
+
const cmd = match[1];
|
|
155
|
+
if (['python', 'python3', 'node', 'npm', 'npx'].includes(cmd))
|
|
156
|
+
continue;
|
|
157
|
+
if (!existing.cliNames.has(cmd)) {
|
|
158
|
+
suggestions.push({ category: 'cli', name: cmd, source: relName });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Node execSync
|
|
162
|
+
const execPattern = /execSync\(\s*['"`](\w+)/g;
|
|
163
|
+
while ((match = execPattern.exec(content)) !== null) {
|
|
164
|
+
const cmd = match[1];
|
|
165
|
+
if (['node', 'npm', 'npx', 'git'].includes(cmd))
|
|
166
|
+
continue;
|
|
167
|
+
if (!existing.cliNames.has(cmd)) {
|
|
168
|
+
suggestions.push({ category: 'cli', name: cmd, source: relName });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// 중복 제거
|
|
173
|
+
const seen = new Set();
|
|
174
|
+
return suggestions.filter((s) => {
|
|
175
|
+
const key = `${s.category}:${s.name}`;
|
|
176
|
+
if (seen.has(key))
|
|
177
|
+
return false;
|
|
178
|
+
seen.add(key);
|
|
179
|
+
return true;
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
function normalizeExisting(requires) {
|
|
183
|
+
const envNames = new Set();
|
|
184
|
+
const cliNames = new Set();
|
|
185
|
+
const pipNames = new Set();
|
|
186
|
+
const npmNames = new Set();
|
|
187
|
+
const hasRuntime = { node: false, python: false };
|
|
188
|
+
if (!requires)
|
|
189
|
+
return { envNames, cliNames, pipNames, npmNames, hasRuntime };
|
|
190
|
+
for (const e of requires.env ?? [])
|
|
191
|
+
envNames.add(e.name);
|
|
192
|
+
for (const c of requires.cli ?? [])
|
|
193
|
+
cliNames.add(c.name);
|
|
194
|
+
for (const n of requires.npm ?? [])
|
|
195
|
+
npmNames.add(typeof n === 'string' ? n : n.name);
|
|
196
|
+
// pip은 Requires 타입에 없지만 relay.yaml에 존재할 수 있음
|
|
197
|
+
const raw = requires;
|
|
198
|
+
if (Array.isArray(raw.pip)) {
|
|
199
|
+
for (const p of raw.pip)
|
|
200
|
+
pipNames.add(typeof p === 'string' ? p : p.name);
|
|
201
|
+
}
|
|
202
|
+
if (requires.runtime?.node)
|
|
203
|
+
hasRuntime.node = true;
|
|
204
|
+
if (requires.runtime?.python)
|
|
205
|
+
hasRuntime.python = true;
|
|
206
|
+
return { envNames, cliNames, pipNames, npmNames, hasRuntime };
|
|
207
|
+
}
|
|
208
|
+
function findScripts(dir) {
|
|
209
|
+
const scripts = [];
|
|
210
|
+
if (!fs_1.default.existsSync(dir))
|
|
211
|
+
return scripts;
|
|
212
|
+
function walk(d) {
|
|
213
|
+
for (const entry of fs_1.default.readdirSync(d, { withFileTypes: true })) {
|
|
214
|
+
if (entry.name.startsWith('.'))
|
|
215
|
+
continue;
|
|
216
|
+
const full = path_1.default.join(d, entry.name);
|
|
217
|
+
if (entry.isDirectory()) {
|
|
218
|
+
walk(full);
|
|
219
|
+
}
|
|
220
|
+
else if (/\.(py|js|ts|mjs|sh)$/.test(entry.name)) {
|
|
221
|
+
scripts.push(full);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
walk(dir);
|
|
226
|
+
return scripts;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* 제안 항목을 사람이 읽기 좋은 형태로 포맷한다.
|
|
230
|
+
*/
|
|
231
|
+
function formatSuggestions(suggestions) {
|
|
232
|
+
const lines = [];
|
|
233
|
+
const envs = suggestions.filter((s) => s.category === 'env');
|
|
234
|
+
const clis = suggestions.filter((s) => s.category === 'cli');
|
|
235
|
+
const pips = suggestions.filter((s) => s.category === 'pip');
|
|
236
|
+
const runtimes = suggestions.filter((s) => s.category === 'runtime');
|
|
237
|
+
if (envs.length > 0) {
|
|
238
|
+
lines.push(' 환경변수:');
|
|
239
|
+
for (const e of envs) {
|
|
240
|
+
const hint = e.setup_hint ? ` (${e.setup_hint})` : '';
|
|
241
|
+
lines.push(` ${e.name}${hint} — ${e.source}에서 감지`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (clis.length > 0) {
|
|
245
|
+
lines.push(' CLI 도구:');
|
|
246
|
+
for (const c of clis)
|
|
247
|
+
lines.push(` ${c.name} — ${c.source}에서 감지`);
|
|
248
|
+
}
|
|
249
|
+
if (pips.length > 0) {
|
|
250
|
+
lines.push(' pip 패키지:');
|
|
251
|
+
for (const p of pips)
|
|
252
|
+
lines.push(` ${p.name} — ${p.source}에서 감지`);
|
|
253
|
+
}
|
|
254
|
+
if (runtimes.length > 0) {
|
|
255
|
+
lines.push(' 런타임:');
|
|
256
|
+
for (const r of runtimes)
|
|
257
|
+
lines.push(` ${r.name} — ${r.source}에서 감지`);
|
|
258
|
+
}
|
|
259
|
+
return lines;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* 제안 항목을 requires 객체에 병합한다.
|
|
263
|
+
*/
|
|
264
|
+
function mergeIntoRequires(requires, suggestions) {
|
|
265
|
+
const result = { ...requires };
|
|
266
|
+
for (const s of suggestions) {
|
|
267
|
+
if (s.category === 'env') {
|
|
268
|
+
if (!result.env)
|
|
269
|
+
result.env = [];
|
|
270
|
+
result.env.push({
|
|
271
|
+
name: s.name,
|
|
272
|
+
required: s.required ?? true,
|
|
273
|
+
description: s.description,
|
|
274
|
+
setup_hint: s.setup_hint,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
else if (s.category === 'cli') {
|
|
278
|
+
if (!result.cli)
|
|
279
|
+
result.cli = [];
|
|
280
|
+
result.cli.push({
|
|
281
|
+
name: s.name,
|
|
282
|
+
install: s.install,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
else if (s.category === 'runtime') {
|
|
286
|
+
if (!result.runtime)
|
|
287
|
+
result.runtime = {};
|
|
288
|
+
if (s.name === 'python')
|
|
289
|
+
result.runtime.python = '>=3.8';
|
|
290
|
+
if (s.name === 'node')
|
|
291
|
+
result.runtime.node = '>=18';
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return result;
|
|
295
|
+
}
|