@skillcdn/console 0.3.1 → 0.4.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.
Files changed (179) hide show
  1. package/README.md +4 -4
  2. package/dist/api.d.ts +4 -4
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +4 -4
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli/cli.d.ts +4 -0
  7. package/dist/cli/cli.d.ts.map +1 -1
  8. package/dist/cli/cli.js +201 -25
  9. package/dist/cli/cli.js.map +1 -1
  10. package/dist/cli/format.d.ts.map +1 -1
  11. package/dist/cli/format.js +15 -11
  12. package/dist/cli/format.js.map +1 -1
  13. package/dist/cli/help.d.ts.map +1 -1
  14. package/dist/cli/help.js +59 -26
  15. package/dist/cli/help.js.map +1 -1
  16. package/dist/cli/main.js +1 -0
  17. package/dist/cli/main.js.map +1 -1
  18. package/dist/cli/pack.d.ts +7 -2
  19. package/dist/cli/pack.d.ts.map +1 -1
  20. package/dist/cli/pack.js +16 -2
  21. package/dist/cli/pack.js.map +1 -1
  22. package/dist/client.d.ts +20 -10
  23. package/dist/client.d.ts.map +1 -1
  24. package/dist/client.js +17 -3
  25. package/dist/client.js.map +1 -1
  26. package/dist/compat.d.ts +28 -6
  27. package/dist/compat.d.ts.map +1 -1
  28. package/dist/compat.js +55 -14
  29. package/dist/compat.js.map +1 -1
  30. package/dist/components/board.d.ts +12 -1
  31. package/dist/components/board.d.ts.map +1 -1
  32. package/dist/components/board.js +57 -7
  33. package/dist/components/board.js.map +1 -1
  34. package/dist/components/calendar.d.ts +17 -7
  35. package/dist/components/calendar.d.ts.map +1 -1
  36. package/dist/components/calendar.js +89 -38
  37. package/dist/components/calendar.js.map +1 -1
  38. package/dist/components/decision-form.d.ts +12 -4
  39. package/dist/components/decision-form.d.ts.map +1 -1
  40. package/dist/components/decision-form.js +24 -6
  41. package/dist/components/decision-form.js.map +1 -1
  42. package/dist/components/decision-list.d.ts +4 -0
  43. package/dist/components/decision-list.d.ts.map +1 -1
  44. package/dist/components/decision-list.js +9 -6
  45. package/dist/components/decision-list.js.map +1 -1
  46. package/dist/components/documents.d.ts.map +1 -1
  47. package/dist/components/documents.js +3 -3
  48. package/dist/components/documents.js.map +1 -1
  49. package/dist/components/event-feed.d.ts.map +1 -1
  50. package/dist/components/event-feed.js +63 -8
  51. package/dist/components/event-feed.js.map +1 -1
  52. package/dist/components/my-decisions.d.ts +20 -0
  53. package/dist/components/my-decisions.d.ts.map +1 -0
  54. package/dist/components/my-decisions.js +36 -0
  55. package/dist/components/my-decisions.js.map +1 -0
  56. package/dist/components/people.d.ts.map +1 -1
  57. package/dist/components/people.js +1 -1
  58. package/dist/components/people.js.map +1 -1
  59. package/dist/components/picker.d.ts +39 -0
  60. package/dist/components/picker.d.ts.map +1 -0
  61. package/dist/components/picker.js +155 -0
  62. package/dist/components/picker.js.map +1 -0
  63. package/dist/components/projects.d.ts.map +1 -1
  64. package/dist/components/projects.js +3 -3
  65. package/dist/components/projects.js.map +1 -1
  66. package/dist/components/roadmap.js +1 -1
  67. package/dist/components/roadmap.js.map +1 -1
  68. package/dist/components/runs.d.ts.map +1 -1
  69. package/dist/components/runs.js +1 -1
  70. package/dist/components/runs.js.map +1 -1
  71. package/dist/components/shell.d.ts.map +1 -1
  72. package/dist/components/shell.js +36 -1
  73. package/dist/components/shell.js.map +1 -1
  74. package/dist/components/task-form.d.ts +18 -4
  75. package/dist/components/task-form.d.ts.map +1 -1
  76. package/dist/components/task-form.js +56 -13
  77. package/dist/components/task-form.js.map +1 -1
  78. package/dist/components/task-list.js +2 -2
  79. package/dist/components/task-list.js.map +1 -1
  80. package/dist/components/task-view.d.ts +7 -1
  81. package/dist/components/task-view.d.ts.map +1 -1
  82. package/dist/components/task-view.js +7 -6
  83. package/dist/components/task-view.js.map +1 -1
  84. package/dist/components/ui.d.ts +17 -0
  85. package/dist/components/ui.d.ts.map +1 -1
  86. package/dist/components/ui.js +26 -11
  87. package/dist/components/ui.js.map +1 -1
  88. package/dist/components/work.d.ts.map +1 -1
  89. package/dist/components/work.js +5 -3
  90. package/dist/components/work.js.map +1 -1
  91. package/dist/console.css +1794 -433
  92. package/dist/console.d.ts.map +1 -1
  93. package/dist/console.js +93 -10
  94. package/dist/console.js.map +1 -1
  95. package/dist/data.d.ts +21 -1
  96. package/dist/data.d.ts.map +1 -1
  97. package/dist/data.js +24 -1
  98. package/dist/data.js.map +1 -1
  99. package/dist/i18n/en.d.ts +45 -3
  100. package/dist/i18n/en.d.ts.map +1 -1
  101. package/dist/i18n/en.js +52 -10
  102. package/dist/i18n/en.js.map +1 -1
  103. package/dist/i18n/ko.d.ts.map +1 -1
  104. package/dist/i18n/ko.js +54 -12
  105. package/dist/i18n/ko.js.map +1 -1
  106. package/dist/index.d.ts +5 -3
  107. package/dist/index.d.ts.map +1 -1
  108. package/dist/index.js +5 -3
  109. package/dist/index.js.map +1 -1
  110. package/dist/limits.d.ts +2 -0
  111. package/dist/limits.d.ts.map +1 -1
  112. package/dist/limits.js +2 -0
  113. package/dist/limits.js.map +1 -1
  114. package/dist/router.d.ts +9 -1
  115. package/dist/router.d.ts.map +1 -1
  116. package/dist/router.js +12 -0
  117. package/dist/router.js.map +1 -1
  118. package/dist/routes.d.ts +3 -1
  119. package/dist/routes.d.ts.map +1 -1
  120. package/dist/routes.js +3 -1
  121. package/dist/routes.js.map +1 -1
  122. package/dist/rules.js +1 -1
  123. package/dist/rules.js.map +1 -1
  124. package/dist/schemas.d.ts +252 -12
  125. package/dist/schemas.d.ts.map +1 -1
  126. package/dist/schemas.js +27 -10
  127. package/dist/schemas.js.map +1 -1
  128. package/dist/vocabulary.d.ts +2 -0
  129. package/dist/vocabulary.d.ts.map +1 -1
  130. package/dist/vocabulary.js +2 -0
  131. package/dist/vocabulary.js.map +1 -1
  132. package/docs/specs/cli.md +14 -13
  133. package/docs/specs/rest.md +24 -19
  134. package/package.json +2 -2
  135. package/skills/building-a-console/SKILL.md +4 -4
  136. package/skills/building-a-console/references/the-default-console.md +4 -4
  137. package/skills/building-a-console/references/the-look-from-a-website.md +5 -5
  138. package/skills/deploying-a-console/SKILL.md +2 -2
  139. package/skills/deploying-a-console/references/resources.md +1 -1
  140. package/skills/working-the-board/SKILL.md +8 -6
  141. package/skills/working-the-board/references/terms.md +3 -3
  142. package/src/api.ts +7 -0
  143. package/src/cli/cli.ts +254 -25
  144. package/src/cli/format.ts +22 -11
  145. package/src/cli/help.ts +63 -26
  146. package/src/cli/main.ts +1 -0
  147. package/src/cli/pack.ts +17 -1
  148. package/src/client.ts +44 -11
  149. package/src/compat.ts +63 -14
  150. package/src/components/board.tsx +124 -25
  151. package/src/components/calendar.tsx +258 -117
  152. package/src/components/decision-form.tsx +71 -23
  153. package/src/components/decision-list.tsx +30 -3
  154. package/src/components/documents.tsx +27 -11
  155. package/src/components/event-feed.tsx +116 -33
  156. package/src/components/my-decisions.tsx +138 -0
  157. package/src/components/people.tsx +3 -1
  158. package/src/components/picker.tsx +297 -0
  159. package/src/components/projects.tsx +7 -5
  160. package/src/components/roadmap.tsx +1 -1
  161. package/src/components/runs.tsx +5 -1
  162. package/src/components/shell.tsx +36 -1
  163. package/src/components/task-form.tsx +149 -63
  164. package/src/components/task-list.tsx +3 -3
  165. package/src/components/task-view.tsx +71 -56
  166. package/src/components/ui.tsx +89 -11
  167. package/src/components/work.tsx +6 -4
  168. package/src/console.tsx +157 -29
  169. package/src/data.ts +61 -1
  170. package/src/i18n/en.ts +54 -10
  171. package/src/i18n/ko.ts +56 -12
  172. package/src/index.ts +19 -1
  173. package/src/limits.ts +2 -0
  174. package/src/router.ts +18 -1
  175. package/src/routes.ts +3 -1
  176. package/src/rules.ts +1 -1
  177. package/src/schemas.ts +29 -9
  178. package/src/styles/console.css +1794 -433
  179. package/src/vocabulary.ts +3 -0
