@iola_adm/iola-cli 0.2.18 → 0.2.20

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.
@@ -32,25 +32,27 @@ const steps = [
32
32
  ];
33
33
 
34
34
  const canAnimate = process.stdout.isTTY && process.env.CI !== "true";
35
+ const setupStarted = process.hrtime.bigint();
35
36
 
36
37
  console.log("");
37
- console.log("IOLA CLI: настройка после установки");
38
+ console.log("IOLA CLI: настройка после установки npm-пакета");
39
+ console.log("Время ниже считает только настройку CLI, без скачивания и распаковки npm-пакета.");
38
40
 
39
41
  for (let index = 0; index < steps.length; index += 1) {
40
42
  const step = steps[index];
41
43
  await runStep(step, index + 1, steps.length);
42
44
  }
43
45
 
44
- console.log("IOLA CLI готова. Запуск: iola");
46
+ console.log(`IOLA CLI готова за ${formatDuration(elapsedMs(setupStarted))}. Запуск: iola`);
45
47
 
46
48
  async function runStep(step, current, total) {
47
- const started = Date.now();
49
+ const started = process.hrtime.bigint();
48
50
  let frame = 0;
49
51
  let lastOutput = "";
50
52
  const prefix = `[${current}/${total}] ${step.title}`;
51
53
  const render = () => {
52
54
  if (!canAnimate) return;
53
- const seconds = Math.max(1, Math.round((Date.now() - started) / 1000));
55
+ const seconds = Math.max(1, Math.floor(elapsedMs(started) / 1000));
54
56
  process.stdout.write(`\r${frames[frame]} ${prefix}... ${seconds}s`);
55
57
  frame = (frame + 1) % frames.length;
56
58
  };
@@ -75,9 +77,9 @@ async function runStep(step, current, total) {
75
77
  }
76
78
 
77
79
  if (canAnimate) {
78
- process.stdout.write(`\r✓ ${prefix} готово за ${formatDuration(Date.now() - started)}\n`);
80
+ process.stdout.write(`\r✓ ${prefix} готово за ${formatDuration(elapsedMs(started))}\n`);
79
81
  } else {
80
- console.log(`✓ ${prefix} готово за ${formatDuration(Date.now() - started)}`);
82
+ console.log(`✓ ${prefix} готово за ${formatDuration(elapsedMs(started))}`);
81
83
  }
82
84
  }
83
85
 
@@ -114,9 +116,15 @@ function run(command, args, onOutput) {
114
116
  });
115
117
  }
116
118
 
