aiakiv 0.1.0 → 0.2.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/README.md +66 -10
- package/dist/bind.js +19 -13
- package/dist/clients/global.js +50 -0
- package/dist/console.js +227 -0
- package/dist/consoleApi.js +215 -0
- package/dist/consoleKeyFile.js +64 -0
- package/dist/ctx.js +13 -0
- package/dist/dashboard.js +197 -0
- package/dist/doctor.js +103 -23
- package/dist/handoff.js +55 -6
- package/dist/main.js +202 -25
- package/dist/mcpProbe.js +3 -1
- package/dist/render.js +108 -0
- package/dist/setup.js +273 -0
- package/dist/skills.js +14 -2
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/doctor.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// `aiakiv doctor [--client …] [--dir <폴더>] [--out <파일>]` — 설정 파일, 클라이언트 등록 목록,
|
|
2
2
|
// 스킬 위치를 확인하고 키로 묶인 연결은 get_save_target 을 직접 불러 본다. 결과는 마크다운 파일로.
|
|
3
|
-
// bind 가 쓰는 나머지(폴더 스킬
|
|
3
|
+
// bind 가 쓰는 나머지(폴더 스킬 여덟 개, Claude 의 enabledMcpjsonServers)도 그 폴더에 있는지 본다.
|
|
4
4
|
// 전역 등록 목록은 bind 와 상관없지만 겹치는 연결을 사람에게 알리려고 계속 본다.
|
|
5
|
+
// setup 이 쓰는 전역 파일(Gemini CLI `~/.gemini/settings.json`, Antigravity `~/.gemini/config/mcp_config.json`)의
|
|
6
|
+
// 키 항목은 get_save_target 으로 binding main 인지 본다(clients/global.ts).
|
|
5
7
|
//
|
|
6
8
|
// 키와 토큰 평문은 화면에도 파일에도 쓰지 않는다(앞 8자 + …). 클라이언트 목록은 다른 서비스의
|
|
7
9
|
// 주소(키가 든 주소가 있을 수 있다)를 옮기지 않도록 AiAkiv 항목만 고른다.
|
|
@@ -10,15 +12,26 @@ import { join, resolve } from "node:path";
|
|
|
10
12
|
import { classifyClaudeName, CLAUDE_KIND_LABEL, findClaude, installedPluginIds, parseClaudeMcpList, } from "./clients/claude.js";
|
|
11
13
|
import { codexHome, findCodex, parseCodexMcpList } from "./clients/codex.js";
|
|
12
14
|
import { cursorGlobalConfig } from "./clients/cursor.js";
|
|
15
|
+
import { globalConfigPath, GLOBAL_CLIENT_IDS, GLOBAL_SERVER_NAME } from "./clients/global.js";
|
|
13
16
|
import { CLAUDE_FOLDER_SETTINGS, CLAUDE_FOLDER_SETTINGS_LOCAL, CLIENT_IDS, FOLDER_CONFIG, looksLikeAiAkiv, } from "./clients/types.js";
|
|
14
17
|
import { jsonServers, jsonStringArray, readJsonObject } from "./config/json.js";
|
|
15
18
|
import { readTomlObject, tomlServers } from "./config/toml.js";
|
|
19
|
+
import { CONSOLE_KEY_FILE, expiryText, LEGACY_KEY_FILE_MESSAGE, readConsoleKeyFile } from "./consoleKeyFile.js";
|
|
16
20
|
import { probeSaveTarget } from "./mcpProbe.js";
|
|
17
21
|
import { failReason } from "./proc.js";
|
|
18
22
|
import { FOLDER_SKILLS_DIR, SKILL_NAMES, SKILLS_AGENT, skillsManualCommand } from "./skills.js";
|
|
19
23
|
import { controlBase, mcpBase } from "./urls.js";
|
|
20
24
|
import { VERSION } from "./version.js";
|
|
21
25
|
export const DEFAULT_REPORT = "aiakiv-verification-test.md";
|
|
26
|
+
export const DOCTOR_CLIENT_IDS = [...CLIENT_IDS, "antigravity"];
|
|
27
|
+
/** 폴더 고정(bind)이 있는 클라이언트인가. */
|
|
28
|
+
export function isFolderClient(c) {
|
|
29
|
+
return CLIENT_IDS.includes(c);
|
|
30
|
+
}
|
|
31
|
+
/** setup 이 전역 파일을 쓰는 클라이언트인가. */
|
|
32
|
+
function isSetupClient(c) {
|
|
33
|
+
return GLOBAL_CLIENT_IDS.includes(c);
|
|
34
|
+
}
|
|
22
35
|
/** 키 가리기: 앞 8자 + …(짧으면 전부 가린다). */
|
|
23
36
|
export function maskSecret(v) {
|
|
24
37
|
return v.length > 12 ? `${v.slice(0, 8)}…` : "…";
|
|
@@ -77,12 +90,23 @@ export function scanConfigs(ctx, dir, clients) {
|
|
|
77
90
|
const entries = [];
|
|
78
91
|
const notes = [];
|
|
79
92
|
const files = [];
|
|
80
|
-
for (const c of clients)
|
|
81
|
-
|
|
93
|
+
for (const c of clients) {
|
|
94
|
+
if (isFolderClient(c))
|
|
95
|
+
files.push({ client: c, scope: "폴더", file: join(dir, FOLDER_CONFIG[c]) });
|
|
96
|
+
}
|
|
82
97
|
if (clients.includes("cursor"))
|
|
83
98
|
files.push({ client: "cursor", scope: "전역", file: cursorGlobalConfig(ctx) });
|
|
84
|
-
|
|
85
|
-
|
|
99
|
+
for (const c of GLOBAL_CLIENT_IDS) {
|
|
100
|
+
if (!clients.includes(c))
|
|
101
|
+
continue;
|
|
102
|
+
const file = globalConfigPath(ctx, c);
|
|
103
|
+
// 폴더가 홈이면 폴더의 `.gemini/settings.json` 이 곧 전역 파일이다. 전역으로 한 번만 본다
|
|
104
|
+
// (setup 이 쓴 계정 전체 키를 폴더 고정 키로 잘못 읽지 않게).
|
|
105
|
+
for (let i = files.length - 1; i >= 0; i -= 1) {
|
|
106
|
+
if (files[i].scope === "폴더" && resolve(files[i].file) === resolve(file))
|
|
107
|
+
files.splice(i, 1);
|
|
108
|
+
}
|
|
109
|
+
files.push({ client: c, scope: "전역", file });
|
|
86
110
|
}
|
|
87
111
|
for (const f of files) {
|
|
88
112
|
if (!existsSync(f.file))
|
|
@@ -184,6 +208,8 @@ export function skillLocations(ctx, dir) {
|
|
|
184
208
|
join(ctx.home, ".agents", "skills"),
|
|
185
209
|
join(ctx.home, ".cursor", "skills"),
|
|
186
210
|
join(ctx.home, ".gemini", "skills"),
|
|
211
|
+
join(ctx.home, ".gemini", "config", "skills"),
|
|
212
|
+
join(ctx.home, ".gemini", "antigravity-cli", "skills"),
|
|
187
213
|
join(codexHome(ctx), "skills"),
|
|
188
214
|
join(dir, ".claude", "skills"),
|
|
189
215
|
join(dir, ".agents", "skills"),
|
|
@@ -208,19 +234,29 @@ export function skillLocations(ctx, dir) {
|
|
|
208
234
|
}
|
|
209
235
|
return out;
|
|
210
236
|
}
|
|
211
|
-
|
|
237
|
+
/** setup 이 쓴 전역 항목인가(Gemini CLI, Antigravity 의 전역 파일의 `AiAkiv`). 이 항목의 키는 binding main 이어야
|
|
238
|
+
* 한다. 같은 파일의 다른 이름(`AiAkiv-<프로젝트>`, 홈 폴더에서 bind 한 경우 등)은 프로젝트 키로 본다. */
|
|
239
|
+
function isSetupEntry(e) {
|
|
240
|
+
return e.scope === "전역" && isSetupClient(e.client) && e.name === GLOBAL_SERVER_NAME;
|
|
241
|
+
}
|
|
242
|
+
function probeLine(p, e) {
|
|
212
243
|
if (p.ok)
|
|
213
244
|
return `팀 ${p.team ?? "?"}, 프로젝트 ${p.project ?? "?"}, binding ${p.binding ?? "?"}`;
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
245
|
+
let hint = "";
|
|
246
|
+
if (p.status === 401) {
|
|
247
|
+
hint = isSetupEntry(e)
|
|
248
|
+
? ` (키가 폐기됐거나 틀립니다. 계정 전체 키는 한 사람에게 하나라 다른 PC 에서 setup 을 하면 이 키가 폐기됩니다. ` +
|
|
249
|
+
`npx aiakiv setup --client ${e.client} 로 다시 받으세요)`
|
|
250
|
+
: " (키가 폐기됐거나 틀립니다. 같은 프로젝트를 다른 폴더에서 bind 하면 이전 키가 폐기되니, 이 폴더에서 bind 를 다시 하세요)";
|
|
251
|
+
}
|
|
217
252
|
return `실패${p.status ? `(${p.status})` : ""}: ${p.reason}${hint}`;
|
|
218
253
|
}
|
|
219
|
-
/** bind 가 스킬을 넣는 폴더 자리마다
|
|
254
|
+
/** bind 가 스킬을 넣는 폴더 자리마다 여덟 개가 있는지. 폴더 고정이 없는 클라이언트(antigravity)는 뺀다. */
|
|
220
255
|
export function folderSkillChecks(dir, clients) {
|
|
221
256
|
const byRel = new Map();
|
|
222
|
-
for (const c of clients)
|
|
257
|
+
for (const c of clients.filter(isFolderClient)) {
|
|
223
258
|
byRel.set(FOLDER_SKILLS_DIR[c], [...(byRel.get(FOLDER_SKILLS_DIR[c]) ?? []), c]);
|
|
259
|
+
}
|
|
224
260
|
return [...byRel].map(([rel, cs]) => {
|
|
225
261
|
const names = aiakivSkillsIn(join(dir, ...rel.split("/")));
|
|
226
262
|
return { rel, clients: cs, names, missing: SKILL_NAMES.filter((n) => !names.includes(n)) };
|
|
@@ -251,6 +287,21 @@ export function claudeEnabledChecks(dir, entries) {
|
|
|
251
287
|
notes,
|
|
252
288
|
};
|
|
253
289
|
}
|
|
290
|
+
/** `.aiakiv/console.json`(console key 가 쓴 계정 키 파일)이 있으면 한 줄. 평문은 싣지 않는다. 없으면 null.
|
|
291
|
+
* 옛 판(R0, 프로젝트 키를 담은 파일)이면 다시 받으라고 적는다(console, dashboard 도 그 파일은 쓰지 않는다). */
|
|
292
|
+
function consoleKeyLine(dir) {
|
|
293
|
+
try {
|
|
294
|
+
const f = readConsoleKeyFile(dir);
|
|
295
|
+
if (!f)
|
|
296
|
+
return null;
|
|
297
|
+
if (f.legacy)
|
|
298
|
+
return `${CONSOLE_KEY_FILE}: ${LEGACY_KEY_FILE_MESSAGE}`;
|
|
299
|
+
return `CLI 계정 키(key_id ${f.key_id ?? maskSecret(f.key)}, 만료 ${expiryText(f.expires_at)}): ${CONSOLE_KEY_FILE}`;
|
|
300
|
+
}
|
|
301
|
+
catch (e) {
|
|
302
|
+
return `CLI 계정 키 파일을 읽을 수 없습니다: ${e.message}`;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
254
305
|
function howLabel(e) {
|
|
255
306
|
if (e.auth === "apikey")
|
|
256
307
|
return `${AUTH_LABEL[e.auth]} ${e.keyMasked}`;
|
|
@@ -267,7 +318,7 @@ export async function runDoctor(ctx, opts) {
|
|
|
267
318
|
ctx.err(`폴더가 없습니다: ${dir}`);
|
|
268
319
|
return 1;
|
|
269
320
|
}
|
|
270
|
-
const clients = opts.clients?.length ? opts.clients : [...
|
|
321
|
+
const clients = opts.clients?.length ? opts.clients : [...DOCTOR_CLIENT_IDS];
|
|
271
322
|
const outFile = opts.out ? resolve(ctx.cwd, opts.out) : join(dir, DEFAULT_REPORT);
|
|
272
323
|
const scan = scanConfigs(ctx, dir, clients);
|
|
273
324
|
const probes = new Map();
|
|
@@ -285,6 +336,7 @@ export async function runDoctor(ctx, opts) {
|
|
|
285
336
|
const skills = skillLocations(ctx, dir);
|
|
286
337
|
const folderSkills = folderSkillChecks(dir, clients);
|
|
287
338
|
const enabled = claudeEnabledChecks(dir, scan.entries);
|
|
339
|
+
const consoleKey = consoleKeyLine(dir);
|
|
288
340
|
const md = [];
|
|
289
341
|
md.push("# AiAkiv 확인 기록", "");
|
|
290
342
|
md.push(`- 일시: ${new Date().toISOString()}`);
|
|
@@ -309,12 +361,14 @@ export async function runDoctor(ctx, opts) {
|
|
|
309
361
|
if (keyed.length) {
|
|
310
362
|
for (const e of keyed) {
|
|
311
363
|
const p = probes.get(e);
|
|
312
|
-
md.push(`- ${e.name}(${e.file}): ${typeof p === "string" ? p : probeLine(p)}`);
|
|
364
|
+
md.push(`- ${e.name}(${e.file}): ${typeof p === "string" ? p : probeLine(p, e)}`);
|
|
313
365
|
}
|
|
314
366
|
}
|
|
315
367
|
else
|
|
316
368
|
md.push("키로 묶인 연결이 없습니다.");
|
|
317
369
|
md.push("");
|
|
370
|
+
if (consoleKey)
|
|
371
|
+
md.push("## CLI 계정 키", "", `- ${consoleKey}`, "");
|
|
318
372
|
md.push("## 클라이언트 등록 목록", "");
|
|
319
373
|
for (const s of lists) {
|
|
320
374
|
md.push(`### ${s.title}`, "");
|
|
@@ -326,7 +380,7 @@ export async function runDoctor(ctx, opts) {
|
|
|
326
380
|
if (skills.length) {
|
|
327
381
|
for (const s of skills) {
|
|
328
382
|
const missing = SKILL_NAMES.filter((n) => !s.names.includes(n));
|
|
329
|
-
md.push(`- ${s.root}: ${s.names.length}개${missing.length ? `(빠짐: ${missing.join(", ")})` : "(
|
|
383
|
+
md.push(`- ${s.root}: ${s.names.length}개${missing.length ? `(빠짐: ${missing.join(", ")})` : "(여덟 개 모두)"}`);
|
|
330
384
|
}
|
|
331
385
|
}
|
|
332
386
|
else
|
|
@@ -334,11 +388,13 @@ export async function runDoctor(ctx, opts) {
|
|
|
334
388
|
md.push("");
|
|
335
389
|
const skillState = (s) => !s.names.length
|
|
336
390
|
? "없음"
|
|
337
|
-
: `${s.names.length}개${s.missing.length ? `(빠짐: ${s.missing.join(", ")})` : "(
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
391
|
+
: `${s.names.length}개${s.missing.length ? `(빠짐: ${s.missing.join(", ")})` : "(여덟 개 모두)"}`;
|
|
392
|
+
if (folderSkills.length) {
|
|
393
|
+
md.push("## 폴더 스킬(bind 가 넣는 자리)", "");
|
|
394
|
+
for (const s of folderSkills)
|
|
395
|
+
md.push(`- ${s.rel}(${s.clients.join(", ")}): ${skillState(s)}`);
|
|
396
|
+
md.push("");
|
|
397
|
+
}
|
|
342
398
|
if (enabled.checks.length || enabled.notes.length) {
|
|
343
399
|
md.push("## Claude 폴더 서버 켜기(enabledMcpjsonServers)", "");
|
|
344
400
|
for (const c of enabled.checks) {
|
|
@@ -360,12 +416,30 @@ export async function runDoctor(ctx, opts) {
|
|
|
360
416
|
for (const [e, p] of probes) {
|
|
361
417
|
if (typeof p === "string")
|
|
362
418
|
continue;
|
|
419
|
+
if (isSetupEntry(e)) {
|
|
420
|
+
// setup 이 쓰는 계정 전체 키는 Main 을 따라간다(binding main).
|
|
421
|
+
if (p.ok && p.binding !== "main") {
|
|
422
|
+
next.push(`${e.name}(${e.client} 전역): binding 이 ${p.binding} 입니다. 계정 전체 키라면 main 이 나와야 합니다. ` +
|
|
423
|
+
`npx aiakiv setup --client ${e.client} --new-key 로 다시 받으세요.`);
|
|
424
|
+
}
|
|
425
|
+
if (!p.ok) {
|
|
426
|
+
next.push(`${e.name}(${e.client} 전역): 키 연결이 실패했습니다. npx aiakiv setup --client ${e.client} 로 다시 받으세요.`);
|
|
427
|
+
}
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
363
430
|
if (p.ok && p.binding !== "api-key") {
|
|
364
431
|
next.push(`${e.name}: binding 이 ${p.binding} 입니다. 프로젝트 고정 키라면 api-key 가 나와야 합니다.`);
|
|
365
432
|
}
|
|
366
433
|
if (!p.ok)
|
|
367
434
|
next.push(`${e.name}: 키 연결이 실패했습니다. 키가 폐기됐으면 npx aiakiv bind 로 다시 받으세요.`);
|
|
368
435
|
}
|
|
436
|
+
// --client 로 setup 대상을 직접 고른 경우에만, 전역 연결이 없으면 설치 명령을 알린다(기본 전체 확인에서는
|
|
437
|
+
// 폴더만 쓰는 사람에게 소음이 된다).
|
|
438
|
+
for (const c of (opts.clients ?? []).filter(isSetupClient)) {
|
|
439
|
+
if (scan.entries.some((e) => e.scope === "전역" && e.client === c))
|
|
440
|
+
continue;
|
|
441
|
+
next.push(`${c} 전역 연결(${globalConfigPath(ctx, c)})이 없습니다. npx aiakiv setup --client ${c} 로 설치하세요.`);
|
|
442
|
+
}
|
|
369
443
|
// 폴더 스킬은 그 자리를 읽는 클라이언트가 이 폴더에 고정돼 있을 때만 다음 단계로 낸다.
|
|
370
444
|
for (const s of folderSkills) {
|
|
371
445
|
const bound = s.clients.filter((c) => scan.entries.some((e) => e.scope === "폴더" && e.client === c));
|
|
@@ -381,9 +455,12 @@ export async function runDoctor(ctx, opts) {
|
|
|
381
455
|
"npx aiakiv bind --client claude 를 다시 하세요.");
|
|
382
456
|
}
|
|
383
457
|
if (!scan.entries.length) {
|
|
384
|
-
|
|
458
|
+
if (clients.some(isFolderClient)) {
|
|
459
|
+
next.push("폴더에 프로젝트를 고정하려면 npx aiakiv bind --client <클라이언트> --project <이름>");
|
|
460
|
+
}
|
|
385
461
|
if (!skills.length) {
|
|
386
|
-
next.push("전역
|
|
462
|
+
next.push("전역 설치는 Gemini CLI 와 Antigravity 만 npx aiakiv setup --client gemini,antigravity 로 합니다. " +
|
|
463
|
+
"그 밖은 설치 프롬프트나 플러그인 마켓을 쓰세요.");
|
|
387
464
|
}
|
|
388
465
|
}
|
|
389
466
|
md.push("## 다음 단계", "");
|
|
@@ -404,7 +481,7 @@ export async function runDoctor(ctx, opts) {
|
|
|
404
481
|
ctx.out(" 설정 파일에 AiAkiv 연결 없음");
|
|
405
482
|
for (const e of scan.entries) {
|
|
406
483
|
const p = probes.get(e);
|
|
407
|
-
const tail = p === undefined ? "" : `, 확인 결과 ${typeof p === "string" ? p : probeLine(p)}`;
|
|
484
|
+
const tail = p === undefined ? "" : `, 확인 결과 ${typeof p === "string" ? p : probeLine(p, e)}`;
|
|
408
485
|
ctx.out(` [${e.scope} ${e.client}] ${e.name}: ${howLabel(e)}${tail}`);
|
|
409
486
|
}
|
|
410
487
|
for (const n of scan.notes)
|
|
@@ -412,7 +489,10 @@ export async function runDoctor(ctx, opts) {
|
|
|
412
489
|
for (const s of lists)
|
|
413
490
|
ctx.out(` ${s.title}: ${s.lines.join(" / ")}`);
|
|
414
491
|
ctx.out(` 스킬: ${skills.length ? skills.map((s) => `${s.root}(${s.names.length})`).join(", ") : "찾지 못함"}`);
|
|
415
|
-
|
|
492
|
+
if (folderSkills.length)
|
|
493
|
+
ctx.out(` 폴더 스킬: ${folderSkills.map((s) => `${s.rel} ${skillState(s)}`).join(", ")}`);
|
|
494
|
+
if (consoleKey)
|
|
495
|
+
ctx.out(` ${consoleKey}`);
|
|
416
496
|
for (const c of enabled.checks) {
|
|
417
497
|
ctx.out(` enabledMcpjsonServers ${c.name}: ${c.in.length ? c.in.join(", ") : "없음"}`);
|
|
418
498
|
}
|
package/dist/handoff.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
// 브라우저 인계로
|
|
1
|
+
// 브라우저 인계로 키 받기(BP-165 §4, BP-171 §4.2).
|
|
2
2
|
//
|
|
3
3
|
// 1. 127.0.0.1 의 빈 포트에 작은 HTTP 서버를 연다. state 는 무작위 24바이트.
|
|
4
|
-
// 2. 웹 콘솔의 인계
|
|
5
|
-
//
|
|
4
|
+
// 2. 웹 콘솔의 인계 화면을 연다. 모양은 셋이다.
|
|
5
|
+
// - 프로젝트 키(bind): `/account/cli/handoff?project&port&state&client[&expires_days]`
|
|
6
|
+
// - 계정 키(console key): `/account/cli/handoff?kind=account&port&state&client=cli[&expires_days]`.
|
|
7
|
+
// project 는 보내지 않는다(계정 키는 팀도 저장 좌표도 없다).
|
|
8
|
+
// - 계정 전체 기억 키(setup): `/account/cli/handoff?kind=dynamic&port&state&client=<클라이언트들>[&expires_days]`.
|
|
9
|
+
// Main 을 따라가는 키라 project 를 보내지 않는다(BP-165 P6).
|
|
10
|
+
// 3. 사람이 버튼을 누르면 콘솔이 `http://127.0.0.1:<port>/callback?code&state` 로 돌려보낸다.
|
|
6
11
|
// state 가 다르면 400 을 주고 계속 기다린다(엉뚱한 요청 하나로 흐름이 끊기지 않게).
|
|
7
12
|
// 4. `POST /api/cli/exchange {code, state, port}` 로 코드를 키로 바꾼다. 평문은 이 응답에만 있다.
|
|
8
13
|
//
|
|
@@ -15,7 +20,16 @@ export function newState() {
|
|
|
15
20
|
return randomBytes(24).toString("base64url");
|
|
16
21
|
}
|
|
17
22
|
export function handoffUrl(control, p) {
|
|
18
|
-
const q = new URLSearchParams(
|
|
23
|
+
const q = new URLSearchParams();
|
|
24
|
+
if (p.kind === "account" || p.kind === "dynamic")
|
|
25
|
+
q.set("kind", p.kind);
|
|
26
|
+
else {
|
|
27
|
+
if (!p.project)
|
|
28
|
+
throw new Error("프로젝트 키 인계에는 프로젝트 이름이 필요합니다.");
|
|
29
|
+
q.set("project", p.project);
|
|
30
|
+
}
|
|
31
|
+
q.set("port", String(p.port));
|
|
32
|
+
q.set("state", p.state);
|
|
19
33
|
if (p.expiresDays !== undefined)
|
|
20
34
|
q.set("expires_days", String(p.expiresDays));
|
|
21
35
|
q.set("client", p.client);
|
|
@@ -94,6 +108,10 @@ export function startLoopback(state, timeoutMs = HANDOFF_TIMEOUT_MS) {
|
|
|
94
108
|
});
|
|
95
109
|
});
|
|
96
110
|
}
|
|
111
|
+
/** 계정 키를 달라고 했는데 응답이 계정 키가 아닐 때(서버가 옛 판). */
|
|
112
|
+
export const ACCOUNT_KEY_UNSUPPORTED = "서버가 계정 키를 지원하지 않습니다. 웹 콘솔을 쓰세요.";
|
|
113
|
+
/** 계정 전체 기억 키를 달라고 했는데 응답이 그 키가 아닐 때(서버가 옛 판). */
|
|
114
|
+
export const DYNAMIC_KEY_UNSUPPORTED = "서버가 계정 전체 키를 아직 지원하지 않습니다. 받은 키는 쓰지 않았습니다. 웹 콘솔의 설치 안내를 쓰세요.";
|
|
97
115
|
export class HandoffError extends Error {
|
|
98
116
|
}
|
|
99
117
|
async function shortReason(res) {
|
|
@@ -114,7 +132,10 @@ async function shortReason(res) {
|
|
|
114
132
|
return "";
|
|
115
133
|
}
|
|
116
134
|
}
|
|
117
|
-
|
|
135
|
+
/** `expect`: 어떤 키를 받으려 했는가. "project" 면 응답에 project 가 있어야 하고, "account" 면
|
|
136
|
+
* 응답 kind 가 "account" 여야 한다(아니면 ACCOUNT_KEY_UNSUPPORTED, 받은 평문은 버린다).
|
|
137
|
+
* "dynamic" 이면 응답 kind 가 "dynamic" 이어야 한다(아니면 DYNAMIC_KEY_UNSUPPORTED). */
|
|
138
|
+
export async function exchangeCode(fetchImpl, control, body, expect = "project") {
|
|
118
139
|
let res;
|
|
119
140
|
try {
|
|
120
141
|
res = await fetchImpl(`${control}/api/cli/exchange`, {
|
|
@@ -147,10 +168,38 @@ export async function exchangeCode(fetchImpl, control, body) {
|
|
|
147
168
|
}
|
|
148
169
|
const o = j;
|
|
149
170
|
// 필드 확인. 오류 문구에 응답 값을 넣지 않는다(평문이 들어 있다).
|
|
150
|
-
|
|
171
|
+
const required = expect === "project" ? ["key_id", "plaintext", "project"] : ["key_id", "plaintext"];
|
|
172
|
+
for (const k of required) {
|
|
151
173
|
if (typeof o?.[k] !== "string" || !o[k]) {
|
|
152
174
|
throw new HandoffError(`키 교환 응답에 ${k} 가 없습니다.`);
|
|
153
175
|
}
|
|
154
176
|
}
|
|
177
|
+
if (expect === "account" && o.kind !== "account")
|
|
178
|
+
throw new HandoffError(ACCOUNT_KEY_UNSUPPORTED);
|
|
179
|
+
if (expect === "dynamic" && o.kind !== "dynamic")
|
|
180
|
+
throw new HandoffError(DYNAMIC_KEY_UNSUPPORTED);
|
|
155
181
|
return o;
|
|
156
182
|
}
|
|
183
|
+
/** 교환 응답의 `scopes`(서버는 "read,write" 나 "manage" 같은 글자, 옛 모양은 배열)를 목록으로. */
|
|
184
|
+
export function scopeList(scopes) {
|
|
185
|
+
const raw = Array.isArray(scopes) ? scopes : typeof scopes === "string" ? scopes.split(",") : [];
|
|
186
|
+
return raw.map((s) => String(s).trim()).filter(Boolean);
|
|
187
|
+
}
|
|
188
|
+
/** 앞선 키를 폐기했다는 안내. 0, 빈 목록, 숫자나 목록이 아니면 null(줄을 내지 않는다).
|
|
189
|
+
* 프로젝트 키(bind)는 같은 프로젝트의 키, 계정 키(console key)는 그 사람의 앞선 계정 키,
|
|
190
|
+
* 계정 전체 키(setup)는 그 사람의 앞선 계정 전체 키(다른 PC 것 포함)가 폐기된다.
|
|
191
|
+
* `n` 이 키 id 목록이면 개수와 함께 id 를 적는다(id 는 평문이 아니다). */
|
|
192
|
+
export function supersededNotice(n, kind = "project") {
|
|
193
|
+
const ids = Array.isArray(n) ? n.filter((x) => typeof x === "string" && !!x) : [];
|
|
194
|
+
const count = Array.isArray(n) ? n.length : typeof n === "number" ? n : 0;
|
|
195
|
+
if (!(count > 0))
|
|
196
|
+
return null;
|
|
197
|
+
const idText = ids.length ? `(키 id ${ids.join(", ")})` : "";
|
|
198
|
+
if (kind === "dynamic") {
|
|
199
|
+
return `앞선 계정 전체 키 ${count}개${idText}를 폐기했습니다. 다른 PC 에 넣어 둔 계정 전체 키도 더는 쓸 수 없으니, ` +
|
|
200
|
+
"그 PC 에서도 setup 을 다시 하세요.";
|
|
201
|
+
}
|
|
202
|
+
return kind === "account"
|
|
203
|
+
? `앞선 계정 키 ${count}개${idText}를 폐기했습니다.`
|
|
204
|
+
: `이전 키 ${count}개${idText}를 폐기했습니다. 다른 폴더에 같은 프로젝트 키를 넣어 두었다면 그 폴더도 다시 bind 하세요.`;
|
|
205
|
+
}
|
package/dist/main.js
CHANGED
|
@@ -1,37 +1,96 @@
|
|
|
1
1
|
// 인자 해석과 명령 고르기. 인자 해석은 Node 내장 util.parseArgs(외부 라이브러리 없음).
|
|
2
2
|
import { parseArgs } from "node:util";
|
|
3
|
+
import { GLOBAL_CLIENT_IDS, SETUP_ONLY_GLOBAL_CLIENTS } from "./clients/global.js";
|
|
3
4
|
import { CLIENT_IDS } from "./clients/types.js";
|
|
4
5
|
import { runBind } from "./bind.js";
|
|
5
|
-
import {
|
|
6
|
+
import { ConsoleUsageError, runConsole, runConsoleKey } from "./console.js";
|
|
7
|
+
import { runDashboard } from "./dashboard.js";
|
|
8
|
+
import { DEFAULT_REPORT, DOCTOR_CLIENT_IDS, runDoctor } from "./doctor.js";
|
|
9
|
+
import { runSetup } from "./setup.js";
|
|
6
10
|
import { DEFAULT_SKILLS_TARBALL_URL } from "./skills.js";
|
|
7
11
|
import { DEFAULT_CONTROL_URL, DEFAULT_MCP_URL } from "./urls.js";
|
|
8
12
|
import { VERSION } from "./version.js";
|
|
9
|
-
export const HELP = `aiakiv ${VERSION}: 폴더에 AiAkiv 프로젝트를 고정하고
|
|
13
|
+
export const HELP = `aiakiv ${VERSION}: 폴더에 AiAkiv 프로젝트를 고정하고 확인하고, 전역 설치와 콘솔 작업을 하는 명령
|
|
10
14
|
|
|
11
15
|
사용법:
|
|
12
16
|
npx aiakiv bind --client codex|claude|gemini|cursor --project <이름> [--dir <폴더>]
|
|
13
17
|
[--expires-days N] [--no-browser] [--no-skills]
|
|
18
|
+
npx aiakiv setup --client gemini|antigravity[,…] [--expires-days N] [--no-browser] [--no-skills] [--new-key]
|
|
14
19
|
npx aiakiv doctor [--client <클라이언트>[,…]] [--dir <폴더>] [--out ${DEFAULT_REPORT}] [--no-probe]
|
|
20
|
+
npx aiakiv console <액션> [--data '<json>' | --data @파일.json] [--json] [--dir <폴더>] [--yes]
|
|
21
|
+
npx aiakiv console describe [--json] [--dir <폴더>]
|
|
22
|
+
npx aiakiv console key [--expires-days N] [--dir <폴더>] [--no-browser]
|
|
23
|
+
npx aiakiv dashboard [--json] [--dir <폴더>]
|
|
15
24
|
|
|
16
25
|
bind 폴더에 프로젝트를 고정합니다. 웹 콘솔의 "폴더 ZIP 받기" 와 같은 한 벌을 그 폴더에 직접 씁니다.
|
|
17
|
-
claude, cursor 는 ?project= 주소(OAuth)를 쓰고, codex, gemini 는 브라우저에서 확인
|
|
26
|
+
claude, cursor 는 ?project= 주소(OAuth)를 쓰고, codex, gemini 는 브라우저에서 확인 버튼을 눌러
|
|
18
27
|
프로젝트 키를 받아 씁니다. 키 평문은 화면에 내지 않고 그 폴더의 설정 파일에만 씁니다.
|
|
19
|
-
함께 쓰는 것: 폴더 스킬
|
|
28
|
+
함께 쓰는 것: 폴더 스킬 여덟 개(claude 는 .claude/skills, 나머지는 .agents/skills,
|
|
20
29
|
--no-skills 면 건너뜀), claude 는 .claude/settings.json 의 enabledMcpjsonServers,
|
|
21
30
|
codex, gemini 는 키가 든 설정 파일을 .gitignore 에(git 저장소이거나 .gitignore 가 있을 때).
|
|
31
|
+
Antigravity 는 앱 전체에 설정 하나라 폴더 고정이 없습니다(setup 을 쓰세요).
|
|
32
|
+
setup Gemini CLI 와 Antigravity 의 전역 설치를 합니다. 브라우저에서 확인 버튼을 눌러 Main(저장 대상)을
|
|
33
|
+
따라가는 계정 전체 기억 키를 받고, 고른 클라이언트 모두의 전역 설정에 서버 이름 AiAkiv 로 씁니다.
|
|
34
|
+
Gemini CLI 는 ~/.gemini/settings.json(httpUrl), Antigravity 는 ~/.gemini/config/mcp_config.json
|
|
35
|
+
(serverUrl). 전역 스킬도 씁니다(Gemini CLI 는 ~/.gemini/skills, Antigravity 는 ~/.gemini/config/skills
|
|
36
|
+
와, ~/.gemini/antigravity-cli 가 있으면 그 아래 skills. --no-skills 면 건너뜀).
|
|
37
|
+
계정 전체 키는 한 사람에게 하나만 살아 있어, 새로 받으면 앞선 키(다른 PC 것 포함)는 폐기됩니다.
|
|
38
|
+
그래서 두 전역 파일에 쓸 수 있는 키(binding main)가 이미 있으면 브라우저를 열지 않고 그 키를
|
|
39
|
+
다시 씁니다. --new-key 면 새로 받습니다. --expires-days 가 없으면 만료가 없습니다.
|
|
22
40
|
doctor 설정 파일, 클라이언트 등록 목록, 스킬 위치를 확인하고 결과를 마크다운 파일로 씁니다.
|
|
23
41
|
키로 묶인 연결은 get_save_target 을 직접 불러 팀, 프로젝트, binding 을 적습니다.
|
|
42
|
+
--client 는 bind 의 넷과 antigravity 를 받습니다(기본은 모두). setup 이 쓴 전역 키는 binding 이
|
|
43
|
+
main 이어야 합니다.
|
|
44
|
+
console 웹 콘솔의 작업(팀, 초대, 프로젝트, Main 전환, 연결, 카드 등)을 실행합니다. 액션 이름과
|
|
45
|
+
인자는 console describe 가 보여 줍니다. 별칭: teams, projects, links, permissions.
|
|
46
|
+
읽기는 bind 가 받은 프로젝트 키(그 키의 팀만)나 계정 키로 됩니다. 쓰기는 계정 키와, 콘솔
|
|
47
|
+
"AI 권한" 에서 켠 권한이 둘 다 있어야 합니다. 접근 범위를 넓히는 쓰기는 실행 전에 한 번 묻고,
|
|
48
|
+
터미널이 아니면 --yes 가 있어야 실행합니다. --json 은 서버 응답을 그대로 냅니다(오류 응답도).
|
|
49
|
+
console key
|
|
50
|
+
계정 관리 키를 브라우저 확인으로 받아 .aiakiv/console.json 에 씁니다. 이 키로는 기억을
|
|
51
|
+
읽거나 쓸 수 없고 관리 작업만 합니다. 기본 만료 90일. 프로젝트나 팀을 고르지 않고, 속한
|
|
52
|
+
모든 팀을 다룹니다(.gitignore 에 /.aiakiv/ 를 더합니다). 다시 받으면 앞선 계정 키는 폐기됩니다.
|
|
53
|
+
dashboard
|
|
54
|
+
팀, 프로젝트, 저장 대상, 켜진 권한, 연결, 카드, 최근 저장 대상 이력을 한 화면에 보입니다.
|
|
55
|
+
한 칸이 실패해도 나머지는 그립니다. 계정 키로는 저장 대상을 읽지 않고, 그 폴더에 프로젝트
|
|
56
|
+
키가 있으면 그 키로 확인합니다.
|
|
24
57
|
|
|
25
|
-
|
|
58
|
+
console 과 dashboard 가 키를 찾는 순서: 환경 변수 AIAKIV_API_KEY, <폴더>/.aiakiv/console.json(계정 키),
|
|
59
|
+
<폴더>의 .codex/config.toml 과 .gemini/settings.json(프로젝트 키). 키 평문은 화면에 내지 않고 키 id 와
|
|
60
|
+
종류만 보입니다.
|
|
61
|
+
|
|
62
|
+
console 과 dashboard 의 종료 코드:
|
|
63
|
+
0 성공
|
|
64
|
+
1 서버 오류(500, 연결 실패, 읽을 수 없는 응답)
|
|
65
|
+
2 사용 오류(인자, --yes 없음, 키 없음)
|
|
66
|
+
3 인증 실패(401: 키가 틀렸거나 폐기, 만료됨)
|
|
67
|
+
4 거절(403: scope_required, static_key_required, account_pending_deletion, forbidden).
|
|
68
|
+
scope_required 는 프로젝트 키로 관리 작업을 한 경우이고, 계정 키를 console key 로 받습니다
|
|
69
|
+
5 속도 제한(429)
|
|
70
|
+
6 권한 꺼짐(console_permission_disabled, 콘솔 "AI 권한" 에서 켭니다)
|
|
71
|
+
7 서버에서 CLI 콘솔 입구가 아직 열리지 않음(404)
|
|
72
|
+
8 그 밖의 오류 응답(unknown_action, invalid_data 등)
|
|
73
|
+
|
|
74
|
+
전역 설치(계정 연결과 전역 스킬)는 Gemini CLI 와 Antigravity 만 setup 으로 합니다. 그 밖은 Claude Code 는
|
|
75
|
+
플러그인 마켓, ChatGPT 와 Codex 는 설치 프롬프트, Cursor 는 홈페이지의 설치 링크를 쓰세요.
|
|
26
76
|
|
|
27
77
|
환경 변수:
|
|
28
78
|
AIAKIV_MCP_URL MCP 주소(기본 ${DEFAULT_MCP_URL})
|
|
29
79
|
AIAKIV_CONTROL_URL 웹 콘솔 주소(기본 ${DEFAULT_CONTROL_URL})
|
|
30
|
-
|
|
80
|
+
AIAKIV_API_KEY console, dashboard 가 먼저 쓰는 키
|
|
81
|
+
AIAKIV_SKILLS_TARBALL_URL 폴더 스킬과 전역 스킬을 받는 주소(기본 ${DEFAULT_SKILLS_TARBALL_URL})
|
|
31
82
|
|
|
32
83
|
공통: -h, --help -v, --version`;
|
|
33
84
|
class UsageError extends Error {
|
|
34
85
|
}
|
|
86
|
+
function parseExpiresDays(v) {
|
|
87
|
+
if (v === undefined)
|
|
88
|
+
return undefined;
|
|
89
|
+
const n = Number(v);
|
|
90
|
+
if (!Number.isInteger(n) || n < 1)
|
|
91
|
+
throw new UsageError("--expires-days 는 1 이상의 정수입니다.");
|
|
92
|
+
return n;
|
|
93
|
+
}
|
|
35
94
|
function parseClient(v, required) {
|
|
36
95
|
if (v === undefined) {
|
|
37
96
|
if (required)
|
|
@@ -43,6 +102,39 @@ function parseClient(v, required) {
|
|
|
43
102
|
}
|
|
44
103
|
return v;
|
|
45
104
|
}
|
|
105
|
+
/** `--client a,b --client c` 를 이름 목록으로(중복은 한 번만). */
|
|
106
|
+
function clientList(values) {
|
|
107
|
+
const out = [];
|
|
108
|
+
for (const c of (values ?? []).flatMap((v) => v.split(",")).map((v) => v.trim()).filter(Boolean)) {
|
|
109
|
+
if (!out.includes(c))
|
|
110
|
+
out.push(c);
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
/** setup 의 --client. gemini, antigravity 만 받는다. 다른 클라이언트면 갈 곳을 알린다. */
|
|
115
|
+
function parseSetupClients(values) {
|
|
116
|
+
const names = clientList(values);
|
|
117
|
+
if (!names.length)
|
|
118
|
+
throw new UsageError(`--client 가 필요합니다(${GLOBAL_CLIENT_IDS.join(", ")} 중에서, 쉼표로 여럿).`);
|
|
119
|
+
for (const n of names) {
|
|
120
|
+
if (GLOBAL_CLIENT_IDS.includes(n))
|
|
121
|
+
continue;
|
|
122
|
+
if (CLIENT_IDS.includes(n) || n === "chatgpt" || n === "grok") {
|
|
123
|
+
throw new UsageError(`${SETUP_ONLY_GLOBAL_CLIENTS} (받은 값: ${n})`);
|
|
124
|
+
}
|
|
125
|
+
throw new UsageError(`--client 는 ${GLOBAL_CLIENT_IDS.join(", ")} 중에서 고릅니다(받은 값: ${n}).`);
|
|
126
|
+
}
|
|
127
|
+
return names;
|
|
128
|
+
}
|
|
129
|
+
/** doctor 의 --client. bind 의 넷과 antigravity. */
|
|
130
|
+
function parseDoctorClients(values) {
|
|
131
|
+
return clientList(values).map((c) => {
|
|
132
|
+
if (!DOCTOR_CLIENT_IDS.includes(c)) {
|
|
133
|
+
throw new UsageError(`--client 는 ${DOCTOR_CLIENT_IDS.join(", ")} 중에서 고릅니다(받은 값: ${c}).`);
|
|
134
|
+
}
|
|
135
|
+
return c;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
46
138
|
export async function main(argv, ctx) {
|
|
47
139
|
const [cmd, ...rest] = argv;
|
|
48
140
|
try {
|
|
@@ -55,11 +147,38 @@ export async function main(argv, ctx) {
|
|
|
55
147
|
return 0;
|
|
56
148
|
}
|
|
57
149
|
if (cmd === "setup") {
|
|
58
|
-
// 첫 판(P2 v1)
|
|
59
|
-
|
|
60
|
-
|
|
150
|
+
// 전역 설치. Gemini CLI 와 Antigravity 만(BP-165 P6). 첫 판(P2 v1)의 setup 과는 다른 명령이다.
|
|
151
|
+
const { values, positionals } = parseArgs({
|
|
152
|
+
args: rest,
|
|
153
|
+
options: {
|
|
154
|
+
client: { type: "string", multiple: true },
|
|
155
|
+
"expires-days": { type: "string" },
|
|
156
|
+
"no-browser": { type: "boolean", default: false },
|
|
157
|
+
"no-skills": { type: "boolean", default: false },
|
|
158
|
+
"new-key": { type: "boolean", default: false },
|
|
159
|
+
help: { type: "boolean", short: "h" },
|
|
160
|
+
},
|
|
161
|
+
strict: true,
|
|
162
|
+
allowPositionals: true,
|
|
163
|
+
});
|
|
164
|
+
if (values.help)
|
|
165
|
+
return (ctx.out(HELP), 0);
|
|
166
|
+
if (positionals.length)
|
|
167
|
+
throw new UsageError(`setup 은 위치 인자를 받지 않습니다: ${positionals.join(" ")}`);
|
|
168
|
+
const clients = parseSetupClients(values.client);
|
|
169
|
+
return await runSetup(ctx, {
|
|
170
|
+
clients,
|
|
171
|
+
expiresDays: parseExpiresDays(values["expires-days"]),
|
|
172
|
+
noBrowser: values["no-browser"],
|
|
173
|
+
skills: !values["no-skills"],
|
|
174
|
+
newKey: values["new-key"],
|
|
175
|
+
});
|
|
61
176
|
}
|
|
62
177
|
if (cmd === "bind") {
|
|
178
|
+
if (rest.some((a) => /^--manage(=|$)/.test(a))) {
|
|
179
|
+
// R0(P2) 에 있던 플래그. 옛 안내를 따라 친 사람에게 갈 곳을 알린다(BP-171 §10 결정 10).
|
|
180
|
+
throw new UsageError("bind --manage 는 없어졌습니다. 관리 작업용 계정 키는 npx aiakiv console key 로 받습니다.");
|
|
181
|
+
}
|
|
63
182
|
const { values } = parseArgs({
|
|
64
183
|
args: rest,
|
|
65
184
|
options: {
|
|
@@ -75,18 +194,16 @@ export async function main(argv, ctx) {
|
|
|
75
194
|
});
|
|
76
195
|
if (values.help)
|
|
77
196
|
return (ctx.out(HELP), 0);
|
|
197
|
+
if (values.client === "antigravity") {
|
|
198
|
+
throw new UsageError("Antigravity 는 앱 전체에 설정 하나라 폴더 고정이 없습니다. npx aiakiv setup --client antigravity 를 쓰세요.");
|
|
199
|
+
}
|
|
78
200
|
const client = parseClient(values.client, true);
|
|
79
201
|
if (!values.project?.trim())
|
|
80
202
|
throw new UsageError("--project <이름> 이 필요합니다.");
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
throw new UsageError("--expires-days 는 1 이상의 정수입니다.");
|
|
86
|
-
}
|
|
87
|
-
if (client === "claude" || client === "cursor") {
|
|
88
|
-
ctx.err(`참고: ${client} 는 키를 받지 않으므로 --expires-days 는 쓰이지 않습니다.`);
|
|
89
|
-
}
|
|
203
|
+
const expiresDays = parseExpiresDays(values["expires-days"]);
|
|
204
|
+
const oauth = client === "claude" || client === "cursor";
|
|
205
|
+
if (oauth && expiresDays !== undefined) {
|
|
206
|
+
ctx.err(`참고: ${client} 는 키를 받지 않으므로 --expires-days 는 쓰이지 않습니다.`);
|
|
90
207
|
}
|
|
91
208
|
return await runBind(ctx, {
|
|
92
209
|
client,
|
|
@@ -111,18 +228,78 @@ export async function main(argv, ctx) {
|
|
|
111
228
|
});
|
|
112
229
|
if (values.help)
|
|
113
230
|
return (ctx.out(HELP), 0);
|
|
114
|
-
const clients = (values.client
|
|
115
|
-
.flatMap((c) => c.split(","))
|
|
116
|
-
.map((c) => c.trim())
|
|
117
|
-
.filter(Boolean)
|
|
118
|
-
.map((c) => parseClient(c, true));
|
|
231
|
+
const clients = parseDoctorClients(values.client);
|
|
119
232
|
return await runDoctor(ctx, { clients, dir: values.dir, out: values.out, noProbe: values["no-probe"] });
|
|
120
233
|
}
|
|
121
|
-
|
|
234
|
+
if (cmd === "console" && rest[0] === "key") {
|
|
235
|
+
const dropped = rest.slice(1).map((a) => /^--(project|manage|team)(=|$)/.exec(a)?.[1]).find(Boolean);
|
|
236
|
+
if (dropped) {
|
|
237
|
+
throw new UsageError(`console key 는 --${dropped} 를 받지 않습니다. 계정 키는 프로젝트나 팀 없이 받습니다.`);
|
|
238
|
+
}
|
|
239
|
+
const { values, positionals } = parseArgs({
|
|
240
|
+
args: rest.slice(1),
|
|
241
|
+
options: {
|
|
242
|
+
"expires-days": { type: "string" },
|
|
243
|
+
dir: { type: "string" },
|
|
244
|
+
"no-browser": { type: "boolean", default: false },
|
|
245
|
+
help: { type: "boolean", short: "h" },
|
|
246
|
+
},
|
|
247
|
+
strict: true,
|
|
248
|
+
allowPositionals: true,
|
|
249
|
+
});
|
|
250
|
+
if (values.help)
|
|
251
|
+
return (ctx.out(HELP), 0);
|
|
252
|
+
if (positionals.length)
|
|
253
|
+
throw new UsageError(`console key 는 위치 인자를 받지 않습니다: ${positionals.join(" ")}`);
|
|
254
|
+
return await runConsoleKey(ctx, {
|
|
255
|
+
expiresDays: parseExpiresDays(values["expires-days"]),
|
|
256
|
+
dir: values.dir,
|
|
257
|
+
noBrowser: values["no-browser"],
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
if (cmd === "console") {
|
|
261
|
+
const { values, positionals } = parseArgs({
|
|
262
|
+
args: rest,
|
|
263
|
+
options: {
|
|
264
|
+
data: { type: "string" },
|
|
265
|
+
json: { type: "boolean", default: false },
|
|
266
|
+
dir: { type: "string" },
|
|
267
|
+
yes: { type: "boolean", default: false },
|
|
268
|
+
help: { type: "boolean", short: "h" },
|
|
269
|
+
},
|
|
270
|
+
strict: true,
|
|
271
|
+
allowPositionals: true,
|
|
272
|
+
});
|
|
273
|
+
if (values.help)
|
|
274
|
+
return (ctx.out(HELP), 0);
|
|
275
|
+
if (positionals.length !== 1) {
|
|
276
|
+
throw new UsageError(positionals.length
|
|
277
|
+
? `액션은 하나만 줍니다(받은 값: ${positionals.join(" ")}).`
|
|
278
|
+
: "액션 이름이 필요합니다. 목록은 npx aiakiv console describe 로 봅니다.");
|
|
279
|
+
}
|
|
280
|
+
return await runConsole(ctx, {
|
|
281
|
+
action: positionals[0], data: values.data, json: values.json, dir: values.dir, yes: values.yes,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
if (cmd === "dashboard") {
|
|
285
|
+
const { values } = parseArgs({
|
|
286
|
+
args: rest,
|
|
287
|
+
options: {
|
|
288
|
+
json: { type: "boolean", default: false },
|
|
289
|
+
dir: { type: "string" },
|
|
290
|
+
help: { type: "boolean", short: "h" },
|
|
291
|
+
},
|
|
292
|
+
strict: true,
|
|
293
|
+
});
|
|
294
|
+
if (values.help)
|
|
295
|
+
return (ctx.out(HELP), 0);
|
|
296
|
+
return await runDashboard(ctx, { json: values.json, dir: values.dir });
|
|
297
|
+
}
|
|
298
|
+
throw new UsageError(`모르는 명령입니다: ${cmd}. bind, setup, doctor, console, dashboard 중 하나를 쓰세요(도움말: --help).`);
|
|
122
299
|
}
|
|
123
300
|
catch (e) {
|
|
124
301
|
const err = e;
|
|
125
|
-
if (e instanceof UsageError || (err.code ?? "").startsWith("ERR_PARSE_ARGS")) {
|
|
302
|
+
if (e instanceof UsageError || e instanceof ConsoleUsageError || (err.code ?? "").startsWith("ERR_PARSE_ARGS")) {
|
|
126
303
|
ctx.err(err.message);
|
|
127
304
|
ctx.err("도움말: npx aiakiv --help");
|
|
128
305
|
return 2;
|