aiakiv 0.2.2 → 0.3.1

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/dist/main.js CHANGED
@@ -6,12 +6,14 @@ import { runBind } from "./bind.js";
6
6
  import { ConsoleUsageError, runConsole, runConsoleKey } from "./console.js";
7
7
  import { runDashboard } from "./dashboard.js";
8
8
  import { DEFAULT_REPORT, DOCTOR_CLIENT_IDS, runDoctor } from "./doctor.js";
9
+ import { DEFAULT_PRECHECK_CONFIG, DEFAULT_PRECHECK_OUT, runEnterprisePrecheck } from "./enterprise/precheck.js";
9
10
  import { realHost } from "./local/checks.js";
10
11
  import { DEFAULT_ENGINE_PORT, runLocalDoctor } from "./local/doctor.js";
12
+ import { CONTAINER_EXPORT_DIR, CONTAINER_IMPORT_DIR, runLocalExport, runLocalImport } from "./local/portable.js";
11
13
  import { runSetup } from "./setup.js";
12
14
  import { DEFAULT_SKILLS_TARBALL_URL } from "./skills.js";
13
15
  import { DEFAULT_CONTROL_URL, DEFAULT_MCP_URL } from "./urls.js";
14
- import { VERSION } from "./version.js";
16
+ import { ENTERPRISE_ENGINE_VERSION, VERSION } from "./version.js";
15
17
  export const HELP = `aiakiv ${VERSION}: 폴더에 AiAkiv 프로젝트를 고정하고 확인하고, 전역 설치와 콘솔 작업을 하는 명령
16
18
 
17
19
  사용법:
@@ -24,6 +26,11 @@ export const HELP = `aiakiv ${VERSION}: 폴더에 AiAkiv 프로젝트를 고정
24
26
  npx aiakiv console key [--expires-days N] [--dir <폴더>] [--no-browser]
25
27
  npx aiakiv dashboard [--json] [--dir <폴더>]
26
28
  npx aiakiv local doctor [--json] [--out <파일.md>] [--port <n>] [--offline]
29
+ npx aiakiv local export <팀> --to <파일|폴더> [--domain <이름>] [--yes] [--container <이름>]
30
+ npx aiakiv local import <파일|폴더> --team <팀> [--as <사용자>] [--domain <이름>] [--dry-run] [--resume] [--yes]
31
+ [--container <이름>]
32
+ npx aiakiv enterprise precheck [--config <toml>] [--image <참조> | --image-tar <파일>] [--device] [--no-browser]
33
+ [--no-login] [--only <영역,...>] [--skip <id 접두어,...>] [--redact-claims] [--out <폴더>] [--json]
27
34
 
28
35
  bind 폴더에 프로젝트를 고정합니다. 웹 콘솔의 "폴더 ZIP 받기" 와 같은 한 벌을 그 폴더에 직접 씁니다.
29
36
  claude, cursor 는 ?project= 주소(OAuth)를 쓰고, codex, gemini 는 브라우저에서 확인 버튼을 눌러
@@ -62,10 +69,40 @@ local doctor
62
69
  자체 설치판(무료 5인 엔진)을 깔기 전에 이 컴퓨터가 준비됐는지 점검합니다. Node, OS, Docker(판,
63
70
  compose v2, 리눅스 컨테이너 모드, 아키텍처, 메모리, CPU), 디스크 여유 공간, 포트(기본 ${DEFAULT_ENGINE_PORT}),
64
71
  인터넷 연결(Docker Hub, www.aiakiv.com, npm 저장소), 프록시 설정, 이미 깔린 엔진을 봅니다. 그 포트에
65
- 엔진이 떠 있으면 엔진 상태, 판 이름, 판, 포트가 묶인 주소도 봅니다. --json 은 보고서 JSON 하나를 내고,
72
+ 엔진이 떠 있으면 엔진 상태, 판 이름, 판, 포트가 묶인 주소도 봅니다. 엔진 컨테이너가 떠 있으면 벡터가
73
+ 없어 검색에 나오지 않는 기억이 있는지 엔진에 묻고, 끊긴 가져오기가 있으면 이어서 할 명령을 알려 줍니다.
74
+ --json 은 보고서 JSON 하나를 내고,
66
75
  --out 은 같은 내용을 마크다운 파일로 씁니다. --port 는 볼 포트를 바꾸고, --offline 이면 인터넷 연결은
