@getxflow/cli 0.1.0 → 0.1.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/README.md CHANGED
@@ -21,6 +21,8 @@ xflow publish
21
21
  | `status` / `push` / `pull` | состояние, отправка и получение исходников |
22
22
  | `deploy` / `publish` / `rollback` / `deployments` | сборка, публикация, откат, история версий |
23
23
  | `functions list` / `functions deploy` | облачные функции проекта из `functions/<имя>/index.ts` |
24
+ | `functions invoke` / `functions logs` | вызвать функцию, посмотреть её падения со стеком |
25
+ | `logs` | ошибки выложенного приложения в браузере |
24
26
  | `projects list` / `projects get` / `open` | проекты организации и адреса приложения |
25
27
  | `skills` | инструкция о платформе для ИИ-агента (формат [Agent Skills](https://agentskills.io)) |
26
28
 
package/dist/bin.js CHANGED
@@ -12,6 +12,7 @@ const version_1 = require("./version");
12
12
  const auth_1 = require("./commands/auth");
13
13
  const projects_1 = require("./commands/projects");
14
14
  const functions_1 = require("./commands/functions");
15
+ const logs_1 = require("./commands/logs");
15
16
  const skills_1 = require("./commands/skills");
16
17
  const sources_1 = require("./commands/sources");
17
18
  const deploy_1 = require("./commands/deploy");
@@ -65,11 +66,22 @@ async function run(args) {
65
66
  await (0, functions_1.functionsDeploy)(rest);
66
67
  return;
67
68
  }
69
+ if (second === 'invoke') {
70
+ await (0, functions_1.functionsInvoke)(rest);
71
+ return;
72
+ }
73
+ if (second === 'logs') {
74
+ await (0, logs_1.functionsLogs)(rest);
75
+ return;
76
+ }
68
77
  if (second === undefined || second === 'list') {
69
78
  await (0, functions_1.functionsList)();
70
79
  return;
71
80
  }
72
- throw new errors_1.CliError(`Неизвестная команда: functions ${second}`, 'Есть list и deploy');
81
+ throw new errors_1.CliError(`Неизвестная команда: functions ${second}`, 'Есть list, deploy, invoke и logs');
82
+ case 'logs':
83
+ await (0, logs_1.logs)(rest);
84
+ return;
73
85
  case 'status':
74
86
  await (0, sources_1.status)();
75
87
  return;
@@ -1,10 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.functionsList = functionsList;
4
+ exports.functionsInvoke = functionsInvoke;
4
5
  exports.functionsDeploy = functionsDeploy;
5
6
  const node_fs_1 = require("node:fs");
6
7
  const node_path_1 = require("node:path");
7
8
  const api_1 = require("../api");
9
+ const args_1 = require("../args");
8
10
  const config_1 = require("../config");
9
11
  const errors_1 = require("../errors");
10
12
  const session_1 = require("../session");
@@ -77,6 +79,64 @@ async function functionsList() {
77
79
  fn.error_message ?? fn.invoke_url ?? '',
78
80
  ]));
79
81
  }
