@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 +24 -1
- package/bin/zeyos.mjs +139 -27
- package/commands/count.mjs +15 -7
- package/commands/create.mjs +12 -5
- package/commands/delete.mjs +8 -3
- package/commands/describe.mjs +22 -1
- package/commands/doctor.mjs +186 -0
- package/commands/get.mjs +17 -4
- package/commands/list.mjs +34 -11
- package/commands/login.mjs +33 -8
- package/commands/logout.mjs +12 -7
- package/commands/profile.mjs +211 -0
- package/commands/skills.mjs +3 -2
- package/commands/update.mjs +11 -4
- package/commands/whoami.mjs +9 -2
- package/config/actionstep.json +21 -0
- package/config/customfield.json +17 -0
- package/config/message.json +20 -0
- package/config/task.json +4 -2
- package/config/ticket.json +2 -1
- package/lib/client.mjs +19 -10
- package/lib/command.mjs +79 -4
- package/lib/config.mjs +280 -45
- package/lib/flags.mjs +3 -3
- package/lib/output.mjs +194 -4
- package/lib/resources.mjs +26 -3
- package/package.json +2 -2
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/lib/resources.mjs
CHANGED
|
@@ -14,13 +14,21 @@
|
|
|
14
14
|
|
|
15
15
|
/** @type {Record<string, ResourceDef>} */
|
|
16
16
|
const REGISTRY = {
|
|
17
|
+
actionstep: {
|
|
18
|
+
list: 'listActionSteps',
|
|
19
|
+
get: 'getActionStep',
|
|
20
|
+
create: 'createActionStep',
|
|
21
|
+
update: 'updateActionStep',
|
|
22
|
+
delete: 'deleteActionStep',
|
|
23
|
+
fields: ['ID', 'actionnum', 'name', 'status', 'date', 'duedate', 'effort', 'ticket', 'task', 'account'],
|
|
24
|
+
},
|
|
17
25
|
ticket: {
|
|
18
26
|
list: 'listTickets',
|
|
19
27
|
get: 'getTicket',
|
|
20
28
|
create: 'createTicket',
|
|
21
29
|
update: 'updateTicket',
|
|
22
30
|
delete: 'deleteTicket',
|
|
23
|
-
fields: ['ID', 'ticketnum', 'name', 'status', 'priority', 'duedate', 'lastmodified'],
|
|
31
|
+
fields: ['ID', 'ticketnum', 'name', 'status', 'priority', 'duedate', 'account', 'project', 'lastmodified'],
|
|
24
32
|
},
|
|
25
33
|
task: {
|
|
26
34
|
list: 'listTasks',
|
|
@@ -28,7 +36,7 @@ const REGISTRY = {
|
|
|
28
36
|
create: 'createTask',
|
|
29
37
|
update: 'updateTask',
|
|
30
38
|
delete: 'deleteTask',
|
|
31
|
-
fields: ['ID', 'tasknum', 'name', 'status', 'priority', 'duedate', 'ticket'],
|
|
39
|
+
fields: ['ID', 'tasknum', 'name', 'status', 'priority', 'duedate', 'ticket', 'project', 'projectedeffort'],
|
|
32
40
|
},
|
|
33
41
|
account: {
|
|
34
42
|
list: 'listAccounts',
|
|
@@ -84,7 +92,7 @@ const REGISTRY = {
|
|
|
84
92
|
create: 'createMessage',
|
|
85
93
|
update: 'updateMessage',
|
|
86
94
|
delete: 'deleteMessage',
|
|
87
|
-
fields: ['ID', 'subject', '
|
|
95
|
+
fields: ['ID', 'date', 'mailbox', 'subject', 'sender_email', 'to_email', 'ticket', 'reference', 'messageid'],
|
|
88
96
|
},
|
|
89
97
|
item: {
|
|
90
98
|
list: 'listItems',
|
|
@@ -144,6 +152,11 @@ const REGISTRY = {
|
|
|
144
152
|
delete: 'deleteCampaign',
|
|
145
153
|
fields: ['ID', 'name', 'status', 'startdate', 'enddate'],
|
|
146
154
|
},
|
|
155
|
+
customfield: {
|
|
156
|
+
list: 'listCustomFields',
|
|
157
|
+
get: 'getCustomField',
|
|
158
|
+
fields: ['ID', 'name', 'identifier', 'context', 'reference', 'type', 'entity', 'activity'],
|
|
159
|
+
},
|
|
147
160
|
file: {
|
|
148
161
|
list: 'listFiles',
|
|
149
162
|
get: 'getFile',
|
|
@@ -174,6 +187,13 @@ const REGISTRY = {
|
|
|
174
187
|
|
|
175
188
|
const ALIASES = {
|
|
176
189
|
// Plurals
|
|
190
|
+
actionsteps: 'actionstep',
|
|
191
|
+
'action-steps': 'actionstep',
|
|
192
|
+
action_steps: 'actionstep',
|
|
193
|
+
timeentry: 'actionstep',
|
|
194
|
+
timeentries: 'actionstep',
|
|
195
|
+
'time-entry': 'actionstep',
|
|
196
|
+
'time-entries': 'actionstep',
|
|
177
197
|
tickets: 'ticket',
|
|
178
198
|
tasks: 'task',
|
|
179
199
|
accounts: 'account',
|
|
@@ -193,6 +213,9 @@ const ALIASES = {
|
|
|
193
213
|
payments: 'payment',
|
|
194
214
|
opportunities:'opportunity',
|
|
195
215
|
campaigns: 'campaign',
|
|
216
|
+
customfields: 'customfield',
|
|
217
|
+
custom_fields: 'customfield',
|
|
218
|
+
'custom-fields': 'customfield',
|
|
196
219
|
files: 'file',
|
|
197
220
|
invitations: 'invitation',
|
|
198
221
|
storages: 'storage',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeyos/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Command-line interface for the ZeyOS API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"node": ">=18.3"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@zeyos/client": "^0.
|
|
42
|
+
"@zeyos/client": "^0.3.0"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
45
|
"test": "node --test test/offline.mjs"
|