67
76
  보지 않습니다. 종료 코드: 0 실패 없음, 3 실패 있음, 2 인자 오류, 4 Docker 를 찾지 못했거나 Docker 에
68
77
  닿지 못함(이때도 보고서는 냅니다).
78
+ local export, local import
79
+ 자체 설치판 엔진의 팀 기억을 .mweft.tar.gz 파일로 내보내고 받습니다. 본체는 엔진 컨테이너 안의
80
+ aiakiv-admin export, import 이고, 이 명령은 떠 있는 엔진 컨테이너를 찾아(compose 서비스 engine, 이미지
81
+ aiakiv/engine) docker exec 로 부르고, 파일을 docker cp 로 옮깁니다. --to 와 <파일> 은 이 컴퓨터의 경로이고,
82
+ 컨테이너 안에는 이 명령이 ${CONTAINER_EXPORT_DIR}, ${CONTAINER_IMPORT_DIR} 아래에 임시 폴더를 만들었다가 끝나면 지웁니다.
83
+ export 의 --to 가 폴더면 그 안에 둡니다. 팀의 도메인이 여럿이면 묶음(아카이브 여럿과 org_manifest.json)이라
84
+ --to 는 폴더가 됩니다. 이미 있는 파일은 --yes 가 있어야 덮어씁니다.
85
+ import 는 묻지 않습니다. --yes 가 없으면 엔진이 계획만 내고 종료 코드 2 로 끝납니다. --dry-run 은 계획만 내고 0,
86
+ --resume 은 끊긴 재임베딩을 이어서 합니다(같은 파일을 다시 주세요). --as 는 가져온 기억의 작성자(기본 팀 소유자).
87
+ 출력은 언제나 JSON 입니다(--json 은 붙여도 같습니다). export 는 한 문서, import 는 엔진의 진행 줄
88
+ ({"step","done","total"})을 받는 대로 내고 마지막 줄이 엔진의 결과입니다. 엔진의 실패 JSON 도 그대로 냅니다.
89
+ 벡터는 엔진이 다룹니다(이 명령은 파일 내용을 읽거나 바꾸지 않습니다). 엔진이 여럿 떠 있으면 --container 로 고릅니다.
90
+ 종료 코드: 엔진의 것을 그대로(0 성공, 2 입력 오류나 계획만, 3 거절, 4 엔진 DB 연결 실패, 5 가져오기 검증 실패).
91
+ 이 명령의 것은 2 인자 오류나 파일 없음이나 이미 있는 파일, 4 Docker 를 쓸 수 없음이나 엔진 컨테이너가 떠 있지
92
+ 않음, 1 docker cp 나 docker exec 실패, 엔진이 결과 없이 끝남.
93
+ enterprise precheck
94
+ 유료 기업판을 깔기 전에 회사의 신원 서버(OIDC), 데이터베이스, 파일 저장소, 메일 서버에 실제로 연결해 봅니다.
95
+ 점검은 유료 기업판 엔진 이미지 안의 aiakiv-admin precheck 가 하고, 이 명령은 Docker 로 그것을 돌립니다.
96
+ 설정 파일(기본 ./${DEFAULT_PRECHECK_CONFIG})이 있는 폴더가 컨테이너의 /config 로 붙고, 그 폴더의 .env 가
97
+ 있으면 함께 넘깁니다(비밀 값은 설정에서 env:이름 으로 가리킵니다. 이 명령은 .env 를 읽지 않습니다).
98
+ 이미지는 --image(이 컴퓨터에 있는 이미지), --image-tar(내려받은 tar, 같은 폴더의 <파일>.sha256 과 지문 비교),
99
+ 둘 다 없으면 aiakiv/engine-enterprise:${ENTERPRISE_ENGINE_VERSION} 이 있으면 그것, 없으면 www.aiakiv.com 에서
100
+ 내려받습니다(~/.aiakiv/cache). 로그인 단계에서 로그인 주소를 보이고 브라우저를 엽니다(--device 는 다른 기기에서
101
+ 넣을 주소와 코드, --no-browser 는 열지 않음). --no-login, --only, --skip, --redact-claims 는 점검에 그대로
102
+ 넘깁니다. 보고서는 그 폴더의 --out(기본 ${DEFAULT_PRECHECK_OUT}) 아래 report.json 과 report.md 입니다.
103
+ --json 이면 이 명령의 줄(cli.docker, cli.image) 뒤에 점검의 줄을 바꾸지 않고 내며 마지막 줄이 보고서입니다.
104
+ 종료 코드: 0 실패 없음, 3 실패 있음, 2 인자나 설정 오류, 4 Docker 를 쓸 수 없음, 5 이미지를 구하지 못함,
105
+ 1 점검이 보고서 없이 끝남.
69
106
 
