@getxflow/cli 0.1.8 → 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 +31 -30
- package/dist/api.js +14 -23
- package/dist/args.js +3 -11
- package/dist/bin.js +10 -9
- package/dist/commands/auth.js +48 -41
- package/dist/commands/db.js +21 -28
- package/dist/commands/deploy.js +21 -36
- package/dist/commands/env.js +22 -31
- package/dist/commands/functions.js +26 -48
- package/dist/commands/logs.js +7 -8
- package/dist/commands/mcp.js +20 -27
- package/dist/commands/projects.js +27 -39
- package/dist/commands/schedules.js +8 -8
- package/dist/commands/skills.js +24 -48
- package/dist/commands/sources.js +38 -47
- package/dist/config.js +13 -13
- package/dist/credentials.js +9 -10
- package/dist/errors.js +1 -1
- package/dist/help.js +215 -216
- package/dist/limits.js +68 -0
- package/dist/session.js +6 -10
- package/dist/template.js +13 -21
- package/dist/tree.js +5 -18
- package/dist/types.js +1 -8
- package/dist/ui.js +47 -25
- package/dist/version.js +3 -7
- package/dist/zip.js +22 -15
- package/package.json +22 -22
- package/skills/xflow/SKILL.md +19 -1
|
@@ -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;
|
|
@@ -41,40 +41,30 @@ async function templates() {
|
|
|
41
41
|
const client = (0, session_1.connect)();
|
|
42
42
|
const { templates: list } = await (0, api_1.apiJson)(client, '/api/v1/templates');
|
|
43
43
|
if (list.length === 0) {
|
|
44
|
-
(0, ui_1.note)('
|
|
44
|
+
(0, ui_1.note)('No templates');
|
|
45
45
|
return;
|
|
46
46
|
}
|
|
47
|
-
(0, ui_1.table)(list.map((t) => [t.id, t.name, t.is_default ? '
|
|
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);
|
|
63
53
|
const client = (0, session_1.connect)();
|
|
64
54
|
if (!isEmptyEnough(target)) {
|
|
65
|
-
throw new errors_1.CliError(
|
|
55
|
+
throw new errors_1.CliError(`The directory ${target} is not empty`, 'Create the project in an empty folder: xflow init my-app. To link an existing folder to a project: xflow link <id>');
|
|
66
56
|
}
|
|
67
57
|
let templateId = (0, args_1.flagString)(args, 'template');
|
|
68
58
|
if (!templateId) {
|
|
69
59
|
const { templates: list } = await (0, api_1.apiJson)(client, '/api/v1/templates');
|
|
70
60
|
templateId = (list.find((t) => t.is_default) ?? list[0])?.id;
|
|
71
61
|
if (!templateId)
|
|
72
|
-
throw new errors_1.CliError('
|
|
62
|
+
throw new errors_1.CliError('The platform has no templates at all');
|
|
73
63
|
}
|
|
74
|
-
(0, ui_1.step)('
|
|
64
|
+
(0, ui_1.step)('Fetching the template');
|
|
75
65
|
const archive = await (0, api_1.apiBinary)(client, `/api/v1/templates/${templateId}/archive`);
|
|
76
66
|
const files = (0, zip_1.zipRead)(archive);
|
|
77
|
-
(0, ui_1.step)('
|
|
67
|
+
(0, ui_1.step)('Creating the project on the platform');
|
|
78
68
|
const project = await (0, api_1.apiJson)(client, '/api/v1/projects', {
|
|
79
69
|
method: 'POST',
|
|
80
70
|
body: { name, database_id: (0, args_1.flagString)(args, 'database') ?? null },
|
|
@@ -94,27 +84,26 @@ async function init(args) {
|
|
|
94
84
|
(0, config_1.ignoreStateInGit)(target);
|
|
95
85
|
}
|
|
96
86
|
catch (e) {
|
|
97
|
-
(0, ui_1.note)((0, ui_1.dim)(`
|
|
98
|
-
(0, ui_1.note)((0, ui_1.dim)('
|
|
87
|
+
(0, ui_1.note)((0, ui_1.dim)(` The project "${project.name}" is already created (${project.id}).`));
|
|
88
|
+
(0, ui_1.note)((0, ui_1.dim)(' Sort the folder out and pick it up: xflow link ' + project.id));
|
|
99
89
|
throw e;
|
|
100
90
|
}
|
|
101
91
|
(0, skills_1.installSkillQuietly)(target);
|
|
102
|
-
(0, ui_1.ok)(
|
|
92
|
+
(0, ui_1.ok)(`Project "${project.name}" created: ${files.length} template files`);
|
|
103
93
|
(0, ui_1.out)('');
|
|
104
|
-
(0, ui_1.out)(` ${(0, ui_1.bold)('
|
|
94
|
+
(0, ui_1.out)(` ${(0, ui_1.bold)('Next:')}`);
|
|
105
95
|
(0, ui_1.out)(` cd ${(0, node_path_1.basename)(target)}`);
|
|
106
96
|
(0, ui_1.out)(' npm install');
|
|
107
|
-
(0, ui_1.out)(' npm run dev #
|
|
108
|
-
(0, ui_1.out)(' xflow deploy #
|
|
97
|
+
(0, ui_1.out)(' npm run dev # develop');
|
|
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;
|
|
114
103
|
const client = (0, session_1.connect)(existing);
|
|
115
104
|
const projectId = args.words[0];
|
|
116
105
|
if (!projectId) {
|
|
117
|
-
throw new errors_1.CliError('
|
|
106
|
+
throw new errors_1.CliError('A project identifier is required', 'The list: xflow projects list');
|
|
118
107
|
}
|
|
119
108
|
const card = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}`);
|
|
120
109
|
const dir = existing ? root : process.cwd();
|
|
@@ -124,14 +113,13 @@ 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
|
-
(0, ui_1.note)((0, ui_1.dim)('
|
|
119
|
+
(0, ui_1.note)((0, ui_1.dim)(' Created .env with the project token and the function addresses'));
|
|
132
120
|
}
|
|
133
121
|
(0, skills_1.installSkillQuietly)(dir);
|
|
134
|
-
(0, ui_1.ok)(
|
|
122
|
+
(0, ui_1.ok)(`The folder is linked to the project "${card.name}"`);
|
|
135
123
|
(0, ui_1.note)((0, ui_1.dim)(` ${(0, node_path_1.join)(dir, config_1.CONFIG_FILE)}`));
|
|
136
124
|
}
|
|
137
125
|
async function list() {
|
|
@@ -139,13 +127,13 @@ async function list() {
|
|
|
139
127
|
const client = (0, session_1.connect)(root ? (0, config_1.readConfig)(root) : undefined);
|
|
140
128
|
const { projects } = await (0, api_1.apiJson)(client, '/api/v1/projects');
|
|
141
129
|
if (projects.length === 0) {
|
|
142
|
-
(0, ui_1.note)('
|
|
130
|
+
(0, ui_1.note)('No projects. To create one: xflow init');
|
|
143
131
|
return;
|
|
144
132
|
}
|
|
145
133
|
(0, ui_1.table)(projects.map((p) => [
|
|
146
134
|
p.id,
|
|
147
135
|
p.name,
|
|
148
|
-
p.live_deploy_id ? '
|
|
136
|
+
p.live_deploy_id ? 'published' : p.dev_deploy_id ? 'dev only' : 'no build',
|
|
149
137
|
(0, ui_1.formatAge)(p.updated_at),
|
|
150
138
|
]));
|
|
151
139
|
}
|
|
@@ -154,7 +142,7 @@ async function get(args) {
|
|
|
154
142
|
const config = root ? (0, config_1.readConfig)(root) : undefined;
|
|
155
143
|
const projectId = args.words[0] ?? config?.projectId;
|
|
156
144
|
if (!projectId) {
|
|
157
|
-
throw new errors_1.CliError('
|
|
145
|
+
throw new errors_1.CliError('A project identifier is required', 'Or run the command inside the project folder');
|
|
158
146
|
}
|
|
159
147
|
const client = (0, session_1.connect)(config);
|
|
160
148
|
const card = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}`);
|
|
@@ -162,19 +150,19 @@ async function get(args) {
|
|
|
162
150
|
if (card.description)
|
|
163
151
|
(0, ui_1.out)(card.description);
|
|
164
152
|
(0, ui_1.out)('');
|
|
165
|
-
(0, ui_1.out)(
|
|
166
|
-
(0, ui_1.out)(
|
|
167
|
-
(0, ui_1.out)(
|
|
153
|
+
(0, ui_1.out)(`Built version: ${card.dev_deploy_id ?? '(no build)'}`);
|
|
154
|
+
(0, ui_1.out)(`Visitors see: ${card.live_deploy_id ?? '(never published)'}`);
|
|
155
|
+
(0, ui_1.out)(`Database: ${card.database ? `${card.database.name} (${card.database.schema})` : '(none)'}`);
|
|
168
156
|
(0, ui_1.out)('');
|
|
169
157
|
(0, ui_1.out)(card.project_url);
|
|
170
158
|
if (card.functions.length > 0) {
|
|
171
159
|
(0, ui_1.out)('');
|
|
172
|
-
(0, ui_1.out)('
|
|
160
|
+
(0, ui_1.out)('Functions:');
|
|
173
161
|
(0, ui_1.table)(card.functions.map((f) => [` ${f.name}`, f.status, (0, ui_1.formatAge)(f.last_deployed_at)]));
|
|
174
162
|
}
|
|
175
163
|
if (card.schedules.length > 0) {
|
|
176
164
|
(0, ui_1.out)('');
|
|
177
|
-
(0, ui_1.out)('
|
|
165
|
+
(0, ui_1.out)('Schedules:');
|
|
178
166
|
(0, ui_1.table)(card.schedules.map((s) => [` ${s.function_name}`, s.cron_expression, s.status]));
|
|
179
167
|
}
|
|
180
168
|
}
|
|
@@ -14,8 +14,8 @@ async function schedulesList() {
|
|
|
14
14
|
const client = (0, session_1.connect)(config);
|
|
15
15
|
const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/schedules`);
|
|
16
16
|
if (data.schedules.length === 0) {
|
|
17
|
-
(0, ui_1.note)('
|
|
18
|
-
(0, ui_1.note)((0, ui_1.dim)('
|
|
17
|
+
(0, ui_1.note)('No schedules');
|
|
18
|
+
(0, ui_1.note)((0, ui_1.dim)(' To run a function on a timer: xflow schedules set <function> "0 3 ? * * *"'));
|
|
19
19
|
return;
|
|
20
20
|
}
|
|
21
21
|
(0, ui_1.table)(data.schedules.map((row) => [
|
|
@@ -31,21 +31,21 @@ async function schedulesSet(args) {
|
|
|
31
31
|
const functionName = args.words[1];
|
|
32
32
|
const cron = args.words[2];
|
|
33
33
|
if (!functionName || !cron) {
|
|
34
|
-
throw new errors_1.CliError('
|
|
34
|
+
throw new errors_1.CliError('A function name and a schedule are required', 'For example: xflow schedules set nightly-report "0 3 ? * * *" runs every day at 03:00 UTC');
|
|
35
35
|
}
|
|
36
36
|
const row = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/schedules`, {
|
|
37
37
|
method: 'POST',
|
|
38
38
|
body: { function: functionName, cron, payload: (0, args_1.flagString)(args, 'payload') ?? null },
|
|
39
39
|
});
|
|
40
|
-
(0, ui_1.ok)(`${(0, ui_1.bold)(row.function)}
|
|
41
|
-
(0, ui_1.note)((0, ui_1.dim)(`
|
|
40
|
+
(0, ui_1.ok)(`${(0, ui_1.bold)(row.function)} runs ${row.description}`);
|
|
41
|
+
(0, ui_1.note)((0, ui_1.dim)(` The time is UTC. To check by hand: xflow functions invoke ${row.function}`));
|
|
42
42
|
}
|
|
43
43
|
async function schedulesRemove(args) {
|
|
44
44
|
const { config } = (0, config_1.requireProject)();
|
|
45
45
|
const client = (0, session_1.connect)(config);
|
|
46
46
|
const functionName = args.words[1];
|
|
47
47
|
if (!functionName) {
|
|
48
|
-
throw new errors_1.CliError('
|
|
48
|
+
throw new errors_1.CliError('A function name is required', 'What runs on a timer: xflow schedules list');
|
|
49
49
|
}
|
|
50
50
|
const cron = args.words[2];
|
|
51
51
|
const query = new URLSearchParams({ function: functionName });
|
|
@@ -53,8 +53,8 @@ async function schedulesRemove(args) {
|
|
|
53
53
|
query.set('cron', cron);
|
|
54
54
|
const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/schedules?${query.toString()}`, { method: 'DELETE' });
|
|
55
55
|
if (result.removed === 0) {
|
|
56
|
-
(0, ui_1.note)(
|
|
56
|
+
(0, ui_1.note)(`Function ${functionName} has no such schedule`);
|
|
57
57
|
return;
|
|
58
58
|
}
|
|
59
|
-
(0, ui_1.ok)(
|
|
59
|
+
(0, ui_1.ok)(`Schedules removed: ${result.removed}`);
|
|
60
60
|
}
|
package/dist/commands/skills.js
CHANGED
|
@@ -9,54 +9,42 @@ 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)) {
|
|
49
|
-
throw new errors_1.CliError('
|
|
37
|
+
throw new errors_1.CliError('The CLI package has no skill file', 'Reinstall @getxflow/cli');
|
|
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,21 +81,15 @@ 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) {
|
|
112
|
-
(0, ui_1.note)((0, ui_1.dim)('
|
|
88
|
+
(0, ui_1.note)((0, ui_1.dim)(' The platform instructions are laid out for the AI agent'));
|
|
113
89
|
}
|
|
114
90
|
}
|
|
115
91
|
catch {
|
|
116
|
-
(0, ui_1.note)((0, ui_1.dim)('
|
|
92
|
+
(0, ui_1.note)((0, ui_1.dim)(' Could not lay out the AI agent instructions: xflow skills'));
|
|
117
93
|
}
|
|
118
94
|
}
|
|
119
95
|
function skills(args) {
|
|
@@ -122,7 +98,7 @@ function skills(args) {
|
|
|
122
98
|
for (const path of touched)
|
|
123
99
|
(0, ui_1.out)(path);
|
|
124
100
|
if (touched.length === 0)
|
|
125
|
-
(0, ui_1.note)((0, ui_1.dim)('
|
|
126
|
-
(0, ui_1.ok)(global ? '
|
|
127
|
-
(0, ui_1.note)((0, ui_1.dim)('
|
|
101
|
+
(0, ui_1.note)((0, ui_1.dim)(' everything is already up to date'));
|
|
102
|
+
(0, ui_1.ok)(global ? 'The xflow skill is available in every project' : 'The xflow skill is in place');
|
|
103
|
+
(0, ui_1.note)((0, ui_1.dim)(' After a CLI update run the command again: the skill ships with it'));
|
|
128
104
|
}
|
package/dist/commands/sources.js
CHANGED
|
@@ -15,25 +15,24 @@ 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}/`] : [])];
|
|
24
23
|
const { files, skippedLinks } = (0, tree_1.collectFiles)(root, (0, tree_1.loadIgnoreRules)(root, extra));
|
|
25
24
|
if (files.length === 0) {
|
|
26
|
-
throw new errors_1.CliError('
|
|
25
|
+
throw new errors_1.CliError('The folder has no files to send', 'Check .xflowignore: everything may be excluded');
|
|
27
26
|
}
|
|
28
27
|
for (const link of skippedLinks) {
|
|
29
|
-
(0, ui_1.warn)(
|
|
28
|
+
(0, ui_1.warn)(`Skipped the link ${link}: symbolic links are not synchronized`);
|
|
30
29
|
}
|
|
31
30
|
const archive = (0, zip_1.zipCreate)(files);
|
|
32
31
|
if (archive.length > MAX_ARCHIVE_BYTES) {
|
|
33
32
|
const top = (0, tree_1.heaviest)(files)
|
|
34
|
-
.map((f) => ` ${f.path}
|
|
33
|
+
.map((f) => ` ${f.path} ${(0, ui_1.formatBytes)(f.content.length)}`)
|
|
35
34
|
.join('\n');
|
|
36
|
-
throw new errors_1.CliError(
|
|
35
|
+
throw new errors_1.CliError(`The archive is ${(0, ui_1.formatBytes)(archive.length)}, the cap is ${(0, ui_1.formatBytes)(MAX_ARCHIVE_BYTES)}`, `The heaviest files:\n${top}\n Exclude what is not needed in .xflowignore`);
|
|
37
36
|
}
|
|
38
37
|
return { files, hash: (0, tree_1.treeHash)(files), archive };
|
|
39
38
|
}
|
|
@@ -41,56 +40,50 @@ 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)('');
|
|
53
|
-
(0, ui_1.warn)(
|
|
47
|
+
(0, ui_1.warn)(`The server holds revision ${server.revision} from ${(0, ui_1.formatAge)(server.created_at)}, ${server.file_count} files`);
|
|
54
48
|
try {
|
|
55
49
|
const info = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/pull`);
|
|
56
50
|
const serverPaths = new Set((0, zip_1.zipRead)(await (0, api_1.downloadUrl)(info.download_url)).map((e) => e.path));
|
|
57
51
|
const localPaths = new Set(local.map((f) => f.path));
|
|
58
52
|
const disappearing = [...serverPaths].filter((p) => !localPaths.has(p));
|
|
59
53
|
if (disappearing.length > 0) {
|
|
60
|
-
(0, ui_1.note)(`
|
|
54
|
+
(0, ui_1.note)(` Files that will disappear (${disappearing.length}):`);
|
|
61
55
|
for (const path of disappearing.slice(0, 20))
|
|
62
56
|
(0, ui_1.note)((0, ui_1.dim)(` ${path}`));
|
|
63
57
|
if (disappearing.length > 20)
|
|
64
|
-
(0, ui_1.note)((0, ui_1.dim)(`
|
|
58
|
+
(0, ui_1.note)((0, ui_1.dim)(` …and ${disappearing.length - 20} more`));
|
|
65
59
|
}
|
|
66
60
|
else {
|
|
67
|
-
(0, ui_1.note)((0, ui_1.dim)('
|
|
61
|
+
(0, ui_1.note)((0, ui_1.dim)(' No files disappear, but the contents of the server revision are replaced by yours'));
|
|
68
62
|
}
|
|
69
63
|
}
|
|
70
64
|
catch {
|
|
71
|
-
(0, ui_1.warn)('
|
|
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)(
|
|
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
|
-
throw new errors_1.CliError('
|
|
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
|
-
(0, ui_1.step)(
|
|
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 });
|
|
90
|
-
(0, ui_1.ok)(
|
|
83
|
+
(0, ui_1.ok)(`Already in sync, revision ${server.revision}`);
|
|
91
84
|
return { revision: server.revision, status: 'unchanged' };
|
|
92
85
|
}
|
|
93
|
-
throw new errors_1.CliError(
|
|
86
|
+
throw new errors_1.CliError(`The server holds revision ${server.revision} while this folder does not remember which version it worked from`, 'Fetch the server copy alongside and compare: xflow pull --into ./server-copy');
|
|
94
87
|
}
|
|
95
88
|
if (options.force && server && server.tree_hash !== tree.hash) {
|
|
96
89
|
await confirmForce(client, config.projectId, server, tree.files);
|
|
@@ -103,10 +96,10 @@ async function pushSources(root, config, client, options) {
|
|
|
103
96
|
const result = await (0, api_1.apiUpload)(client, `/api/v1/projects/${config.projectId}/push`, tree.archive, headers);
|
|
104
97
|
(0, config_1.writeState)(root, { revision: result.revision, treeHash: result.tree_hash });
|
|
105
98
|
if (result.status === 'unchanged') {
|
|
106
|
-
(0, ui_1.ok)(
|
|
99
|
+
(0, ui_1.ok)(`No changes, revision ${result.revision}`);
|
|
107
100
|
}
|
|
108
101
|
else {
|
|
109
|
-
(0, ui_1.ok)(
|
|
102
|
+
(0, ui_1.ok)(`Revision ${result.revision}: ${result.file_count} files, ${(0, ui_1.formatBytes)(result.size_bytes)}`);
|
|
110
103
|
}
|
|
111
104
|
return { revision: result.revision, status: result.status };
|
|
112
105
|
}
|
|
@@ -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;
|
|
@@ -130,14 +122,14 @@ async function pull(args) {
|
|
|
130
122
|
const query = revision !== undefined ? `?revision=${revision}` : '';
|
|
131
123
|
const info = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/pull${query}`);
|
|
132
124
|
if (hasContent(target) && !(0, args_1.flagBool)(args, 'force')) {
|
|
133
|
-
throw new errors_1.CliError(
|
|
134
|
-
'xflow pull --into ./server-copy,
|
|
125
|
+
throw new errors_1.CliError(`The directory ${target} is not empty`, 'The platform cannot merge changes, that is git work. Fetch the server copy alongside: ' +
|
|
126
|
+
'xflow pull --into ./server-copy, or overwrite the folder completely: xflow pull --force');
|
|
135
127
|
}
|
|
136
|
-
(0, ui_1.step)(
|
|
128
|
+
(0, ui_1.step)(`Downloading revision ${info.revision} (${(0, ui_1.formatBytes)(info.size_bytes)})`);
|
|
137
129
|
const entries = (0, zip_1.zipRead)(await (0, api_1.downloadUrl)(info.download_url));
|
|
138
130
|
const hash = (0, tree_1.treeHash)(entries.map((e) => ({ path: e.path, content: e.content })));
|
|
139
131
|
if (hash !== info.tree_hash) {
|
|
140
|
-
(0, ui_1.warn)('
|
|
132
|
+
(0, ui_1.warn)('The hash of the downloaded tree did not match the server one: the contents may have changed in transit');
|
|
141
133
|
}
|
|
142
134
|
(0, node_fs_1.mkdirSync)(target, { recursive: true });
|
|
143
135
|
for (const entry of entries) {
|
|
@@ -145,12 +137,11 @@ 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
|
}
|
|
153
|
-
(0, ui_1.ok)(
|
|
144
|
+
(0, ui_1.ok)(`Revision ${info.revision}: ${entries.length} files in ${target}`);
|
|
154
145
|
}
|
|
155
146
|
async function status() {
|
|
156
147
|
const { root, config } = (0, config_1.requireProject)();
|
|
@@ -163,28 +154,28 @@ async function status() {
|
|
|
163
154
|
]);
|
|
164
155
|
(0, ui_1.out)(`${(0, ui_1.bold)(card.name)} ${(0, ui_1.dim)(card.id)}`);
|
|
165
156
|
(0, ui_1.out)('');
|
|
166
|
-
(0, ui_1.out)(
|
|
157
|
+
(0, ui_1.out)(`Local: ${tree.files.length} files, ${(0, ui_1.formatBytes)(tree.archive.length)} archived`);
|
|
167
158
|
if (!server) {
|
|
168
|
-
(0, ui_1.out)('
|
|
169
|
-
(0, ui_1.note)((0, ui_1.dim)('
|
|
159
|
+
(0, ui_1.out)('On the server: no sources yet');
|
|
160
|
+
(0, ui_1.note)((0, ui_1.dim)(' Send them: xflow push'));
|
|
170
161
|
}
|
|
171
162
|
else {
|
|
172
|
-
(0, ui_1.out)(
|
|
163
|
+
(0, ui_1.out)(`On the server: revision ${server.revision}, ${server.file_count} files, ${(0, ui_1.formatAge)(server.created_at)}`);
|
|
173
164
|
if (server.tree_hash === tree.hash) {
|
|
174
|
-
(0, ui_1.out)('
|
|
165
|
+
(0, ui_1.out)('State: matches the server');
|
|
175
166
|
}
|
|
176
167
|
else if (state.revision !== undefined && state.revision < server.revision) {
|
|
177
|
-
(0, ui_1.out)(
|
|
178
|
-
(0, ui_1.note)((0, ui_1.dim)('
|
|
168
|
+
(0, ui_1.out)(`State: ${(0, ui_1.bold)('diverged')}, the server is newer (you worked from ${state.revision})`);
|
|
169
|
+
(0, ui_1.note)((0, ui_1.dim)(' Fetch the server copy alongside: xflow pull --into ./server-copy'));
|
|
179
170
|
}
|
|
180
171
|
else {
|
|
181
|
-
(0, ui_1.out)(
|
|
182
|
-
(0, ui_1.note)((0, ui_1.dim)('
|
|
172
|
+
(0, ui_1.out)(`State: ${(0, ui_1.bold)('local changes')}`);
|
|
173
|
+
(0, ui_1.note)((0, ui_1.dim)(' Send them: xflow push'));
|
|
183
174
|
}
|
|
184
175
|
}
|
|
185
176
|
(0, ui_1.out)('');
|
|
186
|
-
(0, ui_1.out)(
|
|
187
|
-
(0, ui_1.out)(
|
|
177
|
+
(0, ui_1.out)(`Built version: ${card.dev_deploy_id ?? '(no build)'}`);
|
|
178
|
+
(0, ui_1.out)(`Visitors see: ${card.live_deploy_id ?? '(never published)'}`);
|
|
188
179
|
(0, ui_1.out)('');
|
|
189
180
|
(0, ui_1.out)(card.project_url);
|
|
190
181
|
}
|