@zeyos/cli 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ZeyOS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # ZeyOS CLI
2
+
3
+ The CLI is the default interface for coding agents and shell automation that work against the curated ZeyOS resource registry.
4
+
5
+ The authoritative documentation lives in the repository-level docs:
6
+
7
+ - [Coding Agents](../docs/04-agent-workflows/00-coding-agents.md)
8
+ - [Agent Quickstart](../docs/04-agent-workflows/01-agent-quickstart.md)
9
+ - [CLI Getting Started](../docs/03-cli/01-getting-started.md)
10
+ - [Commands Reference](../docs/03-cli/02-commands.md)
11
+ - [Configuration](../docs/03-cli/03-configuration.md)
12
+
13
+ ## Install
14
+
15
+ From the repository root:
16
+
17
+ ```bash
18
+ npm install
19
+ npm link cli/
20
+ zeyos --help
21
+ ```
22
+
23
+ Or install the package directly:
24
+
25
+ ```bash
26
+ npm install -g @zeyos/cli
27
+ ```
28
+
29
+ ## Quick Start
30
+
31
+ Authenticate:
32
+
33
+ ```bash
34
+ export ZEYOS_CLIENT_SECRET="..."
35
+ zeyos login \
36
+ --base-url https://cloud.zeyos.com/demo \
37
+ --client-id myapp \
38
+ --secret "$ZEYOS_CLIENT_SECRET"
39
+ ```
40
+
41
+ For interactive use, omit `--secret`; the CLI prompts without echoing the secret to the terminal.
42
+
43
+ Verify the current user:
44
+
45
+ ```bash
46
+ zeyos whoami --json
47
+ ```
48
+
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
+
51
+ Inspect the CLI-supported resource registry:
52
+
53
+ ```bash
54
+ zeyos resources
55
+ ```
56
+
57
+ List tickets for automation:
58
+
59
+ ```bash
60
+ zeyos list tickets \
61
+ --fields ID,ticketnum,name,status,priority,lastmodified \
62
+ --filter '{"visibility":0}' \
63
+ --sort -lastmodified \
64
+ --limit 20 \
65
+ --json
66
+ ```
67
+
68
+ Create, update, and delete:
69
+
70
+ ```bash
71
+ zeyos create ticket --data '{"name":"Fix login bug","status":0,"priority":3,"visibility":0}' --json
72
+ zeyos update ticket 42 --data '{"status":4}' --json
73
+ zeyos delete ticket 42
74
+ ```
75
+
76
+ ## Configuration
77
+
78
+ - Local credentials: `.zeyos/auth.json`
79
+ - Global credentials: `~/.config/zeyos/credentials.json`
80
+ - Project resource overrides: `.zeyos/api/<resource>.json`
81
+ - Global resource overrides: `~/.zeyos/api/<resource>.json`
82
+
83
+ ## Coverage Boundary
84
+
85
+ The CLI intentionally covers a curated registry instead of the full API surface. Use `zeyos resources` to see the supported set.
86
+
87
+ 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 ADDED
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ZeyOS CLI — entry point
4
+ *
5
+ * Usage: zeyos <command> [options] [args…]
6
+ *
7
+ * Commands:
8
+ * login Authenticate with ZeyOS
9
+ * logout Revoke session and clear tokens
10
+ * whoami Show current user info
11
+ * list <resource> List records
12
+ * count <resource> Count records
13
+ * get <resource> <id> Fetch a single record
14
+ * show <resource> <id> Alias for get
15
+ * create <resource> Create a new record
16
+ * update <resource> Update a record
17
+ * delete <resource> Delete a record
18
+ * resources List available resource types
19
+ */
20
+
21
+ // ── Version ───────────────────────────────────────────────────────────────────
22
+
23
+ import { createRequire as _createRequire } from 'node:module';
24
+ import { dirname as _dirname } from 'node:path';
25
+ import { fileURLToPath as _fileURLToPath } from 'node:url';
26
+ const _require = _createRequire(import.meta.url);
27
+ const _VERSION = _require('../package.json').version;
28
+
29
+ // ── Global help ───────────────────────────────────────────────────────────────
30
+
31
+ 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:
50
+ --json Output as JSON
51
+ --yaml Output as YAML
52
+ --no-color Disable ANSI colors
53
+ -h, --help Show help for a command
54
+ -v, --version Print the CLI version and exit
55
+
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
+ `;
66
+
67
+ // ── Argument definitions ──────────────────────────────────────────────────────
68
+
69
+ const OPTIONS = {
70
+ // Global
71
+ 'help': { type: 'boolean', short: 'h' },
72
+ 'version': { type: 'boolean', short: 'v' },
73
+ 'json': { type: 'boolean' },
74
+ 'yaml': { type: 'boolean' },
75
+ 'no-color': { type: 'boolean' },
76
+ // login
77
+ 'base-url': { type: 'string' },
78
+ 'client-id': { type: 'string' },
79
+ 'secret': { type: 'string' },
80
+ 'scope': { type: 'string' },
81
+ 'port': { type: 'string' },
82
+ 'global': { type: 'boolean' },
83
+ 'local': { type: 'boolean' },
84
+ 'force': { type: 'boolean' },
85
+ 'clean': { type: 'boolean' },
86
+ 'manual': { type: 'boolean' },
87
+ 'yes': { type: 'boolean', short: 'y' },
88
+ // list
89
+ 'fields': { type: 'string' },
90
+ 'filter': { type: 'string' },
91
+ 'sort': { type: 'string' },
92
+ 'limit': { type: 'string' },
93
+ 'offset': { type: 'string' },
94
+ 'expand': { type: 'string' },
95
+ 'extdata': { type: 'boolean' },
96
+ 'tags': { type: 'boolean' },
97
+ // get
98
+ 'all': { type: 'boolean' },
99
+ // whoami
100
+ 'show-token': { type: 'boolean' },
101
+ // create / update
102
+ 'data': { type: 'string' },
103
+ // delete
104
+ // (--force is already declared above)
105
+ // skills install
106
+ 'target': { type: 'string' },
107
+ 'dir': { type: 'string' },
108
+ 'no-logo': { type: 'boolean' },
109
+ };
110
+
111
+ // ── Command registry ──────────────────────────────────────────────────────────
112
+ // Maps every command and alias to the module that implements it.
113
+
114
+ const COMMANDS = {
115
+ login: '../commands/login.mjs',
116
+ logout: '../commands/logout.mjs',
117
+ whoami: '../commands/whoami.mjs',
118
+ list: '../commands/list.mjs',
119
+ count: '../commands/count.mjs',
120
+ get: '../commands/get.mjs',
121
+ show: '../commands/get.mjs',
122
+ create: '../commands/create.mjs',
123
+ update: '../commands/update.mjs',
124
+ edit: '../commands/update.mjs',
125
+ delete: '../commands/delete.mjs',
126
+ rm: '../commands/delete.mjs',
127
+ remove: '../commands/delete.mjs',
128
+ resources: '../commands/resources.mjs',
129
+ resource: '../commands/resources.mjs',
130
+ describe: '../commands/describe.mjs',
131
+ skills: '../commands/skills.mjs',
132
+ skill: '../commands/skills.mjs',
133
+ };
134
+
135
+ // ── Main ──────────────────────────────────────────────────────────────────────
136
+
137
+ async function main() {
138
+ // Strip 'node' and script path from argv
139
+ const argv = process.argv.slice(2);
140
+
141
+ if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
142
+ process.stdout.write(HELP);
143
+ process.exit(0);
144
+ }
145
+
146
+ if (argv[0] === '--version' || argv[0] === '-v') {
147
+ process.stdout.write(_VERSION + '\n');
148
+ process.exit(0);
149
+ }
150
+
151
+ const command = argv[0];
152
+ const rest = argv.slice(1);
153
+
154
+ // Parse remaining args permissively: known options are parsed normally and
155
+ // unknown --key value flags are captured too (so create/update accept fields).
156
+ const { values, positional } = _parsePermissive(rest, OPTIONS);
157
+
158
+ const modulePath = COMMANDS[command];
159
+ if (!modulePath) {
160
+ process.stderr.write(`Unknown command: "${command}"\n\n${HELP}`);
161
+ process.exit(1);
162
+ }
163
+
164
+ const mod = await import(modulePath);
165
+
166
+ if (values.help) {
167
+ process.stdout.write(mod.USAGE ?? HELP);
168
+ process.exit(0);
169
+ }
170
+
171
+ await mod.run(values, positional);
172
+ }
173
+
174
+ // ── Helpers ───────────────────────────────────────────────────────────────────
175
+
176
+ /**
177
+ * Parse argv with known options; capture unknown --key value pairs too.
178
+ * This lets create/update accept arbitrary --fieldName value flags.
179
+ *
180
+ * Supports both `--key value` and `--key=value` forms.
181
+ */
182
+ function _parsePermissive(argv, options) {
183
+ const values = {};
184
+ const positional = [];
185
+ let i = 0;
186
+
187
+ while (i < argv.length) {
188
+ const arg = argv[i];
189
+
190
+ if (arg === '--') {
191
+ // Everything after -- is positional
192
+ positional.push(...argv.slice(i + 1));
193
+ break;
194
+ }
195
+
196
+ if (arg.startsWith('--')) {
197
+ // Split --key=value form into key + inline value
198
+ const eqIdx = arg.indexOf('=');
199
+ const key = eqIdx === -1 ? arg.slice(2) : arg.slice(2, eqIdx);
200
+ const inlineVal = eqIdx === -1 ? undefined : arg.slice(eqIdx + 1);
201
+ const opt = options[key];
202
+
203
+ if (opt?.type === 'boolean') {
204
+ // --key=value is unusual for booleans; treat as true and ignore =value
205
+ values[key] = true;
206
+ i++;
207
+ continue;
208
+ }
209
+
210
+ if (opt?.type === 'string') {
211
+ if (inlineVal !== undefined) {
212
+ // --key=value form
213
+ values[key] = inlineVal;
214
+ i++;
215
+ } else {
216
+ const next = argv[i + 1];
217
+ // Don't consume the next token as the value if it looks like a flag
218
+ // (starts with '--'), unless it's a negative number like -3 or -3.5.
219
+ if (next !== undefined && next.startsWith('--')) {
220
+ values[key] = '';
221
+ i++;
222
+ } else {
223
+ values[key] = next ?? '';
224
+ i += 2;
225
+ }
226
+ }
227
+ continue;
228
+ }
229
+
230
+ // Unknown option — treat as string
231
+ if (inlineVal !== undefined) {
232
+ // --key=value form for unknown option
233
+ values[key] = inlineVal;
234
+ i++;
235
+ } else if (i + 1 < argv.length && (!argv[i + 1].startsWith('-') || /^-\d/.test(argv[i + 1]))) {
236
+ values[key] = argv[i + 1];
237
+ i += 2;
238
+ } else {
239
+ values[key] = true;
240
+ i++;
241
+ }
242
+ continue;
243
+ }
244
+
245
+ if (arg.startsWith('-') && arg.length === 2) {
246
+ // Short option
247
+ const short = arg[1];
248
+ const match = Object.entries(options).find(([, o]) => o.short === short);
249
+ if (match) {
250
+ const [key, opt] = match;
251
+ if (opt.type === 'boolean') {
252
+ values[key] = true;
253
+ i++;
254
+ } else {
255
+ const next = argv[i + 1];
256
+ if (next !== undefined && next.startsWith('--')) {
257
+ values[key] = '';
258
+ i++;
259
+ } else {
260
+ values[key] = next ?? '';
261
+ i += 2;
262
+ }
263
+ }
264
+ } else {
265
+ i++;
266
+ }
267
+ continue;
268
+ }
269
+
270
+ positional.push(arg);
271
+ i++;
272
+ }
273
+
274
+ return { values, positional };
275
+ }
276
+
277
+ main().catch(err => {
278
+ process.stderr.write(`Fatal: ${err.message}\n`);
279
+ process.exit(1);
280
+ });
@@ -0,0 +1,63 @@
1
+ /**
2
+ * zeyos count <resource>
3
+ *
4
+ * Return the count of records matching an optional filter.
5
+ *
6
+ * Options:
7
+ * --filter <json> JSON filter object e.g. '{"status":1}'
8
+ * --json Output as JSON
9
+ * --yaml Output as YAML
10
+ */
11
+
12
+ import { normalizeCountResult } from '@zeyos/client';
13
+ import { buildCliClient, callApi, parseJsonOption, requireResource } from '../lib/command.mjs';
14
+ import { outputMode, printJson, printYaml } from '../lib/output.mjs';
15
+
16
+ export const USAGE = `\
17
+ Usage: zeyos count <resource> [options]
18
+
19
+ Return the number of records matching an optional filter.
20
+
21
+ Arguments:
22
+ resource Resource name (e.g. tickets, accounts, tasks)
23
+
24
+ Options:
25
+ --filter <json> JSON filter object e.g. '{"status":1}'
26
+ --json Output as JSON ({ "count": N })
27
+ --yaml Output as YAML
28
+ -h, --help Show this help
29
+
30
+ Examples:
31
+ zeyos count tickets
32
+ zeyos count tickets --filter '{"status":1}'
33
+ zeyos count accounts --json
34
+ `;
35
+
36
+ export async function run(values, positional) {
37
+ const resourceName = positional[0];
38
+ const res = requireResource(resourceName, 'zeyos count <resource>');
39
+ const clientState = buildCliClient();
40
+
41
+ // ── Build request body ─────────────────────────────────────────────────────
42
+ const body = { count: true };
43
+
44
+ if (values.filter) {
45
+ body.filters = parseJsonOption(values.filter, 'filter');
46
+ }
47
+
48
+ // ── Call API ───────────────────────────────────────────────────────────────
49
+ const result = await callApi(clientState, res.list, body);
50
+
51
+ const count = normalizeCountResult(result);
52
+
53
+ // ── Output ─────────────────────────────────────────────────────────────────
54
+ const mode = outputMode(values);
55
+
56
+ if (mode === 'json') {
57
+ printJson({ count });
58
+ } else if (mode === 'yaml') {
59
+ printYaml({ count });
60
+ } else {
61
+ process.stdout.write(`${count}\n`);
62
+ }
63
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * zeyos create <resource> [--data <json>] [--field value …]
3
+ *
4
+ * Create a new record. Field values can be supplied either as:
5
+ * - a JSON blob via --data '{"name":"foo","status":1}'
6
+ * - individual --<field> <value> flags (converted automatically)
7
+ *
8
+ * Options:
9
+ * --data <json> Full record as JSON object
10
+ * --json Output created record as JSON
11
+ * --yaml Output created record as YAML
12
+ */
13
+
14
+ import { buildCliClient, buildRecordPayload, callApi, requireResource } from '../lib/command.mjs';
15
+ import { outputMode, printJson, printYaml, printRecord, success } from '../lib/output.mjs';
16
+
17
+ export const USAGE = `\
18
+ Usage: zeyos create <resource> [options]
19
+
20
+ Create a new record.
21
+
22
+ Arguments:
23
+ resource Resource name (e.g. ticket, account)
24
+
25
+ Options:
26
+ --data <json> Record fields as a JSON object
27
+ --<field> <value> Set individual fields e.g. --name "My Ticket" --status 1
28
+ --json Output created record as JSON
29
+ --yaml Output created record as YAML
30
+ -h, --help Show this help
31
+
32
+ Examples:
33
+ zeyos create ticket --name "Fix login bug" --status 0 --priority 2
34
+ zeyos create account --data '{"lastname":"Acme Corp","email":"info@acme.com"}'
35
+ `;
36
+
37
+ export async function run(values, positional) {
38
+ const resourceName = positional[0];
39
+ const res = requireResource(resourceName, 'zeyos create <resource>', 'create', 'creation');
40
+
41
+ // ── Build data payload ─────────────────────────────────────────────────────
42
+ // Validate input before requiring credentials. positional[1] is the
43
+ // (optional) JSON body some callers pass positionally instead of via --data.
44
+ const data = buildRecordPayload(values, positional[1]);
45
+
46
+ const clientState = buildCliClient();
47
+
48
+ // ── Call API ───────────────────────────────────────────────────────────────
49
+ const record = await callApi(clientState, res.create, data);
50
+
51
+ const mode = outputMode(values);
52
+
53
+ if (mode === 'json') {
54
+ printJson(record);
55
+ } else if (mode === 'yaml') {
56
+ printYaml(record);
57
+ } else {
58
+ const id = record?.ID ?? record?.id ?? '?';
59
+ success(`Created ${resourceName} #${id}.`);
60
+ if (record) printRecord(record, res.fields);
61
+ }
62
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * zeyos delete <resource> <id>
3
+ *
4
+ * Delete a record by ID.
5
+ *
6
+ * Options:
7
+ * --force Skip confirmation prompt
8
+ */
9
+
10
+ import { createInterface } from 'node:readline';
11
+ import { buildCliClient, callApi, requireRecordId, requireResource } from '../lib/command.mjs';
12
+ import { success, warn } from '../lib/output.mjs';
13
+
14
+ export const USAGE = `\
15
+ Usage: zeyos delete <resource> <id> [options]
16
+
17
+ Delete a record by ID.
18
+
19
+ Arguments:
20
+ resource Resource name (e.g. ticket, account)
21
+ id Record ID
22
+
23
+ Options:
24
+ --force Skip confirmation prompt
25
+ -h, --help Show this help
26
+
27
+ Examples:
28
+ zeyos delete ticket 42
29
+ zeyos delete account 7 --force
30
+ `;
31
+
32
+ export async function run(values, positional) {
33
+ const resourceName = positional[0];
34
+ const id = positional[1];
35
+
36
+ const res = requireResource(resourceName, 'zeyos delete <resource> <id>', 'delete', 'deletion');
37
+ requireRecordId(id, 'zeyos delete <resource> <id>');
38
+
39
+ // ── Confirmation ───────────────────────────────────────────────────────────
40
+ if (!values.force) {
41
+ const confirmed = await _confirm(`Delete ${resourceName} #${id}? [y/N] `);
42
+ if (!confirmed) {
43
+ warn('Aborted.');
44
+ return;
45
+ }
46
+ }
47
+
48
+ const clientState = buildCliClient();
49
+
50
+ // ── Call API ───────────────────────────────────────────────────────────────
51
+ await callApi(clientState, res.delete, { ID: id }, {
52
+ notFoundMessage: `${resourceName} #${id} not found.`
53
+ });
54
+
55
+ success(`Deleted ${resourceName} #${id}.`);
56
+ }
57
+
58
+ // ── Helpers ───────────────────────────────────────────────────────────────────
59
+
60
+ function _confirm(prompt) {
61
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
62
+ return new Promise(resolve => {
63
+ rl.question(prompt, answer => {
64
+ rl.close();
65
+ resolve(answer.trim().toLowerCase() === 'y');
66
+ });
67
+ });
68
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * zeyos describe <resource>
3
+ *
4
+ * Print a resource's schema — fields, types, foreign keys, enums — straight
5
+ * from the generated schema. Works offline (no login required), so an agent
6
+ * can discover the data model before making any call.
7
+ */
8
+
9
+ import { createZeyosClient } from '@zeyos/client';
10
+ import { resolveResource } from '../lib/resources.mjs';
11
+ import { colors as c, outputMode, printJson, printYaml, printTable, error } from '../lib/output.mjs';
12
+
13
+ export const USAGE = `\
14
+ Usage: zeyos describe <resource> [options]
15
+
16
+ Show the fields, types, foreign keys and enum values for a resource.
17
+ Runs offline — no authentication required.
18
+
19
+ Arguments:
20
+ resource Resource name (e.g. ticket, tickets, account)
21
+
22
+ Options:
23
+ --json Output as JSON
24
+ --yaml Output as YAML
25
+ -h, --help Show this help
26
+
27
+ Examples:
28
+ zeyos describe tickets
29
+ zeyos describe accounts --json
30
+ `;
31
+
32
+ let cachedSchema;
33
+ function schema() {
34
+ if (!cachedSchema) {
35
+ cachedSchema = createZeyosClient({ auth: { mode: 'none' } }).schema;
36
+ }
37
+ return cachedSchema;
38
+ }
39
+
40
+ /**
41
+ * Resolve a user-supplied resource name to the canonical schema key, honoring the
42
+ * same singular/plural/alias rules the other commands use (`create`, `update`,
43
+ * `list` go through resolveResource). Without this, `describe` was the lone command
44
+ * that rejected singular/alias names — `describe ticket` failed while `create
45
+ * ticket` worked. Order: exact schema match → CLI registry → bridge an operationId
46
+ * back to its schema resource.
47
+ */
48
+ function schemaKeyFor(s, input) {
49
+ if (s.describe(input)) return input;
50
+ const def = resolveResource(input);
51
+ if (def) {
52
+ const op = def.list || def.get || def.create || def.update || def.delete;
53
+ const key = op ? s.resourceForOperation(op) : null;
54
+ if (key && s.describe(key)) return key;
55
+ }
56
+ return null;
57
+ }
58
+
59
+ export function run(values, positional = []) {
60
+ const resource = positional[0];
61
+ const s = schema();
62
+
63
+ if (!resource) {
64
+ error('A resource is required. Example: zeyos describe tickets (run "zeyos resources" to list common ones)');
65
+ process.exit(1);
66
+ }
67
+
68
+ const key = schemaKeyFor(s, resource);
69
+ if (!key) {
70
+ error(`Unknown resource "${resource}". Run "zeyos resources" for common resources.`);
71
+ process.exit(1);
72
+ }
73
+ const def = s.describe(key);
74
+
75
+ const mode = outputMode(values);
76
+ if (mode === 'json') {
77
+ printJson(def);
78
+ return;
79
+ }
80
+ if (mode === 'yaml') {
81
+ printYaml(def);
82
+ return;
83
+ }
84
+
85
+ const rows = Object.entries(def.fields).map(([name, field]) => {
86
+ const notes = [];
87
+ if (field.fk) notes.push(`→ ${field.fk}`);
88
+ if (field.indexed) notes.push('indexed');
89
+ if (field.enum) {
90
+ notes.push('enum: ' + Object.entries(field.enum).map(([k, v]) => `${k}=${v}`).join(' '));
91
+ }
92
+ return { field: name, type: field.type, notes: notes.join(' ') };
93
+ });
94
+
95
+ const operations = s.operations(key);
96
+
97
+ process.stdout.write(`\n ${c.bold(def.name)} ${c.dim(`(${def.type}, ${rows.length} fields)`)}\n`);
98
+ printTable(rows, ['field', 'type', 'notes']);
99
+ if (operations.length > 0) {
100
+ process.stdout.write(` ${c.bold('operations')} ${c.dim(operations.join(', '))}\n\n`);
101
+ }
102
+ }