70
107
  console 과 dashboard 가 키를 찾는 순서: 환경 변수 AIAKIV_API_KEY, <폴더>/.aiakiv/console.json(계정 키),
71
108
  <폴더>의 .codex/config.toml 과 .gemini/settings.json(프로젝트 키). 키 평문은 화면에 내지 않고 키 id 와
@@ -91,10 +128,13 @@ console 과 dashboard 의 종료 코드:
91
128
  AIAKIV_CONTROL_URL 웹 콘솔 주소(기본 ${DEFAULT_CONTROL_URL})
92
129
  AIAKIV_API_KEY console, dashboard 가 먼저 쓰는 키
93
130
  AIAKIV_SKILLS_TARBALL_URL 폴더 스킬과 전역 스킬을 받는 주소(기본 ${DEFAULT_SKILLS_TARBALL_URL})
131
+ AIAKIV_ENTERPRISE_MANIFEST_URL enterprise precheck 가 이미지를 내려받을 때 읽는 목록 파일 주소
94
132
 
95
133
  공통: -h, --help -v, --version`;
96
134
  class UsageError extends Error {
97
135
  }
136
+ /** local 의 하위 명령 안내(모르는 하위 명령, 하위 명령 없음). */
137
+ const LOCAL_SUBS = "doctor, export, import 셋입니다";
98
138
  function parseExpiresDays(v) {
99
139
  if (v === undefined)
100
140
  return undefined;
@@ -303,14 +343,93 @@ export async function main(argv, ctx) {
303
343
  });
304
344
  }
305
345
  if (cmd === "local") {
306
- // 자체 설치판(무료 5인 엔진) 명령. 지금은 doctor 하나(docs/211 §5.1, §17). install, status 등이 같은 아래에 붙는다.
346
+ // 자체 설치판(무료 5인 엔진) 명령. 지금은 doctor, export, import(docs/211 §5.1, §7, §17). install, status 등이
347
+ // 같은 아래에 붙는다.
307
348
  const [sub, ...subRest] = rest;
308
349
  if (sub === "-h" || sub === "--help" || sub === "help")
309
350
  return (ctx.out(HELP), 0);
310
351
  if (!sub)
311
- throw new UsageError("local 뒤에 하위 명령이 필요합니다(지금은 doctor 하나입니다).");
352
+ throw new UsageError(`local 뒤에 하위 명령이 필요합니다(지금은 ${LOCAL_SUBS}).`);
353
+ if (sub === "export") {
354
+ const { values, positionals } = parseArgs({
355
+ args: subRest,
356
+ options: {
357
+ to: { type: "string" },
358
+ domain: { type: "string" },
359
+ yes: { type: "boolean", default: false },
360
+ container: { type: "string" },
361
+ json: { type: "boolean", default: false },
362
+ help: { type: "boolean", short: "h" },
363
+ },
364
+ strict: true,
365
+ allowPositionals: true,
366
+ });
367
+ if (values.help)
368
+ return (ctx.out(HELP), 0);
369
+ if (positionals.length !== 1) {
370
+ throw new UsageError(positionals.length
371
+ ? `local export 는 팀 이름 하나를 받습니다(받은 값: ${positionals.join(" ")}).`
372
+ : "local export 에 팀 이름이 필요합니다. 예: npx aiakiv local export 연구팀 --to ./연구팀.mweft.tar.gz");
373
+ }
374
+ if (values.to === undefined)
375
+ throw new UsageError("--to <파일 또는 폴더> 가 필요합니다(호스트 경로).");
376
+ for (const name of ["to", "domain", "container"]) {
377
+ const v = values[name];
378
+ if (v !== undefined && !v.trim())
379
+ throw new UsageError(`--${name} 에 값이 필요합니다.`);
380
+ }
381
+ if (!positionals[0].trim())
382
+ throw new UsageError("팀 이름이 비어 있습니다.");
383
+ return await runLocalExport(ctx, {
384
+ team: positionals[0], to: values.to, domain: values.domain, yes: values.yes, container: values.container,
385
+ });
386
+ }
387
+ if (sub === "import") {
388
+ const { values, positionals } = parseArgs({
389
+ args: subRest,
390
+ options: {
391
+ team: { type: "string" },
392
+ as: { type: "string" },
393
+ domain: { type: "string" },
394
+ "dry-run": { type: "boolean", default: false },
395
+ resume: { type: "boolean", default: false },
396
+ yes: { type: "boolean", default: false },
397
+ container: { type: "string" },
398
+ json: { type: "boolean", default: false },
399
+ help: { type: "boolean", short: "h" },
400
+ },
401
+ strict: true,
402
+ allowPositionals: true,
403
+ });
404
+ if (values.help)
405
+ return (ctx.out(HELP), 0);
406
+ if (positionals.length !== 1) {
407
+ throw new UsageError(positionals.length
408
+ ? `local import 는 파일 하나를 받습니다(받은 값: ${positionals.join(" ")}).`
409
+ : "local import 에 가져올 파일이 필요합니다. 예: npx aiakiv local import ./연구팀.mweft.tar.gz --team 연구팀");
410
+ }
411
+ if (values.team === undefined)
412
+ throw new UsageError("--team <팀> 이 필요합니다.");
413
+ for (const name of ["team", "as", "domain", "container"]) {
414
+ const v = values[name];
415
+ if (v !== undefined && !v.trim())
416
+ throw new UsageError(`--${name} 에 값이 필요합니다.`);
417
+ }
418
+ if (!positionals[0].trim())
419
+ throw new UsageError("가져올 파일 경로가 비어 있습니다.");
420
+ return await runLocalImport(ctx, {
421
+ file: positionals[0],
422
+ team: values.team,
423
+ as: values.as,
424
+ domain: values.domain,
425
+ dryRun: values["dry-run"],
426
+ resume: values.resume,
427
+ yes: values.yes,
428
+ container: values.container,
429
+ });
430
+ }
312
431
  if (sub !== "doctor")
313
- throw new UsageError(`local 의 모르는 하위 명령입니다: ${sub}. 지금은 doctor 하나입니다.`);
432
+ throw new UsageError(`local 의 모르는 하위 명령입니다: ${sub}. 지금은 ${LOCAL_SUBS}.`);
314
433
  const { values, positionals } = parseArgs({
315
434
  args: subRest,
316
435
  options: {
@@ -331,6 +450,62 @@ export async function main(argv, ctx) {
331
450
  throw new UsageError("--out 에 파일 이름이 필요합니다.");
332
451
  return await runLocalDoctor(ctx, { json: values.json, out: values.out, port: parsePort(values.port), offline: values.offline }, realHost());
333
452
  }
453
+ if (cmd === "enterprise") {
454
+ // 유료 기업판 명령. 지금은 precheck 하나(K2G docs/212 BP-173 §3, P3). 점검 본체는 엔진 이미지 안에 있다.
455
+ const [sub, ...subRest] = rest;
456
+ if (sub === "-h" || sub === "--help" || sub === "help")
457
+ return (ctx.out(HELP), 0);
458
+ if (!sub)
459
+ throw new UsageError("enterprise 뒤에 하위 명령이 필요합니다(지금은 precheck 하나입니다).");
460
+ if (sub !== "precheck") {
461
+ throw new UsageError(`enterprise 의 모르는 하위 명령입니다: ${sub}. 지금은 precheck 하나입니다.`);
462
+ }
463
+ const { values, positionals } = parseArgs({
464
+ args: subRest,
465
+ options: {
466
+ config: { type: "string" },
467
+ image: { type: "string" },
468
+ "image-tar": { type: "string" },
469
+ device: { type: "boolean", default: false },
470
+ "no-browser": { type: "boolean", default: false },
471
+ "no-login": { type: "boolean", default: false },
472
+ only: { type: "string" },
473
+ skip: { type: "string" },
474
+ "redact-claims": { type: "boolean", default: false },
475
+ out: { type: "string" },
476
+ json: { type: "boolean", default: false },
477
+ help: { type: "boolean", short: "h" },
478
+ },
479
+ strict: true,
480
+ allowPositionals: true,
481
+ });
482
+ if (values.help)
483
+ return (ctx.out(HELP), 0);
484
+ if (positionals.length) {
485
+ throw new UsageError(`enterprise precheck 는 위치 인자를 받지 않습니다: ${positionals.join(" ")}`);
486
+ }
487
+ if (values.image !== undefined && values["image-tar"] !== undefined) {
488
+ throw new UsageError("--image 와 --image-tar 는 함께 줄 수 없습니다. 둘 중 하나만 주세요.");
489
+ }
490
+ for (const name of ["config", "image", "image-tar", "only", "skip", "out"]) {
491
+ const v = values[name];
492
+ if (v !== undefined && !v.trim())
493
+ throw new UsageError(`--${name} 에 값이 필요합니다.`);
494
+ }
495
+ return await runEnterprisePrecheck(ctx, {
496
+ config: values.config,
497
+ image: values.image,
498
+ imageTar: values["image-tar"],
499
+ device: values.device,
500
+ noBrowser: values["no-browser"],
501
+ noLogin: values["no-login"],
502
+ only: values.only,
503
+ skip: values.skip,
504
+ redactClaims: values["redact-claims"],
505
+ out: values.out,
506
+ json: values.json,
507
+ });
508
+ }
334
509
  if (cmd === "dashboard") {
335
510
  const { values } = parseArgs({
336
511
  args: rest,
package/dist/proc.js CHANGED
@@ -93,6 +93,57 @@ export const run = (file, args, opts = {}) => new Promise((resolve) => {
93
93
  });
94
94
  });
95
95
  });
96
+ /** 조각으로 들어오는 글자를 줄로 자른다. 마지막 줄이 줄바꿈 없이 끝나도 flush 가 넘긴다. */
97
+ function lineSplitter(emit) {
98
+ let buf = "";
99
+ const send = (l) => emit(l.replace(/\r$/, "").replace(ANSI, ""));
100
+ return {
101
+ push: (d) => {
102
+ buf += d;
103
+ let i;
104
+ while ((i = buf.indexOf("\n")) >= 0) {
105
+ send(buf.slice(0, i));
106
+ buf = buf.slice(i + 1);
107
+ }
108
+ },
109
+ flush: () => {
110
+ if (buf)
111
+ send(buf);
112
+ buf = "";
113
+ },
114
+ };
115
+ }
116
+ export const runLines = (file, args, opts) => new Promise((resolve) => {
117
+ const viaCmd = process.platform === "win32" && /\.(cmd|bat)$/i.test(file);
118
+ const env = { ...process.env, NO_COLOR: "1", FORCE_COLOR: "0" };
119
+ const stdio = ["ignore", "pipe", "pipe"];
120
+ let child;
121
+ try {
122
+ child = viaCmd
123
+ ? spawn([quoteForCmd(file), ...args.map(quoteForCmd)].join(" "), {
124
+ shell: true,
125
+ stdio,
126
+ env,
127
+ cwd: opts.cwd,
128
+ windowsHide: true,
129
+ })
130
+ : spawn(file, args, { stdio, env, cwd: opts.cwd, windowsHide: true });
131
+ }
132
+ catch (e) {
133
+ resolve({ code: null, error: e.message });
134
+ return;
135
+ }
136
+ const out = lineSplitter(opts.onStdout);
137
+ const err = lineSplitter(opts.onStderr);
138
+ child.stdout?.setEncoding("utf8").on("data", (d) => out.push(d));
139
+ child.stderr?.setEncoding("utf8").on("data", (d) => err.push(d));
140
+ child.on("error", (e) => resolve({ code: null, error: e.message }));
141
+ child.on("close", (code) => {
142
+ out.flush();
143
+ err.flush();
144
+ resolve({ code });
145
+ });
146
+ });
96
147
  /** 실행 결과를 사람이 읽을 한 줄 이유로. */
97
148
  export function failReason(r) {
98
149
  if (r.error)
package/dist/setup.js CHANGED
@@ -21,7 +21,7 @@ import { ConfigError, jsonServers, readJsonObject, upsertJsonServer } from "./co
21
21
  import { expiryText } from "./consoleKeyFile.js";
22
22
  import { classifyEntry } from "./doctor.js";
23
23
  import { readTextIfExists, writeFileAtomic } from "./fsutil.js";
24
- import { exchangeCode, handoffUrl, HANDOFF_TIMEOUT_MS, newState, scopeList, startLoopback, supersededNotice, } from "./handoff.js";
24
+ import { exchangeCode, handoffUrl, HANDOFF_TIMEOUT_MS, newState, oldKeysKeptNotice, scopeList, startLoopback, supersededNotice, } from "./handoff.js";
25
25
  import { probeSaveTarget } from "./mcpProbe.js";
26
26
  import { downloadSkillPack, SKILL_NAMES, SKILLS_REPO, skillsGlobalManualCommand, writeSkillsTo } from "./skills.js";
27
27
  import { canonicalMcpUrl, controlBase } from "./urls.js";
@@ -145,6 +145,9 @@ async function newKeyByHandoff(ctx, opts, url) {
145
145
  const sup = supersededNotice(res.superseded, "dynamic");
146
146
  if (sup)
147
147
  ctx.out(sup);
148
+ const kept = oldKeysKeptNotice(res.old_keys_kept);
149
+ if (kept)
150
+ ctx.out(kept);
148
151
  if (typeof res.mcp_url === "string" && res.mcp_url && res.mcp_url !== url) {
149
152
  ctx.err(`참고: 서버가 알려 준 MCP 주소(${res.mcp_url})와 설정에 쓰는 주소(${url})가 다릅니다. ` +
150
153
  "AIAKIV_MCP_URL 이 맞는지 확인하세요.");
package/dist/version.js CHANGED
@@ -1,2 +1,6 @@
1
1
  // package.json 의 version 과 같아야 한다(tests/cli.test.mjs 가 확인).
2
- export const VERSION = "0.2.2";
2
+ export const VERSION = "0.3.1";
3
+ // 이 CLI 판이 `aiakiv enterprise precheck` 에서 쓰는 유료 기업판 엔진 이미지 판(aiakiv/engine-enterprise:<판>).
4
+ // K2G packaging/selfhost/build.sh 가 `<이미지>:<판>` 으로 태그를 붙이고(앞에 v 없음), 엔진 판은 K2G
5
+ // pyproject.toml 의 version(이미지의 AIAKIV_ENGINE_VERSION)이다. 엔진 판을 올리면 이 값도 같이 올린다.
6
+ export const ENTERPRISE_ENGINE_VERSION = "0.1.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiakiv",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "description": "AiAkiv CLI: pin a folder to an AiAkiv project (connection, folder skills, settings), check it, and run console actions and a dashboard from the shell.",
5
5
  "license": "MIT",
6
6
  "type": "module",