@zeyos/cli 0.4.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 +13 -0
- package/bin/zeyos.mjs +152 -10
- package/commands/count.mjs +4 -2
- package/commands/doctor.mjs +8 -1
- package/commands/list.mjs +4 -1
- package/commands/login.mjs +4 -0
- package/commands/logout.mjs +35 -9
- package/commands/profile.mjs +117 -22
- package/commands/sum.mjs +112 -0
- package/commands/whoami.mjs +203 -20
- package/lib/client.mjs +32 -14
- package/lib/command.mjs +128 -0
- package/lib/config.mjs +31 -0
- package/lib/resource-config.mjs +11 -7
- package/lib/resources.mjs +107 -0
- package/lib/types.mjs +3 -1
- package/package.json +2 -2
package/commands/sum.mjs
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* zeyos sum <resource> <field>
|
|
3
|
+
*
|
|
4
|
+
* Page through records and sum one numeric field client-side.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { normalizeListResult } from '@zeyos/client';
|
|
8
|
+
import {
|
|
9
|
+
buildCliClient,
|
|
10
|
+
callApi,
|
|
11
|
+
fail,
|
|
12
|
+
maybeDryRun,
|
|
13
|
+
normalizeFilterOperators,
|
|
14
|
+
parseJsonOptionOrFile,
|
|
15
|
+
requireResource
|
|
16
|
+
} from '../lib/command.mjs';
|
|
17
|
+
import { outputMode, printJson, printYaml } from '../lib/output.mjs';
|
|
18
|
+
|
|
19
|
+
export const USAGE = `\
|
|
20
|
+
Usage: zeyos sum <resource> <field> [options]
|
|
21
|
+
|
|
22
|
+
Sum a numeric field across all records matching an optional filter.
|
|
23
|
+
The CLI pages internally so agents do not need to list rows and write ad hoc scripts.
|
|
24
|
+
|
|
25
|
+
Arguments:
|
|
26
|
+
resource Resource name (e.g. actionsteps, transactions, payments)
|
|
27
|
+
field Numeric field to sum (e.g. effort, amount, netamount)
|
|
28
|
+
|
|
29
|
+
Options:
|
|
30
|
+
--filter <json> JSON filter object e.g. '{"status":[1,3]}'
|
|
31
|
+
Arrays normalize to IN; $lt/$lte/$gt/$gte/$ne/$in/$nin and suffix
|
|
32
|
+
keys like field__startswith/field__gt normalize to native operators
|
|
33
|
+
--filter-file <path>
|
|
34
|
+
Read JSON filter object from a file
|
|
35
|
+
--page-size <n> Records per API page (default: 50)
|
|
36
|
+
--limit <n> Maximum records to inspect
|
|
37
|
+
--offset <n> Initial offset (default: 0)
|
|
38
|
+
--json Output as JSON ({ "sum": N, "count": N })
|
|
39
|
+
--yaml Output as YAML
|
|
40
|
+
--query Print the first page request without sending it
|
|
41
|
+
-h, --help Show this help
|
|
42
|
+
|
|
43
|
+
Examples:
|
|
44
|
+
zeyos sum actionsteps effort --filter '{"status":[1,3]}'
|
|
45
|
+
zeyos sum transactions netamount --filter '{"type":3}' --json
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
export async function run(values, positional) {
|
|
49
|
+
const resourceName = positional[0];
|
|
50
|
+
const field = positional[1];
|
|
51
|
+
const res = requireResource(resourceName, 'zeyos sum <resource> <field>');
|
|
52
|
+
if (!field) fail('Missing field name. Usage: zeyos sum <resource> <field>');
|
|
53
|
+
|
|
54
|
+
const pageSize = parsePositiveInt(values['page-size'] ?? '50', '--page-size');
|
|
55
|
+
const maxRows = values.limit == null ? Infinity : parsePositiveInt(values.limit, '--limit');
|
|
56
|
+
let offset = values.offset == null ? 0 : parseNonNegativeInt(values.offset, '--offset');
|
|
57
|
+
|
|
58
|
+
const body = { fields: [field], limit: Math.min(pageSize, maxRows), offset };
|
|
59
|
+
const filters = parseJsonOptionOrFile(values, 'filter', 'filter-file');
|
|
60
|
+
if (filters !== undefined) body.filters = normalizeFilterOperators(filters, { fieldAliases: res.filterAliases });
|
|
61
|
+
|
|
62
|
+
const clientState = buildCliClient(values);
|
|
63
|
+
if (await maybeDryRun(clientState, res.list, body, values)) return;
|
|
64
|
+
|
|
65
|
+
let sum = 0;
|
|
66
|
+
let count = 0;
|
|
67
|
+
|
|
68
|
+
while (count < maxRows) {
|
|
69
|
+
const remaining = maxRows - count;
|
|
70
|
+
const limit = Math.min(pageSize, remaining);
|
|
71
|
+
const pageBody = { ...body, limit, offset };
|
|
72
|
+
const result = await callApi(clientState, res.list, pageBody);
|
|
73
|
+
const rows = normalizeListResult(result).data;
|
|
74
|
+
|
|
75
|
+
for (const row of rows) {
|
|
76
|
+
sum += numericValue(row[field], field);
|
|
77
|
+
count += 1;
|
|
78
|
+
if (count >= maxRows) break;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (rows.length < limit || rows.length === 0) break;
|
|
82
|
+
offset += rows.length;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const mode = outputMode(values);
|
|
86
|
+
if (mode === 'json') {
|
|
87
|
+
printJson({ sum, count, field });
|
|
88
|
+
} else if (mode === 'yaml') {
|
|
89
|
+
printYaml({ sum, count, field });
|
|
90
|
+
} else {
|
|
91
|
+
process.stdout.write(`${sum}\n`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function numericValue(value, field) {
|
|
96
|
+
if (value == null || value === '') return 0;
|
|
97
|
+
const n = Number(value);
|
|
98
|
+
if (!Number.isFinite(n)) fail(`Field "${field}" contains a non-numeric value: ${JSON.stringify(value)}`);
|
|
99
|
+
return n;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parsePositiveInt(value, flag) {
|
|
103
|
+
const n = Number.parseInt(String(value), 10);
|
|
104
|
+
if (!Number.isInteger(n) || n <= 0) fail(`${flag} must be a positive integer.`);
|
|
105
|
+
return n;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function parseNonNegativeInt(value, flag) {
|
|
109
|
+
const n = Number.parseInt(String(value), 10);
|
|
110
|
+
if (!Number.isInteger(n) || n < 0) fail(`${flag} must be a non-negative integer.`);
|
|
111
|
+
return n;
|
|
112
|
+
}
|
package/commands/whoami.mjs
CHANGED
|
@@ -9,8 +9,11 @@
|
|
|
9
9
|
* --show-token Include the current access token in output
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { createInterface } from 'node:readline';
|
|
12
13
|
import { buildClient, syncTokens } from '../lib/client.mjs';
|
|
14
|
+
import { globalConfigPath, profilesConfigPath } from '../lib/config.mjs';
|
|
13
15
|
import { outputMode, printJson, printYaml, printRecord, formatDate, error } from '../lib/output.mjs';
|
|
16
|
+
import { run as runLogin } from './login.mjs';
|
|
14
17
|
|
|
15
18
|
export const USAGE = `\
|
|
16
19
|
Usage: zeyos whoami [options]
|
|
@@ -25,35 +28,23 @@ Options:
|
|
|
25
28
|
`;
|
|
26
29
|
|
|
27
30
|
export async function run(values) {
|
|
28
|
-
let
|
|
29
|
-
try {
|
|
30
|
-
({ client, config, tokenStore, configSource } = buildClient({}, { profile: values.profile }));
|
|
31
|
-
} catch (err) {
|
|
32
|
-
error(err.message);
|
|
33
|
-
process.exit(1);
|
|
34
|
-
}
|
|
31
|
+
let state = _buildClientState(values);
|
|
35
32
|
|
|
36
33
|
let userInfo;
|
|
37
34
|
try {
|
|
38
|
-
userInfo = await
|
|
39
|
-
await syncTokens(tokenStore, configSource);
|
|
35
|
+
userInfo = await _fetchUserInfo(state);
|
|
40
36
|
} catch (err) {
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
error('Your session has expired or is invalid. Re-authenticate with: zeyos login --force');
|
|
46
|
-
} else {
|
|
47
|
-
error(`Failed to fetch user info: ${err.message}`);
|
|
48
|
-
}
|
|
49
|
-
process.exit(1);
|
|
37
|
+
const handled = await _handleFetchError(err, state, values);
|
|
38
|
+
if (!handled) process.exit(1);
|
|
39
|
+
state = handled.state;
|
|
40
|
+
userInfo = handled.userInfo;
|
|
50
41
|
}
|
|
51
42
|
|
|
52
43
|
const mode = outputMode(values);
|
|
53
44
|
|
|
54
45
|
const output = { ...userInfo };
|
|
55
46
|
if (values['show-token']) {
|
|
56
|
-
const tokenSet = await tokenStore.get();
|
|
47
|
+
const tokenSet = await state.tokenStore.get();
|
|
57
48
|
if (tokenSet?.accessToken) output.accessToken = tokenSet.accessToken;
|
|
58
49
|
}
|
|
59
50
|
|
|
@@ -63,7 +54,7 @@ export async function run(values) {
|
|
|
63
54
|
printYaml(output);
|
|
64
55
|
} else {
|
|
65
56
|
// Pretty key-value record with custom formatters
|
|
66
|
-
const dateFormat = config.dateFormat ?? 'YYYY-MM-DD HH:mm';
|
|
57
|
+
const dateFormat = state.config.dateFormat ?? 'YYYY-MM-DD HH:mm';
|
|
67
58
|
const keys = Object.keys(output);
|
|
68
59
|
const formatters = {};
|
|
69
60
|
|
|
@@ -86,6 +77,56 @@ export async function run(values) {
|
|
|
86
77
|
}
|
|
87
78
|
}
|
|
88
79
|
|
|
80
|
+
function _buildClientState(values) {
|
|
81
|
+
try {
|
|
82
|
+
const state = buildClient({}, { profile: values.profile });
|
|
83
|
+
return {
|
|
84
|
+
client: state.client,
|
|
85
|
+
config: state.config,
|
|
86
|
+
tokenStore: state.tokenStore,
|
|
87
|
+
configSource: state.configSource
|
|
88
|
+
};
|
|
89
|
+
} catch (err) {
|
|
90
|
+
error(err.message);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function _fetchUserInfo(state) {
|
|
96
|
+
const userInfo = await state.client.oauth2.getUserInfo();
|
|
97
|
+
await syncTokens(state.tokenStore, state.configSource);
|
|
98
|
+
return userInfo;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function _handleFetchError(err, state, values) {
|
|
102
|
+
const status = err?.status;
|
|
103
|
+
if (status === 502 || status === 503 || status === 504) {
|
|
104
|
+
error(`ZeyOS instance is temporarily unavailable (HTTP ${status}). The server at ${state.config.baseUrl} may be down or restarting — this is server-side, not your credentials.`);
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const authFailure = _authFailureSummary(err);
|
|
109
|
+
if (!authFailure) {
|
|
110
|
+
error(`Failed to fetch user info: ${err.message}`);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
error(_formatAuthFailure(authFailure, err, state.config, state.configSource, values));
|
|
115
|
+
const reauthenticated = await _maybeReauthenticate(state.configSource, values);
|
|
116
|
+
if (!reauthenticated) return null;
|
|
117
|
+
|
|
118
|
+
const nextState = _buildClientState(values);
|
|
119
|
+
try {
|
|
120
|
+
return {
|
|
121
|
+
state: nextState,
|
|
122
|
+
userInfo: await _fetchUserInfo(nextState)
|
|
123
|
+
};
|
|
124
|
+
} catch (retryErr) {
|
|
125
|
+
error(`Re-authentication completed, but fetching user info still failed: ${retryErr.message}`);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
89
130
|
/**
|
|
90
131
|
* Format an array of objects as a multi-line list.
|
|
91
132
|
* Each item is shown as "name (rw)" or "name (ro)" on its own line.
|
|
@@ -105,3 +146,145 @@ function _formatObjectList(items, nameKey, writableKey) {
|
|
|
105
146
|
})
|
|
106
147
|
.join('\n');
|
|
107
148
|
}
|
|
149
|
+
|
|
150
|
+
function _isInvalidRefreshTokenError(err) {
|
|
151
|
+
const detail = `${err?.message ?? ''}\n${_stringifyErrorBody(err?.body)}`;
|
|
152
|
+
return [400, 401, 403].includes(err?.status) &&
|
|
153
|
+
/refresh[_ -]?token|invalid_grant/i.test(detail) &&
|
|
154
|
+
/invalid|expired|forbidden|invalid_grant/i.test(detail);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function _authFailureSummary(err) {
|
|
158
|
+
if (_isInvalidRefreshTokenError(err)) {
|
|
159
|
+
return 'Your stored refresh token is invalid or expired.';
|
|
160
|
+
}
|
|
161
|
+
if (err?.status === 401) {
|
|
162
|
+
return 'Your session has expired or is invalid.';
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function _formatAuthFailure(summary, err, config, source, values) {
|
|
168
|
+
const lines = [
|
|
169
|
+
summary,
|
|
170
|
+
`Platform URL: ${config.baseUrl ?? '(not configured)'}`,
|
|
171
|
+
`Credential source: ${_describeConfigSource(source)}`
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
if (err?.url) {
|
|
175
|
+
lines.push(`OAuth endpoint: ${err.url}`);
|
|
176
|
+
}
|
|
177
|
+
if (err?.status) {
|
|
178
|
+
lines.push(`HTTP status: ${err.status}${err.statusText ? ` ${err.statusText}` : ''}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const detail = _authErrorDetail(err);
|
|
182
|
+
if (detail) {
|
|
183
|
+
lines.push(`OAuth error: ${detail}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
lines.push(`Next step: ${_loginCommand(source, values)}`);
|
|
187
|
+
if (process.env.ZEYOS_TOKEN || process.env.ZEYOS_REFRESH_TOKEN) {
|
|
188
|
+
lines.push('Note: ZEYOS_TOKEN or ZEYOS_REFRESH_TOKEN is set and overrides stored credentials; update or unset it before retrying.');
|
|
189
|
+
}
|
|
190
|
+
return lines.join('\n');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function _describeConfigSource(source) {
|
|
194
|
+
if (!source) {
|
|
195
|
+
return 'environment variables';
|
|
196
|
+
}
|
|
197
|
+
if (source.kind === 'profile') {
|
|
198
|
+
return `profile "${source.name}" (${profilesConfigPath()})`;
|
|
199
|
+
}
|
|
200
|
+
if (source.kind === 'global') {
|
|
201
|
+
return `global credentials (${globalConfigPath()})`;
|
|
202
|
+
}
|
|
203
|
+
if (source.kind === 'local') {
|
|
204
|
+
return `local file ${source.path ?? '.zeyos/auth.json'}`;
|
|
205
|
+
}
|
|
206
|
+
return source.kind ?? 'unknown';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function _loginCommand(source, values) {
|
|
210
|
+
const profile = values.profile ?? (source?.kind === 'profile' ? source.name : null);
|
|
211
|
+
if (profile) {
|
|
212
|
+
return `zeyos login --profile ${_quoteArg(profile)} --force`;
|
|
213
|
+
}
|
|
214
|
+
if (source?.kind === 'global') {
|
|
215
|
+
return 'zeyos login --global --force';
|
|
216
|
+
}
|
|
217
|
+
return 'zeyos login --force';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function _maybeReauthenticate(source, values) {
|
|
221
|
+
if (!_canPromptForReauthentication(source, values)) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const command = _loginCommand(source, values);
|
|
226
|
+
const confirmed = await _confirm(`Re-authenticate now (${command})? [y/N] `);
|
|
227
|
+
if (!confirmed) return false;
|
|
228
|
+
|
|
229
|
+
await runLogin(_loginValues(source, values));
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function _canPromptForReauthentication(source, values) {
|
|
234
|
+
return Boolean(source) &&
|
|
235
|
+
!values.json &&
|
|
236
|
+
!values.yaml &&
|
|
237
|
+
process.stdin.isTTY &&
|
|
238
|
+
process.stderr.isTTY &&
|
|
239
|
+
!process.env.ZEYOS_TOKEN &&
|
|
240
|
+
!process.env.ZEYOS_REFRESH_TOKEN;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function _loginValues(source, values) {
|
|
244
|
+
return {
|
|
245
|
+
...values,
|
|
246
|
+
force: true,
|
|
247
|
+
profile: values.profile ?? (source?.kind === 'profile' ? source.name : undefined),
|
|
248
|
+
global: source?.kind === 'global' ? true : values.global
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function _confirm(prompt) {
|
|
253
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
254
|
+
return new Promise(resolve => {
|
|
255
|
+
rl.question(prompt, answer => {
|
|
256
|
+
rl.close();
|
|
257
|
+
resolve(answer.trim().toLowerCase() === 'y');
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function _authErrorDetail(err) {
|
|
263
|
+
const body = _stringifyErrorBody(err?.body).trim();
|
|
264
|
+
if (body) {
|
|
265
|
+
return body;
|
|
266
|
+
}
|
|
267
|
+
return String(err?.message ?? '').trim();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function _stringifyErrorBody(body) {
|
|
271
|
+
if (body == null) {
|
|
272
|
+
return '';
|
|
273
|
+
}
|
|
274
|
+
if (typeof body === 'string') {
|
|
275
|
+
return body;
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
return JSON.stringify(body);
|
|
279
|
+
} catch {
|
|
280
|
+
return String(body);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function _quoteArg(value) {
|
|
285
|
+
const text = String(value);
|
|
286
|
+
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(text)) {
|
|
287
|
+
return text;
|
|
288
|
+
}
|
|
289
|
+
return `'${text.replaceAll("'", "'\\''")}'`;
|
|
290
|
+
}
|
package/lib/client.mjs
CHANGED
|
@@ -23,29 +23,47 @@ export function buildClient(overrides = {}, opts = {}) {
|
|
|
23
23
|
throw new Error(`Profile "${loaded.profile.name}" not found (selected via ${loaded.profile.origin}). ${known}`);
|
|
24
24
|
}
|
|
25
25
|
const config = { ...loaded.config, ...overrides };
|
|
26
|
-
|
|
26
|
+
const tokenOnly = isTokenOnlyMode(config);
|
|
27
|
+
requireConfig(tokenOnly ? ['baseUrl', 'accessToken'] : ['baseUrl', 'clientId', 'clientSecret', 'accessToken'], config);
|
|
27
28
|
|
|
28
|
-
const tokenStore = new MemoryTokenStore(
|
|
29
|
-
accessToken:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
const tokenStore = new MemoryTokenStore(tokenOnly
|
|
30
|
+
? { accessToken: config.accessToken }
|
|
31
|
+
: {
|
|
32
|
+
accessToken: config.accessToken,
|
|
33
|
+
refreshToken: config.refreshToken,
|
|
34
|
+
expiresAt: config.expiresAt,
|
|
35
|
+
refreshTokenExpiresAt: config.refreshTokenExpiresAt,
|
|
36
|
+
});
|
|
34
37
|
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
const oauth = tokenOnly
|
|
39
|
+
? {
|
|
40
|
+
tokenStore,
|
|
41
|
+
autoRefresh: false,
|
|
42
|
+
}
|
|
43
|
+
: {
|
|
40
44
|
clientId: config.clientId,
|
|
41
45
|
clientSecret: config.clientSecret,
|
|
42
46
|
tokenStore,
|
|
43
47
|
autoRefresh: true,
|
|
44
|
-
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const client = createZeyosClient({
|
|
51
|
+
platform: config.baseUrl,
|
|
52
|
+
auth: {
|
|
53
|
+
mode: 'oauth',
|
|
54
|
+
oauth,
|
|
45
55
|
},
|
|
46
56
|
});
|
|
47
57
|
|
|
48
|
-
return { client, config, tokenStore, configSource: loaded.source };
|
|
58
|
+
return { client, config, tokenStore, configSource: tokenOnly ? null : loaded.source, tokenOnly };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isTruthyEnv(value) {
|
|
62
|
+
return /^(1|true|yes|on)$/i.test(String(value || '').trim());
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isTokenOnlyMode(config) {
|
|
66
|
+
return Boolean(process.env.ZEYOS_TOKEN) || (isTruthyEnv(process.env.ZEYOS_NO_REFRESH) && Boolean(config.accessToken));
|
|
49
67
|
}
|
|
50
68
|
|
|
51
69
|
/**
|
package/lib/command.mjs
CHANGED
|
@@ -99,6 +99,134 @@ export function parseJsonOptionOrFile(values, flagName, fileFlagName = `${flagNa
|
|
|
99
99
|
return undefined;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
const FILTER_OPERATOR_ALIASES = {
|
|
103
|
+
$lt: '<',
|
|
104
|
+
$lte: '<=',
|
|
105
|
+
$gt: '>',
|
|
106
|
+
$gte: '>=',
|
|
107
|
+
$ne: '!=',
|
|
108
|
+
$in: 'IN',
|
|
109
|
+
$nin: '!IN',
|
|
110
|
+
$notIn: '!IN',
|
|
111
|
+
lt: '<',
|
|
112
|
+
lte: '<=',
|
|
113
|
+
gt: '>',
|
|
114
|
+
gte: '>=',
|
|
115
|
+
ne: '!=',
|
|
116
|
+
in: 'IN',
|
|
117
|
+
nin: '!IN',
|
|
118
|
+
notIn: '!IN',
|
|
119
|
+
notin: '!IN'
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const FILTER_SUFFIX_OPERATOR_ALIASES = {
|
|
123
|
+
lt: '<',
|
|
124
|
+
lte: '<=',
|
|
125
|
+
gt: '>',
|
|
126
|
+
gte: '>=',
|
|
127
|
+
ne: '!=',
|
|
128
|
+
in: 'IN',
|
|
129
|
+
nin: '!IN',
|
|
130
|
+
notin: '!IN'
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const FILTER_PATTERN_SUFFIXES = new Set([
|
|
134
|
+
'startswith',
|
|
135
|
+
'istartswith',
|
|
136
|
+
'like',
|
|
137
|
+
'ilike',
|
|
138
|
+
'contains',
|
|
139
|
+
'icontains',
|
|
140
|
+
'regex',
|
|
141
|
+
'iregex'
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
export function normalizeFilterOperators(value, options = {}) {
|
|
145
|
+
if (Array.isArray(value)) {
|
|
146
|
+
return value.map((item) => normalizeFilterOperators(item, options));
|
|
147
|
+
}
|
|
148
|
+
if (!value || typeof value !== 'object') return value;
|
|
149
|
+
|
|
150
|
+
const out = {};
|
|
151
|
+
for (const [key, child] of Object.entries(value)) {
|
|
152
|
+
const suffixFilter = parseFilterSuffix(key, child, options);
|
|
153
|
+
if (suffixFilter) {
|
|
154
|
+
mergeFieldFilter(out, suffixFilter.field, suffixFilter.operator, suffixFilter.value);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const normalizedKey = FILTER_OPERATOR_ALIASES[key] || key;
|
|
159
|
+
const outputKey = isFilterOperatorKey(normalizedKey)
|
|
160
|
+
? normalizedKey
|
|
161
|
+
: normalizeFieldAlias(normalizedKey, options);
|
|
162
|
+
const normalizedChild = normalizeFilterOperators(child, options);
|
|
163
|
+
out[outputKey] = Array.isArray(normalizedChild) && !isFilterOperatorKey(outputKey)
|
|
164
|
+
? { IN: normalizedChild }
|
|
165
|
+
: normalizedChild;
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function isFilterOperatorKey(key) {
|
|
171
|
+
return ['<', '<=', '>', '>=', '!=', 'IN', '!IN', '~~*'].includes(key);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function normalizeFieldAlias(field, options = {}) {
|
|
175
|
+
return options.fieldAliases?.[field] || field;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function parseFilterSuffix(key, child, options) {
|
|
179
|
+
const separator = key.lastIndexOf('__');
|
|
180
|
+
if (separator <= 0) return null;
|
|
181
|
+
|
|
182
|
+
const rawField = key.slice(0, separator);
|
|
183
|
+
const suffix = key.slice(separator + 2).toLowerCase();
|
|
184
|
+
const field = normalizeFieldAlias(rawField, options);
|
|
185
|
+
|
|
186
|
+
if (Object.prototype.hasOwnProperty.call(FILTER_SUFFIX_OPERATOR_ALIASES, suffix)) {
|
|
187
|
+
return {
|
|
188
|
+
field,
|
|
189
|
+
operator: FILTER_SUFFIX_OPERATOR_ALIASES[suffix],
|
|
190
|
+
value: normalizeFilterOperators(child, options)
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (FILTER_PATTERN_SUFFIXES.has(suffix)) {
|
|
195
|
+
return {
|
|
196
|
+
field,
|
|
197
|
+
operator: '~~*',
|
|
198
|
+
value: patternValueForSuffix(suffix, child)
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function patternValueForSuffix(suffix, child) {
|
|
206
|
+
const value = String(child ?? '');
|
|
207
|
+
if (suffix === 'startswith' || suffix === 'istartswith') return `${value}%`;
|
|
208
|
+
if (suffix === 'contains' || suffix === 'icontains') return `%${value}%`;
|
|
209
|
+
if (suffix === 'regex' || suffix === 'iregex') return regexLikeToSqlLike(value);
|
|
210
|
+
return value;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function regexLikeToSqlLike(value) {
|
|
214
|
+
return String(value)
|
|
215
|
+
.replace(/^\^/, '')
|
|
216
|
+
.replace(/\$$/, '')
|
|
217
|
+
.replace(/\\.\\*/g, '%')
|
|
218
|
+
.replace(/\.\*/g, '%');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function mergeFieldFilter(out, field, operator, value) {
|
|
222
|
+
const existing = out[field];
|
|
223
|
+
if (existing && typeof existing === 'object' && !Array.isArray(existing)) {
|
|
224
|
+
out[field] = { ...existing, [operator]: value };
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
out[field] = { [operator]: value };
|
|
228
|
+
}
|
|
229
|
+
|
|
102
230
|
/** Cheap structural check: does this string look like an intended JSON object? */
|
|
103
231
|
function looksLikeJsonObject(value) {
|
|
104
232
|
return typeof value === 'string' && value.trim().startsWith('{');
|
package/lib/config.mjs
CHANGED
|
@@ -60,6 +60,9 @@ export function loadConfig(opts = {}) {
|
|
|
60
60
|
*/
|
|
61
61
|
export function loadConfigWithSource(opts = {}) {
|
|
62
62
|
const env = _fromEnv();
|
|
63
|
+
if (env.accessToken) {
|
|
64
|
+
return { config: env, source: null, profile: null };
|
|
65
|
+
}
|
|
63
66
|
const selection = resolveProfileSelection({ profileFlag: opts.profile });
|
|
64
67
|
|
|
65
68
|
let base = {};
|
|
@@ -187,6 +190,21 @@ export function clearTokens(scope = 'local') {
|
|
|
187
190
|
if (path) _writeJson(path, _stripTokens(_readJson(path)));
|
|
188
191
|
}
|
|
189
192
|
|
|
193
|
+
/** Remove all credential/session fields from the resolved legacy local auth file. */
|
|
194
|
+
export function clearLocalCredentialsForSource(source) {
|
|
195
|
+
if (!source || source.kind !== 'local') return false;
|
|
196
|
+
const path = source.path ?? _findLocalPath();
|
|
197
|
+
if (!path) return false;
|
|
198
|
+
|
|
199
|
+
const current = _readJson(path);
|
|
200
|
+
const hadCredentials = CRED_KEYS.some((key) => Object.prototype.hasOwnProperty.call(current, key));
|
|
201
|
+
if (!hadCredentials) return false;
|
|
202
|
+
|
|
203
|
+
const next = _stripCredentials(current);
|
|
204
|
+
_writeJson(path, next);
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
|
|
190
208
|
/**
|
|
191
209
|
* Persist refreshed tokens back to wherever the active credentials came from.
|
|
192
210
|
* @param {ConfigSource|null} source
|
|
@@ -325,6 +343,11 @@ export function localConfigPath() { return _findLocalPath(); }
|
|
|
325
343
|
export function globalConfigPath() { return GLOBAL_FILE; }
|
|
326
344
|
export function profilesConfigPath() { return PROFILES_FILE; }
|
|
327
345
|
|
|
346
|
+
/** Read the legacy global credentials file directly, without applying the cascade. */
|
|
347
|
+
export function loadGlobalConfig() {
|
|
348
|
+
return _readGlobal();
|
|
349
|
+
}
|
|
350
|
+
|
|
328
351
|
// ── Internals ────────────────────────────────────────────────────────────────
|
|
329
352
|
|
|
330
353
|
function _fromEnv() {
|
|
@@ -368,6 +391,14 @@ function _stripTokens(o) {
|
|
|
368
391
|
return rest;
|
|
369
392
|
}
|
|
370
393
|
|
|
394
|
+
function _stripCredentials(o) {
|
|
395
|
+
const out = { ...o };
|
|
396
|
+
for (const key of CRED_KEYS) {
|
|
397
|
+
delete out[key];
|
|
398
|
+
}
|
|
399
|
+
return out;
|
|
400
|
+
}
|
|
401
|
+
|
|
371
402
|
function _readGlobal() {
|
|
372
403
|
return existsSync(GLOBAL_FILE) ? _readJson(GLOBAL_FILE) : {};
|
|
373
404
|
}
|
package/lib/resource-config.mjs
CHANGED
|
@@ -73,7 +73,7 @@ export function loadResourceConfig(name) {
|
|
|
73
73
|
export function getListFields(res, name, override) {
|
|
74
74
|
// 1. CLI override
|
|
75
75
|
if (override) {
|
|
76
|
-
return _parseFieldsOverride(override);
|
|
76
|
+
return _parseFieldsOverride(override, res?.fieldAliases);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
// 2. Config file
|
|
@@ -181,7 +181,7 @@ export function getGetParams(name) {
|
|
|
181
181
|
* Parse a --fields override string.
|
|
182
182
|
* Supports: comma-separated, JSON object, JSON array.
|
|
183
183
|
*/
|
|
184
|
-
function _parseFieldsOverride(raw) {
|
|
184
|
+
function _parseFieldsOverride(raw, fieldAliases = {}) {
|
|
185
185
|
const trimmed = raw.trim();
|
|
186
186
|
|
|
187
187
|
// JSON object: {"Alias": "path", ...}
|
|
@@ -189,7 +189,7 @@ function _parseFieldsOverride(raw) {
|
|
|
189
189
|
try {
|
|
190
190
|
const obj = JSON.parse(trimmed);
|
|
191
191
|
if (typeof obj === 'object' && obj !== null && !Array.isArray(obj)) {
|
|
192
|
-
const apiFields = _toFieldAliasMap(obj);
|
|
192
|
+
const apiFields = _toFieldAliasMap(obj, fieldAliases);
|
|
193
193
|
return { apiFields, displayColumns: Object.keys(apiFields) };
|
|
194
194
|
}
|
|
195
195
|
} catch (e) {
|
|
@@ -205,7 +205,7 @@ function _parseFieldsOverride(raw) {
|
|
|
205
205
|
if (Array.isArray(arr)) {
|
|
206
206
|
const paths = arr.map(String);
|
|
207
207
|
const apiFields = {};
|
|
208
|
-
for (const p of paths) apiFields[p] = p;
|
|
208
|
+
for (const p of paths) apiFields[p] = normalizeFieldAlias(p, fieldAliases);
|
|
209
209
|
return { apiFields, displayColumns: paths };
|
|
210
210
|
}
|
|
211
211
|
} catch (e) {
|
|
@@ -217,7 +217,7 @@ function _parseFieldsOverride(raw) {
|
|
|
217
217
|
// Comma-separated: "ID,name,status"
|
|
218
218
|
const paths = trimmed.split(',').map(s => s.trim()).filter(Boolean);
|
|
219
219
|
const apiFields = {};
|
|
220
|
-
for (const p of paths) apiFields[p] = p;
|
|
220
|
+
for (const p of paths) apiFields[p] = normalizeFieldAlias(p, fieldAliases);
|
|
221
221
|
return { apiFields, displayColumns: paths };
|
|
222
222
|
}
|
|
223
223
|
|
|
@@ -227,14 +227,18 @@ function _parseFieldsOverride(raw) {
|
|
|
227
227
|
* @param {Record<string, JsonValue>} value
|
|
228
228
|
* @returns {Record<string,string>}
|
|
229
229
|
*/
|
|
230
|
-
function _toFieldAliasMap(value) {
|
|
230
|
+
function _toFieldAliasMap(value, fieldAliases = {}) {
|
|
231
231
|
const fields = {};
|
|
232
232
|
for (const [alias, field] of Object.entries(value)) {
|
|
233
|
-
fields[String(alias)] = String(field);
|
|
233
|
+
fields[String(alias)] = normalizeFieldAlias(String(field), fieldAliases);
|
|
234
234
|
}
|
|
235
235
|
return fields;
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
function normalizeFieldAlias(field, fieldAliases = {}) {
|
|
239
|
+
return fieldAliases[field] || field;
|
|
240
|
+
}
|
|
241
|
+
|
|
238
242
|
// ── Internals ────────────────────────────────────────────────────────────────
|
|
239
243
|
|
|
240
244
|
/**
|