@zeyos/cli 0.1.1 → 0.3.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 +24 -1
- package/bin/zeyos.mjs +139 -27
- package/commands/count.mjs +15 -7
- package/commands/create.mjs +12 -5
- package/commands/delete.mjs +8 -3
- package/commands/describe.mjs +22 -1
- package/commands/doctor.mjs +186 -0
- package/commands/get.mjs +17 -4
- package/commands/list.mjs +34 -11
- package/commands/login.mjs +33 -8
- package/commands/logout.mjs +12 -7
- package/commands/profile.mjs +211 -0
- package/commands/skills.mjs +3 -2
- package/commands/update.mjs +11 -4
- package/commands/whoami.mjs +9 -2
- package/config/actionstep.json +21 -0
- package/config/customfield.json +17 -0
- package/config/message.json +20 -0
- package/config/task.json +4 -2
- package/config/ticket.json +2 -1
- package/lib/client.mjs +19 -10
- package/lib/command.mjs +79 -4
- package/lib/config.mjs +280 -45
- package/lib/flags.mjs +3 -3
- package/lib/output.mjs +194 -4
- package/lib/resources.mjs +26 -3
- package/package.json +2 -2
package/commands/get.mjs
CHANGED
|
@@ -17,11 +17,12 @@
|
|
|
17
17
|
import { loadConfig } from '../lib/config.mjs';
|
|
18
18
|
import { canonicalName } from '../lib/resources.mjs';
|
|
19
19
|
import { getGetFields, getGetParams } from '../lib/resource-config.mjs';
|
|
20
|
-
import { outputMode, printJson, printYaml, printRecord, buildDateFormatters } from '../lib/output.mjs';
|
|
20
|
+
import { outputMode, printJson, printYaml, printRecord, buildDateFormatters, buildEnumFormatters } from '../lib/output.mjs';
|
|
21
21
|
import {
|
|
22
22
|
buildCliClient,
|
|
23
23
|
callApi,
|
|
24
24
|
fail,
|
|
25
|
+
maybeDryRun,
|
|
25
26
|
requireRecordId,
|
|
26
27
|
requireResource
|
|
27
28
|
} from '../lib/command.mjs';
|
|
@@ -44,6 +45,7 @@ Options:
|
|
|
44
45
|
--all Fetch all data (extdata + tags + all fields)
|
|
45
46
|
--json Output as JSON
|
|
46
47
|
--yaml Output as YAML
|
|
48
|
+
--query Print the request route + JSON body without sending it
|
|
47
49
|
-h, --help Show this help
|
|
48
50
|
|
|
49
51
|
Fields format:
|
|
@@ -66,7 +68,7 @@ export async function run(values, positional) {
|
|
|
66
68
|
requireRecordId(id, 'zeyos get <resource> <id>');
|
|
67
69
|
|
|
68
70
|
const resName = canonicalName(resourceName);
|
|
69
|
-
const clientState = buildCliClient();
|
|
71
|
+
const clientState = buildCliClient(values);
|
|
70
72
|
|
|
71
73
|
// ── Build params ───────────────────────────────────────────────────────────
|
|
72
74
|
// GET endpoints use query parameters like ?extdata=1&tags=1 to include
|
|
@@ -98,6 +100,8 @@ export async function run(values, positional) {
|
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
// ── Call API ───────────────────────────────────────────────────────────────
|
|
103
|
+
if (await maybeDryRun(clientState, res.get, params, values)) return;
|
|
104
|
+
|
|
101
105
|
const record = await callApi(clientState, res.get, params, {
|
|
102
106
|
notFoundMessage: `${resourceName} #${id} not found.`
|
|
103
107
|
});
|
|
@@ -121,7 +125,16 @@ export async function run(values, positional) {
|
|
|
121
125
|
const cfg = loadConfig();
|
|
122
126
|
const dateFormat = cfg.dateFormat ?? 'YYYY-MM-DD';
|
|
123
127
|
const displayKeys = fields ?? Object.keys(record);
|
|
124
|
-
const
|
|
125
|
-
|
|
128
|
+
const dateFormatters = buildDateFormatters(displayKeys, dateFormat);
|
|
129
|
+
|
|
130
|
+
// QW-3: schema-driven enum/ID coloring in the single-record view too.
|
|
131
|
+
// Enum values are colored by their resolved label keyword; ID/FK fields are
|
|
132
|
+
// dimmed. Date formatters win for date columns. No-op when color is off.
|
|
133
|
+
const schema = clientState.client.schema;
|
|
134
|
+
const schemaKey = schema?.resourceForOperation?.(res.get);
|
|
135
|
+
const fieldDefs = schemaKey ? schema.describe(schemaKey)?.fields : undefined;
|
|
136
|
+
const enumFormatters = fieldDefs ? buildEnumFormatters(displayKeys, fieldDefs) : {};
|
|
137
|
+
|
|
138
|
+
printRecord(record, displayKeys, fieldLabels, { ...enumFormatters, ...dateFormatters });
|
|
126
139
|
}
|
|
127
140
|
}
|
package/commands/list.mjs
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Options:
|
|
7
7
|
* --fields <list> Field selection (comma-separated or JSON object)
|
|
8
8
|
* --filter <json> JSON filter object e.g. '{"status":1}'
|
|
9
|
+
* --filter-file <path> Read JSON filter object from a file
|
|
9
10
|
* --sort <field> Sort field, prefix with - for descending e.g. '-lastmodified'
|
|
10
11
|
* --limit <n> Max records to fetch (default: 50)
|
|
11
12
|
* --offset <n> Skip first N records (default: 0)
|
|
@@ -19,12 +20,13 @@ import { normalizeListResult } from '@zeyos/client';
|
|
|
19
20
|
import { loadConfig } from '../lib/config.mjs';
|
|
20
21
|
import { canonicalName } from '../lib/resources.mjs';
|
|
21
22
|
import { getListFields } from '../lib/resource-config.mjs';
|
|
22
|
-
import { outputMode, printJson, printYaml, printTable, buildDateFormatters,
|
|
23
|
+
import { outputMode, printJson, printYaml, printTable, buildDateFormatters, buildEnumFormatters, info } from '../lib/output.mjs';
|
|
23
24
|
import {
|
|
24
25
|
buildCliClient,
|
|
25
26
|
callApi,
|
|
26
27
|
fail,
|
|
27
|
-
|
|
28
|
+
maybeDryRun,
|
|
29
|
+
parseJsonOptionOrFile,
|
|
28
30
|
requireApiMethod,
|
|
29
31
|
requireResource
|
|
30
32
|
} from '../lib/command.mjs';
|
|
@@ -40,6 +42,8 @@ Arguments:
|
|
|
40
42
|
Options:
|
|
41
43
|
--fields <list> Field selection (see formats below)
|
|
42
44
|
--filter <json> JSON filter object e.g. '{"status":1}'
|
|
45
|
+
--filter-file <path>
|
|
46
|
+
Read JSON filter object from a file
|
|
43
47
|
--sort <fields> Sort expression e.g. '-lastmodified'
|
|
44
48
|
--limit <n> Max records (default: 50)
|
|
45
49
|
--offset <n> Skip first N records (default: 0)
|
|
@@ -47,6 +51,7 @@ Options:
|
|
|
47
51
|
--expand <list> Expand JSON/binary columns (e.g. binfile, items)
|
|
48
52
|
--json Output as JSON
|
|
49
53
|
--yaml Output as YAML
|
|
54
|
+
--query Print the request route + JSON body without sending it
|
|
50
55
|
-h, --help Show this help
|
|
51
56
|
|
|
52
57
|
Fields format:
|
|
@@ -57,6 +62,7 @@ Fields format:
|
|
|
57
62
|
Examples:
|
|
58
63
|
zeyos list tickets
|
|
59
64
|
zeyos list tickets --filter '{"status":1}' --sort -lastmodified
|
|
65
|
+
zeyos list tickets --filter-file ./filters/open-tickets.json
|
|
60
66
|
zeyos list tickets --fields ID,name,status --limit 10
|
|
61
67
|
zeyos list accounts --fields '{"Name": "lastname", "City": "contact.city"}'
|
|
62
68
|
zeyos list tickets --extdata
|
|
@@ -68,7 +74,6 @@ export async function run(values, positional) {
|
|
|
68
74
|
const res = requireResource(resourceName, 'zeyos list <resource>');
|
|
69
75
|
|
|
70
76
|
const resName = canonicalName(resourceName);
|
|
71
|
-
const clientState = buildCliClient();
|
|
72
77
|
|
|
73
78
|
// ── Resolve field config ──────────────────────────────────────────────────
|
|
74
79
|
const { apiFields, displayColumns } = getListFields(res, resName, values.fields);
|
|
@@ -79,8 +84,9 @@ export async function run(values, positional) {
|
|
|
79
84
|
// Pass configured fields to the API for server-side field selection
|
|
80
85
|
if (apiFields) body.fields = apiFields;
|
|
81
86
|
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
const filters = parseJsonOptionOrFile(values, 'filter', 'filter-file');
|
|
88
|
+
if (filters !== undefined) {
|
|
89
|
+
body.filters = filters;
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
if (values.sort) body.sort = values.sort.split(',').map(s => s.trim()).filter(Boolean);
|
|
@@ -110,6 +116,9 @@ export async function run(values, positional) {
|
|
|
110
116
|
}
|
|
111
117
|
|
|
112
118
|
// ── Call API ───────────────────────────────────────────────────────────────
|
|
119
|
+
const clientState = buildCliClient(values);
|
|
120
|
+
if (await maybeDryRun(clientState, res.list, body, values)) return;
|
|
121
|
+
|
|
113
122
|
const fn = requireApiMethod(clientState, res.list);
|
|
114
123
|
let records = await callApi(clientState, res.list, body);
|
|
115
124
|
|
|
@@ -125,13 +134,27 @@ export async function run(values, positional) {
|
|
|
125
134
|
} else if (mode === 'yaml') {
|
|
126
135
|
printYaml(records);
|
|
127
136
|
} else if (records.length === 0) {
|
|
128
|
-
|
|
137
|
+
// QW-7: an empty result is a neutral fact, not a warning — use the info `·`
|
|
138
|
+
// glyph rather than the `⚠` glyph (which reads as an error).
|
|
139
|
+
info(`No ${resourceName} match.`);
|
|
129
140
|
return;
|
|
130
141
|
} else {
|
|
131
142
|
const cfg = loadConfig();
|
|
132
143
|
const dateFormat = cfg.dateFormat ?? 'YYYY-MM-DD';
|
|
133
|
-
const
|
|
134
|
-
|
|
144
|
+
const dateFormatters = buildDateFormatters(displayColumns, dateFormat, apiFields);
|
|
145
|
+
|
|
146
|
+
// QW-3: schema-driven enum/ID coloring. Resolve the resource's field defs
|
|
147
|
+
// (enums, FKs) via the same schema source `describe` uses, then color enum
|
|
148
|
+
// values by label keyword and dim ID/FK columns. No-op when color is off.
|
|
149
|
+
// Date formatters win for date columns (a column is never both).
|
|
150
|
+
const schema = clientState.client.schema;
|
|
151
|
+
const schemaKey = schema?.resourceForOperation?.(res.list);
|
|
152
|
+
const fieldDefs = schemaKey ? schema.describe(schemaKey)?.fields : undefined;
|
|
153
|
+
const enumFormatters = fieldDefs
|
|
154
|
+
? buildEnumFormatters(displayColumns, fieldDefs, apiFields)
|
|
155
|
+
: {};
|
|
156
|
+
|
|
157
|
+
printTable(records, displayColumns, {}, { ...enumFormatters, ...dateFormatters });
|
|
135
158
|
}
|
|
136
159
|
|
|
137
160
|
// ── Pagination / truncation hint ──────────────────────────────────────────
|
|
@@ -149,14 +172,14 @@ export async function run(values, positional) {
|
|
|
149
172
|
const countResult = await fn(countBody);
|
|
150
173
|
const total = countResult?.count ?? null;
|
|
151
174
|
if (total !== null && total > records.length) {
|
|
152
|
-
info(
|
|
175
|
+
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
176
|
} else if (total !== null) {
|
|
154
|
-
info(
|
|
177
|
+
info(`→ Showing ${from}–${to} of ${total} (--offset ${to} for next page)`);
|
|
155
178
|
}
|
|
156
179
|
} catch {
|
|
157
180
|
// Non-critical — skip pagination info
|
|
158
181
|
}
|
|
159
182
|
} else if (offset > 0) {
|
|
160
|
-
info(
|
|
183
|
+
info(`→ Showing ${from}–${to} of ${to}`);
|
|
161
184
|
}
|
|
162
185
|
}
|
package/commands/login.mjs
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import { createInterface } from 'node:readline';
|
|
22
22
|
import { createZeyosClient, MemoryTokenStore } from '@zeyos/client';
|
|
23
|
-
import { saveConfig, loadConfig }
|
|
23
|
+
import { saveConfig, loadConfig, getProfile, upsertProfile, setActiveProfile } from '../lib/config.mjs';
|
|
24
24
|
import { waitForCallback, callbackUri, BrowserUnavailableError } from '../lib/login-server.mjs';
|
|
25
25
|
import { success, error, info, warn } from '../lib/output.mjs';
|
|
26
26
|
|
|
@@ -35,6 +35,7 @@ Options:
|
|
|
35
35
|
--base-url <url> ZeyOS platform URL (prompted if missing)
|
|
36
36
|
--client-id <id> OAuth client ID (prompted if missing)
|
|
37
37
|
--secret <secret> OAuth client secret (prompted if missing)
|
|
38
|
+
--profile <name> Store credentials/tokens in a named profile and activate it
|
|
38
39
|
--scope <scope> OAuth scope (default: all)
|
|
39
40
|
--port <port> Local callback server port (default: 9005)
|
|
40
41
|
--global Store credentials globally (~/.config/zeyos/credentials.json)
|
|
@@ -45,12 +46,21 @@ Options:
|
|
|
45
46
|
`;
|
|
46
47
|
|
|
47
48
|
export async function run(values) {
|
|
49
|
+
const profileName = values.profile || null;
|
|
48
50
|
const scope = values.global ? 'global' : 'local';
|
|
49
51
|
const port = values.port ? Number(values.port) : DEFAULT_CALLBACK_PORT;
|
|
50
52
|
const redirectUri = callbackUri(port);
|
|
51
53
|
|
|
54
|
+
// Persist either into a named profile or the legacy local/global credential file.
|
|
55
|
+
const persist = (updates) => {
|
|
56
|
+
if (profileName) upsertProfile(profileName, updates);
|
|
57
|
+
else saveConfig(updates, scope);
|
|
58
|
+
};
|
|
59
|
+
|
|
52
60
|
// ── Resolve connection params ──────────────────────────────────────────────
|
|
53
|
-
const existing = values.clean
|
|
61
|
+
const existing = values.clean
|
|
62
|
+
? {}
|
|
63
|
+
: (profileName ? (getProfile(profileName) || {}) : loadConfig());
|
|
54
64
|
if (values.clean) values.force = true;
|
|
55
65
|
|
|
56
66
|
let baseUrl = values['base-url'] ?? existing.baseUrl;
|
|
@@ -79,12 +89,17 @@ export async function run(values) {
|
|
|
79
89
|
}
|
|
80
90
|
|
|
81
91
|
// Save connection params immediately so they are available on retries
|
|
82
|
-
|
|
92
|
+
persist({ baseUrl, clientId, clientSecret });
|
|
83
93
|
|
|
84
94
|
// ── Check if already authenticated ────────────────────────────────────────
|
|
85
95
|
if (existing.accessToken && !values.force) {
|
|
86
|
-
|
|
87
|
-
|
|
96
|
+
if (_tokenExpired(existing)) {
|
|
97
|
+
info('Stored access token has expired — re-authenticating…');
|
|
98
|
+
} else {
|
|
99
|
+
const where = profileName ? `profile "${profileName}"` : 'this scope';
|
|
100
|
+
warn(`Already logged in (${where}). Use --force to re-authenticate.`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
88
103
|
}
|
|
89
104
|
|
|
90
105
|
// ── Build a temporary client (no token yet) ────────────────────────────────
|
|
@@ -138,7 +153,7 @@ export async function run(values) {
|
|
|
138
153
|
info('Exchanging authorization code for tokens…');
|
|
139
154
|
const tokenSet = await client.oauth2.exchangeAuthorizationCode({ code, redirectUri });
|
|
140
155
|
|
|
141
|
-
|
|
156
|
+
persist({
|
|
142
157
|
baseUrl,
|
|
143
158
|
clientId,
|
|
144
159
|
clientSecret,
|
|
@@ -146,9 +161,12 @@ export async function run(values) {
|
|
|
146
161
|
refreshToken: tokenSet.refreshToken ?? undefined,
|
|
147
162
|
expiresAt: tokenSet.expiresAt ?? undefined,
|
|
148
163
|
refreshTokenExpiresAt: tokenSet.refreshTokenExpiresAt ?? undefined,
|
|
149
|
-
}
|
|
164
|
+
});
|
|
150
165
|
|
|
151
|
-
|
|
166
|
+
if (profileName) setActiveProfile(profileName);
|
|
167
|
+
success(profileName
|
|
168
|
+
? `Logged in — profile "${profileName}" is now active.`
|
|
169
|
+
: 'Logged in successfully.');
|
|
152
170
|
} catch (err) {
|
|
153
171
|
error(`Token exchange failed: ${err.message}`);
|
|
154
172
|
process.exit(1);
|
|
@@ -157,6 +175,13 @@ export async function run(values) {
|
|
|
157
175
|
|
|
158
176
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
159
177
|
|
|
178
|
+
/** True when stored creds carry an access token whose expiry is in the past. */
|
|
179
|
+
function _tokenExpired(creds) {
|
|
180
|
+
if (!creds?.accessToken || creds.expiresAt == null) return false;
|
|
181
|
+
const exp = Number(creds.expiresAt) > 2e10 ? Number(creds.expiresAt) / 1000 : Number(creds.expiresAt);
|
|
182
|
+
return Number.isFinite(exp) && exp < Math.floor(Date.now() / 1000);
|
|
183
|
+
}
|
|
184
|
+
|
|
160
185
|
/**
|
|
161
186
|
* Try the browser + callback server flow.
|
|
162
187
|
* On any failure, fall back to prompting the user for the code.
|
package/commands/logout.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { createZeyosClient, MemoryTokenStore } from '@zeyos/client';
|
|
13
|
-
import {
|
|
13
|
+
import { loadConfigWithSource, clearTokens, clearTokensForSource } from '../lib/config.mjs';
|
|
14
14
|
import { success, warn, info } from '../lib/output.mjs';
|
|
15
15
|
|
|
16
16
|
export const USAGE = `\
|
|
@@ -19,13 +19,13 @@ Usage: zeyos logout [options]
|
|
|
19
19
|
Revoke the current session and clear stored tokens.
|
|
20
20
|
|
|
21
21
|
Options:
|
|
22
|
-
--
|
|
23
|
-
|
|
22
|
+
--profile <name> Log out of a specific profile
|
|
23
|
+
--global Target the legacy global credentials file
|
|
24
|
+
-h, --help Show this help
|
|
24
25
|
`;
|
|
25
26
|
|
|
26
27
|
export async function run(values) {
|
|
27
|
-
const
|
|
28
|
-
const config = loadConfig();
|
|
28
|
+
const { config, source } = loadConfigWithSource({ profile: values.profile });
|
|
29
29
|
|
|
30
30
|
if (!config.accessToken) {
|
|
31
31
|
warn('Not currently logged in.');
|
|
@@ -58,6 +58,11 @@ export async function run(values) {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
if (values.global) {
|
|
62
|
+
clearTokens('global');
|
|
63
|
+
} else {
|
|
64
|
+
clearTokensForSource(source);
|
|
65
|
+
}
|
|
66
|
+
const where = source?.kind === 'profile' ? `profile "${source.name}"` : (values.global ? 'global credentials' : 'local credentials');
|
|
67
|
+
success(`Logged out (${where}).`);
|
|
63
68
|
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* zeyos profile <list|current|use|add|remove>
|
|
3
|
+
*
|
|
4
|
+
* Manage named credential profiles (e.g. dev, prod, client-x) and switch between
|
|
5
|
+
* ZeyOS instances without re-running login each time.
|
|
6
|
+
*
|
|
7
|
+
* list Show all profiles (active marked with *)
|
|
8
|
+
* current Show the profile that resolves right now, and why
|
|
9
|
+
* use <name> [--local] Make <name> active globally, or pin it to this project
|
|
10
|
+
* add <name> [opts] Create/update a profile's connection params
|
|
11
|
+
* remove <name> Delete a profile
|
|
12
|
+
*
|
|
13
|
+
* `add` options: --base-url, --client-id, --secret, or --from-current to snapshot
|
|
14
|
+
* the credentials currently in effect (including tokens) into the new profile.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
listProfiles, getProfile, upsertProfile, removeProfile,
|
|
19
|
+
setActiveProfile, writeLocalPin, readLocalPin,
|
|
20
|
+
resolveProfileSelection, loadConfigWithSource, profilesConfigPath
|
|
21
|
+
} from '../lib/config.mjs';
|
|
22
|
+
import { outputMode, printJson, printYaml, printTable, success, error, info, warn } from '../lib/output.mjs';
|
|
23
|
+
|
|
24
|
+
export const USAGE = `\
|
|
25
|
+
Usage: zeyos profile <command> [options]
|
|
26
|
+
|
|
27
|
+
Manage named credential profiles and switch between ZeyOS instances.
|
|
28
|
+
|
|
29
|
+
Commands:
|
|
30
|
+
list List all profiles (active marked with *)
|
|
31
|
+
current Show which profile is in effect, and why
|
|
32
|
+
use <name> Make <name> the active profile (global)
|
|
33
|
+
use <name> --local Pin <name> to the current project (.zeyos/profile)
|
|
34
|
+
add <name> [options] Create or update a profile
|
|
35
|
+
remove <name> Delete a profile
|
|
36
|
+
|
|
37
|
+
Add options:
|
|
38
|
+
--base-url <url> ZeyOS platform URL for the profile
|
|
39
|
+
--client-id <id> OAuth client ID
|
|
40
|
+
--secret <secret> OAuth client secret
|
|
41
|
+
--from-current Snapshot the credentials currently in effect (incl. tokens)
|
|
42
|
+
|
|
43
|
+
Global options:
|
|
44
|
+
--json | --yaml Machine-readable output (list / current)
|
|
45
|
+
-h, --help Show this help
|
|
46
|
+
|
|
47
|
+
Examples:
|
|
48
|
+
zeyos profile add dev --base-url https://zeyos.cms-it.de/dev
|
|
49
|
+
zeyos profile add prod --from-current
|
|
50
|
+
zeyos profile use prod
|
|
51
|
+
zeyos profile use dev --local # only inside this project
|
|
52
|
+
zeyos whoami --profile dev # one-off override on any command
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
export async function run(values, positional) {
|
|
56
|
+
const sub = positional[0] || 'list';
|
|
57
|
+
switch (sub) {
|
|
58
|
+
case 'list': return cmdList(values);
|
|
59
|
+
case 'current': return cmdCurrent(values);
|
|
60
|
+
case 'use': return cmdUse(values, positional[1]);
|
|
61
|
+
case 'add': return cmdAdd(values, positional[1]);
|
|
62
|
+
case 'remove':
|
|
63
|
+
case 'rm':
|
|
64
|
+
case 'delete': return cmdRemove(values, positional[1]);
|
|
65
|
+
default:
|
|
66
|
+
error(`Unknown profile command: "${sub}".`);
|
|
67
|
+
process.stderr.write(`\n${USAGE}`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── list ───────────────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
function cmdList(values) {
|
|
75
|
+
const { active, profiles } = listProfiles();
|
|
76
|
+
const names = Object.keys(profiles);
|
|
77
|
+
const mode = outputMode(values);
|
|
78
|
+
|
|
79
|
+
if (mode === 'json') return printJson({ active, profiles });
|
|
80
|
+
if (mode === 'yaml') return printYaml({ active, profiles });
|
|
81
|
+
|
|
82
|
+
if (names.length === 0) {
|
|
83
|
+
info('No profiles defined yet.');
|
|
84
|
+
console.error(`Create one with: zeyos profile add <name> --base-url <url>`);
|
|
85
|
+
console.error(` or: zeyos login --profile <name>`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const rows = names.map((name) => ({
|
|
90
|
+
name: `${name === active ? '*' : ' '} ${name}`,
|
|
91
|
+
baseUrl: profiles[name].baseUrl || '(no URL)',
|
|
92
|
+
token: tokenStatus(profiles[name])
|
|
93
|
+
}));
|
|
94
|
+
printTable(rows, ['name', 'baseUrl', 'token'], { name: 'PROFILE', baseUrl: 'BASE URL', token: 'TOKEN' });
|
|
95
|
+
console.error(`\nProfiles file: ${profilesConfigPath()}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── current ────────────────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
function cmdCurrent(values) {
|
|
101
|
+
const selection = resolveProfileSelection({ profileFlag: values.profile });
|
|
102
|
+
const loaded = loadConfigWithSource({ profile: values.profile });
|
|
103
|
+
const mode = outputMode(values);
|
|
104
|
+
|
|
105
|
+
const out = {
|
|
106
|
+
profile: selection.name || null,
|
|
107
|
+
origin: selection.origin || null,
|
|
108
|
+
source: loaded.source,
|
|
109
|
+
baseUrl: loaded.config.baseUrl || null,
|
|
110
|
+
token: tokenStatus(loaded.config)
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
if (mode === 'json') return printJson(out);
|
|
114
|
+
if (mode === 'yaml') return printYaml(out);
|
|
115
|
+
|
|
116
|
+
if (selection.name) {
|
|
117
|
+
if (selection.missing) {
|
|
118
|
+
warn(`Selected profile "${selection.name}" (via ${selection.origin}) does not exist.`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
success(`Active profile: ${selection.name} (selected via ${selection.origin})`);
|
|
122
|
+
} else if (loaded.source) {
|
|
123
|
+
info(`No named profile in effect — using legacy ${loaded.source.kind} credentials.`);
|
|
124
|
+
} else {
|
|
125
|
+
warn('No credentials configured. Run `zeyos login` or `zeyos profile add`.');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
console.error(` base URL: ${out.baseUrl || '(none)'}`);
|
|
129
|
+
console.error(` token: ${out.token}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── use ────────────────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
function cmdUse(values, name) {
|
|
135
|
+
if (!name) fail('Usage: zeyos profile use <name> [--local]');
|
|
136
|
+
if (!getProfile(name)) failUnknown(name);
|
|
137
|
+
|
|
138
|
+
if (values.local) {
|
|
139
|
+
const path = writeLocalPin(name);
|
|
140
|
+
success(`Pinned profile "${name}" to this project.`);
|
|
141
|
+
console.error(` ${path}`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
setActiveProfile(name);
|
|
145
|
+
success(`Active profile: ${name}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── add ────────────────────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
function cmdAdd(values, name) {
|
|
151
|
+
if (!name) fail('Usage: zeyos profile add <name> [--base-url <url>] [--client-id <id>] [--secret <secret>] | --from-current');
|
|
152
|
+
|
|
153
|
+
let updates = {};
|
|
154
|
+
if (values['from-current']) {
|
|
155
|
+
const cfg = loadConfigWithSource().config; // whatever is in effect right now
|
|
156
|
+
for (const k of ['baseUrl', 'instance', 'clientId', 'clientSecret', 'accessToken', 'refreshToken', 'expiresAt', 'refreshTokenExpiresAt']) {
|
|
157
|
+
if (cfg[k] != null) updates[k] = cfg[k];
|
|
158
|
+
}
|
|
159
|
+
if (!updates.baseUrl) fail('Nothing to snapshot: no credentials are currently in effect.');
|
|
160
|
+
} else {
|
|
161
|
+
if (values['base-url']) updates.baseUrl = values['base-url'];
|
|
162
|
+
if (values['client-id']) updates.clientId = values['client-id'];
|
|
163
|
+
if (values.secret) updates.clientSecret = values.secret;
|
|
164
|
+
if (Object.keys(updates).length === 0) {
|
|
165
|
+
fail('Provide at least --base-url (and ideally --client-id/--secret), or use --from-current.');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const existed = Boolean(getProfile(name));
|
|
170
|
+
upsertProfile(name, updates);
|
|
171
|
+
success(`${existed ? 'Updated' : 'Created'} profile "${name}".`);
|
|
172
|
+
if (!updates.accessToken) {
|
|
173
|
+
info(`Finish authenticating with: zeyos login --profile ${name}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── remove ─────────────────────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
function cmdRemove(values, name) {
|
|
180
|
+
if (!name) fail('Usage: zeyos profile remove <name>');
|
|
181
|
+
const removed = removeProfile(name);
|
|
182
|
+
if (!removed) failUnknown(name);
|
|
183
|
+
success(`Removed profile "${name}".`);
|
|
184
|
+
const pin = readLocalPin();
|
|
185
|
+
if (pin && pin.name === name) {
|
|
186
|
+
warn(`This project still pins "${name}" (${pin.path}); that pin will no longer resolve.`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── helpers ────────────────────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
/** Human-readable token status from stored creds. Handles seconds or ms expiry. */
|
|
193
|
+
function tokenStatus(creds = {}) {
|
|
194
|
+
if (!creds.accessToken) return 'none';
|
|
195
|
+
const exp = creds.expiresAt;
|
|
196
|
+
if (exp == null) return 'present';
|
|
197
|
+
const expSec = Number(exp) > 2e10 ? Number(exp) / 1000 : Number(exp);
|
|
198
|
+
const now = Math.floor(Date.now() / 1000);
|
|
199
|
+
return expSec < now ? 'expired' : 'ok';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function failUnknown(name) {
|
|
203
|
+
const names = Object.keys(listProfiles().profiles);
|
|
204
|
+
const known = names.length ? `Known profiles: ${names.join(', ')}.` : 'No profiles defined yet.';
|
|
205
|
+
fail(`No such profile: "${name}". ${known}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function fail(message) {
|
|
209
|
+
error(message);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
package/commands/skills.mjs
CHANGED
|
@@ -177,7 +177,7 @@ async function resolveTarget(values) {
|
|
|
177
177
|
} else if (interactive) {
|
|
178
178
|
agent = await promptAgent();
|
|
179
179
|
} else {
|
|
180
|
-
agent = detectAgent() || AGENTS
|
|
180
|
+
agent = detectAgent() || AGENTS.find((a) => a.key === 'agents');
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
// (b) Install globally or just for this project?
|
|
@@ -225,7 +225,8 @@ function promptMenu(question, items, defaultIndex = 0) {
|
|
|
225
225
|
|
|
226
226
|
async function promptAgent() {
|
|
227
227
|
const detected = detectAgent();
|
|
228
|
-
const
|
|
228
|
+
const agentsIdx = AGENTS.findIndex((a) => a.key === 'agents');
|
|
229
|
+
const defaultIndex = detected ? AGENTS.indexOf(detected) : agentsIdx;
|
|
229
230
|
const items = AGENTS.map((a) => ({
|
|
230
231
|
label: a.label,
|
|
231
232
|
hint: a === detected ? `${a.local} (detected here)` : a.local,
|
package/commands/update.mjs
CHANGED
|
@@ -4,15 +4,17 @@
|
|
|
4
4
|
* Update an existing record. Works like `create` but requires an ID.
|
|
5
5
|
*
|
|
6
6
|
* Options:
|
|
7
|
-
* --data <json>
|
|
8
|
-
* --
|
|
9
|
-
* --
|
|
7
|
+
* --data <json> Fields to update as a JSON object
|
|
8
|
+
* --data-file <path> Read fields to update as a JSON object from a file
|
|
9
|
+
* --json Output updated record as JSON
|
|
10
|
+
* --yaml Output updated record as YAML
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import {
|
|
13
14
|
buildCliClient,
|
|
14
15
|
buildRecordPayload,
|
|
15
16
|
callApi,
|
|
17
|
+
maybeDryRun,
|
|
16
18
|
requireRecordId,
|
|
17
19
|
requireResource
|
|
18
20
|
} from '../lib/command.mjs';
|
|
@@ -29,14 +31,17 @@ Arguments:
|
|
|
29
31
|
|
|
30
32
|
Options:
|
|
31
33
|
--data <json> Fields to update as a JSON object
|
|
34
|
+
--data-file <path> Read fields to update as a JSON object from a file
|
|
32
35
|
--<field> <value> Set individual fields e.g. --status 2
|
|
33
36
|
--json Output updated record as JSON
|
|
34
37
|
--yaml Output updated record as YAML
|
|
38
|
+
--query Print the request route + JSON body without sending it
|
|
35
39
|
-h, --help Show this help
|
|
36
40
|
|
|
37
41
|
Examples:
|
|
38
42
|
zeyos update ticket 42 --status 3
|
|
39
43
|
zeyos update account 7 --data '{"email":"new@example.com"}'
|
|
44
|
+
zeyos update ticket 42 --data-file ./ticket-update.json
|
|
40
45
|
`;
|
|
41
46
|
|
|
42
47
|
export async function run(values, positional) {
|
|
@@ -51,9 +56,11 @@ export async function run(values, positional) {
|
|
|
51
56
|
// (optional) JSON body some callers pass positionally instead of via --data.
|
|
52
57
|
const data = buildRecordPayload(values, positional[2]);
|
|
53
58
|
|
|
54
|
-
const clientState = buildCliClient();
|
|
59
|
+
const clientState = buildCliClient(values);
|
|
55
60
|
|
|
56
61
|
// ── Call API ───────────────────────────────────────────────────────────────
|
|
62
|
+
if (await maybeDryRun(clientState, res.update, { ID: id, body: data }, values)) return;
|
|
63
|
+
|
|
57
64
|
const record = await callApi(clientState, res.update, { ID: id, body: data }, {
|
|
58
65
|
notFoundMessage: `${resourceName} #${id} not found.`
|
|
59
66
|
});
|
package/commands/whoami.mjs
CHANGED
|
@@ -27,7 +27,7 @@ Options:
|
|
|
27
27
|
export async function run(values) {
|
|
28
28
|
let client, config, tokenStore, configSource;
|
|
29
29
|
try {
|
|
30
|
-
({ client, config, tokenStore, configSource } = buildClient());
|
|
30
|
+
({ client, config, tokenStore, configSource } = buildClient({}, { profile: values.profile }));
|
|
31
31
|
} catch (err) {
|
|
32
32
|
error(err.message);
|
|
33
33
|
process.exit(1);
|
|
@@ -38,7 +38,14 @@ export async function run(values) {
|
|
|
38
38
|
userInfo = await client.oauth2.getUserInfo();
|
|
39
39
|
await syncTokens(tokenStore, configSource);
|
|
40
40
|
} catch (err) {
|
|
41
|
-
|
|
41
|
+
const status = err?.status;
|
|
42
|
+
if (status === 502 || status === 503 || status === 504) {
|
|
43
|
+
error(`ZeyOS instance is temporarily unavailable (HTTP ${status}). The server at ${config.baseUrl} may be down or restarting — this is server-side, not your credentials.`);
|
|
44
|
+
} else if (status === 401) {
|
|
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
|
+
}
|
|
42
49
|
process.exit(1);
|
|
43
50
|
}
|
|
44
51
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"list": {
|
|
3
|
+
"fields": {
|
|
4
|
+
"ID": "ID",
|
|
5
|
+
"Num": "actionnum",
|
|
6
|
+
"Name": "name",
|
|
7
|
+
"Status": "status",
|
|
8
|
+
"Date": "date",
|
|
9
|
+
"Due": "duedate",
|
|
10
|
+
"Effort": "effort",
|
|
11
|
+
"Ticket": "ticket",
|
|
12
|
+
"Task": "task",
|
|
13
|
+
"Account": "account",
|
|
14
|
+
"Transaction": "transaction"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"get": {
|
|
18
|
+
"fields": ["ID", "actionnum", "name", "description", "status", "date", "duedate", "effort", "ticket", "task", "account", "transaction", "assigneduser", "creator", "creationdate", "lastmodified"],
|
|
19
|
+
"params": { "extdata": 1 }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"list": {
|
|
3
|
+
"fields": {
|
|
4
|
+
"ID": "ID",
|
|
5
|
+
"Name": "name",
|
|
6
|
+
"Identifier": "identifier",
|
|
7
|
+
"Context": "context",
|
|
8
|
+
"Reference": "reference",
|
|
9
|
+
"Type": "type",
|
|
10
|
+
"Entity": "entity",
|
|
11
|
+
"Activity": "activity"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"get": {
|
|
15
|
+
"fields": ["ID", "name", "identifier", "context", "source", "reference", "indexed", "type", "pattern", "entity", "options", "langaliases", "activity"]
|
|
16
|
+
}
|
|
17
|
+
}
|