@decencia/ch-cli 1.13.0 → 1.14.0
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/agent/2t-decencia-channel-issue-coder.md +1 -1
- package/agent/2t-decencia-channel-pr-merger.md +1 -1
- package/agent/2t-decencia-channel-terraformer.md +1 -1
- package/dist/commands/screens.migrate.d.ts +0 -139
- package/dist/commands/screens.migrate.d.ts.map +1 -1
- package/dist/commands/screens.migrate.js +104 -429
- package/dist/commands/screens.migrate.js.map +1 -1
- package/package.json +1 -1
- package/skill/2t-decencia-channel-change-manager/SKILL.md +1 -1
- package/skill/2t-decencia-channel-change-propagation-v2/SKILL.md +1 -1
- package/skill/2t-decencia-channel-cli-v2/SKILL.md +5 -3
- package/skill/2t-decencia-channel-db-schema-v2/SKILL.md +1 -1
- package/skill/2t-decencia-channel-github-issue/SKILL.md +1 -1
- package/skill/2t-decencia-channel-orchestrator/SKILL.md +1 -1
- package/skill/2t-decencia-channel-prd-v2/SKILL.md +1 -1
- package/skill/2t-decencia-channel-project-bootstrap/SKILL.md +1 -1
- package/skill/2t-decencia-channel-spec-v2/SKILL.md +1 -1
- package/skill/2t-decencia-channel-sprint-builder-v2/SKILL.md +1 -1
- package/skill/2t-decencia-channel-sprint-runner/SKILL.md +1 -1
- package/skill/2t-decencia-channel-sqa-v2/SKILL.md +1 -1
- package/skill/2t-decencia-channel-work-status-v2/SKILL.md +1 -1
|
@@ -3,7 +3,7 @@ name: 2t-decencia-channel-issue-coder
|
|
|
3
3
|
description: GitHub issue 1개를 입력으로 받아 끝까지 처리하는 코딩 에이전트. 이슈 읽기 → 소통채널 spec/SQA 확인 → git worktree+브랜치에서 구현 → 자체 소프트리뷰 루프 → unittest + SQA → PR 생성 → 소통채널 workStatus·이슈 기록. 다수 병렬 실행 전제, PR까지만(머지·base 브랜치 직접 push·이슈 close는 사람).
|
|
4
4
|
tools: Read, Write, Edit, Grep, Glob, Bash, Agent
|
|
5
5
|
model: opus
|
|
6
|
-
version: 1.
|
|
6
|
+
version: 1.14.0
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
## 역할
|
|
@@ -3,7 +3,7 @@ name: 2t-decencia-channel-pr-merger
|
|
|
3
3
|
description: issue-coder가 낸 PR을 기본 완전 자율로 base 브랜치(기본 브랜치 또는 sprint 통합 브랜치)에 머지하는 에이전트. 핵심 역량은 다수 병렬 worktree가 만든 충돌을 작업 의도 기반(semantic)으로 해결하는 것. CI green+충돌없음이면 바로 squash 머지, 충돌이면 의도대로 해결 후 테스트로 검증하여 머지. 재SQA·전면 재리뷰는 하지 않음. 다수 PR은 직렬 머지.
|
|
4
4
|
tools: Read, Write, Edit, Grep, Glob, Bash
|
|
5
5
|
model: opus
|
|
6
|
-
version: 1.
|
|
6
|
+
version: 1.14.0
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
## 역할
|
|
@@ -3,7 +3,7 @@ name: 2t-decencia-channel-terraformer
|
|
|
3
3
|
description: 소통채널을 쓰지 않던 기존 코드베이스를 역분석하여 소통채널 프로젝트로 편입(terraforming)하는 자율 에이전트. 코드가 single source of truth — 코드를 읽어 DB 스키마 → 기능명세(spec) → PRD를 bottom-up으로 역생성한다(SQA 제외). 대량 분석은 서브에이전트 병렬, 코드로 못 메우는 의도는 [추정]으로 채우고 "사람확인 질문 리스트"를 산출물로 남긴다. PR/머지·이슈 발행은 하지 않음.
|
|
4
4
|
tools: Read, Write, Edit, Grep, Glob, Bash, Agent
|
|
5
5
|
model: opus
|
|
6
|
-
version: 1.
|
|
6
|
+
version: 1.14.0
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
## 역할
|
|
@@ -1,143 +1,4 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
/**
|
|
3
|
-
* `ch screens migrate` — 레거시 프로젝트를 화면 모델로 옮긴다 (SCREEN-006).
|
|
4
|
-
*
|
|
5
|
-
* 하는 일 세 가지, 이 순서로:
|
|
6
|
-
* 1. spec.ui.route를 씨앗으로 화면(screens)을 중복 없이 만든다.
|
|
7
|
-
* 2. 각 명세의 ui.screenRefs에 그 화면 ID를 더한다 (기존 ui 필드는 그대로 다시 실어 보낸다).
|
|
8
|
-
* 3. 전부 성공한 뒤에야 마지막 쓰기로 ch_projects.screensEnabled를 true로 켠다.
|
|
9
|
-
*
|
|
10
|
-
* 지키는 계약:
|
|
11
|
-
* 비파괴 — spec.ui의 기존 필드(figmaNodeId·route·file·access·interactionMap)를 지우지 않는다.
|
|
12
|
-
* PATCH /specs/:id는 ui를 통째로 교체하므로(specs.service의 `updateData['ui'] = ui`)
|
|
13
|
-
* 반드시 읽어서 병합한 뒤 통째로 다시 보낸다. 그래서 롤백은 플래그 한 개만 내리면 된다.
|
|
14
|
-
* --dry-run — 계획만 낸다. 쓰기 요청(POST/PATCH)을 한 건도 보내지 않는다. 읽기는 한다
|
|
15
|
-
* (계획을 서버 데이터로 계산하므로 — 다른 커맨드의 --dry-run과 다른 점이다).
|
|
16
|
-
* 멱등 — 같은 base route의 화면을 두 번 만들지 않고, 이미 걸린 screenRefs는 건드리지 않는다.
|
|
17
|
-
* 출력 — 기계 모드(--json)면 stdout은 계획/결과 JSON 한 문서뿐이고 사람 메시지는 stderr로 간다.
|
|
18
|
-
* 권한 — admin. screensEnabled 토글이 프로젝트 수정(PATCH /projects/current, admin 전용)이다.
|
|
19
|
-
*/
|
|
20
|
-
/** 마이그레이션이 읽는 명세의 최소 형태 (REST 응답의 부분집합). */
|
|
21
|
-
export interface MigrateSpec {
|
|
22
|
-
id: string;
|
|
23
|
-
name?: string;
|
|
24
|
-
epic?: string;
|
|
25
|
-
ui?: {
|
|
26
|
-
route?: string;
|
|
27
|
-
access?: string;
|
|
28
|
-
screenRefs?: string[];
|
|
29
|
-
[key: string]: unknown;
|
|
30
|
-
};
|
|
31
|
-
[key: string]: unknown;
|
|
32
|
-
}
|
|
33
|
-
/** 마이그레이션이 읽는 화면의 최소 형태. */
|
|
34
|
-
export interface MigrateScreen {
|
|
35
|
-
id: string;
|
|
36
|
-
name?: string;
|
|
37
|
-
route?: string;
|
|
38
|
-
order?: number;
|
|
39
|
-
epic?: string;
|
|
40
|
-
access?: string;
|
|
41
|
-
[key: string]: unknown;
|
|
42
|
-
}
|
|
43
|
-
export interface PlannedScreen {
|
|
44
|
-
/** 정규화된 base route (화면의 정체성). */
|
|
45
|
-
baseRoute: string;
|
|
46
|
-
/** create = 새로 만든다, reuse = 같은 base route 화면이 이미 있어 재사용한다. */
|
|
47
|
-
action: 'create' | 'reuse';
|
|
48
|
-
/** reuse면 기존 화면 ID. create면 적용 전까지 null (적용 후 서버가 준 ID로 채워진다). */
|
|
49
|
-
screenId: string | null;
|
|
50
|
-
name: string;
|
|
51
|
-
order: number;
|
|
52
|
-
epic?: string;
|
|
53
|
-
access?: string;
|
|
54
|
-
/** create일 때 화면에 남길 비고. reuse면 기존 비고를 보존하므로 없다. */
|
|
55
|
-
note?: string;
|
|
56
|
-
/** route에서 떼어낸 원문 주석 — 괄호 주석("PRD 탭")과 쿼리/프래그먼트("?tab=screens"). */
|
|
57
|
-
annotations: string[];
|
|
58
|
-
/** 이 base route에 묶인 명세 (ID 오름차순). */
|
|
59
|
-
specIds: string[];
|
|
60
|
-
/** 이 화면 ID를 ui.screenRefs에 새로 넣어야 하는 명세 (이미 걸린 것은 빠진다). */
|
|
61
|
-
linkSpecIds: string[];
|
|
62
|
-
/** 묶인 명세들의 epic 값이 갈릴 때의 후보 (하나로 일치하면 epic에 복사되고 여기는 1개). */
|
|
63
|
-
epicCandidates: string[];
|
|
64
|
-
/** 묶인 명세들의 ui.access 후보. */
|
|
65
|
-
accessCandidates: string[];
|
|
66
|
-
/** 같은 base route를 가진 기존 화면이 둘 이상일 때 그 ID들 (첫 화면을 재사용한다). */
|
|
67
|
-
duplicateScreenIds: string[];
|
|
68
|
-
}
|
|
69
|
-
export interface SkippedSpec {
|
|
70
|
-
id: string;
|
|
71
|
-
reason: string;
|
|
72
|
-
}
|
|
73
|
-
export interface MigrationPlan {
|
|
74
|
-
screens: PlannedScreen[];
|
|
75
|
-
skipped: SkippedSpec[];
|
|
76
|
-
screensEnabled: {
|
|
77
|
-
before: boolean;
|
|
78
|
-
after: boolean;
|
|
79
|
-
changed: boolean;
|
|
80
|
-
};
|
|
81
|
-
summary: {
|
|
82
|
-
specsTotal: number;
|
|
83
|
-
specsWithRoute: number;
|
|
84
|
-
screensToCreate: number;
|
|
85
|
-
screensToReuse: number;
|
|
86
|
-
specsToLink: number;
|
|
87
|
-
specsSkipped: number;
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
export interface NormalizedRoute {
|
|
91
|
-
/** 정규화된 base route. 얻지 못하면 빈 문자열. */
|
|
92
|
-
baseRoute: string;
|
|
93
|
-
/** 정규화하며 떼어낸 원문 (괄호 주석 내용, 쿼리/프래그먼트). 버리지 않고 화면 비고로 옮긴다. */
|
|
94
|
-
annotations: string[];
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* route 문자열을 base route로 정규화한다.
|
|
98
|
-
*
|
|
99
|
-
* "/projects/[projectId]/specs (PRD 탭)" → "/projects/[projectId]/specs" + 주석 "PRD 탭"
|
|
100
|
-
* "/projects/[projectId]/specs?tab=screens" → "/projects/[projectId]/specs" + 주석 "?tab=screens"
|
|
101
|
-
* "/projects/" → "/projects"
|
|
102
|
-
* "/" → "/" (루트는 끝 슬래시 제거의 예외)
|
|
103
|
-
*
|
|
104
|
-
* 탭을 base route에서 떼는 이유: 탭은 라우트가 아니라 같은 화면의 URL 쿼리(?tab=)다. 괄호로
|
|
105
|
-
* 적혔든 쿼리로 적혔든 같은 페이지이므로 화면 하나로 묶는다. 원문은 버리지 않고 화면 비고에
|
|
106
|
-
* 남겨, 나중에 사람이 탭을 독립 화면으로 쪼갤지 판단할 근거로 쓴다.
|
|
107
|
-
*/
|
|
108
|
-
export declare function normalizeRoute(raw: unknown): NormalizedRoute;
|
|
109
|
-
/** 화면 비고: 씨앗 출처 + 묶인 명세 + 버리지 않은 route 주석 + 갈린 epic/access 후보. */
|
|
110
|
-
export declare function buildScreenNote(planned: {
|
|
111
|
-
specIds: string[];
|
|
112
|
-
annotations: string[];
|
|
113
|
-
epicCandidates: string[];
|
|
114
|
-
accessCandidates: string[];
|
|
115
|
-
epic?: string;
|
|
116
|
-
access?: string;
|
|
117
|
-
}): string;
|
|
118
|
-
/**
|
|
119
|
-
* 계획을 만든다 (읽기만 한 데이터로, 부작용 없이).
|
|
120
|
-
*
|
|
121
|
-
* - route가 없거나 ui가 없는 명세는 화면 없는 기능(배치·API·CLI)이므로 사유와 함께 건너뛴다.
|
|
122
|
-
* - 같은 base route는 화면 하나로 묶는다.
|
|
123
|
-
* - 화면 이름은 묶인 명세 중 ID 오름차순 첫 명세의 이름 (사람이 나중에 고치는 씨앗값).
|
|
124
|
-
* - 화면 순서는 base route 사전순 순번.
|
|
125
|
-
* - epic·access는 묶인 명세의 값이 하나로 일치할 때만 복사하고, 갈리면 비운 채 후보를 비고에 남긴다.
|
|
126
|
-
* - 같은 base route의 화면이 이미 있으면 만들지 않고 재사용한다 (멱등).
|
|
127
|
-
* - route를 가진 명세가 0건이면 화면도 만들지 않고 screensEnabled도 켜지 않는다 (빈 화면 탭 방지).
|
|
128
|
-
*/
|
|
129
|
-
export declare function buildMigrationPlan(specs: MigrateSpec[], existingScreens: MigrateScreen[], screensEnabledBefore: boolean): MigrationPlan;
|
|
130
|
-
/** 화면 생성 요청 바디. relatedSpecIds는 서버 전용이라 절대 싣지 않는다(보내면 조용히 버려진다). */
|
|
131
|
-
export declare function buildCreateScreenBody(planned: PlannedScreen): Record<string, unknown>;
|
|
132
|
-
/**
|
|
133
|
-
* 명세에 보낼 ui 병합 결과. 기존 ui를 통째로 다시 실어 보내면서 screenRefs에만 화면 ID를 더한다.
|
|
134
|
-
* PATCH /specs/:id가 ui를 통째로 교체하기 때문에, 이 병합을 빼먹으면 레거시 필드가
|
|
135
|
-
* 에러 없이 사라진다 (비파괴 계약 위반).
|
|
136
|
-
*/
|
|
137
|
-
export declare function mergeScreenRef(ui: Record<string, unknown> | undefined, screenId: string): {
|
|
138
|
-
ui: Record<string, unknown>;
|
|
139
|
-
changed: boolean;
|
|
140
|
-
};
|
|
141
2
|
/**
|
|
142
3
|
* `ch screens migrate` 등록. screens.ts의 register…(screens, program) 규약을 따른다.
|
|
143
4
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"screens.migrate.d.ts","sourceRoot":"","sources":["../../src/commands/screens.migrate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"screens.migrate.d.ts","sourceRoot":"","sources":["../../src/commands/screens.migrate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAyOpC;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,OAAO,GACf,IAAI,CAsEN"}
|
|
@@ -1,276 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.normalizeRoute = normalizeRoute;
|
|
4
|
-
exports.buildScreenNote = buildScreenNote;
|
|
5
|
-
exports.buildMigrationPlan = buildMigrationPlan;
|
|
6
|
-
exports.buildCreateScreenBody = buildCreateScreenBody;
|
|
7
|
-
exports.mergeScreenRef = mergeScreenRef;
|
|
8
3
|
exports.registerScreensMigrate = registerScreensMigrate;
|
|
9
4
|
const client_1 = require("../client");
|
|
10
5
|
const output_1 = require("../output");
|
|
11
|
-
/** ch-api DTO의 길이 제한 (create-screen.dto.ts) — 넘기면 400이라 CLI가 먼저 접는다. */
|
|
12
|
-
const MAX_NAME = 200;
|
|
13
|
-
const MAX_EPIC = 200;
|
|
14
|
-
const MAX_ACCESS = 200;
|
|
15
|
-
/** 괄호 주석: 반각·전각 괄호 모두. route에 섞인 "(PRD 탭)" 같은 사람 메모다. */
|
|
16
|
-
const ROUTE_COMMENT_RE = /[((][^))]*[))]/g;
|
|
17
|
-
/**
|
|
18
|
-
* route 문자열을 base route로 정규화한다.
|
|
19
|
-
*
|
|
20
|
-
* "/projects/[projectId]/specs (PRD 탭)" → "/projects/[projectId]/specs" + 주석 "PRD 탭"
|
|
21
|
-
* "/projects/[projectId]/specs?tab=screens" → "/projects/[projectId]/specs" + 주석 "?tab=screens"
|
|
22
|
-
* "/projects/" → "/projects"
|
|
23
|
-
* "/" → "/" (루트는 끝 슬래시 제거의 예외)
|
|
24
|
-
*
|
|
25
|
-
* 탭을 base route에서 떼는 이유: 탭은 라우트가 아니라 같은 화면의 URL 쿼리(?tab=)다. 괄호로
|
|
26
|
-
* 적혔든 쿼리로 적혔든 같은 페이지이므로 화면 하나로 묶는다. 원문은 버리지 않고 화면 비고에
|
|
27
|
-
* 남겨, 나중에 사람이 탭을 독립 화면으로 쪼갤지 판단할 근거로 쓴다.
|
|
28
|
-
*/
|
|
29
|
-
function normalizeRoute(raw) {
|
|
30
|
-
const annotations = [];
|
|
31
|
-
let s = typeof raw === 'string' ? raw : '';
|
|
32
|
-
// 1. 괄호 주석 분리
|
|
33
|
-
s = s.replace(ROUTE_COMMENT_RE, (match) => {
|
|
34
|
-
const inner = match.slice(1, -1).trim();
|
|
35
|
-
if (inner)
|
|
36
|
-
annotations.push(inner);
|
|
37
|
-
return ' ';
|
|
38
|
-
});
|
|
39
|
-
// 2. 쿼리·프래그먼트 분리 (탭·앵커는 같은 화면이다)
|
|
40
|
-
s = s.trim();
|
|
41
|
-
const cut = s.search(/[?#]/);
|
|
42
|
-
if (cut >= 0) {
|
|
43
|
-
const suffix = s.slice(cut).trim();
|
|
44
|
-
if (suffix)
|
|
45
|
-
annotations.push(suffix);
|
|
46
|
-
s = s.slice(0, cut);
|
|
47
|
-
}
|
|
48
|
-
// 3. 공백·끝 슬래시 정리 (루트 "/"는 예외)
|
|
49
|
-
s = s.trim();
|
|
50
|
-
while (s.length > 1 && s.endsWith('/')) {
|
|
51
|
-
s = s.slice(0, -1);
|
|
52
|
-
}
|
|
53
|
-
return { baseRoute: s, annotations };
|
|
54
|
-
}
|
|
55
|
-
/** 코드유닛 사전순 비교 — 로케일에 흔들리지 않는 결정적 정렬 (ID·route 모두 ASCII). */
|
|
56
|
-
function byAsc(a, b) {
|
|
57
|
-
return a < b ? -1 : a > b ? 1 : 0;
|
|
58
|
-
}
|
|
59
|
-
/** 빈 문자열·공백뿐인 값을 걸러 순서를 유지한 채 중복 제거. */
|
|
60
|
-
function distinctNonEmpty(values) {
|
|
61
|
-
const seen = new Set();
|
|
62
|
-
const out = [];
|
|
63
|
-
for (const v of values) {
|
|
64
|
-
const t = (v ?? '').trim();
|
|
65
|
-
if (!t || seen.has(t))
|
|
66
|
-
continue;
|
|
67
|
-
seen.add(t);
|
|
68
|
-
out.push(t);
|
|
69
|
-
}
|
|
70
|
-
return out;
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* 묶인 명세들의 값이 "하나로 일치할 때만" 복사한다. 갈리면 undefined(비움).
|
|
74
|
-
*
|
|
75
|
-
* 값이 없는 명세는 반대 의견이 아니라 무응답으로 본다 — 셋 중 둘이 "E3"이고 하나가 비어
|
|
76
|
-
* 있으면 후보는 여전히 하나("E3")이므로 복사한다. 서로 다른 값이 둘 이상일 때만 비운다.
|
|
77
|
-
*
|
|
78
|
-
* 길이 제한을 넘는 값도 복사하지 않는다 — 서버 400으로 마이그레이션이 중간에 죽는 것보다
|
|
79
|
-
* 비워 두고 비고에 후보를 남기는 편이 낫다.
|
|
80
|
-
*/
|
|
81
|
-
function unanimous(candidates, maxLength) {
|
|
82
|
-
if (candidates.length !== 1)
|
|
83
|
-
return undefined;
|
|
84
|
-
const only = candidates[0];
|
|
85
|
-
return only.length <= maxLength ? only : undefined;
|
|
86
|
-
}
|
|
87
|
-
/** 화면 비고: 씨앗 출처 + 묶인 명세 + 버리지 않은 route 주석 + 갈린 epic/access 후보. */
|
|
88
|
-
function buildScreenNote(planned) {
|
|
89
|
-
const lines = [
|
|
90
|
-
'ch screens migrate로 생성 (씨앗: spec.ui.route)',
|
|
91
|
-
`묶인 명세: ${planned.specIds.join(', ')}`,
|
|
92
|
-
];
|
|
93
|
-
if (planned.annotations.length > 0) {
|
|
94
|
-
lines.push(`레거시 route 주석: ${planned.annotations.join(' / ')}`);
|
|
95
|
-
}
|
|
96
|
-
// 값이 갈려서 비운 경우(후보 2개 이상)와, 하나뿐이지만 길이 제한을 넘겨 못 실은 경우 모두
|
|
97
|
-
// 후보를 남긴다 — 사람이 나중에 화면을 고칠 때 근거가 된다.
|
|
98
|
-
if (!planned.epic && planned.epicCandidates.length > 0) {
|
|
99
|
-
lines.push(`에픽 후보: ${planned.epicCandidates.join(' / ')}`);
|
|
100
|
-
}
|
|
101
|
-
if (!planned.access && planned.accessCandidates.length > 0) {
|
|
102
|
-
lines.push(`접근 권한 후보: ${planned.accessCandidates.join(' / ')}`);
|
|
103
|
-
}
|
|
104
|
-
return lines.join('\n');
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* 계획을 만든다 (읽기만 한 데이터로, 부작용 없이).
|
|
108
|
-
*
|
|
109
|
-
* - route가 없거나 ui가 없는 명세는 화면 없는 기능(배치·API·CLI)이므로 사유와 함께 건너뛴다.
|
|
110
|
-
* - 같은 base route는 화면 하나로 묶는다.
|
|
111
|
-
* - 화면 이름은 묶인 명세 중 ID 오름차순 첫 명세의 이름 (사람이 나중에 고치는 씨앗값).
|
|
112
|
-
* - 화면 순서는 base route 사전순 순번.
|
|
113
|
-
* - epic·access는 묶인 명세의 값이 하나로 일치할 때만 복사하고, 갈리면 비운 채 후보를 비고에 남긴다.
|
|
114
|
-
* - 같은 base route의 화면이 이미 있으면 만들지 않고 재사용한다 (멱등).
|
|
115
|
-
* - route를 가진 명세가 0건이면 화면도 만들지 않고 screensEnabled도 켜지 않는다 (빈 화면 탭 방지).
|
|
116
|
-
*/
|
|
117
|
-
function buildMigrationPlan(specs, existingScreens, screensEnabledBefore) {
|
|
118
|
-
const skipped = [];
|
|
119
|
-
const groups = new Map();
|
|
120
|
-
const sortedSpecs = [...(specs ?? [])]
|
|
121
|
-
.filter((s) => s && typeof s.id === 'string')
|
|
122
|
-
.sort((a, b) => byAsc(a.id, b.id));
|
|
123
|
-
for (const spec of sortedSpecs) {
|
|
124
|
-
const ui = spec.ui;
|
|
125
|
-
if (!ui || typeof ui !== 'object') {
|
|
126
|
-
skipped.push({ id: spec.id, reason: 'ui 없음 — 화면 없는 기능(배치·API·CLI)' });
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
const raw = typeof ui.route === 'string' ? ui.route : '';
|
|
130
|
-
if (!raw.trim()) {
|
|
131
|
-
skipped.push({ id: spec.id, reason: 'ui.route 없음 — 화면 없는 기능(배치·API·CLI)' });
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
const { baseRoute, annotations } = normalizeRoute(raw);
|
|
135
|
-
if (!baseRoute) {
|
|
136
|
-
skipped.push({
|
|
137
|
-
id: spec.id,
|
|
138
|
-
reason: `route가 주석뿐이라 base route를 얻지 못함 ("${raw.trim()}")`,
|
|
139
|
-
});
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
if (!baseRoute.startsWith('/')) {
|
|
143
|
-
skipped.push({
|
|
144
|
-
id: spec.id,
|
|
145
|
-
reason: `route가 "/"로 시작하지 않아 화면 후보에서 제외 ("${raw.trim()}") — 필요하면 ch screens create로 직접 만드세요`,
|
|
146
|
-
});
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
const group = groups.get(baseRoute) ?? { specs: [], annotations: [] };
|
|
150
|
-
group.specs.push(spec);
|
|
151
|
-
group.annotations.push(...annotations);
|
|
152
|
-
groups.set(baseRoute, group);
|
|
153
|
-
}
|
|
154
|
-
// 기존 화면을 base route로 색인 (멱등의 핵심). route 없는 화면은 마이그레이션 대상 밖이다.
|
|
155
|
-
const screensByRoute = new Map();
|
|
156
|
-
for (const screen of [...(existingScreens ?? [])]
|
|
157
|
-
.filter((s) => s && typeof s.id === 'string')
|
|
158
|
-
.sort((a, b) => byAsc(a.id, b.id))) {
|
|
159
|
-
const { baseRoute } = normalizeRoute(screen.route);
|
|
160
|
-
if (!baseRoute)
|
|
161
|
-
continue;
|
|
162
|
-
const list = screensByRoute.get(baseRoute) ?? [];
|
|
163
|
-
list.push(screen);
|
|
164
|
-
screensByRoute.set(baseRoute, list);
|
|
165
|
-
}
|
|
166
|
-
const baseRoutes = [...groups.keys()].sort(byAsc);
|
|
167
|
-
const screens = baseRoutes.map((baseRoute, index) => {
|
|
168
|
-
const group = groups.get(baseRoute);
|
|
169
|
-
const specIds = group.specs.map((s) => s.id);
|
|
170
|
-
const annotations = distinctNonEmpty(group.annotations);
|
|
171
|
-
const epicCandidates = distinctNonEmpty(group.specs.map((s) => s.epic));
|
|
172
|
-
const accessCandidates = distinctNonEmpty(group.specs.map((s) => s.ui?.access));
|
|
173
|
-
const seedEpic = unanimous(epicCandidates, MAX_EPIC);
|
|
174
|
-
const seedAccess = unanimous(accessCandidates, MAX_ACCESS);
|
|
175
|
-
const seedName = (group.specs[0]?.name ?? '').trim() || baseRoute;
|
|
176
|
-
const name = seedName.slice(0, MAX_NAME);
|
|
177
|
-
const matches = screensByRoute.get(baseRoute) ?? [];
|
|
178
|
-
const existing = matches[0];
|
|
179
|
-
// 재사용 화면은 그대로 둔다(사람이 이미 고쳤을 수 있다) — 그래서 계획에도 "쓸 값"이 아니라
|
|
180
|
-
// "지금 그 화면에 있는 값"을 싣는다. 씨앗값은 새로 만드는 화면에만 쓴다.
|
|
181
|
-
const planned = {
|
|
182
|
-
baseRoute,
|
|
183
|
-
action: existing ? 'reuse' : 'create',
|
|
184
|
-
screenId: existing ? existing.id : null,
|
|
185
|
-
name: existing ? (existing.name ?? name) : name,
|
|
186
|
-
order: existing ? (existing.order ?? index + 1) : index + 1,
|
|
187
|
-
epic: existing ? existing.epic : seedEpic,
|
|
188
|
-
access: existing ? existing.access : seedAccess,
|
|
189
|
-
annotations,
|
|
190
|
-
specIds,
|
|
191
|
-
// 재사용 화면의 비고는 사람이 고쳤을 수 있으니 덮어쓰지 않는다 (create일 때만 씨앗 비고).
|
|
192
|
-
note: existing
|
|
193
|
-
? undefined
|
|
194
|
-
: buildScreenNote({
|
|
195
|
-
specIds,
|
|
196
|
-
annotations,
|
|
197
|
-
epicCandidates,
|
|
198
|
-
accessCandidates,
|
|
199
|
-
epic: seedEpic,
|
|
200
|
-
access: seedAccess,
|
|
201
|
-
}),
|
|
202
|
-
linkSpecIds: group.specs
|
|
203
|
-
.filter((s) => {
|
|
204
|
-
if (!existing)
|
|
205
|
-
return true; // 새 화면 → 묶인 명세 전부 연결
|
|
206
|
-
const refs = Array.isArray(s.ui?.screenRefs) ? s.ui.screenRefs : [];
|
|
207
|
-
return !refs.includes(existing.id); // 이미 그 화면을 가리키면 그대로 둔다
|
|
208
|
-
})
|
|
209
|
-
.map((s) => s.id),
|
|
210
|
-
epicCandidates,
|
|
211
|
-
accessCandidates,
|
|
212
|
-
duplicateScreenIds: matches.length > 1 ? matches.map((s) => s.id) : [],
|
|
213
|
-
};
|
|
214
|
-
return planned;
|
|
215
|
-
});
|
|
216
|
-
const specsToLink = screens.reduce((n, s) => n + s.linkSpecIds.length, 0);
|
|
217
|
-
const after = screens.length > 0 ? true : screensEnabledBefore;
|
|
218
|
-
return {
|
|
219
|
-
screens,
|
|
220
|
-
skipped,
|
|
221
|
-
screensEnabled: {
|
|
222
|
-
before: screensEnabledBefore,
|
|
223
|
-
after,
|
|
224
|
-
changed: after !== screensEnabledBefore,
|
|
225
|
-
},
|
|
226
|
-
summary: {
|
|
227
|
-
specsTotal: sortedSpecs.length,
|
|
228
|
-
specsWithRoute: screens.reduce((n, s) => n + s.specIds.length, 0),
|
|
229
|
-
screensToCreate: screens.filter((s) => s.action === 'create').length,
|
|
230
|
-
screensToReuse: screens.filter((s) => s.action === 'reuse').length,
|
|
231
|
-
specsToLink,
|
|
232
|
-
specsSkipped: skipped.length,
|
|
233
|
-
},
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
/** 화면 생성 요청 바디. relatedSpecIds는 서버 전용이라 절대 싣지 않는다(보내면 조용히 버려진다). */
|
|
237
|
-
function buildCreateScreenBody(planned) {
|
|
238
|
-
return {
|
|
239
|
-
name: planned.name,
|
|
240
|
-
route: planned.baseRoute,
|
|
241
|
-
order: planned.order,
|
|
242
|
-
...(planned.epic ? { epic: planned.epic } : {}),
|
|
243
|
-
...(planned.access ? { access: planned.access } : {}),
|
|
244
|
-
...(planned.note ? { note: planned.note } : {}),
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
/**
|
|
248
|
-
* 명세에 보낼 ui 병합 결과. 기존 ui를 통째로 다시 실어 보내면서 screenRefs에만 화면 ID를 더한다.
|
|
249
|
-
* PATCH /specs/:id가 ui를 통째로 교체하기 때문에, 이 병합을 빼먹으면 레거시 필드가
|
|
250
|
-
* 에러 없이 사라진다 (비파괴 계약 위반).
|
|
251
|
-
*/
|
|
252
|
-
function mergeScreenRef(ui, screenId) {
|
|
253
|
-
const next = { ...(ui ?? {}) };
|
|
254
|
-
const refs = Array.isArray(next.screenRefs) ? [...next.screenRefs] : [];
|
|
255
|
-
if (refs.includes(screenId)) {
|
|
256
|
-
return { ui: next, changed: false };
|
|
257
|
-
}
|
|
258
|
-
refs.push(screenId);
|
|
259
|
-
next.screenRefs = refs;
|
|
260
|
-
return { ui: next, changed: true };
|
|
261
|
-
}
|
|
262
|
-
// ── 커맨드 ────────────────────────────────────────────────────────────────
|
|
263
|
-
/** 마이그레이션을 중간에 세우는 오류 (screensEnabled를 켜기 전에 멈춘다). */
|
|
264
|
-
class MigrateAbort extends Error {
|
|
265
|
-
}
|
|
266
6
|
/** 기계 모드(=stdout이 순수 JSON이어야 하는 모드)인지. */
|
|
267
7
|
function isMachineOutput(globalOpts) {
|
|
268
8
|
return !!(globalOpts.json || globalOpts.csv || globalOpts.quiet);
|
|
269
9
|
}
|
|
270
10
|
const MIGRATE_HELP = `
|
|
271
|
-
하는 일 (이
|
|
11
|
+
하는 일 (서버가 이 순서로 수행한다, 마지막이 플래그):
|
|
272
12
|
1. spec.ui.route를 씨앗으로 화면을 중복 없이 만든다 (같은 base route = 한 화면)
|
|
273
|
-
2. 각 명세의 ui.screenRefs에 화면 ID를 더한다 (기존 ui 필드는
|
|
13
|
+
2. 각 명세의 ui.screenRefs에 화면 ID를 더한다 (전용 경로 — 기존 ui 필드는 손대지 않는다)
|
|
274
14
|
3. 전부 성공하면 ch_projects.screensEnabled를 true로 켠다
|
|
275
15
|
|
|
276
16
|
route 정규화:
|
|
@@ -280,8 +20,9 @@ route 정규화:
|
|
|
280
20
|
route가 없는 명세(배치·API·CLI)는 건너뛰고 사유를 결과에 남긴다.
|
|
281
21
|
|
|
282
22
|
안전장치:
|
|
283
|
-
--dry-run 계획만 출력하고 아무것도 쓰지 않는다 (읽기만 한다)
|
|
23
|
+
--dry-run 계획만 출력하고 서버가 아무것도 쓰지 않는다 (읽기만 한다)
|
|
284
24
|
--rollback screensEnabled를 false로 내려 레거시 모드로 되돌린다 (화면·screenRefs는 지우지 않는다)
|
|
25
|
+
되돌아갈 route가 없는 명세가 있으면 서버가 거절한다 — 강행하려면 --force
|
|
285
26
|
재실행해도 같은 base route의 화면을 두 번 만들지 않는다 (멱등)
|
|
286
27
|
|
|
287
28
|
예시:
|
|
@@ -289,62 +30,53 @@ route 정규화:
|
|
|
289
30
|
ch screens migrate # 적용
|
|
290
31
|
ch screens migrate --rollback # 화면 모드만 끄기 (되돌리기)
|
|
291
32
|
`;
|
|
292
|
-
/** 모든 명세를 페이지를 넘겨 가며 읽는다 (perPage 최대 100 — 서버 제한). */
|
|
293
|
-
async function fetchAllSpecs(client) {
|
|
294
|
-
const all = [];
|
|
295
|
-
const MAX_PAGES = 200; // 안전장치: 서버가 hasNext를 잘못 주더라도 무한루프 금지
|
|
296
|
-
for (let page = 1; page <= MAX_PAGES; page++) {
|
|
297
|
-
const { data } = await client.get('/specs', { params: { page, perPage: 100 } });
|
|
298
|
-
const items = Array.isArray(data) ? data : (data?.data ?? []);
|
|
299
|
-
all.push(...items);
|
|
300
|
-
if (!items.length || !data?.pagination?.hasNext)
|
|
301
|
-
break;
|
|
302
|
-
}
|
|
303
|
-
return all;
|
|
304
|
-
}
|
|
305
|
-
async function fetchScreens(client) {
|
|
306
|
-
const { data } = await client.get('/screens');
|
|
307
|
-
return Array.isArray(data) ? data : (data?.data ?? []);
|
|
308
|
-
}
|
|
309
|
-
/** 프로젝트의 현재 screensEnabled. 필드가 없는 레거시 문서는 false로 접는다 (백필하지 않는다). */
|
|
310
|
-
async function fetchScreensEnabled(client) {
|
|
311
|
-
const { data } = await client.get('/projects/current');
|
|
312
|
-
return data?.screensEnabled ?? false;
|
|
313
|
-
}
|
|
314
33
|
/**
|
|
315
|
-
*
|
|
34
|
+
* 마지막 한 줄 — 무슨 일이 일어났는가.
|
|
316
35
|
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
36
|
+
* 표 모드에서는 표 뒤에, 기계 모드에서는 stdout(JSON) 대신 stderr에 나간다
|
|
37
|
+
* (printSuccess/printInfo가 기계 모드에서 stderr로 가므로 stdout은 순수 JSON 한 문서로 남는다).
|
|
319
38
|
*/
|
|
320
|
-
|
|
321
|
-
|
|
39
|
+
function printMigrateOutcome(result) {
|
|
40
|
+
const sm = result.summary;
|
|
41
|
+
const flag = result.screensEnabled;
|
|
42
|
+
if (result.dryRun) {
|
|
43
|
+
(0, output_1.printInfo)('[DRY-RUN] 계획만 출력했습니다 — 화면·명세·프로젝트 문서를 하나도 쓰지 않았습니다.');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (result.screens.length === 0) {
|
|
47
|
+
(0, output_1.printInfo)('route를 가진 명세가 없습니다 — 화면을 만들지 않고 screensEnabled도 켜지 않았습니다 (변경 없음).');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
(0, output_1.printSuccess)(`마이그레이션 완료 — 화면 ${sm.screensToCreate}개 생성, ` +
|
|
51
|
+
`${sm.screensToReuse}개 재사용, 명세 ${sm.specsToLink}건 연결` +
|
|
52
|
+
(flag.changed ? ', screensEnabled=true (화면 모드 켬)' : ''));
|
|
322
53
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
if (!refs.includes(screenId)) {
|
|
335
|
-
throw new MigrateAbort(`ch-api가 spec.ui.screenRefs를 저장하지 않았습니다 (${specId} → ${screenId}). ` +
|
|
336
|
-
'screenRefs를 모르는 구버전 ch-api는 요청 검증기(whitelist)가 그 필드를 조용히 버립니다. ' +
|
|
337
|
-
'SCREEN-004(spec.ui.screenRefs) 배포 후 다시 실행하세요 — 이미 만들어진 화면은 재실행 시 재사용됩니다. ' +
|
|
338
|
-
'screensEnabled는 켜지 않았으므로 프로젝트는 레거시 모드 그대로입니다.');
|
|
54
|
+
function printRollbackOutcome(result) {
|
|
55
|
+
if (!result.screensEnabled.before) {
|
|
56
|
+
(0, output_1.printInfo)('이미 레거시 모드입니다 (screensEnabled=false) — 변경 없음.');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (result.dryRun) {
|
|
60
|
+
(0, output_1.printInfo)(result.blocked
|
|
61
|
+
? '[DRY-RUN] 지금 롤백하면 서버가 거절합니다 — 화면 모드를 끄면 위 명세들의 UI 정보가 ' +
|
|
62
|
+
'화면 탭에도 route에도 보이지 않습니다. 그래도 되돌리려면 --force를 붙이세요.'
|
|
63
|
+
: '[DRY-RUN] screensEnabled를 false로 내립니다 (화면 문서·screenRefs는 그대로 둡니다).');
|
|
64
|
+
return;
|
|
339
65
|
}
|
|
66
|
+
(0, output_1.printSuccess)('화면 모드를 껐습니다 (screensEnabled=false). 화면 문서와 명세의 screenRefs는 지우지 않았습니다 — 다시 켜면 그대로 살아납니다.' +
|
|
67
|
+
(result.forced ? ' (--force로 안전장치를 넘겼습니다)' : ''));
|
|
340
68
|
}
|
|
341
69
|
/** 사람용 출력: 계획/결과 표 + 요약. */
|
|
342
|
-
function
|
|
343
|
-
const tag = dryRun ? '[DRY-RUN] ' : '';
|
|
70
|
+
function printMigrateReport(result) {
|
|
71
|
+
const tag = result.dryRun ? '[DRY-RUN] ' : '';
|
|
344
72
|
if (result.screens.length > 0) {
|
|
345
73
|
(0, output_1.printTable)(['route', '동작', 'screenId', '화면명', 'order', '묶인 명세', '연결'], result.screens.map((s) => [
|
|
346
74
|
s.baseRoute,
|
|
347
|
-
s.action === 'create'
|
|
75
|
+
s.action === 'create'
|
|
76
|
+
? result.dryRun
|
|
77
|
+
? '생성 예정'
|
|
78
|
+
: '생성'
|
|
79
|
+
: '재사용',
|
|
348
80
|
s.screenId ?? '-',
|
|
349
81
|
s.name,
|
|
350
82
|
String(s.order),
|
|
@@ -362,123 +94,36 @@ function printHumanReport(result, dryRun) {
|
|
|
362
94
|
const flag = result.screensEnabled;
|
|
363
95
|
(0, output_1.printInfo)(`${tag}screensEnabled: ${flag.before} → ${flag.after}` +
|
|
364
96
|
(flag.changed ? '' : ' (변경 없음)'));
|
|
97
|
+
printMigrateOutcome(result);
|
|
365
98
|
}
|
|
366
|
-
/**
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
action: 'rollback',
|
|
371
|
-
dryRun,
|
|
372
|
-
screensEnabled: { before, after: false, changed: before !== false },
|
|
373
|
-
};
|
|
374
|
-
if (!dryRun && before) {
|
|
375
|
-
await setScreensEnabled(client, false);
|
|
376
|
-
}
|
|
377
|
-
if (isMachineOutput(globalOpts)) {
|
|
378
|
-
(0, output_1.formatOutput)(result, globalOpts);
|
|
379
|
-
}
|
|
380
|
-
if (dryRun) {
|
|
381
|
-
(0, output_1.printInfo)(before
|
|
382
|
-
? '[DRY-RUN] screensEnabled를 false로 내립니다 (화면 문서·screenRefs는 그대로 둡니다).'
|
|
383
|
-
: '[DRY-RUN] 이미 레거시 모드입니다 (screensEnabled=false) — 변경 없음.');
|
|
384
|
-
return;
|
|
99
|
+
/** 사람용 출력: 롤백. */
|
|
100
|
+
function printRollbackReport(result) {
|
|
101
|
+
if (result.blocked && result.unsafeSpecIds.length > 0) {
|
|
102
|
+
(0, output_1.printWarning)(`되돌아갈 route가 없는 명세 ${result.unsafeSpecIds.length}건: ${result.unsafeSpecIds.join(', ')}`);
|
|
385
103
|
}
|
|
386
|
-
|
|
387
|
-
(0, output_1.printInfo)('이미 레거시 모드입니다 (screensEnabled=false) — 변경 없음.');
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
|
-
(0, output_1.printSuccess)('화면 모드를 껐습니다 (screensEnabled=false). 화면 문서와 명세의 screenRefs는 지우지 않았습니다 — 다시 켜면 그대로 살아납니다.');
|
|
104
|
+
printRollbackOutcome(result);
|
|
391
105
|
}
|
|
392
|
-
/**
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
const
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
};
|
|
407
|
-
for (const s of plan.screens) {
|
|
408
|
-
if (s.duplicateScreenIds.length > 1) {
|
|
409
|
-
(0, output_1.printWarning)(`base route "${s.baseRoute}"에 화면이 ${s.duplicateScreenIds.length}개 있습니다 ` +
|
|
410
|
-
`(${s.duplicateScreenIds.join(', ')}) — 첫 화면(${s.screenId})을 재사용합니다.`);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
if (dryRun) {
|
|
414
|
-
emit();
|
|
415
|
-
(0, output_1.printInfo)('[DRY-RUN] 계획만 출력했습니다 — 화면·명세·프로젝트 문서를 하나도 쓰지 않았습니다.');
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
|
-
if (plan.screens.length === 0) {
|
|
419
|
-
emit();
|
|
420
|
-
(0, output_1.printInfo)('route를 가진 명세가 없습니다 — 화면을 만들지 않고 screensEnabled도 켜지 않았습니다 (변경 없음).');
|
|
106
|
+
/**
|
|
107
|
+
* 4xx·5xx 안내 보강. 상태코드별 기본 메시지와 종료코드(≠0)는 client 인터셉터가 이미 처리했다 —
|
|
108
|
+
* 여기서는 이 커맨드에서만 의미 있는 안내를 덧붙인다.
|
|
109
|
+
*/
|
|
110
|
+
function explainFailure(error) {
|
|
111
|
+
const status = error.response?.status;
|
|
112
|
+
const data = error.response?.data;
|
|
113
|
+
const message = String(data?.message ?? '');
|
|
114
|
+
// 라우트 자체가 없을 때만 "구버전 ch-api"라고 말한다. 404는 프로젝트를 못 찾았을 때도 나는데,
|
|
115
|
+
// 그걸 배포 문제로 오진하면 사용자를 엉뚱한 곳으로 보낸다 — 이 이슈가 고치는 버그가 바로
|
|
116
|
+
// 그런 오진 메시지였다. Nest는 매칭되는 라우트가 없으면 "Cannot POST /screens/migrate"를 준다.
|
|
117
|
+
if (status === 404 && /Cannot\s+POST/i.test(message)) {
|
|
118
|
+
(0, output_1.printError)('이 ch-api에는 POST /screens/migrate가 없습니다 (구버전). ch-api 배포 후 다시 실행하세요. ' +
|
|
119
|
+
'아무것도 변경되지 않았습니다.');
|
|
421
120
|
return;
|
|
422
121
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
try {
|
|
428
|
-
// 1) 화면 생성 (없는 base route만)
|
|
429
|
-
for (const planned of plan.screens) {
|
|
430
|
-
if (planned.action !== 'create')
|
|
431
|
-
continue;
|
|
432
|
-
const { data } = await client.post('/screens', buildCreateScreenBody(planned));
|
|
433
|
-
const id = data?.id;
|
|
434
|
-
if (!id) {
|
|
435
|
-
throw new MigrateAbort(`화면 생성 응답에 id가 없습니다 (route: ${planned.baseRoute}). screensEnabled는 켜지 않았습니다.`);
|
|
436
|
-
}
|
|
437
|
-
planned.screenId = String(id);
|
|
438
|
-
}
|
|
439
|
-
// 2) 명세 연결 — 기존 ui를 되읽어 병합 저장 (통째 교체 금지)
|
|
440
|
-
let verified = false;
|
|
441
|
-
for (const planned of plan.screens) {
|
|
442
|
-
const screenId = planned.screenId;
|
|
443
|
-
if (!screenId)
|
|
444
|
-
continue;
|
|
445
|
-
for (const specId of planned.linkSpecIds) {
|
|
446
|
-
const { data: spec } = await client.get(`/specs/${specId}`);
|
|
447
|
-
const { ui, changed } = mergeScreenRef(spec?.ui, screenId);
|
|
448
|
-
if (!changed)
|
|
449
|
-
continue; // 계획 산출 뒤 누가 먼저 걸어 뒀다면 그대로 둔다 (멱등)
|
|
450
|
-
// 버전 기록은 남기지 않는다: 마이그레이션은 기계적 링크 추가라, 명세마다 새 버전을
|
|
451
|
-
// 찍으면 이력이 통째로 노이즈가 된다 (ch specs update --no-version과 같은 선택).
|
|
452
|
-
await client.patch(`/specs/${specId}?newVersion=false`, { ui });
|
|
453
|
-
linked += 1;
|
|
454
|
-
// 첫 쓰기 직후 한 번만: 서버가 screenRefs를 진짜 저장했는지 확인 (구버전 무성 실패 차단)
|
|
455
|
-
if (!verified) {
|
|
456
|
-
await assertScreenRefsPersisted(client, specId, screenId);
|
|
457
|
-
verified = true;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
// 3) 마지막 쓰기 — 화면 생성·연결이 전부 성공한 뒤에야 플래그를 켠다
|
|
462
|
-
if (plan.screensEnabled.changed) {
|
|
463
|
-
await setScreensEnabled(client, true);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
catch (error) {
|
|
467
|
-
// MigrateAbort는 자기 메시지에 이미 안내를 담고 있다. 그 밖의 실패(HTTP 4xx·5xx 등)는
|
|
468
|
-
// 인터셉터가 원인만 찍으므로, 지금 상태와 다음 행동을 여기서 덧붙인다.
|
|
469
|
-
if (!(error instanceof MigrateAbort)) {
|
|
470
|
-
(0, output_1.printWarning)(`마이그레이션이 중간에 멈췄습니다 (화면 생성 ${plan.screens.filter((s) => s.action === 'create' && s.screenId).length}건, 명세 연결 ${linked}건까지 진행). ` +
|
|
471
|
-
'screensEnabled는 켜지 않았으므로 프로젝트는 레거시 모드 그대로입니다 — ' +
|
|
472
|
-
'원인을 고쳐 다시 실행하면 이미 만들어진 화면을 재사용해 이어갑니다.');
|
|
473
|
-
}
|
|
474
|
-
throw error;
|
|
122
|
+
if (status === 409 && Array.isArray(data?.unsafeSpecIds)) {
|
|
123
|
+
(0, output_1.printWarning)(`되돌아갈 route가 없는 명세 ${data.unsafeSpecIds.length}건: ${data.unsafeSpecIds.join(', ')}`);
|
|
124
|
+
(0, output_1.printError)('롤백을 거절했습니다 — 화면 모드를 끄면 이 명세들의 UI 정보가 화면 탭에도 route에도 보이지 않습니다. ' +
|
|
125
|
+
'아무것도 쓰지 않았습니다 (screensEnabled는 그대로 true). 그래도 되돌리려면 --force를 붙이세요.');
|
|
475
126
|
}
|
|
476
|
-
// 실제 연결 건수로 요약을 맞춘다 (경합으로 건너뛴 건이 있으면 계획보다 적을 수 있다)
|
|
477
|
-
plan.summary.specsToLink = linked;
|
|
478
|
-
emit();
|
|
479
|
-
(0, output_1.printSuccess)(`마이그레이션 완료 — 화면 ${plan.summary.screensToCreate}개 생성, ` +
|
|
480
|
-
`${plan.summary.screensToReuse}개 재사용, 명세 ${linked}건 연결` +
|
|
481
|
-
(plan.screensEnabled.changed ? ', screensEnabled=true (화면 모드 켬)' : ''));
|
|
482
127
|
}
|
|
483
128
|
/**
|
|
484
129
|
* `ch screens migrate` 등록. screens.ts의 register…(screens, program) 규약을 따른다.
|
|
@@ -488,26 +133,56 @@ function registerScreensMigrate(screens, program) {
|
|
|
488
133
|
.command('migrate')
|
|
489
134
|
.description('레거시 프로젝트를 화면 모델로 이관 — spec.ui.route를 씨앗으로 화면 생성 + screenRefs 연결 + screensEnabled 켬 (비파괴·멱등, admin)')
|
|
490
135
|
.option('--rollback', 'screensEnabled를 false로 내려 레거시 모드로 되돌린다 (화면 문서·screenRefs는 지우지 않는다)')
|
|
136
|
+
.option('--force', '롤백 안전장치를 넘긴다 — 되돌아갈 route가 없는 명세가 있어도 강행한다 (--rollback 전용)')
|
|
491
137
|
.addHelpText('after', MIGRATE_HELP)
|
|
492
138
|
.action(async (options) => {
|
|
493
139
|
const globalOpts = program.opts();
|
|
494
140
|
const dryRun = !!globalOpts.dryRun;
|
|
141
|
+
const rollback = !!options.rollback;
|
|
142
|
+
if (options.force && !rollback) {
|
|
143
|
+
(0, output_1.printWarning)('--force는 --rollback에서만 쓰입니다 — 무시합니다.');
|
|
144
|
+
}
|
|
145
|
+
// 안전한 기본값도 명시적으로 보낸다. boolean을 보내지 않는 이유: ch-api 글로벌 파이프의
|
|
146
|
+
// enableImplicitConversion이 문자열 "false"를 true로 뒤집어, 막아야 할 롤백을 통과시킨다.
|
|
147
|
+
// 문자열 enum은 그 함정이 없다(암묵 변환이 String(value)라 무해하고, 무효값은 400이다).
|
|
148
|
+
const body = {
|
|
149
|
+
action: rollback ? 'rollback' : 'migrate',
|
|
150
|
+
mode: dryRun ? 'plan' : 'apply',
|
|
151
|
+
...(rollback ? { onUnsafe: options.force ? 'force' : 'block' } : {}),
|
|
152
|
+
};
|
|
495
153
|
try {
|
|
496
154
|
const client = (0, client_1.createClient)({ projectId: globalOpts.project });
|
|
497
|
-
|
|
498
|
-
|
|
155
|
+
const { data } = await client.post('/screens/migrate', body);
|
|
156
|
+
// 서버가 계산한 경고(중복 화면 등)는 stderr로 — stdout은 기계 모드에서 JSON 전용이다.
|
|
157
|
+
for (const warning of data.warnings ?? []) {
|
|
158
|
+
(0, output_1.printWarning)(warning);
|
|
159
|
+
}
|
|
160
|
+
if (isMachineOutput(globalOpts)) {
|
|
161
|
+
// stdout은 서버 응답 JSON 한 문서. 무슨 일이 있었는지 한 줄은 stderr로 남긴다
|
|
162
|
+
// (기계 모드에서 printSuccess/printInfo는 stderr로 간다 — 무성 성공 금지).
|
|
163
|
+
(0, output_1.formatOutput)(data, globalOpts);
|
|
164
|
+
if (data.action === 'rollback') {
|
|
165
|
+
printRollbackOutcome(data);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
printMigrateOutcome(data);
|
|
169
|
+
}
|
|
499
170
|
return;
|
|
500
171
|
}
|
|
501
|
-
|
|
172
|
+
if (data.action === 'rollback') {
|
|
173
|
+
printRollbackReport(data);
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
printMigrateReport(data);
|
|
177
|
+
}
|
|
502
178
|
}
|
|
503
179
|
catch (error) {
|
|
504
|
-
// HTTP
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
(0, output_1.printError)(error.message);
|
|
509
|
-
process.exitCode = 1;
|
|
180
|
+
// HTTP 오류의 기본 메시지·종료코드(≠0)는 client 인터셉터가 이미 세웠다.
|
|
181
|
+
if (error.isAxiosError) {
|
|
182
|
+
explainFailure(error);
|
|
183
|
+
return;
|
|
510
184
|
}
|
|
185
|
+
throw error;
|
|
511
186
|
}
|
|
512
187
|
});
|
|
513
188
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"screens.migrate.js","sourceRoot":"","sources":["../../src/commands/screens.migrate.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"screens.migrate.js","sourceRoot":"","sources":["../../src/commands/screens.migrate.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: 2t-decencia-channel-change-manager
|
|
3
3
|
description: 기존 소통채널 프로젝트의 기능 추가/수정을 사용자와 대화로 명확화 → 전파 스킬로 연관 PRD/spec/DB/SQA 일괄 갱신 → 변경으로 생긴 새 할일을 GitHub 이슈로 발행하는 오케스트레이터 스킬. (1)대화로 변경 내용 확정 → (2)[게이트] 영향 범위 확인 후 전파 적용 → (3)[게이트] 할일 목록 확인 후 이슈 발행. "이 기능 바꿀래"·"기획 수정"·"이거 추가해줘(기존 프로젝트)" 류 요청 시.
|
|
4
4
|
allowed-tools: Bash(ch:*) Bash(git:*) Bash(gh:*) Read Write
|
|
5
|
-
version: 1.
|
|
5
|
+
version: 1.14.0
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
<!-- ch-version-gate -->
|
|
@@ -9,7 +9,7 @@ description: |
|
|
|
9
9
|
(3) ch db-tables 명령(list/get/create/update/delete)을 사용할 때,
|
|
10
10
|
(4) ch screens 명령(list/get/create/update/delete/migrate)으로 화면을 다루거나 레거시 프로젝트를 화면 모드로 이관할 때,
|
|
11
11
|
(5) Spec ID를 JIRA 스타일(`LOGIN-001`)로 직접 지정할 때 (`ch specs create --id`).
|
|
12
|
-
version: 1.
|
|
12
|
+
version: 1.14.0
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
<!-- ch-version-gate -->
|
|
@@ -237,10 +237,11 @@ ch screens update <screenId> --entry-points "" # 진입 경로 모두 해제
|
|
|
237
237
|
|
|
238
238
|
ch screens delete <screenId> # 참조 spec의 ui.screenRefs도 서버가 자동 정리
|
|
239
239
|
|
|
240
|
-
# 레거시 프로젝트 → 화면 모드 이관 (비파괴·멱등, admin)
|
|
240
|
+
# 레거시 프로젝트 → 화면 모드 이관 (비파괴·멱등, admin) — 서버가 수행한다(POST /screens/migrate)
|
|
241
241
|
ch screens migrate --dry-run # 계획만 (아무것도 쓰지 않는다)
|
|
242
242
|
ch screens migrate # 적용: 화면 생성 → screenRefs 연결 → screensEnabled=true
|
|
243
243
|
ch screens migrate --rollback # 화면 모드만 끄기 (화면·screenRefs는 남는다)
|
|
244
|
+
ch screens migrate --rollback --force # 되돌아갈 route가 없어도 강행 (기본은 서버가 409로 거절)
|
|
244
245
|
```
|
|
245
246
|
|
|
246
247
|
- `--id`는 JIRA 스타일(`SCR-001`). 패턴 `^[A-Za-z][A-Za-z0-9_]*-[A-Za-z0-9]+$`, 중복 시 409.
|
|
@@ -295,4 +296,5 @@ ch screens migrate --rollback # 화면 모드만 끄기 (화
|
|
|
295
296
|
- `relatedSpecIds`를 JSON에 명시해도 서버가 무시 (자동 관리 영역).
|
|
296
297
|
- `ch screens update --states`도 배열 **통째 교체**다. 상태 하나만 고치려 해도 `ch screens get <id> --json`으로 받아 배열 전체를 넘긴다.
|
|
297
298
|
- 화면 SSOT는 `screensEnabled=true`인 프로젝트에서만 screens다. 이 플래그가 꺼진(=기존) 프로젝트는 화면 정보의 SSOT가 여전히 `spec.ui.route`다 — screens를 만들어도 그 프로젝트의 화면 정보가 자동으로 옮겨가지 않는다. 옮기려면 `ch screens migrate`.
|
|
298
|
-
- `ch screens migrate`의 `--dry-run`은
|
|
299
|
+
- `ch screens migrate`의 `--dry-run`은 **요청을 한 번 보낸다**(`POST /screens/migrate {mode:'plan'}`). 계산도 쓰기도 서버가 한다. "아무 요청도 안 나간다"가 아니라 "아무 문서도 쓰지 않는다"가 보장이다.
|
|
300
|
+
- `--rollback`은 `screensEnabled`만 내린다. 다만 **되돌아갈 route가 없는 명세**(화면에 걸려 있는데 `ui.route`가 빈 명세 — 화면 우선으로 만든 프로젝트가 그렇다)가 하나라도 있으면 서버가 **409로 거절**한다. 화면 모드를 끄면 그 명세의 UI 정보가 화면 탭에도 route에도 없기 때문이다. 정말 내리려면 `--force`.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: 2t-decencia-channel-github-issue
|
|
3
3
|
description: gh CLI로 현재 repo에 GitHub 이슈를 작성한다. 이슈 1개=유형 1개(feat/fix/bug), 표준 본문 양식(작업내용·관련 명세 백링크·완료조건)으로 발행. "이슈 만들어줘"/"깃헙 이슈로 등록해줘" 요청 시, 또는 소통채널 기획·명세 작업 후 처리할 일들을 GitHub 이슈로 옮길 때 사용. 멱등성/중복방지·확인단계는 다루지 않음(요청대로 바로 생성).
|
|
4
4
|
allowed-tools: Bash(gh:*) Bash(git:*) Read Write
|
|
5
|
-
version: 1.
|
|
5
|
+
version: 1.14.0
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
<!-- ch-version-gate -->
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: 2t-decencia-channel-orchestrator
|
|
3
3
|
description: 소통채널 작업의 단일 진입점(디스패처). 사용자 요청을 듣고 7종(bootstrap·terraformer·change-manager·github-issue·issue-coder·pr-merger·sprint-runner) 중 적절한 곳으로 라우팅하고, 필요하면 여러 단계를 순차 오케스트레이션한다. 대화형 스킬은 메인 세션에서 Skill로, 자율 에이전트는 Agent로 위임. "소통채널 작업 해줘"·"이거 어떻게 처리하지?"처럼 무엇부터 할지 모를 때, 또는 신규기획/코드편입/기획변경/이슈/구현/머지/스프린트실행 어디로든 시작할 때.
|
|
4
4
|
allowed-tools: Skill Agent Read Bash(ch:*) Bash(gh:*) Bash(git:*)
|
|
5
|
-
version: 1.
|
|
5
|
+
version: 1.14.0
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
<!-- ch-version-gate -->
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: 2t-decencia-channel-project-bootstrap
|
|
3
3
|
description: 신규 기획을 사용자와 대화하며 step-by-step으로 소통채널 프로젝트로 만드는 오케스트레이터 스킬. (1)대화로 PRD 완성 → 승인 → (2)페이지명세(spec)+DB스키마 → 승인 → (3)SQA 시트. 각 단계 사용자 승인 게이트, 작성 디테일은 기존 2t-decencia-channel-*-v2 스킬을 적극 재사용. "새 기획 만들어줘"·"프로젝트 처음부터 세팅" 류 요청 시.
|
|
4
4
|
allowed-tools: Bash(ch:*) Bash(git:*) Read Write
|
|
5
|
-
version: 1.
|
|
5
|
+
version: 1.14.0
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
<!-- ch-version-gate -->
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: 2t-decencia-channel-sprint-runner
|
|
3
3
|
description: 특정 Sprint를 끝까지 실행(run)하는 오케스트레이터 스킬. Sprint의 spec을 시드로 이슈 목록을 분해·발행(github-issue)하고, GitHub milestone에 묶인 이슈를 상태별로 분류해 issue-coder(병렬)·pr-merger(직렬)로 전진시킨 뒤, 사람확인이 필요한 곳에서 멈춘다. 사람이 확인·체크 후 재호출하면 멱등하게 이어간다(반복 루프). "스프린트 돌려줘"·"sprint N 실행"·"이 스프린트 개발 진행해줘" 류 요청 시.
|
|
4
4
|
allowed-tools: Skill Agent Read Bash(ch:*) Bash(gh:*) Bash(git:*)
|
|
5
|
-
version: 1.
|
|
5
|
+
version: 1.14.0
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
<!-- ch-version-gate -->
|