@zeyos/cli 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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,33 @@ 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
+
75
+ Inspect dynamic schema definitions:
76
+
77
+ ```bash
78
+ zeyos count customfields --json
79
+ zeyos list customfields --fields ID,name,identifier,context,type --json
80
+ ```
81
+
82
+ Inspect actionsteps/time-entry evidence and ticket mail:
83
+
84
+ ```bash
85
+ zeyos list actionsteps --fields ID,name,status,date,duedate,effort,ticket,account --json
86
+ zeyos list messages --fields ID,date,mailbox,subject,sender_email,to_email,ticket,reference --filter '{"ticket":42}' --json
87
+ ```
88
+
68
89
  Create, update, and delete:
69
90
 
70
91
  ```bash
71
92
  zeyos create ticket --data '{"name":"Fix login bug","status":0,"priority":3,"visibility":0}' --json
93
+ zeyos create ticket --data-file ./ticket.json --json
72
94
  zeyos update ticket 42 --data '{"status":4}' --json
95
+ zeyos update ticket 42 --data-file ./ticket-update.json --json
73
96
  zeyos delete ticket 42
74
97
  ```
75
98
 
@@ -82,6 +105,6 @@ zeyos delete ticket 42
82
105
 
83
106
  ## Coverage Boundary
84
107
 
85
- The CLI intentionally covers a curated registry instead of the full API surface. Use `zeyos resources` to see the supported set.
108
+ The CLI intentionally covers a curated registry instead of the full API surface. It includes operational resources such as tickets, tasks, messages, and actionsteps; use `zeyos resources` to see the supported set.
86
109
 
87
110
  When you need unsupported resources or low-level request control, switch to [`@zeyos/client`](../docs/02-javascript-client/01-getting-started.md) and follow the escalation guidance in [CLI Coverage and Escalation](../docs/04-agent-workflows/03-cli-coverage-and-escalation.md).
package/bin/zeyos.mjs CHANGED
@@ -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,54 @@
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: zeyos <command> [options] [args…]
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
- skills <command> List / show / install ZeyOS agent skills
48
-
49
- Global options:
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
+ ${_c.cyan('profile')} <command> Manage credential profiles / switch instances
55
+
56
+ ${_c.bold('Global options:')}
50
57
  --json Output as JSON
51
58
  --yaml Output as YAML
59
+ --query Print the API route + JSON payload without sending it
60
+ --profile <name> Use a named credential profile for this command
52
61
  --no-color Disable ANSI colors
53
62
  -h, --help Show help for a command
54
63
  -v, --version Print the CLI version and exit
55
64
 
