@zeyos/cli 0.1.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/LICENSE +21 -0
- package/README.md +87 -0
- package/bin/zeyos.mjs +280 -0
- package/commands/count.mjs +63 -0
- package/commands/create.mjs +62 -0
- package/commands/delete.mjs +68 -0
- package/commands/describe.mjs +102 -0
- package/commands/get.mjs +127 -0
- package/commands/list.mjs +162 -0
- package/commands/login.mjs +223 -0
- package/commands/logout.mjs +63 -0
- package/commands/resources.mjs +49 -0
- package/commands/skills.mjs +363 -0
- package/commands/update.mjs +71 -0
- package/commands/whoami.mjs +100 -0
- package/config/account.json +18 -0
- package/config/item.json +16 -0
- package/config/project.json +16 -0
- package/config/task.json +18 -0
- package/config/ticket.json +19 -0
- package/lib/client.mjs +69 -0
- package/lib/command.mjs +148 -0
- package/lib/config.mjs +164 -0
- package/lib/flags.mjs +44 -0
- package/lib/login-server.mjs +149 -0
- package/lib/output.mjs +284 -0
- package/lib/resource-config.mjs +289 -0
- package/lib/resources.mjs +234 -0
- package/lib/types.mjs +46 -0
- package/package.json +47 -0
package/commands/get.mjs
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* zeyos get <resource> <id>
|
|
3
|
+
* zeyos show <resource> <id>
|
|
4
|
+
*
|
|
5
|
+
* Fetch and display a single record by ID.
|
|
6
|
+
*
|
|
7
|
+
* Options:
|
|
8
|
+
* --fields <list> Fields to display (comma-separated or JSON)
|
|
9
|
+
* --extdata Include extended data fields
|
|
10
|
+
* --tags Include tags
|
|
11
|
+
* --expand <list> Expand JSON/binary columns e.g. 'binfile'
|
|
12
|
+
* --all Fetch all data (extdata, tags, all fields)
|
|
13
|
+
* --json Output as JSON
|
|
14
|
+
* --yaml Output as YAML
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { loadConfig } from '../lib/config.mjs';
|
|
18
|
+
import { canonicalName } from '../lib/resources.mjs';
|
|
19
|
+
import { getGetFields, getGetParams } from '../lib/resource-config.mjs';
|
|
20
|
+
import { outputMode, printJson, printYaml, printRecord, buildDateFormatters } from '../lib/output.mjs';
|
|
21
|
+
import {
|
|
22
|
+
buildCliClient,
|
|
23
|
+
callApi,
|
|
24
|
+
fail,
|
|
25
|
+
requireRecordId,
|
|
26
|
+
requireResource
|
|
27
|
+
} from '../lib/command.mjs';
|
|
28
|
+
|
|
29
|
+
export const USAGE = `\
|
|
30
|
+
Usage: zeyos get <resource> <id> [options]
|
|
31
|
+
zeyos show <resource> <id> [options]
|
|
32
|
+
|
|
33
|
+
Fetch and display a single record.
|
|
34
|
+
|
|
35
|
+
Arguments:
|
|
36
|
+
resource Resource name (e.g. ticket, account)
|
|
37
|
+
id Record ID
|
|
38
|
+
|
|
39
|
+
Options:
|
|
40
|
+
--fields <list> Fields to display
|
|
41
|
+
--extdata Include extended data fields
|
|
42
|
+
--tags Include tags
|
|
43
|
+
--expand <list> Expand JSON/binary columns (e.g. binfile, items)
|
|
44
|
+
--all Fetch all data (extdata + tags + all fields)
|
|
45
|
+
--json Output as JSON
|
|
46
|
+
--yaml Output as YAML
|
|
47
|
+
-h, --help Show this help
|
|
48
|
+
|
|
49
|
+
Fields format:
|
|
50
|
+
Comma-separated: --fields ID,name,status,duedate
|
|
51
|
+
JSON object: --fields '{"Id": "ID", "Name": "name"}'
|
|
52
|
+
|
|
53
|
+
Examples:
|
|
54
|
+
zeyos get ticket 42
|
|
55
|
+
zeyos get ticket 42 --extdata
|
|
56
|
+
zeyos get ticket 42 --extdata --tags
|
|
57
|
+
zeyos get ticket 42 --all
|
|
58
|
+
zeyos show account 7 --json
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
export async function run(values, positional) {
|
|
62
|
+
const resourceName = positional[0];
|
|
63
|
+
const id = positional[1];
|
|
64
|
+
|
|
65
|
+
const res = requireResource(resourceName, 'zeyos get <resource> <id>', 'get', 'single-record fetch');
|
|
66
|
+
requireRecordId(id, 'zeyos get <resource> <id>');
|
|
67
|
+
|
|
68
|
+
const resName = canonicalName(resourceName);
|
|
69
|
+
const clientState = buildCliClient();
|
|
70
|
+
|
|
71
|
+
// ── Build params ───────────────────────────────────────────────────────────
|
|
72
|
+
// GET endpoints use query parameters like ?extdata=1&tags=1 to include
|
|
73
|
+
// additional data. The `expand` parameter is only for JSON/binary columns.
|
|
74
|
+
const params = { ID: id };
|
|
75
|
+
|
|
76
|
+
// Collect query params from CLI flags, config defaults, and --all
|
|
77
|
+
const query = getGetParams(resName);
|
|
78
|
+
|
|
79
|
+
// Explicit CLI flags always win
|
|
80
|
+
if (values.extdata) query.extdata = 1;
|
|
81
|
+
if (values.tags) query.tags = 1;
|
|
82
|
+
|
|
83
|
+
// --all includes everything
|
|
84
|
+
if (values.all) {
|
|
85
|
+
query.extdata = 1;
|
|
86
|
+
query.tags = 1;
|
|
87
|
+
query.positions = 1;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// --expand is for JSON/binary column expansion (e.g. binfile, items, data)
|
|
91
|
+
if (values.expand) {
|
|
92
|
+
const expandCols = values.expand.split(',').map(s => s.trim()).filter(Boolean);
|
|
93
|
+
for (const col of expandCols) query[col] = 1;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (Object.keys(query).length > 0) {
|
|
97
|
+
params.query = query;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Call API ───────────────────────────────────────────────────────────────
|
|
101
|
+
const record = await callApi(clientState, res.get, params, {
|
|
102
|
+
notFoundMessage: `${resourceName} #${id} not found.`
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (!record) {
|
|
106
|
+
fail(`${resourceName} #${id} not found.`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Determine display fields ───────────────────────────────────────────────
|
|
110
|
+
const fieldConfig = values.all ? undefined : getGetFields(resName, values.fields);
|
|
111
|
+
const fields = fieldConfig?.keys;
|
|
112
|
+
const fieldLabels = fieldConfig?.labels ?? {};
|
|
113
|
+
|
|
114
|
+
const mode = outputMode(values);
|
|
115
|
+
|
|
116
|
+
if (mode === 'json') {
|
|
117
|
+
printJson(record);
|
|
118
|
+
} else if (mode === 'yaml') {
|
|
119
|
+
printYaml(record);
|
|
120
|
+
} else {
|
|
121
|
+
const cfg = loadConfig();
|
|
122
|
+
const dateFormat = cfg.dateFormat ?? 'YYYY-MM-DD';
|
|
123
|
+
const displayKeys = fields ?? Object.keys(record);
|
|
124
|
+
const formatters = buildDateFormatters(displayKeys, dateFormat);
|
|
125
|
+
printRecord(record, displayKeys, fieldLabels, formatters);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* zeyos list <resource>
|
|
3
|
+
*
|
|
4
|
+
* Query a collection of records.
|
|
5
|
+
*
|
|
6
|
+
* Options:
|
|
7
|
+
* --fields <list> Field selection (comma-separated or JSON object)
|
|
8
|
+
* --filter <json> JSON filter object e.g. '{"status":1}'
|
|
9
|
+
* --sort <field> Sort field, prefix with - for descending e.g. '-lastmodified'
|
|
10
|
+
* --limit <n> Max records to fetch (default: 50)
|
|
11
|
+
* --offset <n> Skip first N records (default: 0)
|
|
12
|
+
* --extdata Include extended data fields
|
|
13
|
+
* --expand <list> Expand JSON/binary columns e.g. 'binfile'
|
|
14
|
+
* --json Output as JSON
|
|
15
|
+
* --yaml Output as YAML
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { normalizeListResult } from '@zeyos/client';
|
|
19
|
+
import { loadConfig } from '../lib/config.mjs';
|
|
20
|
+
import { canonicalName } from '../lib/resources.mjs';
|
|
21
|
+
import { getListFields } from '../lib/resource-config.mjs';
|
|
22
|
+
import { outputMode, printJson, printYaml, printTable, buildDateFormatters, warn, info } from '../lib/output.mjs';
|
|
23
|
+
import {
|
|
24
|
+
buildCliClient,
|
|
25
|
+
callApi,
|
|
26
|
+
fail,
|
|
27
|
+
parseJsonOption,
|
|
28
|
+
requireApiMethod,
|
|
29
|
+
requireResource
|
|
30
|
+
} from '../lib/command.mjs';
|
|
31
|
+
|
|
32
|
+
export const USAGE = `\
|
|
33
|
+
Usage: zeyos list <resource> [options]
|
|
34
|
+
|
|
35
|
+
List records of a given resource type.
|
|
36
|
+
|
|
37
|
+
Arguments:
|
|
38
|
+
resource Resource name (e.g. tickets, accounts, tasks)
|
|
39
|
+
|
|
40
|
+
Options:
|
|
41
|
+
--fields <list> Field selection (see formats below)
|
|
42
|
+
--filter <json> JSON filter object e.g. '{"status":1}'
|
|
43
|
+
--sort <fields> Sort expression e.g. '-lastmodified'
|
|
44
|
+
--limit <n> Max records (default: 50)
|
|
45
|
+
--offset <n> Skip first N records (default: 0)
|
|
46
|
+
--extdata Include extended data fields
|
|
47
|
+
--expand <list> Expand JSON/binary columns (e.g. binfile, items)
|
|
48
|
+
--json Output as JSON
|
|
49
|
+
--yaml Output as YAML
|
|
50
|
+
-h, --help Show this help
|
|
51
|
+
|
|
52
|
+
Fields format:
|
|
53
|
+
Comma-separated: --fields ID,name,status,duedate
|
|
54
|
+
JSON object: --fields '{"Id": "ID", "Name": "name", "City": "contact.city"}'
|
|
55
|
+
JSON array: --fields '["ID", "name", "status"]'
|
|
56
|
+
|
|
57
|
+
Examples:
|
|
58
|
+
zeyos list tickets
|
|
59
|
+
zeyos list tickets --filter '{"status":1}' --sort -lastmodified
|
|
60
|
+
zeyos list tickets --fields ID,name,status --limit 10
|
|
61
|
+
zeyos list accounts --fields '{"Name": "lastname", "City": "contact.city"}'
|
|
62
|
+
zeyos list tickets --extdata
|
|
63
|
+
zeyos list accounts --json
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
export async function run(values, positional) {
|
|
67
|
+
const resourceName = positional[0];
|
|
68
|
+
const res = requireResource(resourceName, 'zeyos list <resource>');
|
|
69
|
+
|
|
70
|
+
const resName = canonicalName(resourceName);
|
|
71
|
+
const clientState = buildCliClient();
|
|
72
|
+
|
|
73
|
+
// ── Resolve field config ──────────────────────────────────────────────────
|
|
74
|
+
const { apiFields, displayColumns } = getListFields(res, resName, values.fields);
|
|
75
|
+
|
|
76
|
+
// ── Build request body ─────────────────────────────────────────────────────
|
|
77
|
+
const body = {};
|
|
78
|
+
|
|
79
|
+
// Pass configured fields to the API for server-side field selection
|
|
80
|
+
if (apiFields) body.fields = apiFields;
|
|
81
|
+
|
|
82
|
+
if (values.filter) {
|
|
83
|
+
body.filters = parseJsonOption(values.filter, 'filter');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (values.sort) body.sort = values.sort.split(',').map(s => s.trim()).filter(Boolean);
|
|
87
|
+
|
|
88
|
+
if (values.limit != null) {
|
|
89
|
+
const n = parseInt(values.limit, 10);
|
|
90
|
+
if (isNaN(n)) fail('--limit must be a number.');
|
|
91
|
+
body.limit = n;
|
|
92
|
+
} else {
|
|
93
|
+
body.limit = 50;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (values.offset != null) {
|
|
97
|
+
const n = parseInt(values.offset, 10);
|
|
98
|
+
if (isNaN(n)) fail('--offset must be a number.');
|
|
99
|
+
body.offset = n;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// --extdata includes extended data fields in the response
|
|
103
|
+
if (values.extdata) {
|
|
104
|
+
body.extdata = 1;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// --expand is for JSON/binary column expansion only (e.g. binfile, items, data)
|
|
108
|
+
if (values.expand) {
|
|
109
|
+
body.expand = values.expand.split(',').map(s => s.trim()).filter(Boolean);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Call API ───────────────────────────────────────────────────────────────
|
|
113
|
+
const fn = requireApiMethod(clientState, res.list);
|
|
114
|
+
let records = await callApi(clientState, res.list, body);
|
|
115
|
+
|
|
116
|
+
records = normalizeListResult(records).data;
|
|
117
|
+
|
|
118
|
+
// ── Output ─────────────────────────────────────────────────────────────────
|
|
119
|
+
const mode = outputMode(values);
|
|
120
|
+
const limit = body.limit ?? 50;
|
|
121
|
+
const offset = body.offset ?? 0;
|
|
122
|
+
|
|
123
|
+
if (mode === 'json') {
|
|
124
|
+
printJson(records);
|
|
125
|
+
} else if (mode === 'yaml') {
|
|
126
|
+
printYaml(records);
|
|
127
|
+
} else if (records.length === 0) {
|
|
128
|
+
warn(`No ${resourceName} found.`);
|
|
129
|
+
return;
|
|
130
|
+
} else {
|
|
131
|
+
const cfg = loadConfig();
|
|
132
|
+
const dateFormat = cfg.dateFormat ?? 'YYYY-MM-DD';
|
|
133
|
+
const formatters = buildDateFormatters(displayColumns, dateFormat, apiFields);
|
|
134
|
+
printTable(records, displayColumns, {}, formatters);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── Pagination / truncation hint ──────────────────────────────────────────
|
|
138
|
+
// Emitted to stderr in EVERY output mode (including --json), so an agent that
|
|
139
|
+
// pipes `list … --json` into a counter gets a signal that the default
|
|
140
|
+
// --limit truncated the result, instead of a silently-wrong total. For a
|
|
141
|
+
// "how many?" question, `zeyos count <resource>` returns the true total.
|
|
142
|
+
const from = offset + 1;
|
|
143
|
+
const to = offset + records.length;
|
|
144
|
+
|
|
145
|
+
if (records.length >= limit) {
|
|
146
|
+
try {
|
|
147
|
+
const countBody = { count: true };
|
|
148
|
+
if (body.filters) countBody.filters = body.filters;
|
|
149
|
+
const countResult = await fn(countBody);
|
|
150
|
+
const total = countResult?.count ?? null;
|
|
151
|
+
if (total !== null && total > records.length) {
|
|
152
|
+
info(`Showing ${from}–${to} of ${total} (default --limit ${limit} truncated this — pass --limit, --offset ${to} for the next page, or use \`zeyos count ${resourceName}\` for the total).`);
|
|
153
|
+
} else if (total !== null) {
|
|
154
|
+
info(`Showing ${from}–${to} of ${total} (--offset ${to} for next page)`);
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
// Non-critical — skip pagination info
|
|
158
|
+
}
|
|
159
|
+
} else if (offset > 0) {
|
|
160
|
+
info(`Showing ${from}–${to} of ${to}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* zeyos login
|
|
3
|
+
*
|
|
4
|
+
* Starts an OAuth 2.0 authorization-code flow.
|
|
5
|
+
*
|
|
6
|
+
* Always prints the authorization URL and callback URL so the user can
|
|
7
|
+
* copy/paste them. Simultaneously tries to open the browser and start
|
|
8
|
+
* a local callback server to catch the redirect automatically.
|
|
9
|
+
*
|
|
10
|
+
* Options:
|
|
11
|
+
* --base-url <url> ZeyOS platform URL (e.g. https://zeyos.cms-it.de/demo)
|
|
12
|
+
* --client-id <id> OAuth client ID
|
|
13
|
+
* --secret <secret> OAuth client secret
|
|
14
|
+
* --scope <scope> OAuth scope (default: all)
|
|
15
|
+
* --port <port> Local callback server port (default: 9005)
|
|
16
|
+
* --global Save credentials to ~/.config/zeyos/credentials.json
|
|
17
|
+
* --force Re-authenticate even if already logged in
|
|
18
|
+
* --manual Skip browser auto-open, prompt for code directly
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { createInterface } from 'node:readline';
|
|
22
|
+
import { createZeyosClient, MemoryTokenStore } from '@zeyos/client';
|
|
23
|
+
import { saveConfig, loadConfig } from '../lib/config.mjs';
|
|
24
|
+
import { waitForCallback, callbackUri, BrowserUnavailableError } from '../lib/login-server.mjs';
|
|
25
|
+
import { success, error, info, warn } from '../lib/output.mjs';
|
|
26
|
+
|
|
27
|
+
const DEFAULT_CALLBACK_PORT = 9005;
|
|
28
|
+
|
|
29
|
+
export const USAGE = `\
|
|
30
|
+
Usage: zeyos login [options]
|
|
31
|
+
|
|
32
|
+
Authenticate with a ZeyOS instance.
|
|
33
|
+
|
|
34
|
+
Options:
|
|
35
|
+
--base-url <url> ZeyOS platform URL (prompted if missing)
|
|
36
|
+
--client-id <id> OAuth client ID (prompted if missing)
|
|
37
|
+
--secret <secret> OAuth client secret (prompted if missing)
|
|
38
|
+
--scope <scope> OAuth scope (default: all)
|
|
39
|
+
--port <port> Local callback server port (default: 9005)
|
|
40
|
+
--global Store credentials globally (~/.config/zeyos/credentials.json)
|
|
41
|
+
--force Re-authenticate even if already logged in
|
|
42
|
+
--clean Discard saved config and re-enter all parameters
|
|
43
|
+
--manual Skip auto-browser, prompt for code paste
|
|
44
|
+
-h, --help Show this help
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
export async function run(values) {
|
|
48
|
+
const scope = values.global ? 'global' : 'local';
|
|
49
|
+
const port = values.port ? Number(values.port) : DEFAULT_CALLBACK_PORT;
|
|
50
|
+
const redirectUri = callbackUri(port);
|
|
51
|
+
|
|
52
|
+
// ── Resolve connection params ──────────────────────────────────────────────
|
|
53
|
+
const existing = values.clean ? {} : loadConfig();
|
|
54
|
+
if (values.clean) values.force = true;
|
|
55
|
+
|
|
56
|
+
let baseUrl = values['base-url'] ?? existing.baseUrl;
|
|
57
|
+
let clientId = values['client-id'] ?? existing.clientId;
|
|
58
|
+
let clientSecret = values['secret'] ?? existing.clientSecret;
|
|
59
|
+
|
|
60
|
+
// Prompt interactively for any missing values
|
|
61
|
+
if (!baseUrl) baseUrl = await _prompt('ZeyOS platform URL');
|
|
62
|
+
|
|
63
|
+
// Before asking for the application ID/secret, show the callback URL so the
|
|
64
|
+
// user can register it as the redirect URI of their ZeyOS OAuth application
|
|
65
|
+
// (the ID/secret only exist once that app has been created).
|
|
66
|
+
if (!clientId || !clientSecret) {
|
|
67
|
+
console.error('');
|
|
68
|
+
info('Add this callback URL as the redirect URI of your ZeyOS OAuth app:');
|
|
69
|
+
console.error(` ${redirectUri}`);
|
|
70
|
+
console.error('');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!clientId) clientId = await _prompt('Application ID');
|
|
74
|
+
if (!clientSecret) clientSecret = await _promptSecret('Application secret');
|
|
75
|
+
|
|
76
|
+
if (!baseUrl || !clientId || !clientSecret) {
|
|
77
|
+
error('ZeyOS URL, application ID and secret are all required.');
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Save connection params immediately so they are available on retries
|
|
82
|
+
saveConfig({ baseUrl, clientId, clientSecret }, scope);
|
|
83
|
+
|
|
84
|
+
// ── Check if already authenticated ────────────────────────────────────────
|
|
85
|
+
if (existing.accessToken && !values.force) {
|
|
86
|
+
warn('Already logged in. Use --force to re-authenticate.');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Build a temporary client (no token yet) ────────────────────────────────
|
|
91
|
+
const tokenStore = new MemoryTokenStore();
|
|
92
|
+
const client = createZeyosClient({
|
|
93
|
+
platform: baseUrl,
|
|
94
|
+
auth: {
|
|
95
|
+
mode: 'oauth',
|
|
96
|
+
oauth: { clientId, clientSecret, tokenStore, autoRefresh: false },
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Generate random state for CSRF protection
|
|
101
|
+
const state = _randomHex(32);
|
|
102
|
+
|
|
103
|
+
const authUrl = client.oauth2.buildAuthorizationUrl({
|
|
104
|
+
redirectUri,
|
|
105
|
+
state,
|
|
106
|
+
scope: values.scope,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ── Always show URLs ───────────────────────────────────────────────────────
|
|
110
|
+
console.error('');
|
|
111
|
+
info('OAuth 2.0 Authorization Code Flow');
|
|
112
|
+
console.error('');
|
|
113
|
+
console.error(' Callback URL (redirect URI):');
|
|
114
|
+
console.error(` ${redirectUri}`);
|
|
115
|
+
console.error('');
|
|
116
|
+
console.error(' Authorization URL:');
|
|
117
|
+
console.error(` ${authUrl}`);
|
|
118
|
+
console.error('');
|
|
119
|
+
|
|
120
|
+
// ── Get the authorization code ─────────────────────────────────────────────
|
|
121
|
+
let code;
|
|
122
|
+
|
|
123
|
+
if (values.manual) {
|
|
124
|
+
// Skip browser, prompt directly
|
|
125
|
+
code = await _promptCode();
|
|
126
|
+
} else {
|
|
127
|
+
// Try browser + callback server; fall back to manual on any failure
|
|
128
|
+
code = await _browserFlowWithFallback(authUrl, port, state);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!code) {
|
|
132
|
+
error('No authorization code provided.');
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── Exchange code for tokens ───────────────────────────────────────────────
|
|
137
|
+
try {
|
|
138
|
+
info('Exchanging authorization code for tokens…');
|
|
139
|
+
const tokenSet = await client.oauth2.exchangeAuthorizationCode({ code, redirectUri });
|
|
140
|
+
|
|
141
|
+
saveConfig({
|
|
142
|
+
baseUrl,
|
|
143
|
+
clientId,
|
|
144
|
+
clientSecret,
|
|
145
|
+
accessToken: tokenSet.accessToken,
|
|
146
|
+
refreshToken: tokenSet.refreshToken ?? undefined,
|
|
147
|
+
expiresAt: tokenSet.expiresAt ?? undefined,
|
|
148
|
+
refreshTokenExpiresAt: tokenSet.refreshTokenExpiresAt ?? undefined,
|
|
149
|
+
}, scope);
|
|
150
|
+
|
|
151
|
+
success('Logged in successfully.');
|
|
152
|
+
} catch (err) {
|
|
153
|
+
error(`Token exchange failed: ${err.message}`);
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Try the browser + callback server flow.
|
|
162
|
+
* On any failure, fall back to prompting the user for the code.
|
|
163
|
+
*/
|
|
164
|
+
async function _browserFlowWithFallback(authUrl, port, state) {
|
|
165
|
+
try {
|
|
166
|
+
info('Starting local callback server and opening browser…');
|
|
167
|
+
const code = await waitForCallback(authUrl, port, state);
|
|
168
|
+
return code;
|
|
169
|
+
} catch (err) {
|
|
170
|
+
if (err instanceof BrowserUnavailableError) {
|
|
171
|
+
warn('Could not open browser automatically.');
|
|
172
|
+
} else {
|
|
173
|
+
warn(`Callback server error: ${err.message}`);
|
|
174
|
+
}
|
|
175
|
+
console.error('');
|
|
176
|
+
console.error(' Paste the authorization code from the browser redirect URL.');
|
|
177
|
+
console.error(' (The URL looks like: …/callback?code=XXXXXXXX&state=…)');
|
|
178
|
+
console.error('');
|
|
179
|
+
return _promptCode();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function _prompt(question) {
|
|
184
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
185
|
+
return new Promise(resolve => {
|
|
186
|
+
rl.question(`${question}: `, answer => {
|
|
187
|
+
rl.close();
|
|
188
|
+
resolve(answer.trim());
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function _promptSecret(question) {
|
|
194
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
195
|
+
return _prompt(question);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: true });
|
|
199
|
+
const originalWrite = rl._writeToOutput.bind(rl);
|
|
200
|
+
rl._writeToOutput = (value) => {
|
|
201
|
+
if (String(value).includes(question) || value === '\n' || value === '\r\n') {
|
|
202
|
+
originalWrite(value);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
return new Promise(resolve => {
|
|
207
|
+
rl.question(`${question}: `, answer => {
|
|
208
|
+
rl.close();
|
|
209
|
+
resolve(answer.trim());
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function _promptCode() {
|
|
215
|
+
return _prompt('Paste the authorization code');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function _randomHex(length) {
|
|
219
|
+
return Array.from(
|
|
220
|
+
{ length },
|
|
221
|
+
() => Math.floor(Math.random() * 256).toString(16).padStart(2, '0')
|
|
222
|
+
).join('');
|
|
223
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* zeyos logout
|
|
3
|
+
*
|
|
4
|
+
* Revokes the current access token (best-effort) and removes stored tokens
|
|
5
|
+
* from the credential file. Connection params (baseUrl, clientId, clientSecret)
|
|
6
|
+
* are kept so a subsequent `zeyos login` works without re-entering them.
|
|
7
|
+
*
|
|
8
|
+
* Options:
|
|
9
|
+
* --global Clear the global credentials file
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createZeyosClient, MemoryTokenStore } from '@zeyos/client';
|
|
13
|
+
import { loadConfig, clearTokens } from '../lib/config.mjs';
|
|
14
|
+
import { success, warn, info } from '../lib/output.mjs';
|
|
15
|
+
|
|
16
|
+
export const USAGE = `\
|
|
17
|
+
Usage: zeyos logout [options]
|
|
18
|
+
|
|
19
|
+
Revoke the current session and clear stored tokens.
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
--global Target the global credentials file
|
|
23
|
+
-h, --help Show this help
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
export async function run(values) {
|
|
27
|
+
const scope = values.global ? 'global' : 'local';
|
|
28
|
+
const config = loadConfig();
|
|
29
|
+
|
|
30
|
+
if (!config.accessToken) {
|
|
31
|
+
warn('Not currently logged in.');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Best-effort token revocation
|
|
36
|
+
if (config.baseUrl && config.clientId && config.clientSecret) {
|
|
37
|
+
try {
|
|
38
|
+
const tokenStore = new MemoryTokenStore({
|
|
39
|
+
accessToken: config.accessToken,
|
|
40
|
+
refreshToken: config.refreshToken,
|
|
41
|
+
});
|
|
42
|
+
const client = createZeyosClient({
|
|
43
|
+
platform: config.baseUrl,
|
|
44
|
+
auth: {
|
|
45
|
+
mode: 'oauth',
|
|
46
|
+
oauth: {
|
|
47
|
+
clientId: config.clientId,
|
|
48
|
+
clientSecret: config.clientSecret,
|
|
49
|
+
tokenStore,
|
|
50
|
+
autoRefresh: false,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
info('Revoking token…');
|
|
55
|
+
await client.oauth2.revokeToken({ token: config.accessToken });
|
|
56
|
+
} catch {
|
|
57
|
+
// Revocation failure is non-fatal — we still clear local tokens
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
clearTokens(scope);
|
|
62
|
+
success('Logged out.');
|
|
63
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* zeyos resources
|
|
3
|
+
*
|
|
4
|
+
* List all resource types known to the CLI.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { listResources, resolveResource } from '../lib/resources.mjs';
|
|
8
|
+
import { colors as c, outputMode, printJson, printYaml } from '../lib/output.mjs';
|
|
9
|
+
|
|
10
|
+
export const USAGE = `\
|
|
11
|
+
Usage: zeyos resources [options]
|
|
12
|
+
|
|
13
|
+
List all resource types available for use with list/get/create/update/delete.
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
--json Output as JSON
|
|
17
|
+
--yaml Output as YAML
|
|
18
|
+
-h, --help Show this help
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
export function run(values) {
|
|
22
|
+
const resources = listResources().map((name) => {
|
|
23
|
+
const res = resolveResource(name);
|
|
24
|
+
const operations = ['list', 'get', 'create', 'update', 'delete']
|
|
25
|
+
.filter(op => res[op]);
|
|
26
|
+
|
|
27
|
+
return { name, operations };
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const mode = outputMode(values);
|
|
31
|
+
if (mode === 'json') {
|
|
32
|
+
printJson(resources);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (mode === 'yaml') {
|
|
36
|
+
printYaml(resources);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
process.stdout.write('\n');
|
|
41
|
+
process.stdout.write(` ${c.bold('RESOURCE')} ${c.bold('OPERATIONS')}\n`);
|
|
42
|
+
process.stdout.write(` ${'─'.repeat(16)} ${'─'.repeat(32)}\n`);
|
|
43
|
+
|
|
44
|
+
for (const resource of resources) {
|
|
45
|
+
process.stdout.write(` ${resource.name.padEnd(16)} ${c.dim(resource.operations.join(', '))}\n`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
process.stdout.write('\n');
|
|
49
|
+
}
|