@getxflow/cli 0.1.9 → 0.1.10
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 +0 -3
- package/dist/commands/auth.js +18 -13
- package/dist/commands/db.js +3 -10
- package/dist/commands/deploy.js +3 -18
- package/dist/commands/env.js +5 -14
- package/dist/commands/functions.js +6 -28
- 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 +240 -243
- 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/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) {
|