@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.
@@ -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, { revision: server.revision, treeHash: server.tree_hash });
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, { revision: result.revision, treeHash: result.tree_hash });
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.connect)(config);
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.connect)(config);
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, { revision: info.revision, treeHash: info.tree_hash });
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.connect)(config);
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
- function writeState(root, state) {
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. */
@@ -1,9 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.saveCredential = saveCredential;
4
- exports.forgetCredential = forgetCredential;
5
- exports.resolveToken = resolveToken;
6
- exports.readCredential = readCredential;
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 saveCredential(apiUrl, credential) {
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[apiUrl] = { ...credential, savedAt: new Date().toISOString() };
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
- function forgetCredential(apiUrl) {
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
- if (!store[apiUrl])
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
- /** XFLOW_TOKEN wins over the store; the caller decides whether env is allowed for this address. */
55
- function resolveToken(apiUrl, allowEnv = true) {
56
- if (allowEnv) {
57
- const fromEnv = process.env.XFLOW_TOKEN?.trim();
58
- if (fromEnv)
59
- return fromEnv;
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
- return readStore()[apiUrl]?.token ?? null;
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
- function readCredential(apiUrl) {
64
- return readStore()[apiUrl] ?? null;
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
  }