@getxflow/cli 0.5.0 → 0.6.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 +3 -1
- package/dist/api.js +10 -1
- package/dist/args.js +1 -0
- package/dist/bin.js +25 -0
- package/dist/commands/skills.js +38 -1
- package/dist/commands/update.js +161 -0
- package/dist/commands/update.test.js +34 -0
- package/dist/help.js +27 -1
- package/dist/limits.js +8 -3
- package/dist/limits.test.js +25 -0
- package/dist/state.js +46 -0
- package/dist/version.js +1 -1
- package/package.json +22 -22
- package/skills/xflow/SKILL.md +4 -0
package/README.md
CHANGED
|
@@ -30,13 +30,15 @@ else's code belongs next to it.
|
|
|
30
30
|
| `projects list` / `projects get` / `open` | projects of the organization and the application addresses |
|
|
31
31
|
| `skills` | platform instructions for an AI agent ([Agent Skills](https://agentskills.io) format) |
|
|
32
32
|
| `mcp install` | connect the agent to the platform directly, without a terminal |
|
|
33
|
+
| `update` | update the CLI itself, and the skill that ships inside it |
|
|
33
34
|
|
|
34
35
|
Help for one command: `xflow help <command>`.
|
|
35
36
|
|
|
36
37
|
You write the application yourself or with your agent: the platform stays out of the code.
|
|
37
38
|
`xflow skills` puts a description of the platform alongside it, which Claude Code, Codex and
|
|
38
39
|
OpenClaw pick up on their own when a task touches XFlow. Your `AGENTS.md` and `CLAUDE.md` are
|
|
39
|
-
left alone.
|
|
40
|
+
left alone. The description ships inside the package and shares its version, so `xflow update`
|
|
41
|
+
brings both.
|
|
40
42
|
|
|
41
43
|
`xflow mcp install` connects the agent to the platform directly: the database, migrations,
|
|
42
44
|
functions, schedules, variables and versions become its tools, with no command output to parse.
|
package/dist/api.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ApiError = void 0;
|
|
4
|
+
exports.newerVersionSeen = newerVersionSeen;
|
|
4
5
|
exports.apiJson = apiJson;
|
|
5
6
|
exports.apiBinary = apiBinary;
|
|
6
7
|
exports.apiUpload = apiUpload;
|
|
@@ -48,10 +49,18 @@ function isOlder(current, required) {
|
|
|
48
49
|
}
|
|
49
50
|
return false;
|
|
50
51
|
}
|
|
52
|
+
let newerVersion = null;
|
|
53
|
+
/** The published version the platform reported, if it is newer than this copy. */
|
|
54
|
+
function newerVersionSeen() {
|
|
55
|
+
return newerVersion;
|
|
56
|
+
}
|
|
51
57
|
function assertProtocol(response) {
|
|
58
|
+
const latest = response.headers.get('x-xflow-cli-latest');
|
|
59
|
+
if (latest && isOlder(version_1.CLI_VERSION, latest))
|
|
60
|
+
newerVersion = latest;
|
|
52
61
|
const required = response.headers.get('x-xflow-cli-min');
|
|
53
62
|
if (required && isOlder(version_1.CLI_VERSION, required)) {
|
|
54
|
-
throw new ApiError(`The platform requires CLI ${required} or newer, ${version_1.CLI_VERSION} is installed`, 'outdated_cli', response.status, '
|
|
63
|
+
throw new ApiError(`The platform requires CLI ${required} or newer, ${version_1.CLI_VERSION} is installed`, 'outdated_cli', response.status, 'Run: xflow update');
|
|
55
64
|
}
|
|
56
65
|
}
|
|
57
66
|
async function send(client, path, options = {}) {
|
package/dist/args.js
CHANGED
package/dist/bin.js
CHANGED
|
@@ -7,6 +7,7 @@ const config_1 = require("./config");
|
|
|
7
7
|
const errors_1 = require("./errors");
|
|
8
8
|
const help_1 = require("./help");
|
|
9
9
|
const limits_1 = require("./limits");
|
|
10
|
+
const state_1 = require("./state");
|
|
10
11
|
const ui_1 = require("./ui");
|
|
11
12
|
const zip_1 = require("./zip");
|
|
12
13
|
const version_1 = require("./version");
|
|
@@ -21,6 +22,7 @@ const schedules_1 = require("./commands/schedules");
|
|
|
21
22
|
const skills_1 = require("./commands/skills");
|
|
22
23
|
const sources_1 = require("./commands/sources");
|
|
23
24
|
const deploy_1 = require("./commands/deploy");
|
|
25
|
+
const update_1 = require("./commands/update");
|
|
24
26
|
async function run(args) {
|
|
25
27
|
const [first, second] = args.words;
|
|
26
28
|
const rest = { ...args, words: args.words.slice(1) };
|
|
@@ -156,10 +158,25 @@ async function run(args) {
|
|
|
156
158
|
case 'deployments':
|
|
157
159
|
await (0, deploy_1.deployments)();
|
|
158
160
|
return;
|
|
161
|
+
case 'update':
|
|
162
|
+
(0, update_1.update)();
|
|
163
|
+
return;
|
|
159
164
|
default:
|
|
160
165
|
throw new errors_1.CliError(`Unknown command: ${first}`, 'The list of commands: xflow help');
|
|
161
166
|
}
|
|
162
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* The platform reports the published version on every response, so this costs no
|
|
170
|
+
* request. One line, after the command has had its say, and at most once a day:
|
|
171
|
+
* an agent runs a dozen commands per session and cannot act on twelve reminders.
|
|
172
|
+
*/
|
|
173
|
+
function nudge() {
|
|
174
|
+
const latest = (0, api_1.newerVersionSeen)();
|
|
175
|
+
if (latest === null || !(0, state_1.shouldNudge)(latest))
|
|
176
|
+
return;
|
|
177
|
+
(0, ui_1.note)((0, ui_1.dim)(` xflow ${latest} is out, ${version_1.CLI_VERSION} is installed: run xflow update`));
|
|
178
|
+
(0, state_1.markNudged)(latest);
|
|
179
|
+
}
|
|
163
180
|
async function main() {
|
|
164
181
|
const args = (0, args_1.parseArgs)(process.argv.slice(2));
|
|
165
182
|
if ((0, args_1.flagBool)(args, 'version')) {
|
|
@@ -170,6 +187,9 @@ async function main() {
|
|
|
170
187
|
(0, help_1.help)(args.words[0]);
|
|
171
188
|
return 0;
|
|
172
189
|
}
|
|
190
|
+
// The update command says it better itself, and the min-version refusal already
|
|
191
|
+
// carries the same advice in its hint.
|
|
192
|
+
let quiet = args.words[0] === 'update';
|
|
173
193
|
try {
|
|
174
194
|
await run(args);
|
|
175
195
|
return 0;
|
|
@@ -181,6 +201,7 @@ async function main() {
|
|
|
181
201
|
(0, ui_1.note)((0, ui_1.dim)(` ${(0, limits_1.limitLine)(e.limit)}`));
|
|
182
202
|
if (e.hint)
|
|
183
203
|
(0, ui_1.note)((0, ui_1.dim)(` ${e.hint}`));
|
|
204
|
+
quiet = quiet || e.code === 'outdated_cli';
|
|
184
205
|
return 1;
|
|
185
206
|
}
|
|
186
207
|
if (e instanceof errors_1.CliError || e instanceof config_1.ConfigError || e instanceof zip_1.ZipError) {
|
|
@@ -195,6 +216,10 @@ async function main() {
|
|
|
195
216
|
(0, ui_1.note)((0, ui_1.dim)(e.stack));
|
|
196
217
|
return 1;
|
|
197
218
|
}
|
|
219
|
+
finally {
|
|
220
|
+
if (!quiet)
|
|
221
|
+
nudge();
|
|
222
|
+
}
|
|
198
223
|
}
|
|
199
224
|
main().then((code) => {
|
|
200
225
|
process.exitCode = code;
|
package/dist/commands/skills.js
CHANGED
|
@@ -99,6 +99,33 @@ function install(base, ids, global) {
|
|
|
99
99
|
}
|
|
100
100
|
return changes;
|
|
101
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Rewrite every copy that already exists and create none: this is what xflow update
|
|
104
|
+
* calls, and it runs wherever the user happened to stand. Installing defaults here
|
|
105
|
+
* would litter a random folder with .claude, .agents and an AGENTS.md.
|
|
106
|
+
*/
|
|
107
|
+
function refreshInstalled(base) {
|
|
108
|
+
const skill = skillSource();
|
|
109
|
+
const changes = [];
|
|
110
|
+
const seen = new Set();
|
|
111
|
+
const put = (path, content) => {
|
|
112
|
+
if (seen.has(path) || !(0, node_fs_1.existsSync)(path))
|
|
113
|
+
return;
|
|
114
|
+
seen.add(path);
|
|
115
|
+
const state = writeIfChanged(path, content);
|
|
116
|
+
if (state)
|
|
117
|
+
changes.push({ path, state });
|
|
118
|
+
};
|
|
119
|
+
for (const agent of AGENTS) {
|
|
120
|
+
put(skillPath(base, agent, false), skill);
|
|
121
|
+
put(skillPath(base, agent, true), skill);
|
|
122
|
+
// The Cursor rule is not one of the agent paths: without this line it keeps the
|
|
123
|
+
// old text next to a fresh SKILL.md, and the two drift apart unnoticed.
|
|
124
|
+
if (agent.rule === true)
|
|
125
|
+
put((0, node_path_1.join)(base, ...CURSOR_RULE), cursorRule(skill));
|
|
126
|
+
}
|
|
127
|
+
return changes;
|
|
128
|
+
}
|
|
102
129
|
/** Defaults plus whatever is already installed, so an update run refreshes everything. */
|
|
103
130
|
function defaultIds(base, global) {
|
|
104
131
|
const ids = new Set(global ? DEFAULT_GLOBAL : DEFAULT_PROJECT);
|
|
@@ -163,6 +190,16 @@ async function skills(args) {
|
|
|
163
190
|
listAgents(process.cwd());
|
|
164
191
|
return;
|
|
165
192
|
}
|
|
193
|
+
if ((0, args_1.flagBool)(args, 'refresh')) {
|
|
194
|
+
const refreshed = refreshInstalled(process.cwd());
|
|
195
|
+
for (const change of refreshed)
|
|
196
|
+
(0, ui_1.out)(`~ ${change.path}`);
|
|
197
|
+
if (refreshed.length === 0)
|
|
198
|
+
(0, ui_1.note)((0, ui_1.dim)(' nothing installed here, nothing to refresh'));
|
|
199
|
+
else
|
|
200
|
+
(0, ui_1.ok)('The xflow skill matches the CLI');
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
166
203
|
if (sub !== undefined) {
|
|
167
204
|
throw new errors_1.CliError(`Unknown command: skills ${sub}`, 'Available: xflow skills [list]');
|
|
168
205
|
}
|
|
@@ -201,5 +238,5 @@ async function skills(args) {
|
|
|
201
238
|
if (changes.length === 0)
|
|
202
239
|
(0, ui_1.note)((0, ui_1.dim)(' everything is already up to date'));
|
|
203
240
|
(0, ui_1.ok)(global ? 'The xflow skill is available in every project' : 'The xflow skill is in place');
|
|
204
|
-
(0, ui_1.note)((0, ui_1.dim)('
|
|
241
|
+
(0, ui_1.note)((0, ui_1.dim)(' The skill ships with the CLI: xflow update brings both'));
|
|
205
242
|
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.classifyPath = classifyPath;
|
|
4
|
+
exports.update = update;
|
|
5
|
+
const node_child_process_1 = require("node:child_process");
|
|
6
|
+
const node_fs_1 = require("node:fs");
|
|
7
|
+
const node_path_1 = require("node:path");
|
|
8
|
+
const errors_1 = require("../errors");
|
|
9
|
+
const ui_1 = require("../ui");
|
|
10
|
+
const version_1 = require("../version");
|
|
11
|
+
const PACKAGE = '@getxflow/cli';
|
|
12
|
+
const WINDOWS = process.platform === 'win32';
|
|
13
|
+
function classifyPath(dir) {
|
|
14
|
+
const parts = dir.split(/[\\/]/).map((part) => part.toLowerCase());
|
|
15
|
+
if (parts.includes('_npx'))
|
|
16
|
+
return 'npx';
|
|
17
|
+
if (parts.includes('pnpm') || parts.includes('.pnpm'))
|
|
18
|
+
return 'pnpm';
|
|
19
|
+
if (parts.includes('.bun'))
|
|
20
|
+
return 'bun';
|
|
21
|
+
if (parts.includes('.yarn'))
|
|
22
|
+
return 'yarn';
|
|
23
|
+
// A global npm prefix has a node_modules too, so this only separates a source
|
|
24
|
+
// checkout (npm link, tsx cli/src) from everything installed.
|
|
25
|
+
if (!parts.includes('node_modules'))
|
|
26
|
+
return 'checkout';
|
|
27
|
+
return 'node_modules';
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The manifest of a project that depends on the package, if this copy is such a
|
|
31
|
+
* dependency. A dependency and a tool need opposite answers: inside npm scripts the
|
|
32
|
+
* local .bin wins over PATH, so a global install would leave the old version running.
|
|
33
|
+
*/
|
|
34
|
+
function projectManifest(dir) {
|
|
35
|
+
let current = dir;
|
|
36
|
+
for (let depth = 0; depth < 12; depth++) {
|
|
37
|
+
const parent = (0, node_path_1.dirname)(current);
|
|
38
|
+
if (parent === current)
|
|
39
|
+
return null;
|
|
40
|
+
if (current.split(/[\\/]/).pop()?.toLowerCase() === 'node_modules') {
|
|
41
|
+
const path = (0, node_path_1.join)(parent, 'package.json');
|
|
42
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
43
|
+
return null;
|
|
44
|
+
try {
|
|
45
|
+
const pkg = JSON.parse((0, node_fs_1.readFileSync)(path, 'utf-8'));
|
|
46
|
+
const declared = pkg.dependencies?.[PACKAGE] ?? pkg.devDependencies?.[PACKAGE];
|
|
47
|
+
return declared === undefined ? null : path;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
current = parent;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
function npmRoot() {
|
|
58
|
+
try {
|
|
59
|
+
const output = (0, node_child_process_1.execFileSync)('npm', ['root', '-g'], {
|
|
60
|
+
encoding: 'utf-8',
|
|
61
|
+
shell: WINDOWS,
|
|
62
|
+
timeout: 60_000,
|
|
63
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
64
|
+
});
|
|
65
|
+
const dir = output.trim();
|
|
66
|
+
return dir.length > 0 && (0, node_fs_1.existsSync)(dir) ? dir : null;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function packageDir(root) {
|
|
73
|
+
return (0, node_path_1.join)(root, '@getxflow', 'cli');
|
|
74
|
+
}
|
|
75
|
+
function installedVersion(root) {
|
|
76
|
+
try {
|
|
77
|
+
const pkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(packageDir(root), 'package.json'), 'utf-8'));
|
|
78
|
+
return pkg.version ?? null;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The skill ships inside the package and shares its version, so a fresh CLI with a
|
|
86
|
+
* stale skill is an agent reading last month's instructions. Run through the freshly
|
|
87
|
+
* installed binary by absolute path: PATH may point at another copy, and this process
|
|
88
|
+
* still holds the old code in memory.
|
|
89
|
+
*/
|
|
90
|
+
function refreshSkill(root) {
|
|
91
|
+
const bin = (0, node_path_1.join)(packageDir(root), 'dist', 'bin.js');
|
|
92
|
+
if (!(0, node_fs_1.existsSync)(bin)) {
|
|
93
|
+
(0, ui_1.note)((0, ui_1.dim)(' Refresh the agent instructions yourself: xflow skills'));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const result = (0, node_child_process_1.spawnSync)(process.execPath, [bin, 'skills', '--refresh'], { stdio: 'inherit' });
|
|
97
|
+
if (result.error || result.status !== 0) {
|
|
98
|
+
(0, ui_1.note)((0, ui_1.dim)(' Could not refresh the agent instructions, run xflow skills'));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function manual(command, why) {
|
|
102
|
+
(0, ui_1.note)((0, ui_1.dim)(` ${why}`));
|
|
103
|
+
(0, ui_1.note)(` ${command}`);
|
|
104
|
+
}
|
|
105
|
+
function update() {
|
|
106
|
+
const kind = classifyPath(__dirname);
|
|
107
|
+
if (kind === 'checkout') {
|
|
108
|
+
(0, ui_1.ok)(`xflow ${version_1.CLI_VERSION} runs from a source checkout`);
|
|
109
|
+
(0, ui_1.note)((0, ui_1.dim)(' Nothing to update here: pull the repository and rebuild'));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (kind === 'npx') {
|
|
113
|
+
(0, ui_1.ok)(`xflow ${version_1.CLI_VERSION} runs from the npx cache`);
|
|
114
|
+
manual(`npm i -g ${PACKAGE}@latest`, 'npx keeps its own copy. Install the CLI properly:');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (kind === 'pnpm') {
|
|
118
|
+
manual(`pnpm add -g ${PACKAGE}@latest`, 'Installed by pnpm, so npm must not touch it:');
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (kind === 'bun') {
|
|
122
|
+
manual(`bun add -g ${PACKAGE}@latest`, 'Installed by bun, so npm must not touch it:');
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (kind === 'yarn') {
|
|
126
|
+
manual(`yarn global add ${PACKAGE}@latest`, 'Installed by yarn, so npm must not touch it:');
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const manifest = projectManifest(__dirname);
|
|
130
|
+
if (manifest !== null) {
|
|
131
|
+
(0, ui_1.ok)(`xflow ${version_1.CLI_VERSION} is a dependency of this project`);
|
|
132
|
+
manual(`npm i -D ${PACKAGE}@latest`, `Declared in ${manifest}. A global install would change nothing: inside npm scripts the local copy wins`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
(0, ui_1.step)(`updating ${PACKAGE} from ${version_1.CLI_VERSION}`);
|
|
136
|
+
const result = (0, node_child_process_1.spawnSync)('npm', ['i', '-g', `${PACKAGE}@latest`], {
|
|
137
|
+
stdio: 'inherit',
|
|
138
|
+
shell: WINDOWS,
|
|
139
|
+
});
|
|
140
|
+
if (result.error) {
|
|
141
|
+
throw new errors_1.CliError(`Could not run npm: ${result.error.message}`, `Install by hand: npm i -g ${PACKAGE}@latest`);
|
|
142
|
+
}
|
|
143
|
+
if (result.status !== 0) {
|
|
144
|
+
throw new errors_1.CliError('npm could not install the package', WINDOWS
|
|
145
|
+
? 'The npm output is above. Windows also locks the xflow.cmd of a running command: close the other terminals and repeat'
|
|
146
|
+
: 'The npm output is above. A permission error means the global folder belongs to root: fix npm rather than run it with sudo');
|
|
147
|
+
}
|
|
148
|
+
const root = npmRoot();
|
|
149
|
+
const after = root === null ? null : installedVersion(root);
|
|
150
|
+
if (root === null || after === null) {
|
|
151
|
+
(0, ui_1.ok)(`${PACKAGE} installed`);
|
|
152
|
+
(0, ui_1.note)((0, ui_1.dim)(' Check what is on disk now: xflow --version'));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (after === version_1.CLI_VERSION) {
|
|
156
|
+
(0, ui_1.ok)(`xflow ${after} is the latest version`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
(0, ui_1.ok)(`xflow ${version_1.CLI_VERSION} → ${after}`);
|
|
160
|
+
refreshSkill(root);
|
|
161
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
7
|
+
const node_test_1 = require("node:test");
|
|
8
|
+
const update_1 = require("./update");
|
|
9
|
+
/**
|
|
10
|
+
* The price of a mistake here is npm running over a pnpm or bun installation, which
|
|
11
|
+
* leaves two copies and a user who cannot tell which one answers. Path shapes only,
|
|
12
|
+
* the rest of the command is a run, not a rule.
|
|
13
|
+
*/
|
|
14
|
+
(0, node_test_1.test)('a global npm prefix is an ordinary install', () => {
|
|
15
|
+
strict_1.default.equal((0, update_1.classifyPath)('C:\\Users\\me\\AppData\\Roaming\\npm\\node_modules\\@getxflow\\cli\\dist'), 'node_modules');
|
|
16
|
+
strict_1.default.equal((0, update_1.classifyPath)('/usr/local/lib/node_modules/@getxflow/cli/dist'), 'node_modules');
|
|
17
|
+
strict_1.default.equal((0, update_1.classifyPath)('/home/me/.npm-global/lib/node_modules/@getxflow/cli/dist'), 'node_modules');
|
|
18
|
+
});
|
|
19
|
+
(0, node_test_1.test)('npm as a path segment is not pnpm', () => {
|
|
20
|
+
strict_1.default.notEqual((0, update_1.classifyPath)('C:\\Users\\me\\AppData\\Roaming\\npm\\node_modules\\@getxflow\\cli\\dist'), 'pnpm');
|
|
21
|
+
});
|
|
22
|
+
(0, node_test_1.test)('other package managers are recognized by their own folders', () => {
|
|
23
|
+
strict_1.default.equal((0, update_1.classifyPath)('/home/me/.local/share/pnpm/global/5/node_modules/@getxflow/cli/dist'), 'pnpm');
|
|
24
|
+
strict_1.default.equal((0, update_1.classifyPath)('/home/me/project/node_modules/.pnpm/@getxflow+cli@0.6.0/node_modules/@getxflow/cli/dist'), 'pnpm');
|
|
25
|
+
strict_1.default.equal((0, update_1.classifyPath)('/home/me/.bun/install/global/node_modules/@getxflow/cli/dist'), 'bun');
|
|
26
|
+
strict_1.default.equal((0, update_1.classifyPath)('/home/me/project/.yarn/cache/@getxflow-cli/dist'), 'yarn');
|
|
27
|
+
});
|
|
28
|
+
(0, node_test_1.test)('an npx run is a cached copy, not an installation', () => {
|
|
29
|
+
strict_1.default.equal((0, update_1.classifyPath)('/home/me/.npm/_npx/8f1a2b/node_modules/@getxflow/cli/dist'), 'npx');
|
|
30
|
+
});
|
|
31
|
+
(0, node_test_1.test)('without node_modules the code runs from a checkout', () => {
|
|
32
|
+
strict_1.default.equal((0, update_1.classifyPath)('C:\\cursor\\xflow 2.0\\cli\\dist'), 'checkout');
|
|
33
|
+
strict_1.default.equal((0, update_1.classifyPath)('/home/me/xflow/cli/src'), 'checkout');
|
|
34
|
+
});
|
package/dist/help.js
CHANGED
|
@@ -57,6 +57,7 @@ ${(0, ui_1.bold)('Reference')}
|
|
|
57
57
|
xflow projects get [id] project card
|
|
58
58
|
xflow whoami whose key this is and what it can do
|
|
59
59
|
xflow logout forget the key
|
|
60
|
+
xflow update update the CLI itself, and the skill that ships with it
|
|
60
61
|
|
|
61
62
|
${(0, ui_1.bold)('Environment')}
|
|
62
63
|
XFLOW_TOKEN access key (for CI, instead of xflow login)
|
|
@@ -65,6 +66,29 @@ ${(0, ui_1.bold)('Environment')}
|
|
|
65
66
|
More about one command: xflow help <command>`);
|
|
66
67
|
}
|
|
67
68
|
const TOPICS = {
|
|
69
|
+
update: `${(0, ui_1.bold)('xflow update')}: bring the CLI up to date
|
|
70
|
+
|
|
71
|
+
Installs the published version and, if anything changed, rewrites the platform
|
|
72
|
+
instructions the agents already have: the skill ships inside the package and shares
|
|
73
|
+
its version, so a fresh CLI next to a stale skill means the agent is reading last
|
|
74
|
+
month's rules. Copies that are not installed are not created, so it is safe to run
|
|
75
|
+
from any folder.
|
|
76
|
+
|
|
77
|
+
The platform reports the published version with every answer, which is where the
|
|
78
|
+
${(0, ui_1.bold)('xflow N is out')} line comes from. It appears once a day at most, and never in CI,
|
|
79
|
+
where nobody is going to update anything anyway.
|
|
80
|
+
|
|
81
|
+
The command works out how this copy was installed from its own path, and refuses to
|
|
82
|
+
cross package managers: an install made by pnpm or bun is printed as their own
|
|
83
|
+
command rather than handed to npm, which would leave you with two copies and no way
|
|
84
|
+
to tell which one answers. Two more cases end the same way. Under ${(0, ui_1.bold)('npx')} there is
|
|
85
|
+
nothing to update, only a cache. And a copy inside a project's node_modules is a
|
|
86
|
+
dependency of that project: ${(0, ui_1.bold)('npm i -D @getxflow/cli@latest')} is the fix, because
|
|
87
|
+
inside npm scripts the local copy wins over anything installed globally.
|
|
88
|
+
|
|
89
|
+
A refusal from the platform saying the CLI is too old is a different thing: that one
|
|
90
|
+
is a hard gate on a changed contract, and until you run this command nothing else
|
|
91
|
+
will work.`,
|
|
68
92
|
db: `${(0, ui_1.bold)('xflow db')}: schema, data, migrations
|
|
69
93
|
|
|
70
94
|
xflow db status what is applied and what is waiting
|
|
@@ -199,7 +223,8 @@ An agent has no way to know about XFlow: the platform is not in its training. Th
|
|
|
199
223
|
instructions explain how to release and publish, where the design system components
|
|
200
224
|
come from, and where to look for production errors. ${(0, ui_1.bold)('init')} and ${(0, ui_1.bold)('link')} lay them
|
|
201
225
|
down on their own for the common agents, so this command exists to pick tools
|
|
202
|
-
precisely
|
|
226
|
+
precisely. Refreshing after a CLI update is not your job any more: the skill and the
|
|
227
|
+
CLI ship as one version, and ${(0, ui_1.bold)('xflow update')} rewrites the copies you already have.
|
|
203
228
|
|
|
204
229
|
Run in a terminal, it asks two questions: where (this project, or the home folder,
|
|
205
230
|
which makes the skill visible in every project) and for which agents. Copies that are
|
|
@@ -211,6 +236,7 @@ already installed copy are refreshed.
|
|
|
211
236
|
--agent claude,cursor exact agents, no questions asked
|
|
212
237
|
--global the home folder instead of the project
|
|
213
238
|
--yes no questions: defaults plus what is installed
|
|
239
|
+
--refresh rewrite the copies that exist, create none
|
|
214
240
|
|
|
215
241
|
The format is shared (agentskills.io) and inside a project most tools read the shared
|
|
216
242
|
.agents/skills folder, so the paths differ mostly in the home directory. Cursor also
|
package/dist/limits.js
CHANGED
|
@@ -17,9 +17,14 @@ const DENIAL_LABELS = {
|
|
|
17
17
|
/** Why repeating the same command will not help. */
|
|
18
18
|
function limitLine(detail) {
|
|
19
19
|
const label = DENIAL_LABELS[detail.code] ?? detail.code;
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
const counted = detail.used !== undefined && detail.limit !== undefined;
|
|
21
|
+
// An interval is not a tally: "5 of 60 used" reads as five minutes spent out
|
|
22
|
+
// of sixty, when it means a run every five minutes against a sixty minimum.
|
|
23
|
+
const numbers = !counted
|
|
24
|
+
? ''
|
|
25
|
+
: detail.code === 'limit_schedule_interval'
|
|
26
|
+
? `, every ${detail.used} min against a ${detail.limit} min minimum`
|
|
27
|
+
: `, ${detail.used} of ${detail.limit} used`;
|
|
23
28
|
return `Plan limit: ${label}${numbers}. Repeating the same command will not help.`;
|
|
24
29
|
}
|
|
25
30
|
const QUOTA_ORDER = [
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_test_1 = require("node:test");
|
|
7
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
8
|
+
const limits_1 = require("./limits");
|
|
9
|
+
(0, node_test_1.test)('a counted limit reads as a tally', () => {
|
|
10
|
+
const line = (0, limits_1.limitLine)({ code: 'limit_functions', used: 29, limit: 29 });
|
|
11
|
+
strict_1.default.equal(line, 'Plan limit: cloud functions, 29 of 29 used. Repeating the same command will not help.');
|
|
12
|
+
});
|
|
13
|
+
(0, node_test_1.test)('an interval reads as a frequency, not as a tally', () => {
|
|
14
|
+
const line = (0, limits_1.limitLine)({ code: 'limit_schedule_interval', used: 5, limit: 60 });
|
|
15
|
+
strict_1.default.match(line, /every 5 min against a 60 min minimum/);
|
|
16
|
+
strict_1.default.doesNotMatch(line, /used/);
|
|
17
|
+
});
|
|
18
|
+
(0, node_test_1.test)('a refusal without numbers stays a plain sentence', () => {
|
|
19
|
+
const line = (0, limits_1.limitLine)({ code: 'plan_no_schedules' });
|
|
20
|
+
strict_1.default.equal(line, 'Plan limit: schedules. Repeating the same command will not help.');
|
|
21
|
+
});
|
|
22
|
+
(0, node_test_1.test)('an unknown code falls back to the code itself', () => {
|
|
23
|
+
const line = (0, limits_1.limitLine)({ code: 'limit_unheard_of' });
|
|
24
|
+
strict_1.default.match(line, /Plan limit: limit_unheard_of\./);
|
|
25
|
+
});
|
package/dist/state.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.shouldNudge = shouldNudge;
|
|
4
|
+
exports.markNudged = markNudged;
|
|
5
|
+
const node_fs_1 = require("node:fs");
|
|
6
|
+
const node_os_1 = require("node:os");
|
|
7
|
+
const node_path_1 = require("node:path");
|
|
8
|
+
const NUDGE_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
9
|
+
function statePath() {
|
|
10
|
+
return (0, node_path_1.join)((0, node_os_1.homedir)(), '.xflow', 'state.json');
|
|
11
|
+
}
|
|
12
|
+
function read() {
|
|
13
|
+
const path = statePath();
|
|
14
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
15
|
+
return {};
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse((0, node_fs_1.readFileSync)(path, 'utf-8'));
|
|
18
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Once a day per version. CI never updates the CLI, so it hears nothing. */
|
|
25
|
+
function shouldNudge(version) {
|
|
26
|
+
if (process.env.CI)
|
|
27
|
+
return false;
|
|
28
|
+
const state = read();
|
|
29
|
+
if (state.nudgedVersion !== version)
|
|
30
|
+
return true;
|
|
31
|
+
const at = state.nudgedAt ? Date.parse(state.nudgedAt) : Number.NaN;
|
|
32
|
+
if (!Number.isFinite(at))
|
|
33
|
+
return true;
|
|
34
|
+
return Date.now() - at > NUDGE_INTERVAL_MS;
|
|
35
|
+
}
|
|
36
|
+
/** Best effort: a read-only home folder must not break the command that just worked. */
|
|
37
|
+
function markNudged(version) {
|
|
38
|
+
try {
|
|
39
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.join)((0, node_os_1.homedir)(), '.xflow'), { recursive: true, mode: 0o700 });
|
|
40
|
+
const state = { ...read(), nudgedVersion: version, nudgedAt: new Date().toISOString() };
|
|
41
|
+
(0, node_fs_1.writeFileSync)(statePath(), `${JSON.stringify(state, null, 2)}\n`, 'utf-8');
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Worst case the notice repeats next time.
|
|
45
|
+
}
|
|
46
|
+
}
|
package/dist/version.js
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DEFAULT_API_URL = exports.CLI_VERSION = void 0;
|
|
4
4
|
/** Keep in sync with cli/package.json. */
|
|
5
|
-
exports.CLI_VERSION = '0.
|
|
5
|
+
exports.CLI_VERSION = '0.6.0';
|
|
6
6
|
/** Overridden by XFLOW_API_URL or the `api` field in xflow.json. */
|
|
7
7
|
exports.DEFAULT_API_URL = 'https://app.getxflow.com';
|
package/package.json
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@getxflow/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "CLI for the XFlow platform: source sync, deployment and publishing of applications",
|
|
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
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@getxflow/cli",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "CLI for the XFlow platform: source sync, deployment and publishing of applications",
|
|
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
|
+
}
|
package/skills/xflow/SKILL.md
CHANGED
|
@@ -17,6 +17,10 @@ memory. If a command is not in the help output, it does not exist: guessing flag
|
|
|
17
17
|
pointless. The CLI prints a hint with almost every error, read it in full, it usually
|
|
18
18
|
contains the fix.
|
|
19
19
|
|
|
20
|
+
When the CLI says a newer version is out, or refuses to work because the platform wants
|
|
21
|
+
a newer one, run `xflow update`. It also rewrites these instructions, which ship inside
|
|
22
|
+
the package: what you are reading may be older than the platform you are working on.
|
|
23
|
+
|
|
20
24
|
## Hard rules
|
|
21
25
|
|
|
22
26
|
These mistakes cost the most because nothing fails at the moment they are made, or
|