@getxflow/cli 0.0.1 → 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/dist/ui.js ADDED
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.dim = exports.bold = void 0;
4
+ exports.out = out;
5
+ exports.note = note;
6
+ exports.step = step;
7
+ exports.ok = ok;
8
+ exports.warn = warn;
9
+ exports.fail = fail;
10
+ exports.table = table;
11
+ exports.formatBytes = formatBytes;
12
+ exports.formatAge = formatAge;
13
+ exports.confirmWord = confirmWord;
14
+ const node_readline_1 = require("node:readline");
15
+ /**
16
+ * Вывод CLI.
17
+ *
18
+ * Всё пишется через один модуль, а не через console: человек читает stdout,
19
+ * машина парсит его же, а служебные сообщения (шаги, предупреждения) обязаны
20
+ * уходить в stderr, иначе `xflow pull > file` сложит их в файл вместе с данными.
21
+ */
22
+ const color = process.stdout.isTTY === true && !process.env.NO_COLOR;
23
+ function paint(code, text) {
24
+ return color ? `\u001b[${code}m${text}\u001b[0m` : text;
25
+ }
26
+ const bold = (t) => paint('1', t);
27
+ exports.bold = bold;
28
+ const dim = (t) => paint('2', t);
29
+ exports.dim = dim;
30
+ const red = (t) => paint('31', t);
31
+ const green = (t) => paint('32', t);
32
+ const yellow = (t) => paint('33', t);
33
+ /** Данные: то, ради чего команду запускали. */
34
+ function out(line = '') {
35
+ process.stdout.write(`${line}\n`);
36
+ }
37
+ /** Ход работы: шаги, предупреждения, ошибки. */
38
+ function note(line = '') {
39
+ process.stderr.write(`${line}\n`);
40
+ }
41
+ function step(message) {
42
+ note((0, exports.dim)(`→ ${message}`));
43
+ }
44
+ function ok(message) {
45
+ note(green(`✓ ${message}`));
46
+ }
47
+ function warn(message) {
48
+ note(yellow(`! ${message}`));
49
+ }
50
+ function fail(message) {
51
+ note(red(`✗ ${message}`));
52
+ }
53
+ /** Таблица с выравниванием по колонкам. Заголовок не рисуем: он не нужен на 3 строки. */
54
+ function table(rows) {
55
+ if (rows.length === 0)
56
+ return;
57
+ const widths = [];
58
+ for (const row of rows) {
59
+ row.forEach((cell, i) => {
60
+ widths[i] = Math.max(widths[i] ?? 0, cell.length);
61
+ });
62
+ }
63
+ for (const row of rows) {
64
+ out(row.map((cell, i) => (i === row.length - 1 ? cell : cell.padEnd(widths[i]))).join(' '));
65
+ }
66
+ }
67
+ function formatBytes(bytes) {
68
+ if (bytes < 1024)
69
+ return `${bytes} Б`;
70
+ if (bytes < 1024 * 1024)
71
+ return `${(bytes / 1024).toFixed(1)} КБ`;
72
+ return `${(bytes / 1024 / 1024).toFixed(1)} МБ`;
73
+ }
74
+ function formatAge(iso) {
75
+ if (!iso)
76
+ return '—';
77
+ const ms = Date.now() - new Date(iso).getTime();
78
+ if (!Number.isFinite(ms) || ms < 0)
79
+ return '—';
80
+ const minutes = Math.floor(ms / 60000);
81
+ if (minutes < 1)
82
+ return 'только что';
83
+ if (minutes < 60)
84
+ return `${minutes} мин назад`;
85
+ const hours = Math.floor(minutes / 60);
86
+ if (hours < 24)
87
+ return `${hours} ч назад`;
88
+ return `${Math.floor(hours / 24)} дн назад`;
89
+ }
90
+ /**
91
+ * Подтверждение опасного действия.
92
+ *
93
+ * Требуем ввести конкретное слово, а не «y»: `--force` перетирает чужую работу,
94
+ * и подтверждение должно стоить осознанного действия. В неинтерактивном режиме
95
+ * (CI, запуск из агента) подтвердить нельзя вовсе — это сознательно (D23).
96
+ */
97
+ async function confirmWord(question, expected) {
98
+ if (!process.stdin.isTTY) {
99
+ fail('Подтверждение возможно только в интерактивном терминале');
100
+ note((0, exports.dim)(' В CI это правильное поведение: расхождение версий должно ронять сборку, а не перетирать чужую работу'));
101
+ return false;
102
+ }
103
+ const rl = (0, node_readline_1.createInterface)({ input: process.stdin, output: process.stderr });
104
+ try {
105
+ const answer = await new Promise((resolve) => {
106
+ rl.question(`${question}\n Введите «${expected}» для подтверждения: `, resolve);
107
+ });
108
+ return answer.trim() === expected;
109
+ }
110
+ finally {
111
+ rl.close();
112
+ }
113
+ }
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_API_URL = exports.CLI_VERSION = void 0;
4
+ /**
5
+ * Версия CLI. Держать синхронно с cli/package.json: читать package.json в
6
+ * рантайме нельзя, после сборки он лежит на уровень выше dist и в бандл не
7
+ * попадает.
8
+ */
9
+ exports.CLI_VERSION = '0.1.1';
10
+ /** Адрес платформы по умолчанию. Переопределяется XFLOW_API_URL и полем `api` в xflow.json. */
11
+ exports.DEFAULT_API_URL = 'https://app.getxflow.com';
package/dist/zip.js ADDED
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ZipError = void 0;
4
+ exports.zipCreate = zipCreate;
5
+ exports.zipRead = zipRead;
6
+ const node_zlib_1 = require("node:zlib");
7
+ class ZipError extends Error {
8
+ }
9
+ exports.ZipError = ZipError;
10
+ const LOCAL_SIG = 0x04034b50;
11
+ const CENTRAL_SIG = 0x02014b50;
12
+ const EOCD_SIG = 0x06054b50;
13
+ /** Флаг «имена в UTF-8»: без него кириллица в путях читается как мохибейк. */
14
+ const FLAG_UTF8 = 0x0800;
15
+ /** 1980-01-01 в формате DOS: время сборки в архив не пишем, чтобы он был воспроизводим. */
16
+ const DOS_DATE = 0x0021;
17
+ const DOS_TIME = 0x0000;
18
+ const CRC_TABLE = (() => {
19
+ const table = new Uint32Array(256);
20
+ for (let i = 0; i < 256; i++) {
21
+ let c = i;
22
+ for (let k = 0; k < 8; k++) {
23
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
24
+ }
25
+ table[i] = c >>> 0;
26
+ }
27
+ return table;
28
+ })();
29
+ function crc32(data) {
30
+ let crc = 0xffffffff;
31
+ for (let i = 0; i < data.length; i++) {
32
+ crc = CRC_TABLE[(crc ^ data[i]) & 0xff] ^ (crc >>> 8);
33
+ }
34
+ return (crc ^ 0xffffffff) >>> 0;
35
+ }
36
+ function u16(value) {
37
+ const buffer = Buffer.alloc(2);
38
+ buffer.writeUInt16LE(value);
39
+ return buffer;
40
+ }
41
+ function u32(value) {
42
+ const buffer = Buffer.alloc(4);
43
+ buffer.writeUInt32LE(value >>> 0);
44
+ return buffer;
45
+ }
46
+ function zipCreate(entries) {
47
+ const locals = [];
48
+ const centrals = [];
49
+ let offset = 0;
50
+ // Порядок фиксируем: одно и то же дерево должно давать один и тот же архив.
51
+ const sorted = [...entries].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
52
+ for (const entry of sorted) {
53
+ const name = Buffer.from(entry.path, 'utf-8');
54
+ const crc = crc32(entry.content);
55
+ // Сжимаем, но только если это что-то даёт: у уже сжатых файлов (png, zip)
56
+ // deflate добавляет байты, и хранение как есть честнее.
57
+ const deflated = (0, node_zlib_1.deflateRawSync)(entry.content, { level: 6 });
58
+ const compressed = deflated.length < entry.content.length;
59
+ const method = compressed ? 8 : 0;
60
+ const payload = compressed ? deflated : entry.content;
61
+ const local = Buffer.concat([
62
+ u32(LOCAL_SIG),
63
+ u16(20),
64
+ u16(FLAG_UTF8),
65
+ u16(method),
66
+ u16(DOS_TIME),
67
+ u16(DOS_DATE),
68
+ u32(crc),
69
+ u32(payload.length),
70
+ u32(entry.content.length),
71
+ u16(name.length),
72
+ u16(0),
73
+ name,
74
+ payload,
75
+ ]);
76
+ locals.push(local);
77
+ centrals.push(Buffer.concat([
78
+ u32(CENTRAL_SIG),
79
+ u16(20),
80
+ u16(20),
81
+ u16(FLAG_UTF8),
82
+ u16(method),
83
+ u16(DOS_TIME),
84
+ u16(DOS_DATE),
85
+ u32(crc),
86
+ u32(payload.length),
87
+ u32(entry.content.length),
88
+ u16(name.length),
89
+ u16(0),
90
+ u16(0),
91
+ u16(0),
92
+ u16(0),
93
+ u32(0),
94
+ u32(offset),
95
+ name,
96
+ ]));
97
+ offset += local.length;
98
+ }
99
+ const central = Buffer.concat(centrals);
100
+ const eocd = Buffer.concat([
101
+ u32(EOCD_SIG),
102
+ u16(0),
103
+ u16(0),
104
+ u16(sorted.length),
105
+ u16(sorted.length),
106
+ u32(central.length),
107
+ u32(offset),
108
+ u16(0),
109
+ ]);
110
+ return Buffer.concat([...locals, central, eocd]);
111
+ }
112
+ function findEocd(buffer) {
113
+ // Комментарий архива может занимать до 64 КБ, поэтому ищем сигнатуру с конца.
114
+ const from = Math.max(0, buffer.length - 66_000);
115
+ for (let i = buffer.length - 22; i >= from; i--) {
116
+ if (buffer.readUInt32LE(i) === EOCD_SIG)
117
+ return i;
118
+ }
119
+ return -1;
120
+ }
121
+ function zipRead(buffer) {
122
+ if (buffer.length < 22)
123
+ throw new ZipError('Файл слишком мал для zip');
124
+ const eocd = findEocd(buffer);
125
+ if (eocd === -1)
126
+ throw new ZipError('Это не zip: не найден конец архива');
127
+ const count = buffer.readUInt16LE(eocd + 10);
128
+ let pointer = buffer.readUInt32LE(eocd + 16);
129
+ const entries = [];
130
+ for (let i = 0; i < count; i++) {
131
+ if (pointer + 46 > buffer.length || buffer.readUInt32LE(pointer) !== CENTRAL_SIG) {
132
+ throw new ZipError('Повреждён оглавление архива');
133
+ }
134
+ const method = buffer.readUInt16LE(pointer + 10);
135
+ const compressedSize = buffer.readUInt32LE(pointer + 20);
136
+ const uncompressedSize = buffer.readUInt32LE(pointer + 24);
137
+ const nameLength = buffer.readUInt16LE(pointer + 28);
138
+ const extraLength = buffer.readUInt16LE(pointer + 30);
139
+ const commentLength = buffer.readUInt16LE(pointer + 32);
140
+ const localOffset = buffer.readUInt32LE(pointer + 42);
141
+ const name = buffer.toString('utf-8', pointer + 46, pointer + 46 + nameLength);
142
+ pointer += 46 + nameLength + extraLength + commentLength;
143
+ if (name.endsWith('/'))
144
+ continue;
145
+ if (compressedSize === 0xffffffff || uncompressedSize === 0xffffffff) {
146
+ throw new ZipError(`Архив в формате zip64 не поддерживается: ${name}`);
147
+ }
148
+ if (buffer.readUInt32LE(localOffset) !== LOCAL_SIG) {
149
+ throw new ZipError(`Повреждена запись архива: ${name}`);
150
+ }
151
+ const localNameLength = buffer.readUInt16LE(localOffset + 26);
152
+ const localExtraLength = buffer.readUInt16LE(localOffset + 28);
153
+ const dataStart = localOffset + 30 + localNameLength + localExtraLength;
154
+ const raw = buffer.subarray(dataStart, dataStart + compressedSize);
155
+ let content;
156
+ if (method === 0) {
157
+ content = Buffer.from(raw);
158
+ }
159
+ else if (method === 8) {
160
+ try {
161
+ content = (0, node_zlib_1.inflateRawSync)(raw);
162
+ }
163
+ catch (e) {
164
+ throw new ZipError(`Не удалось распаковать ${name}: ${e instanceof Error ? e.message : e}`);
165
+ }
166
+ }
167
+ else {
168
+ throw new ZipError(`Неизвестный метод сжатия (${method}) у ${name}`);
169
+ }
170
+ entries.push({ path: name.replace(/\\/g, '/'), content });
171
+ }
172
+ return entries;
173
+ }
package/package.json CHANGED
@@ -1,27 +1,22 @@
1
- {
2
- "name": "@getxflow/cli",
3
- "version": "0.0.1",
4
- "description": "XFlow CLI аналитика кабинетов маркетплейсов для вашего агента",
5
- "keywords": [
6
- "wildberries",
7
- "marketplace",
8
- "analytics",
9
- "mcp",
10
- "cli"
11
- ],
12
- "license": "UNLICENSED",
13
- "type": "module",
14
- "bin": {
15
- "xflow": "index.js"
16
- },
17
- "files": [
18
- "index.js",
19
- "README.md"
20
- ],
21
- "engines": {
22
- "node": ">=24"
23
- },
24
- "publishConfig": {
25
- "access": "public"
26
- }
27
- }
1
+ {
2
+ "name": "@getxflow/cli",
3
+ "version": "0.1.1",
4
+ "description": "CLI платформы XFlow: синхронизация кода, деплой и публикация приложений",
5
+ "license": "UNLICENSED",
6
+ "engines": {
7
+ "node": ">=20"
8
+ },
9
+ "bin": {
10
+ "xflow": "dist/bin.js"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "skills"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsc -p tsconfig.json"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ }
22
+ }
@@ -0,0 +1,110 @@
1
+ ---
2
+ name: xflow
3
+ description: Build, deploy and publish apps on the XFlow platform with the xflow CLI. Use when the project root has xflow.json or VITE_XFLOW_* variables, when asked to deploy, publish, roll back a version, sync sources with the platform, look into errors from a deployed app, or build UI on the platform design system.
4
+ ---
5
+
6
+ # XFlow
7
+
8
+ Hosting for web apps. The code lives in an ordinary repository on the developer
9
+ machine and is built locally; the platform takes the finished build and serves it.
10
+ A platform project is recognized by the `xflow.json` file in its root.
11
+
12
+ ## First rule
13
+
14
+ Check commands and flags against `xflow help` and `xflow help <command>`, not against
15
+ memory. If a command is not in the help output, it does not exist: guessing flags is
16
+ pointless. The CLI prints a hint with almost every error, read it in full, it usually
17
+ contains the fix. Help output is in Russian.
18
+
19
+ ## Workflow
20
+
21
+ 1. Change the code.
22
+ 2. `npm run typecheck` for a two-second type check (older projects may not have the
23
+ script, then `npx tsc --noEmit`).
24
+ 3. `npm run build` if the change is substantial, before deploying.
25
+ 4. `xflow deploy` sends the sources, builds locally, uploads the version. It prints
26
+ a URL: that is the dev address, visible to the team, not to visitors.
27
+ 5. Open and check it: the URL from the output, or `xflow open`.
28
+ 6. `xflow publish` makes that same version visible to visitors.
29
+
30
+ The split is deliberate: shipping a build and showing it are two separate decisions.
31
+ Until `publish` runs, visitors keep seeing the previous version.
32
+
33
+ Rolling back: `xflow deployments` lists the version history, `xflow rollback <id>`
34
+ points the dev address back at an earlier build. Sources stay on their own revision.
35
+
36
+ The build runs on the developer machine. The platform has no builder, it serves
37
+ static files: a build without `index.html` in the root is rejected.
38
+
39
+ ## Cloud functions
40
+
41
+ Server-side code lives in `functions/<name>/index.ts` and exports `handler`. Deploy it
42
+ with `xflow functions deploy` (one name to deploy a single function, no name for all),
43
+ list what is live with `xflow functions list`. The handler returns
44
+ `{ statusCode, body }` where `body` is a JSON string.
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
+
52
+ Calls from the app must carry the `X-Project-Token` header; the token is already in
53
+ the app environment as `VITE_XFLOW_PROJECT_TOKEN`. Treat a function as a public API:
54
+ that token ships inside the frontend bundle.
55
+
56
+ Organization secrets reach a function only if it mentions them via `process.env`, so
57
+ read them by name and do not build variable names dynamically.
58
+
59
+ ## Syncing code
60
+
61
+ `xflow status` shows how the local copy differs from the server revision.
62
+ `xflow push` sends sources, `xflow pull` fetches them.
63
+
64
+ If a push is rejected, the server revision is newer, meaning someone pushed first.
65
+ Fetch their work next to yours (`xflow pull --into ./server-copy`), merge it locally,
66
+ then push again. `--force` destroys their work: a last resort, not a way around the
67
+ error.
68
+
69
+ ## Do not
70
+
71
+ - Edit `xflow.json` by hand: the CLI writes it.
72
+ - Commit `.env`: it holds the project token.
73
+ - Push with `--force` without checking `xflow status` first.
74
+ - Invent platform commands: what is not in `xflow help` does not exist.
75
+
76
+ ## App design
77
+
78
+ The platform design system already ships inside the project, and the app is supposed
79
+ to look like a part of the platform:
80
+
81
+ - `src/components/ui` for primitives: buttons, inputs, dialogs, tables, menus
82
+ - `src/components/blocks` for composed blocks: `data-table`, `entity-form`,
83
+ `filter-bar`, `detail-panel`, `kanban`, `charts`, `user-picker`, `relation-picker`
84
+ - `src/index.css` for color tokens
85
+
86
+ Before writing your own component, check whether a block already covers it: props are
87
+ typed next to each block, larger ones keep a separate `types.ts`. Take colors from
88
+ tokens (`bg-card`, `text-muted-foreground`, `bg-success-soft` and the like). A custom
89
+ hex palette makes the app look foreign inside the platform, which is the whole reason
90
+ the design system sits in the project.
91
+
92
+ The app runs inside the platform in an iframe and receives the theme and the current
93
+ user from it. The `usePlatformAuth()` hook gives the name, role, permissions and the
94
+ list of organization members: that is what `user-picker` is built on.
95
+
96
+ ## Errors from a deployed app
97
+
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.
105
+
106
+ ## Environment
107
+
108
+ `VITE_XFLOW_PROJECT_TOKEN` and `VITE_XFLOW_API_URL` are written by the CLI when the
109
+ project is created, there is no need to edit them by hand. For CI and agent runs:
110
+ `XFLOW_TOKEN` replaces `xflow login`, `XFLOW_API_URL` points at another platform host.
package/index.js DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- console.log('XFlow CLI — в разработке.');