hey-axi 0.2.0 → 0.3.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/README.md +31 -15
- package/package.json +1 -1
- package/skills/hey-axi/SKILL.md +48 -45
- package/skills/hey-axi/references/commands.md +39 -36
- package/src/activity.js +160 -0
- package/src/args.js +75 -10
- package/src/arity.js +94 -0
- package/src/cli.js +373 -136
- package/src/errors.js +128 -31
- package/src/examples.js +57 -0
- package/src/guide.js +21 -2
- package/src/home-path.js +12 -0
- package/src/home.js +48 -16
- package/src/hooks.js +176 -23
- package/src/manifest.json +699 -251
- package/src/policy.js +154 -11
- package/src/router.js +45 -11
- package/src/scope.js +177 -0
- package/src/shape.js +197 -56
- package/src/text.js +3 -3
- package/src/version.js +1 -1
package/src/shape.js
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
// Shape HEY's JSON
|
|
2
|
-
// -
|
|
1
|
+
// Shape HEY's JSON for agents (AXI principles 2-5 and 9):
|
|
2
|
+
// - at most four default fields per list command, `--fields a,b,c` to choose, `--fields all`
|
|
3
3
|
// - long text truncated with its total size, `--full` to get HEY's output untouched
|
|
4
|
-
// -
|
|
5
|
-
//
|
|
4
|
+
// - every list gets a `count` (`N of T total`, `N total`, or `N shown; more available`)
|
|
5
|
+
// and a definitive `empty` line when it has no rows, enveloped or not
|
|
6
|
+
// - HEY's breadcrumbs as `help` lines that name hey-axi commands and carry the
|
|
7
|
+
// invocation's selectors (--account, --base-url)
|
|
6
8
|
import { heyToAxi } from "./text.js";
|
|
7
9
|
|
|
8
10
|
// A field is "key" or "key=path", where path is dotted (`creator.name`), may index an
|
|
9
11
|
// array (`messages.0.creator.name`), may map over one (`recipients.to[].email_address`,
|
|
10
12
|
// joined with ", "), and may list fallbacks (`name|subject`).
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
// Box rows keep both ids: `id` (the box item) feeds seen/unseen/move/label, `topic_id`
|
|
14
|
+
// (the thread) feeds thread read/reply/forward. `seen` and `at` are one --fields away.
|
|
15
|
+
const POSTINGS = ["id", "topic_id", "from=creator.name|sender.name|alternative_sender_name", "subject=name|subject"];
|
|
16
|
+
const EVENTS = ["id", "title", "starts_at", "ends_at"];
|
|
13
17
|
const NAMED = ["id", "name"];
|
|
14
18
|
|
|
15
19
|
export const LIST_FIELDS = {
|
|
@@ -24,17 +28,17 @@ export const LIST_FIELDS = {
|
|
|
24
28
|
"set-aside view": POSTINGS,
|
|
25
29
|
"set-aside group view": POSTINGS,
|
|
26
30
|
"contact threads": POSTINGS,
|
|
27
|
-
"bubble list": [
|
|
31
|
+
"bubble list": ["id", "topic_id", "subject=name|subject", "bubbles_up=bubble_up_schedule.bubble_up_at"],
|
|
28
32
|
"box list": ["id", "kind", "name"],
|
|
29
|
-
search: ["
|
|
30
|
-
"screener list": ["id", "
|
|
31
|
-
"screener history": ["id", "status", "
|
|
33
|
+
search: ["topic_id", "subject", "from=messages.0.creator.name|creator.name", "at=updated_at|created_at"],
|
|
34
|
+
"screener list": ["id", "email_address", "subject", "topic_id"],
|
|
35
|
+
"screener history": ["id", "status", "email_address=email_address|petitioner.email_address", "decided=decided|updated_at"],
|
|
32
36
|
"event list": EVENTS,
|
|
33
37
|
"event day": EVENTS,
|
|
34
38
|
"event week": EVENTS,
|
|
35
39
|
"todo list": ["id", "title", "starts_at", "completed_at"],
|
|
36
40
|
"habit list": ["id", "title=title|name", "days"],
|
|
37
|
-
"journal list": ["id", "starts_at"
|
|
41
|
+
"journal list": ["id", "date=date|starts_at"],
|
|
38
42
|
"draft list": ["id", "subject", "summary", "at=updated_at|created_at"],
|
|
39
43
|
"contact list": ["id", "name", "email_address"],
|
|
40
44
|
"label list": NAMED,
|
|
@@ -42,15 +46,23 @@ export const LIST_FIELDS = {
|
|
|
42
46
|
"workflow list": NAMED,
|
|
43
47
|
"calendar list": ["id", "name", "kind", "owned"],
|
|
44
48
|
"attachment list": ["id", "filename", "content_type", "byte_size"],
|
|
45
|
-
"clip list": ["id", "
|
|
46
|
-
"snippet list": ["id", "name"
|
|
47
|
-
"timetrack list": ["id", "starts_at", "ends_at", "category"
|
|
49
|
+
"clip list": ["id", "topic_id=topic_id|topic.id", "at=created_at"],
|
|
50
|
+
"snippet list": ["id", "name"],
|
|
51
|
+
"timetrack list": ["id", "starts_at", "ends_at", "category"],
|
|
48
52
|
"timetrack categories": ["id", "title"],
|
|
49
53
|
"account list": ["id", "email=email|email_address", "name", "active"],
|
|
50
54
|
"set-aside group list": ["id", "thread_count"],
|
|
51
55
|
};
|
|
52
56
|
|
|
53
57
|
// Detail views keep their long text (truncated), but drop URLs and duplicated fields.
|
|
58
|
+
// Lists whose entries are long-form content: the list shows ids and dates only, and a help
|
|
59
|
+
// line says how to get the content (AXI principle 2: long text belongs in detail views).
|
|
60
|
+
export const CONTENT_LISTS = {
|
|
61
|
+
"journal list": (line, carry) => `Run \`${withSelectors("hey-axi journal read <date>", carry)}\` to read one entry, or \`${line} --fields id,starts_at,content\` to include previews`,
|
|
62
|
+
"clip list": (line) => `Run \`${line} --fields id,content\` to include each clip's content`,
|
|
63
|
+
"snippet list": (line) => `Run \`${line} --fields id,name,content\` to include each snippet's content`,
|
|
64
|
+
};
|
|
65
|
+
|
|
54
66
|
export const DETAIL_FIELDS = {
|
|
55
67
|
"thread read": ["id", "at=created_at", "from=creator.name|sender.name", "email=creator.email_address|sender.email_address", "to=recipients.to[].email_address", "cc=recipients.cc[].email_address", "body=body|summary"],
|
|
56
68
|
};
|
|
@@ -88,12 +100,15 @@ export function parseField(spec) {
|
|
|
88
100
|
return index === -1 ? { key: spec, path: spec } : { key: spec.slice(0, index), path: spec.slice(index + 1) };
|
|
89
101
|
}
|
|
90
102
|
|
|
91
|
-
|
|
103
|
+
export const MAX_LIST_FIELDS = 4;
|
|
104
|
+
|
|
105
|
+
// Fields for a list without a declared schema: up to four familiar keys, or the first
|
|
106
|
+
// four keys when none of the familiar ones are there.
|
|
92
107
|
function heuristicFields(items) {
|
|
93
|
-
const keys = new Set(items.flatMap((item) => (item && typeof item === "object" ? Object.keys(item) : [])));
|
|
94
|
-
if (keys.
|
|
95
|
-
const picked = HEURISTIC_KEYS.filter((key) => keys.
|
|
96
|
-
return picked.length >= 2 ? picked :
|
|
108
|
+
const keys = [...new Set(items.flatMap((item) => (item && typeof item === "object" ? Object.keys(item) : [])))];
|
|
109
|
+
if (keys.length <= MAX_LIST_FIELDS) return null;
|
|
110
|
+
const picked = HEURISTIC_KEYS.filter((key) => keys.includes(key)).slice(0, MAX_LIST_FIELDS);
|
|
111
|
+
return picked.length >= 2 ? picked : keys.slice(0, MAX_LIST_FIELDS);
|
|
97
112
|
}
|
|
98
113
|
|
|
99
114
|
// Project items onto fields. Returns { rows, unknown } where unknown lists requested
|
|
@@ -127,15 +142,22 @@ export function truncateDeep(value, limit, onTruncate, list = false) {
|
|
|
127
142
|
}
|
|
128
143
|
|
|
129
144
|
const isObjectList = (value) => Array.isArray(value) && value.every((item) => item && typeof item === "object" && !Array.isArray(item));
|
|
130
|
-
|
|
145
|
+
// URLs and paging fields (summarized by `count` and the "see the rest" help) are left out.
|
|
146
|
+
const DROP_CONTAINER_KEY = (key) => /(^|_)url$|^signed_stream_name$|^(total_count|next_page|next_cursor|has_more)$/.test(key);
|
|
131
147
|
|
|
132
|
-
// Find the collection in HEY's `data`: the array itself
|
|
133
|
-
// inside a container object (e.g. a box with its
|
|
148
|
+
// Find the collection in HEY's `data`: the array itself (of objects or of plain
|
|
149
|
+
// values), the one array of objects inside a container object (e.g. a box with its
|
|
150
|
+
// postings), or, when a container holds several, all of them (`lists`).
|
|
134
151
|
export function findList(data) {
|
|
135
152
|
if (isObjectList(data)) return { list: data };
|
|
136
|
-
if (
|
|
153
|
+
if (Array.isArray(data)) return { list: data, primitive: true };
|
|
154
|
+
if (data && typeof data === "object") {
|
|
137
155
|
const keys = Object.keys(data).filter((key) => isObjectList(data[key]));
|
|
138
156
|
if (keys.length === 1) return { list: data[keys[0]], key: keys[0], container: data };
|
|
157
|
+
if (keys.length > 1) return { lists: keys.map((key) => ({ key, list: data[key] })), container: data, list: keys.flatMap((key) => data[key]) };
|
|
158
|
+
// One list of plain values next to other fields ({items: ["a","b"], total_count: 40}).
|
|
159
|
+
const plain = Object.keys(data).filter((key) => Array.isArray(data[key]));
|
|
160
|
+
if (keys.length === 0 && plain.length === 1) return { list: data[plain[0]], key: plain[0], container: data, primitive: true };
|
|
139
161
|
}
|
|
140
162
|
return null;
|
|
141
163
|
}
|
|
@@ -170,6 +192,38 @@ export function shapeData(data, { path, fields = null }) {
|
|
|
170
192
|
if (!found) {
|
|
171
193
|
return { data: truncateDeep(data, DETAIL_TEXT_LIMIT, mark), truncated, empty: false };
|
|
172
194
|
}
|
|
195
|
+
if (found.primitive && !found.container) {
|
|
196
|
+
return { data: truncateDeep(found.list, LIST_TEXT_LIMIT, mark, true), truncated, empty: found.list.length === 0 };
|
|
197
|
+
}
|
|
198
|
+
if (found.primitive) {
|
|
199
|
+
const shaped = {};
|
|
200
|
+
for (const [name, value] of Object.entries(found.container)) {
|
|
201
|
+
if (name === found.key) shaped[name] = truncateDeep(value, LIST_TEXT_LIMIT, mark, true);
|
|
202
|
+
else if (fields === "all" || !DROP_CONTAINER_KEY(name)) shaped[name] = truncateDeep(value, DETAIL_TEXT_LIMIT, mark);
|
|
203
|
+
}
|
|
204
|
+
return { data: shaped, truncated, empty: found.list.length === 0 };
|
|
205
|
+
}
|
|
206
|
+
if (found.lists) {
|
|
207
|
+
// Several collections side by side: each gets the minimal schema of its own items.
|
|
208
|
+
const shaped = {};
|
|
209
|
+
for (const [name, value] of Object.entries(found.container)) {
|
|
210
|
+
const entry = found.lists.find((item) => item.key === name);
|
|
211
|
+
if (entry && Array.isArray(fields) && fields.length) {
|
|
212
|
+
// Explicit fields must exist in at least one of the lists.
|
|
213
|
+
const unknown = fields.filter((field) => !found.lists.some(({ list }) => project(list, [field], { strict: false }).rows.some((row) => Object.keys(row).length)));
|
|
214
|
+
if (unknown.length) throw new FieldError(unknown, [...new Set(found.lists.flatMap(({ list }) => list.slice(0, 5).flatMap((item) => Object.keys(item || {}))))]);
|
|
215
|
+
}
|
|
216
|
+
if (entry) {
|
|
217
|
+
const specs = fields === "all" ? null : Array.isArray(fields) && fields.length ? fields : heuristicFields(entry.list);
|
|
218
|
+
const rows = specs ? project(entry.list, specs).rows : entry.list;
|
|
219
|
+
shaped[name] = truncateDeep(rows, specs ? LIST_TEXT_LIMIT : DETAIL_TEXT_LIMIT, mark, Boolean(specs));
|
|
220
|
+
} else if (fields === "all" || !DROP_CONTAINER_KEY(name)) {
|
|
221
|
+
// Content next to the lists is kept (long text cut with its size), never dropped.
|
|
222
|
+
shaped[name] = truncateDeep(value, DETAIL_TEXT_LIMIT, mark);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return { data: shaped, truncated, empty: found.list.length === 0 };
|
|
226
|
+
}
|
|
173
227
|
|
|
174
228
|
const { list, key, container } = found;
|
|
175
229
|
let rows = list;
|
|
@@ -197,7 +251,8 @@ export function shapeData(data, { path, fields = null }) {
|
|
|
197
251
|
shaped = {};
|
|
198
252
|
for (const [name, value] of Object.entries(container)) {
|
|
199
253
|
if (name === key) shaped[name] = rows;
|
|
200
|
-
else if (fields === "all" ||
|
|
254
|
+
else if (fields === "all" || !DROP_CONTAINER_KEY(name)) {
|
|
255
|
+
// Content next to the list is kept (long text cut with its size), never dropped.
|
|
201
256
|
shaped[name] = truncateDeep(value, DETAIL_TEXT_LIMIT, mark);
|
|
202
257
|
}
|
|
203
258
|
}
|
|
@@ -205,47 +260,133 @@ export function shapeData(data, { path, fields = null }) {
|
|
|
205
260
|
return { data: shaped, truncated, empty: list.length === 0 };
|
|
206
261
|
}
|
|
207
262
|
|
|
208
|
-
|
|
209
|
-
|
|
263
|
+
const MORE_NOTICE = /more (are )?available|more results|use --all|--page/i;
|
|
264
|
+
const TOTAL_NOTICE = /\b\d[\d,]* of (\d[\d,]*)\b/;
|
|
265
|
+
|
|
266
|
+
// What HEY told us about the size of a list: { shown, total?, more, next? }. Sources, in
|
|
267
|
+
// order: meta.total_count, a "Showing N of T" notice, and HEY's paging signals
|
|
268
|
+
// (next_page, has_more, a "more available" notice). With none of them the list is complete.
|
|
269
|
+
export function listSize(envelope, shown, container = null) {
|
|
270
|
+
// HEY puts paging in meta, or (box and other postings listings) next to the list in data.
|
|
271
|
+
const ownMeta = envelope && typeof envelope.meta === "object" && envelope.meta ? envelope.meta : {};
|
|
272
|
+
const side = container && typeof container === "object" ? Object.fromEntries(["total_count", "next_page", "next_cursor", "has_more"].filter((key) => key in container).map((key) => [key, container[key]])) : {};
|
|
273
|
+
const meta = { ...side, ...ownMeta };
|
|
274
|
+
if (typeof meta.total_count === "number" && meta.total_count < shown) delete meta.total_count;
|
|
275
|
+
const next = meta.next_page ?? meta.next_cursor ?? null;
|
|
276
|
+
const noticed = String(envelope?.notice || "").match(TOTAL_NOTICE);
|
|
277
|
+
const total = typeof meta.total_count === "number" ? meta.total_count : noticed ? Number(noticed[1].replace(/,/g, "")) : undefined;
|
|
278
|
+
const more = total !== undefined ? total > shown : Boolean(next || meta.has_more || MORE_NOTICE.test(envelope?.notice || ""));
|
|
279
|
+
// Complete only when HEY says so: a total, has_more: false, or a null next_page.
|
|
280
|
+
const complete = total !== undefined ? !more : !more && (meta.has_more === false || ("next_page" in meta && meta.next_page === null) || ("next_cursor" in meta && meta.next_cursor === null));
|
|
281
|
+
return { shown, total, more, next, complete };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// `count: 25 of 847 total` / `count: 8 total` / `count: 25 shown; more available` /
|
|
285
|
+
// `count: 25 shown; no more pages reported` (no total and no explicit end signal:
|
|
286
|
+
// hey-axi does not claim the list is complete).
|
|
287
|
+
export function countLine({ shown, total, more, complete, all = false }) {
|
|
288
|
+
if (total !== undefined) return `${shown} of ${total} total`;
|
|
289
|
+
if (more) return `${shown} shown; more available`;
|
|
290
|
+
// An empty page is only "0 total" when HEY says nothing is left (or --all fetched everything).
|
|
291
|
+
return complete || all ? `${shown} total` : `${shown} shown; no more pages reported`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Append the invocation's selectors (`--account 2`) to a suggested command, unless the
|
|
295
|
+
// command already names them.
|
|
296
|
+
export function withSelectors(command, carry = []) {
|
|
297
|
+
const extra = [];
|
|
298
|
+
for (let i = 0; i < carry.length; i += 2) {
|
|
299
|
+
if (!command.includes(carry[i])) extra.push(carry[i], carry[i + 1]);
|
|
300
|
+
}
|
|
301
|
+
return extra.length ? `${command} ${extra.join(" ")}` : command;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Carry the selectors into every `hey-axi …` command quoted inside a text (a hint, a
|
|
305
|
+
// string breadcrumb, an error's help line).
|
|
306
|
+
export function carrySelectors(text, carry = []) {
|
|
307
|
+
if (typeof text !== "string" || !carry.length) return text;
|
|
308
|
+
if (/^hey-axi \S/.test(text) && !/[`'"]/.test(text)) return withSelectors(text, carry);
|
|
309
|
+
// `…` may contain quotes ("<query>"); '…' and "…" may not contain their own quote.
|
|
310
|
+
let out = text.replace(/`(hey-axi [^`]+)`/g, (match, command) => `\`${withSelectors(command, carry)}\``);
|
|
311
|
+
out = out.replace(/(^|[^`\w])(['"])(hey-axi [^'"`]+?)\2/g, (match, lead, quote, command) => `${lead}${quote}${withSelectors(command, carry)}${quote}`);
|
|
312
|
+
// Unquoted "Run hey-axi …" to the end of the sentence.
|
|
313
|
+
return out.replace(/(\bRun:? )(hey-axi [^`'".;,\n]+?)(?=[.;,]?\s*$|[.;,]\s)/g, (match, run, command) => `${run}${withSelectors(command.trim(), carry)}`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function breadcrumbHelp(crumb, carry) {
|
|
317
|
+
if (typeof crumb === "string") return carrySelectors(heyToAxi(crumb), carry);
|
|
210
318
|
if (!crumb || typeof crumb !== "object" || !crumb.command) return null;
|
|
211
319
|
const description = crumb.description ? ` to ${crumb.description.charAt(0).toLowerCase()}${crumb.description.slice(1)}` : "";
|
|
212
|
-
return `Run \`${heyToAxi(crumb.command)}\`${description}`;
|
|
320
|
+
return `Run \`${withSelectors(heyToAxi(crumb.command), carry)}\`${description}`;
|
|
213
321
|
}
|
|
214
322
|
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
let truncated = false;
|
|
221
|
-
const data = truncateDeep(envelope, DETAIL_TEXT_LIMIT, () => { truncated = true; });
|
|
222
|
-
if (!truncated) return data;
|
|
223
|
-
const help = [`Run \`${commandLine} --full\` to see complete content`];
|
|
224
|
-
return data && typeof data === "object" && !Array.isArray(data) ? { ...data, help } : { data, help };
|
|
225
|
-
}
|
|
323
|
+
// The help line that reveals the rest of a truncated list (AXI principle 9).
|
|
324
|
+
// The command line without one flag and its value (`--page 1`, `--page=1`).
|
|
325
|
+
function dropFlag(commandLine, name) {
|
|
326
|
+
return commandLine.replace(new RegExp(`\\s--${name}(=\\S+|\\s+(?!-)\\S+)?(?=\\s|$)`, "g"), "");
|
|
327
|
+
}
|
|
226
328
|
|
|
227
|
-
|
|
329
|
+
export function moreHelp(size, { commandLine: line, pageFlags = [] }) {
|
|
330
|
+
if (!size.more) return null;
|
|
331
|
+
// The hint replaces paging flags already on the line instead of repeating them.
|
|
332
|
+
const commandLine = dropFlag(dropFlag(line, "page"), "all");
|
|
333
|
+
const all = size.total !== undefined ? `all ${size.total}` : "all of them";
|
|
334
|
+
if (pageFlags.includes("all")) return `Run \`${commandLine} --all\` for ${all}`;
|
|
335
|
+
if (size.next && pageFlags.includes("page")) return `Run \`${commandLine} --page ${size.next}\` for the next page`;
|
|
336
|
+
if (pageFlags.includes("limit")) return `Run \`${dropFlag(commandLine, "limit")} --limit <n>\` to show more`;
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Shape whatever HEY printed. options: { path, fields, commandLine, carry, pageFlags, quiet }.
|
|
341
|
+
// carry: selector args from the invocation (["--account", "2"]) for suggested commands
|
|
342
|
+
// pageFlags: which of all/page/limit the command takes, for the "see the rest" hint
|
|
343
|
+
// quiet: drop HEY's summary, notice, breadcrumbs and meta; keep hey-axi's count,
|
|
344
|
+
// empty state and --full/--all hints
|
|
345
|
+
// Returns the object to print.
|
|
346
|
+
export function shapeEnvelope(envelope, { path, fields = null, commandLine, carry = [], pageFlags = [], quiet = false, all = false }) {
|
|
347
|
+
const enveloped = envelope && typeof envelope === "object" && !Array.isArray(envelope) && "data" in envelope;
|
|
348
|
+
const body = enveloped ? envelope.data : envelope;
|
|
349
|
+
const { data, truncated, empty } = shapeData(body, { path, fields });
|
|
350
|
+
const found = findList(body);
|
|
228
351
|
const out = {};
|
|
229
|
-
if (envelope.ok !== undefined) out.ok = envelope.ok;
|
|
230
|
-
if (envelope.summary) out.summary = envelope.summary;
|
|
231
|
-
const
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
352
|
+
if (enveloped && envelope.ok !== undefined) out.ok = envelope.ok;
|
|
353
|
+
if (enveloped && envelope.summary && !quiet) out.summary = envelope.summary;
|
|
354
|
+
const size = found && !found.lists ? listSize(enveloped ? envelope : null, found.list.length, found.container) : null;
|
|
355
|
+
if (size) out.count = countLine({ ...size, all });
|
|
356
|
+
let multiSize = null;
|
|
357
|
+
if (found?.lists) {
|
|
358
|
+
// Several lists: each one's length, plus whatever HEY says about the whole result.
|
|
359
|
+
multiSize = listSize(enveloped ? envelope : null, found.list.length, found.container);
|
|
360
|
+
const parts = found.lists.map(({ key, list }) => `${list.length} ${key}`).join(", ");
|
|
361
|
+
out.count = multiSize.total !== undefined ? `${parts} (${multiSize.total} total)` : multiSize.more ? `${parts}; more available` : parts;
|
|
362
|
+
}
|
|
363
|
+
const nothing = body === null || body === undefined || (typeof body === "object" && !Array.isArray(body) && Object.keys(body).length === 0);
|
|
364
|
+
if (!enveloped && !found && !truncated && !nothing) return data;
|
|
239
365
|
out.data = data;
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
out
|
|
366
|
+
const reported = size || multiSize;
|
|
367
|
+
if (empty && reported && (reported.more || reported.total > 0)) {
|
|
368
|
+
// An empty page while HEY reports more is not "nothing".
|
|
369
|
+
out.empty = `0 results on this page for \`${commandLine}\`; HEY reports ${reported.total > 0 ? `${reported.total} in total` : "more"}`;
|
|
370
|
+
} else if (empty) out.empty = `0 results for \`${commandLine}\``;
|
|
371
|
+
else if (nothing && !(enveloped && envelope.summary)) out.empty = `no data returned for \`${commandLine}\``;
|
|
372
|
+
const help = [];
|
|
373
|
+
if (enveloped && !quiet) {
|
|
374
|
+
for (const [key, value] of Object.entries(envelope)) {
|
|
375
|
+
if (["ok", "data", "summary", "meta", "breadcrumbs", "notice"].includes(key)) continue;
|
|
376
|
+
out[key] = value;
|
|
377
|
+
}
|
|
378
|
+
if (envelope.notice) out.notice = carrySelectors(heyToAxi(envelope.notice), carry);
|
|
379
|
+
help.push(...(Array.isArray(envelope.breadcrumbs) ? envelope.breadcrumbs : []).map((crumb) => breadcrumbHelp(crumb, carry)).filter(Boolean));
|
|
244
380
|
}
|
|
245
|
-
|
|
246
|
-
|
|
381
|
+
const more = (size || multiSize) && moreHelp(size || multiSize, { commandLine, pageFlags });
|
|
382
|
+
if (more) help.push(more);
|
|
247
383
|
if (truncated) help.push(`Run \`${commandLine} --full\` to see complete content`);
|
|
384
|
+
if (CONTENT_LISTS[path] && !fields && !empty && found) help.push(CONTENT_LISTS[path](commandLine, carry));
|
|
248
385
|
if (help.length) out.help = help;
|
|
249
|
-
if (
|
|
386
|
+
if (enveloped && !quiet && envelope.meta && typeof envelope.meta === "object") {
|
|
387
|
+
const meta = { ...envelope.meta };
|
|
388
|
+
for (const key of ["total_count", "pages_fetched", "next_page", "next_cursor", "has_more"]) delete meta[key];
|
|
389
|
+
if (Object.keys(meta).length) out.meta = meta;
|
|
390
|
+
}
|
|
250
391
|
return out;
|
|
251
392
|
}
|
package/src/text.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// Small text helpers shared by output, help and errors.
|
|
2
2
|
|
|
3
3
|
// Point command suggestions at hey-axi instead of the wrapped CLI: "hey thread read 1"
|
|
4
|
-
// becomes "hey-axi thread read 1". Only `hey` followed by a lowercase command word
|
|
5
|
-
// rewritten, so prose ("Hey there", "hey.com") is left alone.
|
|
4
|
+
// becomes "hey-axi thread read 1". Only `hey` followed by a lowercase command word or a
|
|
5
|
+
// flag is rewritten, so prose ("Hey there", "hey.com") is left alone.
|
|
6
6
|
export function heyToAxi(text) {
|
|
7
7
|
if (typeof text !== "string") return text;
|
|
8
|
-
return text.replace(/(^|[\s`'"(])hey (?=[a-z][a-z-]
|
|
8
|
+
return text.replace(/(^|[\s`'"(])hey (?=[a-z][a-z-]*|--?[a-z])/g, "$1hey-axi ");
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
// Quote an argv word for display in a suggested command line.
|
package/src/version.js
CHANGED