56
- Examples:
57
- zeyos login --base-url https://cloud.zeyos.com/demo --client-id myapp --secret "$ZEYOS_CLIENT_SECRET"
58
- zeyos list tickets --filter '{"status":1}' --sort -lastmodified
59
- zeyos count tickets --filter '{"status":1}'
60
- zeyos get ticket 42
61
- zeyos get ticket 42 --all
62
- zeyos create ticket --name "Fix login bug" --priority 3
63
- zeyos update ticket 42 --status 2
64
- zeyos delete ticket 42 --force
65
+ ${_c.bold('Examples:')}
66
+ ${_z} login --base-url https://cloud.zeyos.com/demo --client-id myapp --secret "$ZEYOS_CLIENT_SECRET"
67
+ ${_z} list tickets --filter '{"status":1}' --sort -lastmodified
68
+ ${_z} list tickets --filter-file ./filters/open-tickets.json
69
+ ${_z} count tickets --filter '{"status":1}'
70
+ ${_z} get ticket 42
71
+ ${_z} get ticket 42 --all
72
+ ${_z} create ticket --name "Fix login bug" --priority 3
73
+ ${_z} update ticket 42 --status 2
74
+ ${_z} delete ticket 42 --force
65
75
  `;
66
76
 
67
77
  // ── Argument definitions ──────────────────────────────────────────────────────
@@ -73,6 +83,8 @@ const OPTIONS = {
73
83
  'json': { type: 'boolean' },
74
84
  'yaml': { type: 'boolean' },
75
85
  'no-color': { type: 'boolean' },
86
+ 'query': { type: 'boolean' },
87
+ 'profile': { type: 'string' },
76
88
  // login
77
89
  'base-url': { type: 'string' },
78
90
  'client-id': { type: 'string' },
@@ -88,6 +100,7 @@ const OPTIONS = {
88
100
  // list
89
101
  'fields': { type: 'string' },
90
102
  'filter': { type: 'string' },
103
+ 'filter-file': { type: 'string' },
91
104
  'sort': { type: 'string' },
92
105
  'limit': { type: 'string' },
93
106
  'offset': { type: 'string' },
@@ -100,12 +113,15 @@ const OPTIONS = {
100
113
  'show-token': { type: 'boolean' },
101
114
  // create / update
102
115
  'data': { type: 'string' },
116
+ 'data-file': { type: 'string' },
103
117
  // delete
104
118
  // (--force is already declared above)
105
119
  // skills install
106
120
  'target': { type: 'string' },
107
121
  'dir': { type: 'string' },
108
122
  'no-logo': { type: 'boolean' },
123
+ // profile
124
+ 'from-current': { type: 'boolean' },
109
125
  };
110
126
 
111
127
  // ── Command registry ──────────────────────────────────────────────────────────
@@ -128,8 +144,46 @@ const COMMANDS = {
128
144
  resources: '../commands/resources.mjs',
129
145
  resource: '../commands/resources.mjs',
130
146
  describe: '../commands/describe.mjs',
147
+ doctor: '../commands/doctor.mjs',
131
148
  skills: '../commands/skills.mjs',
132
149
  skill: '../commands/skills.mjs',
150
+ profile: '../commands/profile.mjs',
151
+ profiles: '../commands/profile.mjs',
152
+ };
153
+
154
+ // ── Per-command flag allow-lists ────────────────────────────────────────────────
155
+ // Unknown flags are rejected (e.g. `zeyos list --invalid`) so typos surface
156
+ // immediately instead of being silently ignored. `create`/`update` are the
157
+ // exception: they accept arbitrary `--<field>` flags, marked with `null` below.
158
+
159
+ const ALWAYS_FLAGS = ['help', 'json', 'yaml', 'no-color', 'profile'];
160
+ const SKILLS_FLAGS = ['target', 'dir', 'global', 'local', 'force', 'yes', 'no-logo'];
161
+ const PROFILE_FLAGS = ['base-url', 'client-id', 'secret', 'local', 'from-current'];
162
+ const DELETE_FLAGS = ['force', 'query'];
163
+ const GET_FLAGS = ['fields', 'extdata', 'tags', 'expand', 'all', 'query'];
164
+
165
+ const COMMAND_FLAGS = {
166
+ login: ['base-url', 'client-id', 'secret', 'scope', 'port', 'global', 'force', 'clean', 'manual'],
167
+ logout: ['global'],
168
+ whoami: ['show-token'],
169
+ list: ['fields', 'filter', 'filter-file', 'sort', 'limit', 'offset', 'extdata', 'expand', 'query'],
170
+ count: ['filter', 'filter-file', 'query'],
171
+ get: GET_FLAGS,
172
+ show: GET_FLAGS,
173
+ create: null,
174
+ update: null,
175
+ edit: null,
176
+ delete: DELETE_FLAGS,
177
+ rm: DELETE_FLAGS,
178
+ remove: DELETE_FLAGS,
179
+ resources: [],
180
+ resource: [],
181
+ describe: [],
182
+ doctor: [],
183
+ skills: SKILLS_FLAGS,
184
+ skill: SKILLS_FLAGS,
185
+ profile: PROFILE_FLAGS,
186
+ profiles: PROFILE_FLAGS,
133
187
  };
134
188
 
135
189
  // ── Main ──────────────────────────────────────────────────────────────────────
@@ -149,6 +203,14 @@ async function main() {
149
203
  }
150
204
 
151
205
  const command = argv[0];
206
+
207
+ // A leading flag (e.g. `zeyos --invalid`) is not a command — surface it as a
208
+ // bad option rather than letting it masquerade as one.
209
+ if (command.startsWith('-')) {
210
+ process.stderr.write(`Unknown option: "${command}". Run 'zeyos --help' for usage.\n`);
211
+ process.exit(1);
212
+ }
213
+
152
214
  const rest = argv.slice(1);
153
215
 
154
216
  // Parse remaining args permissively: known options are parsed normally and
@@ -168,6 +230,24 @@ async function main() {
168
230
  process.exit(0);
169
231
  }
170
232
 
233
+ // Reject unknown flags so typos / unsupported options fail loudly instead of
234
+ // being silently ignored. `create`/`update` opt out (COMMAND_FLAGS = null)
235
+ // because they accept arbitrary `--<field>` flags as record data.
236
+ const allowed = COMMAND_FLAGS[command];
237
+ if (allowed) {
238
+ const allowedSet = new Set([...ALWAYS_FLAGS, ...allowed]);
239
+ const unknown = Object.keys(values).filter((key) => !allowedSet.has(key));
240
+ if (unknown.length > 0) {
241
+ const flag = unknown[0];
242
+ const hint = _suggestFlag(flag, [...allowedSet]);
243
+ process.stderr.write(
244
+ `Unknown option: --${flag}${hint ? ` (did you mean --${hint}?)` : ''}\n\n` +
245
+ `Run 'zeyos ${command} --help' for available options.\n`
246
+ );
247
+ process.exit(1);
248
+ }
249
+ }
250
+
171
251
  await mod.run(values, positional);
172
252
  }
173
253
 
@@ -274,6 +354,38 @@ function _parsePermissive(argv, options) {
274
354
  return { values, positional };
275
355
  }
276
356
 
357
+ /** Suggest the closest allowed flag for an unknown one, if it's a near miss. */
358
+ function _suggestFlag(input, candidates) {
359
+ let best = null;
360
+ let bestDist = Infinity;
361
+ for (const candidate of candidates) {
362
+ const dist = _levenshtein(input, candidate);
363
+ if (dist < bestDist) {
364
+ bestDist = dist;
365
+ best = candidate;
366
+ }
367
+ }
368
+ // Only suggest a reasonably close match (avoid nonsense "did you mean").
369
+ return bestDist <= Math.max(2, Math.floor(input.length / 2)) ? best : null;
370
+ }
371
+
372
+ /** Levenshtein edit distance between two short strings. */
373
+ function _levenshtein(a, b) {
374
+ const m = a.length;
375
+ const n = b.length;
376
+ const dp = Array.from({ length: m + 1 }, (_, i) => i);
377
+ for (let j = 1; j <= n; j++) {
378
+ let prev = dp[0];
379
+ dp[0] = j;
380
+ for (let i = 1; i <= m; i++) {
381
+ const tmp = dp[i];
382
+ dp[i] = a[i - 1] === b[j - 1] ? prev : 1 + Math.min(prev, dp[i], dp[i - 1]);
383
+ prev = tmp;
384
+ }
385
+ }
386
+ return dp[m];
387
+ }
388
+
277
389
  main().catch(err => {
278
390
  process.stderr.write(`Fatal: ${err.message}\n`);
279
391
  process.exit(1);
@@ -4,13 +4,14 @@
4
4
  * Return the count of records matching an optional filter.
5
5
  *
6
6
  * Options:
7
- * --filter <json> JSON filter object e.g. '{"status":1}'
8
- * --json Output as JSON
9
- * --yaml Output as YAML
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, parseJsonOption, requireResource } from '../lib/command.mjs';
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
- if (values.filter) {
45
- body.filters = parseJsonOption(values.filter, 'filter');
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(values);
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);
@@ -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> Full record as JSON object
10
- * --json Output created record as JSON
11
- * --yaml Output created record as YAML
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) {
@@ -43,9 +48,11 @@ export async function run(values, positional) {
43
48
  // (optional) JSON body some callers pass positionally instead of via --data.
44
49
  const data = buildRecordPayload(values, positional[1]);
45
50
 
46
- const clientState = buildCliClient();
51
+ const clientState = buildCliClient(values);
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);
@@ -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(values);
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.`
@@ -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
- notes.push('enum: ' + Object.entries(field.enum).map(([k, v]) => `${k}=${v}`).join(' '));
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
+ }