@iola_adm/iola-cli 0.2.17 → 0.2.19
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 +1 -1
- package/bin/postinstall.js +16 -8
- package/package.json +1 -1
- package/src/cli.js +78 -15
- package/test/smoke-test.js +7 -0
- package/wiki/Yandex-Connector.md +3 -0
package/README.md
CHANGED
|
@@ -199,7 +199,7 @@ iola yandex status
|
|
|
199
199
|
|
|
200
200
|
Yandex Connector использует две встроенные OAuth-группы: `IOLA CLI A` для Yandex ID, Диска, Почты и документов через Диск; `IOLA CLI B` для Календаря, Контактов и Телемоста через календарь. Такси, Маркет и Доставка записаны в backlog только как сценарии подготовки ссылки/маршрута/списка без заказа и оплаты.
|
|
201
201
|
|
|
202
|
-
В `/yandex` функции выбираются номерами через запятую, как в мастере настройки. Там же есть пункт `Удалить подключение-коннектор`, который чистит локальные токены и настройки Yandex Connector. OAuth-права сами по себе не создают функциональность: под каждый сервис нужны отдельные команды и тулы.
|
|
202
|
+
В `/yandex` функции выбираются номерами через запятую, как в мастере настройки. Там же есть пункт `Удалить подключение-коннектор`, который чистит локальные токены и настройки Yandex Connector. Мастер считает коннектор готовым только после токенов обеих групп. OAuth-права сами по себе не создают функциональность: под каждый сервис нужны отдельные команды и тулы.
|
|
203
203
|
|
|
204
204
|
Инструкция: [Yandex Connector](https://github.com/adm-iola/iola-cli/wiki/Yandex-Connector).
|
|
205
205
|
|
package/bin/postinstall.js
CHANGED
|
@@ -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(
|
|
46
|
+
console.log(`IOLA CLI готова за ${formatDuration(elapsedMs(setupStarted))}. Запуск: iola`);
|
|
45
47
|
|
|
46
48
|
async function runStep(step, current, total) {
|
|
47
|
-
const started =
|
|
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.
|
|
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(
|
|
80
|
+
process.stdout.write(`\r✓ ${prefix} готово за ${formatDuration(elapsedMs(started))}\n`);
|
|
79
81
|
} else {
|
|
80
|
-
console.log(`✓ ${prefix} готово за ${formatDuration(
|
|
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
|
-
|
|
119
|
-
|
|
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
package/src/cli.js
CHANGED
|
@@ -2019,7 +2019,7 @@ async function doctor(args = []) {
|
|
|
2019
2019
|
openaiKey: process.env.OPENAI_API_KEY ? "env" : secrets.openai?.apiKey ? "local" : "missing",
|
|
2020
2020
|
openrouterKey: process.env.OPENROUTER_API_KEY ? "env" : secrets.openrouter?.apiKey ? "local" : "missing",
|
|
2021
2021
|
yandexGeocoderKey: (process.env.YANDEX_GEOCODER_API_KEY || process.env.YANDEX_MAPS_API_KEY) ? "env" : secrets.yandexGeocoder?.apiKey ? "local" : "missing",
|
|
2022
|
-
yandexConnector: (
|
|
2022
|
+
yandexConnector: getYandexConnectorSecretStatus(secrets),
|
|
2023
2023
|
yandexAuthorized: config.yandex?.authorizedServices?.join(", ") || "-",
|
|
2024
2024
|
yandexServices: config.yandex?.enabledServices?.join(", ") || (secrets.cloud?.["yandex-disk"]?.token ? "disk (legacy cloud token)" : "-"),
|
|
2025
2025
|
ollama: diagnostics.ollama.installed ? diagnostics.ollama.version : "not-installed",
|
|
@@ -3465,7 +3465,16 @@ function getYandexConnectorRedirectUrl() {
|
|
|
3465
3465
|
}
|
|
3466
3466
|
|
|
3467
3467
|
async function runYandexBrowserOAuth({ appId = "core", clientId, services, redirectUrl }) {
|
|
3468
|
-
|
|
3468
|
+
let token = "";
|
|
3469
|
+
try {
|
|
3470
|
+
token = await waitForYandexOAuthToken({ clientId, services, redirectUrl });
|
|
3471
|
+
} catch (error) {
|
|
3472
|
+
if (!process.stdin.isTTY || !String(error?.message || error).includes("Время ожидания")) throw error;
|
|
3473
|
+
console.log("Автоматический прием OAuth-токена не сработал.");
|
|
3474
|
+
console.log("Если в адресной строке браузера есть access_token, вставьте только значение access_token.");
|
|
3475
|
+
token = (await askText("Yandex OAuth access_token [Enter - пропустить]: ")).trim();
|
|
3476
|
+
if (!token) throw error;
|
|
3477
|
+
}
|
|
3469
3478
|
await setYandexConnectorToken(["--token", token, "--app", appId]);
|
|
3470
3479
|
console.log("Yandex Connector подключен.");
|
|
3471
3480
|
}
|
|
@@ -3474,10 +3483,28 @@ function waitForYandexOAuthToken({ clientId, services, redirectUrl }) {
|
|
|
3474
3483
|
return new Promise((resolvePromise, reject) => {
|
|
3475
3484
|
let settled = false;
|
|
3476
3485
|
const timeoutMs = 180000;
|
|
3486
|
+
const finish = (token) => {
|
|
3487
|
+
if (!token) throw new Error("Yandex OAuth token не получен.");
|
|
3488
|
+
if (!settled) {
|
|
3489
|
+
settled = true;
|
|
3490
|
+
clearTimeout(timer);
|
|
3491
|
+
resolvePromise(String(token));
|
|
3492
|
+
}
|
|
3493
|
+
};
|
|
3477
3494
|
const server = createServer(async (req, res) => {
|
|
3478
3495
|
try {
|
|
3479
3496
|
const url = new URL(req.url || "/", redirectUrl);
|
|
3480
3497
|
if (url.pathname === YANDEX_CONNECTOR_REDIRECT_PATH && req.method === "GET") {
|
|
3498
|
+
const tokenFromQuery = url.searchParams.get("access_token") || url.searchParams.get("token");
|
|
3499
|
+
const errorFromQuery = url.searchParams.get("error") || "";
|
|
3500
|
+
if (errorFromQuery) throw new Error(`Yandex OAuth error: ${errorFromQuery}`);
|
|
3501
|
+
if (tokenFromQuery) {
|
|
3502
|
+
finish(tokenFromQuery);
|
|
3503
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
3504
|
+
res.end("<p>Yandex Connector подключен. Можно закрыть вкладку и вернуться в терминал.</p>");
|
|
3505
|
+
server.close();
|
|
3506
|
+
return;
|
|
3507
|
+
}
|
|
3481
3508
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
3482
3509
|
res.end(`<!doctype html>
|
|
3483
3510
|
<html lang="ru"><head><meta charset="utf-8"><title>IOLA Yandex Connector</title></head>
|
|
@@ -3485,33 +3512,45 @@ function waitForYandexOAuthToken({ clientId, services, redirectUrl }) {
|
|
|
3485
3512
|
<p>Передаю токен в iola-cli...</p>
|
|
3486
3513
|
<script>
|
|
3487
3514
|
(async () => {
|
|
3488
|
-
const params = new URLSearchParams(location.hash.replace(/^#/, ""));
|
|
3515
|
+
const params = new URLSearchParams(location.hash.replace(/^#/, "") || location.search.replace(/^\\?/, ""));
|
|
3489
3516
|
const token = params.get("access_token");
|
|
3490
3517
|
const error = params.get("error") || "";
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3518
|
+
const qs = new URLSearchParams({ token: token || "", error }).toString();
|
|
3519
|
+
try {
|
|
3520
|
+
await fetch("/yandex/oauth/token", {
|
|
3521
|
+
method: "POST",
|
|
3522
|
+
headers: { "content-type": "application/json" },
|
|
3523
|
+
body: JSON.stringify({ token, error })
|
|
3524
|
+
});
|
|
3525
|
+
} catch {
|
|
3526
|
+
location.replace("/yandex/oauth/token?" + qs);
|
|
3527
|
+
return;
|
|
3528
|
+
}
|
|
3496
3529
|
document.body.innerHTML = token
|
|
3497
3530
|
? "<p>Yandex Connector подключен. Можно закрыть вкладку и вернуться в терминал.</p>"
|
|
3498
3531
|
: "<p>Не удалось получить токен. Вернитесь в терминал.</p>";
|
|
3532
|
+
if (!token || error) location.replace("/yandex/oauth/token?" + qs);
|
|
3499
3533
|
})();
|
|
3500
3534
|
</script>
|
|
3501
3535
|
</body></html>`);
|
|
3502
3536
|
return;
|
|
3503
3537
|
}
|
|
3538
|
+
if (url.pathname === "/yandex/oauth/token" && req.method === "GET") {
|
|
3539
|
+
const token = url.searchParams.get("token") || url.searchParams.get("access_token");
|
|
3540
|
+
const error = url.searchParams.get("error") || "";
|
|
3541
|
+
if (error) throw new Error(`Yandex OAuth error: ${error}`);
|
|
3542
|
+
finish(token);
|
|
3543
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
3544
|
+
res.end("<p>Yandex Connector подключен. Можно закрыть вкладку и вернуться в терминал.</p>");
|
|
3545
|
+
server.close();
|
|
3546
|
+
return;
|
|
3547
|
+
}
|
|
3504
3548
|
if (url.pathname === "/yandex/oauth/token" && req.method === "POST") {
|
|
3505
3549
|
const chunks = [];
|
|
3506
3550
|
for await (const chunk of req) chunks.push(chunk);
|
|
3507
3551
|
const payload = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
|
|
3508
3552
|
if (payload.error) throw new Error(`Yandex OAuth error: ${payload.error}`);
|
|
3509
|
-
|
|
3510
|
-
if (!settled) {
|
|
3511
|
-
settled = true;
|
|
3512
|
-
clearTimeout(timer);
|
|
3513
|
-
resolvePromise(String(payload.token));
|
|
3514
|
-
}
|
|
3553
|
+
finish(payload.token);
|
|
3515
3554
|
res.writeHead(200, { "content-type": "application/json" });
|
|
3516
3555
|
res.end(JSON.stringify({ ok: true }));
|
|
3517
3556
|
server.close();
|
|
@@ -3582,6 +3621,30 @@ function getYandexOAuthAppById(appId) {
|
|
|
3582
3621
|
|| null;
|
|
3583
3622
|
}
|
|
3584
3623
|
|
|
3624
|
+
function getYandexConnectorConnectedAppIds(secrets = {}) {
|
|
3625
|
+
const apps = new Set(Object.keys(secrets.yandex?.oauthApps || {}));
|
|
3626
|
+
if (secrets.yandex?.oauthToken) apps.add("core");
|
|
3627
|
+
if (secrets.cloud?.["yandex-disk"]?.token) apps.add("core");
|
|
3628
|
+
return apps;
|
|
3629
|
+
}
|
|
3630
|
+
|
|
3631
|
+
function isYandexConnectorFullyConnected(secrets = {}) {
|
|
3632
|
+
if (process.env.YANDEX_OAUTH_TOKEN) return true;
|
|
3633
|
+
const connected = getYandexConnectorConnectedAppIds(secrets);
|
|
3634
|
+
const required = getConfiguredYandexOAuthApps().map((app) => app.id);
|
|
3635
|
+
return required.length > 0 && required.every((id) => connected.has(id));
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3638
|
+
function getYandexConnectorSecretStatus(secrets = {}) {
|
|
3639
|
+
if (process.env.YANDEX_OAUTH_TOKEN) return "env";
|
|
3640
|
+
const connected = getYandexConnectorConnectedAppIds(secrets);
|
|
3641
|
+
const required = getConfiguredYandexOAuthApps().map((app) => app.id);
|
|
3642
|
+
if (required.length === 0) return connected.size ? "local" : "missing";
|
|
3643
|
+
if (required.every((id) => connected.has(id))) return "ready";
|
|
3644
|
+
if (connected.size > 0) return `partial (${[...connected].join(", ")})`;
|
|
3645
|
+
return "missing";
|
|
3646
|
+
}
|
|
3647
|
+
|
|
3585
3648
|
async function setYandexConnectorToken(args = []) {
|
|
3586
3649
|
const options = parseOptions(args);
|
|
3587
3650
|
const appId = options.app || "core";
|
|
@@ -11402,7 +11465,7 @@ async function getOnboardComponentStatus() {
|
|
|
11402
11465
|
browser: browser.installed === "yes",
|
|
11403
11466
|
"yandex-geocoder": Boolean(yandexGeocoderKey),
|
|
11404
11467
|
cloud: Object.keys(cloudSecrets).length > 0,
|
|
11405
|
-
yandex:
|
|
11468
|
+
yandex: isYandexConnectorFullyConnected(secrets),
|
|
11406
11469
|
};
|
|
11407
11470
|
}
|
|
11408
11471
|
|
package/test/smoke-test.js
CHANGED
|
@@ -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) => {
|
|
@@ -68,11 +69,17 @@ assertIncludes(cliSource, "runYandexBrowserOAuth", "Yandex setup should support
|
|
|
68
69
|
assertIncludes(cliSource, "IOLA_YANDEX_OAUTH_CLIENT_ID", "Yandex setup should use a packaged/env OAuth client id");
|
|
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");
|
|
72
|
+
assertIncludes(cliSource, "Автоматический прием OAuth-токена не сработал", "Yandex OAuth should provide manual token fallback");
|
|
73
|
+
assertIncludes(cliSource, "partial (", "Yandex connector status should report partial connections");
|
|
74
|
+
assertIncludes(cliSource, "isYandexConnectorFullyConnected", "Yandex master status should require all OAuth app tokens");
|
|
71
75
|
assertIncludes(cliSource, "--app", "Yandex token command should persist tokens by OAuth app group");
|
|
72
76
|
assertNotIncludes(cliSource, "Сервисы через запятую [identity,disk]", "Yandex setup should not ask for services during connector setup");
|
|
73
77
|
if (!packageJson.files.includes("docs/assets/iola-oauth-icon.png")) {
|
|
74
78
|
throw new Error("package files should include the Yandex OAuth icon");
|
|
75
79
|
}
|
|
80
|
+
assertIncludes(postinstallSource, "process.hrtime.bigint()", "postinstall should use a monotonic timer");
|
|
81
|
+
assertIncludes(postinstallSource, "без скачивания и распаковки npm-пакета", "postinstall timing should not imply full npm install time");
|
|
82
|
+
assertIncludes(postinstallSource, "IOLA CLI готова за", "postinstall should print total setup duration");
|
|
76
83
|
|
|
77
84
|
const commands = await runCli(["commands"]);
|
|
78
85
|
assertIncludes(commands, "iola browser status|install|open|text|html|screenshot|pdf|click|type|eval", "commands");
|
package/wiki/Yandex-Connector.md
CHANGED
|
@@ -76,6 +76,7 @@ iola yandex setup
|
|
|
76
76
|
- `calendar:all`;
|
|
77
77
|
- `addressbook:all`.
|
|
78
78
|
4. После каждой успешной авторизации браузер вернется на локальную страницу `iola-cli`, а CLI сам сохранит OAuth-токен нужной группы.
|
|
79
|
+
Если браузер не смог передать токен обратно в CLI автоматически, CLI попросит вставить `access_token` вручную. Вставлять нужно только значение параметра `access_token`, без всего URL.
|
|
79
80
|
5. Если автоматический браузерный flow недоступен, используйте fallback для разработки:
|
|
80
81
|
|
|
81
82
|
```bash
|
|
@@ -98,6 +99,8 @@ iola yandex menu
|
|
|
98
99
|
Меню `/yandex` работает как мастер настройки: сервисы выбираются номерами через запятую, а не вводом технических названий.
|
|
99
100
|
В этом же меню есть отдельный пункт `Удалить подключение-коннектор`: он удаляет локальные токены и настройки Yandex Connector, чтобы можно было подключить другой Яндекс-аккаунт или отказаться от сервисов Яндекса.
|
|
100
101
|
|
|
102
|
+
В мастере настройки Yandex Connector считается готовым только после сохранения токенов обеих групп: `IOLA CLI A` и `IOLA CLI B`.
|
|
103
|
+
|
|
101
104
|
7. Проверьте:
|
|
102
105
|
|
|
103
106
|
```bash
|