@getxflow/cli 0.1.9 → 0.2.0
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 +45 -45
- package/dist/api.js +5 -9
- package/dist/args.js +2 -10
- package/dist/bin.js +3 -6
- package/dist/commands/auth.js +18 -13
- package/dist/commands/db.js +3 -10
- package/dist/commands/deploy.js +40 -18
- package/dist/commands/env.js +6 -15
- package/dist/commands/functions.js +4 -113
- package/dist/commands/logs.js +1 -2
- package/dist/commands/mcp.js +6 -13
- package/dist/commands/projects.js +4 -16
- package/dist/commands/skills.js +18 -42
- package/dist/commands/sources.js +7 -16
- package/dist/config.js +8 -8
- package/dist/credentials.js +9 -10
- package/dist/errors.js +1 -1
- package/dist/help.js +257 -244
- package/dist/limits.js +5 -15
- package/dist/session.js +5 -9
- package/dist/template.js +25 -33
- package/dist/tree.js +5 -18
- package/dist/types.js +1 -8
- package/dist/ui.js +36 -14
- package/dist/version.js +3 -7
- package/dist/zip.js +15 -8
- package/package.json +1 -1
- package/skills/xflow/SKILL.md +21 -16
package/dist/commands/logs.js
CHANGED
|
@@ -8,7 +8,6 @@ const config_1 = require("../config");
|
|
|
8
8
|
const session_1 = require("../session");
|
|
9
9
|
const ui_1 = require("../ui");
|
|
10
10
|
const DEFAULT_LIMIT = 10;
|
|
11
|
-
/** Стек с хвостом консоли бывает длинным: показываем начало, оно и есть причина. */
|
|
12
11
|
const STACK_MAX_LINES = 14;
|
|
13
12
|
function stamp(iso) {
|
|
14
13
|
const date = new Date(iso);
|
|
@@ -24,7 +23,7 @@ async function fetchLogs(source, name, args) {
|
|
|
24
23
|
const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/logs?${query.toString()}`);
|
|
25
24
|
return data.logs;
|
|
26
25
|
}
|
|
27
|
-
/**
|
|
26
|
+
/** Newest at the bottom. */
|
|
28
27
|
function render(rows) {
|
|
29
28
|
for (const row of [...rows].reverse()) {
|
|
30
29
|
const label = row.source === 'function' ? (row.function ?? 'function') : 'browser';
|
package/dist/commands/mcp.js
CHANGED
|
@@ -9,7 +9,7 @@ const args_1 = require("../args");
|
|
|
9
9
|
const config_1 = require("../config");
|
|
10
10
|
const session_1 = require("../session");
|
|
11
11
|
const ui_1 = require("../ui");
|
|
12
|
-
/**
|
|
12
|
+
/** Clients with their own server-add command. */
|
|
13
13
|
const CLIENTS = [
|
|
14
14
|
{
|
|
15
15
|
binary: 'claude',
|
|
@@ -24,26 +24,20 @@ const CLIENTS = [
|
|
|
24
24
|
'--header',
|
|
25
25
|
`Authorization: Bearer ${token}`,
|
|
26
26
|
],
|
|
27
|
-
//
|
|
28
|
-
// выполняют и после смены ключа, и после обновления пакета.
|
|
27
|
+
// Re-running must update the entry, not fail on it.
|
|
29
28
|
reset: ['mcp', 'remove', 'xflow', '-s', 'local'],
|
|
30
29
|
},
|
|
31
30
|
];
|
|
32
31
|
const WINDOWS = process.platform === 'win32';
|
|
33
32
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* На Windows без оболочки не найти `.cmd`-обёртки, которыми ставятся все
|
|
37
|
-
* консольные пакеты npm. А оболочка не экранирует аргументы, а склеивает их:
|
|
38
|
-
* заголовок «Authorization: Bearer …» разваливается по пробелу. Поэтому на
|
|
39
|
-
* Windows собираем строку сами и сами же расставляем кавычки.
|
|
33
|
+
* Windows needs a shell to resolve npm's .cmd shims, and the shell joins
|
|
34
|
+
* arguments instead of escaping them: quote by hand.
|
|
40
35
|
*/
|
|
41
36
|
function quote(value) {
|
|
42
37
|
return WINDOWS ? `"${value.replace(/"/g, '""')}"` : value;
|
|
43
38
|
}
|
|
44
39
|
function execute(binary, args) {
|
|
45
|
-
//
|
|
46
|
-
// напечатать добавленные заголовки целиком, вместе с ключом.
|
|
40
|
+
// Capture output: clients may echo the added header together with the key.
|
|
47
41
|
const options = { encoding: 'utf-8' };
|
|
48
42
|
return WINDOWS
|
|
49
43
|
? (0, node_child_process_1.spawnSync)([binary, ...args.map(quote)].join(' '), { ...options, shell: true })
|
|
@@ -53,7 +47,7 @@ function hasBinary(binary) {
|
|
|
53
47
|
return execute(binary, ['--version']).status === 0;
|
|
54
48
|
}
|
|
55
49
|
async function mcpInstall(args) {
|
|
56
|
-
//
|
|
50
|
+
// The server covers the organization: the command works outside a project folder too.
|
|
57
51
|
const root = (0, config_1.findProjectRoot)();
|
|
58
52
|
const client = (0, session_1.connect)(root ? (0, config_1.readConfig)(root) : undefined);
|
|
59
53
|
const url = `${client.apiUrl.replace(/\/+$/, '')}/api/mcp`;
|
|
@@ -87,7 +81,6 @@ async function mcpInstall(args) {
|
|
|
87
81
|
(0, ui_1.out)(` address: ${url}`);
|
|
88
82
|
(0, ui_1.out)(` header: Authorization: Bearer <key>`);
|
|
89
83
|
}
|
|
90
|
-
// Ключ печатаем только по явной просьбе: см. комментарий в шапке файла.
|
|
91
84
|
if ((0, args_1.flagBool)(args, 'show-token')) {
|
|
92
85
|
(0, ui_1.out)('');
|
|
93
86
|
(0, ui_1.out)(`${(0, ui_1.bold)('Key')} ${client.token}`);
|
|
@@ -16,7 +16,7 @@ const skills_1 = require("./skills");
|
|
|
16
16
|
const template_1 = require("../template");
|
|
17
17
|
const zip_1 = require("../zip");
|
|
18
18
|
const ui_1 = require("../ui");
|
|
19
|
-
/**
|
|
19
|
+
/** .git alone does not count: linking a fresh clone is normal. */
|
|
20
20
|
function isEmptyEnough(dir) {
|
|
21
21
|
if (!(0, node_fs_1.existsSync)(dir))
|
|
22
22
|
return true;
|
|
@@ -30,7 +30,7 @@ function write(root, path, content) {
|
|
|
30
30
|
function escapeHtml(value) {
|
|
31
31
|
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
32
32
|
}
|
|
33
|
-
/**
|
|
33
|
+
/** Give the project its own tab title. */
|
|
34
34
|
function titleFromProject(path, content, name) {
|
|
35
35
|
if (path !== 'index.html')
|
|
36
36
|
return content;
|
|
@@ -46,17 +46,7 @@ async function templates() {
|
|
|
46
46
|
}
|
|
47
47
|
(0, ui_1.table)(list.map((t) => [t.id, t.name, t.is_default ? 'default' : '', t.description ?? '']));
|
|
48
48
|
}
|
|
49
|
-
/**
|
|
50
|
-
* Создать проект: шаблон платформы, локальная папка, проект на платформе.
|
|
51
|
-
*
|
|
52
|
-
* Шаблон приезжает с платформы, а не лежит в этом пакете. Иначе появились бы две
|
|
53
|
-
* стартовые точки — веб и CLI, — и приложения из них выглядели бы по-разному, хотя
|
|
54
|
-
* платформа у них одна.
|
|
55
|
-
*
|
|
56
|
-
* Отсюда и порядок: проект заводится первым, потому что без него неоткуда взять
|
|
57
|
-
* токен для `.env`. Если запись файлов сорвётся, проект останется пустым, и CLI
|
|
58
|
-
* скажет, как его подобрать (`xflow link`), — молча оставлять сироту нельзя.
|
|
59
|
-
*/
|
|
49
|
+
/** Create a project from the platform template. */
|
|
60
50
|
async function init(args) {
|
|
61
51
|
const target = (0, node_path_1.resolve)(args.words[0] ?? '.');
|
|
62
52
|
const name = (0, args_1.flagString)(args, 'name') ?? (0, node_path_1.basename)(target);
|
|
@@ -107,7 +97,6 @@ async function init(args) {
|
|
|
107
97
|
(0, ui_1.out)(' npm run dev # develop');
|
|
108
98
|
(0, ui_1.out)(' xflow deploy # send the code, build and release');
|
|
109
99
|
}
|
|
110
|
-
/** Связать текущую папку с уже существующим проектом. */
|
|
111
100
|
async function link(args) {
|
|
112
101
|
const root = (0, config_1.findProjectRoot)() ?? process.cwd();
|
|
113
102
|
const existing = (0, node_fs_1.existsSync)((0, node_path_1.join)(root, config_1.CONFIG_FILE)) ? (0, config_1.readConfig)(root) : undefined;
|
|
@@ -124,8 +113,7 @@ async function link(args) {
|
|
|
124
113
|
build: existing?.build ?? { command: 'npm run build', dir: 'dist' },
|
|
125
114
|
});
|
|
126
115
|
(0, config_1.ignoreStateInGit)(dir);
|
|
127
|
-
//
|
|
128
|
-
// молча теряет доступ к облачным функциям. Восстанавливаем, но чужой не трогаем.
|
|
116
|
+
// A fresh clone has no .env: recreate it, never overwrite an existing one.
|
|
129
117
|
if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(dir, '.env')) && card.project_token) {
|
|
130
118
|
write(dir, '.env', (0, template_1.envFile)(card.project_token, client.apiUrl, card.functions));
|
|
131
119
|
(0, ui_1.note)((0, ui_1.dim)(' Created .env with the project token and the function addresses'));
|
package/dist/commands/skills.js
CHANGED
|
@@ -9,40 +9,28 @@ const args_1 = require("../args");
|
|
|
9
9
|
const errors_1 = require("../errors");
|
|
10
10
|
const ui_1 = require("../ui");
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* текст под его заголовком. Спрашивать, чей агент, мы не хотим: раскладываем всем.
|
|
12
|
+
* The SKILL.md format is shared across tools (agentskills.io); each agent reads
|
|
13
|
+
* its own folder. Cursor reads .cursor/rules/*.mdc instead, so it gets the same
|
|
14
|
+
* text under its own header.
|
|
16
15
|
*/
|
|
17
16
|
const SKILL_DIRS = [
|
|
18
17
|
['.claude', 'skills'],
|
|
19
18
|
['.agents', 'skills'],
|
|
20
19
|
];
|
|
21
|
-
/**
|
|
22
|
-
* В домашней папке набор другой. Общий .agents/skills там читает OpenClaw, а
|
|
23
|
-
* Codex смотрит его только внутри репозитория: свой личный он держит отдельно,
|
|
24
|
-
* в .codex/skills. Без этой папки `--global` тихо не работает для Codex.
|
|
25
|
-
*/
|
|
20
|
+
/** Codex reads its global skills from .codex/skills, not from .agents. */
|
|
26
21
|
const GLOBAL_SKILL_DIRS = [...SKILL_DIRS, ['.codex', 'skills']];
|
|
27
22
|
const CURSOR_RULE = ['.cursor', 'rules', 'xflow.mdc'];
|
|
28
|
-
/**
|
|
29
|
-
* Указатель в AGENTS.md.
|
|
30
|
-
*
|
|
31
|
-
* Скилл подхватывается лениво: только когда описание совпало с задачей. Просьба
|
|
32
|
-
* «добавь таблицу клиентов» с ним не совпадает, и агент не узнаёт, что проект
|
|
33
|
-
* вообще живёт на платформе. AGENTS.md читается всегда и без условий, поэтому
|
|
34
|
-
* пара строк здесь решает ту часть, которую ленивый скилл закрыть не может.
|
|
35
|
-
*/
|
|
23
|
+
/** AGENTS.md is always read, unlike the lazily loaded skill: a short pointer lives there. */
|
|
36
24
|
const POINTER_MARKER = '<!-- xflow-skill -->';
|
|
37
|
-
const POINTER = `${POINTER_MARKER}
|
|
38
|
-
## XFlow
|
|
39
|
-
|
|
40
|
-
This project is hosted on the XFlow platform and ships through the \`xflow\` CLI, not
|
|
41
|
-
through a git push. Read \`.agents/skills/xflow/SKILL.md\` before deploying, publishing,
|
|
42
|
-
rolling back, touching the database or migrations, cloud functions, schedules,
|
|
43
|
-
environment variables or production logs. Command list: \`xflow help\`.
|
|
25
|
+
const POINTER = `${POINTER_MARKER}
|
|
26
|
+
## XFlow
|
|
27
|
+
|
|
28
|
+
This project is hosted on the XFlow platform and ships through the \`xflow\` CLI, not
|
|
29
|
+
through a git push. Read \`.agents/skills/xflow/SKILL.md\` before deploying, publishing,
|
|
30
|
+
rolling back, touching the database or migrations, cloud functions, schedules,
|
|
31
|
+
environment variables or production logs. Command list: \`xflow help\`.
|
|
44
32
|
`;
|
|
45
|
-
/**
|
|
33
|
+
/** The skill ships inside the package, next to dist. */
|
|
46
34
|
function skillSource() {
|
|
47
35
|
const path = (0, node_path_1.join)(__dirname, '..', '..', 'skills', 'xflow', 'SKILL.md');
|
|
48
36
|
if (!(0, node_fs_1.existsSync)(path)) {
|
|
@@ -50,13 +38,13 @@ function skillSource() {
|
|
|
50
38
|
}
|
|
51
39
|
return (0, node_fs_1.readFileSync)(path, 'utf-8');
|
|
52
40
|
}
|
|
53
|
-
/**
|
|
41
|
+
/** The same text under Cursor's own header. */
|
|
54
42
|
function cursorRule(skill) {
|
|
55
43
|
const description = /^description:\s*(.+)$/m.exec(skill)?.[1] ?? 'XFlow platform';
|
|
56
44
|
const body = skill.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '');
|
|
57
45
|
return `---\ndescription: ${description}\nalwaysApply: false\n---\n\n${body}`;
|
|
58
46
|
}
|
|
59
|
-
/**
|
|
47
|
+
/** Writes only when the content differs. Returns the touched path. */
|
|
60
48
|
function writeIfChanged(path, content) {
|
|
61
49
|
const before = (0, node_fs_1.existsSync)(path) ? (0, node_fs_1.readFileSync)(path, 'utf-8') : null;
|
|
62
50
|
if (before === content)
|
|
@@ -65,11 +53,7 @@ function writeIfChanged(path, content) {
|
|
|
65
53
|
(0, node_fs_1.writeFileSync)(path, content, 'utf-8');
|
|
66
54
|
return path;
|
|
67
55
|
}
|
|
68
|
-
/**
|
|
69
|
-
* Дописать указатель в AGENTS.md: только в конец и только один раз по маркеру.
|
|
70
|
-
* Файл принадлежит пользователю, там его правила, переписывать его мы не вправе.
|
|
71
|
-
* CLAUDE.md не трогаем вовсе: Claude Code и так читает .claude/skills.
|
|
72
|
-
*/
|
|
56
|
+
/** Append once, by marker: the file belongs to the user. */
|
|
73
57
|
function appendPointer(base) {
|
|
74
58
|
const path = (0, node_path_1.join)(base, 'AGENTS.md');
|
|
75
59
|
const before = (0, node_fs_1.existsSync)(path) ? (0, node_fs_1.readFileSync)(path, 'utf-8') : null;
|
|
@@ -78,7 +62,6 @@ function appendPointer(base) {
|
|
|
78
62
|
(0, node_fs_1.writeFileSync)(path, before ? `${before.replace(/\s*$/, '')}\n\n${POINTER}` : POINTER, 'utf-8');
|
|
79
63
|
return path;
|
|
80
64
|
}
|
|
81
|
-
/** Разложить инструкцию по папкам, где её найдёт агент. Отдаёт тронутые файлы. */
|
|
82
65
|
function install(base, global) {
|
|
83
66
|
const skill = skillSource();
|
|
84
67
|
const touched = [];
|
|
@@ -87,8 +70,7 @@ function install(base, global) {
|
|
|
87
70
|
if (path)
|
|
88
71
|
touched.push(path);
|
|
89
72
|
}
|
|
90
|
-
//
|
|
91
|
-
// аналогов у них нет, а в домашней папке их никто не читает.
|
|
73
|
+
// Project-level only: these have no global equivalents.
|
|
92
74
|
if (!global) {
|
|
93
75
|
const rule = writeIfChanged((0, node_path_1.join)(base, ...CURSOR_RULE), cursorRule(skill));
|
|
94
76
|
if (rule)
|
|
@@ -99,13 +81,7 @@ function install(base, global) {
|
|
|
99
81
|
}
|
|
100
82
|
return touched;
|
|
101
83
|
}
|
|
102
|
-
/**
|
|
103
|
-
* Установка попутно, из `init` и `link`.
|
|
104
|
-
*
|
|
105
|
-
* Отдельной командой до этого доходили единицы, и агент оставался без знания о
|
|
106
|
-
* платформе — той самой, на которой ему предстоит работать. При этом инструкция
|
|
107
|
-
* не то, ради чего запускали команду, поэтому её сбой не роняет привязку папки.
|
|
108
|
-
*/
|
|
84
|
+
/** Best-effort install from init and link: a failure must not break the linking. */
|
|
109
85
|
function installSkillQuietly(base) {
|
|
110
86
|
try {
|
|
111
87
|
if (install(base, false).length > 0) {
|
package/dist/commands/sources.js
CHANGED
|
@@ -15,9 +15,8 @@ const session_1 = require("../session");
|
|
|
15
15
|
const tree_1 = require("../tree");
|
|
16
16
|
const zip_1 = require("../zip");
|
|
17
17
|
const ui_1 = require("../ui");
|
|
18
|
-
/**
|
|
18
|
+
/** Server cap, checked locally to fail before uploading. */
|
|
19
19
|
const MAX_ARCHIVE_BYTES = 25 * 1024 * 1024;
|
|
20
|
-
/** Собрать дерево исходников: что уходит на сервер и с каким хешем. */
|
|
21
20
|
function prepareTree(root, config) {
|
|
22
21
|
const buildDir = config.build?.dir?.replace(/^\.\//, '').replace(/\/+$/, '');
|
|
23
22
|
const extra = [...(config.ignore ?? []), ...(buildDir ? [`${buildDir}/`] : [])];
|
|
@@ -41,12 +40,7 @@ async function latestRevision(client, projectId) {
|
|
|
41
40
|
const { revisions } = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/revisions`);
|
|
42
41
|
return revisions[0] ?? null;
|
|
43
42
|
}
|
|
44
|
-
/**
|
|
45
|
-
* Показать, чью работу перетирает `--force`, и спросить подтверждение.
|
|
46
|
-
*
|
|
47
|
-
* Без этого текст «используйте --force» превращается в инструкцию, которую
|
|
48
|
-
* агент выполнит не задумываясь, а чужие правки исчезнут без следа (D23).
|
|
49
|
-
*/
|
|
43
|
+
/** Show what --force would overwrite and ask for confirmation. */
|
|
50
44
|
async function confirmForce(client, projectId, server, local) {
|
|
51
45
|
const card = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}`);
|
|
52
46
|
(0, ui_1.note)('');
|
|
@@ -71,19 +65,18 @@ async function confirmForce(client, projectId, server, local) {
|
|
|
71
65
|
(0, ui_1.warn)('Could not read the server copy: the list of disappearing files is unavailable');
|
|
72
66
|
}
|
|
73
67
|
(0, ui_1.note)('');
|
|
74
|
-
const confirmed = await (0, ui_1.confirmWord)(`This overwrites
|
|
68
|
+
const confirmed = await (0, ui_1.confirmWord)(`This overwrites work that is not yours, with no way to recover it from the platform.`, card.name);
|
|
75
69
|
if (!confirmed)
|
|
76
70
|
throw new errors_1.CliError('Cancelled');
|
|
77
71
|
}
|
|
78
|
-
/**
|
|
72
|
+
/** Used by push and by the first step of deploy. */
|
|
79
73
|
async function pushSources(root, config, client, options) {
|
|
80
74
|
const tree = prepareTree(root, config);
|
|
81
75
|
const state = (0, config_1.readState)(root);
|
|
82
76
|
(0, ui_1.step)(`Sending ${tree.files.length} files (${(0, ui_1.formatBytes)(tree.archive.length)})`);
|
|
83
77
|
const server = await latestRevision(client, config.projectId);
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
// нельзя, иначе чужая работа исчезнет без единой ошибки.
|
|
78
|
+
// Unknown base revision (fresh clone): a matching hash just syncs,
|
|
79
|
+
// a mismatch must not overwrite silently.
|
|
87
80
|
if (server && state.revision === undefined && !options.force) {
|
|
88
81
|
if (server.tree_hash === tree.hash) {
|
|
89
82
|
(0, config_1.writeState)(root, { revision: server.revision, treeHash: server.tree_hash });
|
|
@@ -115,7 +108,6 @@ async function push(args) {
|
|
|
115
108
|
const client = (0, session_1.connect)(config);
|
|
116
109
|
await pushSources(root, config, client, { force: (0, args_1.flagBool)(args, 'force') });
|
|
117
110
|
}
|
|
118
|
-
/** Есть ли в каталоге что-то, кроме служебного. */
|
|
119
111
|
function hasContent(dir) {
|
|
120
112
|
if (!(0, node_fs_1.existsSync)(dir))
|
|
121
113
|
return false;
|
|
@@ -145,8 +137,7 @@ async function pull(args) {
|
|
|
145
137
|
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true });
|
|
146
138
|
(0, node_fs_1.writeFileSync)(path, entry.content);
|
|
147
139
|
}
|
|
148
|
-
//
|
|
149
|
-
// ревизий отношения не имеет, и запись туда сбила бы его у рабочей.
|
|
140
|
+
// Only the working folder tracks revisions; a side copy (--into) must not reset them.
|
|
150
141
|
if (!into) {
|
|
151
142
|
(0, config_1.writeState)(root, { revision: info.revision, treeHash: info.tree_hash });
|
|
152
143
|
}
|
package/dist/config.js
CHANGED
|
@@ -17,7 +17,7 @@ const STATE_DIR = '.xflow';
|
|
|
17
17
|
class ConfigError extends Error {
|
|
18
18
|
}
|
|
19
19
|
exports.ConfigError = ConfigError;
|
|
20
|
-
/**
|
|
20
|
+
/** Walk up from cwd. */
|
|
21
21
|
function findProjectRoot(from = process.cwd()) {
|
|
22
22
|
let dir = (0, node_path_1.resolve)(from);
|
|
23
23
|
for (;;) {
|
|
@@ -47,7 +47,6 @@ function readConfig(root) {
|
|
|
47
47
|
function writeConfig(root, config) {
|
|
48
48
|
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(root, exports.CONFIG_FILE), `${JSON.stringify(config, null, 2)}\n`, 'utf-8');
|
|
49
49
|
}
|
|
50
|
-
/** Конфигурация проекта, из которого запущена команда. Бросает, если папка не связана. */
|
|
51
50
|
function requireProject() {
|
|
52
51
|
const root = findProjectRoot();
|
|
53
52
|
if (!root) {
|
|
@@ -58,8 +57,12 @@ function requireProject() {
|
|
|
58
57
|
return { root, config: readConfig(root) };
|
|
59
58
|
}
|
|
60
59
|
function apiUrlFor(config) {
|
|
61
|
-
const raw = process.env.XFLOW_API_URL?.trim() || config?.api?.trim() || version_1.DEFAULT_API_URL;
|
|
62
|
-
|
|
60
|
+
const raw = (process.env.XFLOW_API_URL?.trim() || config?.api?.trim() || version_1.DEFAULT_API_URL).replace(/\/+$/, '');
|
|
61
|
+
// The address ends up in headers and child processes: keep it a plain http(s) URL.
|
|
62
|
+
if (!/^https?:\/\/[A-Za-z0-9.-]+(:\d+)?(\/[A-Za-z0-9._/-]*)?$/.test(raw)) {
|
|
63
|
+
throw new ConfigError(`Invalid platform address: ${raw}`);
|
|
64
|
+
}
|
|
65
|
+
return raw;
|
|
63
66
|
}
|
|
64
67
|
function readState(root) {
|
|
65
68
|
const path = (0, node_path_1.join)(root, STATE_DIR, 'state.json');
|
|
@@ -77,10 +80,7 @@ function writeState(root, state) {
|
|
|
77
80
|
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
78
81
|
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(dir, 'state.json'), `${JSON.stringify(state, null, 2)}\n`, 'utf-8');
|
|
79
82
|
}
|
|
80
|
-
/**
|
|
81
|
-
* Состояние в git не нужно никому, но забыть про него легко: дописываем в
|
|
82
|
-
* .gitignore сами. Если файла нет, создаём — проект почти всегда под git.
|
|
83
|
-
*/
|
|
83
|
+
/** Keep .xflow/ out of git. */
|
|
84
84
|
function ignoreStateInGit(root) {
|
|
85
85
|
const path = (0, node_path_1.join)(root, '.gitignore');
|
|
86
86
|
const line = `${STATE_DIR}/`;
|
package/dist/credentials.js
CHANGED
|
@@ -30,12 +30,12 @@ function writeStore(store) {
|
|
|
30
30
|
(0, node_fs_1.mkdirSync)(dir, { recursive: true, mode: 0o700 });
|
|
31
31
|
const path = credentialsPath();
|
|
32
32
|
(0, node_fs_1.writeFileSync)(path, `${JSON.stringify(store, null, 2)}\n`, { mode: 0o600 });
|
|
33
|
-
//
|
|
33
|
+
// writeFileSync does not change the mode of an existing file.
|
|
34
34
|
try {
|
|
35
35
|
(0, node_fs_1.chmodSync)(path, 0o600);
|
|
36
36
|
}
|
|
37
37
|
catch {
|
|
38
|
-
// Windows
|
|
38
|
+
// Windows has no POSIX modes.
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
function saveCredential(apiUrl, credential) {
|
|
@@ -51,14 +51,13 @@ function forgetCredential(apiUrl) {
|
|
|
51
51
|
writeStore(store);
|
|
52
52
|
return true;
|
|
53
53
|
}
|
|
54
|
-
/**
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return fromEnv;
|
|
54
|
+
/** XFLOW_TOKEN wins over the store; the caller decides whether env is allowed for this address. */
|
|
55
|
+
function resolveToken(apiUrl, allowEnv = true) {
|
|
56
|
+
if (allowEnv) {
|
|
57
|
+
const fromEnv = process.env.XFLOW_TOKEN?.trim();
|
|
58
|
+
if (fromEnv)
|
|
59
|
+
return fromEnv;
|
|
60
|
+
}
|
|
62
61
|
return readStore()[apiUrl]?.token ?? null;
|
|
63
62
|
}
|
|
64
63
|
function readCredential(apiUrl) {
|
package/dist/errors.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.CliError = void 0;
|
|
4
|
-
/**
|
|
4
|
+
/** Shown to the user as is, without a stack trace. */
|
|
5
5
|
class CliError extends Error {
|
|
6
6
|
hint;
|
|
7
7
|
constructor(message, hint) {
|