82
+ /** Ответ функции: JSON разворачиваем, прочее отдаём как есть. */
83
+ function prettyBody(text) {
84
+ try {
85
+ return JSON.stringify(JSON.parse(text), null, 2);
86
+ }
87
+ catch {
88
+ return text;
89
+ }
90
+ }
91
+ /**
92
+ * Вызвать функцию так же, как её вызывает приложение.
93
+ *
94
+ * Токен проекта берём из карточки на платформе, а не из локального `.env`: в
95
+ * свежем клоне файла нет вовсе, а команда должна работать сразу после link.
96
+ */
97
+ async function functionsInvoke(args) {
98
+ const { config } = (0, config_1.requireProject)();
99
+ const client = (0, session_1.connect)(config);
100
+ const name = args.words[1];
101
+ if (!name)
102
+ throw new errors_1.CliError('Нужно имя функции', 'Что выложено: xflow functions list');
103
+ const card = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}`);
104
+ const fn = card.functions.find((item) => item.name === name);
105
+ if (!fn || !fn.invoke_url) {
106
+ throw new errors_1.CliError(`Функция ${name} не выложена`, card.functions.length > 0
107
+ ? `Выложены: ${card.functions.map((item) => item.name).join(', ')}`
108
+ : `Выкатить: xflow functions deploy ${name}`);
109
+ }
110
+ const data = (0, args_1.flagString)(args, 'data');
111
+ const method = ((0, args_1.flagString)(args, 'method') ?? (data ? 'POST' : 'GET')).toUpperCase();
112
+ const sendsBody = method !== 'GET' && method !== 'HEAD';
113
+ const started = Date.now();
114
+ let response;
115
+ try {
116
+ response = await fetch(fn.invoke_url, {
117
+ method,
118
+ headers: {
119
+ 'Content-Type': 'application/json',
120
+ 'X-Project-Token': card.project_token ?? '',
121
+ },
122
+ body: sendsBody ? (data ?? '{}') : undefined,
123
+ // У функции свой потолок в 30 секунд: ждём чуть дольше, чтобы увидеть её
124
+ // собственный таймаут, а не свой.
125
+ signal: AbortSignal.timeout(40_000),
126
+ });
127
+ }
128
+ catch (e) {
129
+ throw new errors_1.CliError(`Функция не ответила: ${e instanceof Error ? e.message : String(e)}`, 'Проверьте, что она выложена: xflow functions list');
130
+ }
131
+ const elapsed = Date.now() - started;
132
+ const text = await response.text();
133
+ (0, ui_1.note)((0, ui_1.dim)(`${response.status} ${response.statusText} за ${elapsed} мс`));
134
+ if (text)
135
+ (0, ui_1.out)(prettyBody(text));
136
+ if (!response.ok) {
137
+ throw new errors_1.CliError(`Функция ответила ${response.status}`, `Стек и вывод консоли: xflow functions logs ${name}`);
138
+ }
139
+ }
80
140
  async function functionsDeploy(args) {
81
141
  const { root, config } = (0, config_1.requireProject)();
82
142
  const client = (0, session_1.connect)(config);
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.logs = logs;
4
+ exports.functionsLogs = functionsLogs;
5
+ const api_1 = require("../api");
6
+ const args_1 = require("../args");
7
+ const config_1 = require("../config");
8
+ const session_1 = require("../session");
9
+ const ui_1 = require("../ui");
10
+ const DEFAULT_LIMIT = 10;
11
+ /** Стек с хвостом консоли бывает длинным: показываем начало, оно и есть причина. */
12
+ const STACK_MAX_LINES = 14;
13
+ function stamp(iso) {
14
+ const date = new Date(iso);
15
+ const pad = (value) => String(value).padStart(2, '0');
16
+ return `${pad(date.getDate())}.${pad(date.getMonth() + 1)} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
17
+ }
18
+ async function fetchLogs(source, name, args) {
19
+ const { config } = (0, config_1.requireProject)();
20
+ const client = (0, session_1.connect)(config);
21
+ const query = new URLSearchParams({ source, limit: String((0, args_1.flagNumber)(args, 'limit') ?? DEFAULT_LIMIT) });
22
+ if (name)
23
+ query.set('name', name);
24
+ const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/logs?${query.toString()}`);
25
+ return data.logs;
26
+ }
27
+ /** Свежее — внизу: так последняя ошибка оказывается перед глазами, а не уезжает вверх. */
28
+ function render(rows) {
29
+ for (const row of [...rows].reverse()) {
30
+ const label = row.source === 'function' ? (row.function ?? 'функция') : 'браузер';
31
+ (0, ui_1.out)(`${(0, ui_1.dim)(stamp(row.timestamp))} ${(0, ui_1.bold)(label)} ${row.message}`);
32
+ if (row.stack) {
33
+ const lines = row.stack.split('\n');
34
+ for (const line of lines.slice(0, STACK_MAX_LINES))
35
+ (0, ui_1.out)((0, ui_1.dim)(` ${line}`));
36
+ if (lines.length > STACK_MAX_LINES)
37
+ (0, ui_1.out)((0, ui_1.dim)(` … ещё ${lines.length - STACK_MAX_LINES} строк`));
38
+ }
39
+ (0, ui_1.out)('');
40
+ }
41
+ }
42
+ async function logs(args) {
43
+ const rows = await fetchLogs('client', undefined, args);
44
+ if (rows.length === 0) {
45
+ (0, ui_1.note)('Ошибок из браузера нет');
46
+ (0, ui_1.note)((0, ui_1.dim)(' Сюда попадают падения выложенного приложения, а не локального npm run dev'));
47
+ return;
48
+ }
49
+ render(rows);
50
+ }
51
+ async function functionsLogs(args) {
52
+ const name = args.words[1];
53
+ const rows = await fetchLogs('function', name, args);
54
+ if (rows.length === 0) {
55
+ (0, ui_1.note)(name ? `Функция ${name} не падала` : 'Функции не падали');
56
+ (0, ui_1.note)((0, ui_1.dim)(' Сюда попадают только неудачные вызовы: успешные ничего не пишут'));
57
+ return;
58
+ }
59
+ render(rows);
60
+ }
package/dist/help.js CHANGED
@@ -37,8 +37,12 @@ ${(0, ui_1.bold)('Выкладка')}
37
37
  ${(0, ui_1.bold)('Функции')}
38
38
  xflow functions list что выложено
39
39
  xflow functions deploy [имя] собрать и выкатить функцию
40
+ xflow functions invoke <имя> [--data '{"a":1}']
41
+ вызвать функцию и показать ответ
42
+ xflow functions logs [имя] падения функций: стек и вывод консоли
40
43
 
41
44
  ${(0, ui_1.bold)('Справочное')}
45
+ xflow logs [--limit N] ошибки выложенного приложения в браузере
42
46
  xflow projects list проекты организации
43
47
  xflow projects get [id] карточка проекта
44
48
  xflow open [--live] открыть приложение в браузере
@@ -52,6 +56,35 @@ ${(0, ui_1.bold)('Окружение')}
52
56
  Подробнее о команде: xflow help <команда>`);
