aiakiv 0.2.1 → 0.3.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 +106 -0
- package/dist/ctx.js +2 -1
- package/dist/dashboard.js +59 -4
- package/dist/enterprise/precheck.js +656 -0
- package/dist/enterprise/reportMarkdown.js +188 -0
- package/dist/local/checks.js +529 -0
- package/dist/local/doctor.js +355 -0
- package/dist/local/portable.js +495 -0
- package/dist/main.js +227 -2
- package/dist/proc.js +51 -0
- package/dist/snippet.js +5 -9
- package/dist/version.js +5 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,6 +7,10 @@ Gemini CLI 와 Antigravity 는 전역 설치도 합니다. 설계는 K2G `docs/2
|
|
|
7
7
|
npx aiakiv bind --client codex|claude|gemini|cursor --project <이름> [--dir <폴더>] [--expires-days N] [--no-browser] [--no-skills]
|
|
8
8
|
npx aiakiv setup --client gemini|antigravity[,...] [--expires-days N] [--no-browser] [--no-skills] [--new-key]
|
|
9
9
|
npx aiakiv doctor [--client <클라이언트>[,...]] [--dir <폴더>] [--out aiakiv-verification-test.md] [--no-probe]
|
|
10
|
+
npx aiakiv local export <팀> --to <파일|폴더> [--domain <이름>] [--yes] [--container <이름>]
|
|
11
|
+
npx aiakiv local import <파일|폴더> --team <팀> [--as <사용자>] [--domain <이름>] [--dry-run] [--resume] [--yes] [--container <이름>]
|
|
12
|
+
npx aiakiv enterprise precheck [--config <toml>] [--image <참조> | --image-tar <파일>] [--device] [--no-browser] [--no-login]
|
|
13
|
+
[--only <영역,...>] [--skip <id 접두어,...>] [--redact-claims] [--out <폴더>] [--json]
|
|
10
14
|
```
|
|
11
15
|
|
|
12
16
|
전역 설치(계정 연결과 전역 스킬)는 Gemini CLI 와 Antigravity 만 `setup` 으로 합니다. 그 밖은 Claude Code 는
|
|
@@ -172,6 +176,107 @@ CLI 는 폐기된 개수와 키 id 를 알립니다. 그래서 쓸 수 있는
|
|
|
172
176
|
`--no-probe` 면 부르지 않습니다. OAuth 항목은 "클라이언트 안에서 get_save_target 을 부르라" 는 다음 단계를 적습니다.
|
|
173
177
|
- 결과는 `aiakiv-verification-test.md`(기본은 그 폴더)에 씁니다. 키로 묶인 연결이 실패하면 종료 코드 1 입니다.
|
|
174
178
|
|
|
179
|
+
## local export, local import: 자체 설치판 팀 기억 내보내기와 받기
|
|
180
|
+
|
|
181
|
+
자체 설치판(무료 5인 엔진) 팀의 기억을 `.mweft.tar.gz` 파일로 내보내고, 그 파일(또는 호스티드에서 내보낸 파일)을 팀에
|
|
182
|
+
가져옵니다. 본체는 엔진 컨테이너 안의 `aiakiv-admin export`, `aiakiv-admin import` 이고, 이 명령은 지금까지 손으로 하던
|
|
183
|
+
`docker compose exec engine aiakiv-admin ...` 와 `docker compose cp` 를 대신합니다. 설계는 K2G
|
|
184
|
+
`docs/211_blue_print-172-free_engine_5_users.md` §5.1, §5.2, §7, 엔진 명령의 동작은 K2G `packaging/selfhost/aiakiv-admin.md`
|
|
185
|
+
§5.10, §5.11 에 있습니다.
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
npx aiakiv local export 연구팀 --to ./연구팀.mweft.tar.gz
|
|
189
|
+
npx aiakiv local import ./연구팀.mweft.tar.gz --team 새팀 --dry-run # 계획만(종료 코드 0)
|
|
190
|
+
npx aiakiv local import ./연구팀.mweft.tar.gz --team 새팀 # --yes 가 없어 계획만(종료 코드 2)
|
|
191
|
+
npx aiakiv local import ./연구팀.mweft.tar.gz --team 새팀 --yes # 줄마다 진행, 마지막 줄이 결과
|
|
192
|
+
npx aiakiv local import ./연구팀.mweft.tar.gz --team 새팀 --resume # 재임베딩이 끊겼을 때
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
1. **엔진 컨테이너 찾기**: `docker info` 로 Docker 를 보고, `docker ps -a` 에서 엔진 이미지(`aiakiv/engine`,
|
|
196
|
+
`aiakiv/engine-enterprise`)로 도는 compose 서비스 `engine`(또는 compose 밖에서 띄운 것)을 찾습니다. 같은 이미지로 도는
|
|
197
|
+
유료 기업판 `control` 은 고르지 않습니다. 떠 있는 엔진이 여럿이면 종료 코드 2 로 멈추고, `--container <이름>` 으로
|
|
198
|
+
고릅니다(이름이나 id 앞부분, 이때는 이미지를 따지지 않습니다).
|
|
199
|
+
2. **경로**: `--to` 와 import 의 `<파일>` 은 이 컴퓨터의 경로이고 지금 폴더를 기준으로 풉니다(Windows 의 드라이브 문자,
|
|
200
|
+
역슬래시, 공백 그대로). 엔진에는 이 명령이 컨테이너 안에 만든 임시 폴더(`/data/exports/aiakiv-cli-<시각>-<무작위>`,
|
|
201
|
+
`/data/imports/aiakiv-cli-<시각>-<무작위>`)의 경로만 넘깁니다. `docker cp` 에는 호스트 쪽을 늘 절대 경로로 줍니다.
|
|
202
|
+
3. **export**: 임시 폴더에 `aiakiv-admin export <팀> --to <임시 폴더> --yes --json [--domain]` 을 돌리고(새로 만든 빈
|
|
203
|
+
폴더라 엔진에는 `--yes` 를 줍니다), 결과를 `docker cp` 로 꺼냅니다. `--to` 가 이미 있는 폴더이거나 끝이 `/`(Windows 는
|
|
204
|
+
`\` 도)면 그 안에 엔진이 지은 이름(`<팀>_<도메인>_<시각>.mweft.tar.gz`)으로 둡니다. 이미 있는 파일은 `--yes` 가 있어야
|
|
205
|
+
덮어씁니다(이 검사는 Docker 를 부르기 전에 합니다). 팀의 도메인이 여럿이면 엔진이 묶음(아카이브 여럿과
|
|
206
|
+
`org_manifest.json`)을 내고, 그 내용을 `--to` 폴더에 꺼냅니다(`--to` 가 파일이면 종료 코드 2).
|
|
207
|
+
4. **import**: 호스트 파일(또는 묶음 폴더)을 임시 폴더로 `docker cp` 하고 `aiakiv-admin import <컨테이너 경로> --team <팀>
|
|
208
|
+
[--as] [--domain] [--dry-run] [--resume] [--yes] --json` 을 부릅니다. 묻지 않습니다. `--yes` 가 없으면 엔진이 계획만 내고
|
|
209
|
+
종료 코드 2 로 끝납니다(같은 명령에 `--yes` 를 붙여 다시). `--resume` 은 끊긴 재임베딩을 이어서 하며 같은 호스트 파일을
|
|
210
|
+
다시 옮겨 줍니다(엔진은 파일 이름이 아니라 내용 지문으로 가져오기 기록을 찾습니다).
|
|
211
|
+
5. **임시 폴더 지우기**: 끝나면 컨테이너 안 임시 폴더를 지웁니다(`docker exec -u 0 ... rm -rf`, `docker cp` 로 넣은 파일이
|
|
212
|
+
root 소유라서). 엔진은 `--to` 를 생략한 내보내기를 데이터 볼륨에 남겨 두지만, 이 명령의 임시 폴더는 옮기려고 만든
|
|
213
|
+
것이라 남기면 데이터 볼륨만 찹니다(벡터가 실려 큽니다). 예외로 export 가 호스트로 꺼내지 못하면 파일을 남기고 손으로
|
|
214
|
+
꺼내는 `docker cp` 명령을 알립니다. 명령을 중간에 끊으면(Ctrl+C) 임시 폴더가 남을 수 있습니다.
|
|
215
|
+
6. **출력**: 표준 출력에는 JSON 만 냅니다(`--json` 은 붙여도 같습니다). export 는 한 문서
|
|
216
|
+
`{"ok":true,"command":"local export","kind":"archive|bundle","path":<호스트 경로>,"container":{"name","path","removed"},"engine":<엔진의 결과 그대로>}`.
|
|
217
|
+
import 는 이 명령의 줄 `{"step":"cli.copy","done":1,"total":1,"container":…,"path":<컨테이너 경로>}` 뒤에 엔진의 줄(진행
|
|
218
|
+
`{"step","done","total"}`, 마지막 줄이 결과)을 받는 대로 바꾸지 않고 냅니다. 엔진의 실패 JSON(이유, 남은 수, record id)도
|
|
219
|
+
그대로입니다. 이 명령 자신의 실패는 `{"ok":false,"error":…,"message":…,"fix":…}` 한 문서이고, 같은 문장을 표준 오류에도
|
|
220
|
+
냅니다. 엔진의 로그와 JSON 이 아닌 줄은 표준 오류로 냅니다.
|
|
221
|
+
7. **벡터**: 이 명령은 파일 내용을 읽거나 바꾸지 않습니다. 벡터를 싣고 버리고 다시 만드는 일은 엔진이 합니다(호스티드에서
|
|
222
|
+
내보낸 파일은 벡터가 없어 가져올 때 엔진이 다시 임베딩합니다).
|
|
223
|
+
|
|
224
|
+
종료 코드: 엔진의 것을 그대로 돌려줍니다(0 성공, 2 입력 오류나 `--yes` 없이 계획만, 3 거절, 4 엔진의 DB 연결 실패,
|
|
225
|
+
5 가져오기 검증 실패, 1 예기치 못한 오류). 이 명령의 것은 enterprise precheck 와 같은 번호로 2 인자 오류, 파일 없음, 이미
|
|
226
|
+
있는 파일, 엔진이 여럿, 4 Docker 를 쓸 수 없음(실행 파일 없음, 데몬에 닿지 못함)이나 엔진 컨테이너가 떠 있지 않음,
|
|
227
|
+
1 `docker cp` 나 `docker exec` 실패, 엔진이 결과 없이 끝남입니다. 오류의 종류는 JSON 의 `error` 로 가릅니다.
|
|
228
|
+
|
|
229
|
+
## enterprise precheck: 유료 기업판 설치 전 점검
|
|
230
|
+
|
|
231
|
+
유료 기업판을 깔기 전에 회사의 신원 서버(OIDC), 데이터베이스, 파일 저장소, 메일 서버에 실제로 연결해 보고 보고서를
|
|
232
|
+
냅니다. 점검 본체는 유료 기업판 엔진 이미지 안의 `aiakiv-admin precheck` 이고, 이 명령은 Docker 로 그것을 돌립니다.
|
|
233
|
+
설계는 K2G `docs/212_blue_print-173-enterprise_precheck_login.md` §3, 점검의 동작은 K2G
|
|
234
|
+
`packaging/enterprise/precheck/README.md` §2 에 있습니다. 라이선스 없이 돕니다.
|
|
235
|
+
|
|
236
|
+
```
|
|
237
|
+
npx aiakiv enterprise precheck --config ./aiakiv-enterprise.toml
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
1. **설정 파일**: `--config`(기본 `./aiakiv-enterprise.toml`). 없으면 종료 코드 2 와 함께 예시 파일
|
|
241
|
+
`aiakiv-enterprise.example.toml` 을 복사해 고치라고 안내합니다. 설정 파일이 있는 폴더가 작업 폴더이고 컨테이너의
|
|
242
|
+
`/config` 로 붙습니다(설정의 `ca_bundle = "/config/corp-ca.pem"` 은 그 폴더의 `corp-ca.pem` 입니다). 같은 폴더의
|
|
243
|
+
`.env` 가 있으면 compose 의 `env_file` 로 넘깁니다. 비밀 값은 설정에서 `env:이름` 으로 가리킵니다. 이 명령은 `.env`
|
|
244
|
+
를 열어 읽지 않습니다.
|
|
245
|
+
2. **Docker**: docker 실행 파일, 데몬, compose v2, 리눅스 컨테이너 모드를 봅니다. 안 되면 종료 코드 4 와 설치나 켜는 법.
|
|
246
|
+
3. **이미지**(이 순서):
|
|
247
|
+
- `--image <참조>`: 이 컴퓨터에 이미 있는 이미지(`docker image inspect`). 없으면 종료 코드 5.
|
|
248
|
+
- `--image-tar <파일>`: 내용 지문(sha256)을 계산해 같은 폴더의 `<파일>.sha256`(있으면, `<지문>` 이나
|
|
249
|
+
`<지문> <파일 이름>` 형식)과 비교합니다. 다르면 종료 코드 5. 맞으면 `docker load` 하고 그 출력의 이미지 이름을 씁니다.
|
|
250
|
+
- 둘 다 없으면 이 CLI 판에 고정한 `aiakiv/engine-enterprise:<판>`(지금 `0.1.0`, `src/version.ts` 의
|
|
251
|
+
`ENTERPRISE_ENGINE_VERSION`)이 있으면 그것을 씁니다. 없으면 `https://www.aiakiv.com/download/enterprise/<판>/manifest.json`
|
|
252
|
+
을 받아 Docker 의 아키텍처(amd64, arm64)에 맞는 tar 를 `~/.aiakiv/cache/` 에 내려받고 지문을 확인한 뒤 `docker load`
|
|
253
|
+
합니다. 목록에 닿지 못하면 종료 코드 5 와 "홈페이지(https://www.aiakiv.com/enterprise)에서 점검용 이미지를 내려받아
|
|
254
|
+
--image-tar 로 넣으세요" 를 냅니다.
|
|
255
|
+
4. **compose 파일**: `<작업 폴더>/.aiakiv-precheck/compose.yml` 을 돌릴 때마다 다시 씁니다. K2G `precheck-compose.yml` 의
|
|
256
|
+
`precheck` 서비스와 같은 내용(entrypoint `aiakiv-admin precheck`, healthcheck 끔, 포트 `127.0.0.1:7777:7777`)이고,
|
|
257
|
+
이미지는 3에서 정한 참조로 고정하며 받으러 나가지 않습니다(`pull_policy: never`). 볼륨은 Windows 드라이브 문자와
|
|
258
|
+
헷갈리지 않게 긴 형식(`type: bind`, `source`, `target: /config`)으로 적고, Windows 경로의 역슬래시는 `/` 로 바꿉니다.
|
|
259
|
+
동봉 Keycloak 서비스는 넣지 않습니다.
|
|
260
|
+
5. **실행**: `docker compose -p aiakiv-precheck -f <그 파일> run --rm --service-ports -T precheck --config /config/<설정 파일>
|
|
261
|
+
--json` 에 `--device`, `--no-login`, `--only`, `--skip`, `--redact-claims` 를 그대로 붙입니다.
|
|
262
|
+
6. **로그인**: 점검이 로그인 대기 줄을 내면 로그인 주소(기기 코드 방식이면 주소와 코드)를 눈에 띄게 보이고 브라우저를
|
|
263
|
+
엽니다. `--device` 나 `--no-browser` 면 열지 않고 주소만 보입니다. 비밀번호는 다루지 않습니다. 점검은 로그인을 600초
|
|
264
|
+
기다립니다(점검 본체의 기본값).
|
|
265
|
+
7. **보고서**: 점검의 마지막 줄(보고서 JSON)을 받아 이 CLI 가 `<작업 폴더>/<--out>/` 에 `report.json` 과 `report.md` 를
|
|
266
|
+
씁니다(`--out` 기본 `report`, 작업 폴더 안의 상대 경로만, 밖이면 종료 코드 2). 컨테이너에는 `--out` 을 넘기지 않습니다.
|
|
267
|
+
Linux 에서 점검 컨테이너(사용자 10001)가 붙인 폴더에 쓰지 못하는 문제를 피하려는 것이고, 두 파일의 이름과 형식은 점검
|
|
268
|
+
본체의 `--out` 과 같습니다. `report.md` 의 말은 이 CLI 의 환경 변수 `AIAKIV_LANGUAGE`, 설정 파일 맨 위의 `language`,
|
|
269
|
+
기본 `en` 순서로 정합니다.
|
|
270
|
+
8. **화면**: 기본은 점검마다 한 줄(상태, id, 걸린 시간, 내용), 끝에 결과, 고칠 것(실패와 주의의 `fix`), 견적과 발급 조건
|
|
271
|
+
(`quote.eligible`), 보고서 파일 위치입니다. JSON 이 아닌 줄은 stderr 로 그대로 냅니다. `--json` 이면 이 명령의 줄
|
|
272
|
+
(`{"step":"cli.docker",…}`, `{"step":"cli.image","status":…,"image":…,"source":"flag|tar|local|download"}`) 뒤에 점검의
|
|
273
|
+
줄을 바꾸지 않고 내고, 마지막 줄이 점검의 보고서입니다. JSON 이 아닌 줄은 버리고, 로그인 안내는 stderr 로 냅니다.
|
|
274
|
+
|
|
275
|
+
종료 코드: 점검의 것(0 실패 없음, 3 실패 있음, 2 설정 오류)을 그대로 돌려줍니다. 이 명령의 것은 2 인자 오류(설정 파일
|
|
276
|
+
없음, `--out` 이 작업 폴더 밖, `--image` 와 `--image-tar` 를 함께 줌), 4 Docker 를 쓸 수 없음(실행 파일 없음, 데몬에 닿지
|
|
277
|
+
못함, compose v2 없음, Windows 컨테이너 모드), 5 이미지를 구하지 못함, 1 점검이 보고서 없이 끝남(포트 7777 사용 중 등)이나
|
|
278
|
+
보고서 파일을 쓰지 못함입니다.
|
|
279
|
+
|
|
175
280
|
## 키 평문을 내지 않는 원칙
|
|
176
281
|
|
|
177
282
|
- 받은 키 평문은 **그 클라이언트의 설정 파일에만** 씁니다(bind 는 그 폴더의 설정 파일, setup 은 Gemini CLI 와
|
|
@@ -197,6 +302,7 @@ CLI 는 폐기된 개수와 키 id 를 알립니다. 그래서 쓸 수 있는
|
|
|
197
302
|
| `AIAKIV_MCP_URL` | `https://mcp.aiakiv.com/mcp` | MCP 주소. 끝의 `/mcp` 는 빼고 줘도 됩니다. |
|
|
198
303
|
| `AIAKIV_CONTROL_URL` | `https://app.aiakiv.com` | 웹 콘솔 주소(인계 화면과 키 교환). |
|
|
199
304
|
| `AIAKIV_SKILLS_TARBALL_URL` | `https://codeload.github.com/AiAkiv/aiakiv-plugins/tar.gz/refs/heads/main` | 폴더 스킬과 전역 스킬(setup)을 받는 tar.gz 주소(http 나 https). 시험은 로컬 서버로 바꿉니다. |
|
|
305
|
+
| `AIAKIV_ENTERPRISE_MANIFEST_URL` | `https://www.aiakiv.com/download/enterprise/<판>/manifest.json` | `enterprise precheck` 가 이미지를 내려받을 때 읽는 목록 파일 주소(http 나 https). 시험은 로컬 서버로 바꿉니다. |
|
|
200
306
|
| `CODEX_HOME` | `~/.codex` | Codex 설정 위치(플러그인 캐시, 스킬 확인에 씁니다). |
|
|
201
307
|
|
|
202
308
|
## 개발
|
package/dist/ctx.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// 시험은 가짜(가짜 실행기, 가짜 브라우저, 임시 홈 폴더)를 넣어 같은 코드를 돈다.
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { openUrl } from "./browser.js";
|
|
5
|
-
import { run, which } from "./proc.js";
|
|
5
|
+
import { run, runLines, which } from "./proc.js";
|
|
6
6
|
async function askLine(question) {
|
|
7
7
|
const { createInterface } = await import("node:readline/promises");
|
|
8
8
|
// 질문은 stderr 로 — `--json` 의 stdout 을 더럽히지 않는다.
|
|
@@ -28,6 +28,7 @@ export function realCtx() {
|
|
|
28
28
|
err: (line = "") => process.stderr.write(`${line}\n`),
|
|
29
29
|
which: (name) => which(name, env, platform),
|
|
30
30
|
run,
|
|
31
|
+
runLines,
|
|
31
32
|
openUrl: (url) => openUrl(url, platform),
|
|
32
33
|
fetch: globalThis.fetch.bind(globalThis),
|
|
33
34
|
};
|
package/dist/dashboard.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
// `aiakiv dashboard [--json] [--dir <폴더>]`(BP-171 §5.2) — 한 화면 요약.
|
|
2
2
|
//
|
|
3
|
-
// 읽기 액션
|
|
4
|
-
// list_target_history limit 5)과 MCP get_save_target 한 번. 계정 키(.aiakiv/console.json)는 MCP 가
|
|
3
|
+
// 읽기 액션 일곱(list_teams, list_projects, list_permissions, list_links, list_cards,
|
|
4
|
+
// list_target_history limit 5, get_usage 인자 없음)과 MCP get_save_target 한 번. 계정 키(.aiakiv/console.json)는 MCP 가
|
|
5
5
|
// 거절하므로 그 키로는 get_save_target 을 부르지 않는다(BP-171 §4.3). 그 폴더에 프로젝트 키(bind 가 쓴
|
|
6
6
|
// codex, gemini 설정)가 따로 있으면 그 키로 부르고, 없으면 저장 대상 칸을 건너뛴다. 환경 변수 키는
|
|
7
7
|
// 종류를 알 수 없어 그대로 부른다(계정 키면 그 칸에 MCP 의 거절 이유가 적힌다). 한 칸이 실패해도(권한 꺼짐, 카드 앱
|
|
8
8
|
// 미등록, 403 등) 그 칸에 이유를 적고 나머지는 그린다. 401(키 문제), 404(입구 꺼짐), 연결 실패는
|
|
9
|
-
// 다른 칸도 같은 결과이므로 거기서 멈춘다(
|
|
9
|
+
// 다른 칸도 같은 결과이므로 거기서 멈춘다(일곱 번을 다 두드리지 않는다).
|
|
10
|
+
//
|
|
11
|
+
// 사용량 칸(BP-171 §5.5, P2c): get_usage 가 없는 옛 서버는 200 unknown_action 을 돌려준다. 그 칸에만
|
|
12
|
+
// "서버가 아직 지원하지 않음" 을 적는다. usage_unavailable(서버가 상태를 읽지 못함)도 그 칸에서 끝난다.
|
|
10
13
|
//
|
|
11
14
|
// 개수: 응답에 전체 수(total)가 있으면 그것을 쓴다. 팀, 프로젝트, 연결, 권한 목록은 서버가 페이지를
|
|
12
15
|
// 나누지 않고 전부 주므로(control list_orgs, list_projects, list_links, 권한 카탈로그) 받은 개수가
|
|
@@ -34,6 +37,45 @@ function countText(body, list, complete) {
|
|
|
34
37
|
return `${n}개`;
|
|
35
38
|
return `받은 ${n}개(더 있을 수 있음)`;
|
|
36
39
|
}
|
|
40
|
+
/** 정수부에 세 자리마다 쉼표(1000 → 1,000). 숫자가 아니면 undefined. */
|
|
41
|
+
function num(v) {
|
|
42
|
+
if (typeof v !== "number" || !Number.isFinite(v))
|
|
43
|
+
return undefined;
|
|
44
|
+
const [int, frac] = String(v).split(".");
|
|
45
|
+
return `${int.replace(/\B(?=(\d{3})+(?!\d))/g, ",")}${frac ? `.${frac}` : ""}`;
|
|
46
|
+
}
|
|
47
|
+
/** ISO 시각을 이 컴퓨터의 시각대로 `YYYY-MM-DD HH:MM`. 읽을 수 없으면 받은 글자 그대로. */
|
|
48
|
+
function localTime(iso) {
|
|
49
|
+
const d = new Date(iso);
|
|
50
|
+
if (Number.isNaN(d.getTime()))
|
|
51
|
+
return iso;
|
|
52
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
53
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
|
54
|
+
}
|
|
55
|
+
/** `used/limit`, 한도가 없으면 `used`. */
|
|
56
|
+
const usedOf = (o) => `${num(o.used) ?? "?"}${num(o.limit) !== undefined ? `/${num(o.limit)}` : ""}`;
|
|
57
|
+
/** get_usage 응답(BP-171 §5.5) → 한 줄. off 는 숫자가 null 이다. total 이 null 이면 총량을 뺀다. */
|
|
58
|
+
function usageLines(b) {
|
|
59
|
+
if (b.mode === "off")
|
|
60
|
+
return ["한도 없음"];
|
|
61
|
+
const parts = [];
|
|
62
|
+
if (isObj(b.daily)) {
|
|
63
|
+
const d = b.daily;
|
|
64
|
+
const extra = [
|
|
65
|
+
num(d.remaining) !== undefined ? `남은 ${num(d.remaining)}` : "",
|
|
66
|
+
typeof d.resets_at === "string" && d.resets_at ? `초기화 ${localTime(d.resets_at)}` : "",
|
|
67
|
+
].filter(Boolean);
|
|
68
|
+
parts.push(`오늘 ${usedOf(d)} 저장${extra.length ? `(${extra.join(", ")})` : ""}`);
|
|
69
|
+
}
|
|
70
|
+
if (isObj(b.total))
|
|
71
|
+
parts.push(`총량 ${usedOf(b.total)}`);
|
|
72
|
+
let line = parts.join(", ") || "응답에 사용량 숫자가 없습니다";
|
|
73
|
+
if (b.mode === "shadow")
|
|
74
|
+
line += " (한도를 적용하지 않는 관찰 모드)";
|
|
75
|
+
else if (b.mode !== "enforce")
|
|
76
|
+
line += ` (모드 ${s(b.mode) || "?"})`;
|
|
77
|
+
return [line];
|
|
78
|
+
}
|
|
37
79
|
const CELLS = [
|
|
38
80
|
{
|
|
39
81
|
field: "teams", title: "팀", action: "list_teams", data: {},
|
|
@@ -101,6 +143,19 @@ const CELLS = [
|
|
|
101
143
|
return lines;
|
|
102
144
|
},
|
|
103
145
|
},
|
|
146
|
+
{
|
|
147
|
+
field: "usage", title: "사용량", action: "get_usage", data: {},
|
|
148
|
+
render: usageLines,
|
|
149
|
+
failure: (o) => {
|
|
150
|
+
if (o.exit !== EXIT.APP_ERROR)
|
|
151
|
+
return undefined;
|
|
152
|
+
if (o.error?.code === "unknown_action")
|
|
153
|
+
return ["서버가 아직 지원하지 않음"];
|
|
154
|
+
if (o.error?.code === "usage_unavailable")
|
|
155
|
+
return ["지금 사용량을 읽을 수 없습니다(usage_unavailable). 잠시 뒤 다시 보세요."];
|
|
156
|
+
return undefined;
|
|
157
|
+
},
|
|
158
|
+
},
|
|
104
159
|
];
|
|
105
160
|
/** 일찍 멈출 결과: 키 문제(401), 입구 꺼짐(404), 연결 실패. 다른 칸도 같다. */
|
|
106
161
|
function stopsAll(r, o) {
|
|
@@ -188,7 +243,7 @@ export async function runDashboard(ctx, opts) {
|
|
|
188
243
|
if (o.exit === EXIT.OK && isObj(r.body))
|
|
189
244
|
row(cell.title, cell.render(r.body));
|
|
190
245
|
else
|
|
191
|
-
row(cell.title, cellFailure(o));
|
|
246
|
+
row(cell.title, cell.failure?.(o) ?? cellFailure(o));
|
|
192
247
|
}
|
|
193
248
|
if (!anyOk) {
|
|
194
249
|
ctx.err("모든 칸이 실패했습니다.");
|