@zeyos/cli 0.2.0 → 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 +15 -1
- package/bin/zeyos.mjs +11 -1
- package/commands/count.mjs +1 -1
- package/commands/create.mjs +1 -1
- package/commands/delete.mjs +1 -1
- package/commands/get.mjs +1 -1
- package/commands/list.mjs +1 -1
- package/commands/login.mjs +33 -8
- package/commands/logout.mjs +12 -7
- package/commands/profile.mjs +211 -0
- package/commands/update.mjs +1 -1
- 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 +2 -2
- package/lib/config.mjs +280 -45
- package/lib/flags.mjs +1 -1
- package/lib/resources.mjs +26 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -72,6 +72,20 @@ For larger or reusable filters, put the JSON in a file:
|
|
|
72
72
|
zeyos list tickets --filter-file ./filters/open-tickets.json --json
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
+
Inspect dynamic schema definitions:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
zeyos count customfields --json
|
|
79
|
+
zeyos list customfields --fields ID,name,identifier,context,type --json
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Inspect actionsteps/time-entry evidence and ticket mail:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
zeyos list actionsteps --fields ID,name,status,date,duedate,effort,ticket,account --json
|
|
86
|
+
zeyos list messages --fields ID,date,mailbox,subject,sender_email,to_email,ticket,reference --filter '{"ticket":42}' --json
|
|
87
|
+
```
|
|
88
|
+
|
|
75
89
|
Create, update, and delete:
|
|
76
90
|
|
|
77
91
|
```bash
|
|
@@ -91,6 +105,6 @@ zeyos delete ticket 42
|
|
|
91
105
|
|
|
92
106
|
## Coverage Boundary
|
|
93
107
|
|
|
94
|
-
The CLI intentionally covers a curated registry instead of the full API surface.
|
|
108
|
+
The CLI intentionally covers a curated registry instead of the full API surface. It includes operational resources such as tickets, tasks, messages, and actionsteps; use `zeyos resources` to see the supported set.
|
|
95
109
|
|
|
96
110
|
When you need unsupported resources or low-level request control, switch to [`@zeyos/client`](../docs/02-javascript-client/01-getting-started.md) and follow the escalation guidance in [CLI Coverage and Escalation](../docs/04-agent-workflows/03-cli-coverage-and-escalation.md).
|
package/bin/zeyos.mjs
CHANGED
|
@@ -51,11 +51,13 @@ ${_c.bold('Commands:')}
|
|
|
51
51
|
${_c.cyan('describe')} <resource> Show a resource's fields, types and enums
|
|
52
52
|
${_c.cyan('doctor')} agent Check local CLI readiness for coding agents
|
|
53
53
|
${_c.cyan('skills')} <command> List / show / install ZeyOS agent skills
|
|
54
|
+
${_c.cyan('profile')} <command> Manage credential profiles / switch instances
|
|
54
55
|
|
|
55
56
|
${_c.bold('Global options:')}
|
|
56
57
|
--json Output as JSON
|
|
57
58
|
--yaml Output as YAML
|
|
58
59
|
--query Print the API route + JSON payload without sending it
|
|
60
|
+
--profile <name> Use a named credential profile for this command
|
|
59
61
|
--no-color Disable ANSI colors
|
|
60
62
|
-h, --help Show help for a command
|
|
61
63
|
-v, --version Print the CLI version and exit
|
|
@@ -82,6 +84,7 @@ const OPTIONS = {
|
|
|
82
84
|
'yaml': { type: 'boolean' },
|
|
83
85
|
'no-color': { type: 'boolean' },
|
|
84
86
|
'query': { type: 'boolean' },
|
|
87
|
+
'profile': { type: 'string' },
|
|
85
88
|
// login
|
|
86
89
|
'base-url': { type: 'string' },
|
|
87
90
|
'client-id': { type: 'string' },
|
|
@@ -117,6 +120,8 @@ const OPTIONS = {
|
|
|
117
120
|
'target': { type: 'string' },
|
|
118
121
|
'dir': { type: 'string' },
|
|
119
122
|
'no-logo': { type: 'boolean' },
|
|
123
|
+
// profile
|
|
124
|
+
'from-current': { type: 'boolean' },
|
|
120
125
|
};
|
|
121
126
|
|
|
122
127
|
// ── Command registry ──────────────────────────────────────────────────────────
|
|
@@ -142,6 +147,8 @@ const COMMANDS = {
|
|
|
142
147
|
doctor: '../commands/doctor.mjs',
|
|
143
148
|
skills: '../commands/skills.mjs',
|
|
144
149
|
skill: '../commands/skills.mjs',
|
|
150
|
+
profile: '../commands/profile.mjs',
|
|
151
|
+
profiles: '../commands/profile.mjs',
|
|
145
152
|
};
|
|
146
153
|
|
|
147
154
|
// ── Per-command flag allow-lists ────────────────────────────────────────────────
|
|
@@ -149,8 +156,9 @@ const COMMANDS = {
|
|
|
149
156
|
// immediately instead of being silently ignored. `create`/`update` are the
|
|
150
157
|
// exception: they accept arbitrary `--<field>` flags, marked with `null` below.
|
|
151
158
|
|
|
152
|
-
const ALWAYS_FLAGS = ['help', 'json', 'yaml', 'no-color'];
|
|
159
|
+
const ALWAYS_FLAGS = ['help', 'json', 'yaml', 'no-color', 'profile'];
|
|
153
160
|
const SKILLS_FLAGS = ['target', 'dir', 'global', 'local', 'force', 'yes', 'no-logo'];
|
|
161
|
+
const PROFILE_FLAGS = ['base-url', 'client-id', 'secret', 'local', 'from-current'];
|
|
154
162
|
const DELETE_FLAGS = ['force', 'query'];
|
|
155
163
|
const GET_FLAGS = ['fields', 'extdata', 'tags', 'expand', 'all', 'query'];
|
|
156
164
|
|
|
@@ -174,6 +182,8 @@ const COMMAND_FLAGS = {
|
|
|
174
182
|
doctor: [],
|
|
175
183
|
skills: SKILLS_FLAGS,
|
|
176
184
|
skill: SKILLS_FLAGS,
|
|
185
|
+
profile: PROFILE_FLAGS,
|
|
186
|
+
profiles: PROFILE_FLAGS,
|
|
177
187
|
};
|
|
178
188
|
|
|
179
189
|
// ── Main ──────────────────────────────────────────────────────────────────────
|
package/commands/count.mjs
CHANGED
|
@@ -51,7 +51,7 @@ export async function run(values, positional) {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
// ── Call API ───────────────────────────────────────────────────────────────
|
|
54
|
-
const clientState = buildCliClient();
|
|
54
|
+
const clientState = buildCliClient(values);
|
|
55
55
|
if (await maybeDryRun(clientState, res.list, body, values)) return;
|
|
56
56
|
|
|
57
57
|
const result = await callApi(clientState, res.list, body);
|
package/commands/create.mjs
CHANGED
|
@@ -48,7 +48,7 @@ export async function run(values, positional) {
|
|
|
48
48
|
// (optional) JSON body some callers pass positionally instead of via --data.
|
|
49
49
|
const data = buildRecordPayload(values, positional[1]);
|
|
50
50
|
|
|
51
|
-
const clientState = buildCliClient();
|
|
51
|
+
const clientState = buildCliClient(values);
|
|
52
52
|
|
|
53
53
|
// ── Call API ───────────────────────────────────────────────────────────────
|
|
54
54
|
if (await maybeDryRun(clientState, res.create, data, values)) return;
|
package/commands/delete.mjs
CHANGED
|
@@ -37,7 +37,7 @@ export async function run(values, positional) {
|
|
|
37
37
|
const res = requireResource(resourceName, 'zeyos delete <resource> <id>', 'delete', 'deletion');
|
|
38
38
|
requireRecordId(id, 'zeyos delete <resource> <id>');
|
|
39
39
|
|
|
40
|
-
const clientState = buildCliClient();
|
|
40
|
+
const clientState = buildCliClient(values);
|
|
41
41
|
|
|
42
42
|
// ── Dry run ────────────────────────────────────────────────────────────────
|
|
43
43
|
// Show the request without prompting or deleting anything.
|
package/commands/get.mjs
CHANGED
|
@@ -68,7 +68,7 @@ export async function run(values, positional) {
|
|
|
68
68
|
requireRecordId(id, 'zeyos get <resource> <id>');
|
|
69
69
|
|
|
70
70
|
const resName = canonicalName(resourceName);
|
|
71
|
-
const clientState = buildCliClient();
|
|
71
|
+
const clientState = buildCliClient(values);
|
|
72
72
|
|
|
73
73
|
// ── Build params ───────────────────────────────────────────────────────────
|
|
74
74
|
// GET endpoints use query parameters like ?extdata=1&tags=1 to include
|
package/commands/list.mjs
CHANGED
|
@@ -116,7 +116,7 @@ export async function run(values, positional) {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
// ── Call API ───────────────────────────────────────────────────────────────
|
|
119
|
-
const clientState = buildCliClient();
|
|
119
|
+
const clientState = buildCliClient(values);
|
|
120
120
|
if (await maybeDryRun(clientState, res.list, body, values)) return;
|
|
121
121
|
|
|
122
122
|
const fn = requireApiMethod(clientState, res.list);
|
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/update.mjs
CHANGED
|
@@ -56,7 +56,7 @@ export async function run(values, positional) {
|
|
|
56
56
|
// (optional) JSON body some callers pass positionally instead of via --data.
|
|
57
57
|
const data = buildRecordPayload(values, positional[2]);
|
|
58
58
|
|
|
59
|
-
const clientState = buildCliClient();
|
|
59
|
+
const clientState = buildCliClient(values);
|
|
60
60
|
|
|
61
61
|
// ── Call API ───────────────────────────────────────────────────────────────
|
|
62
62
|
if (await maybeDryRun(clientState, res.update, { ID: id, body: data }, values)) return;
|
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
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"list": {
|
|
3
|
+
"fields": {
|
|
4
|
+
"ID": "ID",
|
|
5
|
+
"Date": "date",
|
|
6
|
+
"Mailbox": "mailbox",
|
|
7
|
+
"Subject": "subject",
|
|
8
|
+
"From": "sender_email",
|
|
9
|
+
"To": "to_email",
|
|
10
|
+
"Ticket": "ticket",
|
|
11
|
+
"Opportunity": "opportunity",
|
|
12
|
+
"Reference": "reference",
|
|
13
|
+
"Message-ID": "messageid"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"get": {
|
|
17
|
+
"fields": ["ID", "date", "mailbox", "subject", "sender", "sender_email", "sender_name", "to", "to_email", "to_name", "cc", "bcc", "ticket", "opportunity", "reference", "messageid", "contenttype", "text", "attachments", "senddate", "senderror", "creationdate", "lastmodified"],
|
|
18
|
+
"params": { "extdata": 1 }
|
|
19
|
+
}
|
|
20
|
+
}
|
package/config/task.json
CHANGED
|
@@ -8,11 +8,13 @@
|
|
|
8
8
|
"Priority": "priority",
|
|
9
9
|
"Agent": "assigneduser.name",
|
|
10
10
|
"Due": "duedate",
|
|
11
|
-
"Ticket": "ticket"
|
|
11
|
+
"Ticket": "ticket",
|
|
12
|
+
"Project": "project",
|
|
13
|
+
"Projected": "projectedeffort"
|
|
12
14
|
}
|
|
13
15
|
},
|
|
14
16
|
"get": {
|
|
15
|
-
"fields": ["ID", "tasknum", "name", "description", "status", "priority", "duedate", "ticket", "creator", "
|
|
17
|
+
"fields": ["ID", "tasknum", "name", "description", "status", "priority", "datefrom", "duedate", "ticket", "project", "projectedeffort", "assigneduser", "creator", "creationdate", "lastmodified"],
|
|
16
18
|
"params": { "extdata": 1 }
|
|
17
19
|
}
|
|
18
20
|
}
|
package/config/ticket.json
CHANGED
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
"Status": "status",
|
|
8
8
|
"Priority": "priority",
|
|
9
9
|
"Account": "account.lastname",
|
|
10
|
+
"Project": "project",
|
|
10
11
|
"Agent": "assigneduser.name",
|
|
11
12
|
"Due": "duedate",
|
|
12
13
|
"Modified": "lastmodified"
|
|
13
14
|
}
|
|
14
15
|
},
|
|
15
16
|
"get": {
|
|
16
|
-
"fields": ["ID", "ticketnum", "name", "description", "status", "priority", "duedate", "
|
|
17
|
+
"fields": ["ID", "ticketnum", "name", "description", "status", "priority", "duedate", "account", "project", "assigneduser", "creator", "creationdate", "lastmodified"],
|
|
17
18
|
"params": { "extdata": 1 }
|
|
18
19
|
}
|
|
19
20
|
}
|
package/lib/client.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Also provides a helper that persists refreshed tokens back to the config file.
|
|
4
4
|
*/
|
|
5
5
|
import { createZeyosClient, MemoryTokenStore } from '@zeyos/client';
|
|
6
|
-
import { loadConfigWithSource,
|
|
6
|
+
import { loadConfigWithSource, persistTokens, requireConfig, listProfiles } from './config.mjs';
|
|
7
7
|
|
|
8
8
|
/** @typedef {import('./types.mjs').CliConfig} CliConfig */
|
|
9
9
|
|
|
@@ -12,10 +12,16 @@ import { loadConfigWithSource, saveConfig, requireConfig } from './config.mjs';
|
|
|
12
12
|
* Throws a friendly error if required config keys are missing.
|
|
13
13
|
*
|
|
14
14
|
* @param {CliConfig} [overrides] Extra config values (e.g. from CLI flags)
|
|
15
|
-
* @
|
|
15
|
+
* @param {{ profile?: string }} [opts] Profile selector (from --profile / ZEYOS_PROFILE)
|
|
16
|
+
* @returns {{ client: ReturnType<typeof createZeyosClient>, config: CliConfig, tokenStore: MemoryTokenStore, configSource: import('./config.mjs').ConfigSource|null }}
|
|
16
17
|
*/
|
|
17
|
-
export function buildClient(overrides = {}) {
|
|
18
|
-
const loaded = loadConfigWithSource();
|
|
18
|
+
export function buildClient(overrides = {}, opts = {}) {
|
|
19
|
+
const loaded = loadConfigWithSource({ profile: opts.profile });
|
|
20
|
+
if (loaded.profile?.missing) {
|
|
21
|
+
const names = Object.keys(listProfiles().profiles);
|
|
22
|
+
const known = names.length ? `Known profiles: ${names.join(', ')}.` : 'No profiles defined yet — create one with `zeyos profile add <name>`.';
|
|
23
|
+
throw new Error(`Profile "${loaded.profile.name}" not found (selected via ${loaded.profile.origin}). ${known}`);
|
|
24
|
+
}
|
|
19
25
|
const config = { ...loaded.config, ...overrides };
|
|
20
26
|
requireConfig(['baseUrl', 'clientId', 'clientSecret', 'accessToken'], config);
|
|
21
27
|
|
|
@@ -43,25 +49,28 @@ export function buildClient(overrides = {}) {
|
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
/**
|
|
46
|
-
* Persist any refreshed tokens back to the credential store
|
|
52
|
+
* Persist any refreshed tokens back to the credential store the config came from
|
|
53
|
+
* (a named profile, the legacy local auth.json, or the legacy global file).
|
|
47
54
|
* Call this after API operations to keep tokens up-to-date.
|
|
48
55
|
*
|
|
49
56
|
* @param {MemoryTokenStore} tokenStore
|
|
50
|
-
* @param {'local'|'global'|null}
|
|
57
|
+
* @param {import('./config.mjs').ConfigSource|'local'|'global'|null} source
|
|
51
58
|
*/
|
|
52
|
-
export async function syncTokens(tokenStore,
|
|
53
|
-
if (!
|
|
59
|
+
export async function syncTokens(tokenStore, source = 'local') {
|
|
60
|
+
if (!source) {
|
|
54
61
|
return;
|
|
55
62
|
}
|
|
63
|
+
// Back-compat: a bare 'local'/'global' string still works.
|
|
64
|
+
const resolved = typeof source === 'string' ? { kind: source } : source;
|
|
56
65
|
try {
|
|
57
66
|
const ts = await tokenStore.get();
|
|
58
67
|
if (ts?.accessToken) {
|
|
59
|
-
|
|
68
|
+
persistTokens(resolved, {
|
|
60
69
|
accessToken: ts.accessToken,
|
|
61
70
|
refreshToken: ts.refreshToken,
|
|
62
71
|
expiresAt: ts.expiresAt,
|
|
63
72
|
refreshTokenExpiresAt: ts.refreshTokenExpiresAt,
|
|
64
|
-
}
|
|
73
|
+
});
|
|
65
74
|
}
|
|
66
75
|
} catch {
|
|
67
76
|
// non-critical
|
package/lib/command.mjs
CHANGED
|
@@ -33,9 +33,9 @@ export function requireRecordId(id, usage) {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export function buildCliClient() {
|
|
36
|
+
export function buildCliClient(values = {}) {
|
|
37
37
|
try {
|
|
38
|
-
return buildClient();
|
|
38
|
+
return buildClient({}, { profile: values.profile });
|
|
39
39
|
} catch (err) {
|
|
40
40
|
fail(err.message);
|
|
41
41
|
}
|
package/lib/config.mjs
CHANGED
|
@@ -1,52 +1,142 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Credential configuration
|
|
3
|
-
* 1. Environment variables (ZEYOS_BASE_URL, ZEYOS_TOKEN, …)
|
|
4
|
-
* 2. .zeyos/auth.json (walk up from CWD, like .gitconfig)
|
|
5
|
-
* 3. ~/.config/zeyos/credentials.json
|
|
2
|
+
* Credential configuration with named profiles.
|
|
6
3
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* A "profile" is a full credential set (baseUrl, clientId, clientSecret, tokens)
|
|
5
|
+
* stored under a name. Profiles live in a global registry; a project can pin which
|
|
6
|
+
* profile it uses. The legacy single-file layout still works unchanged.
|
|
7
|
+
*
|
|
8
|
+
* Resolution cascade (first match decides which credential set is the base):
|
|
9
|
+
* 1. --profile <name> (CLI flag) -> named profile
|
|
10
|
+
* 2. ZEYOS_PROFILE (env var) -> named profile
|
|
11
|
+
* 3. .zeyos/profile (project pin, walked up) -> named profile
|
|
12
|
+
* 4. .zeyos/auth.json (legacy local, walked up)
|
|
13
|
+
* 5. profiles.json "active" (global active profile)
|
|
14
|
+
* 6. ~/.config/zeyos/credentials.json (legacy global)
|
|
15
|
+
* Environment credential vars (ZEYOS_BASE_URL, ZEYOS_TOKEN, …) always field-merge
|
|
16
|
+
* on top of whichever base was chosen.
|
|
17
|
+
*
|
|
18
|
+
* Add .zeyos/auth.json and .zeyos/profile to .gitignore.
|
|
9
19
|
*/
|
|
10
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
20
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from 'node:fs';
|
|
11
21
|
import { join, dirname } from 'node:path';
|
|
12
22
|
import { homedir } from 'node:os';
|
|
13
23
|
|
|
14
24
|
/** @typedef {import('./types.mjs').CliConfig} CliConfig */
|
|
25
|
+
/** @typedef {{ kind: 'profile'|'local'|'global', name?: string, path?: string }} ConfigSource */
|
|
15
26
|
|
|
16
27
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
17
28
|
|
|
18
|
-
const LOCAL_DIR
|
|
19
|
-
const LOCAL_FILE
|
|
20
|
-
const
|
|
21
|
-
const
|
|
29
|
+
const LOCAL_DIR = '.zeyos';
|
|
30
|
+
const LOCAL_FILE = 'auth.json';
|
|
31
|
+
const PIN_FILE = 'profile';
|
|
32
|
+
const GLOBAL_DIR = join(homedir(), '.config', 'zeyos');
|
|
33
|
+
const GLOBAL_FILE = join(GLOBAL_DIR, 'credentials.json');
|
|
34
|
+
const PROFILES_FILE = join(GLOBAL_DIR, 'profiles.json');
|
|
35
|
+
|
|
36
|
+
/** Credential fields that make up a profile / auth file. */
|
|
37
|
+
const CRED_KEYS = [
|
|
38
|
+
'baseUrl', 'instance', 'clientId', 'clientSecret',
|
|
39
|
+
'accessToken', 'refreshToken', 'expiresAt', 'refreshTokenExpiresAt'
|
|
40
|
+
];
|
|
41
|
+
const TOKEN_KEYS = ['accessToken', 'refreshToken', 'expiresAt', 'refreshTokenExpiresAt'];
|
|
22
42
|
|
|
23
43
|
// ── Load ─────────────────────────────────────────────────────────────────────
|
|
24
44
|
|
|
25
45
|
/**
|
|
26
46
|
* Load the full config object using the cascade.
|
|
27
|
-
*
|
|
47
|
+
* @param {{ profile?: string }} [opts]
|
|
28
48
|
*/
|
|
29
|
-
export function loadConfig() {
|
|
30
|
-
return loadConfigWithSource().config;
|
|
49
|
+
export function loadConfig(opts = {}) {
|
|
50
|
+
return loadConfigWithSource(opts).config;
|
|
31
51
|
}
|
|
32
52
|
|
|
33
53
|
/**
|
|
34
|
-
* Load config and identify the credential
|
|
35
|
-
*
|
|
36
|
-
*
|
|
54
|
+
* Load config and identify the credential store that should receive refreshed
|
|
55
|
+
* tokens (the `source`). Env credential vars field-merge on top of the resolved
|
|
56
|
+
* base so they always win.
|
|
57
|
+
*
|
|
58
|
+
* @param {{ profile?: string }} [opts]
|
|
59
|
+
* @returns {{ config: CliConfig, source: ConfigSource|null, profile: { name: string, origin: string }|null }}
|
|
37
60
|
*/
|
|
38
|
-
export function loadConfigWithSource() {
|
|
39
|
-
const localPath = _findLocalPath();
|
|
40
|
-
const globalFile = _readGlobal();
|
|
41
|
-
const localFile = localPath ? _readJson(localPath) : {};
|
|
61
|
+
export function loadConfigWithSource(opts = {}) {
|
|
42
62
|
const env = _fromEnv();
|
|
63
|
+
const selection = resolveProfileSelection({ profileFlag: opts.profile });
|
|
64
|
+
|
|
65
|
+
let base = {};
|
|
66
|
+
let source = null;
|
|
67
|
+
|
|
68
|
+
if (selection.name) {
|
|
69
|
+
// An explicit/active profile was selected (flag, env, pin, or active pointer).
|
|
70
|
+
const prof = getProfile(selection.name);
|
|
71
|
+
base = prof ?? {};
|
|
72
|
+
source = { kind: 'profile', name: selection.name };
|
|
73
|
+
// selection.missing is surfaced via resolveProfileSelection consumers; base
|
|
74
|
+
// stays {} so requireConfig reports the missing fields.
|
|
75
|
+
} else {
|
|
76
|
+
// No named profile in play — fall back to the legacy single-file layout.
|
|
77
|
+
const localPath = _findLocalPath();
|
|
78
|
+
if (localPath) {
|
|
79
|
+
base = _readJson(localPath);
|
|
80
|
+
source = { kind: 'local', path: localPath };
|
|
81
|
+
} else if (existsSync(GLOBAL_FILE)) {
|
|
82
|
+
base = _readGlobal();
|
|
83
|
+
source = { kind: 'global' };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
43
86
|
|
|
44
87
|
return {
|
|
45
|
-
config: { ...
|
|
46
|
-
source
|
|
88
|
+
config: { ...base, ...env },
|
|
89
|
+
source,
|
|
90
|
+
profile: selection.name
|
|
91
|
+
? { name: selection.name, origin: selection.origin, missing: Boolean(selection.missing) }
|
|
92
|
+
: null
|
|
47
93
|
};
|
|
48
94
|
}
|
|
49
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Decide which profile name applies, and where the decision came from.
|
|
98
|
+
* Order: flag > ZEYOS_PROFILE env > project pin (.zeyos/profile) > global active.
|
|
99
|
+
* `.zeyos/auth.json` (legacy local) deliberately sits BELOW the pin but is handled
|
|
100
|
+
* in loadConfigWithSource (it is not a named profile).
|
|
101
|
+
*
|
|
102
|
+
* @param {{ profileFlag?: string }} [opts]
|
|
103
|
+
* @returns {{ name: string|null, origin: 'flag'|'env'|'pin'|'active'|null, path?: string, missing?: boolean }}
|
|
104
|
+
*/
|
|
105
|
+
export function resolveProfileSelection(opts = {}) {
|
|
106
|
+
const flag = opts.profileFlag;
|
|
107
|
+
if (flag) return _withExistence({ name: flag, origin: 'flag' });
|
|
108
|
+
|
|
109
|
+
const envName = process.env.ZEYOS_PROFILE;
|
|
110
|
+
if (envName) return _withExistence({ name: envName, origin: 'env' });
|
|
111
|
+
|
|
112
|
+
const pin = readLocalPin();
|
|
113
|
+
// A pin only selects a named profile if there is NO legacy local auth.json that
|
|
114
|
+
// sits closer to the cwd (legacy projects keep working). When both exist at the
|
|
115
|
+
// same place the explicit pin wins.
|
|
116
|
+
if (pin) {
|
|
117
|
+
const localPath = _findLocalPath();
|
|
118
|
+
if (!localPath || _isSameOrShallower(pin.dir, localPath)) {
|
|
119
|
+
return _withExistence({ name: pin.name, origin: 'pin', path: pin.path });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// If a legacy local auth.json exists, it (not the global active profile) is the
|
|
124
|
+
// base — handled by the caller. Only fall through to the active profile when no
|
|
125
|
+
// local file shadows it.
|
|
126
|
+
if (_findLocalPath()) return { name: null, origin: null };
|
|
127
|
+
|
|
128
|
+
const active = getActiveProfileName();
|
|
129
|
+
if (active) return _withExistence({ name: active, origin: 'active' });
|
|
130
|
+
|
|
131
|
+
return { name: null, origin: null };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function _withExistence(sel) {
|
|
135
|
+
return { ...sel, missing: getProfile(sel.name) == null };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── Require ──────────────────────────────────────────────────────────────────
|
|
139
|
+
|
|
50
140
|
/**
|
|
51
141
|
* Require specific keys to be present; throws a human-friendly error if not.
|
|
52
142
|
* @param {string[]} keys
|
|
@@ -67,12 +157,11 @@ export function requireConfig(keys, config) {
|
|
|
67
157
|
throw new Error(`Missing required configuration:\n${messages.join('\n')}`);
|
|
68
158
|
}
|
|
69
159
|
|
|
70
|
-
// ── Save
|
|
160
|
+
// ── Save (legacy single-file) ─────────────────────────────────────────────────
|
|
71
161
|
|
|
72
162
|
/**
|
|
73
|
-
* Save (merge) config values into the nearest .zeyos/auth.json
|
|
74
|
-
*
|
|
75
|
-
* global file when `scope === 'global'`.
|
|
163
|
+
* Save (merge) config values into the nearest .zeyos/auth.json (or create one in
|
|
164
|
+
* the current directory), or the global credentials file when scope === 'global'.
|
|
76
165
|
*
|
|
77
166
|
* @param {CliConfig} updates
|
|
78
167
|
* @param {'local'|'global'} scope
|
|
@@ -90,39 +179,163 @@ export function saveConfig(updates, scope = 'local') {
|
|
|
90
179
|
|
|
91
180
|
/** Remove the stored tokens (leave connection params intact). */
|
|
92
181
|
export function clearTokens(scope = 'local') {
|
|
93
|
-
const strip = o => {
|
|
94
|
-
const { accessToken, refreshToken, expiresAt, refreshTokenExpiresAt, ...rest } = o;
|
|
95
|
-
return rest;
|
|
96
|
-
};
|
|
97
182
|
if (scope === 'global') {
|
|
98
|
-
_writeGlobal(
|
|
183
|
+
_writeGlobal(_stripTokens(_readGlobal()));
|
|
99
184
|
return;
|
|
100
185
|
}
|
|
101
186
|
const path = _findLocalPath();
|
|
102
|
-
if (path) _writeJson(path,
|
|
187
|
+
if (path) _writeJson(path, _stripTokens(_readJson(path)));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Persist refreshed tokens back to wherever the active credentials came from.
|
|
192
|
+
* @param {ConfigSource|null} source
|
|
193
|
+
* @param {Partial<CliConfig>} tokens
|
|
194
|
+
*/
|
|
195
|
+
export function persistTokens(source, tokens) {
|
|
196
|
+
if (!source) return;
|
|
197
|
+
const slice = {};
|
|
198
|
+
for (const k of TOKEN_KEYS) if (k in tokens) slice[k] = tokens[k];
|
|
199
|
+
if (source.kind === 'profile') {
|
|
200
|
+
upsertProfile(source.name, slice);
|
|
201
|
+
} else if (source.kind === 'global') {
|
|
202
|
+
saveConfig(slice, 'global');
|
|
203
|
+
} else {
|
|
204
|
+
saveConfig(slice, 'local');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Clear tokens from whichever store the source points at. */
|
|
209
|
+
export function clearTokensForSource(source) {
|
|
210
|
+
if (!source) return;
|
|
211
|
+
if (source.kind === 'profile') {
|
|
212
|
+
const prof = getProfile(source.name);
|
|
213
|
+
if (prof) upsertProfile(source.name, _stripTokens(prof), { replace: true });
|
|
214
|
+
} else if (source.kind === 'global') {
|
|
215
|
+
clearTokens('global');
|
|
216
|
+
} else {
|
|
217
|
+
clearTokens('local');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── Profiles ───────────────────────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
/** Read the profiles registry: { active, profiles }. */
|
|
224
|
+
export function readProfiles() {
|
|
225
|
+
const raw = existsSync(PROFILES_FILE) ? _readJson(PROFILES_FILE) : {};
|
|
226
|
+
return {
|
|
227
|
+
active: typeof raw.active === 'string' ? raw.active : null,
|
|
228
|
+
profiles: raw.profiles && typeof raw.profiles === 'object' ? raw.profiles : {}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** List profile names with their (token-stripped-safe) creds and the active name. */
|
|
233
|
+
export function listProfiles() {
|
|
234
|
+
const { active, profiles } = readProfiles();
|
|
235
|
+
return {
|
|
236
|
+
active,
|
|
237
|
+
profiles: Object.fromEntries(Object.entries(profiles).map(([name, creds]) => [name, { ...creds }]))
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Return a single profile's credentials, or null. */
|
|
242
|
+
export function getProfile(name) {
|
|
243
|
+
if (!name) return null;
|
|
244
|
+
const { profiles } = readProfiles();
|
|
245
|
+
return profiles[name] ? { ...profiles[name] } : null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Create or update a profile (field merge by default).
|
|
250
|
+
* @param {string} name
|
|
251
|
+
* @param {Partial<CliConfig>} updates
|
|
252
|
+
* @param {{ replace?: boolean }} [opts] replace: overwrite instead of merge
|
|
253
|
+
*/
|
|
254
|
+
export function upsertProfile(name, updates, opts = {}) {
|
|
255
|
+
if (!name) throw new Error('Profile name is required.');
|
|
256
|
+
const reg = readProfiles();
|
|
257
|
+
const current = opts.replace ? {} : (reg.profiles[name] || {});
|
|
258
|
+
reg.profiles[name] = _onlyCredKeys({ ...current, ...updates });
|
|
259
|
+
if (!reg.active) reg.active = name; // first profile becomes active
|
|
260
|
+
_writeProfiles(reg);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Delete a profile. Clears the active pointer if it referenced this profile. */
|
|
264
|
+
export function removeProfile(name) {
|
|
265
|
+
const reg = readProfiles();
|
|
266
|
+
if (!(name in reg.profiles)) return false;
|
|
267
|
+
delete reg.profiles[name];
|
|
268
|
+
if (reg.active === name) {
|
|
269
|
+
reg.active = Object.keys(reg.profiles)[0] ?? null;
|
|
270
|
+
}
|
|
271
|
+
_writeProfiles(reg);
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Set the global active profile. Throws if it does not exist. */
|
|
276
|
+
export function setActiveProfile(name) {
|
|
277
|
+
const reg = readProfiles();
|
|
278
|
+
if (!(name in reg.profiles)) throw new Error(`No such profile: "${name}".`);
|
|
279
|
+
reg.active = name;
|
|
280
|
+
_writeProfiles(reg);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function getActiveProfileName() {
|
|
284
|
+
return readProfiles().active;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ── Project pin (.zeyos/profile) ─────────────────────────────────────────────
|
|
288
|
+
|
|
289
|
+
/** Read the nearest project pin: { name, path, dir } or null. */
|
|
290
|
+
export function readLocalPin() {
|
|
291
|
+
let dir = process.cwd();
|
|
292
|
+
for (let i = 0; i < 20; i++) {
|
|
293
|
+
const candidate = join(dir, LOCAL_DIR, PIN_FILE);
|
|
294
|
+
if (existsSync(candidate)) {
|
|
295
|
+
try {
|
|
296
|
+
const name = readFileSync(candidate, 'utf8').trim();
|
|
297
|
+
if (name) return { name, path: candidate, dir };
|
|
298
|
+
} catch { /* ignore unreadable pin */ }
|
|
299
|
+
}
|
|
300
|
+
const parent = dirname(dir);
|
|
301
|
+
if (parent === dir) break;
|
|
302
|
+
dir = parent;
|
|
303
|
+
}
|
|
304
|
+
return null;
|
|
103
305
|
}
|
|
104
306
|
|
|
105
|
-
/**
|
|
106
|
-
export function
|
|
107
|
-
|
|
307
|
+
/** Pin a profile for the current project (writes ./.zeyos/profile). */
|
|
308
|
+
export function writeLocalPin(name, dir = process.cwd()) {
|
|
309
|
+
const path = join(dir, LOCAL_DIR, PIN_FILE);
|
|
310
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
311
|
+
writeFileSync(path, `${name}\n`, { mode: 0o600 });
|
|
312
|
+
return path;
|
|
108
313
|
}
|
|
109
314
|
|
|
110
|
-
/**
|
|
111
|
-
export function
|
|
112
|
-
|
|
315
|
+
/** Remove the nearest project pin, if any. Returns the removed path or null. */
|
|
316
|
+
export function clearLocalPin() {
|
|
317
|
+
const pin = readLocalPin();
|
|
318
|
+
if (pin) { unlinkSync(pin.path); return pin.path; }
|
|
319
|
+
return null;
|
|
113
320
|
}
|
|
114
321
|
|
|
322
|
+
// ── Paths (for messages) ───────────────────────────────────────────────────────
|
|
323
|
+
|
|
324
|
+
export function localConfigPath() { return _findLocalPath(); }
|
|
325
|
+
export function globalConfigPath() { return GLOBAL_FILE; }
|
|
326
|
+
export function profilesConfigPath() { return PROFILES_FILE; }
|
|
327
|
+
|
|
115
328
|
// ── Internals ────────────────────────────────────────────────────────────────
|
|
116
329
|
|
|
117
330
|
function _fromEnv() {
|
|
118
331
|
const e = process.env;
|
|
119
332
|
const out = {};
|
|
120
|
-
if (e.ZEYOS_BASE_URL)
|
|
121
|
-
if (e.ZEYOS_INSTANCE)
|
|
122
|
-
if (e.ZEYOS_CLIENT_ID)
|
|
123
|
-
if (e.ZEYOS_CLIENT_SECRET)
|
|
124
|
-
if (e.ZEYOS_TOKEN)
|
|
125
|
-
if (e.ZEYOS_REFRESH_TOKEN)
|
|
333
|
+
if (e.ZEYOS_BASE_URL) out.baseUrl = e.ZEYOS_BASE_URL;
|
|
334
|
+
if (e.ZEYOS_INSTANCE) out.instance = e.ZEYOS_INSTANCE;
|
|
335
|
+
if (e.ZEYOS_CLIENT_ID) out.clientId = e.ZEYOS_CLIENT_ID;
|
|
336
|
+
if (e.ZEYOS_CLIENT_SECRET) out.clientSecret = e.ZEYOS_CLIENT_SECRET;
|
|
337
|
+
if (e.ZEYOS_TOKEN) out.accessToken = e.ZEYOS_TOKEN;
|
|
338
|
+
if (e.ZEYOS_REFRESH_TOKEN) out.refreshToken = e.ZEYOS_REFRESH_TOKEN;
|
|
126
339
|
return out;
|
|
127
340
|
}
|
|
128
341
|
|
|
@@ -138,6 +351,23 @@ function _findLocalPath() {
|
|
|
138
351
|
return null;
|
|
139
352
|
}
|
|
140
353
|
|
|
354
|
+
/** True when the pin directory is at or above the auth.json's directory. */
|
|
355
|
+
function _isSameOrShallower(pinDir, localPath) {
|
|
356
|
+
const localDir = dirname(dirname(localPath)); // strip /.zeyos/auth.json
|
|
357
|
+
return pinDir.length <= localDir.length;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function _onlyCredKeys(obj) {
|
|
361
|
+
const out = {};
|
|
362
|
+
for (const k of CRED_KEYS) if (obj[k] != null) out[k] = obj[k];
|
|
363
|
+
return out;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function _stripTokens(o) {
|
|
367
|
+
const { accessToken, refreshToken, expiresAt, refreshTokenExpiresAt, ...rest } = o;
|
|
368
|
+
return rest;
|
|
369
|
+
}
|
|
370
|
+
|
|
141
371
|
function _readGlobal() {
|
|
142
372
|
return existsSync(GLOBAL_FILE) ? _readJson(GLOBAL_FILE) : {};
|
|
143
373
|
}
|
|
@@ -147,6 +377,11 @@ function _writeGlobal(data) {
|
|
|
147
377
|
_writeJson(GLOBAL_FILE, data);
|
|
148
378
|
}
|
|
149
379
|
|
|
380
|
+
function _writeProfiles(reg) {
|
|
381
|
+
mkdirSync(GLOBAL_DIR, { recursive: true });
|
|
382
|
+
_writeJson(PROFILES_FILE, { active: reg.active ?? null, profiles: reg.profiles ?? {} });
|
|
383
|
+
}
|
|
384
|
+
|
|
150
385
|
function _readJson(path) {
|
|
151
386
|
try {
|
|
152
387
|
return JSON.parse(readFileSync(path, 'utf8'));
|
package/lib/flags.mjs
CHANGED
|
@@ -10,7 +10,7 @@ const RESERVED_FLAGS = new Set([
|
|
|
10
10
|
'no-color', 'force', 'fields', 'filter', 'filter-file', 'sort',
|
|
11
11
|
'limit', 'offset', 'expand', 'base-url', 'client-id',
|
|
12
12
|
'secret', 'scope', 'global', 'port', 'manual', 'show-token',
|
|
13
|
-
'extdata', 'tags', 'all', 'clean', 'query',
|
|
13
|
+
'extdata', 'tags', 'all', 'clean', 'query', 'profile',
|
|
14
14
|
]);
|
|
15
15
|
|
|
16
16
|
/**
|
package/lib/resources.mjs
CHANGED
|
@@ -14,13 +14,21 @@
|
|
|
14
14
|
|
|
15
15
|
/** @type {Record<string, ResourceDef>} */
|
|
16
16
|
const REGISTRY = {
|
|
17
|
+
actionstep: {
|
|
18
|
+
list: 'listActionSteps',
|
|
19
|
+
get: 'getActionStep',
|
|
20
|
+
create: 'createActionStep',
|
|
21
|
+
update: 'updateActionStep',
|
|
22
|
+
delete: 'deleteActionStep',
|
|
23
|
+
fields: ['ID', 'actionnum', 'name', 'status', 'date', 'duedate', 'effort', 'ticket', 'task', 'account'],
|
|
24
|
+
},
|
|
17
25
|
ticket: {
|
|
18
26
|
list: 'listTickets',
|
|
19
27
|
get: 'getTicket',
|
|
20
28
|
create: 'createTicket',
|
|
21
29
|
update: 'updateTicket',
|
|
22
30
|
delete: 'deleteTicket',
|
|
23
|
-
fields: ['ID', 'ticketnum', 'name', 'status', 'priority', 'duedate', 'lastmodified'],
|
|
31
|
+
fields: ['ID', 'ticketnum', 'name', 'status', 'priority', 'duedate', 'account', 'project', 'lastmodified'],
|
|
24
32
|
},
|
|
25
33
|
task: {
|
|
26
34
|
list: 'listTasks',
|
|
@@ -28,7 +36,7 @@ const REGISTRY = {
|
|
|
28
36
|
create: 'createTask',
|
|
29
37
|
update: 'updateTask',
|
|
30
38
|
delete: 'deleteTask',
|
|
31
|
-
fields: ['ID', 'tasknum', 'name', 'status', 'priority', 'duedate', 'ticket'],
|
|
39
|
+
fields: ['ID', 'tasknum', 'name', 'status', 'priority', 'duedate', 'ticket', 'project', 'projectedeffort'],
|
|
32
40
|
},
|
|
33
41
|
account: {
|
|
34
42
|
list: 'listAccounts',
|
|
@@ -84,7 +92,7 @@ const REGISTRY = {
|
|
|
84
92
|
create: 'createMessage',
|
|
85
93
|
update: 'updateMessage',
|
|
86
94
|
delete: 'deleteMessage',
|
|
87
|
-
fields: ['ID', 'subject', '
|
|
95
|
+
fields: ['ID', 'date', 'mailbox', 'subject', 'sender_email', 'to_email', 'ticket', 'reference', 'messageid'],
|
|
88
96
|
},
|
|
89
97
|
item: {
|
|
90
98
|
list: 'listItems',
|
|
@@ -144,6 +152,11 @@ const REGISTRY = {
|
|
|
144
152
|
delete: 'deleteCampaign',
|
|
145
153
|
fields: ['ID', 'name', 'status', 'startdate', 'enddate'],
|
|
146
154
|
},
|
|
155
|
+
customfield: {
|
|
156
|
+
list: 'listCustomFields',
|
|
157
|
+
get: 'getCustomField',
|
|
158
|
+
fields: ['ID', 'name', 'identifier', 'context', 'reference', 'type', 'entity', 'activity'],
|
|
159
|
+
},
|
|
147
160
|
file: {
|
|
148
161
|
list: 'listFiles',
|
|
149
162
|
get: 'getFile',
|
|
@@ -174,6 +187,13 @@ const REGISTRY = {
|
|
|
174
187
|
|
|
175
188
|
const ALIASES = {
|
|
176
189
|
// Plurals
|
|
190
|
+
actionsteps: 'actionstep',
|
|
191
|
+
'action-steps': 'actionstep',
|
|
192
|
+
action_steps: 'actionstep',
|
|
193
|
+
timeentry: 'actionstep',
|
|
194
|
+
timeentries: 'actionstep',
|
|
195
|
+
'time-entry': 'actionstep',
|
|
196
|
+
'time-entries': 'actionstep',
|
|
177
197
|
tickets: 'ticket',
|
|
178
198
|
tasks: 'task',
|
|
179
199
|
accounts: 'account',
|
|
@@ -193,6 +213,9 @@ const ALIASES = {
|
|
|
193
213
|
payments: 'payment',
|
|
194
214
|
opportunities:'opportunity',
|
|
195
215
|
campaigns: 'campaign',
|
|
216
|
+
customfields: 'customfield',
|
|
217
|
+
custom_fields: 'customfield',
|
|
218
|
+
'custom-fields': 'customfield',
|
|
196
219
|
files: 'file',
|
|
197
220
|
invitations: 'invitation',
|
|
198
221
|
storages: 'storage',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeyos/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Command-line interface for the ZeyOS API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"node": ">=18.3"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@zeyos/client": "^0.
|
|
42
|
+
"@zeyos/client": "^0.3.0"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
45
|
"test": "node --test test/offline.mjs"
|