@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/README.md
CHANGED
|
@@ -47,6 +47,9 @@ zeyos whoami --json
|
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
`whoami` does not print access tokens by default. Use `zeyos whoami --show-token --json` only when you intentionally need to pass a token to another local tool.
|
|
50
|
+
If a stored refresh token has expired, interactive `whoami` offers to re-authenticate
|
|
51
|
+
and then retries the user lookup. In `--json`/`--yaml` or non-interactive runs, it
|
|
52
|
+
prints a diagnostic plus the matching `zeyos login --force` command instead.
|
|
50
53
|
|
|
51
54
|
Inspect the CLI-supported resource registry:
|
|
52
55
|
|
|
@@ -77,15 +80,25 @@ Inspect dynamic schema definitions:
|
|
|
77
80
|
```bash
|
|
78
81
|
zeyos count customfields --json
|
|
79
82
|
zeyos list customfields --fields ID,name,identifier,context,type --json
|
|
83
|
+
zeyos count dunning --json
|
|
80
84
|
```
|
|
81
85
|
|
|
82
86
|
Inspect actionsteps/time-entry evidence and ticket mail:
|
|
83
87
|
|
|
84
88
|
```bash
|
|
85
89
|
zeyos list actionsteps --fields ID,name,status,date,duedate,effort,ticket,account --json
|
|
90
|
+
zeyos sum actionsteps effort --filter '{"status":[1,3]}' --json
|
|
86
91
|
zeyos list messages --fields ID,date,mailbox,subject,sender_email,to_email,ticket,reference --filter '{"ticket":42}' --json
|
|
87
92
|
```
|
|
88
93
|
|
|
94
|
+
Filters accept common agent-generated shapes and normalize them to the native ZeyOS
|
|
95
|
+
request form. Use `--query --json` to inspect the request without sending it:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
zeyos list tickets --filter '{"status":{"$nin":[8,9,10]},"priority":[3,4]}' --query --json
|
|
99
|
+
zeyos list accounts --filter '{"name__like":"Acme%","ID__in":[1,2,3]}' --query --json
|
|
100
|
+
```
|
|
101
|
+
|
|
89
102
|
Create, update, and delete:
|
|
90
103
|
|
|
91
104
|
```bash
|
package/bin/zeyos.mjs
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Commands:
|
|
8
8
|
* login Authenticate with ZeyOS
|
|
9
|
-
* logout Revoke session and clear
|
|
9
|
+
* logout Revoke session and clear stored credentials
|
|
10
10
|
* whoami Show current user info
|
|
11
11
|
* list <resource> List records
|
|
12
12
|
* count <resource> Count records
|
|
13
|
+
* sum <resource> Sum a numeric field across matching records
|
|
13
14
|
* get <resource> <id> Fetch a single record
|
|
14
15
|
* show <resource> <id> Alias for get
|
|
15
16
|
* create <resource> Create a new record
|
|
@@ -38,10 +39,11 @@ Usage: ${_z} <command> [options] [args…]
|
|
|
38
39
|
|
|
39
40
|
${_c.bold('Commands:')}
|
|
40
41
|
${_c.cyan('login')} Authenticate with a ZeyOS instance
|
|
41
|
-
${_c.cyan('logout')} Revoke session and clear stored
|
|
42
|
+
${_c.cyan('logout')} Revoke session and clear stored credentials
|
|
42
43
|
${_c.cyan('whoami')} Show currently authenticated user
|
|
43
44
|
${_c.cyan('list')} <resource> List / query records
|
|
44
45
|
${_c.cyan('count')} <resource> Count records (with optional filter)
|
|
46
|
+
${_c.cyan('sum')} <resource> <field> Sum a numeric field
|
|
45
47
|
${_c.cyan('get')} <resource> <id> Fetch a single record by ID
|
|
46
48
|
${_c.cyan('show')} <resource> <id> Alias for get
|
|
47
49
|
${_c.cyan('create')} <resource> Create a new record
|
|
@@ -68,6 +70,7 @@ ${_c.bold('Examples:')}
|
|
|
68
70
|
${_z} list tickets --filter '{"status":1}' --sort -lastmodified
|
|
69
71
|
${_z} list tickets --filter-file ./filters/open-tickets.json
|
|
70
72
|
${_z} count tickets --filter '{"status":1}'
|
|
73
|
+
${_z} sum actionsteps effort --filter '{"status":[1,3]}'
|
|
71
74
|
${_z} get ticket 42
|
|
72
75
|
${_z} get ticket 42 --all
|
|
73
76
|
${_z} create ticket --name "Fix login bug" --priority 3
|
|
@@ -105,6 +108,7 @@ const OPTIONS = {
|
|
|
105
108
|
'sort': { type: 'string' },
|
|
106
109
|
'limit': { type: 'string' },
|
|
107
110
|
'offset': { type: 'string' },
|
|
111
|
+
'page-size': { type: 'string' },
|
|
108
112
|
'expand': { type: 'string' },
|
|
109
113
|
'extdata': { type: 'boolean' },
|
|
110
114
|
'tags': { type: 'boolean' },
|
|
@@ -127,6 +131,12 @@ const OPTIONS = {
|
|
|
127
131
|
'from-current': { type: 'boolean' },
|
|
128
132
|
};
|
|
129
133
|
|
|
134
|
+
const COMMON_COMMAND_HELP = `\
|
|
135
|
+
Global options:
|
|
136
|
+
--profile <name> Use a named credential profile for this command
|
|
137
|
+
--no-color Disable ANSI colors
|
|
138
|
+
`;
|
|
139
|
+
|
|
130
140
|
// ── Command registry ──────────────────────────────────────────────────────────
|
|
131
141
|
// Maps every command and alias to the module that implements it.
|
|
132
142
|
|
|
@@ -136,6 +146,7 @@ const COMMANDS = {
|
|
|
136
146
|
whoami: '../commands/whoami.mjs',
|
|
137
147
|
list: '../commands/list.mjs',
|
|
138
148
|
count: '../commands/count.mjs',
|
|
149
|
+
sum: '../commands/sum.mjs',
|
|
139
150
|
get: '../commands/get.mjs',
|
|
140
151
|
show: '../commands/get.mjs',
|
|
141
152
|
create: '../commands/create.mjs',
|
|
@@ -161,6 +172,7 @@ const COMMANDS = {
|
|
|
161
172
|
// exception: they accept arbitrary `--<field>` flags, marked with `null` below.
|
|
162
173
|
|
|
163
174
|
const ALWAYS_FLAGS = ['help', 'json', 'yaml', 'no-color', 'profile'];
|
|
175
|
+
const LEADING_FLAGS = [...ALWAYS_FLAGS, 'version', 'query'];
|
|
164
176
|
const SKILLS_FLAGS = ['target', 'dir', 'global', 'local', 'force', 'yes', 'no-logo'];
|
|
165
177
|
const OKF_FLAGS = ['dir', 'out', 'force', 'no-logo'];
|
|
166
178
|
const PROFILE_FLAGS = ['base-url', 'client-id', 'secret', 'local', 'from-current'];
|
|
@@ -173,6 +185,7 @@ const COMMAND_FLAGS = {
|
|
|
173
185
|
whoami: ['show-token'],
|
|
174
186
|
list: ['fields', 'filter', 'filter-file', 'sort', 'limit', 'offset', 'extdata', 'expand', 'query'],
|
|
175
187
|
count: ['filter', 'filter-file', 'query'],
|
|
188
|
+
sum: ['filter', 'filter-file', 'limit', 'offset', 'page-size', 'query'],
|
|
176
189
|
get: GET_FLAGS,
|
|
177
190
|
show: GET_FLAGS,
|
|
178
191
|
create: null,
|
|
@@ -192,6 +205,8 @@ const COMMAND_FLAGS = {
|
|
|
192
205
|
profiles: PROFILE_FLAGS,
|
|
193
206
|
};
|
|
194
207
|
|
|
208
|
+
const CREDENTIAL_MUTATION_COMMANDS = new Set(['login', 'logout', 'profile', 'profiles']);
|
|
209
|
+
|
|
195
210
|
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
196
211
|
|
|
197
212
|
async function main() {
|
|
@@ -208,20 +223,37 @@ async function main() {
|
|
|
208
223
|
process.exit(0);
|
|
209
224
|
}
|
|
210
225
|
|
|
211
|
-
const
|
|
226
|
+
const lead = _splitLeadingFlags(argv);
|
|
227
|
+
if (lead.values.help && lead.argv.length === 0) {
|
|
228
|
+
process.stdout.write(HELP);
|
|
229
|
+
process.exit(0);
|
|
230
|
+
}
|
|
231
|
+
if (lead.values.version && lead.argv.length === 0) {
|
|
232
|
+
process.stdout.write(_VERSION + '\n');
|
|
233
|
+
process.exit(0);
|
|
234
|
+
}
|
|
235
|
+
if (lead.argv.length === 0) {
|
|
236
|
+
process.stdout.write(HELP);
|
|
237
|
+
process.exit(0);
|
|
238
|
+
}
|
|
212
239
|
|
|
213
|
-
|
|
214
|
-
// bad option rather than letting it masquerade as one.
|
|
240
|
+
const command = lead.argv[0];
|
|
215
241
|
if (command.startsWith('-')) {
|
|
216
|
-
|
|
217
|
-
process.exit(1);
|
|
242
|
+
_failLeadingOption(command);
|
|
218
243
|
}
|
|
219
244
|
|
|
220
|
-
const rest = argv.slice(1);
|
|
245
|
+
const rest = lead.argv.slice(1);
|
|
246
|
+
|
|
247
|
+
if (process.env.ZEYOS_CREDENTIALS_READONLY && CREDENTIAL_MUTATION_COMMANDS.has(command)) {
|
|
248
|
+
process.stderr.write(`Credential command "${command}" is disabled because ZEYOS_CREDENTIALS_READONLY is set.\n`);
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
221
251
|
|
|
222
252
|
// Parse remaining args permissively: known options are parsed normally and
|
|
223
253
|
// unknown --key value flags are captured too (so create/update accept fields).
|
|
224
|
-
const
|
|
254
|
+
const parsed = _parsePermissive(rest, OPTIONS);
|
|
255
|
+
const values = { ...lead.values, ...parsed.values };
|
|
256
|
+
const positional = parsed.positional;
|
|
225
257
|
|
|
226
258
|
const modulePath = COMMANDS[command];
|
|
227
259
|
if (!modulePath) {
|
|
@@ -232,7 +264,7 @@ async function main() {
|
|
|
232
264
|
const mod = await import(modulePath);
|
|
233
265
|
|
|
234
266
|
if (values.help) {
|
|
235
|
-
process.stdout.write(mod.USAGE ?? HELP);
|
|
267
|
+
process.stdout.write(_formatCommandHelp(mod.USAGE ?? HELP));
|
|
236
268
|
process.exit(0);
|
|
237
269
|
}
|
|
238
270
|
|
|
@@ -254,11 +286,121 @@ async function main() {
|
|
|
254
286
|
}
|
|
255
287
|
}
|
|
256
288
|
|
|
289
|
+
_validateKnownStringValues(values);
|
|
290
|
+
|
|
257
291
|
await mod.run(values, positional);
|
|
258
292
|
}
|
|
259
293
|
|
|
260
294
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
261
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Parse documented global flags that appear before the command name, e.g.
|
|
298
|
+
* `zeyos --profile dev whoami`. Command-specific flags remain after the
|
|
299
|
+
* command so the per-command allow-list can validate them.
|
|
300
|
+
*/
|
|
301
|
+
function _splitLeadingFlags(argv) {
|
|
302
|
+
const values = {};
|
|
303
|
+
let i = 0;
|
|
304
|
+
|
|
305
|
+
while (i < argv.length) {
|
|
306
|
+
const arg = argv[i];
|
|
307
|
+
|
|
308
|
+
if (arg === '--') {
|
|
309
|
+
return { values, argv: argv.slice(i + 1) };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (arg.startsWith('--')) {
|
|
313
|
+
const eqIdx = arg.indexOf('=');
|
|
314
|
+
const key = eqIdx === -1 ? arg.slice(2) : arg.slice(2, eqIdx);
|
|
315
|
+
const inlineVal = eqIdx === -1 ? undefined : arg.slice(eqIdx + 1);
|
|
316
|
+
const opt = OPTIONS[key];
|
|
317
|
+
|
|
318
|
+
if (!LEADING_FLAGS.includes(key)) {
|
|
319
|
+
_failLeadingOption(arg);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (opt?.type === 'boolean') {
|
|
323
|
+
values[key] = true;
|
|
324
|
+
i++;
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (opt?.type === 'string') {
|
|
329
|
+
if (inlineVal !== undefined) {
|
|
330
|
+
values[key] = inlineVal;
|
|
331
|
+
i++;
|
|
332
|
+
} else {
|
|
333
|
+
const next = argv[i + 1];
|
|
334
|
+
if (next !== undefined && next.startsWith('--')) {
|
|
335
|
+
values[key] = '';
|
|
336
|
+
i++;
|
|
337
|
+
} else {
|
|
338
|
+
values[key] = next ?? '';
|
|
339
|
+
i += 2;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (arg.startsWith('-') && arg.length === 2) {
|
|
347
|
+
const short = arg[1];
|
|
348
|
+
const match = Object.entries(OPTIONS).find(([, o]) => o.short === short);
|
|
349
|
+
if (!match || !LEADING_FLAGS.includes(match[0])) {
|
|
350
|
+
_failLeadingOption(arg);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const [key, opt] = match;
|
|
354
|
+
if (opt.type === 'boolean') {
|
|
355
|
+
values[key] = true;
|
|
356
|
+
i++;
|
|
357
|
+
} else {
|
|
358
|
+
const next = argv[i + 1];
|
|
359
|
+
if (next !== undefined && next.startsWith('--')) {
|
|
360
|
+
values[key] = '';
|
|
361
|
+
i++;
|
|
362
|
+
} else {
|
|
363
|
+
values[key] = next ?? '';
|
|
364
|
+
i += 2;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return { values, argv: argv.slice(i) };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function _failLeadingOption(flag) {
|
|
377
|
+
process.stderr.write(`Unknown option: "${flag}". Run 'zeyos --help' for usage.\n`);
|
|
378
|
+
process.exit(1);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function _formatCommandHelp(usage) {
|
|
382
|
+
if (usage === HELP || /--profile\s+<name>/.test(usage)) {
|
|
383
|
+
return usage;
|
|
384
|
+
}
|
|
385
|
+
if (/Global options:\n/.test(usage)) {
|
|
386
|
+
return usage.replace(
|
|
387
|
+
/Global options:\n/,
|
|
388
|
+
`Global options:\n --profile <name> Use a named credential profile for this command\n --no-color Disable ANSI colors\n`
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
const trimmed = usage.endsWith('\n') ? usage : `${usage}\n`;
|
|
392
|
+
return `${trimmed}\n${COMMON_COMMAND_HELP}`;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function _validateKnownStringValues(values) {
|
|
396
|
+
for (const [key, value] of Object.entries(values)) {
|
|
397
|
+
if (OPTIONS[key]?.type === 'string' && value === '') {
|
|
398
|
+
process.stderr.write(`Option --${key} requires a value.\n`);
|
|
399
|
+
process.exit(1);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
262
404
|
/**
|
|
263
405
|
* Parse argv with known options; capture unknown --key value pairs too.
|
|
264
406
|
* This lets create/update accept arbitrary --fieldName value flags.
|
package/commands/count.mjs
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { normalizeCountResult } from '@zeyos/client';
|
|
14
|
-
import { buildCliClient, callApi, maybeDryRun, parseJsonOptionOrFile, requireResource } from '../lib/command.mjs';
|
|
14
|
+
import { buildCliClient, callApi, maybeDryRun, normalizeFilterOperators, parseJsonOptionOrFile, requireResource } from '../lib/command.mjs';
|
|
15
15
|
import { outputMode, printJson, printYaml } from '../lib/output.mjs';
|
|
16
16
|
|
|
17
17
|
export const USAGE = `\
|
|
@@ -24,6 +24,8 @@ Arguments:
|
|
|
24
24
|
|
|
25
25
|
Options:
|
|
26
26
|
--filter <json> JSON filter object e.g. '{"status":1}'
|
|
27
|
+
Arrays normalize to IN; $lt/$lte/$gt/$gte/$ne/$in/$nin and suffix
|
|
28
|
+
keys like field__startswith/field__gt normalize to native operators
|
|
27
29
|
--filter-file <path>
|
|
28
30
|
Read JSON filter object from a file
|
|
29
31
|
--json Output as JSON ({ "count": N })
|
|
@@ -47,7 +49,7 @@ export async function run(values, positional) {
|
|
|
47
49
|
|
|
48
50
|
const filters = parseJsonOptionOrFile(values, 'filter', 'filter-file');
|
|
49
51
|
if (filters !== undefined) {
|
|
50
|
-
body.filters = filters;
|
|
52
|
+
body.filters = normalizeFilterOperators(filters, { fieldAliases: res.filterAliases });
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
// ── Call API ───────────────────────────────────────────────────────────────
|
package/commands/doctor.mjs
CHANGED
|
@@ -21,6 +21,8 @@ const ENV_KEYS = {
|
|
|
21
21
|
ZEYOS_CLIENT_SECRET: 'clientSecret',
|
|
22
22
|
ZEYOS_TOKEN: 'accessToken',
|
|
23
23
|
ZEYOS_REFRESH_TOKEN: 'refreshToken',
|
|
24
|
+
ZEYOS_NO_REFRESH: 'tokenOnly',
|
|
25
|
+
ZEYOS_CREDENTIALS_READONLY: 'credentialsReadonly',
|
|
24
26
|
};
|
|
25
27
|
|
|
26
28
|
export const USAGE = `\
|
|
@@ -82,8 +84,9 @@ function buildAgentReport() {
|
|
|
82
84
|
clientSecret: Boolean(config.clientSecret),
|
|
83
85
|
accessToken: Boolean(config.accessToken),
|
|
84
86
|
refreshToken: Boolean(config.refreshToken),
|
|
87
|
+
tokenOnly: Boolean(process.env.ZEYOS_TOKEN) || isTruthyEnv(process.env.ZEYOS_NO_REFRESH),
|
|
85
88
|
};
|
|
86
|
-
const ready = Boolean(effective.baseUrl && effective.
|
|
89
|
+
const ready = Boolean(effective.baseUrl && effective.accessToken && (effective.tokenOnly || (effective.clientId && effective.clientSecret)));
|
|
87
90
|
const resources = inspectResources();
|
|
88
91
|
|
|
89
92
|
return {
|
|
@@ -117,6 +120,10 @@ function buildAgentReport() {
|
|
|
117
120
|
};
|
|
118
121
|
}
|
|
119
122
|
|
|
123
|
+
function isTruthyEnv(value) {
|
|
124
|
+
return /^(1|true|yes|on)$/i.test(String(value || '').trim());
|
|
125
|
+
}
|
|
126
|
+
|
|
120
127
|
function inspectResources() {
|
|
121
128
|
const names = listResources();
|
|
122
129
|
const missing = [];
|
package/commands/list.mjs
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
callApi,
|
|
27
27
|
fail,
|
|
28
28
|
maybeDryRun,
|
|
29
|
+
normalizeFilterOperators,
|
|
29
30
|
parseJsonOptionOrFile,
|
|
30
31
|
requireApiMethod,
|
|
31
32
|
requireResource
|
|
@@ -42,6 +43,8 @@ Arguments:
|
|
|
42
43
|
Options:
|
|
43
44
|
--fields <list> Field selection (see formats below)
|
|
44
45
|
--filter <json> JSON filter object e.g. '{"status":1}'
|
|
46
|
+
Arrays normalize to IN; $lt/$lte/$gt/$gte/$ne/$in/$nin and suffix
|
|
47
|
+
keys like field__startswith/field__gt normalize to native operators
|
|
45
48
|
--filter-file <path>
|
|
46
49
|
Read JSON filter object from a file
|
|
47
50
|
--sort <fields> Sort expression e.g. '-lastmodified'
|
|
@@ -86,7 +89,7 @@ export async function run(values, positional) {
|
|
|
86
89
|
|
|
87
90
|
const filters = parseJsonOptionOrFile(values, 'filter', 'filter-file');
|
|
88
91
|
if (filters !== undefined) {
|
|
89
|
-
body.filters = filters;
|
|
92
|
+
body.filters = normalizeFilterOperators(filters, { fieldAliases: res.filterAliases });
|
|
90
93
|
}
|
|
91
94
|
|
|
92
95
|
if (values.sort) body.sort = values.sort.split(',').map(s => s.trim()).filter(Boolean);
|
package/commands/login.mjs
CHANGED
|
@@ -49,6 +49,10 @@ export async function run(values) {
|
|
|
49
49
|
const profileName = values.profile || null;
|
|
50
50
|
const scope = values.global ? 'global' : 'local';
|
|
51
51
|
const port = values.port ? Number(values.port) : DEFAULT_CALLBACK_PORT;
|
|
52
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
53
|
+
error('--port must be an integer between 1 and 65535.');
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
52
56
|
const redirectUri = callbackUri(port);
|
|
53
57
|
|
|
54
58
|
// Persist either into a named profile or the legacy local/global credential file.
|
package/commands/logout.mjs
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* zeyos logout
|
|
3
3
|
*
|
|
4
|
-
* Revokes the current access token (best-effort) and
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Revokes the current access token (best-effort) and clears the selected
|
|
5
|
+
* stored session. Local legacy credentials are cleared completely so a
|
|
6
|
+
* subsequent `zeyos login` starts from fresh connection parameters.
|
|
7
7
|
*
|
|
8
8
|
* Options:
|
|
9
9
|
* --global Clear the global credentials file
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { createZeyosClient, MemoryTokenStore } from '@zeyos/client';
|
|
13
|
-
import {
|
|
14
|
-
|
|
13
|
+
import {
|
|
14
|
+
loadConfigWithSource,
|
|
15
|
+
loadGlobalConfig,
|
|
16
|
+
clearTokensForSource,
|
|
17
|
+
clearLocalCredentialsForSource,
|
|
18
|
+
listProfiles
|
|
19
|
+
} from '../lib/config.mjs';
|
|
20
|
+
import { success, warn, info, error } from '../lib/output.mjs';
|
|
15
21
|
|
|
16
22
|
export const USAGE = `\
|
|
17
23
|
Usage: zeyos logout [options]
|
|
18
24
|
|
|
19
|
-
Revoke the current session and clear stored
|
|
25
|
+
Revoke the current session and clear stored credentials.
|
|
20
26
|
|
|
21
27
|
Options:
|
|
22
28
|
--profile <name> Log out of a specific profile
|
|
@@ -25,9 +31,29 @@ Options:
|
|
|
25
31
|
`;
|
|
26
32
|
|
|
27
33
|
export async function run(values) {
|
|
28
|
-
|
|
34
|
+
let config;
|
|
35
|
+
let source;
|
|
36
|
+
|
|
37
|
+
if (values.global) {
|
|
38
|
+
config = loadGlobalConfig();
|
|
39
|
+
source = { kind: 'global' };
|
|
40
|
+
} else {
|
|
41
|
+
const loaded = loadConfigWithSource({ profile: values.profile });
|
|
42
|
+
if (loaded.profile?.missing) {
|
|
43
|
+
const names = Object.keys(listProfiles().profiles);
|
|
44
|
+
const known = names.length ? `Known profiles: ${names.join(', ')}.` : 'No profiles defined yet.';
|
|
45
|
+
error(`Profile "${loaded.profile.name}" not found (selected via ${loaded.profile.origin}). ${known}`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
config = loaded.config;
|
|
49
|
+
source = loaded.source;
|
|
50
|
+
}
|
|
29
51
|
|
|
30
52
|
if (!config.accessToken) {
|
|
53
|
+
if (source?.kind === 'local' && clearLocalCredentialsForSource(source)) {
|
|
54
|
+
success('Logged out (local credentials).');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
31
57
|
warn('Not currently logged in.');
|
|
32
58
|
return;
|
|
33
59
|
}
|
|
@@ -58,8 +84,8 @@ export async function run(values) {
|
|
|
58
84
|
}
|
|
59
85
|
}
|
|
60
86
|
|
|
61
|
-
if (
|
|
62
|
-
|
|
87
|
+
if (source?.kind === 'local') {
|
|
88
|
+
clearLocalCredentialsForSource(source);
|
|
63
89
|
} else {
|
|
64
90
|
clearTokensForSource(source);
|
|
65
91
|
}
|
package/commands/profile.mjs
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* the credentials currently in effect (including tokens) into the new profile.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { createInterface } from 'node:readline';
|
|
17
18
|
import {
|
|
18
19
|
listProfiles, getProfile, upsertProfile, removeProfile,
|
|
19
20
|
setActiveProfile, writeLocalPin, readLocalPin,
|
|
@@ -31,7 +32,7 @@ Commands:
|
|
|
31
32
|
current Show which profile is in effect, and why
|
|
32
33
|
use <name> Make <name> the active profile (global)
|
|
33
34
|
use <name> --local Pin <name> to the current project (.zeyos/profile)
|
|
34
|
-
add <name> [options]
|
|
35
|
+
add [<name>] [options] Create or update a profile; prompts when run without connection options
|
|
35
36
|
remove <name> Delete a profile
|
|
36
37
|
|
|
37
38
|
Add options:
|
|
@@ -45,6 +46,7 @@ Global options:
|
|
|
45
46
|
-h, --help Show this help
|
|
46
47
|
|
|
47
48
|
Examples:
|
|
49
|
+
zeyos profile add # prompt for name and connection params
|
|
48
50
|
zeyos profile add dev --base-url https://zeyos.cms-it.de/dev
|
|
49
51
|
zeyos profile add prod --from-current
|
|
50
52
|
zeyos profile use prod
|
|
@@ -147,30 +149,42 @@ function cmdUse(values, name) {
|
|
|
147
149
|
|
|
148
150
|
// ── add ────────────────────────────────────────────────────────────────────────
|
|
149
151
|
|
|
150
|
-
function cmdAdd(values, name) {
|
|
151
|
-
|
|
152
|
+
async function cmdAdd(values, name) {
|
|
153
|
+
let promptSession = null;
|
|
154
|
+
const ask = (question, opts) => {
|
|
155
|
+
promptSession ??= createPromptSession();
|
|
156
|
+
return promptSession.ask(question, opts);
|
|
157
|
+
};
|
|
152
158
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
159
|
+
try {
|
|
160
|
+
const profileName = name || await ask('Profile name');
|
|
161
|
+
if (!profileName) fail('Profile name is required.');
|
|
162
|
+
|
|
163
|
+
let updates = {};
|
|
164
|
+
if (values['from-current']) {
|
|
165
|
+
const cfg = loadConfigWithSource().config; // whatever is in effect right now
|
|
166
|
+
for (const k of ['baseUrl', 'instance', 'clientId', 'clientSecret', 'accessToken', 'refreshToken', 'expiresAt', 'refreshTokenExpiresAt']) {
|
|
167
|
+
if (cfg[k] != null) updates[k] = cfg[k];
|
|
168
|
+
}
|
|
169
|
+
if (!updates.baseUrl) fail('Nothing to snapshot: no credentials are currently in effect.');
|
|
170
|
+
} else {
|
|
171
|
+
if (values['base-url']) updates.baseUrl = values['base-url'];
|
|
172
|
+
if (values['client-id']) updates.clientId = values['client-id'];
|
|
173
|
+
if (values.secret) updates.clientSecret = values.secret;
|
|
174
|
+
|
|
175
|
+
if (Object.keys(updates).length === 0) {
|
|
176
|
+
updates = await promptProfileCredentials(profileName, ask);
|
|
177
|
+
}
|
|
166
178
|
}
|
|
167
|
-
}
|
|
168
179
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
180
|
+
const existed = Boolean(getProfile(profileName));
|
|
181
|
+
upsertProfile(profileName, updates);
|
|
182
|
+
success(`${existed ? 'Updated' : 'Created'} profile "${profileName}".`);
|
|
183
|
+
if (!updates.accessToken) {
|
|
184
|
+
info(`Finish authenticating with: zeyos login --profile ${profileName}`);
|
|
185
|
+
}
|
|
186
|
+
} finally {
|
|
187
|
+
promptSession?.close();
|
|
174
188
|
}
|
|
175
189
|
}
|
|
176
190
|
|
|
@@ -209,3 +223,84 @@ function fail(message) {
|
|
|
209
223
|
error(message);
|
|
210
224
|
process.exit(1);
|
|
211
225
|
}
|
|
226
|
+
|
|
227
|
+
async function promptProfileCredentials(name, ask) {
|
|
228
|
+
const existing = getProfile(name) || {};
|
|
229
|
+
|
|
230
|
+
info(`Creating profile "${name}".`);
|
|
231
|
+
info('This stores the platform and OAuth app credentials; tokens are added by login.');
|
|
232
|
+
console.error('');
|
|
233
|
+
|
|
234
|
+
const baseUrl = await ask('ZeyOS platform URL', { currentValue: existing.baseUrl });
|
|
235
|
+
const clientId = await ask('Application ID', { currentValue: existing.clientId });
|
|
236
|
+
const clientSecret = await ask('Application secret', { currentValue: existing.clientSecret, secret: true });
|
|
237
|
+
|
|
238
|
+
if (!baseUrl || !clientId || !clientSecret) {
|
|
239
|
+
fail('ZeyOS URL, application ID and secret are all required.');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return { baseUrl, clientId, clientSecret };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function createPromptSession() {
|
|
246
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: process.stdin.isTTY && process.stderr.isTTY });
|
|
247
|
+
const originalWrite = rl._writeToOutput.bind(rl);
|
|
248
|
+
let hiddenPrompt = null;
|
|
249
|
+
let closed = false;
|
|
250
|
+
const queuedLines = [];
|
|
251
|
+
const waitingResolvers = [];
|
|
252
|
+
|
|
253
|
+
rl.on('line', (line) => {
|
|
254
|
+
const resolve = waitingResolvers.shift();
|
|
255
|
+
if (resolve) {
|
|
256
|
+
resolve(line);
|
|
257
|
+
} else {
|
|
258
|
+
queuedLines.push(line);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
rl.on('close', () => {
|
|
263
|
+
closed = true;
|
|
264
|
+
let resolve;
|
|
265
|
+
while ((resolve = waitingResolvers.shift())) {
|
|
266
|
+
resolve('');
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
rl._writeToOutput = (value) => {
|
|
271
|
+
if (!hiddenPrompt || String(value).includes(hiddenPrompt) || value === '\n' || value === '\r\n') {
|
|
272
|
+
originalWrite(value);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const readLine = () => {
|
|
277
|
+
if (queuedLines.length) {
|
|
278
|
+
return Promise.resolve(queuedLines.shift());
|
|
279
|
+
}
|
|
280
|
+
if (closed) {
|
|
281
|
+
return Promise.resolve('');
|
|
282
|
+
}
|
|
283
|
+
return new Promise(resolve => {
|
|
284
|
+
waitingResolvers.push(resolve);
|
|
285
|
+
});
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
ask(question, opts = {}) {
|
|
290
|
+
const currentValue = opts.currentValue || '';
|
|
291
|
+
const defaultLabel = opts.secret && currentValue ? 'stored, press Enter to keep' : currentValue;
|
|
292
|
+
const prompt = defaultLabel ? `${question} [${defaultLabel}]` : question;
|
|
293
|
+
hiddenPrompt = opts.secret && process.stdin.isTTY && process.stderr.isTTY ? prompt : null;
|
|
294
|
+
process.stderr.write(`${prompt}: `);
|
|
295
|
+
|
|
296
|
+
return readLine()
|
|
297
|
+
.then(answer => {
|
|
298
|
+
hiddenPrompt = null;
|
|
299
|
+
return answer.trim() || currentValue || '';
|
|
300
|
+
});
|
|
301
|
+
},
|
|
302
|
+
close() {
|
|
303
|
+
rl.close();
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
}
|