@zeyos/cli 0.3.0 → 0.5.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 +3 -0
- package/bin/zeyos.mjs +8 -2
- package/commands/login.mjs +4 -0
- package/commands/logout.mjs +35 -9
- package/commands/okf.mjs +192 -0
- package/commands/profile.mjs +117 -22
- package/commands/whoami.mjs +203 -20
- package/lib/config.mjs +28 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -47,6 +47,9 @@ zeyos whoami --json
|
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
`whoami` does not print access tokens by default. Use `zeyos whoami --show-token --json` only when you intentionally need to pass a token to another local tool.
|
|
50
|
+
If a stored refresh token has expired, interactive `whoami` offers to re-authenticate
|
|
51
|
+
and then retries the user lookup. In `--json`/`--yaml` or non-interactive runs, it
|
|
52
|
+
prints a diagnostic plus the matching `zeyos login --force` command instead.
|
|
50
53
|
|
|
51
54
|
Inspect the CLI-supported resource registry:
|
|
52
55
|
|
package/bin/zeyos.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Commands:
|
|
8
8
|
* login Authenticate with ZeyOS
|
|
9
|
-
* logout Revoke session and clear
|
|
9
|
+
* logout Revoke session and clear stored credentials
|
|
10
10
|
* whoami Show current user info
|
|
11
11
|
* list <resource> List records
|
|
12
12
|
* count <resource> Count records
|
|
@@ -38,7 +38,7 @@ Usage: ${_z} <command> [options] [args…]
|
|
|
38
38
|
|
|
39
39
|
${_c.bold('Commands:')}
|
|
40
40
|
${_c.cyan('login')} Authenticate with a ZeyOS instance
|
|
41
|
-
${_c.cyan('logout')} Revoke session and clear stored
|
|
41
|
+
${_c.cyan('logout')} Revoke session and clear stored credentials
|
|
42
42
|
${_c.cyan('whoami')} Show currently authenticated user
|
|
43
43
|
${_c.cyan('list')} <resource> List / query records
|
|
44
44
|
${_c.cyan('count')} <resource> Count records (with optional filter)
|
|
@@ -51,6 +51,7 @@ ${_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('okf')} <command> List / show / check / export the OKF knowledge bundle
|
|
54
55
|
${_c.cyan('profile')} <command> Manage credential profiles / switch instances
|
|
55
56
|
|
|
56
57
|
${_c.bold('Global options:')}
|
|
@@ -120,6 +121,8 @@ const OPTIONS = {
|
|
|
120
121
|
'target': { type: 'string' },
|
|
121
122
|
'dir': { type: 'string' },
|
|
122
123
|
'no-logo': { type: 'boolean' },
|
|
124
|
+
// okf
|
|
125
|
+
'out': { type: 'string' },
|
|
123
126
|
// profile
|
|
124
127
|
'from-current': { type: 'boolean' },
|
|
125
128
|
};
|
|
@@ -147,6 +150,7 @@ const COMMANDS = {
|
|
|
147
150
|
doctor: '../commands/doctor.mjs',
|
|
148
151
|
skills: '../commands/skills.mjs',
|
|
149
152
|
skill: '../commands/skills.mjs',
|
|
153
|
+
okf: '../commands/okf.mjs',
|
|
150
154
|
profile: '../commands/profile.mjs',
|
|
151
155
|
profiles: '../commands/profile.mjs',
|
|
152
156
|
};
|
|
@@ -158,6 +162,7 @@ const COMMANDS = {
|
|
|
158
162
|
|
|
159
163
|
const ALWAYS_FLAGS = ['help', 'json', 'yaml', 'no-color', 'profile'];
|
|
160
164
|
const SKILLS_FLAGS = ['target', 'dir', 'global', 'local', 'force', 'yes', 'no-logo'];
|
|
165
|
+
const OKF_FLAGS = ['dir', 'out', 'force', 'no-logo'];
|
|
161
166
|
const PROFILE_FLAGS = ['base-url', 'client-id', 'secret', 'local', 'from-current'];
|
|
162
167
|
const DELETE_FLAGS = ['force', 'query'];
|
|
163
168
|
const GET_FLAGS = ['fields', 'extdata', 'tags', 'expand', 'all', 'query'];
|
|
@@ -182,6 +187,7 @@ const COMMAND_FLAGS = {
|
|
|
182
187
|
doctor: [],
|
|
183
188
|
skills: SKILLS_FLAGS,
|
|
184
189
|
skill: SKILLS_FLAGS,
|
|
190
|
+
okf: OKF_FLAGS,
|
|
185
191
|
profile: PROFILE_FLAGS,
|
|
186
192
|
profiles: PROFILE_FLAGS,
|
|
187
193
|
};
|
package/commands/login.mjs
CHANGED
|
@@ -49,6 +49,10 @@ export async function run(values) {
|
|
|
49
49
|
const profileName = values.profile || null;
|
|
50
50
|
const scope = values.global ? 'global' : 'local';
|
|
51
51
|
const port = values.port ? Number(values.port) : DEFAULT_CALLBACK_PORT;
|
|
52
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
53
|
+
error('--port must be an integer between 1 and 65535.');
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
52
56
|
const redirectUri = callbackUri(port);
|
|
53
57
|
|
|
54
58
|
// Persist either into a named profile or the legacy local/global credential file.
|
package/commands/logout.mjs
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* zeyos logout
|
|
3
3
|
*
|
|
4
|
-
* Revokes the current access token (best-effort) and
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Revokes the current access token (best-effort) and clears the selected
|
|
5
|
+
* stored session. Local legacy credentials are cleared completely so a
|
|
6
|
+
* subsequent `zeyos login` starts from fresh connection parameters.
|
|
7
7
|
*
|
|
8
8
|
* Options:
|
|
9
9
|
* --global Clear the global credentials file
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { createZeyosClient, MemoryTokenStore } from '@zeyos/client';
|
|
13
|
-
import {
|
|
14
|
-
|
|
13
|
+
import {
|
|
14
|
+
loadConfigWithSource,
|
|
15
|
+
loadGlobalConfig,
|
|
16
|
+
clearTokensForSource,
|
|
17
|
+
clearLocalCredentialsForSource,
|
|
18
|
+
listProfiles
|
|
19
|
+
} from '../lib/config.mjs';
|
|
20
|
+
import { success, warn, info, error } from '../lib/output.mjs';
|
|
15
21
|
|
|
16
22
|
export const USAGE = `\
|
|
17
23
|
Usage: zeyos logout [options]
|
|
18
24
|
|
|
19
|
-
Revoke the current session and clear stored
|
|
25
|
+
Revoke the current session and clear stored credentials.
|
|
20
26
|
|
|
21
27
|
Options:
|
|
22
28
|
--profile <name> Log out of a specific profile
|
|
@@ -25,9 +31,29 @@ Options:
|
|
|
25
31
|
`;
|
|
26
32
|
|
|
27
33
|
export async function run(values) {
|
|
28
|
-
|
|
34
|
+
let config;
|
|
35
|
+
let source;
|
|
36
|
+
|
|
37
|
+
if (values.global) {
|
|
38
|
+
config = loadGlobalConfig();
|
|
39
|
+
source = { kind: 'global' };
|
|
40
|
+
} else {
|
|
41
|
+
const loaded = loadConfigWithSource({ profile: values.profile });
|
|
42
|
+
if (loaded.profile?.missing) {
|
|
43
|
+
const names = Object.keys(listProfiles().profiles);
|
|
44
|
+
const known = names.length ? `Known profiles: ${names.join(', ')}.` : 'No profiles defined yet.';
|
|
45
|
+
error(`Profile "${loaded.profile.name}" not found (selected via ${loaded.profile.origin}). ${known}`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
config = loaded.config;
|
|
49
|
+
source = loaded.source;
|
|
50
|
+
}
|
|
29
51
|
|
|
30
52
|
if (!config.accessToken) {
|
|
53
|
+
if (source?.kind === 'local' && clearLocalCredentialsForSource(source)) {
|
|
54
|
+
success('Logged out (local credentials).');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
31
57
|
warn('Not currently logged in.');
|
|
32
58
|
return;
|
|
33
59
|
}
|
|
@@ -58,8 +84,8 @@ export async function run(values) {
|
|
|
58
84
|
}
|
|
59
85
|
}
|
|
60
86
|
|
|
61
|
-
if (
|
|
62
|
-
|
|
87
|
+
if (source?.kind === 'local') {
|
|
88
|
+
clearLocalCredentialsForSource(source);
|
|
63
89
|
} else {
|
|
64
90
|
clearTokensForSource(source);
|
|
65
91
|
}
|
package/commands/okf.mjs
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* zeyos okf <list|show|check|build|export>
|
|
3
|
+
*
|
|
4
|
+
* Work with the Open Knowledge Format (OKF v0.1) bundle that ships with
|
|
5
|
+
* @zeyos/client (under okf/): a directory of Markdown concept docs describing the
|
|
6
|
+
* ZeyOS data model (entities, schema, foreign keys, enums, indexes, operations)
|
|
7
|
+
* plus curated metrics, playbooks, and query concepts. Consumers — coding agents,
|
|
8
|
+
* viewers, search — read it; this command lists/shows/validates the shipped bundle
|
|
9
|
+
* and can synthesize or export one.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync, existsSync, cpSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
|
|
17
|
+
import { loadOkfBundle, validateOkfFiles, buildOkf, OKF_VERSION } from '@zeyos/client';
|
|
18
|
+
import { outputMode, printJson, printYaml, printTable, colors, success, error, info, warn } from '../lib/output.mjs';
|
|
19
|
+
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
|
|
22
|
+
export const USAGE = `\
|
|
23
|
+
Usage: zeyos okf <command> [options]
|
|
24
|
+
|
|
25
|
+
Commands:
|
|
26
|
+
list List concepts in the OKF bundle (type, id, title)
|
|
27
|
+
show <concept> Print a concept doc (e.g. "tickets" or "entities/tickets")
|
|
28
|
+
check Validate the bundle for OKF v0.1 conformance
|
|
29
|
+
build [--out <dir>] Synthesize an OKF bundle from the client's schema
|
|
30
|
+
export [--out <dir>] Copy the shipped okf/ bundle into a directory
|
|
31
|
+
|
|
32
|
+
Options:
|
|
33
|
+
--dir <path> Read from an explicit bundle directory (list/show/check)
|
|
34
|
+
--out <path> Write to this directory (build/export; default ./okf)
|
|
35
|
+
--force Overwrite an existing target (export)
|
|
36
|
+
--json Output as JSON
|
|
37
|
+
--yaml Output as YAML
|
|
38
|
+
-h, --help Show this help
|
|
39
|
+
|
|
40
|
+
Examples:
|
|
41
|
+
zeyos okf list
|
|
42
|
+
zeyos okf show tickets
|
|
43
|
+
zeyos okf check
|
|
44
|
+
zeyos okf export --out ./vendor/okf
|
|
45
|
+
zeyos okf build --out ./okf-live`;
|
|
46
|
+
|
|
47
|
+
// Locate the okf/ bundle shipped inside the @zeyos/client package (mirrors the
|
|
48
|
+
// skills command's findAgentsDir).
|
|
49
|
+
function findOkfDir() {
|
|
50
|
+
let entry;
|
|
51
|
+
try {
|
|
52
|
+
entry = require.resolve('@zeyos/client');
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
let dir = path.dirname(entry);
|
|
57
|
+
for (let i = 0; i < 6; i++) {
|
|
58
|
+
const candidate = path.join(dir, 'okf');
|
|
59
|
+
if (existsSync(path.join(candidate, 'index.md'))) return candidate;
|
|
60
|
+
const parent = path.dirname(dir);
|
|
61
|
+
if (parent === dir) break;
|
|
62
|
+
dir = parent;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function expandHome(p) {
|
|
68
|
+
if (p === '~') return homedir();
|
|
69
|
+
if (p.startsWith('~/') || p.startsWith('~\\')) return path.join(homedir(), p.slice(2));
|
|
70
|
+
return p;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function displayPath(abs) {
|
|
74
|
+
const rel = path.relative(process.cwd(), abs);
|
|
75
|
+
if (rel && !rel.startsWith('..') && !path.isAbsolute(rel)) return rel;
|
|
76
|
+
const home = homedir();
|
|
77
|
+
if (abs === home || abs.startsWith(home + path.sep)) return '~' + abs.slice(home.length);
|
|
78
|
+
return abs;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolveInputDir(values) {
|
|
82
|
+
if (values.dir) return path.resolve(expandHome(values.dir));
|
|
83
|
+
const found = findOkfDir();
|
|
84
|
+
if (!found) {
|
|
85
|
+
error('Could not locate the bundled OKF bundle (the @zeyos/client okf/ directory). Pass --dir <path>.');
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
return found;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function conceptsToRows(concepts) {
|
|
92
|
+
return Object.entries(concepts)
|
|
93
|
+
.map(([id, { frontmatter }]) => ({ type: frontmatter.type || '(none)', concept: id, title: frontmatter.title || '' }))
|
|
94
|
+
.sort((a, b) => a.concept.localeCompare(b.concept));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function runList(values) {
|
|
98
|
+
const dir = resolveInputDir(values);
|
|
99
|
+
const { version, concepts } = await loadOkfBundle(dir);
|
|
100
|
+
const rows = conceptsToRows(concepts);
|
|
101
|
+
const mode = outputMode(values);
|
|
102
|
+
if (mode === 'json') return printJson({ version, concepts: rows });
|
|
103
|
+
if (mode === 'yaml') return printYaml({ version, concepts: rows });
|
|
104
|
+
printTable(rows, ['type', 'concept', 'title']);
|
|
105
|
+
info(`OKF v${version || OKF_VERSION} — ${rows.length} concepts in ${displayPath(dir)}. Show one with: zeyos okf show <concept>`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function runShow(values, name) {
|
|
109
|
+
const dir = resolveInputDir(values);
|
|
110
|
+
const candidates = [name, `${name}.md`, `entities/${name}.md`, path.join(dir, name), path.join(dir, `${name}.md`), path.join(dir, 'entities', `${name}.md`)];
|
|
111
|
+
for (const candidate of candidates) {
|
|
112
|
+
const abs = path.isAbsolute(candidate) ? candidate : path.join(dir, candidate);
|
|
113
|
+
if (existsSync(abs)) {
|
|
114
|
+
process.stdout.write(readFileSync(abs, 'utf8'));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
error(`Unknown concept "${name}". Run "zeyos okf list".`);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function runCheck(values) {
|
|
123
|
+
const dir = resolveInputDir(values);
|
|
124
|
+
const { files } = await loadOkfBundle(dir);
|
|
125
|
+
const result = validateOkfFiles(files);
|
|
126
|
+
const mode = outputMode(values);
|
|
127
|
+
if (mode === 'json') { printJson(result); process.exit(result.valid ? 0 : 1); }
|
|
128
|
+
if (mode === 'yaml') { printYaml(result); process.exit(result.valid ? 0 : 1); }
|
|
129
|
+
if (result.valid) {
|
|
130
|
+
success(`OKF bundle is conformant: ${result.conceptCount} concepts, 0 errors (${displayPath(dir)}).`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
for (const err of result.errors) error(`${err.path}: ${err.message}`);
|
|
134
|
+
error(`OKF bundle is NOT conformant: ${result.errors.length} error(s).`);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function writeBundle(outDir, files) {
|
|
139
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
140
|
+
const abs = path.join(outDir, rel);
|
|
141
|
+
mkdirSync(path.dirname(abs), { recursive: true });
|
|
142
|
+
writeFileSync(abs, content, 'utf8');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function runBuild(values) {
|
|
147
|
+
const outDir = path.resolve(expandHome(values.out || 'okf'));
|
|
148
|
+
const files = buildOkf();
|
|
149
|
+
writeBundle(outDir, files);
|
|
150
|
+
const mode = outputMode(values);
|
|
151
|
+
const summary = { out: outDir, files: Object.keys(files).length };
|
|
152
|
+
if (mode === 'json') return printJson(summary);
|
|
153
|
+
if (mode === 'yaml') return printYaml(summary);
|
|
154
|
+
success(`Synthesized ${summary.files} OKF files → ${displayPath(outDir)}`);
|
|
155
|
+
info('This is the runtime projection from the client schema (structural only). The shipped okf/ bundle adds curated metrics, playbooks, and notes.');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function runExport(values) {
|
|
159
|
+
const dir = resolveInputDir(values);
|
|
160
|
+
const outDir = path.resolve(expandHome(values.out || 'okf'));
|
|
161
|
+
if (existsSync(outDir) && !values.force) {
|
|
162
|
+
if (path.resolve(dir) === outDir) {
|
|
163
|
+
warn(`Source and target are the same (${displayPath(outDir)}); nothing to do.`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
error(`Target ${displayPath(outDir)} already exists. Use --force to overwrite.`);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
cpSync(dir, outDir, { recursive: true });
|
|
170
|
+
const mode = outputMode(values);
|
|
171
|
+
const summary = { from: dir, out: outDir };
|
|
172
|
+
if (mode === 'json') return printJson(summary);
|
|
173
|
+
if (mode === 'yaml') return printYaml(summary);
|
|
174
|
+
success(`Exported OKF bundle → ${displayPath(outDir)}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function run(values, positional = []) {
|
|
178
|
+
const sub = positional[0] || 'list';
|
|
179
|
+
const rest = positional.slice(1);
|
|
180
|
+
switch (sub) {
|
|
181
|
+
case 'list': return runList(values);
|
|
182
|
+
case 'show':
|
|
183
|
+
if (!rest[0]) { error('Usage: zeyos okf show <concept>'); process.exit(1); }
|
|
184
|
+
return runShow(values, rest[0]);
|
|
185
|
+
case 'check': return runCheck(values);
|
|
186
|
+
case 'build': return runBuild(values);
|
|
187
|
+
case 'export': return runExport(values);
|
|
188
|
+
default:
|
|
189
|
+
error(`Unknown okf command "${sub}".\n\n${USAGE}`);
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
}
|
package/commands/profile.mjs
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* the credentials currently in effect (including tokens) into the new profile.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { createInterface } from 'node:readline';
|
|
17
18
|
import {
|
|
18
19
|
listProfiles, getProfile, upsertProfile, removeProfile,
|
|
19
20
|
setActiveProfile, writeLocalPin, readLocalPin,
|
|
@@ -31,7 +32,7 @@ Commands:
|
|
|
31
32
|
current Show which profile is in effect, and why
|
|
32
33
|
use <name> Make <name> the active profile (global)
|
|
33
34
|
use <name> --local Pin <name> to the current project (.zeyos/profile)
|
|
34
|
-
add <name> [options]
|
|
35
|
+
add [<name>] [options] Create or update a profile; prompts when run without connection options
|
|
35
36
|
remove <name> Delete a profile
|
|
36
37
|
|
|
37
38
|
Add options:
|
|
@@ -45,6 +46,7 @@ Global options:
|
|
|
45
46
|
-h, --help Show this help
|
|
46
47
|
|
|
47
48
|
Examples:
|
|
49
|
+
zeyos profile add # prompt for name and connection params
|
|
48
50
|
zeyos profile add dev --base-url https://zeyos.cms-it.de/dev
|
|
49
51
|
zeyos profile add prod --from-current
|
|
50
52
|
zeyos profile use prod
|
|
@@ -147,30 +149,42 @@ function cmdUse(values, name) {
|
|
|
147
149
|
|
|
148
150
|
// ── add ────────────────────────────────────────────────────────────────────────
|
|
149
151
|
|
|
150
|
-
function cmdAdd(values, name) {
|
|
151
|
-
|
|
152
|
+
async function cmdAdd(values, name) {
|
|
153
|
+
let promptSession = null;
|
|
154
|
+
const ask = (question, opts) => {
|
|
155
|
+
promptSession ??= createPromptSession();
|
|
156
|
+
return promptSession.ask(question, opts);
|
|
157
|
+
};
|
|
152
158
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
159
|
+
try {
|
|
160
|
+
const profileName = name || await ask('Profile name');
|
|
161
|
+
if (!profileName) fail('Profile name is required.');
|
|
162
|
+
|
|
163
|
+
let updates = {};
|
|
164
|
+
if (values['from-current']) {
|
|
165
|
+
const cfg = loadConfigWithSource().config; // whatever is in effect right now
|
|
166
|
+
for (const k of ['baseUrl', 'instance', 'clientId', 'clientSecret', 'accessToken', 'refreshToken', 'expiresAt', 'refreshTokenExpiresAt']) {
|
|
167
|
+
if (cfg[k] != null) updates[k] = cfg[k];
|
|
168
|
+
}
|
|
169
|
+
if (!updates.baseUrl) fail('Nothing to snapshot: no credentials are currently in effect.');
|
|
170
|
+
} else {
|
|
171
|
+
if (values['base-url']) updates.baseUrl = values['base-url'];
|
|
172
|
+
if (values['client-id']) updates.clientId = values['client-id'];
|
|
173
|
+
if (values.secret) updates.clientSecret = values.secret;
|
|
174
|
+
|
|
175
|
+
if (Object.keys(updates).length === 0) {
|
|
176
|
+
updates = await promptProfileCredentials(profileName, ask);
|
|
177
|
+
}
|
|
166
178
|
}
|
|
167
|
-
}
|
|
168
179
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
180
|
+
const existed = Boolean(getProfile(profileName));
|
|
181
|
+
upsertProfile(profileName, updates);
|
|
182
|
+
success(`${existed ? 'Updated' : 'Created'} profile "${profileName}".`);
|
|
183
|
+
if (!updates.accessToken) {
|
|
184
|
+
info(`Finish authenticating with: zeyos login --profile ${profileName}`);
|
|
185
|
+
}
|
|
186
|
+
} finally {
|
|
187
|
+
promptSession?.close();
|
|
174
188
|
}
|
|
175
189
|
}
|
|
176
190
|
|
|
@@ -209,3 +223,84 @@ function fail(message) {
|
|
|
209
223
|
error(message);
|
|
210
224
|
process.exit(1);
|
|
211
225
|
}
|
|
226
|
+
|
|
227
|
+
async function promptProfileCredentials(name, ask) {
|
|
228
|
+
const existing = getProfile(name) || {};
|
|
229
|
+
|
|
230
|
+
info(`Creating profile "${name}".`);
|
|
231
|
+
info('This stores the platform and OAuth app credentials; tokens are added by login.');
|
|
232
|
+
console.error('');
|
|
233
|
+
|
|
234
|
+
const baseUrl = await ask('ZeyOS platform URL', { currentValue: existing.baseUrl });
|
|
235
|
+
const clientId = await ask('Application ID', { currentValue: existing.clientId });
|
|
236
|
+
const clientSecret = await ask('Application secret', { currentValue: existing.clientSecret, secret: true });
|
|
237
|
+
|
|
238
|
+
if (!baseUrl || !clientId || !clientSecret) {
|
|
239
|
+
fail('ZeyOS URL, application ID and secret are all required.');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return { baseUrl, clientId, clientSecret };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function createPromptSession() {
|
|
246
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: process.stdin.isTTY && process.stderr.isTTY });
|
|
247
|
+
const originalWrite = rl._writeToOutput.bind(rl);
|
|
248
|
+
let hiddenPrompt = null;
|
|
249
|
+
let closed = false;
|
|
250
|
+
const queuedLines = [];
|
|
251
|
+
const waitingResolvers = [];
|
|
252
|
+
|
|
253
|
+
rl.on('line', (line) => {
|
|
254
|
+
const resolve = waitingResolvers.shift();
|
|
255
|
+
if (resolve) {
|
|
256
|
+
resolve(line);
|
|
257
|
+
} else {
|
|
258
|
+
queuedLines.push(line);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
rl.on('close', () => {
|
|
263
|
+
closed = true;
|
|
264
|
+
let resolve;
|
|
265
|
+
while ((resolve = waitingResolvers.shift())) {
|
|
266
|
+
resolve('');
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
rl._writeToOutput = (value) => {
|
|
271
|
+
if (!hiddenPrompt || String(value).includes(hiddenPrompt) || value === '\n' || value === '\r\n') {
|
|
272
|
+
originalWrite(value);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const readLine = () => {
|
|
277
|
+
if (queuedLines.length) {
|
|
278
|
+
return Promise.resolve(queuedLines.shift());
|
|
279
|
+
}
|
|
280
|
+
if (closed) {
|
|
281
|
+
return Promise.resolve('');
|
|
282
|
+
}
|
|
283
|
+
return new Promise(resolve => {
|
|
284
|
+
waitingResolvers.push(resolve);
|
|
285
|
+
});
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
ask(question, opts = {}) {
|
|
290
|
+
const currentValue = opts.currentValue || '';
|
|
291
|
+
const defaultLabel = opts.secret && currentValue ? 'stored, press Enter to keep' : currentValue;
|
|
292
|
+
const prompt = defaultLabel ? `${question} [${defaultLabel}]` : question;
|
|
293
|
+
hiddenPrompt = opts.secret && process.stdin.isTTY && process.stderr.isTTY ? prompt : null;
|
|
294
|
+
process.stderr.write(`${prompt}: `);
|
|
295
|
+
|
|
296
|
+
return readLine()
|
|
297
|
+
.then(answer => {
|
|
298
|
+
hiddenPrompt = null;
|
|
299
|
+
return answer.trim() || currentValue || '';
|
|
300
|
+
});
|
|
301
|
+
},
|
|
302
|
+
close() {
|
|
303
|
+
rl.close();
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
}
|
package/commands/whoami.mjs
CHANGED
|
@@ -9,8 +9,11 @@
|
|
|
9
9
|
* --show-token Include the current access token in output
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { createInterface } from 'node:readline';
|
|
12
13
|
import { buildClient, syncTokens } from '../lib/client.mjs';
|
|
14
|
+
import { globalConfigPath, profilesConfigPath } from '../lib/config.mjs';
|
|
13
15
|
import { outputMode, printJson, printYaml, printRecord, formatDate, error } from '../lib/output.mjs';
|
|
16
|
+
import { run as runLogin } from './login.mjs';
|
|
14
17
|
|
|
15
18
|
export const USAGE = `\
|
|
16
19
|
Usage: zeyos whoami [options]
|
|
@@ -25,35 +28,23 @@ Options:
|
|
|
25
28
|
`;
|
|
26
29
|
|
|
27
30
|
export async function run(values) {
|
|
28
|
-
let
|
|
29
|
-
try {
|
|
30
|
-
({ client, config, tokenStore, configSource } = buildClient({}, { profile: values.profile }));
|
|
31
|
-
} catch (err) {
|
|
32
|
-
error(err.message);
|
|
33
|
-
process.exit(1);
|
|
34
|
-
}
|
|
31
|
+
let state = _buildClientState(values);
|
|
35
32
|
|
|
36
33
|
let userInfo;
|
|
37
34
|
try {
|
|
38
|
-
userInfo = await
|
|
39
|
-
await syncTokens(tokenStore, configSource);
|
|
35
|
+
userInfo = await _fetchUserInfo(state);
|
|
40
36
|
} catch (err) {
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
}
|
|
49
|
-
process.exit(1);
|
|
37
|
+
const handled = await _handleFetchError(err, state, values);
|
|
38
|
+
if (!handled) process.exit(1);
|
|
39
|
+
state = handled.state;
|
|
40
|
+
userInfo = handled.userInfo;
|
|
50
41
|
}
|
|
51
42
|
|
|
52
43
|
const mode = outputMode(values);
|
|
53
44
|
|
|
54
45
|
const output = { ...userInfo };
|
|
55
46
|
if (values['show-token']) {
|
|
56
|
-
const tokenSet = await tokenStore.get();
|
|
47
|
+
const tokenSet = await state.tokenStore.get();
|
|
57
48
|
if (tokenSet?.accessToken) output.accessToken = tokenSet.accessToken;
|
|
58
49
|
}
|
|
59
50
|
|
|
@@ -63,7 +54,7 @@ export async function run(values) {
|
|
|
63
54
|
printYaml(output);
|
|
64
55
|
} else {
|
|
65
56
|
// Pretty key-value record with custom formatters
|
|
66
|
-
const dateFormat = config.dateFormat ?? 'YYYY-MM-DD HH:mm';
|
|
57
|
+
const dateFormat = state.config.dateFormat ?? 'YYYY-MM-DD HH:mm';
|
|
67
58
|
const keys = Object.keys(output);
|
|
68
59
|
const formatters = {};
|
|
69
60
|
|
|
@@ -86,6 +77,56 @@ export async function run(values) {
|
|
|
86
77
|
}
|
|
87
78
|
}
|
|
88
79
|
|
|
80
|
+
function _buildClientState(values) {
|
|
81
|
+
try {
|
|
82
|
+
const state = buildClient({}, { profile: values.profile });
|
|
83
|
+
return {
|
|
84
|
+
client: state.client,
|
|
85
|
+
config: state.config,
|
|
86
|
+
tokenStore: state.tokenStore,
|
|
87
|
+
configSource: state.configSource
|
|
88
|
+
};
|
|
89
|
+
} catch (err) {
|
|
90
|
+
error(err.message);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function _fetchUserInfo(state) {
|
|
96
|
+
const userInfo = await state.client.oauth2.getUserInfo();
|
|
97
|
+
await syncTokens(state.tokenStore, state.configSource);
|
|
98
|
+
return userInfo;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function _handleFetchError(err, state, values) {
|
|
102
|
+
const status = err?.status;
|
|
103
|
+
if (status === 502 || status === 503 || status === 504) {
|
|
104
|
+
error(`ZeyOS instance is temporarily unavailable (HTTP ${status}). The server at ${state.config.baseUrl} may be down or restarting — this is server-side, not your credentials.`);
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const authFailure = _authFailureSummary(err);
|
|
109
|
+
if (!authFailure) {
|
|
110
|
+
error(`Failed to fetch user info: ${err.message}`);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
error(_formatAuthFailure(authFailure, err, state.config, state.configSource, values));
|
|
115
|
+
const reauthenticated = await _maybeReauthenticate(state.configSource, values);
|
|
116
|
+
if (!reauthenticated) return null;
|
|
117
|
+
|
|
118
|
+
const nextState = _buildClientState(values);
|
|
119
|
+
try {
|
|
120
|
+
return {
|
|
121
|
+
state: nextState,
|
|
122
|
+
userInfo: await _fetchUserInfo(nextState)
|
|
123
|
+
};
|
|
124
|
+
} catch (retryErr) {
|
|
125
|
+
error(`Re-authentication completed, but fetching user info still failed: ${retryErr.message}`);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
89
130
|
/**
|
|
90
131
|
* Format an array of objects as a multi-line list.
|
|
91
132
|
* Each item is shown as "name (rw)" or "name (ro)" on its own line.
|
|
@@ -105,3 +146,145 @@ function _formatObjectList(items, nameKey, writableKey) {
|
|
|
105
146
|
})
|
|
106
147
|
.join('\n');
|
|
107
148
|
}
|
|
149
|
+
|
|
150
|
+
function _isInvalidRefreshTokenError(err) {
|
|
151
|
+
const detail = `${err?.message ?? ''}\n${_stringifyErrorBody(err?.body)}`;
|
|
152
|
+
return [400, 401, 403].includes(err?.status) &&
|
|
153
|
+
/refresh[_ -]?token|invalid_grant/i.test(detail) &&
|
|
154
|
+
/invalid|expired|forbidden|invalid_grant/i.test(detail);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function _authFailureSummary(err) {
|
|
158
|
+
if (_isInvalidRefreshTokenError(err)) {
|
|
159
|
+
return 'Your stored refresh token is invalid or expired.';
|
|
160
|
+
}
|
|
161
|
+
if (err?.status === 401) {
|
|
162
|
+
return 'Your session has expired or is invalid.';
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function _formatAuthFailure(summary, err, config, source, values) {
|
|
168
|
+
const lines = [
|
|
169
|
+
summary,
|
|
170
|
+
`Platform URL: ${config.baseUrl ?? '(not configured)'}`,
|
|
171
|
+
`Credential source: ${_describeConfigSource(source)}`
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
if (err?.url) {
|
|
175
|
+
lines.push(`OAuth endpoint: ${err.url}`);
|
|
176
|
+
}
|
|
177
|
+
if (err?.status) {
|
|
178
|
+
lines.push(`HTTP status: ${err.status}${err.statusText ? ` ${err.statusText}` : ''}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const detail = _authErrorDetail(err);
|
|
182
|
+
if (detail) {
|
|
183
|
+
lines.push(`OAuth error: ${detail}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
lines.push(`Next step: ${_loginCommand(source, values)}`);
|
|
187
|
+
if (process.env.ZEYOS_TOKEN || process.env.ZEYOS_REFRESH_TOKEN) {
|
|
188
|
+
lines.push('Note: ZEYOS_TOKEN or ZEYOS_REFRESH_TOKEN is set and overrides stored credentials; update or unset it before retrying.');
|
|
189
|
+
}
|
|
190
|
+
return lines.join('\n');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function _describeConfigSource(source) {
|
|
194
|
+
if (!source) {
|
|
195
|
+
return 'environment variables';
|
|
196
|
+
}
|
|
197
|
+
if (source.kind === 'profile') {
|
|
198
|
+
return `profile "${source.name}" (${profilesConfigPath()})`;
|
|
199
|
+
}
|
|
200
|
+
if (source.kind === 'global') {
|
|
201
|
+
return `global credentials (${globalConfigPath()})`;
|
|
202
|
+
}
|
|
203
|
+
if (source.kind === 'local') {
|
|
204
|
+
return `local file ${source.path ?? '.zeyos/auth.json'}`;
|
|
205
|
+
}
|
|
206
|
+
return source.kind ?? 'unknown';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function _loginCommand(source, values) {
|
|
210
|
+
const profile = values.profile ?? (source?.kind === 'profile' ? source.name : null);
|
|
211
|
+
if (profile) {
|
|
212
|
+
return `zeyos login --profile ${_quoteArg(profile)} --force`;
|
|
213
|
+
}
|
|
214
|
+
if (source?.kind === 'global') {
|
|
215
|
+
return 'zeyos login --global --force';
|
|
216
|
+
}
|
|
217
|
+
return 'zeyos login --force';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function _maybeReauthenticate(source, values) {
|
|
221
|
+
if (!_canPromptForReauthentication(source, values)) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const command = _loginCommand(source, values);
|
|
226
|
+
const confirmed = await _confirm(`Re-authenticate now (${command})? [y/N] `);
|
|
227
|
+
if (!confirmed) return false;
|
|
228
|
+
|
|
229
|
+
await runLogin(_loginValues(source, values));
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function _canPromptForReauthentication(source, values) {
|
|
234
|
+
return Boolean(source) &&
|
|
235
|
+
!values.json &&
|
|
236
|
+
!values.yaml &&
|
|
237
|
+
process.stdin.isTTY &&
|
|
238
|
+
process.stderr.isTTY &&
|
|
239
|
+
!process.env.ZEYOS_TOKEN &&
|
|
240
|
+
!process.env.ZEYOS_REFRESH_TOKEN;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function _loginValues(source, values) {
|
|
244
|
+
return {
|
|
245
|
+
...values,
|
|
246
|
+
force: true,
|
|
247
|
+
profile: values.profile ?? (source?.kind === 'profile' ? source.name : undefined),
|
|
248
|
+
global: source?.kind === 'global' ? true : values.global
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function _confirm(prompt) {
|
|
253
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
254
|
+
return new Promise(resolve => {
|
|
255
|
+
rl.question(prompt, answer => {
|
|
256
|
+
rl.close();
|
|
257
|
+
resolve(answer.trim().toLowerCase() === 'y');
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function _authErrorDetail(err) {
|
|
263
|
+
const body = _stringifyErrorBody(err?.body).trim();
|
|
264
|
+
if (body) {
|
|
265
|
+
return body;
|
|
266
|
+
}
|
|
267
|
+
return String(err?.message ?? '').trim();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function _stringifyErrorBody(body) {
|
|
271
|
+
if (body == null) {
|
|
272
|
+
return '';
|
|
273
|
+
}
|
|
274
|
+
if (typeof body === 'string') {
|
|
275
|
+
return body;
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
return JSON.stringify(body);
|
|
279
|
+
} catch {
|
|
280
|
+
return String(body);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function _quoteArg(value) {
|
|
285
|
+
const text = String(value);
|
|
286
|
+
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(text)) {
|
|
287
|
+
return text;
|
|
288
|
+
}
|
|
289
|
+
return `'${text.replaceAll("'", "'\\''")}'`;
|
|
290
|
+
}
|
package/lib/config.mjs
CHANGED
|
@@ -187,6 +187,21 @@ export function clearTokens(scope = 'local') {
|
|
|
187
187
|
if (path) _writeJson(path, _stripTokens(_readJson(path)));
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
/** Remove all credential/session fields from the resolved legacy local auth file. */
|
|
191
|
+
export function clearLocalCredentialsForSource(source) {
|
|
192
|
+
if (!source || source.kind !== 'local') return false;
|
|
193
|
+
const path = source.path ?? _findLocalPath();
|
|
194
|
+
if (!path) return false;
|
|
195
|
+
|
|
196
|
+
const current = _readJson(path);
|
|
197
|
+
const hadCredentials = CRED_KEYS.some((key) => Object.prototype.hasOwnProperty.call(current, key));
|
|
198
|
+
if (!hadCredentials) return false;
|
|
199
|
+
|
|
200
|
+
const next = _stripCredentials(current);
|
|
201
|
+
_writeJson(path, next);
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
|
|
190
205
|
/**
|
|
191
206
|
* Persist refreshed tokens back to wherever the active credentials came from.
|
|
192
207
|
* @param {ConfigSource|null} source
|
|
@@ -325,6 +340,11 @@ export function localConfigPath() { return _findLocalPath(); }
|
|
|
325
340
|
export function globalConfigPath() { return GLOBAL_FILE; }
|
|
326
341
|
export function profilesConfigPath() { return PROFILES_FILE; }
|
|
327
342
|
|
|
343
|
+
/** Read the legacy global credentials file directly, without applying the cascade. */
|
|
344
|
+
export function loadGlobalConfig() {
|
|
345
|
+
return _readGlobal();
|
|
346
|
+
}
|
|
347
|
+
|
|
328
348
|
// ── Internals ────────────────────────────────────────────────────────────────
|
|
329
349
|
|
|
330
350
|
function _fromEnv() {
|
|
@@ -368,6 +388,14 @@ function _stripTokens(o) {
|
|
|
368
388
|
return rest;
|
|
369
389
|
}
|
|
370
390
|
|
|
391
|
+
function _stripCredentials(o) {
|
|
392
|
+
const out = { ...o };
|
|
393
|
+
for (const key of CRED_KEYS) {
|
|
394
|
+
delete out[key];
|
|
395
|
+
}
|
|
396
|
+
return out;
|
|
397
|
+
}
|
|
398
|
+
|
|
371
399
|
function _readGlobal() {
|
|
372
400
|
return existsSync(GLOBAL_FILE) ? _readJson(GLOBAL_FILE) : {};
|
|
373
401
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeyos/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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.5.0"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
45
|
"test": "node --test test/offline.mjs"
|