relayax-cli 0.2.40 → 0.3.41
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/diff.d.ts +2 -0
- package/dist/commands/diff.js +72 -0
- package/dist/commands/install.js +73 -205
- package/dist/commands/join.d.ts +3 -2
- package/dist/commands/join.js +18 -69
- package/dist/commands/list.js +18 -21
- package/dist/commands/orgs.d.ts +10 -0
- package/dist/commands/orgs.js +128 -0
- package/dist/commands/package.d.ts +2 -0
- package/dist/commands/package.js +287 -0
- package/dist/commands/publish.js +63 -58
- package/dist/commands/search.js +1 -1
- package/dist/commands/spaces.d.ts +0 -1
- package/dist/commands/versions.d.ts +2 -0
- package/dist/commands/versions.js +44 -0
- package/dist/index.js +8 -2
- package/dist/lib/api.d.ts +7 -6
- package/dist/lib/api.js +14 -29
- package/dist/lib/command-adapter.js +147 -51
- package/dist/lib/config.d.ts +9 -4
- package/dist/lib/config.js +105 -23
- package/dist/lib/guide.d.ts +13 -5
- package/dist/lib/guide.js +142 -50
- package/dist/lib/preamble.js +3 -4
- package/dist/lib/slug.d.ts +0 -1
- package/dist/lib/slug.js +3 -7
- package/dist/types.d.ts +2 -2
- package/package.json +1 -1
|
@@ -412,7 +412,7 @@ https://relayax.com/api/registry/{owner}/{slug}/guide.md
|
|
|
412
412
|
"installed_at": "2026-03-20T12:00:00.000Z",
|
|
413
413
|
"scope": "global",
|
|
414
414
|
"deploy_scope": "global",
|
|
415
|
-
"
|
|
415
|
+
"org_slug": null
|
|
416
416
|
}
|
|
417
417
|
]
|
|
418
418
|
}
|
|
@@ -425,19 +425,19 @@ https://relayax.com/api/registry/{owner}/{slug}/guide.md
|
|
|
425
425
|
| @author/team-name | v1.2.0 | 글로벌 | 3/20 |
|
|
426
426
|
|
|
427
427
|
- \`deploy_scope\`가 \`"global"\` → 글로벌, \`"local"\` → 로컬, 없으면 → 미배치
|
|
428
|
-
- \`
|
|
428
|
+
- \`org_slug\`가 있으면 \`[Org: slug]\` 표시
|
|
429
429
|
|
|
430
|
-
### 2.
|
|
430
|
+
### 2. Organization 목록
|
|
431
431
|
|
|
432
|
-
\`relay
|
|
432
|
+
\`relay orgs list --json\` 명령어를 실행합니다.
|
|
433
433
|
|
|
434
434
|
**JSON 응답 구조:**
|
|
435
435
|
\`\`\`json
|
|
436
436
|
{
|
|
437
|
-
"
|
|
437
|
+
"orgs": [
|
|
438
438
|
{
|
|
439
|
-
"slug": "my-
|
|
440
|
-
"name": "내
|
|
439
|
+
"slug": "my-org",
|
|
440
|
+
"name": "내 조직",
|
|
441
441
|
"description": "설명",
|
|
442
442
|
"role": "owner"
|
|
443
443
|
}
|
|
@@ -446,24 +446,24 @@ https://relayax.com/api/registry/{owner}/{slug}/guide.md
|
|
|
446
446
|
\`\`\`
|
|
447
447
|
|
|
448
448
|
**표시:**
|
|
449
|
-
- \`role\`: owner →
|
|
449
|
+
- \`role\`: owner → 오너, admin → 관리자, builder → 빌더, member → 멤버
|
|
450
450
|
${ERROR_HANDLING_GUIDE}
|
|
451
|
-
-
|
|
451
|
+
- Org 조회 실패해도 설치된 팀 목록은 정상 표시합니다 (로컬 데이터).
|
|
452
452
|
|
|
453
|
-
### 3.
|
|
454
|
-
- \`--
|
|
453
|
+
### 3. Org 팀 목록 (옵션)
|
|
454
|
+
- \`--org <slug>\` 인자가 있으면: \`relay list --org <org-slug> --json\`으로 해당 Organization의 팀 목록도 보여줍니다.
|
|
455
455
|
|
|
456
456
|
### 4. 안내
|
|
457
457
|
- 설치된 팀이 없으면 \`/relay-install\`로 팀을 탐색·설치해보라고 안내합니다.
|
|
458
|
-
-
|
|
459
|
-
-
|
|
460
|
-
-
|
|
458
|
+
- Org가 있으면 활용법을 안내합니다:
|
|
459
|
+
- Org 팀 설치: \`relay install @<org-slug>/<team>\`
|
|
460
|
+
- Org 관리: www.relayax.com/orgs/<slug>
|
|
461
461
|
|
|
462
462
|
## 예시
|
|
463
463
|
|
|
464
464
|
사용자: /relay-status
|
|
465
465
|
→ relay list --json 실행
|
|
466
|
-
→ relay
|
|
466
|
+
→ relay orgs list --json 실행 (병렬 가능)
|
|
467
467
|
|
|
468
468
|
**설치된 팀 (2개)**
|
|
469
469
|
|
|
@@ -506,58 +506,154 @@ ${ERROR_HANDLING_GUIDE}
|
|
|
506
506
|
description: '현재 팀 패키지를 relay Space에 배포합니다',
|
|
507
507
|
body: `현재 디렉토리의 에이전트 팀(.relay/)을 분석하고, 보안 점검 및 requirements를 구성한 뒤, 사용가이드를 생성하고 relay Space에 배포합니다.
|
|
508
508
|
|
|
509
|
-
## 사전 준비
|
|
509
|
+
## 사전 준비
|
|
510
510
|
|
|
511
|
-
### 0-1.
|
|
511
|
+
### 0-1. 인증 확인
|
|
512
512
|
|
|
513
|
-
|
|
513
|
+
- \`relay status --json\` 명령어를 실행하여 로그인 상태를 확인합니다.
|
|
514
|
+
- 인증되어 있으면 다음 단계로 진행합니다.
|
|
515
|
+
- 미인증이면 바로 로그인을 진행합니다:
|
|
516
|
+
1. \`relay login\` 실행 (timeout 300초)
|
|
517
|
+
- 브라우저가 자동으로 열리고, 사용자가 로그인을 완료하면 토큰이 자동 저장됩니다.
|
|
518
|
+
2. 완료 후 \`relay status --json\`으로 로그인 성공을 확인합니다.
|
|
519
|
+
|
|
520
|
+
### 0-2. 소스 패키징 (source → .relay/)
|
|
521
|
+
|
|
522
|
+
\`relay package\` CLI 명령을 사용하여 소스 디렉토리의 콘텐츠를 .relay/로 동기화합니다.
|
|
523
|
+
소스 탐색과 파일 비교는 CLI가 결정적으로 처리하고, 에이전트는 결과를 사용자에게 보여주고 흐름을 조율합니다.
|
|
524
|
+
|
|
525
|
+
#### A. 최초 배포 (.relay/relay.yaml이 없음)
|
|
514
526
|
|
|
515
|
-
|
|
527
|
+
##### 1단계: 소스 탐색
|
|
516
528
|
|
|
517
|
-
|
|
529
|
+
\`relay package --init --json\` 실행
|
|
530
|
+
- CLI가 프로젝트에서 에이전트 CLI 디렉토리(.claude/, .codex/, .gemini/ 등)를 자동 탐색합니다.
|
|
531
|
+
- JSON 결과의 \`detected\` 배열에 각 디렉토리별 콘텐츠 요약이 포함됩니다:
|
|
532
|
+
\`\`\`json
|
|
533
|
+
{
|
|
534
|
+
"status": "init_required",
|
|
535
|
+
"detected": [
|
|
536
|
+
{ "source": ".claude", "name": "Claude Code", "summary": { "skills": 2, "commands": 3 }, "fileCount": 8 },
|
|
537
|
+
{ "source": ".codex", "name": "Codex", "summary": { "agents": 1 }, "fileCount": 2 }
|
|
538
|
+
]
|
|
539
|
+
}
|
|
540
|
+
\`\`\`
|
|
518
541
|
|
|
519
|
-
|
|
542
|
+
- **detected가 0개** → "배포 가능한 에이전트 콘텐츠가 없습니다. skills/이나 commands/를 먼저 만들어주세요." 안내 후 중단
|
|
520
543
|
|
|
521
|
-
2
|
|
522
|
-
- question: "팀 이름을 입력해주세요"
|
|
523
|
-
- 기본값으로 현재 디렉토리명을 제안합니다.
|
|
544
|
+
##### 2단계: 콘텐츠 분석 & 팀 포지셔닝
|
|
524
545
|
|
|
525
|
-
|
|
526
|
-
- question: "팀을 한 줄로 설명해주세요 (Space에 표시됩니다)"
|
|
527
|
-
- 기본값으로 적절한 값을 만들어줍니다.
|
|
546
|
+
detected된 소스 디렉토리의 skills/, commands/, agents/, rules/ 파일 **내용을 직접 읽어** 팀의 정체성을 파악합니다.
|
|
528
547
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
548
|
+
**분석 관점:**
|
|
549
|
+
- 이 팀이 **무엇을 하는 팀**인지 (코드 리뷰? QA? 문서 생성? 데이터 분석?)
|
|
550
|
+
- 어떤 **기술 스택/도메인**에 특화되어 있는지 (Supabase? React? Python?)
|
|
551
|
+
- 설치자에게 **어떤 가치**를 제공하는지
|
|
552
|
+
|
|
553
|
+
이 분석을 기반으로 팀을 하나의 "제품"으로 포지셔닝합니다.
|
|
554
|
+
|
|
555
|
+
**중요: 소스 디렉토리 이름(.claude 등)은 인프라 디테일이므로 사용자에게 노출하지 않습니다.**
|
|
556
|
+
사용자에게는 팀의 기능과 정체성 중심으로 질문합니다.
|
|
557
|
+
|
|
558
|
+
##### 3단계: 배포 제안
|
|
559
|
+
|
|
560
|
+
분석 결과를 바탕으로 팀 배포를 제안합니다.
|
|
561
|
+
|
|
562
|
+
**detected가 1개일 때:**
|
|
563
|
+
|
|
564
|
+
**AskUserQuestion 호출:**
|
|
565
|
+
- question: 콘텐츠 분석 기반의 포지셔닝 질문
|
|
566
|
+
- 예시:
|
|
567
|
+
- "Supabase 웹 개발팀으로 배포할까요? (skills 2개, commands 3개)"
|
|
568
|
+
- "코드 리뷰 자동화 팀으로 배포할까요? (skills 1개, agents 2개)"
|
|
569
|
+
- "Next.js QA 테스트팀으로 배포할까요? (commands 5개)"
|
|
570
|
+
- options: \`["배포", "취소"]\`
|
|
571
|
+
|
|
572
|
+
**detected가 여러 개일 때:**
|
|
573
|
+
각 소스의 콘텐츠를 분석하여 서로 다른 팀으로 포지셔닝합니다.
|
|
574
|
+
|
|
575
|
+
**AskUserQuestion 호출:**
|
|
576
|
+
- question: "어떤 팀으로 배포할까요?"
|
|
577
|
+
- options: 콘텐츠 기반 설명 (예: \`["Supabase 웹 개발팀 (skills 2개, commands 3개)", "QA 자동화 에이전트 (agents 1개)"]\`)
|
|
578
|
+
- 디렉토리 이름은 내부적으로만 매핑하고, 사용자에게는 팀의 기능으로 보여줍니다.
|
|
579
|
+
|
|
580
|
+
##### 4단계: 팀 정보 확정
|
|
581
|
+
|
|
582
|
+
포지셔닝 분석을 기반으로 팀 이름과 설명을 제안합니다.
|
|
583
|
+
|
|
584
|
+
**AskUserQuestion 호출:**
|
|
585
|
+
- question: "팀 이름을 확인해주세요"
|
|
586
|
+
- 분석된 포지셔닝에서 자연스러운 팀 이름을 제안합니다 (예: "supabase-web-dev", "code-reviewer")
|
|
587
|
+
- 현재 디렉토리명이 아닌, **콘텐츠 기반** 이름을 기본값으로 제시합니다.
|
|
588
|
+
|
|
589
|
+
**AskUserQuestion 호출:**
|
|
590
|
+
- question: "팀 설명을 확인해주세요 (Space에 표시됩니다)"
|
|
591
|
+
- 분석한 콘텐츠를 기반으로 설치자 관점의 설명을 제안합니다.
|
|
592
|
+
- 좋은 예: "Supabase 기반 웹앱의 DB 마이그레이션, API 개발, 테스트를 자동화합니다"
|
|
593
|
+
- 나쁜 예: ".claude 디렉토리의 skills와 commands를 패키징한 팀"
|
|
594
|
+
|
|
595
|
+
##### 5단계: 초기화 & 패키징
|
|
596
|
+
|
|
597
|
+
자동 처리:
|
|
598
|
+
- \`.relay/relay.yaml\` 생성:
|
|
599
|
+
\`\`\`yaml
|
|
600
|
+
name: <확정된 이름>
|
|
601
|
+
slug: <이름에서 자동 생성 — 소문자, 특수문자→하이픈>
|
|
602
|
+
description: <확정된 설명>
|
|
603
|
+
source: <선택된 소스 디렉토리> # 예: .claude (내부용)
|
|
604
|
+
version: 1.0.0
|
|
605
|
+
tags: []
|
|
606
|
+
\`\`\`
|
|
607
|
+
- \`relay package --source <선택된 소스> --sync --json\` 실행하여 콘텐츠를 .relay/로 복사
|
|
608
|
+
- \`relay init --auto\` 실행하여 글로벌 커맨드 설치 보장
|
|
609
|
+
- 결과를 사용자에게 표시
|
|
610
|
+
|
|
611
|
+
#### B. 재배포 (.relay/relay.yaml이 있음)
|
|
612
|
+
|
|
613
|
+
1. \`relay package --json\` 실행
|
|
614
|
+
- CLI가 relay.yaml의 \`source\` 필드를 읽고, 소스 디렉토리와 .relay/를 파일 해시로 비교합니다.
|
|
615
|
+
- JSON 결과 예시:
|
|
616
|
+
\`\`\`json
|
|
617
|
+
{
|
|
618
|
+
"source": ".claude",
|
|
619
|
+
"sourceName": "Claude Code",
|
|
620
|
+
"synced": false,
|
|
621
|
+
"diff": [
|
|
622
|
+
{ "relPath": "skills/code-review/SKILL.md", "status": "modified" },
|
|
623
|
+
{ "relPath": "commands/deploy.md", "status": "added" },
|
|
624
|
+
{ "relPath": "commands/old-cmd.md", "status": "deleted" }
|
|
625
|
+
],
|
|
626
|
+
"summary": { "added": 1, "modified": 1, "deleted": 1, "unchanged": 5 }
|
|
627
|
+
}
|
|
538
628
|
\`\`\`
|
|
539
|
-
- \`relay init --update\` 실행하여 글로벌 커맨드 설치 보장
|
|
540
629
|
|
|
541
|
-
|
|
630
|
+
2. **변경이 있으면** (added + modified + deleted > 0) → diff를 사용자에게 보여줍니다:
|
|
631
|
+
\`\`\`
|
|
632
|
+
📦 소스 동기화 (.claude/ → .relay/)
|
|
633
|
+
변경: skills/code-review/SKILL.md
|
|
634
|
+
신규: commands/deploy.md
|
|
635
|
+
삭제: commands/old-cmd.md
|
|
636
|
+
유지: 5개 파일
|
|
637
|
+
\`\`\`
|
|
542
638
|
|
|
543
|
-
|
|
639
|
+
**AskUserQuestion 호출:**
|
|
640
|
+
- question: "소스 변경사항을 .relay/에 반영할까요?"
|
|
641
|
+
- options: \`["반영", "변경 확인", "건너뛰기"]\`
|
|
544
642
|
|
|
545
|
-
|
|
546
|
-
-
|
|
547
|
-
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
643
|
+
**응답 처리:**
|
|
644
|
+
- "반영" → \`relay package --sync --json\` 실행하여 동기화
|
|
645
|
+
- "변경 확인" → 변경된 파일의 내용을 직접 읽어 diff를 상세히 보여준 후 다시 AskUserQuestion
|
|
646
|
+
- "건너뛰기" → 현재 .relay/ 그대로 배포
|
|
647
|
+
|
|
648
|
+
3. **변경이 없으면** → "✓ 소스와 동기화 상태입니다." 표시 후 다음 단계로
|
|
551
649
|
|
|
552
|
-
|
|
553
|
-
- .relay/ 디렉토리의 skills/, agents/, rules/, commands/를 탐색합니다.
|
|
554
|
-
- 각 파일의 이름과 description을 추출합니다.
|
|
650
|
+
4. \`source\` 필드가 없으면 → .relay/ 내 콘텐츠를 직접 편집하는 모드로 간주하고 동기화를 건너뜁니다.
|
|
555
651
|
|
|
556
652
|
## 인터랙션 플로우
|
|
557
653
|
|
|
558
|
-
이 커맨드는
|
|
654
|
+
이 커맨드는 4단계 인터랙션으로 진행됩니다. 각 단계에서 반드시 AskUserQuestion 도구를 사용하세요.
|
|
559
655
|
|
|
560
|
-
### Step
|
|
656
|
+
### Step 1. 버전 범프
|
|
561
657
|
|
|
562
658
|
relay.yaml의 현재 \`version\`을 읽고 semver 범프를 제안합니다.
|
|
563
659
|
|
|
@@ -741,5 +837,5 @@ ${ERROR_HANDLING_GUIDE}`,
|
|
|
741
837
|
];
|
|
742
838
|
// ─── Builder Commands (로컬 설치) ───
|
|
743
839
|
// relay-publish가 글로벌로 승격되어 현재 비어있음.
|
|
744
|
-
// relay init --
|
|
840
|
+
// relay init --auto만 실행하면 모든 커맨드가 한번에 업데이트됨.
|
|
745
841
|
exports.BUILDER_COMMANDS = [];
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -22,10 +22,15 @@ export declare function saveTokenData(data: TokenData): void;
|
|
|
22
22
|
export declare function saveToken(token: string): void;
|
|
23
23
|
/**
|
|
24
24
|
* 유효한 access_token을 반환한다.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
25
|
+
*
|
|
26
|
+
* Supabase는 refresh token rotation을 사용하므로:
|
|
27
|
+
* - refresh 시 이전 refresh_token이 무효화됨
|
|
28
|
+
* - 병렬 CLI 호출에서 동시 refresh 방지 필요 (lock)
|
|
29
|
+
* - refresh 성공 시 새 토큰을 즉시 파일에 저장
|
|
30
|
+
*
|
|
31
|
+
* 타이밍:
|
|
32
|
+
* - 만료 10분 전부터 proactive refresh
|
|
33
|
+
* - refresh 실패해도 access_token이 아직 유효하면 계속 사용
|
|
29
34
|
*/
|
|
30
35
|
export declare function getValidToken(): Promise<string | undefined>;
|
|
31
36
|
/** 프로젝트 로컬 installed.json 읽기 */
|
package/dist/lib/config.js
CHANGED
|
@@ -81,56 +81,138 @@ function saveTokenData(data) {
|
|
|
81
81
|
ensureGlobalRelayDir();
|
|
82
82
|
const tokenFile = path_1.default.join(GLOBAL_RELAY_DIR, 'token');
|
|
83
83
|
fs_1.default.writeFileSync(tokenFile, JSON.stringify(data), { mode: 0o600 });
|
|
84
|
+
// writeFileSync mode only applies on creation — fix existing files
|
|
85
|
+
fs_1.default.chmodSync(tokenFile, 0o600);
|
|
84
86
|
}
|
|
85
87
|
function saveToken(token) {
|
|
86
88
|
ensureGlobalRelayDir();
|
|
87
89
|
const tokenFile = path_1.default.join(GLOBAL_RELAY_DIR, 'token');
|
|
88
90
|
fs_1.default.writeFileSync(tokenFile, JSON.stringify({ access_token: token }), { mode: 0o600 });
|
|
91
|
+
fs_1.default.chmodSync(tokenFile, 0o600);
|
|
89
92
|
}
|
|
93
|
+
const LOCK_FILE = path_1.default.join(os_1.default.homedir(), '.relay', '.token.lock');
|
|
94
|
+
const LOCK_TIMEOUT = 15000; // 15s
|
|
90
95
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* 2. expires_at이 아직 유효하면 access_token 반환
|
|
94
|
-
* 3. 만료되었으면 refresh_token으로 갱신 시도
|
|
95
|
-
* 4. 갱신 실패 시 undefined (재로그인 필요)
|
|
96
|
+
* 파일 기반 lock — 여러 CLI 프로세스가 동시에 refresh하는 것을 방지.
|
|
97
|
+
* Supabase refresh token rotation으로 인해 동시 refresh가 치명적.
|
|
96
98
|
*/
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return
|
|
99
|
+
function acquireLock() {
|
|
100
|
+
try {
|
|
101
|
+
// O_EXCL: 파일이 이미 존재하면 실패 (atomic)
|
|
102
|
+
const fd = fs_1.default.openSync(LOCK_FILE, fs_1.default.constants.O_CREAT | fs_1.default.constants.O_EXCL | fs_1.default.constants.O_WRONLY);
|
|
103
|
+
fs_1.default.writeSync(fd, String(Date.now()));
|
|
104
|
+
fs_1.default.closeSync(fd);
|
|
105
|
+
return true;
|
|
104
106
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
catch {
|
|
108
|
+
// lock 파일이 이미 있음 — stale check
|
|
109
|
+
try {
|
|
110
|
+
const content = fs_1.default.readFileSync(LOCK_FILE, 'utf-8');
|
|
111
|
+
const lockTime = Number(content);
|
|
112
|
+
if (Date.now() - lockTime > LOCK_TIMEOUT) {
|
|
113
|
+
// stale lock — 제거 후 재시도
|
|
114
|
+
fs_1.default.unlinkSync(LOCK_FILE);
|
|
115
|
+
return acquireLock();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch { /* ignore */ }
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function releaseLock() {
|
|
123
|
+
try {
|
|
124
|
+
fs_1.default.unlinkSync(LOCK_FILE);
|
|
125
|
+
}
|
|
126
|
+
catch { /* ignore */ }
|
|
127
|
+
}
|
|
128
|
+
async function doRefresh(refreshToken) {
|
|
108
129
|
try {
|
|
109
130
|
const res = await fetch(`${exports.API_URL}/api/auth/refresh`, {
|
|
110
131
|
method: 'POST',
|
|
111
132
|
headers: { 'Content-Type': 'application/json' },
|
|
112
|
-
body: JSON.stringify({ refresh_token:
|
|
133
|
+
body: JSON.stringify({ refresh_token: refreshToken }),
|
|
134
|
+
signal: AbortSignal.timeout(10000),
|
|
113
135
|
});
|
|
114
136
|
if (!res.ok)
|
|
115
|
-
return
|
|
116
|
-
|
|
117
|
-
saveTokenData(refreshed);
|
|
118
|
-
return refreshed.access_token;
|
|
137
|
+
return null;
|
|
138
|
+
return (await res.json());
|
|
119
139
|
}
|
|
120
140
|
catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* 유효한 access_token을 반환한다.
|
|
146
|
+
*
|
|
147
|
+
* Supabase는 refresh token rotation을 사용하므로:
|
|
148
|
+
* - refresh 시 이전 refresh_token이 무효화됨
|
|
149
|
+
* - 병렬 CLI 호출에서 동시 refresh 방지 필요 (lock)
|
|
150
|
+
* - refresh 성공 시 새 토큰을 즉시 파일에 저장
|
|
151
|
+
*
|
|
152
|
+
* 타이밍:
|
|
153
|
+
* - 만료 10분 전부터 proactive refresh
|
|
154
|
+
* - refresh 실패해도 access_token이 아직 유효하면 계속 사용
|
|
155
|
+
*/
|
|
156
|
+
async function getValidToken() {
|
|
157
|
+
// 매번 파일에서 새로 읽음 (다른 프로세스가 갱신했을 수 있으므로)
|
|
158
|
+
const data = loadTokenData();
|
|
159
|
+
if (!data)
|
|
121
160
|
return undefined;
|
|
161
|
+
const now = Date.now() / 1000;
|
|
162
|
+
// expires_at이 없으면(레거시) → 유효하다고 간주
|
|
163
|
+
if (!data.expires_at)
|
|
164
|
+
return data.access_token;
|
|
165
|
+
// 10분 이상 남았으면 → 그대로 사용 (refresh 불필요)
|
|
166
|
+
if (data.expires_at > now + 600) {
|
|
167
|
+
return data.access_token;
|
|
168
|
+
}
|
|
169
|
+
// refresh_token 없으면 만료 전까지만 사용
|
|
170
|
+
if (!data.refresh_token) {
|
|
171
|
+
return data.expires_at > now ? data.access_token : undefined;
|
|
172
|
+
}
|
|
173
|
+
// Refresh 시도 — lock으로 프로세스 간 동시 refresh 방지
|
|
174
|
+
if (acquireLock()) {
|
|
175
|
+
try {
|
|
176
|
+
const refreshed = await doRefresh(data.refresh_token);
|
|
177
|
+
if (refreshed) {
|
|
178
|
+
saveTokenData(refreshed);
|
|
179
|
+
return refreshed.access_token;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
releaseLock();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
// 다른 프로세스가 refresh 중 — 잠시 후 파일에서 다시 읽기
|
|
188
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
189
|
+
const retryData = loadTokenData();
|
|
190
|
+
if (retryData?.expires_at && retryData.expires_at > now + 30) {
|
|
191
|
+
return retryData.access_token;
|
|
192
|
+
}
|
|
122
193
|
}
|
|
194
|
+
// access_token이 아직 유효하면 사용
|
|
195
|
+
return data.expires_at > now ? data.access_token : undefined;
|
|
123
196
|
}
|
|
124
197
|
/**
|
|
125
|
-
*
|
|
126
|
-
*
|
|
198
|
+
* 레거시 키 정규화:
|
|
199
|
+
* - `@spaces/{slug}/{team}` → `@{slug}/{team}` (Space 레거시)
|
|
200
|
+
* - `space_slug` → `org_slug` (필드명 마이그레이션)
|
|
127
201
|
*/
|
|
128
202
|
function normalizeInstalledRegistry(raw) {
|
|
129
203
|
const normalized = {};
|
|
130
204
|
for (const [key, value] of Object.entries(raw)) {
|
|
205
|
+
// @spaces/ 레거시 키 정규화
|
|
131
206
|
const m = key.match(/^@spaces\/([a-z0-9][a-z0-9-]*)\/([a-z0-9][a-z0-9-]*)$/);
|
|
132
207
|
const normalizedKey = m ? `@${m[1]}/${m[2]}` : key;
|
|
133
|
-
|
|
208
|
+
// space_slug → org_slug 필드 마이그레이션
|
|
209
|
+
const entry = { ...value };
|
|
210
|
+
if ('space_slug' in entry) {
|
|
211
|
+
const spaceSlugs = entry;
|
|
212
|
+
entry.org_slug = spaceSlugs.space_slug;
|
|
213
|
+
delete spaceSlugs.space_slug;
|
|
214
|
+
}
|
|
215
|
+
normalized[normalizedKey] = entry;
|
|
134
216
|
}
|
|
135
217
|
return normalized;
|
|
136
218
|
}
|
package/dist/lib/guide.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import type { Requires } from '../commands/publish.js';
|
|
2
|
+
interface CommandEntry {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function generateGuide(config: {
|
|
7
|
+
slug: string;
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
version: string;
|
|
11
|
+
visibility?: string;
|
|
12
|
+
}, commands: CommandEntry[], requires?: Requires): string;
|
|
13
|
+
export {};
|