archgraph 0.1.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/LICENSE +21 -0
- package/README.md +123 -0
- package/bin/bgx.js +2 -0
- package/docs/examples/executors.example.json +15 -0
- package/docs/examples/view-spec.yaml +6 -0
- package/docs/release.md +15 -0
- package/docs/view-spec.md +28 -0
- package/integrations/README.md +20 -0
- package/integrations/claude/.claude/skills/backend-graphing/SKILL.md +56 -0
- package/integrations/claude/.claude/skills/backend-graphing-describe/SKILL.md +50 -0
- package/integrations/claude/.claude-plugin/marketplace.json +18 -0
- package/integrations/claude/.claude-plugin/plugin.json +9 -0
- package/integrations/claude/skills/backend-graphing/SKILL.md +56 -0
- package/integrations/claude/skills/backend-graphing-describe/SKILL.md +50 -0
- package/integrations/codex/skills/backend-graphing/SKILL.md +56 -0
- package/integrations/codex/skills/backend-graphing-describe/SKILL.md +50 -0
- package/package.json +49 -0
- package/packages/cli/src/index.js +415 -0
- package/packages/core/src/analyze-project.js +1238 -0
- package/packages/core/src/export.js +77 -0
- package/packages/core/src/index.js +4 -0
- package/packages/core/src/types.js +37 -0
- package/packages/core/src/view.js +86 -0
- package/packages/viewer/public/app.js +226 -0
- package/packages/viewer/public/canvas.js +181 -0
- package/packages/viewer/public/comments.js +193 -0
- package/packages/viewer/public/index.html +95 -0
- package/packages/viewer/public/layout.js +72 -0
- package/packages/viewer/public/minimap.js +92 -0
- package/packages/viewer/public/render.js +366 -0
- package/packages/viewer/public/sidebar.js +107 -0
- package/packages/viewer/public/styles.css +728 -0
- package/packages/viewer/public/theme.js +19 -0
- package/packages/viewer/public/tooltip.js +44 -0
- package/packages/viewer/src/index.js +590 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import { gzipSync } from 'node:zlib';
|
|
4
|
+
import { access, cp, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
|
|
9
|
+
async function loadWorkspaceModule(name, fallbackPath) {
|
|
10
|
+
try {
|
|
11
|
+
return await import(name);
|
|
12
|
+
} catch {
|
|
13
|
+
return import(fallbackPath);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const coreModule = await loadWorkspaceModule('@backend-graphing/core', '../../core/src/index.js');
|
|
18
|
+
const viewerModule = await loadWorkspaceModule('@backend-graphing/viewer', '../../viewer/src/index.js');
|
|
19
|
+
|
|
20
|
+
const { analyzeProject, buildView, exportGraph } = coreModule;
|
|
21
|
+
const { startViewerServer } = viewerModule;
|
|
22
|
+
const CLI_FILE = fileURLToPath(import.meta.url);
|
|
23
|
+
const PROJECT_ROOT = resolve(dirname(CLI_FILE), '../../../');
|
|
24
|
+
|
|
25
|
+
function usage() {
|
|
26
|
+
return [
|
|
27
|
+
'Usage:',
|
|
28
|
+
' bgx analyze --project <path> --out <file> [--format graph-json] [--compress gzip]',
|
|
29
|
+
' bgx export --graph <file> --format cypher|mermaid --out <file>',
|
|
30
|
+
' bgx view --graph <file> [--spec <view.yaml|json>] [--serve] [--port 4310]',
|
|
31
|
+
' bgx describe --graph <file> --out <file> --executor codex|claude [--executor-config <path>] [--language bilingual] [--detail structured]',
|
|
32
|
+
' bgx install [--target codex|claude|all] [--codex-home <dir>] [--claude-home <dir>] [--dry-run]',
|
|
33
|
+
].join('\n');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseYamlValue(raw) {
|
|
37
|
+
const value = raw.trim();
|
|
38
|
+
if (value === 'true') return true;
|
|
39
|
+
if (value === 'false') return false;
|
|
40
|
+
if (/^-?\d+$/.test(value)) return Number(value);
|
|
41
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
42
|
+
return value
|
|
43
|
+
.slice(1, -1)
|
|
44
|
+
.split(',')
|
|
45
|
+
.map((item) => item.trim())
|
|
46
|
+
.filter(Boolean)
|
|
47
|
+
.map((item) => item.replace(/^['"]|['"]$/g, ''));
|
|
48
|
+
}
|
|
49
|
+
return value.replace(/^['"]|['"]$/g, '');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseSpecText(text) {
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(text);
|
|
55
|
+
} catch {
|
|
56
|
+
const out = {};
|
|
57
|
+
for (const line of text.split('\n')) {
|
|
58
|
+
const trimmed = line.trim();
|
|
59
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
60
|
+
const idx = trimmed.indexOf(':');
|
|
61
|
+
if (idx <= 0) continue;
|
|
62
|
+
const key = trimmed.slice(0, idx).trim();
|
|
63
|
+
const value = trimmed.slice(idx + 1).trim();
|
|
64
|
+
out[key] = parseYamlValue(value);
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function pathExists(path) {
|
|
71
|
+
try {
|
|
72
|
+
await access(path);
|
|
73
|
+
return true;
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function edgeKindsFrom(nodeId, edges) {
|
|
80
|
+
return edges.filter((edge) => edge.from === nodeId);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function edgeKindsTo(nodeId, edges) {
|
|
84
|
+
return edges.filter((edge) => edge.to === nodeId);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function buildEntrySections(label, kind, dependencies, conditions) {
|
|
88
|
+
const depText = dependencies.length ? dependencies.join(', ') : 'None';
|
|
89
|
+
const condText = conditions.length ? conditions.join(', ') : 'No explicit branch conditions detected';
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
ko: {
|
|
93
|
+
purpose: `${label} (${kind})의 핵심 목적을 설명하고, 아키텍처 흐름에서의 역할을 요약합니다.`,
|
|
94
|
+
inputs_outputs: `입력은 호출 컨텍스트/요청 데이터이며 출력은 하위 처리(${depText})로 전달되는 결과입니다.`,
|
|
95
|
+
main_flow: `선택된 노드에서 1-hop 의존 경로를 따라 주요 실행 흐름을 구성합니다: ${depText}.`,
|
|
96
|
+
conditions: `정적 분석 기반 분기 조건: ${condText}.`,
|
|
97
|
+
dependencies: `직접 의존 관계: ${depText}.`,
|
|
98
|
+
risks_notes: '런타임 동적 분기/리플렉션은 정적 그래프에 완전히 반영되지 않을 수 있습니다.',
|
|
99
|
+
},
|
|
100
|
+
en: {
|
|
101
|
+
purpose: `Summarizes the role of ${label} (${kind}) in the endpoint-first architecture flow.`,
|
|
102
|
+
inputs_outputs: `Inputs come from caller/request context and outputs are delegated through direct dependencies (${depText}).`,
|
|
103
|
+
main_flow: `Primary execution path is represented by 1-hop graph dependencies: ${depText}.`,
|
|
104
|
+
conditions: `Static branch conditions detected: ${condText}.`,
|
|
105
|
+
dependencies: `Direct dependency set: ${depText}.`,
|
|
106
|
+
risks_notes: 'Dynamic runtime dispatch and reflection may not be fully captured by static analysis.',
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function buildDescriptionBundle(graph, { executor, language = 'bilingual', detail = 'structured' }) {
|
|
112
|
+
const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
113
|
+
const entries = [];
|
|
114
|
+
|
|
115
|
+
for (const node of graph.nodes) {
|
|
116
|
+
if (!['endpoint', 'function', 'route_handler', 'agent', 'condition'].includes(node.kind)) continue;
|
|
117
|
+
|
|
118
|
+
const outgoing = edgeKindsFrom(node.id, graph.edges)
|
|
119
|
+
.map((edge) => nodeById.get(edge.to)?.label)
|
|
120
|
+
.filter(Boolean)
|
|
121
|
+
.slice(0, 12);
|
|
122
|
+
|
|
123
|
+
const conditionEdges = edgeKindsFrom(node.id, graph.edges)
|
|
124
|
+
.filter((edge) => edge.kind === 'condition_true' || edge.kind === 'condition_false' || edge.kind === 'condition_case')
|
|
125
|
+
.map((edge) => `${edge.kind}:${edge.label ?? nodeById.get(edge.to)?.label ?? edge.to}`)
|
|
126
|
+
.slice(0, 12);
|
|
127
|
+
|
|
128
|
+
const inbound = edgeKindsTo(node.id, graph.edges)
|
|
129
|
+
.map((edge) => edge.kind)
|
|
130
|
+
.slice(0, 8);
|
|
131
|
+
|
|
132
|
+
const sections = buildEntrySections(node.label, node.kind, [...new Set([...outgoing, ...inbound])], conditionEdges);
|
|
133
|
+
|
|
134
|
+
entries.push({
|
|
135
|
+
id: node.id,
|
|
136
|
+
kind: node.kind,
|
|
137
|
+
label: node.label,
|
|
138
|
+
language,
|
|
139
|
+
detail,
|
|
140
|
+
sections,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const group of graph.groups ?? []) {
|
|
145
|
+
if (!['endpoint', 'purpose', 'agent'].includes(group.kind)) continue;
|
|
146
|
+
const memberLabels = group.memberNodeIds
|
|
147
|
+
.map((id) => nodeById.get(id)?.label)
|
|
148
|
+
.filter(Boolean)
|
|
149
|
+
.slice(0, 18);
|
|
150
|
+
|
|
151
|
+
entries.push({
|
|
152
|
+
id: `group:${group.id}`,
|
|
153
|
+
kind: `group:${group.kind}`,
|
|
154
|
+
label: group.name,
|
|
155
|
+
language,
|
|
156
|
+
detail,
|
|
157
|
+
sections: buildEntrySections(group.name, `group:${group.kind}`, memberLabels, []),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
version: '1',
|
|
163
|
+
generatedAt: new Date().toISOString(),
|
|
164
|
+
sourceGraph: {
|
|
165
|
+
schemaVersion: graph.schemaVersion,
|
|
166
|
+
project: graph.project,
|
|
167
|
+
nodes: graph.nodes.length,
|
|
168
|
+
edges: graph.edges.length,
|
|
169
|
+
},
|
|
170
|
+
generator: {
|
|
171
|
+
executor,
|
|
172
|
+
language,
|
|
173
|
+
detail,
|
|
174
|
+
},
|
|
175
|
+
entries,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function installCodexSkill({ codexHome, dryRun }) {
|
|
180
|
+
const skillNames = ['backend-graphing', 'backend-graphing-describe'];
|
|
181
|
+
const installs = [];
|
|
182
|
+
|
|
183
|
+
for (const name of skillNames) {
|
|
184
|
+
const source = join(PROJECT_ROOT, 'integrations', 'codex', 'skills', name);
|
|
185
|
+
const destination = join(codexHome, 'skills', name);
|
|
186
|
+
|
|
187
|
+
if (!(await pathExists(source))) {
|
|
188
|
+
throw new Error(`Codex skill source not found: ${source}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
installs.push({ source, destination });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!dryRun) {
|
|
195
|
+
await mkdir(join(codexHome, 'skills'), { recursive: true });
|
|
196
|
+
for (const item of installs) {
|
|
197
|
+
await cp(item.source, item.destination, { recursive: true, force: true });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return installs;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function installClaudePlugin({ claudeHome, dryRun }) {
|
|
205
|
+
const pluginSource = join(PROJECT_ROOT, 'integrations', 'claude');
|
|
206
|
+
const pluginDestination = join(claudeHome, 'plugins', 'marketplaces', 'backend-graphing');
|
|
207
|
+
const skillNames = ['backend-graphing', 'backend-graphing-describe'];
|
|
208
|
+
|
|
209
|
+
if (!(await pathExists(pluginSource))) {
|
|
210
|
+
throw new Error(`Claude plugin source not found: ${pluginSource}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const skillCopies = skillNames.map((name) => ({
|
|
214
|
+
skillSource: join(pluginSource, '.claude', 'skills', name),
|
|
215
|
+
skillDestination: join(claudeHome, 'skills', name),
|
|
216
|
+
}));
|
|
217
|
+
|
|
218
|
+
if (!dryRun) {
|
|
219
|
+
await mkdir(join(claudeHome, 'plugins', 'marketplaces'), { recursive: true });
|
|
220
|
+
await cp(pluginSource, pluginDestination, { recursive: true, force: true });
|
|
221
|
+
|
|
222
|
+
await mkdir(join(claudeHome, 'skills'), { recursive: true });
|
|
223
|
+
for (const copySpec of skillCopies) {
|
|
224
|
+
if (await pathExists(copySpec.skillSource)) {
|
|
225
|
+
await cp(copySpec.skillSource, copySpec.skillDestination, { recursive: true, force: true });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
pluginSource,
|
|
232
|
+
pluginDestination,
|
|
233
|
+
skillCopies,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function run() {
|
|
238
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
239
|
+
|
|
240
|
+
if (command === '--help' || command === '-h' || command === 'help') {
|
|
241
|
+
console.log(usage());
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (command === '--version' || command === '-v') {
|
|
246
|
+
console.log('0.1.0');
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!command) {
|
|
251
|
+
console.log(usage());
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const parsed = parseArgs({
|
|
256
|
+
args: rest,
|
|
257
|
+
allowPositionals: true,
|
|
258
|
+
strict: false,
|
|
259
|
+
options: {
|
|
260
|
+
project: { type: 'string' },
|
|
261
|
+
out: { type: 'string' },
|
|
262
|
+
format: { type: 'string' },
|
|
263
|
+
graph: { type: 'string' },
|
|
264
|
+
spec: { type: 'string' },
|
|
265
|
+
serve: { type: 'boolean' },
|
|
266
|
+
port: { type: 'string' },
|
|
267
|
+
compress: { type: 'string' },
|
|
268
|
+
frontend: { type: 'string' },
|
|
269
|
+
executor: { type: 'string' },
|
|
270
|
+
'executor-config': { type: 'string' },
|
|
271
|
+
language: { type: 'string' },
|
|
272
|
+
detail: { type: 'string' },
|
|
273
|
+
descriptions: { type: 'string' },
|
|
274
|
+
target: { type: 'string' },
|
|
275
|
+
'codex-home': { type: 'string' },
|
|
276
|
+
'claude-home': { type: 'string' },
|
|
277
|
+
'dry-run': { type: 'boolean' },
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
if (command === 'analyze') {
|
|
282
|
+
if (!parsed.values.project || !parsed.values.out) {
|
|
283
|
+
throw new Error('analyze requires --project and --out');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const graph = await analyzeProject({
|
|
287
|
+
projectPath: parsed.values.project,
|
|
288
|
+
includeFrontend: Boolean(parsed.values.frontend),
|
|
289
|
+
frontendPath: parsed.values.frontend,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const format = parsed.values.format ?? 'graph-json';
|
|
293
|
+
if (format !== 'graph-json') {
|
|
294
|
+
await exportGraph(graph, format, parsed.values.out);
|
|
295
|
+
console.log(`Wrote ${parsed.values.out}`);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const json = `${JSON.stringify(graph, null, 2)}\n`;
|
|
300
|
+
if (parsed.values.compress === 'gzip') {
|
|
301
|
+
const compressed = gzipSync(Buffer.from(json, 'utf8'));
|
|
302
|
+
await writeFile(parsed.values.out, compressed);
|
|
303
|
+
} else {
|
|
304
|
+
await writeFile(parsed.values.out, json, 'utf8');
|
|
305
|
+
}
|
|
306
|
+
console.log(`Wrote ${parsed.values.out}`);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (command === 'export') {
|
|
311
|
+
if (!parsed.values.graph || !parsed.values.format || !parsed.values.out) {
|
|
312
|
+
throw new Error('export requires --graph --format --out');
|
|
313
|
+
}
|
|
314
|
+
const graphText = await readFile(parsed.values.graph, 'utf8');
|
|
315
|
+
const graph = JSON.parse(graphText);
|
|
316
|
+
await exportGraph(graph, parsed.values.format, parsed.values.out);
|
|
317
|
+
console.log(`Wrote ${parsed.values.out}`);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (command === 'view') {
|
|
322
|
+
if (!parsed.values.graph) {
|
|
323
|
+
throw new Error('view requires --graph');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const graph = JSON.parse(await readFile(parsed.values.graph, 'utf8'));
|
|
327
|
+
const spec = parsed.values.spec
|
|
328
|
+
? parseSpecText(await readFile(parsed.values.spec, 'utf8'))
|
|
329
|
+
: { version: '1', depth: 2, groupBy: 'endpoint' };
|
|
330
|
+
|
|
331
|
+
const view = buildView(graph, spec);
|
|
332
|
+
|
|
333
|
+
if (parsed.values.serve) {
|
|
334
|
+
const port = parsed.values.port ? Number(parsed.values.port) : 4310;
|
|
335
|
+
await startViewerServer({
|
|
336
|
+
graph,
|
|
337
|
+
view,
|
|
338
|
+
port,
|
|
339
|
+
descriptionsPath: parsed.values.descriptions,
|
|
340
|
+
});
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const out = parsed.values.out ?? 'view.json';
|
|
345
|
+
await writeFile(out, `${JSON.stringify(view, null, 2)}\n`, 'utf8');
|
|
346
|
+
console.log(`Wrote ${out}`);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (command === 'describe') {
|
|
351
|
+
if (!parsed.values.graph || !parsed.values.out || !parsed.values.executor) {
|
|
352
|
+
throw new Error('describe requires --graph --out --executor');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (!['codex', 'claude'].includes(parsed.values.executor)) {
|
|
356
|
+
throw new Error('describe --executor must be codex or claude');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (parsed.values['executor-config']) {
|
|
360
|
+
const configText = await readFile(parsed.values['executor-config'], 'utf8');
|
|
361
|
+
const config = JSON.parse(configText);
|
|
362
|
+
if (!config?.[parsed.values.executor]) {
|
|
363
|
+
throw new Error(`Executor not found in config: ${parsed.values.executor}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const graph = JSON.parse(await readFile(parsed.values.graph, 'utf8'));
|
|
368
|
+
const bundle = buildDescriptionBundle(graph, {
|
|
369
|
+
executor: parsed.values.executor,
|
|
370
|
+
language: parsed.values.language ?? 'bilingual',
|
|
371
|
+
detail: parsed.values.detail ?? 'structured',
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
await writeFile(parsed.values.out, `${JSON.stringify(bundle, null, 2)}\n`, 'utf8');
|
|
375
|
+
console.log(`Wrote ${parsed.values.out}`);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (command === 'install') {
|
|
380
|
+
const target = parsed.values.target ?? 'all';
|
|
381
|
+
if (!['codex', 'claude', 'all'].includes(target)) {
|
|
382
|
+
throw new Error(`Invalid --target value: ${target}`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const codexHome = parsed.values['codex-home'] ?? process.env.CODEX_HOME ?? join(homedir(), '.codex');
|
|
386
|
+
const claudeHome = parsed.values['claude-home'] ?? join(homedir(), '.claude');
|
|
387
|
+
const dryRun = Boolean(parsed.values['dry-run']);
|
|
388
|
+
|
|
389
|
+
if (target === 'codex' || target === 'all') {
|
|
390
|
+
const installed = await installCodexSkill({ codexHome, dryRun });
|
|
391
|
+
for (const item of installed) {
|
|
392
|
+
console.log(`${dryRun ? '[dry-run] ' : ''}Codex skill: ${item.source} -> ${item.destination}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (target === 'claude' || target === 'all') {
|
|
397
|
+
const installed = await installClaudePlugin({ claudeHome, dryRun });
|
|
398
|
+
console.log(
|
|
399
|
+
`${dryRun ? '[dry-run] ' : ''}Claude plugin: ${installed.pluginSource} -> ${installed.pluginDestination}`,
|
|
400
|
+
);
|
|
401
|
+
for (const item of installed.skillCopies) {
|
|
402
|
+
console.log(`${dryRun ? '[dry-run] ' : ''}Claude skill: ${item.skillSource} -> ${item.skillDestination}`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
throw new Error(`Unknown command: ${command}`);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
run().catch((error) => {
|
|
413
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
414
|
+
process.exitCode = 1;
|
|
415
|
+
});
|