@zeyos/cli 0.1.1 → 0.2.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 +9 -0
- package/bin/zeyos.mjs +129 -27
- package/commands/count.mjs +15 -7
- package/commands/create.mjs +11 -4
- package/commands/delete.mjs +8 -3
- package/commands/describe.mjs +22 -1
- package/commands/doctor.mjs +186 -0
- package/commands/get.mjs +16 -3
- package/commands/list.mjs +34 -11
- package/commands/skills.mjs +3 -2
- package/commands/update.mjs +10 -3
- package/lib/command.mjs +77 -2
- package/lib/flags.mjs +3 -3
- package/lib/output.mjs +194 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -52,6 +52,7 @@ Inspect the CLI-supported resource registry:
|
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
54
|
zeyos resources
|
|
55
|
+
zeyos doctor agent --json
|
|
55
56
|
```
|
|
56
57
|
|
|
57
58
|
List tickets for automation:
|
|
@@ -65,11 +66,19 @@ zeyos list tickets \
|
|
|
65
66
|
--json
|
|
66
67
|
```
|
|
67
68
|
|
|
69
|
+
For larger or reusable filters, put the JSON in a file:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
zeyos list tickets --filter-file ./filters/open-tickets.json --json
|
|
73
|
+
```
|
|
74
|
+
|
|
68
75
|
Create, update, and delete:
|
|
69
76
|
|
|
70
77
|
```bash
|
|
71
78
|
zeyos create ticket --data '{"name":"Fix login bug","status":0,"priority":3,"visibility":0}' --json
|
|
79
|
+
zeyos create ticket --data-file ./ticket.json --json
|
|
72
80
|
zeyos update ticket 42 --data '{"status":4}' --json
|
|
81
|
+
zeyos update ticket 42 --data-file ./ticket-update.json --json
|
|
73
82
|
zeyos delete ticket 42
|
|
74
83
|
```
|
|
75
84
|
|
package/bin/zeyos.mjs
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* update <resource> Update a record
|
|
17
17
|
* delete <resource> Delete a record
|
|
18
18
|
* resources List available resource types
|
|
19
|
+
* doctor agent Check local CLI readiness for coding agents
|
|
19
20
|
*/
|
|
20
21
|
|
|
21
22
|
// ── Version ───────────────────────────────────────────────────────────────────
|
|
@@ -23,45 +24,52 @@
|
|
|
23
24
|
import { createRequire as _createRequire } from 'node:module';
|
|
24
25
|
import { dirname as _dirname } from 'node:path';
|
|
25
26
|
import { fileURLToPath as _fileURLToPath } from 'node:url';
|
|
27
|
+
import { colors as _c } from '../lib/output.mjs';
|
|
26
28
|
const _require = _createRequire(import.meta.url);
|
|
27
29
|
const _VERSION = _require('../package.json').version;
|
|
28
30
|
|
|
29
31
|
// ── Global help ───────────────────────────────────────────────────────────────
|
|
30
32
|
|
|
33
|
+
// Section headers are bold and the `zeyos` binary / command names are cyan,
|
|
34
|
+
// gated by USE_COLOR in output.mjs (so `zeyos --help | less` stays plain text).
|
|
35
|
+
const _z = _c.cyan('zeyos');
|
|
31
36
|
const HELP = `\
|
|
32
|
-
Usage:
|
|
33
|
-
|
|
34
|
-
Commands:
|
|
35
|
-
login Authenticate with a ZeyOS instance
|
|
36
|
-
logout Revoke session and clear stored tokens
|
|
37
|
-
whoami Show currently authenticated user
|
|
38
|
-
list <resource> List / query records
|
|
39
|
-
count <resource> Count records (with optional filter)
|
|
40
|
-
get <resource> <id> Fetch a single record by ID
|
|
41
|
-
show <resource> <id> Alias for get
|
|
42
|
-
create <resource> Create a new record
|
|
43
|
-
update <resource> <id> Update an existing record
|
|
44
|
-
delete <resource> <id> Delete a record
|
|
45
|
-
resources List all available resource types
|
|
46
|
-
describe <resource> Show a resource's fields, types and enums
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
37
|
+
Usage: ${_z} <command> [options] [args…]
|
|
38
|
+
|
|
39
|
+
${_c.bold('Commands:')}
|
|
40
|
+
${_c.cyan('login')} Authenticate with a ZeyOS instance
|
|
41
|
+
${_c.cyan('logout')} Revoke session and clear stored tokens
|
|
42
|
+
${_c.cyan('whoami')} Show currently authenticated user
|
|
43
|
+
${_c.cyan('list')} <resource> List / query records
|
|
44
|
+
${_c.cyan('count')} <resource> Count records (with optional filter)
|
|
45
|
+
${_c.cyan('get')} <resource> <id> Fetch a single record by ID
|
|
46
|
+
${_c.cyan('show')} <resource> <id> Alias for get
|
|
47
|
+
${_c.cyan('create')} <resource> Create a new record
|
|
48
|
+
${_c.cyan('update')} <resource> <id> Update an existing record
|
|
49
|
+
${_c.cyan('delete')} <resource> <id> Delete a record
|
|
50
|
+
${_c.cyan('resources')} List all available resource types
|
|
51
|
+
${_c.cyan('describe')} <resource> Show a resource's fields, types and enums
|
|
52
|
+
${_c.cyan('doctor')} agent Check local CLI readiness for coding agents
|
|
53
|
+
${_c.cyan('skills')} <command> List / show / install ZeyOS agent skills
|
|
54
|
+
|
|
55
|
+
${_c.bold('Global options:')}
|
|
50
56
|
--json Output as JSON
|
|
51
57
|
--yaml Output as YAML
|
|
58
|
+
--query Print the API route + JSON payload without sending it
|
|
52
59
|
--no-color Disable ANSI colors
|
|
53
60
|
-h, --help Show help for a command
|
|
54
61
|
-v, --version Print the CLI version and exit
|
|
55
62
|
|
|
56
|
-
Examples:
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
${_c.bold('Examples:')}
|
|
64
|
+
${_z} login --base-url https://cloud.zeyos.com/demo --client-id myapp --secret "$ZEYOS_CLIENT_SECRET"
|
|
65
|
+
${_z} list tickets --filter '{"status":1}' --sort -lastmodified
|
|
66
|
+
${_z} list tickets --filter-file ./filters/open-tickets.json
|
|
67
|
+
${_z} count tickets --filter '{"status":1}'
|
|
68
|
+
${_z} get ticket 42
|
|
69
|
+
${_z} get ticket 42 --all
|
|
70
|
+
${_z} create ticket --name "Fix login bug" --priority 3
|
|
71
|
+
${_z} update ticket 42 --status 2
|
|
72
|
+
${_z} delete ticket 42 --force
|
|
65
73
|
`;
|
|
66
74
|
|
|
67
75
|
// ── Argument definitions ──────────────────────────────────────────────────────
|
|
@@ -73,6 +81,7 @@ const OPTIONS = {
|
|
|
73
81
|
'json': { type: 'boolean' },
|
|
74
82
|
'yaml': { type: 'boolean' },
|
|
75
83
|
'no-color': { type: 'boolean' },
|
|
84
|
+
'query': { type: 'boolean' },
|
|
76
85
|
// login
|
|
77
86
|
'base-url': { type: 'string' },
|
|
78
87
|
'client-id': { type: 'string' },
|
|
@@ -88,6 +97,7 @@ const OPTIONS = {
|
|
|
88
97
|
// list
|
|
89
98
|
'fields': { type: 'string' },
|
|
90
99
|
'filter': { type: 'string' },
|
|
100
|
+
'filter-file': { type: 'string' },
|
|
91
101
|
'sort': { type: 'string' },
|
|
92
102
|
'limit': { type: 'string' },
|
|
93
103
|
'offset': { type: 'string' },
|
|
@@ -100,6 +110,7 @@ const OPTIONS = {
|
|
|
100
110
|
'show-token': { type: 'boolean' },
|
|
101
111
|
// create / update
|
|
102
112
|
'data': { type: 'string' },
|
|
113
|
+
'data-file': { type: 'string' },
|
|
103
114
|
// delete
|
|
104
115
|
// (--force is already declared above)
|
|
105
116
|
// skills install
|
|
@@ -128,10 +139,43 @@ const COMMANDS = {
|
|
|
128
139
|
resources: '../commands/resources.mjs',
|
|
129
140
|
resource: '../commands/resources.mjs',
|
|
130
141
|
describe: '../commands/describe.mjs',
|
|
142
|
+
doctor: '../commands/doctor.mjs',
|
|
131
143
|
skills: '../commands/skills.mjs',
|
|
132
144
|
skill: '../commands/skills.mjs',
|
|
133
145
|
};
|
|
134
146
|
|
|
147
|
+
// ── Per-command flag allow-lists ────────────────────────────────────────────────
|
|
148
|
+
// Unknown flags are rejected (e.g. `zeyos list --invalid`) so typos surface
|
|
149
|
+
// immediately instead of being silently ignored. `create`/`update` are the
|
|
150
|
+
// exception: they accept arbitrary `--<field>` flags, marked with `null` below.
|
|
151
|
+
|
|
152
|
+
const ALWAYS_FLAGS = ['help', 'json', 'yaml', 'no-color'];
|
|
153
|
+
const SKILLS_FLAGS = ['target', 'dir', 'global', 'local', 'force', 'yes', 'no-logo'];
|
|
154
|
+
const DELETE_FLAGS = ['force', 'query'];
|
|
155
|
+
const GET_FLAGS = ['fields', 'extdata', 'tags', 'expand', 'all', 'query'];
|
|
156
|
+
|
|
157
|
+
const COMMAND_FLAGS = {
|
|
158
|
+
login: ['base-url', 'client-id', 'secret', 'scope', 'port', 'global', 'force', 'clean', 'manual'],
|
|
159
|
+
logout: ['global'],
|
|
160
|
+
whoami: ['show-token'],
|
|
161
|
+
list: ['fields', 'filter', 'filter-file', 'sort', 'limit', 'offset', 'extdata', 'expand', 'query'],
|
|
162
|
+
count: ['filter', 'filter-file', 'query'],
|
|
163
|
+
get: GET_FLAGS,
|
|
164
|
+
show: GET_FLAGS,
|
|
165
|
+
create: null,
|
|
166
|
+
update: null,
|
|
167
|
+
edit: null,
|
|
168
|
+
delete: DELETE_FLAGS,
|
|
169
|
+
rm: DELETE_FLAGS,
|
|
170
|
+
remove: DELETE_FLAGS,
|
|
171
|
+
resources: [],
|
|
172
|
+
resource: [],
|
|
173
|
+
describe: [],
|
|
174
|
+
doctor: [],
|
|
175
|
+
skills: SKILLS_FLAGS,
|
|
176
|
+
skill: SKILLS_FLAGS,
|
|
177
|
+
};
|
|
178
|
+
|
|
135
179
|
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
136
180
|
|
|
137
181
|
async function main() {
|
|
@@ -149,6 +193,14 @@ async function main() {
|
|
|
149
193
|
}
|
|
150
194
|
|
|
151
195
|
const command = argv[0];
|
|
196
|
+
|
|
197
|
+
// A leading flag (e.g. `zeyos --invalid`) is not a command — surface it as a
|
|
198
|
+
// bad option rather than letting it masquerade as one.
|
|
199
|
+
if (command.startsWith('-')) {
|
|
200
|
+
process.stderr.write(`Unknown option: "${command}". Run 'zeyos --help' for usage.\n`);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
|
|
152
204
|
const rest = argv.slice(1);
|
|
153
205
|
|
|
154
206
|
// Parse remaining args permissively: known options are parsed normally and
|
|
@@ -168,6 +220,24 @@ async function main() {
|
|
|
168
220
|
process.exit(0);
|
|
169
221
|
}
|
|
170
222
|
|
|
223
|
+
// Reject unknown flags so typos / unsupported options fail loudly instead of
|
|
224
|
+
// being silently ignored. `create`/`update` opt out (COMMAND_FLAGS = null)
|
|
225
|
+
// because they accept arbitrary `--<field>` flags as record data.
|
|
226
|
+
const allowed = COMMAND_FLAGS[command];
|
|
227
|
+
if (allowed) {
|
|
228
|
+
const allowedSet = new Set([...ALWAYS_FLAGS, ...allowed]);
|
|
229
|
+
const unknown = Object.keys(values).filter((key) => !allowedSet.has(key));
|
|
230
|
+
if (unknown.length > 0) {
|
|
231
|
+
const flag = unknown[0];
|
|
232
|
+
const hint = _suggestFlag(flag, [...allowedSet]);
|
|
233
|
+
process.stderr.write(
|
|
234
|
+
`Unknown option: --${flag}${hint ? ` (did you mean --${hint}?)` : ''}\n\n` +
|
|
235
|
+
`Run 'zeyos ${command} --help' for available options.\n`
|
|
236
|
+
);
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
171
241
|
await mod.run(values, positional);
|
|
172
242
|
}
|
|
173
243
|
|
|
@@ -274,6 +344,38 @@ function _parsePermissive(argv, options) {
|
|
|
274
344
|
return { values, positional };
|
|
275
345
|
}
|
|
276
346
|
|
|
347
|
+
/** Suggest the closest allowed flag for an unknown one, if it's a near miss. */
|
|
348
|
+
function _suggestFlag(input, candidates) {
|
|
349
|
+
let best = null;
|
|
350
|
+
let bestDist = Infinity;
|
|
351
|
+
for (const candidate of candidates) {
|
|
352
|
+
const dist = _levenshtein(input, candidate);
|
|
353
|
+
if (dist < bestDist) {
|
|
354
|
+
bestDist = dist;
|
|
355
|
+
best = candidate;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// Only suggest a reasonably close match (avoid nonsense "did you mean").
|
|
359
|
+
return bestDist <= Math.max(2, Math.floor(input.length / 2)) ? best : null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** Levenshtein edit distance between two short strings. */
|
|
363
|
+
function _levenshtein(a, b) {
|
|
364
|
+
const m = a.length;
|
|
365
|
+
const n = b.length;
|
|
366
|
+
const dp = Array.from({ length: m + 1 }, (_, i) => i);
|
|
367
|
+
for (let j = 1; j <= n; j++) {
|
|
368
|
+
let prev = dp[0];
|
|
369
|
+
dp[0] = j;
|
|
370
|
+
for (let i = 1; i <= m; i++) {
|
|
371
|
+
const tmp = dp[i];
|
|
372
|
+
dp[i] = a[i - 1] === b[j - 1] ? prev : 1 + Math.min(prev, dp[i], dp[i - 1]);
|
|
373
|
+
prev = tmp;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return dp[m];
|
|
377
|
+
}
|
|
378
|
+
|
|
277
379
|
main().catch(err => {
|
|
278
380
|
process.stderr.write(`Fatal: ${err.message}\n`);
|
|
279
381
|
process.exit(1);
|
package/commands/count.mjs
CHANGED
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
* Return the count of records matching an optional filter.
|
|
5
5
|
*
|
|
6
6
|
* Options:
|
|
7
|
-
* --filter <json>
|
|
8
|
-
* --
|
|
9
|
-
* --
|
|
7
|
+
* --filter <json> JSON filter object e.g. '{"status":1}'
|
|
8
|
+
* --filter-file <path> Read JSON filter object from a file
|
|
9
|
+
* --json Output as JSON
|
|
10
|
+
* --yaml Output as YAML
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import { normalizeCountResult } from '@zeyos/client';
|
|
13
|
-
import { buildCliClient, callApi,
|
|
14
|
+
import { buildCliClient, callApi, maybeDryRun, parseJsonOptionOrFile, requireResource } from '../lib/command.mjs';
|
|
14
15
|
import { outputMode, printJson, printYaml } from '../lib/output.mjs';
|
|
15
16
|
|
|
16
17
|
export const USAGE = `\
|
|
@@ -23,29 +24,36 @@ Arguments:
|
|
|
23
24
|
|
|
24
25
|
Options:
|
|
25
26
|
--filter <json> JSON filter object e.g. '{"status":1}'
|
|
27
|
+
--filter-file <path>
|
|
28
|
+
Read JSON filter object from a file
|
|
26
29
|
--json Output as JSON ({ "count": N })
|
|
27
30
|
--yaml Output as YAML
|
|
31
|
+
--query Print the request route + JSON body without sending it
|
|
28
32
|
-h, --help Show this help
|
|
29
33
|
|
|
30
34
|
Examples:
|
|
31
35
|
zeyos count tickets
|
|
32
36
|
zeyos count tickets --filter '{"status":1}'
|
|
37
|
+
zeyos count tickets --filter-file ./filters/open-tickets.json
|
|
33
38
|
zeyos count accounts --json
|
|
34
39
|
`;
|
|
35
40
|
|
|
36
41
|
export async function run(values, positional) {
|
|
37
42
|
const resourceName = positional[0];
|
|
38
43
|
const res = requireResource(resourceName, 'zeyos count <resource>');
|
|
39
|
-
const clientState = buildCliClient();
|
|
40
44
|
|
|
41
45
|
// ── Build request body ─────────────────────────────────────────────────────
|
|
42
46
|
const body = { count: true };
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
const filters = parseJsonOptionOrFile(values, 'filter', 'filter-file');
|
|
49
|
+
if (filters !== undefined) {
|
|
50
|
+
body.filters = filters;
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
// ── Call API ───────────────────────────────────────────────────────────────
|
|
54
|
+
const clientState = buildCliClient();
|
|
55
|
+
if (await maybeDryRun(clientState, res.list, body, values)) return;
|
|
56
|
+
|
|
49
57
|
const result = await callApi(clientState, res.list, body);
|
|
50
58
|
|
|
51
59
|
const count = normalizeCountResult(result);
|
package/commands/create.mjs
CHANGED
|
@@ -3,15 +3,17 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Create a new record. Field values can be supplied either as:
|
|
5
5
|
* - a JSON blob via --data '{"name":"foo","status":1}'
|
|
6
|
+
* - a JSON file via --data-file ./ticket.json
|
|
6
7
|
* - individual --<field> <value> flags (converted automatically)
|
|
7
8
|
*
|
|
8
9
|
* Options:
|
|
9
|
-
* --data <json>
|
|
10
|
-
* --
|
|
11
|
-
* --
|
|
10
|
+
* --data <json> Full record as JSON object
|
|
11
|
+
* --data-file <path> Read full record JSON object from a file
|
|
12
|
+
* --json Output created record as JSON
|
|
13
|
+
* --yaml Output created record as YAML
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
|
-
import { buildCliClient, buildRecordPayload, callApi, requireResource } from '../lib/command.mjs';
|
|
16
|
+
import { buildCliClient, buildRecordPayload, callApi, maybeDryRun, requireResource } from '../lib/command.mjs';
|
|
15
17
|
import { outputMode, printJson, printYaml, printRecord, success } from '../lib/output.mjs';
|
|
16
18
|
|
|
17
19
|
export const USAGE = `\
|
|
@@ -24,14 +26,17 @@ Arguments:
|
|
|
24
26
|
|
|
25
27
|
Options:
|
|
26
28
|
--data <json> Record fields as a JSON object
|
|
29
|
+
--data-file <path> Read record fields as a JSON object from a file
|
|
27
30
|
--<field> <value> Set individual fields e.g. --name "My Ticket" --status 1
|
|
28
31
|
--json Output created record as JSON
|
|
29
32
|
--yaml Output created record as YAML
|
|
33
|
+
--query Print the request route + JSON body without sending it
|
|
30
34
|
-h, --help Show this help
|
|
31
35
|
|
|
32
36
|
Examples:
|
|
33
37
|
zeyos create ticket --name "Fix login bug" --status 0 --priority 2
|
|
34
38
|
zeyos create account --data '{"lastname":"Acme Corp","email":"info@acme.com"}'
|
|
39
|
+
zeyos create ticket --data-file ./ticket.json
|
|
35
40
|
`;
|
|
36
41
|
|
|
37
42
|
export async function run(values, positional) {
|
|
@@ -46,6 +51,8 @@ export async function run(values, positional) {
|
|
|
46
51
|
const clientState = buildCliClient();
|
|
47
52
|
|
|
48
53
|
// ── Call API ───────────────────────────────────────────────────────────────
|
|
54
|
+
if (await maybeDryRun(clientState, res.create, data, values)) return;
|
|
55
|
+
|
|
49
56
|
const record = await callApi(clientState, res.create, data);
|
|
50
57
|
|
|
51
58
|
const mode = outputMode(values);
|
package/commands/delete.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { createInterface } from 'node:readline';
|
|
11
|
-
import { buildCliClient, callApi, requireRecordId, requireResource } from '../lib/command.mjs';
|
|
11
|
+
import { buildCliClient, callApi, maybeDryRun, requireRecordId, requireResource } from '../lib/command.mjs';
|
|
12
12
|
import { success, warn } from '../lib/output.mjs';
|
|
13
13
|
|
|
14
14
|
export const USAGE = `\
|
|
@@ -22,6 +22,7 @@ Arguments:
|
|
|
22
22
|
|
|
23
23
|
Options:
|
|
24
24
|
--force Skip confirmation prompt
|
|
25
|
+
--query Print the request route + JSON body without sending it
|
|
25
26
|
-h, --help Show this help
|
|
26
27
|
|
|
27
28
|
Examples:
|
|
@@ -36,6 +37,12 @@ export async function run(values, positional) {
|
|
|
36
37
|
const res = requireResource(resourceName, 'zeyos delete <resource> <id>', 'delete', 'deletion');
|
|
37
38
|
requireRecordId(id, 'zeyos delete <resource> <id>');
|
|
38
39
|
|
|
40
|
+
const clientState = buildCliClient();
|
|
41
|
+
|
|
42
|
+
// ── Dry run ────────────────────────────────────────────────────────────────
|
|
43
|
+
// Show the request without prompting or deleting anything.
|
|
44
|
+
if (await maybeDryRun(clientState, res.delete, { ID: id }, values)) return;
|
|
45
|
+
|
|
39
46
|
// ── Confirmation ───────────────────────────────────────────────────────────
|
|
40
47
|
if (!values.force) {
|
|
41
48
|
const confirmed = await _confirm(`Delete ${resourceName} #${id}? [y/N] `);
|
|
@@ -45,8 +52,6 @@ export async function run(values, positional) {
|
|
|
45
52
|
}
|
|
46
53
|
}
|
|
47
54
|
|
|
48
|
-
const clientState = buildCliClient();
|
|
49
|
-
|
|
50
55
|
// ── Call API ───────────────────────────────────────────────────────────────
|
|
51
56
|
await callApi(clientState, res.delete, { ID: id }, {
|
|
52
57
|
notFoundMessage: `${resourceName} #${id} not found.`
|
package/commands/describe.mjs
CHANGED
|
@@ -82,12 +82,19 @@ export function run(values, positional = []) {
|
|
|
82
82
|
return;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
// Keep the join-critical flags (→ fk, indexed, enum) in the table, but keep
|
|
86
|
+
// the `enum:` note SHORT so the long value list never blows out the column.
|
|
87
|
+
// The full enum values are printed below the table (see `enumDetails`), so FK
|
|
88
|
+
// and index flags stay legible in-line and the enum codes remain discoverable.
|
|
89
|
+
const enumDetails = [];
|
|
85
90
|
const rows = Object.entries(def.fields).map(([name, field]) => {
|
|
86
91
|
const notes = [];
|
|
87
92
|
if (field.fk) notes.push(`→ ${field.fk}`);
|
|
88
93
|
if (field.indexed) notes.push('indexed');
|
|
89
94
|
if (field.enum) {
|
|
90
|
-
|
|
95
|
+
const count = Object.keys(field.enum).length;
|
|
96
|
+
notes.push(`enum (${count})`);
|
|
97
|
+
enumDetails.push({ name, values: field.enum });
|
|
91
98
|
}
|
|
92
99
|
return { field: name, type: field.type, notes: notes.join(' ') };
|
|
93
100
|
});
|
|
@@ -96,6 +103,20 @@ export function run(values, positional = []) {
|
|
|
96
103
|
|
|
97
104
|
process.stdout.write(`\n ${c.bold(def.name)} ${c.dim(`(${def.type}, ${rows.length} fields)`)}\n`);
|
|
98
105
|
printTable(rows, ['field', 'type', 'notes']);
|
|
106
|
+
|
|
107
|
+
// Full enum values, one field per block, below the table. Each `code = LABEL`
|
|
108
|
+
// pair is on its own line so even long enums (e.g. ticket status) stay readable.
|
|
109
|
+
if (enumDetails.length > 0) {
|
|
110
|
+
process.stdout.write(` ${c.bold('enums')}\n`);
|
|
111
|
+
for (const { name, values } of enumDetails) {
|
|
112
|
+
process.stdout.write(` ${c.cyan(name)}\n`);
|
|
113
|
+
for (const [code, label] of Object.entries(values)) {
|
|
114
|
+
process.stdout.write(` ${c.dim(code.padStart(2))} ${label}\n`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
process.stdout.write('\n');
|
|
118
|
+
}
|
|
119
|
+
|
|
99
120
|
if (operations.length > 0) {
|
|
100
121
|
process.stdout.write(` ${c.bold('operations')} ${c.dim(operations.join(', '))}\n\n`);
|
|
101
122
|
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* zeyos doctor agent
|
|
3
|
+
*
|
|
4
|
+
* Offline diagnostic for coding agents before they rely on the CLI.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createRequire } from 'node:module';
|
|
8
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
9
|
+
import { loadConfigWithSource, localConfigPath, globalConfigPath } from '../lib/config.mjs';
|
|
10
|
+
import { loadResourceConfig } from '../lib/resource-config.mjs';
|
|
11
|
+
import { listResources, resolveResource } from '../lib/resources.mjs';
|
|
12
|
+
import { colors as c, error, outputMode, printJson, printYaml } from '../lib/output.mjs';
|
|
13
|
+
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
const VERSION = require('../package.json').version;
|
|
16
|
+
|
|
17
|
+
const ENV_KEYS = {
|
|
18
|
+
ZEYOS_BASE_URL: 'baseUrl',
|
|
19
|
+
ZEYOS_INSTANCE: 'instance',
|
|
20
|
+
ZEYOS_CLIENT_ID: 'clientId',
|
|
21
|
+
ZEYOS_CLIENT_SECRET: 'clientSecret',
|
|
22
|
+
ZEYOS_TOKEN: 'accessToken',
|
|
23
|
+
ZEYOS_REFRESH_TOKEN: 'refreshToken',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const USAGE = `\
|
|
27
|
+
Usage: zeyos doctor agent [options]
|
|
28
|
+
|
|
29
|
+
Check local CLI readiness for coding agents. Runs offline and never prints
|
|
30
|
+
tokens or client secrets.
|
|
31
|
+
|
|
32
|
+
Options:
|
|
33
|
+
--json Output as JSON
|
|
34
|
+
--yaml Output as YAML
|
|
35
|
+
-h, --help Show this help
|
|
36
|
+
|
|
37
|
+
Examples:
|
|
38
|
+
zeyos doctor agent
|
|
39
|
+
zeyos doctor agent --json
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
export function run(values, positional = []) {
|
|
43
|
+
const subject = positional[0];
|
|
44
|
+
if (subject !== 'agent') {
|
|
45
|
+
error('Unknown doctor target. Usage: zeyos doctor agent');
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const report = buildAgentReport();
|
|
50
|
+
const mode = outputMode(values);
|
|
51
|
+
|
|
52
|
+
if (mode === 'json') {
|
|
53
|
+
printJson(report);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (mode === 'yaml') {
|
|
57
|
+
printYaml(report);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
printAgentReport(report);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildAgentReport() {
|
|
65
|
+
const localPath = localConfigPath();
|
|
66
|
+
const globalPath = globalConfigPath();
|
|
67
|
+
const envVariables = Object.keys(ENV_KEYS).filter((key) => process.env[key]);
|
|
68
|
+
let loaded = { config: {}, source: null };
|
|
69
|
+
let configError = null;
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
loaded = loadConfigWithSource();
|
|
73
|
+
} catch (err) {
|
|
74
|
+
configError = err.message || String(err);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const config = loaded.config;
|
|
78
|
+
const effective = {
|
|
79
|
+
baseUrl: Boolean(config.baseUrl),
|
|
80
|
+
instance: Boolean(config.instance),
|
|
81
|
+
clientId: Boolean(config.clientId),
|
|
82
|
+
clientSecret: Boolean(config.clientSecret),
|
|
83
|
+
accessToken: Boolean(config.accessToken),
|
|
84
|
+
refreshToken: Boolean(config.refreshToken),
|
|
85
|
+
};
|
|
86
|
+
const ready = Boolean(effective.baseUrl && effective.clientId && effective.clientSecret && effective.accessToken);
|
|
87
|
+
const resources = inspectResources();
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
ok: ready && !configError && resources.ok,
|
|
91
|
+
cli: {
|
|
92
|
+
version: VERSION,
|
|
93
|
+
},
|
|
94
|
+
connection: {
|
|
95
|
+
baseUrl: config.baseUrl ?? null,
|
|
96
|
+
instance: config.instance ?? null,
|
|
97
|
+
},
|
|
98
|
+
auth: {
|
|
99
|
+
ready,
|
|
100
|
+
source: envVariables.length > 0 ? 'env' : loaded.source,
|
|
101
|
+
env: {
|
|
102
|
+
present: envVariables.length > 0,
|
|
103
|
+
variables: envVariables,
|
|
104
|
+
},
|
|
105
|
+
local: {
|
|
106
|
+
present: Boolean(localPath),
|
|
107
|
+
path: localPath,
|
|
108
|
+
},
|
|
109
|
+
global: {
|
|
110
|
+
present: existsSync(globalPath),
|
|
111
|
+
path: globalPath,
|
|
112
|
+
},
|
|
113
|
+
effective,
|
|
114
|
+
error: configError,
|
|
115
|
+
},
|
|
116
|
+
resources,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function inspectResources() {
|
|
121
|
+
const names = listResources();
|
|
122
|
+
const missing = [];
|
|
123
|
+
const configErrors = [];
|
|
124
|
+
|
|
125
|
+
for (const name of names) {
|
|
126
|
+
if (!resolveResource(name)) {
|
|
127
|
+
missing.push(name);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
loadResourceConfig(name);
|
|
133
|
+
} catch (err) {
|
|
134
|
+
configErrors.push(err.message || String(err));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
ok: names.length > 0 && missing.length === 0 && configErrors.length === 0,
|
|
140
|
+
count: names.length,
|
|
141
|
+
shippedConfigCount: countShippedResourceConfigs(),
|
|
142
|
+
missing,
|
|
143
|
+
configErrors,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function countShippedResourceConfigs() {
|
|
148
|
+
try {
|
|
149
|
+
return readdirSync(new URL('../config/', import.meta.url))
|
|
150
|
+
.filter((name) => name.endsWith('.json'))
|
|
151
|
+
.length;
|
|
152
|
+
} catch {
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function printAgentReport(report) {
|
|
158
|
+
process.stdout.write('\n');
|
|
159
|
+
process.stdout.write(` ${c.bold('ZeyOS CLI doctor: agent')}\n\n`);
|
|
160
|
+
process.stdout.write(` CLI version ${report.cli.version}\n`);
|
|
161
|
+
process.stdout.write(` Base URL ${report.connection.baseUrl ?? '(not set)'}\n`);
|
|
162
|
+
process.stdout.write(` Instance ${report.connection.instance ?? '(not set)'}\n`);
|
|
163
|
+
process.stdout.write(` Auth ready ${yesNo(report.auth.ready)}\n`);
|
|
164
|
+
process.stdout.write(` Auth source ${report.auth.source ?? '(none)'}\n`);
|
|
165
|
+
process.stdout.write(` Env config ${report.auth.env.present ? report.auth.env.variables.join(', ') : '(none)'}\n`);
|
|
166
|
+
process.stdout.write(` Local config ${report.auth.local.present ? report.auth.local.path : '(none)'}\n`);
|
|
167
|
+
process.stdout.write(` Global config ${report.auth.global.present ? report.auth.global.path : '(none)'}\n`);
|
|
168
|
+
process.stdout.write(` Resource registry ${report.resources.ok ? 'ok' : 'problem'} (${report.resources.count} resources, ${report.resources.shippedConfigCount} shipped configs)\n`);
|
|
169
|
+
|
|
170
|
+
if (report.auth.error) {
|
|
171
|
+
process.stdout.write(`\n ${c.bold('Auth config error')}\n`);
|
|
172
|
+
process.stdout.write(` ${report.auth.error}\n`);
|
|
173
|
+
}
|
|
174
|
+
if (report.resources.configErrors.length > 0) {
|
|
175
|
+
process.stdout.write(`\n ${c.bold('Resource config errors')}\n`);
|
|
176
|
+
for (const message of report.resources.configErrors) {
|
|
177
|
+
process.stdout.write(` ${message}\n`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
process.stdout.write('\n');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function yesNo(value) {
|
|
185
|
+
return value ? 'yes' : 'no';
|
|
186
|
+
}
|
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:
|
|
@@ -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();
|
|
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/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) {
|
|
@@ -54,6 +59,8 @@ export async function run(values, positional) {
|
|
|
54
59
|
const clientState = buildCliClient();
|
|
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/lib/command.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
1
3
|
import { buildClient, syncTokens } from './client.mjs';
|
|
2
4
|
import { collectFieldFlags } from './flags.mjs';
|
|
3
5
|
import { resolveResource } from './resources.mjs';
|
|
4
|
-
import { error, info, warn } from './output.mjs';
|
|
6
|
+
import { error, info, warn, printQuery } from './output.mjs';
|
|
5
7
|
|
|
6
8
|
export function fail(message) {
|
|
7
9
|
error(message);
|
|
@@ -49,6 +51,54 @@ export function parseJsonOption(value, flagName) {
|
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
export function parseJsonFileOption(value, flagName) {
|
|
55
|
+
if (value == null || value === '') {
|
|
56
|
+
fail(`--${flagName} requires a file path.`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const filePath = String(value);
|
|
60
|
+
const absolutePath = resolve(process.cwd(), filePath);
|
|
61
|
+
let text;
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
text = readFileSync(absolutePath, 'utf8');
|
|
65
|
+
} catch (err) {
|
|
66
|
+
if (err?.code === 'ENOENT') {
|
|
67
|
+
fail(`--${flagName} file not found: ${filePath}`);
|
|
68
|
+
}
|
|
69
|
+
if (err?.code === 'EISDIR') {
|
|
70
|
+
fail(`--${flagName} points to a directory, not a JSON file: ${filePath}`);
|
|
71
|
+
}
|
|
72
|
+
fail(`Could not read --${flagName} file ${filePath}: ${err.message || err}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(text);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
fail(`--${flagName} file must contain valid JSON: ${filePath} (${err.message || err})`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function parseJsonOptionOrFile(values, flagName, fileFlagName = `${flagName}-file`) {
|
|
83
|
+
const hasInline = Object.prototype.hasOwnProperty.call(values, flagName);
|
|
84
|
+
const hasFile = Object.prototype.hasOwnProperty.call(values, fileFlagName);
|
|
85
|
+
|
|
86
|
+
if (hasInline && hasFile) {
|
|
87
|
+
fail(`Use either --${flagName} or --${fileFlagName}, not both.`);
|
|
88
|
+
}
|
|
89
|
+
if (hasInline) {
|
|
90
|
+
if (values[flagName] === '') {
|
|
91
|
+
fail(`--${flagName} requires a JSON value. Use --${fileFlagName} <path> for file input.`);
|
|
92
|
+
}
|
|
93
|
+
return parseJsonOption(values[flagName], flagName);
|
|
94
|
+
}
|
|
95
|
+
if (hasFile) {
|
|
96
|
+
return parseJsonFileOption(values[fileFlagName], fileFlagName);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
52
102
|
/** Cheap structural check: does this string look like an intended JSON object? */
|
|
53
103
|
function looksLikeJsonObject(value) {
|
|
54
104
|
return typeof value === 'string' && value.trim().startsWith('{');
|
|
@@ -90,7 +140,7 @@ function tryParseJsonObject(value) {
|
|
|
90
140
|
* @returns {Record<string, unknown>}
|
|
91
141
|
*/
|
|
92
142
|
export function buildRecordPayload(values, positionalData) {
|
|
93
|
-
const parsed =
|
|
143
|
+
const parsed = parseJsonOptionOrFile(values, 'data', 'data-file');
|
|
94
144
|
const data = parsed === undefined ? {} : parsed;
|
|
95
145
|
|
|
96
146
|
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
@@ -125,6 +175,31 @@ export function buildRecordPayload(values, positionalData) {
|
|
|
125
175
|
fail('No fields provided. Use --data or individual --<field> flags.');
|
|
126
176
|
}
|
|
127
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Handle the global `--query` flag: instead of sending the request, ask the
|
|
180
|
+
* client to resolve the route + payload (dry run) and print them. Returns
|
|
181
|
+
* `true` when it handled a dry run, so the caller can `return` early.
|
|
182
|
+
*
|
|
183
|
+
* @param {ReturnType<typeof buildCliClient>} clientState
|
|
184
|
+
* @param {string} operationId
|
|
185
|
+
* @param {unknown} input - the same input the real call would receive
|
|
186
|
+
* @param {Record<string, unknown>} values - parsed CLI flags
|
|
187
|
+
* @returns {Promise<boolean>}
|
|
188
|
+
*/
|
|
189
|
+
export async function maybeDryRun(clientState, operationId, input, values) {
|
|
190
|
+
if (!values.query) return false;
|
|
191
|
+
|
|
192
|
+
const fn = requireApiMethod(clientState, operationId);
|
|
193
|
+
let descriptor;
|
|
194
|
+
try {
|
|
195
|
+
descriptor = await fn(input, { dryRun: true });
|
|
196
|
+
} catch (err) {
|
|
197
|
+
fail(`Could not build request: ${err.message}`);
|
|
198
|
+
}
|
|
199
|
+
printQuery(descriptor, values);
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
|
|
128
203
|
export function requireApiMethod(clientState, operationId) {
|
|
129
204
|
const fn = clientState.client.api[operationId];
|
|
130
205
|
if (typeof fn !== 'function') {
|
package/lib/flags.mjs
CHANGED
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
// Global CLI flags that are never record fields. Any other --flag on
|
|
7
7
|
// create/update is treated as a field on the record being written.
|
|
8
8
|
const RESERVED_FLAGS = new Set([
|
|
9
|
-
'data', 'json', 'yaml', 'help', 'h',
|
|
10
|
-
'no-color', 'force', 'fields', 'filter', 'sort',
|
|
9
|
+
'data', 'data-file', 'json', 'yaml', 'help', 'h',
|
|
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',
|
|
13
|
+
'extdata', 'tags', 'all', 'clean', 'query',
|
|
14
14
|
]);
|
|
15
15
|
|
|
16
16
|
/**
|
package/lib/output.mjs
CHANGED
|
@@ -20,6 +20,7 @@ const c = {
|
|
|
20
20
|
red: s => USE_COLOR ? `\x1b[31m${s}\x1b[0m` : s,
|
|
21
21
|
yellow: s => USE_COLOR ? `\x1b[33m${s}\x1b[0m` : s,
|
|
22
22
|
cyan: s => USE_COLOR ? `\x1b[36m${s}\x1b[0m` : s,
|
|
23
|
+
gray: s => USE_COLOR ? `\x1b[90m${s}\x1b[0m` : s, // bright-black: dim IDs / muted cells
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
export { c as colors };
|
|
@@ -39,6 +40,33 @@ export function printJson(data) {
|
|
|
39
40
|
process.stdout.write(JSON.stringify(data, null, 2) + '\n');
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
// ── Query (dry run) ─────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Print a dry-run request descriptor (from `--query`): the resolved HTTP route
|
|
47
|
+
* and the JSON payload that *would* be sent, without performing the request.
|
|
48
|
+
*
|
|
49
|
+
* @param {{method:string,url:string,body?:unknown,bodyType?:string}} descriptor
|
|
50
|
+
* @param {Record<string, unknown>} [values] - parsed CLI flags (for --json/--yaml)
|
|
51
|
+
*/
|
|
52
|
+
export function printQuery(descriptor, values = {}) {
|
|
53
|
+
if (values.json) { printJson(descriptor); return; }
|
|
54
|
+
if (values.yaml) { printYaml(descriptor); return; }
|
|
55
|
+
|
|
56
|
+
const { method, url, body, bodyType } = descriptor;
|
|
57
|
+
process.stdout.write(`${c.bold(method)} ${url}\n`);
|
|
58
|
+
if (bodyType) {
|
|
59
|
+
const contentType = bodyType === 'form' ? 'application/x-www-form-urlencoded' : 'application/json';
|
|
60
|
+
process.stdout.write(c.dim(`Content-Type: ${contentType}`) + '\n');
|
|
61
|
+
}
|
|
62
|
+
process.stdout.write('\n');
|
|
63
|
+
if (body === undefined || body === null) {
|
|
64
|
+
process.stdout.write(c.dim('(no request body)') + '\n');
|
|
65
|
+
} else {
|
|
66
|
+
process.stdout.write(JSON.stringify(body, null, 2) + '\n');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
42
70
|
// ── YAML ──────────────────────────────────────────────────────────────────────
|
|
43
71
|
|
|
44
72
|
export function printYaml(data) {
|
|
@@ -129,14 +157,54 @@ export function printTable(rows, columns, labels = {}, formatters = {}) {
|
|
|
129
157
|
Math.max(headers[i].length, ...data.map(row => _visibleLength(row[i])))
|
|
130
158
|
);
|
|
131
159
|
|
|
132
|
-
|
|
160
|
+
// QW-2: detect numeric columns (every non-empty cell is a plain number,
|
|
161
|
+
// ignoring ANSI) so we can right-align them — header included.
|
|
162
|
+
const numeric = columns.map((_, i) => {
|
|
163
|
+
let sawValue = false;
|
|
164
|
+
for (const row of data) {
|
|
165
|
+
const plain = row[i].replace(/\x1b\[[0-9;]*m/g, '');
|
|
166
|
+
if (plain === '' || plain === '—') continue; // blank / em-dash placeholder
|
|
167
|
+
sawValue = true;
|
|
168
|
+
if (!/^-?\d+(\.\d+)?$/.test(plain)) return false;
|
|
169
|
+
}
|
|
170
|
+
return sawValue;
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// QW-1: when stdout is a TTY, shrink the widest column(s) until the row fits
|
|
174
|
+
// the terminal. Non-TTY (piped) output stays full-width so `| grep`/`| awk`
|
|
175
|
+
// see complete cell text. Budget = columns − 2 leading spaces − 2 per gutter.
|
|
176
|
+
if (process.stdout.isTTY) {
|
|
177
|
+
const term = process.stdout.columns;
|
|
178
|
+
if (term && term > 0) {
|
|
179
|
+
const MIN_COL = 8;
|
|
180
|
+
const gutters = (widths.length - 1) * 2;
|
|
181
|
+
const budget = term - 2 - gutters;
|
|
182
|
+
// Repeatedly trim the single widest column above the floor until it fits.
|
|
183
|
+
let total = widths.reduce((a, b) => a + b, 0);
|
|
184
|
+
while (total > budget) {
|
|
185
|
+
let widest = -1;
|
|
186
|
+
for (let i = 0; i < widths.length; i++) {
|
|
187
|
+
if (widths[i] > MIN_COL && (widest === -1 || widths[i] > widths[widest])) widest = i;
|
|
188
|
+
}
|
|
189
|
+
if (widest === -1) break; // every column already at the floor
|
|
190
|
+
widths[widest]--;
|
|
191
|
+
total--;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const align = (str, i) => (numeric[i] ? _padLeft(str, widths[i]) : _pad(_truncate(str, widths[i]), widths[i]));
|
|
197
|
+
|
|
198
|
+
const headerRow = headers.map((h, i) =>
|
|
199
|
+
numeric[i] ? _padLeft(c.bold(h), widths[i]) : _pad(_truncate(c.bold(h), widths[i]), widths[i])
|
|
200
|
+
).join(' ');
|
|
133
201
|
const separator = widths.map(w => '─'.repeat(w)).join(' ');
|
|
134
202
|
|
|
135
203
|
process.stdout.write('\n');
|
|
136
204
|
process.stdout.write(' ' + headerRow + '\n');
|
|
137
205
|
process.stdout.write(' ' + c.dim(separator) + '\n');
|
|
138
206
|
for (const row of data) {
|
|
139
|
-
process.stdout.write(' ' + row.map((v, i) =>
|
|
207
|
+
process.stdout.write(' ' + row.map((v, i) => align(v, i)).join(' ') + '\n');
|
|
140
208
|
}
|
|
141
209
|
process.stdout.write('\n');
|
|
142
210
|
}
|
|
@@ -163,10 +231,15 @@ export function printRecord(record, keys, labels = {}, formatters = {}) {
|
|
|
163
231
|
|
|
164
232
|
if (formatters[key]) {
|
|
165
233
|
display = String(formatters[key](val, record));
|
|
166
|
-
} else if (val === null) {
|
|
234
|
+
} else if (val === null || val === '') {
|
|
235
|
+
// QW-6: render null AND empty string as a dim em-dash, not a blank gap.
|
|
167
236
|
display = c.dim('—');
|
|
237
|
+
} else if (Array.isArray(val)) {
|
|
238
|
+
// QW-6: empty array → dim em-dash; otherwise compact JSON.
|
|
239
|
+
display = val.length === 0 ? c.dim('—') : JSON.stringify(val);
|
|
168
240
|
} else if (typeof val === 'object') {
|
|
169
|
-
|
|
241
|
+
// QW-6: empty object → dim em-dash; otherwise compact JSON.
|
|
242
|
+
display = Object.keys(val).length === 0 ? c.dim('—') : JSON.stringify(val);
|
|
170
243
|
} else {
|
|
171
244
|
display = String(val);
|
|
172
245
|
}
|
|
@@ -269,6 +342,77 @@ export function buildDateFormatters(columns, dateFormat = 'YYYY-MM-DD', aliasToP
|
|
|
269
342
|
return formatters;
|
|
270
343
|
}
|
|
271
344
|
|
|
345
|
+
// ── Semantic enum / ID coloring (QW-3) ─────────────────────────────────────────
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Pick a colorizer for an enum LABEL by keyword.
|
|
349
|
+
*
|
|
350
|
+
* Enum codes are resource-specific (ticket status 1 = AWAITINGACCEPTANCE but
|
|
351
|
+
* transaction status 1 = COMPLETED), so color is derived from the label text —
|
|
352
|
+
* never from the numeric code. Returns `null` when no keyword matches, so the
|
|
353
|
+
* caller renders the value plain rather than guessing.
|
|
354
|
+
*
|
|
355
|
+
* @param {string} label
|
|
356
|
+
* @returns {((s:string)=>string)|null}
|
|
357
|
+
*/
|
|
358
|
+
function _enumColorForLabel(label) {
|
|
359
|
+
const L = String(label).toUpperCase();
|
|
360
|
+
// Positive / terminal-success states → green.
|
|
361
|
+
if (/COMPLETED|BOOKED|ACTIVE|DONE|ACCEPTED|PAID/.test(L)) return c.green;
|
|
362
|
+
// Failure / negative states → red.
|
|
363
|
+
if (/CANCELLED|CANCELED|FAILED|REJECTED|DELETED|OVERDUE/.test(L)) return c.red;
|
|
364
|
+
// Priority extremes.
|
|
365
|
+
if (/HIGHEST|HIGH/.test(L)) return c.red;
|
|
366
|
+
if (/LOWEST|LOW/.test(L)) return c.dim;
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** A field name that denotes a record identifier / foreign key → render dim. */
|
|
371
|
+
function _isIdField(name) {
|
|
372
|
+
const lower = String(name).toLowerCase();
|
|
373
|
+
return lower === 'id' || lower.endsWith('id');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Build value formatters that colorize enum + ID columns, schema-driven.
|
|
378
|
+
*
|
|
379
|
+
* For each display column, the API field path (via `aliasToPath`) is reduced to
|
|
380
|
+
* its leaf column name and looked up in `fieldDefs` (a resource's
|
|
381
|
+
* `schema.describe(resource).fields` map). Columns whose field has an `enum` are
|
|
382
|
+
* colored by label keyword; ID/FK columns are dimmed. Columns with no resolvable
|
|
383
|
+
* enum label are left plain. No-ops entirely when color is disabled.
|
|
384
|
+
*
|
|
385
|
+
* @param {string[]} columns - display column keys
|
|
386
|
+
* @param {Record<string, {enum?:Record<string,string>, fk?:string}>} [fieldDefs]
|
|
387
|
+
* @param {Record<string,string>} [aliasToPath] - alias → API field path
|
|
388
|
+
* @returns {Record<string, ValueFormatter>}
|
|
389
|
+
*/
|
|
390
|
+
export function buildEnumFormatters(columns, fieldDefs = {}, aliasToPath) {
|
|
391
|
+
const formatters = {};
|
|
392
|
+
if (!USE_COLOR) return formatters; // color-gated: nothing to do when plain.
|
|
393
|
+
|
|
394
|
+
for (const col of columns) {
|
|
395
|
+
const fieldPath = aliasToPath?.[col] ?? col;
|
|
396
|
+
// Dot-notation joins (contact.city) can't be mapped to a base column reliably.
|
|
397
|
+
if (fieldPath.includes('.')) continue;
|
|
398
|
+
const def = fieldDefs[fieldPath];
|
|
399
|
+
|
|
400
|
+
if (def?.enum) {
|
|
401
|
+
const enumMap = def.enum;
|
|
402
|
+
formatters[col] = (val) => {
|
|
403
|
+
if (val == null || val === '') return c.dim('—');
|
|
404
|
+
const label = enumMap[String(val)];
|
|
405
|
+
if (label == null) return String(val); // unknown code → plain, never guess
|
|
406
|
+
const paint = _enumColorForLabel(label);
|
|
407
|
+
return paint ? paint(String(val)) : String(val);
|
|
408
|
+
};
|
|
409
|
+
} else if (_isIdField(fieldPath) || def?.fk) {
|
|
410
|
+
formatters[col] = (val) => (val == null || val === '' ? c.dim('—') : c.gray(String(val)));
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return formatters;
|
|
414
|
+
}
|
|
415
|
+
|
|
272
416
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
273
417
|
|
|
274
418
|
/** String length ignoring ANSI escape codes. */
|
|
@@ -282,3 +426,49 @@ function _pad(str, len) {
|
|
|
282
426
|
if (visible >= len) return str;
|
|
283
427
|
return str + ' '.repeat(len - visible);
|
|
284
428
|
}
|
|
429
|
+
|
|
430
|
+
/** Left-pad a string to a visible width (right-align), ANSI-aware. */
|
|
431
|
+
function _padLeft(str, len) {
|
|
432
|
+
const visible = _visibleLength(str);
|
|
433
|
+
if (visible >= len) return str;
|
|
434
|
+
return ' '.repeat(len - visible) + str;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Truncate a string to a max visible width, appending '…', ANSI-aware.
|
|
439
|
+
* Preserves any trailing reset so colored cells don't bleed. Strings already
|
|
440
|
+
* within the budget are returned untouched.
|
|
441
|
+
*
|
|
442
|
+
* @param {string} str
|
|
443
|
+
* @param {number} max - max visible width (including the ellipsis)
|
|
444
|
+
* @returns {string}
|
|
445
|
+
*/
|
|
446
|
+
function _truncate(str, max) {
|
|
447
|
+
if (max <= 0) return '';
|
|
448
|
+
if (_visibleLength(str) <= max) return str;
|
|
449
|
+
|
|
450
|
+
// Walk the string copying characters, skipping over ANSI sequences (which
|
|
451
|
+
// have zero visible width), until we've kept (max - 1) visible chars; then
|
|
452
|
+
// append the ellipsis and any trailing reset.
|
|
453
|
+
const keep = max - 1;
|
|
454
|
+
let out = '';
|
|
455
|
+
let visible = 0;
|
|
456
|
+
let i = 0;
|
|
457
|
+
let hadColor = false;
|
|
458
|
+
while (i < str.length && visible < keep) {
|
|
459
|
+
const ansi = str.slice(i).match(/^\x1b\[[0-9;]*m/);
|
|
460
|
+
if (ansi) {
|
|
461
|
+
out += ansi[0];
|
|
462
|
+
hadColor = true;
|
|
463
|
+
i += ansi[0].length;
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
out += str[i];
|
|
467
|
+
visible++;
|
|
468
|
+
i++;
|
|
469
|
+
}
|
|
470
|
+
out += '…';
|
|
471
|
+
// Re-apply a reset if the original was colored so the ellipsis/padding stay clean.
|
|
472
|
+
if (hadColor) out += '\x1b[0m';
|
|
473
|
+
return out;
|
|
474
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeyos/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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.2.0"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
45
|
"test": "node --test test/offline.mjs"
|