@getxflow/cli 0.6.5 → 0.7.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/dist/api.js +28 -4
- package/dist/bin.js +25 -2
- package/dist/commands/auth.js +58 -30
- package/dist/commands/connections.js +89 -2
- package/dist/commands/deploy.js +7 -7
- package/dist/commands/env.js +1 -0
- package/dist/commands/mcp.js +21 -0
- package/dist/commands/org.js +80 -0
- package/dist/commands/projects.js +28 -2
- package/dist/commands/sources.js +18 -6
- package/dist/config.js +20 -1
- package/dist/credentials.js +203 -17
- package/dist/help.js +379 -328
- package/dist/session.js +68 -5
- package/dist/version.js +1 -1
- package/package.json +23 -23
- package/skills/xflow/SKILL.md +485 -451
package/dist/commands/sources.js
CHANGED
|
@@ -79,7 +79,11 @@ async function pushSources(root, config, client, options) {
|
|
|
79
79
|
// a mismatch must not overwrite silently.
|
|
80
80
|
if (server && state.revision === undefined && !options.force) {
|
|
81
81
|
if (server.tree_hash === tree.hash) {
|
|
82
|
-
(0, config_1.writeState)(root, {
|
|
82
|
+
(0, config_1.writeState)(root, {
|
|
83
|
+
revision: server.revision,
|
|
84
|
+
treeHash: server.tree_hash,
|
|
85
|
+
organizationId: client.organizationId ?? undefined,
|
|
86
|
+
});
|
|
83
87
|
(0, ui_1.ok)(`Already in sync, revision ${server.revision}`);
|
|
84
88
|
return { revision: server.revision, status: 'unchanged' };
|
|
85
89
|
}
|
|
@@ -94,7 +98,11 @@ async function pushSources(root, config, client, options) {
|
|
|
94
98
|
if (options.force)
|
|
95
99
|
headers['X-Force'] = 'true';
|
|
96
100
|
const result = await (0, api_1.apiUpload)(client, `/api/v1/projects/${config.projectId}/push`, tree.archive, headers);
|
|
97
|
-
(0, config_1.writeState)(root, {
|
|
101
|
+
(0, config_1.writeState)(root, {
|
|
102
|
+
revision: result.revision,
|
|
103
|
+
treeHash: result.tree_hash,
|
|
104
|
+
organizationId: client.organizationId ?? undefined,
|
|
105
|
+
});
|
|
98
106
|
if (result.status === 'unchanged') {
|
|
99
107
|
(0, ui_1.ok)(`No changes, revision ${result.revision}`);
|
|
100
108
|
}
|
|
@@ -105,7 +113,7 @@ async function pushSources(root, config, client, options) {
|
|
|
105
113
|
}
|
|
106
114
|
async function push(args) {
|
|
107
115
|
const { root, config } = (0, config_1.requireProject)();
|
|
108
|
-
const client = (0, session_1.
|
|
116
|
+
const client = await (0, session_1.connectProject)(root, config);
|
|
109
117
|
await pushSources(root, config, client, { force: (0, args_1.flagBool)(args, 'force') });
|
|
110
118
|
}
|
|
111
119
|
function hasContent(dir) {
|
|
@@ -117,7 +125,7 @@ async function pull(args) {
|
|
|
117
125
|
const { root, config } = (0, config_1.requireProject)();
|
|
118
126
|
const into = (0, args_1.flagString)(args, 'into');
|
|
119
127
|
const target = into ? (0, node_path_1.resolve)(into) : root;
|
|
120
|
-
const client = (0, session_1.
|
|
128
|
+
const client = await (0, session_1.connectProject)(root, config);
|
|
121
129
|
const revision = (0, args_1.flagNumber)(args, 'revision');
|
|
122
130
|
const query = revision !== undefined ? `?revision=${revision}` : '';
|
|
123
131
|
const info = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/pull${query}`);
|
|
@@ -139,13 +147,17 @@ async function pull(args) {
|
|
|
139
147
|
}
|
|
140
148
|
// Only the working folder tracks revisions; a side copy (--into) must not reset them.
|
|
141
149
|
if (!into) {
|
|
142
|
-
(0, config_1.writeState)(root, {
|
|
150
|
+
(0, config_1.writeState)(root, {
|
|
151
|
+
revision: info.revision,
|
|
152
|
+
treeHash: info.tree_hash,
|
|
153
|
+
organizationId: client.organizationId ?? undefined,
|
|
154
|
+
});
|
|
143
155
|
}
|
|
144
156
|
(0, ui_1.ok)(`Revision ${info.revision}: ${entries.length} files in ${target}`);
|
|
145
157
|
}
|
|
146
158
|
async function status() {
|
|
147
159
|
const { root, config } = (0, config_1.requireProject)();
|
|
148
|
-
const client = (0, session_1.
|
|
160
|
+
const client = await (0, session_1.connectProject)(root, config);
|
|
149
161
|
const tree = prepareTree(root, config);
|
|
150
162
|
const state = (0, config_1.readState)(root);
|
|
151
163
|
const [card, server] = await Promise.all([
|
package/dist/config.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.ConfigError = exports.CONFIG_FILE = void 0;
|
|
|
4
4
|
exports.findProjectRoot = findProjectRoot;
|
|
5
5
|
exports.readConfig = readConfig;
|
|
6
6
|
exports.writeConfig = writeConfig;
|
|
7
|
+
exports.localConfig = localConfig;
|
|
7
8
|
exports.requireProject = requireProject;
|
|
8
9
|
exports.apiUrlFor = apiUrlFor;
|
|
9
10
|
exports.readState = readState;
|
|
@@ -47,6 +48,18 @@ function readConfig(root) {
|
|
|
47
48
|
function writeConfig(root, config) {
|
|
48
49
|
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(root, exports.CONFIG_FILE), `${JSON.stringify(config, null, 2)}\n`, 'utf-8');
|
|
49
50
|
}
|
|
51
|
+
/** The config of the folder we are in, when there is one and it parses. */
|
|
52
|
+
function localConfig() {
|
|
53
|
+
const root = findProjectRoot();
|
|
54
|
+
if (!root)
|
|
55
|
+
return undefined;
|
|
56
|
+
try {
|
|
57
|
+
return readConfig(root);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
50
63
|
function requireProject() {
|
|
51
64
|
const root = findProjectRoot();
|
|
52
65
|
if (!root) {
|
|
@@ -75,9 +88,15 @@ function readState(root) {
|
|
|
75
88
|
return {};
|
|
76
89
|
}
|
|
77
90
|
}
|
|
78
|
-
|
|
91
|
+
/** Merge, never replace: callers write their own fields, the rest survives. */
|
|
92
|
+
function writeState(root, patch) {
|
|
79
93
|
const dir = (0, node_path_1.join)(root, STATE_DIR);
|
|
80
94
|
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
95
|
+
const state = { ...readState(root) };
|
|
96
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
97
|
+
if (value !== undefined)
|
|
98
|
+
state[key] = value;
|
|
99
|
+
}
|
|
81
100
|
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(dir, 'state.json'), `${JSON.stringify(state, null, 2)}\n`, 'utf-8');
|
|
82
101
|
}
|
|
83
102
|
/** Keep .xflow/ out of git. */
|
package/dist/credentials.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
exports.
|
|
5
|
-
exports.
|
|
6
|
-
exports.
|
|
3
|
+
exports.reconcile = reconcile;
|
|
4
|
+
exports.listOrgs = listOrgs;
|
|
5
|
+
exports.saveOrgCredential = saveOrgCredential;
|
|
6
|
+
exports.rememberOrgName = rememberOrgName;
|
|
7
|
+
exports.setActiveOrg = setActiveOrg;
|
|
8
|
+
exports.forgetOrg = forgetOrg;
|
|
9
|
+
exports.forgetAllOrgs = forgetAllOrgs;
|
|
10
|
+
exports.anyMultipleOrgs = anyMultipleOrgs;
|
|
11
|
+
exports.pickKey = pickKey;
|
|
12
|
+
exports.storedKeyInputs = storedKeyInputs;
|
|
7
13
|
const node_fs_1 = require("node:fs");
|
|
8
14
|
const node_os_1 = require("node:os");
|
|
9
15
|
const node_path_1 = require("node:path");
|
|
@@ -38,28 +44,208 @@ function writeStore(store) {
|
|
|
38
44
|
// Windows has no POSIX modes.
|
|
39
45
|
}
|
|
40
46
|
}
|
|
41
|
-
function
|
|
47
|
+
function isLegacy(value) {
|
|
48
|
+
return !!value && typeof value.token === 'string';
|
|
49
|
+
}
|
|
50
|
+
/** The mirror for older CLIs: the key of the active organization, or nothing. */
|
|
51
|
+
function syncLegacy(store, apiUrl) {
|
|
52
|
+
const address = store.orgs?.[apiUrl];
|
|
53
|
+
if (!address)
|
|
54
|
+
return;
|
|
55
|
+
const active = address.active ? address.keys[address.active] : undefined;
|
|
56
|
+
if (active && address.active) {
|
|
57
|
+
store[apiUrl] = { token: active.token, organizationId: address.active, savedAt: active.savedAt };
|
|
58
|
+
address.mirror = address.active;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
delete store[apiUrl];
|
|
62
|
+
address.mirror = null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Fold writes made by an older CLI back into the map. Its login rewrites only
|
|
67
|
+
* the mirror record, its logout deletes only it; both are treated as what the
|
|
68
|
+
* person meant: "sign in to this organization" and "forget the key I am using".
|
|
69
|
+
* Returns true when the store changed and has to be written back.
|
|
70
|
+
*/
|
|
71
|
+
function reconcile(store, apiUrl) {
|
|
72
|
+
const legacy = store[apiUrl];
|
|
73
|
+
let address = store.orgs?.[apiUrl];
|
|
74
|
+
let changed = false;
|
|
75
|
+
if (isLegacy(legacy) && legacy.organizationId) {
|
|
76
|
+
const orgs = address ?? { active: null, keys: {} };
|
|
77
|
+
const known = orgs.keys[legacy.organizationId];
|
|
78
|
+
if (!known || known.token !== legacy.token || orgs.active !== legacy.organizationId) {
|
|
79
|
+
orgs.keys[legacy.organizationId] = { ...known, token: legacy.token, savedAt: legacy.savedAt };
|
|
80
|
+
orgs.active = legacy.organizationId;
|
|
81
|
+
store.orgs = store.orgs ?? {};
|
|
82
|
+
store.orgs[apiUrl] = orgs;
|
|
83
|
+
address = orgs;
|
|
84
|
+
changed = true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (!isLegacy(legacy) && address?.active && address.keys[address.active]) {
|
|
88
|
+
if (address.mirror === address.active) {
|
|
89
|
+
// The mirror we wrote for this key is gone: an older CLI logged out.
|
|
90
|
+
delete address.keys[address.active];
|
|
91
|
+
address.active = null;
|
|
92
|
+
}
|
|
93
|
+
// Otherwise no mirror was ever written for this key (a hand-made or
|
|
94
|
+
// restored file): deleting it would lose a key nobody asked to forget.
|
|
95
|
+
// Either way the store changed: the mirror has to be rebuilt.
|
|
96
|
+
changed = true;
|
|
97
|
+
}
|
|
98
|
+
// The pointer must land on a stored key, or on nothing.
|
|
99
|
+
if (address) {
|
|
100
|
+
const ids = Object.keys(address.keys);
|
|
101
|
+
const active = address.active && address.keys[address.active] ? address.active : (ids[0] ?? null);
|
|
102
|
+
if (address.active !== active) {
|
|
103
|
+
address.active = active;
|
|
104
|
+
changed = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (changed)
|
|
108
|
+
syncLegacy(store, apiUrl);
|
|
109
|
+
return changed;
|
|
110
|
+
}
|
|
111
|
+
/** Read the store with the drift of older CLIs healed, persisting the healing. */
|
|
112
|
+
function load(apiUrl) {
|
|
42
113
|
const store = readStore();
|
|
43
|
-
store
|
|
114
|
+
if (reconcile(store, apiUrl))
|
|
115
|
+
commit(store, apiUrl);
|
|
116
|
+
return store;
|
|
117
|
+
}
|
|
118
|
+
function commit(store, apiUrl) {
|
|
119
|
+
syncLegacy(store, apiUrl);
|
|
120
|
+
const address = store.orgs?.[apiUrl];
|
|
121
|
+
if (address && Object.keys(address.keys).length === 0) {
|
|
122
|
+
delete store.orgs?.[apiUrl];
|
|
123
|
+
if (store.orgs && Object.keys(store.orgs).length === 0)
|
|
124
|
+
delete store.orgs;
|
|
125
|
+
}
|
|
44
126
|
writeStore(store);
|
|
45
127
|
}
|
|
46
|
-
|
|
128
|
+
/** Organizations with a stored key for the address, the active one first. */
|
|
129
|
+
function listOrgs(apiUrl) {
|
|
130
|
+
const address = load(apiUrl).orgs?.[apiUrl];
|
|
131
|
+
if (!address)
|
|
132
|
+
return [];
|
|
133
|
+
return Object.entries(address.keys)
|
|
134
|
+
.map(([organizationId, key]) => ({
|
|
135
|
+
organizationId,
|
|
136
|
+
name: key.name ?? null,
|
|
137
|
+
token: key.token,
|
|
138
|
+
active: organizationId === address.active,
|
|
139
|
+
}))
|
|
140
|
+
.sort((a, b) => Number(b.active) - Number(a.active));
|
|
141
|
+
}
|
|
142
|
+
/** Store the key and make its organization the active one. */
|
|
143
|
+
function saveOrgCredential(apiUrl, organizationId, token, name) {
|
|
144
|
+
const store = readStore();
|
|
145
|
+
reconcile(store, apiUrl);
|
|
146
|
+
const orgs = store.orgs?.[apiUrl] ?? { active: null, keys: {} };
|
|
147
|
+
const known = orgs.keys[organizationId];
|
|
148
|
+
orgs.keys[organizationId] = {
|
|
149
|
+
token,
|
|
150
|
+
name: name ?? known?.name ?? null,
|
|
151
|
+
savedAt: new Date().toISOString(),
|
|
152
|
+
};
|
|
153
|
+
orgs.active = organizationId;
|
|
154
|
+
store.orgs = store.orgs ?? {};
|
|
155
|
+
store.orgs[apiUrl] = orgs;
|
|
156
|
+
commit(store, apiUrl);
|
|
157
|
+
}
|
|
158
|
+
/** Cache the display name; a stale one is refreshed by any login or whoami. */
|
|
159
|
+
function rememberOrgName(apiUrl, organizationId, name) {
|
|
47
160
|
const store = readStore();
|
|
48
|
-
|
|
161
|
+
reconcile(store, apiUrl);
|
|
162
|
+
const entry = store.orgs?.[apiUrl]?.keys[organizationId];
|
|
163
|
+
if (!entry || entry.name === name)
|
|
164
|
+
return;
|
|
165
|
+
entry.name = name;
|
|
166
|
+
commit(store, apiUrl);
|
|
167
|
+
}
|
|
168
|
+
function setActiveOrg(apiUrl, organizationId) {
|
|
169
|
+
const store = readStore();
|
|
170
|
+
reconcile(store, apiUrl);
|
|
171
|
+
const orgs = store.orgs?.[apiUrl];
|
|
172
|
+
if (!orgs?.keys[organizationId])
|
|
173
|
+
return false;
|
|
174
|
+
if (orgs.active !== organizationId) {
|
|
175
|
+
orgs.active = organizationId;
|
|
176
|
+
commit(store, apiUrl);
|
|
177
|
+
}
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
/** Forget one organization; the pointer moves to any remaining one. */
|
|
181
|
+
function forgetOrg(apiUrl, organizationId) {
|
|
182
|
+
const store = readStore();
|
|
183
|
+
reconcile(store, apiUrl);
|
|
184
|
+
const orgs = store.orgs?.[apiUrl];
|
|
185
|
+
if (!orgs?.keys[organizationId])
|
|
186
|
+
return { removed: false, nextActive: null };
|
|
187
|
+
delete orgs.keys[organizationId];
|
|
188
|
+
if (orgs.active === organizationId)
|
|
189
|
+
orgs.active = Object.keys(orgs.keys)[0] ?? null;
|
|
190
|
+
commit(store, apiUrl);
|
|
191
|
+
const next = orgs.active ? orgs.keys[orgs.active] : undefined;
|
|
192
|
+
return {
|
|
193
|
+
removed: true,
|
|
194
|
+
nextActive: next && orgs.active
|
|
195
|
+
? { organizationId: orgs.active, name: next.name ?? null, token: next.token, active: true }
|
|
196
|
+
: null,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/** Forget every key of the address; other addresses are untouched. */
|
|
200
|
+
function forgetAllOrgs(apiUrl) {
|
|
201
|
+
const store = readStore();
|
|
202
|
+
reconcile(store, apiUrl);
|
|
203
|
+
const had = !!store.orgs?.[apiUrl] || isLegacy(store[apiUrl]);
|
|
204
|
+
if (!had)
|
|
49
205
|
return false;
|
|
206
|
+
if (store.orgs) {
|
|
207
|
+
delete store.orgs[apiUrl];
|
|
208
|
+
if (Object.keys(store.orgs).length === 0)
|
|
209
|
+
delete store.orgs;
|
|
210
|
+
}
|
|
50
211
|
delete store[apiUrl];
|
|
51
212
|
writeStore(store);
|
|
52
213
|
return true;
|
|
53
214
|
}
|
|
54
|
-
/**
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
215
|
+
/** True when any address holds keys of more than one organization. */
|
|
216
|
+
function anyMultipleOrgs() {
|
|
217
|
+
const store = readStore();
|
|
218
|
+
return Object.values(store.orgs ?? {}).some((address) => Object.keys(address.keys).length > 1);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* One rule for the whole CLI: XFLOW_TOKEN (default address only) beats the
|
|
222
|
+
* folder's organization, which beats the active one. A folder bound to an
|
|
223
|
+
* organization without a stored key is an error, not a fallback: the command
|
|
224
|
+
* would silently run against a different organization.
|
|
225
|
+
*/
|
|
226
|
+
function pickKey(inputs) {
|
|
227
|
+
if (inputs.envAllowed && inputs.env)
|
|
228
|
+
return { token: inputs.env, source: 'env', organizationId: null };
|
|
229
|
+
if (inputs.folderOrg) {
|
|
230
|
+
const key = inputs.orgs?.keys[inputs.folderOrg];
|
|
231
|
+
if (key)
|
|
232
|
+
return { token: key.token, source: 'folder', organizationId: inputs.folderOrg };
|
|
233
|
+
return { missing: 'folder-key', organizationId: inputs.folderOrg };
|
|
60
234
|
}
|
|
61
|
-
|
|
235
|
+
const activeId = inputs.orgs?.active;
|
|
236
|
+
const active = activeId ? inputs.orgs?.keys[activeId] : undefined;
|
|
237
|
+
if (active && activeId)
|
|
238
|
+
return { token: active.token, source: 'active', organizationId: activeId };
|
|
239
|
+
if (inputs.legacyToken)
|
|
240
|
+
return { token: inputs.legacyToken, source: 'active', organizationId: null };
|
|
241
|
+
return null;
|
|
62
242
|
}
|
|
63
|
-
|
|
64
|
-
|
|
243
|
+
/** The store-backed half of pickKey's inputs; the caller adds env and the folder binding. */
|
|
244
|
+
function storedKeyInputs(apiUrl) {
|
|
245
|
+
const store = load(apiUrl);
|
|
246
|
+
const legacy = store[apiUrl];
|
|
247
|
+
return {
|
|
248
|
+
orgs: store.orgs?.[apiUrl] ?? null,
|
|
249
|
+
legacyToken: isLegacy(legacy) ? legacy.token : null,
|
|
250
|
+
};
|
|
65
251
|
}
|