@getxflow/cli 0.5.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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, 'Update: npm i -g @getxflow/cli');
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
@@ -13,6 +13,7 @@ const BOOLEAN_FLAGS = new Set([
13
13
  'help',
14
14
  'version',
15
15
  'global',
16
+ 'refresh',
16
17
  'live',
17
18
  'no-push',
18
19
  'skip-build',
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;
@@ -1,13 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.mcpInstall = mcpInstall;
4
- const node_child_process_1 = require("node:child_process");
5
4
  const node_os_1 = require("node:os");
6
5
  const node_path_1 = require("node:path");
7
6
  const api_1 = require("../api");
8
7
  const args_1 = require("../args");
9
8
  const config_1 = require("../config");
10
9
  const session_1 = require("../session");
10
+ const spawn_1 = require("../spawn");
11
11
  const ui_1 = require("../ui");
12
12
  /** Clients with their own server-add command. */
13
13
  const CLIENTS = [
@@ -28,23 +28,10 @@ const CLIENTS = [
28
28
  reset: ['mcp', 'remove', 'xflow', '-s', 'local'],
29
29
  },
30
30
  ];
31
- const WINDOWS = process.platform === 'win32';
32
- /**
33
- * Windows needs a shell to resolve npm's .cmd shims, and the shell joins
34
- * arguments instead of escaping them: quote by hand.
35
- */
36
- function quote(value) {
37
- return WINDOWS ? `"${value.replace(/"/g, '""')}"` : value;
38
- }
39
- function execute(binary, args) {
40
- // Capture output: clients may echo the added header together with the key.
41
- const options = { encoding: 'utf-8' };
42
- return WINDOWS
43
- ? (0, node_child_process_1.spawnSync)([binary, ...args.map(quote)].join(' '), { ...options, shell: true })
44
- : (0, node_child_process_1.spawnSync)(binary, args, options);
45
- }
31
+ // Output is captured, not inherited: clients may echo the added header together
32
+ // with the key.
46
33
  function hasBinary(binary) {
47
- return execute(binary, ['--version']).status === 0;
34
+ return (0, spawn_1.run)(binary, ['--version']).status === 0;
48
35
  }
49
36
  async function mcpInstall(args) {
50
37
  // The server covers the organization: the command works outside a project folder too.
@@ -62,8 +49,8 @@ async function mcpInstall(args) {
62
49
  continue;
63
50
  found++;
64
51
  (0, ui_1.step)(`Writing into ${target.label}`);
65
- execute(target.binary, [...target.reset]);
66
- const result = execute(target.binary, target.args(url, client.token));
52
+ (0, spawn_1.run)(target.binary, [...target.reset]);
53
+ const result = (0, spawn_1.run)(target.binary, target.args(url, client.token));
67
54
  if (result.status === 0) {
68
55
  installed++;
69
56
  (0, ui_1.ok)(`${target.label}: the xflow server is connected`);
@@ -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)(' After a CLI update run the command again: the skill ships with it'));
241
+ (0, ui_1.note)((0, ui_1.dim)(' The skill ships with the CLI: xflow update brings both'));
205
242
  }
@@ -0,0 +1,154 @@
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 spawn_1 = require("../spawn");
10
+ const ui_1 = require("../ui");
11
+ const version_1 = require("../version");
12
+ const PACKAGE = '@getxflow/cli';
13
+ const WINDOWS = process.platform === 'win32';
14
+ function classifyPath(dir) {
15
+ const parts = dir.split(/[\\/]/).map((part) => part.toLowerCase());
16
+ if (parts.includes('_npx'))
17
+ return 'npx';
18
+ if (parts.includes('pnpm') || parts.includes('.pnpm'))
19
+ return 'pnpm';
20
+ if (parts.includes('.bun'))
21
+ return 'bun';
22
+ if (parts.includes('.yarn'))
23
+ return 'yarn';
24
+ // A global npm prefix has a node_modules too, so this only separates a source
25
+ // checkout (npm link, tsx cli/src) from everything installed.
26
+ if (!parts.includes('node_modules'))
27
+ return 'checkout';
28
+ return 'node_modules';
29
+ }
30
+ /**
31
+ * The manifest of a project that depends on the package, if this copy is such a
32
+ * dependency. A dependency and a tool need opposite answers: inside npm scripts the
33
+ * local .bin wins over PATH, so a global install would leave the old version running.
34
+ */
35
+ function projectManifest(dir) {
36
+ let current = dir;
37
+ for (let depth = 0; depth < 12; depth++) {
38
+ const parent = (0, node_path_1.dirname)(current);
39
+ if (parent === current)
40
+ return null;
41
+ if (current.split(/[\\/]/).pop()?.toLowerCase() === 'node_modules') {
42
+ const path = (0, node_path_1.join)(parent, 'package.json');
43
+ if (!(0, node_fs_1.existsSync)(path))
44
+ return null;
45
+ try {
46
+ const pkg = JSON.parse((0, node_fs_1.readFileSync)(path, 'utf-8'));
47
+ const declared = pkg.dependencies?.[PACKAGE] ?? pkg.devDependencies?.[PACKAGE];
48
+ return declared === undefined ? null : path;
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ current = parent;
55
+ }
56
+ return null;
57
+ }
58
+ function npmRoot() {
59
+ const result = (0, spawn_1.run)('npm', ['root', '-g'], {
60
+ timeout: 60_000,
61
+ stdio: ['ignore', 'pipe', 'ignore'],
62
+ });
63
+ if (result.error || result.status !== 0 || typeof result.stdout !== 'string')
64
+ return null;
65
+ const dir = result.stdout.trim();
66
+ return dir.length > 0 && (0, node_fs_1.existsSync)(dir) ? dir : null;
67
+ }
68
+ function packageDir(root) {
69
+ return (0, node_path_1.join)(root, '@getxflow', 'cli');
70
+ }
71
+ function installedVersion(root) {
72
+ try {
73
+ const pkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(packageDir(root), 'package.json'), 'utf-8'));
74
+ return pkg.version ?? null;
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ }
80
+ /**
81
+ * The skill ships inside the package and shares its version, so a fresh CLI with a
82
+ * stale skill is an agent reading last month's instructions. Run through the freshly
83
+ * installed binary by absolute path: PATH may point at another copy, and this process
84
+ * still holds the old code in memory.
85
+ */
86
+ function refreshSkill(root) {
87
+ const bin = (0, node_path_1.join)(packageDir(root), 'dist', 'bin.js');
88
+ if (!(0, node_fs_1.existsSync)(bin)) {
89
+ (0, ui_1.note)((0, ui_1.dim)(' Refresh the agent instructions yourself: xflow skills'));
90
+ return;
91
+ }
92
+ const result = (0, node_child_process_1.spawnSync)(process.execPath, [bin, 'skills', '--refresh'], { stdio: 'inherit' });
93
+ if (result.error || result.status !== 0) {
94
+ (0, ui_1.note)((0, ui_1.dim)(' Could not refresh the agent instructions, run xflow skills'));
95
+ }
96
+ }
97
+ function manual(command, why) {
98
+ (0, ui_1.note)((0, ui_1.dim)(` ${why}`));
99
+ (0, ui_1.note)(` ${command}`);
100
+ }
101
+ function update() {
102
+ const kind = classifyPath(__dirname);
103
+ if (kind === 'checkout') {
104
+ (0, ui_1.ok)(`xflow ${version_1.CLI_VERSION} runs from a source checkout`);
105
+ (0, ui_1.note)((0, ui_1.dim)(' Nothing to update here: pull the repository and rebuild'));
106
+ return;
107
+ }
108
+ if (kind === 'npx') {
109
+ (0, ui_1.ok)(`xflow ${version_1.CLI_VERSION} runs from the npx cache`);
110
+ manual(`npm i -g ${PACKAGE}@latest`, 'npx keeps its own copy. Install the CLI properly:');
111
+ return;
112
+ }
113
+ if (kind === 'pnpm') {
114
+ manual(`pnpm add -g ${PACKAGE}@latest`, 'Installed by pnpm, so npm must not touch it:');
115
+ return;
116
+ }
117
+ if (kind === 'bun') {
118
+ manual(`bun add -g ${PACKAGE}@latest`, 'Installed by bun, so npm must not touch it:');
119
+ return;
120
+ }
121
+ if (kind === 'yarn') {
122
+ manual(`yarn global add ${PACKAGE}@latest`, 'Installed by yarn, so npm must not touch it:');
123
+ return;
124
+ }
125
+ const manifest = projectManifest(__dirname);
126
+ if (manifest !== null) {
127
+ (0, ui_1.ok)(`xflow ${version_1.CLI_VERSION} is a dependency of this project`);
128
+ manual(`npm i -D ${PACKAGE}@latest`, `Declared in ${manifest}. A global install would change nothing: inside npm scripts the local copy wins`);
129
+ return;
130
+ }
131
+ (0, ui_1.step)(`updating ${PACKAGE} from ${version_1.CLI_VERSION}`);
132
+ const result = (0, spawn_1.run)('npm', ['i', '-g', `${PACKAGE}@latest`], { stdio: 'inherit' });
133
+ if (result.error) {
134
+ throw new errors_1.CliError(`Could not run npm: ${result.error.message}`, `Install by hand: npm i -g ${PACKAGE}@latest`);
135
+ }
136
+ if (result.status !== 0) {
137
+ throw new errors_1.CliError('npm could not install the package', WINDOWS
138
+ ? 'The npm output is above. Windows also locks the xflow.cmd of a running command: close the other terminals and repeat'
139
+ : 'The npm output is above. A permission error means the global folder belongs to root: fix npm rather than run it with sudo');
140
+ }
141
+ const root = npmRoot();
142
+ const after = root === null ? null : installedVersion(root);
143
+ if (root === null || after === null) {
144
+ (0, ui_1.ok)(`${PACKAGE} installed`);
145
+ (0, ui_1.note)((0, ui_1.dim)(' Check what is on disk now: xflow --version'));
146
+ return;
147
+ }
148
+ if (after === version_1.CLI_VERSION) {
149
+ (0, ui_1.ok)(`xflow ${after} is the latest version`);
150
+ return;
151
+ }
152
+ (0, ui_1.ok)(`xflow ${version_1.CLI_VERSION} → ${after}`);
153
+ refreshSkill(root);
154
+ }
@@ -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 and to refresh after a CLI update: the skill and the CLI ship as one version.
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
@@ -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/spawn.js ADDED
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.run = run;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const WINDOWS = process.platform === 'win32';
6
+ /**
7
+ * Windows resolves npm and other .cmd shims only through a shell, and a shell
8
+ * together with an args array is what Node deprecated in DEP0190: it joins them
9
+ * itself, without escaping, and says so in the middle of our output. So the command
10
+ * line is built and quoted here, and no other platform gets a shell at all.
11
+ */
12
+ function quote(value) {
13
+ return WINDOWS ? `"${value.replace(/"/g, '""')}"` : value;
14
+ }
15
+ function run(binary, args, options = {}) {
16
+ const common = { stdio: options.stdio, timeout: options.timeout, encoding: 'utf-8' };
17
+ return WINDOWS
18
+ ? (0, node_child_process_1.spawnSync)([binary, ...args.map(quote)].join(' '), { ...common, shell: true })
19
+ : (0, node_child_process_1.spawnSync)(binary, args, common);
20
+ }
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.1';
5
+ exports.CLI_VERSION = '0.6.1';
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.5.1",
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.1",
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
+ }
@@ -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