53
57
  }
54
58
  const TOPICS = {
59
+ logs: `${(0, ui_1.bold)('xflow logs')} и ${(0, ui_1.bold)('xflow functions logs')} — ошибки проекта
60
+
61
+ Один поток на проект: падения выложенного приложения в браузере и падения
62
+ облачных функций. Хранятся последние 200 записей, дальше вытесняются.
63
+
64
+ --limit N сколько показать (по умолчанию 10)
65
+
66
+ Функция сообщает о падении сама: свои логи Yandex наружу отдаёт только по gRPC,
67
+ поэтому обёртка ловит исключение, уносит вместе с ним хвост консоли (последние 40
68
+ строк ${(0, ui_1.bold)('console.log')} этого вызова) и отправляет на платформу. Успешные вызовы не
69
+ пишут ничего: иначе каждый запрос платил бы за это задержкой.
70
+
71
+ Не попадёт сюда: падение на старте модуля (функция не успевает дойти до обёртки),
72
+ превышение 30 секунд и нехватка памяти. Такое видно ответом на
73
+ ${(0, ui_1.bold)('xflow functions invoke')}.
74
+
75
+ Ошибки браузера собирает ${(0, ui_1.bold)('src/utils/error-logger.ts')} шаблона и только с
76
+ выложенных адресов: локальный ${(0, ui_1.bold)('npm run dev')} сюда не пишет.`,
77
+ invoke: `${(0, ui_1.bold)('xflow functions invoke')} <имя> — вызвать функцию
78
+
79
+ Вызывает так же, как её вызывает приложение: с заголовком X-Project-Token.
80
+ Токен берётся из карточки проекта на платформе, локальный .env не нужен.
81
+
82
+ --data '{"a":1}' тело запроса (по умолчанию метод становится POST)
83
+ --method GET другой метод
84
+
85
+ Печатает статус, время ответа и тело. Ненулевой код возврата на статусе 4xx и
86
+ 5xx: в CI такой вызов должен ронять шаг. Причину падения показывает
87
+ ${(0, ui_1.bold)('xflow functions logs <имя>')}.`,
55
88
  skills: `${(0, ui_1.bold)('xflow skills')} — инструкция о платформе для ИИ-агента
56
89
 
57
90
  Кладёт файл в формате Agent Skills (agentskills.io): что такое XFlow, как выкладывать
package/dist/version.js CHANGED
@@ -6,6 +6,6 @@ exports.DEFAULT_API_URL = exports.CLI_VERSION = void 0;
6
6
  * рантайме нельзя, после сборки он лежит на уровень выше dist и в бандл не
7
7
  * попадает.
8
8
  */
9
- exports.CLI_VERSION = '0.1.0';
9
+ exports.CLI_VERSION = '0.1.1';
10
10
  /** Адрес платформы по умолчанию. Переопределяется XFLOW_API_URL и полем `api` в xflow.json. */
11
11
  exports.DEFAULT_API_URL = 'https://app.getxflow.com';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getxflow/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "CLI платформы XFlow: синхронизация кода, деплой и публикация приложений",
5
5
  "license": "UNLICENSED",
6
6
  "engines": {
@@ -43,6 +43,12 @@ with `xflow functions deploy` (one name to deploy a single function, no name for
43
43
  list what is live with `xflow functions list`. The handler returns
44
44
  `{ statusCode, body }` where `body` is a JSON string.
45
45
 
46
+ Debugging a deployed function is two commands: `xflow functions invoke <name>` calls it
47
+ the way the app does and prints status, timing and body (`--data '{"a":1}'` sends a body),
48
+ and `xflow functions logs <name>` shows the failures, each with its stack and the console
49
+ output of that call. Only failed calls are logged, so an empty output means the function
50
+ never crashed, not that logging is broken.
51
+
46
52
  Calls from the app must carry the `X-Project-Token` header; the token is already in
47
53
  the app environment as `VITE_XFLOW_PROJECT_TOKEN`. Treat a function as a public API:
48
54
  that token ships inside the frontend bundle.
@@ -89,9 +95,13 @@ list of organization members: that is what `user-picker` is built on.
89
95
 
90
96
  ## Errors from a deployed app
91
97
 
92
- Client-side failures on deployed addresses show up in the platform: open the project,
93
- section «Облако» (Cloud), tab «Логи» (Logs). Unhandled errors, rejected promises and
94
- 5xx responses land there. The last 200 records per project are kept.
98
+ `xflow logs` prints what broke in the browser on deployed addresses: unhandled errors,
99
+ rejected promises and 5xx responses. The same stream, functions included, is in the
100
+ platform UI: open the project, section «Облако» (Cloud), tab «Логи» (Logs). The last 200
101
+ records per project are kept.
102
+
103
+ Local `npm run dev` does not report anything: these logs exist for what you cannot open
104
+ in your own devtools.
95
105
 
96
106
  ## Environment
97
107