package/dist/cli/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { basename, extname } from "node:path";
2
2
  import { parseArgs } from "node:util";
3
3
  import { ApiError, createClient, } from "../client.js";
4
- import { CLIENT_RANGE_HEADER, judgeClient, parseVersion, VERSION_HEADER } from "../compat.js";
5
- import { addDays, dayOf as dayOfDate, isDay } from "../dates.js";
4
+ import { CLIENT_RANGE_HEADER, CONSOLE_AT_LEAST, consoleRange, formatRange, judgeClient, judgeConsole, parseVersion, VERSION_HEADER, formatVersion as versionText, } from "../compat.js";
5
+ import { addDays, dayOf as dayOfDate, firstDayOfWeek, isDay, weekOf } from "../dates.js";
6
6
  import { DEFAULT_PAGE_LIMIT, LIST_LIMIT, MAX_OPTIONS, MIN_OPTIONS } from "../limits.js";
7
7
  import { fillRules, RULES_PATH, RULES_TITLE } from "../rules.js";
8
8
  import { hostOfLogin, } from "../schemas.js";
@@ -606,10 +606,13 @@ const whoami = async (args, io) => {
606
606
  const verdict = command === undefined || server === undefined
607
607
  ? undefined
608
608
  : judgeClient(command, server).verdict;
609
+ // The oldest console this command works with (ADR-0038), as it stands within the command's line.
610
+ const atLeast = io.consoleAtLeast ?? CONSOLE_AT_LEAST;
611
+ const floor = command === undefined ? atLeast : versionText(consoleRange(command, atLeast).atLeast);
609
612
  // A console from before the rule says nothing of itself; the command says so instead.
610
613
  const consoleWords = me.console === undefined
611
614
  ? "the console does not say its version, so it is older than 0.1.18"
612
- : `the console is ${me.console.version} and takes ${me.console.clientRange}`;
615
+ : `the console is ${me.console.version} and takes ${me.console.clientRange}; this command works with a console from ${floor} on`;
613
616
  answer(io, session, {
614
617
  ...me,
615
618
  console: me.console ?? null,
@@ -673,6 +676,15 @@ const use = async (args, io) => {
673
676
  ].join("\n"));
674
677
  return EXIT.ok;
675
678
  };
679
+ /**
680
+ * The last day of this week (ADR-0039): the week begins as the workspace says, or, left to the
681
+ * language, as the person's language has it, else this machine's.
682
+ */
683
+ async function endOfWeek(io, session, today) {
684
+ const me = await session.client.me();
685
+ const language = me.language ?? io.locale ?? "en";
686
+ return weekOf(today, firstDayOfWeek(language, me.workspace.weekStart ?? "auto")).at(-1) ?? today;
687
+ }
676
688
  const tasks = async (args, io) => {
677
689
  const { values } = parse(args, {
678
690
  state: { type: "string" },
@@ -682,12 +694,13 @@ const tasks = async (args, io) => {
682
694
  category: { type: "string" },
683
695
  goal: { type: "string" },
684
696
  due: { type: "string" },
697
+ search: { type: "string" },
685
698
  archived: { type: "boolean" },
686
699
  ...PAGE_OPTIONS,
687
700
  }, false);
688
701
  const state = stateOf(text(values, "state"));
689
702
  if (on(values, "mine") && on(values, "unassigned")) {
690
- throw misuse("Say one: --mine for the tasks assigned to you, or --unassigned for those assigned to nobody");
703
+ throw misuse("Say one: --mine for the tasks you work on, or --unassigned for those nobody works on");
691
704
  }
692
705
  const page = pageOf(values);
693
706
  const session = await open(io, values);
@@ -700,19 +713,22 @@ const tasks = async (args, io) => {
700
713
  : due === "overdue"
701
714
  ? addDays(today, -1)
702
715
  : due === "week"
703
- ? addDays(today, 6)
716
+ ? await endOfWeek(io, session, today)
704
717
  : (dayOf(io, due) ?? undefined);
705
- const assignee = on(values, "mine")
706
- ? ((await personIdOf(session, "me")) ?? undefined)
707
- : undefined;
718
+ const worker = on(values, "mine") ? ((await personIdOf(session, "me")) ?? undefined) : undefined;
719
+ const search = text(values, "search")?.trim();
720
+ if (search === "") {
721
+ throw misuse("Say what to search for: console tasks --search <words>");
722
+ }
708
723
  const found = await project.tasks({
709
724
  ...(state === undefined ? {} : { state }),
710
725
  ...(on(values, "open") || due === "overdue" ? { open: true } : {}),
711
- ...(assignee === undefined ? {} : { assignee }),
726
+ ...(worker === undefined ? {} : { worker }),
712
727
  ...(on(values, "unassigned") ? { unassigned: true } : {}),
713
728
  ...(text(values, "category") === undefined ? {} : { category: text(values, "category") }),
714
729
  ...(text(values, "goal") === undefined ? {} : { goal: text(values, "goal") }),
715
730
  ...(dueBy === undefined ? {} : { dueBy }),
731
+ ...(search === undefined ? {} : { q: search }),
716
732
  ...(on(values, "archived") ? { archived: true } : {}),
717
733
  ...page,
718
734
  });
@@ -813,14 +829,41 @@ const skills = async (args, io) => {
813
829
  answer(io, session, { ...browsed, read: whole === undefined ? null : bodyOfFolder(whole) }, () => formatSkillBrowse(browsed, whole));
814
830
  return EXIT.ok;
815
831
  };
832
+ /** The most characters one read of a file asks for: what the console's API allows on one page. */
833
+ const MAX_FILE_LIMIT = 100_000;
834
+ /** A count given as a flag, within its bounds, or nothing; anything else is not understood. */
835
+ function countOf(given, name, bounds) {
836
+ if (given === undefined) {
837
+ return undefined;
838
+ }
839
+ const count = Number(given);
840
+ if (!Number.isInteger(count) ||
841
+ count < bounds.least ||
842
+ (bounds.most !== undefined && count > bounds.most)) {
843
+ const range = bounds.most === undefined ? `${bounds.least} or more` : `${bounds.least} to ${bounds.most}`;
844
+ throw misuse(`Not a number for --${name}, ${range}: ${given}`);
845
+ }
846
+ return count;
847
+ }
816
848
  const skill = async (args, io) => {
817
- const { values, positionals } = parse(args, { file: { type: "string" } });
849
+ const { values, positionals } = parse(args, {
850
+ file: { type: "string" },
851
+ offset: { type: "string" },
852
+ limit: { type: "string" },
853
+ });
818
854
  const file = text(values, "file");
819
855
  // A file is read by its own path from the repository's root; a skill's path beside it would be
820
856
  // ignored, which reads as if it mattered, so the two together are not understood.
821
857
  if (file !== undefined && positionals.length > 0) {
822
858
  throw misuse("Say one: console skill <path of its SKILL.md> for a skill, or console skill --file <path> for a file of the pack.");
823
859
  }
860
+ // A section is a page of a file read as asked (SkillCDN 0.4): the flags mean nothing without one.
861
+ if (file === undefined &&
862
+ (text(values, "offset") !== undefined || text(values, "limit") !== undefined)) {
863
+ throw misuse("--offset and --limit go with --file: console skill --file <path> [--offset <n>] [--limit <n>]");
864
+ }
865
+ const offset = countOf(text(values, "offset"), "offset", { least: 0 });
866
+ const limit = countOf(text(values, "limit"), "limit", { least: 1, most: MAX_FILE_LIMIT });
824
867
  const session = await open(io, values);
825
868
  const project = await session.project();
826
869
  const { listing, summary } = await packOf(project);
@@ -828,6 +871,14 @@ const skill = async (args, io) => {
828
871
  answer(io, session, listing, () => formatSkills(listing, io.skillsDir));
829
872
  return EXIT.ok;
830
873
  }
874
+ if (file !== undefined && (offset !== undefined || limit !== undefined)) {
875
+ // One page as asked, the section a search or an outline named; the result says where the
876
+ // file continues, and asks for nothing.
877
+ const page = await project.skillFile(file, { offset, limit });
878
+ const section = page.read === null ? undefined : joinFile([page], summary, limit);
879
+ answer(io, session, { ...page, read: section === undefined ? null : bodyOfFile(section) }, () => formatSkillFile(page, section));
880
+ return EXIT.ok;
881
+ }
831
882
  if (file !== undefined) {
832
883
  // The file whole, page after page, as the console reads it from the pack.
833
884
  const pages = [];
@@ -980,9 +1031,37 @@ const TASK_FIELDS = {
980
1031
  goal: { type: "string" },
981
1032
  starts: { type: "string" },
982
1033
  due: { type: "string" },
983
- assignee: { type: "string" },
1034
+ worker: { type: "string", multiple: true },
984
1035
  parent: { type: "string" },
985
1036
  };
1037
+ /**
1038
+ * The people `--worker` names, each a login or `me`, given again or with commas between for
1039
+ * several, in place of those there were; `none` alone for nobody; nothing when the flag was
1040
+ * not given.
1041
+ */
1042
+ async function workerIdsOf(session, given) {
1043
+ if (given.length === 0) {
1044
+ return undefined;
1045
+ }
1046
+ const names = given
1047
+ .flatMap((value) => value.split(","))
1048
+ .map((name) => name.trim())
1049
+ .filter((name) => name.length > 0);
1050
+ if (names.includes("none")) {
1051
+ if (names.length > 1) {
1052
+ throw misuse("Say --worker none alone, for nobody; or the logins of the workers.");
1053
+ }
1054
+ return [];
1055
+ }
1056
+ const ids = [];
1057
+ for (const name of names) {
1058
+ const id = await personIdOf(session, name);
1059
+ if (typeof id === "string" && !ids.includes(id)) {
1060
+ ids.push(id);
1061
+ }
1062
+ }
1063
+ return ids;
1064
+ }
986
1065
  /** What the flags say of a task's fields, as the API takes them; `none` clears a field. */
987
1066
  async function taskFieldsOf(io, session, project, values) {
988
1067
  const state = stateOf(text(values, "state"));
@@ -991,7 +1070,7 @@ async function taskFieldsOf(io, session, project, values) {
991
1070
  const goalId = orNone(text(values, "goal"));
992
1071
  const startsOn = dayOf(io, text(values, "starts"), true);
993
1072
  const dueOn = dayOf(io, text(values, "due"), true);
994
- const assigneeId = await personIdOf(session, text(values, "assignee"));
1073
+ const workerIds = await workerIdsOf(session, texts(values, "worker"));
995
1074
  const parent = text(values, "parent");
996
1075
  const parentId = parent === undefined ? undefined : parent === "none" ? null : (await project.task(parent)).id;
997
1076
  return {
@@ -1001,7 +1080,7 @@ async function taskFieldsOf(io, session, project, values) {
1001
1080
  ...(goalId === undefined ? {} : { goalId }),
1002
1081
  ...(startsOn === undefined ? {} : { startsOn }),
1003
1082
  ...(dueOn === undefined ? {} : { dueOn }),
1004
- ...(assigneeId === undefined ? {} : { assigneeId }),
1083
+ ...(workerIds === undefined ? {} : { workerIds }),
1005
1084
  ...(parentId === undefined ? {} : { parentId }),
1006
1085
  };
1007
1086
  }
@@ -1042,7 +1121,7 @@ const editTask = async (args, io) => {
1042
1121
  ...(await taskFieldsOf(io, session, project, values)),
1043
1122
  };
1044
1123
  if (Object.keys(patch).length === 0) {
1045
- throw misuse("Say what to change: --title, --body, --state, --priority, --category, --goal, --starts, --due, --assignee or --parent; none clears a field.");
1124
+ throw misuse("Say what to change: --title, --body, --state, --priority, --category, --goal, --starts, --due, --worker or --parent; none clears a field.");
1046
1125
  }
1047
1126
  const found = await project.task(ref);
1048
1127
  const changed = await project.updateTask(found.id, patch);
@@ -1308,8 +1387,13 @@ const ask = async (args, io) => {
1308
1387
  file: { type: "string" },
1309
1388
  run: { type: "string" },
1310
1389
  wait: { type: "string" },
1390
+ to: { type: "string" },
1311
1391
  });
1312
1392
  const question = theOne(positionals, 'Say the question: console ask "<question>" --option "<label>" --option "<label>"');
1393
+ const to = text(values, "to");
1394
+ if (to === "none") {
1395
+ throw misuse("Say whom the decision is for: --to <login|me>.");
1396
+ }
1313
1397
  const options = texts(values, "option");
1314
1398
  if (options.length < MIN_OPTIONS || options.length > MAX_OPTIONS) {
1315
1399
  throw misuse(`Give ${MIN_OPTIONS} to ${MAX_OPTIONS} options, one --option each.`);
@@ -1319,18 +1403,49 @@ const ask = async (args, io) => {
1319
1403
  const session = await open(io, values);
1320
1404
  const project = await session.project();
1321
1405
  const runId = await currentRun(project, values);
1406
+ // Whom the decision is for (ADR-0042): the person the agent works for, unless --to names another.
1407
+ const deciderId = await personIdOf(session, to);
1322
1408
  const raised = await project.raiseDecision({
1323
1409
  question,
1324
1410
  options: options.map((label) => ({ label })),
1325
1411
  runId,
1326
1412
  ...(body === undefined ? {} : { body }),
1413
+ ...(typeof deciderId === "string" ? { deciderId } : {}),
1327
1414
  });
1328
1415
  // The id is printed before any waiting, so that a wait cut short loses nothing.
1329
1416
  io.stdout(session.json
1330
1417
  ? `${JSON.stringify(raised, null, 2)}\n`
1331
- : `Decision ${raised.id} raised on run ${runId}; the run waits for a person.\ndecision id: ${raised.id}\n${formatOptions(raised)}\n`);
1418
+ : `Decision ${raised.id} raised on run ${runId}; the run waits for ${raised.decider.login}.\ndecision id: ${raised.id}\n${formatOptions(raised)}\n`);
1332
1419
  return settle(io, session, project, raised, seconds, true);
1333
1420
  };
1421
+ /**
1422
+ * Writes a person's answer on a decision, in their name through the agent (ADR-0021): the
1423
+ * option they chose, or their own words, and a note about it, so that a choice made in chat
1424
+ * or in passing reaches the record the run waits on.
1425
+ */
1426
+ const answering = async (args, io) => {
1427
+ const { values, positionals } = parse(args, {
1428
+ option: { type: "string" },
1429
+ own: { type: "string" },
1430
+ note: { type: "string" },
1431
+ file: { type: "string" },
1432
+ });
1433
+ const id = theOne(positionals, 'Say which decision: console answer <id> --option <n>|--own "<answer>" [--note <markdown>|--file <path>]');
1434
+ const option = text(values, "option");
1435
+ const own = text(values, "own");
1436
+ if ((option === undefined) === (own === undefined)) {
1437
+ throw misuse('Say the option chosen, --option <n>, or the person\'s own answer, --own "<words>": one of the two.');
1438
+ }
1439
+ const note = await bodyOf(io, values, text(values, "note"));
1440
+ const session = await open(io, values);
1441
+ const project = await session.project();
1442
+ const answered = await project.answerDecision(id, {
1443
+ ...(own === undefined ? { option } : { own }),
1444
+ ...(note === undefined ? {} : { note }),
1445
+ });
1446
+ answer(io, session, answered, () => formatDecision(answered));
1447
+ return EXIT.ok;
1448
+ };
1334
1449
  const decision = async (args, io) => {
1335
1450
  const { values, positionals } = parse(args, {
1336
1451
  wait: { type: "string" },
@@ -1487,20 +1602,33 @@ const restore = async (args, io) => {
1487
1602
  return EXIT.ok;
1488
1603
  };
1489
1604
  const goals = async (args, io) => {
1490
- const { values } = parse(args, { category: { type: "string" }, state: { type: "string" } }, false);
1605
+ const { values } = parse(args, {
1606
+ category: { type: "string" },
1607
+ state: { type: "string" },
1608
+ search: { type: "string" },
1609
+ ...PAGE_OPTIONS,
1610
+ }, false);
1491
1611
  const state = goalStateOf(text(values, "state"));
1492
1612
  const category = text(values, "category");
1613
+ const search = text(values, "search")?.trim();
1614
+ if (search === "") {
1615
+ throw misuse("Say what to search for: console goals --search <words>");
1616
+ }
1617
+ const page = pageOf(values);
1493
1618
  const session = await open(io, values);
1494
1619
  const project = await session.project();
1495
1620
  const found = await project.goals({
1496
1621
  ...(state === undefined ? {} : { state }),
1497
1622
  ...(category === undefined ? {} : { category }),
1623
+ ...(search === undefined ? {} : { q: search }),
1624
+ ...page,
1498
1625
  });
1499
1626
  answer(io, session, found, () => found.items.length === 0
1500
- ? state === undefined && category === undefined
1627
+ ? state === undefined && category === undefined && search === undefined
1501
1628
  ? "No goals. An owner sets one: console goal new <title> --due <day>"
1502
1629
  : "No goal matches."
1503
- : found.items.map(formatGoalLine).join("\n"));
1630
+ : found.items.map(formatGoalLine).join("\n") +
1631
+ formatMore(found.more, page.limit, page.offset));
1504
1632
  return EXIT.ok;
1505
1633
  };
1506
1634
  const GOAL_FIELDS = {
@@ -1579,14 +1707,31 @@ const goal = async (args, io) => {
1579
1707
  return EXIT.ok;
1580
1708
  };
1581
1709
  const decisions = async (args, io) => {
1582
- const { values } = parse(args, { open: { type: "boolean" }, ...PAGE_OPTIONS }, false);
1710
+ const { values } = parse(args, {
1711
+ open: { type: "boolean" },
1712
+ mine: { type: "boolean" },
1713
+ raised: { type: "boolean" },
1714
+ ...PAGE_OPTIONS,
1715
+ }, false);
1583
1716
  const page = pageOf(values);
1584
1717
  const session = await open(io, values);
1718
+ const open_ = on(values, "open") ? { open: true } : {};
1719
+ if (on(values, "mine") || on(values, "raised")) {
1720
+ // One's own across the projects (ADR-0042), addressed to the person or raised by them for
1721
+ // someone else: the waiting first, each with its project's key.
1722
+ const raised = on(values, "raised");
1723
+ const found = await session.client.myDecisions({ ...open_, raised, ...page });
1724
+ answer(io, session, found, () => found.items.length === 0
1725
+ ? raised
1726
+ ? "No decisions you asked of others."
1727
+ : "No decisions of yours."
1728
+ : found.items
1729
+ .map(({ project, decision }) => `${project.key} ${formatDecisionLine(decision)}`)
1730
+ .join("\n") + formatMore(found.more, page.limit, page.offset));
1731
+ return EXIT.ok;
1732
+ }
1585
1733
  const project = await session.project();
1586
- const found = await project.decisions({
1587
- ...(on(values, "open") ? { open: true } : {}),
1588
- ...page,
1589
- });
1734
+ const found = await project.decisions({ ...open_, ...page });
1590
1735
  answer(io, session, found, () => found.items.length === 0
1591
1736
  ? "No decisions."
1592
1737
  : found.items.map(formatDecisionLine).join("\n") +
@@ -2155,6 +2300,7 @@ const COMMANDS = {
2155
2300
  comment,
2156
2301
  file,
2157
2302
  ask,
2303
+ answer: answering,
2158
2304
  decision,
2159
2305
  decisions,
2160
2306
  finish: ending("finished"),
@@ -2238,11 +2384,35 @@ async function noticeVersions(io, seen) {
2238
2384
  shownAt: new Date(now).toISOString(),
2239
2385
  });
2240
2386
  }
2387
+ /**
2388
+ * The refusal of a console older than this command works with (ADR-0038), in a console's own
2389
+ * form: a `426` the command reads as it reads the console's, before it acts on anything the
2390
+ * console answered.
2391
+ */
2392
+ function consoleRefusal(io, seen) {
2393
+ const command = parseVersion(io.version);
2394
+ const server = parseVersion(seen.version);
2395
+ if (command === undefined || server === undefined) {
2396
+ return undefined;
2397
+ }
2398
+ const { verdict, range } = judgeConsole(server, command, io.consoleAtLeast ?? CONSOLE_AT_LEAST);
2399
+ if (verdict !== "outdated") {
2400
+ return undefined;
2401
+ }
2402
+ return Response.json({
2403
+ error: {
2404
+ code: "console.outdated",
2405
+ message: `This console, ${seen.version}, is older than this command (${io.version}) works with (${formatRange(range)}).`,
2406
+ },
2407
+ }, { status: 426 });
2408
+ }
2241
2409
  /** What a refusal of the console is followed by, when there is something to do about it. */
2242
2410
  function hintFor(error, io, seen) {
2243
2411
  switch (error.code) {
2244
2412
  case "client.outdated":
2245
2413
  return `Update the command: ${updateWords(io, seen.version)}\n`;
2414
+ case "console.outdated":
2415
+ return `Install the console's version: ${updateWords(io, seen.version)}; or ask the console's operator to update the console.\n`;
2246
2416
  case "client.too_new":
2247
2417
  return `Install the console's version: ${updateWords(io, seen.version)}; or ask the console's operator to update the console.\n`;
2248
2418
  case "auth.required":
@@ -2317,7 +2487,11 @@ export async function runCli(argv, io) {
2317
2487
  const response = await io.fetch(input, init);
2318
2488
  seen.version = response.headers.get(VERSION_HEADER) ?? seen.version;
2319
2489
  seen.range = response.headers.get(CLIENT_RANGE_HEADER) ?? seen.range;
2320
- return response;
2490
+ // A console older than this command works with is refused here (ADR-0038); `serve`
2491
+ // carries the pages' own requests, which are not the command's to judge.
2492
+ // A refusal of the console's own is passed on as it is.
2493
+ return ((name === "serve" || response.status === 426 ? undefined : consoleRefusal(io, seen)) ??
2494
+ response);
2321
2495
  },
2322
2496
  };
2323
2497
  try {
@@ -2348,7 +2522,9 @@ export async function runCli(argv, io) {
2348
2522
  }
2349
2523
  if (error instanceof ApiError) {
2350
2524
  io.stderr(`${error.code}: ${error.message}\n${hintFor(error, io, seen)}`);
2351
- return error.code === "client.outdated" || error.code === "client.too_new"
2525
+ return error.code === "client.outdated" ||
2526
+ error.code === "client.too_new" ||
2527
+ error.code === "console.outdated"
2352
2528
  ? EXIT.incompatible
2353
2529
  : EXIT.failed;
2354
2530
  }