@dusky-bluehour/agent-service 0.6.2
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/LICENSE +21 -0
- package/README.md +205 -0
- package/antigravity/README.md +37 -0
- package/antigravity/agents/agent-catalog.json +72 -0
- package/antigravity/artifacts/artifact-catalog.json +184 -0
- package/antigravity/commands/command-catalog.json +942 -0
- package/antigravity/skills/code-review-and-improvement/SKILL.md +15 -0
- package/antigravity/skills/frontend-repetition-pack/SKILL.md +15 -0
- package/antigravity/skills/incident-response/SKILL.md +15 -0
- package/antigravity/skills/prd-to-production-pipeline/SKILL.md +16 -0
- package/antigravity/skills/release-and-operations/SKILL.md +15 -0
- package/antigravity/skills/security-hardening/SKILL.md +15 -0
- package/antigravity/skills/service-lifecycle-orchestration/SKILL.md +16 -0
- package/antigravity/workflows/workflow-catalog.json +362 -0
- package/catalog/tool-catalog.ko.json +296 -0
- package/claude-code/README.md +47 -0
- package/claude-code/agent-teams/team-catalog.json +69 -0
- package/claude-code/commands/command-catalog.json +942 -0
- package/claude-code/skills/code-review-and-improvement/SKILL.md +16 -0
- package/claude-code/skills/frontend-repetition-pack/SKILL.md +16 -0
- package/claude-code/skills/incident-response/SKILL.md +16 -0
- package/claude-code/skills/prd-to-production-pipeline/SKILL.md +17 -0
- package/claude-code/skills/release-and-operations/SKILL.md +16 -0
- package/claude-code/skills/security-hardening/SKILL.md +15 -0
- package/claude-code/skills/service-lifecycle-orchestration/SKILL.md +17 -0
- package/claude-code/subagents/backend-engineer.md +20 -0
- package/claude-code/subagents/code-reviewer.md +19 -0
- package/claude-code/subagents/frontend-engineer.md +20 -0
- package/claude-code/subagents/hook-refactor-engineer.md +19 -0
- package/claude-code/subagents/incident-commander.md +19 -0
- package/claude-code/subagents/lead-orchestrator.md +18 -0
- package/claude-code/subagents/operations-owner.md +20 -0
- package/claude-code/subagents/performance-engineer.md +19 -0
- package/claude-code/subagents/prd-writer.md +20 -0
- package/claude-code/subagents/product-planner.md +19 -0
- package/claude-code/subagents/qa-engineer.md +19 -0
- package/claude-code/subagents/security-engineer.md +20 -0
- package/claude-code/subagents/solution-architect.md +19 -0
- package/claude-code/subagents/sre-release-engineer.md +20 -0
- package/claude-code/subagents/ui-component-engineer.md +19 -0
- package/claude-code/workflows/workflow-catalog.json +680 -0
- package/codex/README.md +38 -0
- package/codex/automations/automation-recipes.toml +30 -0
- package/codex/commands/command-catalog.json +942 -0
- package/codex/instructions/AGENTS.override.template.md +21 -0
- package/codex/instructions/AGENTS.template.md +31 -0
- package/codex/skills/code-review-and-improvement/SKILL.md +16 -0
- package/codex/skills/code-review-and-improvement/agents/openai.yaml +4 -0
- package/codex/skills/frontend-repetition-pack/SKILL.md +15 -0
- package/codex/skills/frontend-repetition-pack/agents/openai.yaml +4 -0
- package/codex/skills/incident-response/SKILL.md +16 -0
- package/codex/skills/incident-response/agents/openai.yaml +4 -0
- package/codex/skills/prd-to-production-pipeline/SKILL.md +16 -0
- package/codex/skills/prd-to-production-pipeline/agents/openai.yaml +4 -0
- package/codex/skills/release-and-operations/SKILL.md +15 -0
- package/codex/skills/release-and-operations/agents/openai.yaml +4 -0
- package/codex/skills/security-hardening/SKILL.md +15 -0
- package/codex/skills/security-hardening/agents/openai.yaml +4 -0
- package/codex/skills/service-lifecycle-orchestration/SKILL.md +17 -0
- package/codex/skills/service-lifecycle-orchestration/agents/openai.yaml +4 -0
- package/codex/workflows/workflow-catalog.json +444 -0
- package/package.json +44 -0
- package/scripts/init.mjs +993 -0
- package/scripts/validate.mjs +591 -0
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { execFile } from 'node:child_process';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
const rootDir = path.resolve(__dirname, '..');
|
|
14
|
+
|
|
15
|
+
const toolDirs = ['claude-code', 'codex', 'antigravity'];
|
|
16
|
+
const errors = [];
|
|
17
|
+
|
|
18
|
+
function fail(message) {
|
|
19
|
+
errors.push(message);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function exists(filePath) {
|
|
23
|
+
try {
|
|
24
|
+
await fs.access(filePath);
|
|
25
|
+
return true;
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function readJson(filePath) {
|
|
32
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
33
|
+
return JSON.parse(raw);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseFrontmatter(content) {
|
|
37
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
38
|
+
if (!match) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const yaml = match[1];
|
|
43
|
+
const result = {};
|
|
44
|
+
for (const line of yaml.split('\n')) {
|
|
45
|
+
const idx = line.indexOf(':');
|
|
46
|
+
if (idx <= 0) continue;
|
|
47
|
+
const key = line.slice(0, idx).trim();
|
|
48
|
+
const value = line.slice(idx + 1).trim();
|
|
49
|
+
result[key] = value;
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function validateCatalog() {
|
|
55
|
+
const catalogPath = path.join(rootDir, 'catalog', 'tool-catalog.ko.json');
|
|
56
|
+
if (!(await exists(catalogPath))) {
|
|
57
|
+
fail('[catalog] tool-catalog.ko.json 누락');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let catalog;
|
|
62
|
+
try {
|
|
63
|
+
catalog = await readJson(catalogPath);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
fail(`[catalog] JSON 파싱 실패: ${error.message}`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!Array.isArray(catalog.tools) || catalog.tools.length === 0) {
|
|
70
|
+
fail('[catalog] tools 배열 누락 또는 비어 있음');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const toolIdSet = new Set();
|
|
75
|
+
const toolComponentIdMap = new Map();
|
|
76
|
+
for (const tool of catalog.tools) {
|
|
77
|
+
if (!tool.id || !tool.root || !tool.readme || !Array.isArray(tool.components)) {
|
|
78
|
+
fail(`[catalog] tool 필수 필드 누락: ${JSON.stringify(tool)}`);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (toolIdSet.has(tool.id)) {
|
|
83
|
+
fail(`[catalog] 중복 tool id: ${tool.id}`);
|
|
84
|
+
}
|
|
85
|
+
toolIdSet.add(tool.id);
|
|
86
|
+
|
|
87
|
+
const toolRootPath = path.join(rootDir, tool.root);
|
|
88
|
+
if (!(await exists(toolRootPath))) {
|
|
89
|
+
fail(`[catalog] tool root 누락: ${tool.root}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const readmePath = path.join(rootDir, tool.root, tool.readme);
|
|
93
|
+
if (!(await exists(readmePath))) {
|
|
94
|
+
fail(`[catalog] tool readme 누락: ${tool.root}/${tool.readme}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const componentIds = new Set();
|
|
98
|
+
for (const component of tool.components) {
|
|
99
|
+
if (!component.id || !component.path) {
|
|
100
|
+
fail(`[catalog] component 필수 필드 누락: ${tool.id}`);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (componentIds.has(component.id)) {
|
|
105
|
+
fail(`[catalog] 중복 component id: ${tool.id}/${component.id}`);
|
|
106
|
+
}
|
|
107
|
+
componentIds.add(component.id);
|
|
108
|
+
|
|
109
|
+
const compPath = path.join(rootDir, tool.root, component.path);
|
|
110
|
+
if (!(await exists(compPath))) {
|
|
111
|
+
fail(`[catalog] component 경로 누락: ${tool.id}/${component.path}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
toolComponentIdMap.set(tool.id, componentIds);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!Array.isArray(catalog.presets) || catalog.presets.length === 0) {
|
|
118
|
+
fail('[catalog] presets 배열 누락 또는 비어 있음');
|
|
119
|
+
} else {
|
|
120
|
+
const presetIdSet = new Set();
|
|
121
|
+
for (const preset of catalog.presets) {
|
|
122
|
+
if (!preset.id || !preset.title || !Array.isArray(preset.tools) || !preset.components) {
|
|
123
|
+
fail(`[catalog] preset 필수 필드 누락: ${JSON.stringify(preset)}`);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (presetIdSet.has(preset.id)) {
|
|
128
|
+
fail(`[catalog] 중복 preset id: ${preset.id}`);
|
|
129
|
+
}
|
|
130
|
+
presetIdSet.add(preset.id);
|
|
131
|
+
|
|
132
|
+
for (const toolId of preset.tools) {
|
|
133
|
+
if (!toolIdSet.has(toolId)) {
|
|
134
|
+
fail(`[catalog] preset(${preset.id})의 tool이 유효하지 않음: ${toolId}`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const requested = preset.components[toolId];
|
|
139
|
+
if (!Array.isArray(requested) || requested.length === 0) {
|
|
140
|
+
fail(`[catalog] preset(${preset.id})의 구성요소 누락: ${toolId}`);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const availableIds = toolComponentIdMap.get(toolId) ?? new Set();
|
|
145
|
+
for (const componentId of requested) {
|
|
146
|
+
if (!availableIds.has(componentId)) {
|
|
147
|
+
fail(`[catalog] preset(${preset.id})의 구성요소가 유효하지 않음: ${toolId}/${componentId}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (const toolId of toolDirs) {
|
|
155
|
+
if (!toolIdSet.has(toolId)) {
|
|
156
|
+
fail(`[catalog] toolDirs 대비 누락된 tool id: ${toolId}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function validatePackageJson() {
|
|
162
|
+
const packagePath = path.join(rootDir, 'package.json');
|
|
163
|
+
if (!(await exists(packagePath))) {
|
|
164
|
+
fail('[package] package.json 누락');
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let pkg;
|
|
169
|
+
try {
|
|
170
|
+
pkg = await readJson(packagePath);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
fail(`[package] package.json 파싱 실패: ${error.message}`);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const requiredScripts = ['validate', 'pack:dry-run', 'prepublish:check', 'prepublishOnly'];
|
|
177
|
+
for (const scriptName of requiredScripts) {
|
|
178
|
+
if (!pkg.scripts || !(scriptName in pkg.scripts)) {
|
|
179
|
+
fail(`[package] scripts 누락: ${scriptName}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const requiredFiles = [
|
|
184
|
+
'claude-code',
|
|
185
|
+
'antigravity',
|
|
186
|
+
'codex',
|
|
187
|
+
'catalog/tool-catalog.ko.json',
|
|
188
|
+
'scripts/init.mjs',
|
|
189
|
+
'scripts/validate.mjs'
|
|
190
|
+
];
|
|
191
|
+
for (const fileEntry of requiredFiles) {
|
|
192
|
+
if (!Array.isArray(pkg.files) || !pkg.files.includes(fileEntry)) {
|
|
193
|
+
fail(`[package] files 누락: ${fileEntry}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const excludedFromPublish = [
|
|
198
|
+
'docs/COMPOSITION.ko.md',
|
|
199
|
+
'docs/DEPLOYMENT-GUIDE.ko.md',
|
|
200
|
+
'docs/UPDATE-GUIDE.ko.md',
|
|
201
|
+
'docs/UX-FLOW.ko.md'
|
|
202
|
+
];
|
|
203
|
+
for (const fileEntry of excludedFromPublish) {
|
|
204
|
+
if (Array.isArray(pkg.files) && pkg.files.includes(fileEntry)) {
|
|
205
|
+
fail(`[package] files에서 제외 필요: ${fileEntry}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
for (const docFile of excludedFromPublish) {
|
|
210
|
+
const docPath = path.join(rootDir, docFile);
|
|
211
|
+
if (!(await exists(docPath))) {
|
|
212
|
+
fail(`[docs] 문서 누락: ${docFile}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function validateSkillDirectory(toolName) {
|
|
218
|
+
const skillsPath = path.join(rootDir, toolName, 'skills');
|
|
219
|
+
if (!(await exists(skillsPath))) {
|
|
220
|
+
fail(`[${toolName}] skills 디렉터리 없음`);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const skillEntries = await fs.readdir(skillsPath, { withFileTypes: true });
|
|
225
|
+
const skillDirs = skillEntries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
226
|
+
|
|
227
|
+
if (skillDirs.length === 0) {
|
|
228
|
+
fail(`[${toolName}] skills 디렉터리가 비어 있음`);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
for (const skillName of skillDirs) {
|
|
233
|
+
const skillFile = path.join(skillsPath, skillName, 'SKILL.md');
|
|
234
|
+
if (!(await exists(skillFile))) {
|
|
235
|
+
fail(`[${toolName}] SKILL.md 누락: ${skillName}`);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const content = await fs.readFile(skillFile, 'utf8');
|
|
240
|
+
const fm = parseFrontmatter(content);
|
|
241
|
+
if (!fm) {
|
|
242
|
+
fail(`[${toolName}] frontmatter 누락: ${skillFile}`);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!fm.name || !fm.description) {
|
|
247
|
+
fail(`[${toolName}] frontmatter name/description 누락: ${skillFile}`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (toolName === 'codex') {
|
|
251
|
+
const openaiYaml = path.join(skillsPath, skillName, 'agents', 'openai.yaml');
|
|
252
|
+
if (!(await exists(openaiYaml))) {
|
|
253
|
+
fail(`[codex] openai.yaml 누락: ${skillName}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function validateCommandCatalog(toolName) {
|
|
260
|
+
const filePath = path.join(rootDir, toolName, 'commands', 'command-catalog.json');
|
|
261
|
+
if (!(await exists(filePath))) {
|
|
262
|
+
fail(`[${toolName}] command-catalog.json 누락`);
|
|
263
|
+
return { commandIds: new Set() };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let data;
|
|
267
|
+
try {
|
|
268
|
+
data = await readJson(filePath);
|
|
269
|
+
} catch (error) {
|
|
270
|
+
fail(`[${toolName}] command-catalog.json 파싱 실패: ${error.message}`);
|
|
271
|
+
return { commandIds: new Set() };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!Array.isArray(data.commands) || data.commands.length === 0) {
|
|
275
|
+
fail(`[${toolName}] commands 배열 누락 또는 비어 있음`);
|
|
276
|
+
return { commandIds: new Set() };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const requiredFields = [
|
|
280
|
+
'id',
|
|
281
|
+
'name',
|
|
282
|
+
'phase',
|
|
283
|
+
'owner_role',
|
|
284
|
+
'purpose',
|
|
285
|
+
'input_contract',
|
|
286
|
+
'execution_contract',
|
|
287
|
+
'quality_gates',
|
|
288
|
+
'output_contract',
|
|
289
|
+
'handoff'
|
|
290
|
+
];
|
|
291
|
+
|
|
292
|
+
const commandIds = new Set();
|
|
293
|
+
for (const cmd of data.commands) {
|
|
294
|
+
for (const field of requiredFields) {
|
|
295
|
+
if (!(field in cmd)) {
|
|
296
|
+
fail(`[${toolName}] 명령 필드 누락 (${cmd.id ?? 'unknown'}): ${field}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!cmd.id) {
|
|
301
|
+
fail(`[${toolName}] 명령 ID 누락`);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (commandIds.has(cmd.id)) {
|
|
306
|
+
fail(`[${toolName}] 중복 명령 ID: ${cmd.id}`);
|
|
307
|
+
}
|
|
308
|
+
commandIds.add(cmd.id);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const repeatedIds = [
|
|
312
|
+
'CMD-DEV-FE-UI-COMPONENTIZE',
|
|
313
|
+
'CMD-DEV-FE-HOOK-SEPARATE',
|
|
314
|
+
'CMD-DEV-PERF-OPTIMIZE'
|
|
315
|
+
];
|
|
316
|
+
|
|
317
|
+
for (const rid of repeatedIds) {
|
|
318
|
+
if (!commandIds.has(rid)) {
|
|
319
|
+
fail(`[${toolName}] 반복 작업 명령 누락: ${rid}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return { commandIds };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function validateWorkflowCatalog(toolName, commandIds) {
|
|
327
|
+
const filePath = path.join(rootDir, toolName, 'workflows', 'workflow-catalog.json');
|
|
328
|
+
if (!(await exists(filePath))) {
|
|
329
|
+
fail(`[${toolName}] workflow-catalog.json 누락`);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
let data;
|
|
334
|
+
try {
|
|
335
|
+
data = await readJson(filePath);
|
|
336
|
+
} catch (error) {
|
|
337
|
+
fail(`[${toolName}] workflow-catalog.json 파싱 실패: ${error.message}`);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (!Array.isArray(data.workflows) || data.workflows.length === 0) {
|
|
342
|
+
fail(`[${toolName}] workflows 배열 누락 또는 비어 있음`);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
for (const wf of data.workflows) {
|
|
347
|
+
if (!wf.id || !wf.name) {
|
|
348
|
+
fail(`[${toolName}] workflow id/name 누락`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (!Array.isArray(wf.stages) || wf.stages.length === 0) {
|
|
352
|
+
fail(`[${toolName}] stage 누락: ${wf.id ?? 'unknown'}`);
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
for (const stage of wf.stages) {
|
|
357
|
+
if (!Array.isArray(stage.commands)) {
|
|
358
|
+
fail(`[${toolName}] stage commands 누락: ${wf.id}/${stage.stage_id ?? 'unknown'}`);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
for (const cmdId of stage.commands) {
|
|
363
|
+
if (!commandIds.has(cmdId)) {
|
|
364
|
+
fail(`[${toolName}] workflow가 존재하지 않는 명령 참조: ${wf.id} -> ${cmdId}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function validateClaudeExtras() {
|
|
372
|
+
const subagentsPath = path.join(rootDir, 'claude-code', 'subagents');
|
|
373
|
+
if (!(await exists(subagentsPath))) {
|
|
374
|
+
fail('[claude-code] subagents 디렉터리 누락');
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const files = (await fs.readdir(subagentsPath)).filter((file) => file.endsWith('.md'));
|
|
379
|
+
if (files.length === 0) {
|
|
380
|
+
fail('[claude-code] subagents 파일 없음');
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
for (const file of files) {
|
|
385
|
+
const content = await fs.readFile(path.join(subagentsPath, file), 'utf8');
|
|
386
|
+
const fm = parseFrontmatter(content);
|
|
387
|
+
if (!fm || !fm.name || !fm.description) {
|
|
388
|
+
fail(`[claude-code] subagent frontmatter 누락: ${file}`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const teamCatalog = path.join(rootDir, 'claude-code', 'agent-teams', 'team-catalog.json');
|
|
393
|
+
if (!(await exists(teamCatalog))) {
|
|
394
|
+
fail('[claude-code] team-catalog.json 누락');
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function validateAntigravityExtras() {
|
|
399
|
+
const agentCatalog = path.join(rootDir, 'antigravity', 'agents', 'agent-catalog.json');
|
|
400
|
+
const artifactCatalog = path.join(rootDir, 'antigravity', 'artifacts', 'artifact-catalog.json');
|
|
401
|
+
|
|
402
|
+
for (const f of [agentCatalog, artifactCatalog]) {
|
|
403
|
+
if (!(await exists(f))) {
|
|
404
|
+
fail(`[antigravity] 누락: ${path.relative(rootDir, f)}`);
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
try {
|
|
409
|
+
await readJson(f);
|
|
410
|
+
} catch (error) {
|
|
411
|
+
fail(`[antigravity] JSON 파싱 실패: ${path.relative(rootDir, f)} (${error.message})`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function validateReadmes() {
|
|
417
|
+
for (const tool of toolDirs) {
|
|
418
|
+
const filePath = path.join(rootDir, tool, 'README.md');
|
|
419
|
+
if (!(await exists(filePath))) {
|
|
420
|
+
fail(`[${tool}] README.md 누락`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function runCliSmokeTests() {
|
|
426
|
+
const nodeBin = process.execPath;
|
|
427
|
+
const cliPath = path.join(rootDir, 'scripts', 'init.mjs');
|
|
428
|
+
|
|
429
|
+
try {
|
|
430
|
+
await execFileAsync(nodeBin, [cliPath, 'list'], { cwd: rootDir });
|
|
431
|
+
} catch (error) {
|
|
432
|
+
fail(`[cli] list 실행 실패: ${error.message}`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
await execFileAsync(
|
|
437
|
+
nodeBin,
|
|
438
|
+
[
|
|
439
|
+
cliPath,
|
|
440
|
+
'setup',
|
|
441
|
+
'--tool',
|
|
442
|
+
'codex',
|
|
443
|
+
'--components',
|
|
444
|
+
'skills,workflows,commands',
|
|
445
|
+
'--target',
|
|
446
|
+
'/tmp/tri-agent-manager-validate',
|
|
447
|
+
'--dry-run',
|
|
448
|
+
'--yes',
|
|
449
|
+
'--non-interactive'
|
|
450
|
+
],
|
|
451
|
+
{ cwd: rootDir }
|
|
452
|
+
);
|
|
453
|
+
} catch (error) {
|
|
454
|
+
fail(`[cli] setup dry-run 실행 실패: ${error.message}`);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
try {
|
|
458
|
+
await execFileAsync(
|
|
459
|
+
nodeBin,
|
|
460
|
+
[
|
|
461
|
+
cliPath,
|
|
462
|
+
'install',
|
|
463
|
+
'--preset',
|
|
464
|
+
'balanced-core',
|
|
465
|
+
'--target',
|
|
466
|
+
'/tmp/tri-agent-manager-validate',
|
|
467
|
+
'--dry-run',
|
|
468
|
+
'--yes',
|
|
469
|
+
'--non-interactive'
|
|
470
|
+
],
|
|
471
|
+
{ cwd: rootDir }
|
|
472
|
+
);
|
|
473
|
+
} catch (error) {
|
|
474
|
+
fail(`[cli] install preset dry-run 실행 실패: ${error.message}`);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
try {
|
|
478
|
+
await execFileAsync(
|
|
479
|
+
nodeBin,
|
|
480
|
+
[
|
|
481
|
+
cliPath,
|
|
482
|
+
'install',
|
|
483
|
+
'--tool',
|
|
484
|
+
'codex',
|
|
485
|
+
'--components',
|
|
486
|
+
'skills,workflows,commands',
|
|
487
|
+
'--target',
|
|
488
|
+
'/tmp/tri-agent-manager-validate',
|
|
489
|
+
'--dry-run',
|
|
490
|
+
'--yes',
|
|
491
|
+
'--non-interactive'
|
|
492
|
+
],
|
|
493
|
+
{ cwd: rootDir }
|
|
494
|
+
);
|
|
495
|
+
} catch (error) {
|
|
496
|
+
fail(`[cli] install dry-run 실행 실패: ${error.message}`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
try {
|
|
500
|
+
await execFileAsync(
|
|
501
|
+
nodeBin,
|
|
502
|
+
[
|
|
503
|
+
cliPath,
|
|
504
|
+
'update',
|
|
505
|
+
'--preset',
|
|
506
|
+
'frontend-refactor',
|
|
507
|
+
'--target',
|
|
508
|
+
'/tmp/tri-agent-manager-validate',
|
|
509
|
+
'--dry-run',
|
|
510
|
+
'--yes',
|
|
511
|
+
'--non-interactive'
|
|
512
|
+
],
|
|
513
|
+
{ cwd: rootDir }
|
|
514
|
+
);
|
|
515
|
+
} catch (error) {
|
|
516
|
+
fail(`[cli] update preset dry-run 실행 실패: ${error.message}`);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
try {
|
|
520
|
+
await execFileAsync(
|
|
521
|
+
nodeBin,
|
|
522
|
+
[
|
|
523
|
+
cliPath,
|
|
524
|
+
'install',
|
|
525
|
+
'--preset',
|
|
526
|
+
'prd-to-production',
|
|
527
|
+
'--target',
|
|
528
|
+
'/tmp/tri-agent-manager-validate',
|
|
529
|
+
'--dry-run',
|
|
530
|
+
'--yes',
|
|
531
|
+
'--non-interactive'
|
|
532
|
+
],
|
|
533
|
+
{ cwd: rootDir }
|
|
534
|
+
);
|
|
535
|
+
} catch (error) {
|
|
536
|
+
fail(`[cli] install prd-to-production preset dry-run 실행 실패: ${error.message}`);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
try {
|
|
540
|
+
await execFileAsync(
|
|
541
|
+
nodeBin,
|
|
542
|
+
[
|
|
543
|
+
cliPath,
|
|
544
|
+
'update',
|
|
545
|
+
'--tool',
|
|
546
|
+
'codex',
|
|
547
|
+
'--components',
|
|
548
|
+
'skills,workflows,commands',
|
|
549
|
+
'--target',
|
|
550
|
+
'/tmp/tri-agent-manager-validate',
|
|
551
|
+
'--dry-run',
|
|
552
|
+
'--yes',
|
|
553
|
+
'--non-interactive'
|
|
554
|
+
],
|
|
555
|
+
{ cwd: rootDir }
|
|
556
|
+
);
|
|
557
|
+
} catch (error) {
|
|
558
|
+
fail(`[cli] update dry-run 실행 실패: ${error.message}`);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async function main() {
|
|
563
|
+
await validatePackageJson();
|
|
564
|
+
await validateCatalog();
|
|
565
|
+
await validateReadmes();
|
|
566
|
+
|
|
567
|
+
for (const toolName of toolDirs) {
|
|
568
|
+
await validateSkillDirectory(toolName);
|
|
569
|
+
const { commandIds } = await validateCommandCatalog(toolName);
|
|
570
|
+
await validateWorkflowCatalog(toolName, commandIds);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
await validateClaudeExtras();
|
|
574
|
+
await validateAntigravityExtras();
|
|
575
|
+
await runCliSmokeTests();
|
|
576
|
+
|
|
577
|
+
if (errors.length > 0) {
|
|
578
|
+
console.error('검증 실패:');
|
|
579
|
+
for (const error of errors) {
|
|
580
|
+
console.error(`- ${error}`);
|
|
581
|
+
}
|
|
582
|
+
process.exit(1);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
console.log('검증 통과: 구조/계약/CLI 동작이 모두 유효합니다.');
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
main().catch((error) => {
|
|
589
|
+
console.error(`검증 스크립트 오류: ${error.message}`);
|
|
590
|
+
process.exit(1);
|
|
591
|
+
});
|