@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.
@@ -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
+ }