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
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// 점검 보고서 파일(report.json, report.md). 점검 본체의 `--out` 이 쓰는 것과 같은 이름과 형식을 이 CLI 가 쓴다.
|
|
2
|
+
// 컨테이너에 --out 을 넘기지 않는 까닭: Linux 에서는 점검 컨테이너가 사용자 10001 로 돌아 붙인 폴더에 쓰지 못할 수
|
|
3
|
+
// 있다(K2G packaging/enterprise/precheck/README.md §5 Q14). 그래서 --json 의 마지막 줄(보고서)을 받아 여기서 쓴다.
|
|
4
|
+
//
|
|
5
|
+
// 형식은 K2G src/k2g/selfhost/precheck/runner.py `_write_outputs` 와 report.py `to_markdown` 을 옮긴 것이다.
|
|
6
|
+
// - report.json: json.dumps(보고서, ensure_ascii=False, indent=2) + "\n"
|
|
7
|
+
// - report.md: to_markdown(보고서). 말은 설치 언어(en 기본, ko)를 따른다. 저쪽 문장을 바꾸면 여기도 함께 바꾼다.
|
|
8
|
+
// 값을 글자로 바꾸는 str() 은 Python 과 같게 흉내 낸다(True, False, 목록과 사전의 repr). 깊은 중첩의 따옴표
|
|
9
|
+
// 고르기 같은 세부는 다를 수 있다.
|
|
10
|
+
import { parse as parseToml } from "smol-toml";
|
|
11
|
+
const T = {
|
|
12
|
+
"status.pass": ["PASS", "통과"],
|
|
13
|
+
"status.warn": ["WARN", "주의"],
|
|
14
|
+
"status.fail": ["FAIL", "실패"],
|
|
15
|
+
"status.skip": ["SKIP", "건너뜀"],
|
|
16
|
+
"status.info": ["INFO", "정보"],
|
|
17
|
+
seconds: ["{s}s", "{s}초"],
|
|
18
|
+
minutes: ["{m}m {s}s", "{m}분 {s}초"],
|
|
19
|
+
"md.title": ["# AiAkiv enterprise pre-install check report", "# AiAkiv 유료 기업판 설치 전 점검 보고서"],
|
|
20
|
+
"md.tool": ["- Tool: {tool} {version}, engine {engine}", "- 도구: {tool} {version}, 엔진 {engine}"],
|
|
21
|
+
"md.time": ["- Started: {started}, finished: {finished}", "- 시작: {started}, 끝: {finished}"],
|
|
22
|
+
"md.result": ["- Result: {verdict} {counts}", "- 결과: {verdict}{counts}"],
|
|
23
|
+
"verdict.ok": ["no failures", "실패 없음"],
|
|
24
|
+
"verdict.fail": ["has failures", "실패 있음"],
|
|
25
|
+
counts: ["(pass {p}, warn {w}, fail {f}, skip {s}, info {i})", "(통과 {p}, 주의 {w}, 실패 {f}, 건너뜀 {s}, 정보 {i})"],
|
|
26
|
+
"md.mode": [
|
|
27
|
+
"- Login mode: {mode}, meets the quote and license conditions: {eligible}",
|
|
28
|
+
"- 로그인 방식: {mode}, 견적과 발급 조건 충족: {eligible}",
|
|
29
|
+
],
|
|
30
|
+
yes: ["yes", "예"],
|
|
31
|
+
no: ["no", "아니오"],
|
|
32
|
+
"md.failed_required": ["- Required checks not met: {ids}", "- 충족하지 못한 필수 점검: {ids}"],
|
|
33
|
+
"md.sha": ["- Settings file sha256: {sha}", "- 설정 파일 sha256: {sha}"],
|
|
34
|
+
"md.warnings": ["## Settings warnings", "## 설정 경고"],
|
|
35
|
+
"md.target": ["## Target", "## 대상"],
|
|
36
|
+
"md.host": ["## Host", "## 호스트"],
|
|
37
|
+
"md.measurements": ["## Measurements", "## 측정값"],
|
|
38
|
+
"md.checks": ["## Checks", "## 점검"],
|
|
39
|
+
"md.header": ["| id | Result | Details | How to fix | Time |", "| id | 결과 | 내용 | 고칠 방법 | 시간 |"],
|
|
40
|
+
"md.details": ["## Check details", "## 점검 세부 값"],
|
|
41
|
+
};
|
|
42
|
+
function t(lang, key, args = {}) {
|
|
43
|
+
const pair = T[key];
|
|
44
|
+
const text = pair[lang === "ko" ? 1 : 0];
|
|
45
|
+
return text.replace(/\{(\w+)\}/g, (m, k) => (k in args ? String(args[k]) : m));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 점검의 설치 언어. 점검 본체(K2G k2g.selfhost.i18n.current_language)와 같은 순서로 이 CLI 가 볼 수 있는 것만 본다:
|
|
49
|
+
* 이 CLI 의 환경 변수 AIAKIV_LANGUAGE, 설정 파일 맨 위의 language, 기본 en. 작업 폴더 .env 의 AIAKIV_LANGUAGE 는
|
|
50
|
+
* 읽지 않는다(.env 는 열지 않는다).
|
|
51
|
+
*/
|
|
52
|
+
export function reportLanguage(env, configText) {
|
|
53
|
+
const norm = (v) => {
|
|
54
|
+
const s = typeof v === "string" ? v.trim().toLowerCase() : "";
|
|
55
|
+
return s === "en" || s === "ko" ? s : null;
|
|
56
|
+
};
|
|
57
|
+
const fromEnv = env.AIAKIV_LANGUAGE;
|
|
58
|
+
if (fromEnv !== undefined && fromEnv.trim())
|
|
59
|
+
return norm(fromEnv) ?? "en";
|
|
60
|
+
if (configText) {
|
|
61
|
+
try {
|
|
62
|
+
const v = parseToml(configText).language;
|
|
63
|
+
if (v !== undefined)
|
|
64
|
+
return norm(v) ?? "en";
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
/* 설정 파일을 못 읽으면 점검도 설정 오류로 끝난다. 말은 기본 */
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return "en";
|
|
71
|
+
}
|
|
72
|
+
/** Python repr 흉내(목록과 사전 안의 값). */
|
|
73
|
+
function pyRepr(v) {
|
|
74
|
+
if (v === null || v === undefined)
|
|
75
|
+
return "None";
|
|
76
|
+
if (typeof v === "boolean")
|
|
77
|
+
return v ? "True" : "False";
|
|
78
|
+
if (typeof v === "number")
|
|
79
|
+
return pyNumber(v);
|
|
80
|
+
if (typeof v === "string") {
|
|
81
|
+
const q = v.includes("'") && !v.includes('"') ? '"' : "'";
|
|
82
|
+
const body = v.replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
|
83
|
+
return q + (q === "'" ? body.replace(/'/g, "\\'") : body) + q;
|
|
84
|
+
}
|
|
85
|
+
if (Array.isArray(v))
|
|
86
|
+
return `[${v.map(pyRepr).join(", ")}]`;
|
|
87
|
+
if (typeof v === "object") {
|
|
88
|
+
return `{${Object.entries(v).map(([k, x]) => `${pyRepr(k)}: ${pyRepr(x)}`).join(", ")}}`;
|
|
89
|
+
}
|
|
90
|
+
return String(v);
|
|
91
|
+
}
|
|
92
|
+
function pyNumber(n) {
|
|
93
|
+
if (!Number.isFinite(n))
|
|
94
|
+
return n > 0 ? "inf" : n < 0 ? "-inf" : "nan";
|
|
95
|
+
return String(n);
|
|
96
|
+
}
|
|
97
|
+
/** Python str() 흉내. */
|
|
98
|
+
function pyStr(v) {
|
|
99
|
+
if (typeof v === "string")
|
|
100
|
+
return v;
|
|
101
|
+
return pyRepr(v);
|
|
102
|
+
}
|
|
103
|
+
/** report.py `_cell`: None 은 빈 칸, | 는 \|, 줄바꿈은 빈칸. */
|
|
104
|
+
function cell(v) {
|
|
105
|
+
return (v === null || v === undefined ? "" : pyStr(v)).replace(/\|/g, "\\|").replace(/\r/g, " ").replace(/\n/g, " ");
|
|
106
|
+
}
|
|
107
|
+
/** Python round() 의 짝수 쪽 반올림. */
|
|
108
|
+
function roundHalfEven(x) {
|
|
109
|
+
const f = Math.floor(x);
|
|
110
|
+
const d = x - f;
|
|
111
|
+
if (d > 0.5)
|
|
112
|
+
return f + 1;
|
|
113
|
+
if (d < 0.5)
|
|
114
|
+
return f;
|
|
115
|
+
return f % 2 === 0 ? f : f + 1;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Python f"{ms/1000:.1f}". 정수 ms 에서 반 올림 경계가 정확히 걸리는 것은 ms % 500 == 250(1250 은 1.25 가 이진수로
|
|
119
|
+
* 정확하다)뿐이고, Python 은 그때 짝수 쪽(1.2), JS toFixed 는 큰 쪽(1.3)으로 간다. 그 경우만 따로 맞춘다.
|
|
120
|
+
*/
|
|
121
|
+
function oneDecimal(ms) {
|
|
122
|
+
if (Number.isInteger(ms) && ms % 500 === 250) {
|
|
123
|
+
const tenths = Math.floor(ms / 100);
|
|
124
|
+
return ((tenths % 2 === 0 ? tenths : tenths + 1) / 10).toFixed(1);
|
|
125
|
+
}
|
|
126
|
+
return (ms / 1000).toFixed(1);
|
|
127
|
+
}
|
|
128
|
+
/** report.py `fmt_ms`. */
|
|
129
|
+
export function fmtMs(lang, ms) {
|
|
130
|
+
if (ms === null || ms === undefined)
|
|
131
|
+
return "-";
|
|
132
|
+
if (ms < 1000)
|
|
133
|
+
return `${Math.trunc(ms)}ms`;
|
|
134
|
+
const s = ms / 1000;
|
|
135
|
+
if (s < 60)
|
|
136
|
+
return t(lang, "seconds", { s: oneDecimal(ms) });
|
|
137
|
+
const m = Math.floor(s / 60);
|
|
138
|
+
return t(lang, "minutes", { m, s: roundHalfEven(s - m * 60) });
|
|
139
|
+
}
|
|
140
|
+
const STATUSES = ["pass", "warn", "fail", "skip", "info"];
|
|
141
|
+
const statusLabel = (lang, s) => typeof s === "string" && STATUSES.includes(s) ? t(lang, `status.${s}`) : String(s);
|
|
142
|
+
const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
143
|
+
const entries = (v) => (isObj(v) ? Object.entries(v) : []);
|
|
144
|
+
/** report.json 의 본문. */
|
|
145
|
+
export function reportJsonText(report) {
|
|
146
|
+
return `${JSON.stringify(report, null, 2)}\n`;
|
|
147
|
+
}
|
|
148
|
+
/** report.md 의 본문(report.py `to_markdown`). */
|
|
149
|
+
export function toMarkdown(r, lang) {
|
|
150
|
+
const q = isObj(r.quote) ? r.quote : {};
|
|
151
|
+
const s = isObj(r.summary) ? r.summary : {};
|
|
152
|
+
const n = (k) => s[k] ?? 0;
|
|
153
|
+
const verdict = t(lang, r.ok ? "verdict.ok" : "verdict.fail");
|
|
154
|
+
const counts = t(lang, "counts", { p: n("pass"), w: n("warn"), f: n("fail"), s: n("skip"), i: n("info") });
|
|
155
|
+
const lines = [
|
|
156
|
+
t(lang, "md.title"),
|
|
157
|
+
"",
|
|
158
|
+
t(lang, "md.tool", { tool: pyStr(r.tool), version: pyStr(r.tool_version), engine: pyStr(q.engine_version ?? "-") }),
|
|
159
|
+
t(lang, "md.time", { started: pyStr(r.started_at), finished: r.finished_at ? pyStr(r.finished_at) : "-" }),
|
|
160
|
+
t(lang, "md.result", { verdict, counts }),
|
|
161
|
+
t(lang, "md.mode", { mode: pyStr(q.mode ?? "-"), eligible: t(lang, q.eligible ? "yes" : "no") }),
|
|
162
|
+
];
|
|
163
|
+
if (Array.isArray(q.failed_required) && q.failed_required.length) {
|
|
164
|
+
lines.push(t(lang, "md.failed_required", { ids: q.failed_required.map(pyStr).join(", ") }));
|
|
165
|
+
}
|
|
166
|
+
lines.push(t(lang, "md.sha", { sha: pyStr(q.config_sha256 ?? "-") }));
|
|
167
|
+
if (Array.isArray(r.warnings) && r.warnings.length) {
|
|
168
|
+
lines.push("", t(lang, "md.warnings"), "", ...r.warnings.map((w) => `- ${pyStr(w)}`));
|
|
169
|
+
}
|
|
170
|
+
lines.push("", t(lang, "md.target"), "", ...entries(r.target).map(([k, v]) => `- ${k}: ${cell(v)}`));
|
|
171
|
+
lines.push("", t(lang, "md.host"), "", ...entries(r.host).map(([k, v]) => `- ${k}: ${cell(v)}`));
|
|
172
|
+
lines.push("", t(lang, "md.measurements"), "", ...entries(r.measurements).map(([k, v]) => `- ${k}: ${cell(v)}`));
|
|
173
|
+
lines.push("", t(lang, "md.checks"), "", t(lang, "md.header"), "|---|---|---|---|---|");
|
|
174
|
+
const checks = Array.isArray(r.checks) ? r.checks.filter(isObj) : [];
|
|
175
|
+
for (const c of checks) {
|
|
176
|
+
const ms = typeof c.ms === "number" ? fmtMs(lang, c.ms) : "";
|
|
177
|
+
lines.push(`| ${cell(c.id)} | ${statusLabel(lang, c.status)} | ${cell(c.detail)} | ${cell(c.fix)} | ${ms} |`);
|
|
178
|
+
}
|
|
179
|
+
const withData = checks.filter((c) => c.data !== undefined && c.data !== null);
|
|
180
|
+
if (withData.length) {
|
|
181
|
+
lines.push("", t(lang, "md.details"));
|
|
182
|
+
for (const c of withData) {
|
|
183
|
+
lines.push("", `### ${pyStr(c.id)}`, "", "```json", JSON.stringify(c.data, null, 2), "```");
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
lines.push("", "## quote", "", "```json", JSON.stringify(q, null, 2), "```", "");
|
|
187
|
+
return lines.join("\n");
|
|
188
|
+
}
|