relayax-cli 0.1.98 → 0.1.991
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/check-update.d.ts +2 -0
- package/dist/commands/check-update.js +61 -0
- package/dist/commands/install.js +46 -1
- package/dist/commands/publish.js +31 -3
- package/dist/commands/update.js +3 -0
- package/dist/index.js +2 -0
- package/dist/lib/api.d.ts +1 -0
- package/dist/lib/api.js +19 -0
- package/dist/lib/command-adapter.js +24 -7
- package/dist/lib/device-hash.d.ts +1 -0
- package/dist/lib/device-hash.js +16 -0
- package/dist/lib/preamble.d.ts +3 -0
- package/dist/lib/preamble.js +63 -0
- package/dist/lib/update-cache.d.ts +2 -0
- package/dist/lib/update-cache.js +51 -0
- package/dist/lib/version-check.d.ts +10 -0
- package/dist/lib/version-check.js +74 -0
- package/dist/types.d.ts +15 -0
- package/package.json +1 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerCheckUpdate = registerCheckUpdate;
|
|
4
|
+
const version_check_js_1 = require("../lib/version-check.js");
|
|
5
|
+
function registerCheckUpdate(program) {
|
|
6
|
+
program
|
|
7
|
+
.command('check-update [slug]')
|
|
8
|
+
.description('CLI 및 설치된 팀의 업데이트를 확인합니다')
|
|
9
|
+
.option('--quiet', '업데이트가 있을 때만 머신 리더블 출력')
|
|
10
|
+
.option('--force', '캐시를 무시하고 강제 체크')
|
|
11
|
+
.action(async (slug, opts) => {
|
|
12
|
+
const quiet = opts.quiet ?? false;
|
|
13
|
+
const force = opts.force ?? false;
|
|
14
|
+
// CLI version check
|
|
15
|
+
const cliResult = await (0, version_check_js_1.checkCliVersion)(force);
|
|
16
|
+
if (cliResult) {
|
|
17
|
+
if (quiet) {
|
|
18
|
+
console.log(`CLI_UPGRADE_AVAILABLE ${cliResult.current} ${cliResult.latest}`);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
console.log(`\n\x1b[33m⚠ relay v${cliResult.latest} available\x1b[0m (현재 v${cliResult.current})`);
|
|
22
|
+
console.log(` 실행: npm update -g relayax-cli\n`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// Team version check
|
|
26
|
+
if (slug) {
|
|
27
|
+
const teamResult = await (0, version_check_js_1.checkTeamVersion)(slug, force);
|
|
28
|
+
if (teamResult) {
|
|
29
|
+
if (quiet) {
|
|
30
|
+
const byAuthor = teamResult.author ? ` ${teamResult.author}` : '';
|
|
31
|
+
console.log(`TEAM_UPGRADE_AVAILABLE ${slug} ${teamResult.current} ${teamResult.latest}${byAuthor}`);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
const byAuthor = teamResult.author ? ` \x1b[90m(by @${teamResult.author})\x1b[0m` : '';
|
|
35
|
+
console.log(`\x1b[33m⚠ ${slug} v${teamResult.latest} available\x1b[0m${byAuthor} (현재 v${teamResult.current})`);
|
|
36
|
+
console.log(` 실행: relay update ${slug}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else if (!quiet && !cliResult) {
|
|
40
|
+
console.log('모든 것이 최신 상태입니다.');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const teamResults = await (0, version_check_js_1.checkAllTeams)(force);
|
|
45
|
+
for (const result of teamResults) {
|
|
46
|
+
if (quiet) {
|
|
47
|
+
const byAuthor = result.author ? ` ${result.author}` : '';
|
|
48
|
+
console.log(`TEAM_UPGRADE_AVAILABLE ${result.slug} ${result.current} ${result.latest}${byAuthor}`);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
const byAuthor = result.author ? ` \x1b[90m(by @${result.author})\x1b[0m` : '';
|
|
52
|
+
console.log(`\x1b[33m⚠ ${result.slug} v${result.latest} available\x1b[0m${byAuthor} (현재 v${result.current})`);
|
|
53
|
+
console.log(` 실행: relay update ${result.slug}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (!quiet && !cliResult && teamResults.length === 0) {
|
|
57
|
+
console.log('모든 것이 최신 상태입니다.');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
package/dist/commands/install.js
CHANGED
|
@@ -9,6 +9,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
9
9
|
const api_js_1 = require("../lib/api.js");
|
|
10
10
|
const storage_js_1 = require("../lib/storage.js");
|
|
11
11
|
const config_js_1 = require("../lib/config.js");
|
|
12
|
+
const preamble_js_1 = require("../lib/preamble.js");
|
|
12
13
|
function registerInstall(program) {
|
|
13
14
|
program
|
|
14
15
|
.command('install <slug>')
|
|
@@ -61,6 +62,8 @@ function registerInstall(program) {
|
|
|
61
62
|
return count;
|
|
62
63
|
}
|
|
63
64
|
const fileCount = countFiles(teamDir);
|
|
65
|
+
// 5.5. Inject update-check preamble into SKILL.md files
|
|
66
|
+
(0, preamble_js_1.injectPreambleToTeam)(teamDir, slug);
|
|
64
67
|
// 6. Record in installed.json
|
|
65
68
|
const installed = (0, config_js_1.loadInstalled)();
|
|
66
69
|
installed[slug] = {
|
|
@@ -84,7 +87,10 @@ function registerInstall(program) {
|
|
|
84
87
|
console.log(JSON.stringify(result));
|
|
85
88
|
}
|
|
86
89
|
else {
|
|
87
|
-
|
|
90
|
+
const authorUsername = team.author?.username;
|
|
91
|
+
const authorDisplayName = team.author?.display_name ?? authorUsername ?? '';
|
|
92
|
+
const authorSuffix = authorUsername ? ` \x1b[90mby @${authorUsername}\x1b[0m` : '';
|
|
93
|
+
console.log(`\n\x1b[32m✓ ${team.name} 다운로드 완료\x1b[0m v${team.version}${authorSuffix}`);
|
|
88
94
|
console.log(` 위치: \x1b[36m${teamDir}\x1b[0m`);
|
|
89
95
|
console.log(` 파일: ${fileCount}개`);
|
|
90
96
|
if (team.commands.length > 0) {
|
|
@@ -93,6 +99,45 @@ function registerInstall(program) {
|
|
|
93
99
|
console.log(` \x1b[33m/${cmd.name}\x1b[0m - ${cmd.description}`);
|
|
94
100
|
}
|
|
95
101
|
}
|
|
102
|
+
// Builder info block
|
|
103
|
+
if (team.welcome) {
|
|
104
|
+
console.log(`\n ┌─ 💬 ${authorDisplayName}님의 메시지 ${'─'.repeat(Math.max(0, 38 - authorDisplayName.length))}┐`);
|
|
105
|
+
const lines = team.welcome.match(/.{1,50}/g) ?? [team.welcome];
|
|
106
|
+
for (const line of lines) {
|
|
107
|
+
console.log(` │ "${line}"`);
|
|
108
|
+
}
|
|
109
|
+
console.log(` └${'─'.repeat(44)}┘`);
|
|
110
|
+
}
|
|
111
|
+
const contactLinks = team.author?.contact_links ?? {};
|
|
112
|
+
const contactEntries = Object.entries(contactLinks);
|
|
113
|
+
if (contactEntries.length > 0) {
|
|
114
|
+
const parts = contactEntries.map(([key, val]) => `${key}: ${val}`);
|
|
115
|
+
console.log(`\n 연락처: ${parts.join(' | ')}`);
|
|
116
|
+
}
|
|
117
|
+
if (authorUsername) {
|
|
118
|
+
console.log(`\n \x1b[90m👤 프로필: relayax.com/@${authorUsername}\x1b[0m`);
|
|
119
|
+
}
|
|
120
|
+
if (team.latest_post && authorUsername) {
|
|
121
|
+
console.log(` \x1b[90m📝 활용 팁: relayax.com/@${authorUsername}/posts/${team.latest_post.slug}\x1b[0m`);
|
|
122
|
+
}
|
|
123
|
+
// Follow prompt (only when logged in)
|
|
124
|
+
const token = (0, config_js_1.loadToken)();
|
|
125
|
+
if (authorUsername && token) {
|
|
126
|
+
try {
|
|
127
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
128
|
+
const shouldFollow = await confirm({
|
|
129
|
+
message: `@${authorUsername}을 팔로우하시겠습니까? (이메일로 새 소식을 받습니다)`,
|
|
130
|
+
default: true,
|
|
131
|
+
});
|
|
132
|
+
if (shouldFollow) {
|
|
133
|
+
await (0, api_js_1.followBuilder)(authorUsername);
|
|
134
|
+
console.log(`\x1b[32m ✓ @${authorUsername} 팔로우 완료\x1b[0m`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// non-critical: skip on error or non-interactive terminal
|
|
139
|
+
}
|
|
140
|
+
}
|
|
96
141
|
console.log('\n 에이전트가 /relay-install로 환경을 구성합니다.');
|
|
97
142
|
}
|
|
98
143
|
}
|
package/dist/commands/publish.js
CHANGED
|
@@ -10,6 +10,9 @@ const os_1 = __importDefault(require("os"));
|
|
|
10
10
|
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
11
11
|
const tar_1 = require("tar");
|
|
12
12
|
const config_js_1 = require("../lib/config.js");
|
|
13
|
+
const version_check_js_1 = require("../lib/version-check.js");
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
15
|
+
const cliPkg = require('../../package.json');
|
|
13
16
|
const VALID_DIRS = ['skills', 'agents', 'rules', 'commands'];
|
|
14
17
|
const IMAGE_EXTS = ['.png', '.jpg', '.jpeg', '.webp'];
|
|
15
18
|
/** 개별 포트폴리오 이미지 최대 크기 (2 MB) */
|
|
@@ -263,6 +266,30 @@ function registerPublish(program) {
|
|
|
263
266
|
const relayDir = path_1.default.join(teamDir, '.relay');
|
|
264
267
|
const relayYamlPath = path_1.default.join(relayDir, 'relay.yaml');
|
|
265
268
|
const isTTY = Boolean(process.stdin.isTTY) && !json;
|
|
269
|
+
// CLI update check before publish
|
|
270
|
+
if (isTTY) {
|
|
271
|
+
const cliUpdate = await (0, version_check_js_1.checkCliVersion)(true);
|
|
272
|
+
if (cliUpdate) {
|
|
273
|
+
console.error(`\n\x1b[33m⚠ relay v${cliUpdate.latest}이 있습니다\x1b[0m (현재 v${cliUpdate.current})`);
|
|
274
|
+
console.error(' 최신 버전에서는 설치자에게 자동 업데이트 알림이 지원됩니다.');
|
|
275
|
+
console.error(` 업데이트: \x1b[36mnpm update -g relayax-cli\x1b[0m\n`);
|
|
276
|
+
try {
|
|
277
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
278
|
+
const shouldContinue = await confirm({
|
|
279
|
+
message: '현재 버전으로 계속 배포할까요?',
|
|
280
|
+
default: true,
|
|
281
|
+
});
|
|
282
|
+
if (!shouldContinue) {
|
|
283
|
+
console.error('\n배포를 취소했습니다. CLI를 업데이트한 후 다시 시도하세요.');
|
|
284
|
+
process.exit(0);
|
|
285
|
+
}
|
|
286
|
+
console.error('');
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
// non-interactive fallback: continue
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
266
293
|
// Check .relay/relay.yaml exists
|
|
267
294
|
if (!fs_1.default.existsSync(relayYamlPath)) {
|
|
268
295
|
if (!isTTY) {
|
|
@@ -302,9 +329,9 @@ function registerPublish(program) {
|
|
|
302
329
|
{ name: '초대 코드 필요', value: 'invite-only' },
|
|
303
330
|
],
|
|
304
331
|
});
|
|
305
|
-
console.error('\n\x1b[2m💡 프로필에 연락처를 설정하면 설치 시 명함이 전달됩니다: www.relayax.com/dashboard/
|
|
332
|
+
console.error('\n\x1b[2m💡 프로필에 연락처를 설정하면 설치 시 명함이 전달됩니다: www.relayax.com/dashboard/profile\x1b[0m');
|
|
306
333
|
if (visibility === 'invite-only') {
|
|
307
|
-
console.error('\x1b[2m💡 invite-only 팀은 웹 대시보드에서 사용자를 초대하세요: www.relayax.com/dashboard\x1b[0m');
|
|
334
|
+
console.error('\x1b[2m💡 invite-only 팀은 웹 대시보드에서 사용자를 초대하세요: www.relayax.com/dashboard/teams\x1b[0m');
|
|
308
335
|
}
|
|
309
336
|
console.error('');
|
|
310
337
|
const tags = tagsRaw
|
|
@@ -335,7 +362,7 @@ function registerPublish(program) {
|
|
|
335
362
|
}
|
|
336
363
|
// Profile hint
|
|
337
364
|
if (isTTY) {
|
|
338
|
-
console.error('💡 프로필에 연락처를 설정하면 설치 시 명함이 전달됩니다: www.relayax.com/dashboard/
|
|
365
|
+
console.error('💡 프로필에 연락처를 설정하면 설치 시 명함이 전달됩니다: www.relayax.com/dashboard/profile');
|
|
339
366
|
}
|
|
340
367
|
// Validate structure (콘텐츠는 .relay/ 안에 있음)
|
|
341
368
|
const hasDirs = VALID_DIRS.some((d) => {
|
|
@@ -381,6 +408,7 @@ function registerPublish(program) {
|
|
|
381
408
|
changelog: config.changelog,
|
|
382
409
|
requires: config.requires,
|
|
383
410
|
visibility: config.visibility,
|
|
411
|
+
cli_version: cliPkg.version,
|
|
384
412
|
};
|
|
385
413
|
if (!json) {
|
|
386
414
|
console.error(`패키지 생성 중... (${config.name} v${config.version})`);
|
package/dist/commands/update.js
CHANGED
|
@@ -5,6 +5,7 @@ const api_js_1 = require("../lib/api.js");
|
|
|
5
5
|
const storage_js_1 = require("../lib/storage.js");
|
|
6
6
|
const installer_js_1 = require("../lib/installer.js");
|
|
7
7
|
const config_js_1 = require("../lib/config.js");
|
|
8
|
+
const preamble_js_1 = require("../lib/preamble.js");
|
|
8
9
|
function registerUpdate(program) {
|
|
9
10
|
program
|
|
10
11
|
.command('update <slug>')
|
|
@@ -54,6 +55,8 @@ function registerUpdate(program) {
|
|
|
54
55
|
await (0, storage_js_1.extractPackage)(tarPath, extractDir);
|
|
55
56
|
// Copy files to install_path
|
|
56
57
|
const files = (0, installer_js_1.installTeam)(extractDir, installPath);
|
|
58
|
+
// Inject update-check preamble into SKILL.md files
|
|
59
|
+
(0, preamble_js_1.injectPreambleToTeam)(installPath, slug);
|
|
57
60
|
// Update installed.json with new version
|
|
58
61
|
installed[slug] = {
|
|
59
62
|
version: latestVersion,
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ const publish_js_1 = require("./commands/publish.js");
|
|
|
13
13
|
const login_js_1 = require("./commands/login.js");
|
|
14
14
|
const update_js_1 = require("./commands/update.js");
|
|
15
15
|
const outdated_js_1 = require("./commands/outdated.js");
|
|
16
|
+
const check_update_js_1 = require("./commands/check-update.js");
|
|
16
17
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
17
18
|
const pkg = require('../package.json');
|
|
18
19
|
const program = new commander_1.Command();
|
|
@@ -32,4 +33,5 @@ program
|
|
|
32
33
|
(0, login_js_1.registerLogin)(program);
|
|
33
34
|
(0, update_js_1.registerUpdate)(program);
|
|
34
35
|
(0, outdated_js_1.registerOutdated)(program);
|
|
36
|
+
(0, check_update_js_1.registerCheckUpdate)(program);
|
|
35
37
|
program.parse();
|
package/dist/lib/api.d.ts
CHANGED
package/dist/lib/api.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.fetchTeamInfo = fetchTeamInfo;
|
|
|
4
4
|
exports.searchTeams = searchTeams;
|
|
5
5
|
exports.fetchTeamVersions = fetchTeamVersions;
|
|
6
6
|
exports.reportInstall = reportInstall;
|
|
7
|
+
exports.followBuilder = followBuilder;
|
|
7
8
|
const config_js_1 = require("./config.js");
|
|
8
9
|
async function fetchTeamInfo(slug) {
|
|
9
10
|
const url = `${config_js_1.API_URL}/api/registry/${slug}`;
|
|
@@ -42,3 +43,21 @@ async function reportInstall(slug) {
|
|
|
42
43
|
// non-critical: ignore errors
|
|
43
44
|
});
|
|
44
45
|
}
|
|
46
|
+
async function followBuilder(username) {
|
|
47
|
+
const token = (0, config_js_1.loadToken)();
|
|
48
|
+
const headers = {
|
|
49
|
+
'Content-Type': 'application/json',
|
|
50
|
+
};
|
|
51
|
+
if (token) {
|
|
52
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
53
|
+
}
|
|
54
|
+
const res = await fetch(`https://www.relayax.com/api/follows`, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers,
|
|
57
|
+
body: JSON.stringify({ following_username: username, email_opt_in: true }),
|
|
58
|
+
});
|
|
59
|
+
if (!res.ok) {
|
|
60
|
+
const body = await res.text();
|
|
61
|
+
throw new Error(`팔로우 실패 (${res.status}): ${body}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -300,13 +300,30 @@ requires:
|
|
|
300
300
|
- .relay/portfolio/cover.png에 저장합니다.
|
|
301
301
|
- 사용자에게 "이 cover를 사용할까요?" 확인. 직접 제공도 가능.
|
|
302
302
|
|
|
303
|
-
#### 슬롯 2: demo (
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
-
|
|
303
|
+
#### 슬롯 2: demo (자동 생성 — 동작 시연)
|
|
304
|
+
에이전트가 **직접 팀의 커맨드를 실행하여** demo를 자동 생성합니다. 사용자에게 묻기 전에 먼저 만듭니다.
|
|
305
|
+
|
|
306
|
+
**생성 절차:**
|
|
307
|
+
1. .relay/commands/ 의 커맨드 목록을 읽고, 가장 대표적인 커맨드를 선택합니다.
|
|
308
|
+
2. 해당 커맨드를 예시 주제/데이터로 실행합니다.
|
|
309
|
+
- 콘텐츠 팀: "라즈베리파이5 신제품 소개" 같은 예시 주제로 커맨드 실행
|
|
310
|
+
- 크롤링 팀: 예시 URL로 실행하며 브라우저 동작을 녹화
|
|
311
|
+
- 분석 팀: 샘플 데이터로 실행하여 결과 캡처
|
|
312
|
+
3. 실행 결과물로 demo를 생성합니다:
|
|
313
|
+
- 여러 장 이미지 (카드뉴스 등): 슬라이드쇼 GIF (각 장 2초 간격)
|
|
314
|
+
- 브라우저 동작: 동작 녹화 GIF
|
|
315
|
+
- 단일 결과물: 결과 이미지를 demo로 사용
|
|
316
|
+
4. 생성된 demo를 사용자에게 보여줍니다: "이 demo를 사용할까요?"
|
|
317
|
+
- 수정 요청 가능: "다른 주제로 다시 만들어줘", "좀 더 짧게"
|
|
318
|
+
- 확정 시 .relay/portfolio/demo.gif에 저장
|
|
319
|
+
|
|
320
|
+
**예시 주제 선택 기준:**
|
|
321
|
+
- relay.yaml의 description, tags에서 도메인 힌트 추출
|
|
322
|
+
- 기존 output/에 결과물이 있으면 같은 주제 활용
|
|
323
|
+
- 없으면 팀 설명에서 가장 그럴듯한 예시 주제를 자체 생성
|
|
324
|
+
|
|
325
|
+
**GIF 규격:** 최대 5MB, .relay/portfolio/demo.gif
|
|
326
|
+
**대안:** 외부 영상 URL (YouTube, Loom)도 가능 — relay.yaml에 기록
|
|
310
327
|
|
|
311
328
|
#### 슬롯 3: gallery (선택 — 결과물 쇼케이스, 최대 5장)
|
|
312
329
|
- 규격: 800x600px 이하, WebP, 각 500KB 이하
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getDeviceHash(): string;
|
|
@@ -0,0 +1,16 @@
|
|
|
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.getDeviceHash = getDeviceHash;
|
|
7
|
+
const crypto_1 = require("crypto");
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
let cachedHash = null;
|
|
10
|
+
function getDeviceHash() {
|
|
11
|
+
if (cachedHash)
|
|
12
|
+
return cachedHash;
|
|
13
|
+
const raw = `${os_1.default.hostname()}:${os_1.default.userInfo().username}`;
|
|
14
|
+
cachedHash = (0, crypto_1.createHash)('sha256').update(raw).digest('hex');
|
|
15
|
+
return cachedHash;
|
|
16
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
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.generatePreamble = generatePreamble;
|
|
7
|
+
exports.injectPreamble = injectPreamble;
|
|
8
|
+
exports.injectPreambleToTeam = injectPreambleToTeam;
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const PREAMBLE_START = '<!-- RELAY_PREAMBLE_START - DO NOT EDIT -->';
|
|
12
|
+
const PREAMBLE_END = '<!-- RELAY_PREAMBLE_END -->';
|
|
13
|
+
function generatePreamble(slug) {
|
|
14
|
+
return `${PREAMBLE_START}
|
|
15
|
+
## Preamble (auto-generated by relay)
|
|
16
|
+
|
|
17
|
+
\`\`\`bash
|
|
18
|
+
relay check-update ${slug} --quiet 2>/dev/null
|
|
19
|
+
\`\`\`
|
|
20
|
+
|
|
21
|
+
If output shows \`CLI_UPGRADE_AVAILABLE <old> <new>\`: tell user "relay v{new} available. Run: npm update -g relayax-cli"
|
|
22
|
+
If output shows \`TEAM_UPGRADE_AVAILABLE\`: tell user "${slug} v{new} available (by @{author}). Run: relay update ${slug}"
|
|
23
|
+
${PREAMBLE_END}`;
|
|
24
|
+
}
|
|
25
|
+
function injectPreamble(filePath, slug) {
|
|
26
|
+
const content = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
27
|
+
const preamble = generatePreamble(slug);
|
|
28
|
+
// Replace existing preamble or prepend
|
|
29
|
+
const startIdx = content.indexOf(PREAMBLE_START);
|
|
30
|
+
const endIdx = content.indexOf(PREAMBLE_END);
|
|
31
|
+
let newContent;
|
|
32
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
33
|
+
// Replace existing
|
|
34
|
+
newContent =
|
|
35
|
+
content.slice(0, startIdx) +
|
|
36
|
+
preamble +
|
|
37
|
+
content.slice(endIdx + PREAMBLE_END.length);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
// Prepend
|
|
41
|
+
newContent = preamble + '\n\n' + content;
|
|
42
|
+
}
|
|
43
|
+
fs_1.default.writeFileSync(filePath, newContent);
|
|
44
|
+
}
|
|
45
|
+
function injectPreambleToTeam(teamDir, slug) {
|
|
46
|
+
let count = 0;
|
|
47
|
+
function walk(dir) {
|
|
48
|
+
if (!fs_1.default.existsSync(dir))
|
|
49
|
+
return;
|
|
50
|
+
for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
51
|
+
const fullPath = path_1.default.join(dir, entry.name);
|
|
52
|
+
if (entry.isDirectory()) {
|
|
53
|
+
walk(fullPath);
|
|
54
|
+
}
|
|
55
|
+
else if (entry.name === 'SKILL.md') {
|
|
56
|
+
injectPreamble(fullPath, slug);
|
|
57
|
+
count++;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
walk(teamDir);
|
|
62
|
+
return count;
|
|
63
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
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.isCacheValid = isCacheValid;
|
|
7
|
+
exports.updateCacheTimestamp = updateCacheTimestamp;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const os_1 = __importDefault(require("os"));
|
|
11
|
+
const RELAY_DIR = path_1.default.join(os_1.default.homedir(), '.relay');
|
|
12
|
+
const CACHE_FILE = path_1.default.join(RELAY_DIR, 'last-update-check');
|
|
13
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
14
|
+
function loadCache() {
|
|
15
|
+
try {
|
|
16
|
+
if (!fs_1.default.existsSync(CACHE_FILE))
|
|
17
|
+
return {};
|
|
18
|
+
return JSON.parse(fs_1.default.readFileSync(CACHE_FILE, 'utf-8'));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function saveCache(data) {
|
|
25
|
+
if (!fs_1.default.existsSync(RELAY_DIR)) {
|
|
26
|
+
fs_1.default.mkdirSync(RELAY_DIR, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
fs_1.default.writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2));
|
|
29
|
+
}
|
|
30
|
+
function isCacheValid(key, force) {
|
|
31
|
+
if (force)
|
|
32
|
+
return false;
|
|
33
|
+
const cache = loadCache();
|
|
34
|
+
const timestamp = key === 'cli' ? cache.cli : cache.teams?.[key];
|
|
35
|
+
if (!timestamp)
|
|
36
|
+
return false;
|
|
37
|
+
return Date.now() - new Date(timestamp).getTime() < CACHE_TTL_MS;
|
|
38
|
+
}
|
|
39
|
+
function updateCacheTimestamp(key) {
|
|
40
|
+
const cache = loadCache();
|
|
41
|
+
const now = new Date().toISOString();
|
|
42
|
+
if (key === 'cli') {
|
|
43
|
+
cache.cli = now;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
if (!cache.teams)
|
|
47
|
+
cache.teams = {};
|
|
48
|
+
cache.teams[key] = now;
|
|
49
|
+
}
|
|
50
|
+
saveCache(cache);
|
|
51
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface UpdateResult {
|
|
2
|
+
type: 'cli' | 'team';
|
|
3
|
+
slug?: string;
|
|
4
|
+
current: string;
|
|
5
|
+
latest: string;
|
|
6
|
+
author?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function checkCliVersion(force?: boolean): Promise<UpdateResult | null>;
|
|
9
|
+
export declare function checkTeamVersion(slug: string, force?: boolean): Promise<UpdateResult | null>;
|
|
10
|
+
export declare function checkAllTeams(force?: boolean): Promise<UpdateResult[]>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkCliVersion = checkCliVersion;
|
|
4
|
+
exports.checkTeamVersion = checkTeamVersion;
|
|
5
|
+
exports.checkAllTeams = checkAllTeams;
|
|
6
|
+
const config_js_1 = require("./config.js");
|
|
7
|
+
const api_js_1 = require("./api.js");
|
|
8
|
+
const update_cache_js_1 = require("./update-cache.js");
|
|
9
|
+
const device_hash_js_1 = require("./device-hash.js");
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
11
|
+
const pkg = require('../../package.json');
|
|
12
|
+
async function checkCliVersion(force) {
|
|
13
|
+
if ((0, update_cache_js_1.isCacheValid)('cli', force))
|
|
14
|
+
return null;
|
|
15
|
+
try {
|
|
16
|
+
const res = await fetch('https://registry.npmjs.org/relayax-cli/latest', {
|
|
17
|
+
signal: AbortSignal.timeout(3000),
|
|
18
|
+
});
|
|
19
|
+
if (!res.ok)
|
|
20
|
+
return null;
|
|
21
|
+
const data = (await res.json());
|
|
22
|
+
(0, update_cache_js_1.updateCacheTimestamp)('cli');
|
|
23
|
+
if (data.version !== pkg.version) {
|
|
24
|
+
return { type: 'cli', current: pkg.version, latest: data.version };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// network error — silently skip
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
async function checkTeamVersion(slug, force) {
|
|
33
|
+
if ((0, update_cache_js_1.isCacheValid)(slug, force))
|
|
34
|
+
return null;
|
|
35
|
+
try {
|
|
36
|
+
const installed = (0, config_js_1.loadInstalled)();
|
|
37
|
+
const entry = installed[slug];
|
|
38
|
+
if (!entry?.version)
|
|
39
|
+
return null;
|
|
40
|
+
const team = await (0, api_js_1.fetchTeamInfo)(slug);
|
|
41
|
+
(0, update_cache_js_1.updateCacheTimestamp)(slug);
|
|
42
|
+
// Send usage ping (fire-and-forget)
|
|
43
|
+
fetch(`${config_js_1.API_URL}/api/registry/${slug}/ping`, {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: { 'Content-Type': 'application/json' },
|
|
46
|
+
body: JSON.stringify({ device_hash: (0, device_hash_js_1.getDeviceHash)() }),
|
|
47
|
+
signal: AbortSignal.timeout(3000),
|
|
48
|
+
}).catch(() => { });
|
|
49
|
+
if (team.version !== entry.version) {
|
|
50
|
+
return {
|
|
51
|
+
type: 'team',
|
|
52
|
+
slug,
|
|
53
|
+
current: entry.version,
|
|
54
|
+
latest: team.version,
|
|
55
|
+
author: team.author?.username,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// network error — silently skip
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
async function checkAllTeams(force) {
|
|
65
|
+
const installed = (0, config_js_1.loadInstalled)();
|
|
66
|
+
const slugs = Object.keys(installed);
|
|
67
|
+
const results = [];
|
|
68
|
+
for (const slug of slugs) {
|
|
69
|
+
const result = await checkTeamVersion(slug, force);
|
|
70
|
+
if (result)
|
|
71
|
+
results.push(result);
|
|
72
|
+
}
|
|
73
|
+
return results;
|
|
74
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface InstalledRegistry {
|
|
|
9
9
|
export interface TeamRegistryInfo {
|
|
10
10
|
slug: string;
|
|
11
11
|
name: string;
|
|
12
|
+
description?: string;
|
|
12
13
|
version: string;
|
|
13
14
|
package_url: string;
|
|
14
15
|
commands: {
|
|
@@ -20,7 +21,21 @@ export interface TeamRegistryInfo {
|
|
|
20
21
|
rules: number;
|
|
21
22
|
skills: number;
|
|
22
23
|
};
|
|
24
|
+
tags?: string[];
|
|
25
|
+
install_count?: number;
|
|
26
|
+
requires?: Record<string, unknown>;
|
|
23
27
|
visibility?: "public" | "login-only" | "invite-only";
|
|
28
|
+
welcome?: string | null;
|
|
29
|
+
contact?: Record<string, string> | null;
|
|
30
|
+
author?: {
|
|
31
|
+
username: string;
|
|
32
|
+
display_name: string | null;
|
|
33
|
+
contact_links: Record<string, string>;
|
|
34
|
+
} | null;
|
|
35
|
+
latest_post?: {
|
|
36
|
+
title: string;
|
|
37
|
+
slug: string;
|
|
38
|
+
} | null;
|
|
24
39
|
}
|
|
25
40
|
export interface SearchResult {
|
|
26
41
|
slug: string;
|