119
+ function elapsedMs(started) {
120
+ return Number(process.hrtime.bigint() - started) / 1_000_000;
121
+ }
122
+
117
123
  function formatDuration(ms) {
118
- const seconds = Math.max(1, Math.round(ms / 1000));
119
- if (seconds < 60) return `${seconds}s`;
124
+ if (ms < 1000) return `${Math.max(1, Math.round(ms))}ms`;
125
+ const totalSeconds = ms / 1000;
126
+ if (totalSeconds < 60) return `${totalSeconds.toFixed(1)}s`;
127
+ const seconds = Math.floor(totalSeconds);
120
128
  const minutes = Math.floor(seconds / 60);
121
129
  const rest = seconds % 60;
122
130
  return `${minutes}m ${rest}s`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.2.18",
3
+ "version": "0.2.20",
4
4
  "description": "CLI и AI-агент городского округа Йошкар-Ола.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/adm-iola/iola-cli#readme",
package/src/cli.js CHANGED
@@ -3315,8 +3315,13 @@ async function setupYandexConnector(args = []) {
3315
3315
  console.log("Выбрать активные функции можно командой /yandex или iola yandex menu.");
3316
3316
  if (clientId || oauthApps.length) {
3317
3317
  if (process.stdin.isTTY && !options["print-url"]) {
3318
+ const secrets = await loadSecrets();
3318
3319
  console.log("Открываю браузер для входа в Яндекс. После авторизации токен сохранится автоматически.");
3319
3320
  for (const app of oauthApps.length ? oauthApps : [{ id: "custom", title: "Yandex Connector", clientId, services: authorizedServices }]) {
3321
+ if (!options.force && hasYandexOAuthAppToken(secrets, app.id)) {
3322
+ console.log(`Авторизация: ${app.title} уже подключена, пропускаю.`);
3323
+ continue;
3324
+ }
3320
3325
  console.log(`Авторизация: ${app.title}`);
3321
3326
  await runYandexBrowserOAuth({ appId: app.id, clientId: app.clientId, services: app.services, redirectUrl });
3322
3327
  }
@@ -3483,11 +3488,16 @@ function waitForYandexOAuthToken({ clientId, services, redirectUrl }) {
3483
3488
  return new Promise((resolvePromise, reject) => {
3484
3489
  let settled = false;
3485
3490
  const timeoutMs = 180000;
3491
+ const debug = process.env.IOLA_YANDEX_OAUTH_DEBUG === "1";
3492
+ const logDebug = (message) => {
3493
+ if (debug) console.log(`[yandex-oauth] ${message}`);
3494
+ };
3486
3495
  const finish = (token) => {
3487
3496
  if (!token) throw new Error("Yandex OAuth token не получен.");
3488
3497
  if (!settled) {
3489
3498
  settled = true;
3490
3499
  clearTimeout(timer);
3500
+ logDebug("token received");
3491
3501
  resolvePromise(String(token));
3492
3502
  }
3493
3503
  };
@@ -3495,6 +3505,7 @@ function waitForYandexOAuthToken({ clientId, services, redirectUrl }) {
3495
3505
  try {
3496
3506
  const url = new URL(req.url || "/", redirectUrl);
3497
3507
  if (url.pathname === YANDEX_CONNECTOR_REDIRECT_PATH && req.method === "GET") {
3508
+ logDebug(`GET ${url.pathname}${url.search || ""}`);
3498
3509
  const tokenFromQuery = url.searchParams.get("access_token") || url.searchParams.get("token");
3499
3510
  const errorFromQuery = url.searchParams.get("error") || "";
3500
3511
  if (errorFromQuery) throw new Error(`Yandex OAuth error: ${errorFromQuery}`);
@@ -3536,6 +3547,7 @@ function waitForYandexOAuthToken({ clientId, services, redirectUrl }) {
3536
3547
  return;
3537
3548
  }
3538
3549
  if (url.pathname === "/yandex/oauth/token" && req.method === "GET") {
3550
+ logDebug(`GET ${url.pathname}${url.search || ""}`);
3539
3551
  const token = url.searchParams.get("token") || url.searchParams.get("access_token");
3540
3552
  const error = url.searchParams.get("error") || "";
3541
3553
  if (error) throw new Error(`Yandex OAuth error: ${error}`);
@@ -3549,11 +3561,18 @@ function waitForYandexOAuthToken({ clientId, services, redirectUrl }) {
3549
3561
  const chunks = [];
3550
3562
  for await (const chunk of req) chunks.push(chunk);
3551
3563
  const payload = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
3564
+ logDebug(`POST ${url.pathname}; token=${payload.token ? "yes" : "no"} error=${payload.error ? "yes" : "no"}`);
3552
3565
  if (payload.error) throw new Error(`Yandex OAuth error: ${payload.error}`);
3553
- finish(payload.token);
3554
3566
  res.writeHead(200, { "content-type": "application/json" });
3555
3567
  res.end(JSON.stringify({ ok: true }));
3556
- server.close();
3568
+ res.on("finish", () => {
3569
+ try {
3570
+ finish(payload.token);
3571
+ } catch (error) {
3572
+ if (!settled) reject(error);
3573
+ }
3574
+ server.close();
3575
+ });
3557
3576
  return;
3558
3577
  }
3559
3578
  res.writeHead(404);
@@ -3586,6 +3605,7 @@ function waitForYandexOAuthToken({ clientId, services, redirectUrl }) {
3586
3605
  server.listen(YANDEX_CONNECTOR_REDIRECT_PORT, YANDEX_CONNECTOR_REDIRECT_HOST, async () => {
3587
3606
  const authUrl = buildYandexOAuthUrl({ clientId, services, redirectUrl });
3588
3607
  console.log(`Если браузер не открылся, откройте ссылку вручную: ${authUrl}`);
3608
+ console.log("Ожидаю возврат токена из браузера...");
3589
3609
  try {
3590
3610
  await openUrl(authUrl);
3591
3611
  } catch (error) {
@@ -3628,6 +3648,13 @@ function getYandexConnectorConnectedAppIds(secrets = {}) {
3628
3648
  return apps;
3629
3649
  }
3630
3650
 
3651
+ function hasYandexOAuthAppToken(secrets = {}, appId = "core") {
3652
+ if (process.env.YANDEX_OAUTH_TOKEN) return true;
3653
+ if (secrets.yandex?.oauthApps?.[appId]?.token) return true;
3654
+ if (appId === "core" && (secrets.yandex?.oauthToken || secrets.cloud?.["yandex-disk"]?.token)) return true;
3655
+ return false;
3656
+ }
3657
+
3631
3658
  function isYandexConnectorFullyConnected(secrets = {}) {
3632
3659
  if (process.env.YANDEX_OAUTH_TOKEN) return true;
3633
3660
  const connected = getYandexConnectorConnectedAppIds(secrets);
@@ -11510,7 +11537,7 @@ function parseOptions(args) {
11510
11537
 
11511
11538
  for (let index = 0; index < args.length; index += 1) {
11512
11539
  const arg = args[index];
11513
- if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--stream-json" || arg === "--stdio" || arg === "--system" || arg === "--headed" || arg === "--headless" || arg === "--no-history" || arg === "--summary" || arg === "--all" || arg === "--full" || arg === "--unread" || arg === "--once" || arg === "--local" || arg === "--cache" || arg === "--tools" || arg === "--files" || arg === "--plan" || arg === "--trace" || arg === "--diff" || arg === "--stage" || arg === "--fts" || arg === "--bare" || arg === "--quiet" || arg === "--optional" || arg === "--project" || arg === "--dry-run" || arg === "--no-color" || arg === "--fail-on-empty" || arg === "--debug" || arg === "--fix" || arg === "--append" || arg === "--preserve-active" || arg === "--open" || arg === "--print-url") {
11540
+ if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--stream-json" || arg === "--stdio" || arg === "--system" || arg === "--headed" || arg === "--headless" || arg === "--no-history" || arg === "--summary" || arg === "--all" || arg === "--full" || arg === "--unread" || arg === "--once" || arg === "--local" || arg === "--cache" || arg === "--tools" || arg === "--files" || arg === "--plan" || arg === "--trace" || arg === "--diff" || arg === "--stage" || arg === "--fts" || arg === "--bare" || arg === "--quiet" || arg === "--optional" || arg === "--project" || arg === "--dry-run" || arg === "--no-color" || arg === "--fail-on-empty" || arg === "--debug" || arg === "--fix" || arg === "--force" || arg === "--append" || arg === "--preserve-active" || arg === "--open" || arg === "--print-url") {
11514
11541
  result[arg.slice(2)] = true;
11515
11542
  } else if (arg === "--check" || arg === "--upgrade-node") {
11516
11543
  result.check = true;
@@ -7,6 +7,7 @@ const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
7
7
  const binPath = resolve(rootDir, "bin", "iola.js");
8
8
  const packageJson = JSON.parse(await readFile(resolve(rootDir, "package.json"), "utf8"));
9
9
  const cliSource = await readFile(resolve(rootDir, "src", "cli.js"), "utf8");
10
+ const postinstallSource = await readFile(resolve(rootDir, "bin", "postinstall.js"), "utf8");
10
11
 
11
12
  function runCli(args) {
12
13
  return new Promise((resolvePromise, reject) => {
@@ -69,13 +70,19 @@ assertIncludes(cliSource, "IOLA_YANDEX_OAUTH_CLIENT_ID", "Yandex setup should us
69
70
  assertIncludes(cliSource, "IOLA_YANDEX_ORGANIZER_OAUTH_CLIENT_ID", "Yandex setup should support the organizer OAuth app group");
70
71
  assertIncludes(cliSource, "addressbook:all", "Yandex contacts should use the addressbook OAuth scope");
71
72
  assertIncludes(cliSource, "Автоматический прием OAuth-токена не сработал", "Yandex OAuth should provide manual token fallback");
73
+ assertIncludes(cliSource, "уже подключена, пропускаю", "Yandex setup should skip already connected OAuth app groups");
74
+ assertIncludes(cliSource, "IOLA_YANDEX_OAUTH_DEBUG", "Yandex OAuth callback should have debug logging");
72
75
  assertIncludes(cliSource, "partial (", "Yandex connector status should report partial connections");
76
+ assertIncludes(cliSource, "hasYandexOAuthAppToken", "Yandex setup should detect tokens per OAuth app");
73
77
  assertIncludes(cliSource, "isYandexConnectorFullyConnected", "Yandex master status should require all OAuth app tokens");
74
78
  assertIncludes(cliSource, "--app", "Yandex token command should persist tokens by OAuth app group");
75
79
  assertNotIncludes(cliSource, "Сервисы через запятую [identity,disk]", "Yandex setup should not ask for services during connector setup");
76
80
  if (!packageJson.files.includes("docs/assets/iola-oauth-icon.png")) {
77
81
  throw new Error("package files should include the Yandex OAuth icon");
78
82
  }
83
+ assertIncludes(postinstallSource, "process.hrtime.bigint()", "postinstall should use a monotonic timer");
84
+ assertIncludes(postinstallSource, "без скачивания и распаковки npm-пакета", "postinstall timing should not imply full npm install time");
85
+ assertIncludes(postinstallSource, "IOLA CLI готова за", "postinstall should print total setup duration");
79
86
 
80
87
  const commands = await runCli(["commands"]);
81
88
  assertIncludes(commands, "iola browser status|install|open|text|html|screenshot|pdf|click|type|eval", "commands");