@skillcdn/console 0.3.0 → 0.3.2

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 (170) 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 +5 -1
  7. package/dist/cli/cli.d.ts.map +1 -1
  8. package/dist/cli/cli.js +290 -46
  9. package/dist/cli/cli.js.map +1 -1
  10. package/dist/cli/credentials.d.ts +5 -0
  11. package/dist/cli/credentials.d.ts.map +1 -1
  12. package/dist/cli/credentials.js +2 -0
  13. package/dist/cli/credentials.js.map +1 -1
  14. package/dist/cli/directory.d.ts +7 -0
  15. package/dist/cli/directory.d.ts.map +1 -1
  16. package/dist/cli/directory.js +7 -0
  17. package/dist/cli/directory.js.map +1 -1
  18. package/dist/cli/format.d.ts +10 -1
  19. package/dist/cli/format.d.ts.map +1 -1
  20. package/dist/cli/format.js +76 -20
  21. package/dist/cli/format.js.map +1 -1
  22. package/dist/cli/help.d.ts.map +1 -1
  23. package/dist/cli/help.js +42 -23
  24. package/dist/cli/help.js.map +1 -1
  25. package/dist/cli/main.js +1 -0
  26. package/dist/cli/main.js.map +1 -1
  27. package/dist/client.d.ts +39 -9
  28. package/dist/client.d.ts.map +1 -1
  29. package/dist/client.js +24 -3
  30. package/dist/client.js.map +1 -1
  31. package/dist/compat.d.ts +28 -6
  32. package/dist/compat.d.ts.map +1 -1
  33. package/dist/compat.js +55 -14
  34. package/dist/compat.js.map +1 -1
  35. package/dist/components/board.d.ts.map +1 -1
  36. package/dist/components/board.js +6 -4
  37. package/dist/components/board.js.map +1 -1
  38. package/dist/components/calendar.d.ts +18 -8
  39. package/dist/components/calendar.d.ts.map +1 -1
  40. package/dist/components/calendar.js +39 -27
  41. package/dist/components/calendar.js.map +1 -1
  42. package/dist/components/event-feed.d.ts.map +1 -1
  43. package/dist/components/event-feed.js +2 -0
  44. package/dist/components/event-feed.js.map +1 -1
  45. package/dist/components/pack-connect.d.ts +23 -0
  46. package/dist/components/pack-connect.d.ts.map +1 -0
  47. package/dist/components/pack-connect.js +86 -0
  48. package/dist/components/pack-connect.js.map +1 -0
  49. package/dist/components/pack-token.d.ts +27 -0
  50. package/dist/components/pack-token.d.ts.map +1 -0
  51. package/dist/components/pack-token.js +11 -0
  52. package/dist/components/pack-token.js.map +1 -0
  53. package/dist/components/picker.d.ts +39 -0
  54. package/dist/components/picker.d.ts.map +1 -0
  55. package/dist/components/picker.js +155 -0
  56. package/dist/components/picker.js.map +1 -0
  57. package/dist/components/projects.d.ts +13 -0
  58. package/dist/components/projects.d.ts.map +1 -1
  59. package/dist/components/projects.js +37 -5
  60. package/dist/components/projects.js.map +1 -1
  61. package/dist/components/task-form.d.ts +18 -4
  62. package/dist/components/task-form.d.ts.map +1 -1
  63. package/dist/components/task-form.js +56 -13
  64. package/dist/components/task-form.js.map +1 -1
  65. package/dist/components/task-list.js +2 -2
  66. package/dist/components/task-list.js.map +1 -1
  67. package/dist/components/task-view.d.ts +7 -1
  68. package/dist/components/task-view.d.ts.map +1 -1
  69. package/dist/components/task-view.js +6 -5
  70. package/dist/components/task-view.js.map +1 -1
  71. package/dist/components/ui.d.ts +7 -0
  72. package/dist/components/ui.d.ts.map +1 -1
  73. package/dist/components/ui.js +11 -0
  74. package/dist/components/ui.js.map +1 -1
  75. package/dist/components/work.d.ts +3 -1
  76. package/dist/components/work.d.ts.map +1 -1
  77. package/dist/components/work.js +6 -4
  78. package/dist/components/work.js.map +1 -1
  79. package/dist/components/workspace.d.ts +46 -0
  80. package/dist/components/workspace.d.ts.map +1 -0
  81. package/dist/components/workspace.js +124 -0
  82. package/dist/components/workspace.js.map +1 -0
  83. package/dist/console.css +319 -11
  84. package/dist/console.d.ts.map +1 -1
  85. package/dist/console.js +83 -24
  86. package/dist/console.js.map +1 -1
  87. package/dist/data.d.ts +26 -1
  88. package/dist/data.d.ts.map +1 -1
  89. package/dist/data.js +59 -1
  90. package/dist/data.js.map +1 -1
  91. package/dist/dates.d.ts +12 -2
  92. package/dist/dates.d.ts.map +1 -1
  93. package/dist/dates.js +20 -2
  94. package/dist/dates.js.map +1 -1
  95. package/dist/i18n/en.d.ts +64 -3
  96. package/dist/i18n/en.d.ts.map +1 -1
  97. package/dist/i18n/en.js +81 -16
  98. package/dist/i18n/en.js.map +1 -1
  99. package/dist/i18n/ko.d.ts.map +1 -1
  100. package/dist/i18n/ko.js +77 -16
  101. package/dist/i18n/ko.js.map +1 -1
  102. package/dist/index.d.ts +6 -2
  103. package/dist/index.d.ts.map +1 -1
  104. package/dist/index.js +6 -2
  105. package/dist/index.js.map +1 -1
  106. package/dist/limits.d.ts +2 -0
  107. package/dist/limits.d.ts.map +1 -1
  108. package/dist/limits.js +2 -0
  109. package/dist/limits.js.map +1 -1
  110. package/dist/router.d.ts +4 -4
  111. package/dist/router.d.ts.map +1 -1
  112. package/dist/router.js +11 -2
  113. package/dist/router.js.map +1 -1
  114. package/dist/routes.d.ts +4 -0
  115. package/dist/routes.d.ts.map +1 -1
  116. package/dist/routes.js +4 -0
  117. package/dist/routes.js.map +1 -1
  118. package/dist/schemas.d.ts +194 -13
  119. package/dist/schemas.d.ts.map +1 -1
  120. package/dist/schemas.js +121 -21
  121. package/dist/schemas.js.map +1 -1
  122. package/dist/vocabulary.d.ts +21 -1
  123. package/dist/vocabulary.d.ts.map +1 -1
  124. package/dist/vocabulary.js +18 -0
  125. package/dist/vocabulary.js.map +1 -1
  126. package/docs/specs/cli.md +25 -20
  127. package/docs/specs/rest.md +42 -19
  128. package/package.json +2 -2
  129. package/skills/building-a-console/SKILL.md +6 -6
  130. package/skills/building-a-console/references/the-look-from-a-website.md +39 -0
  131. package/skills/deploying-a-console/SKILL.md +9 -8
  132. package/skills/deploying-a-console/references/resources.md +1 -1
  133. package/skills/working-the-board/SKILL.md +5 -5
  134. package/skills/working-the-board/references/setting-up.md +1 -1
  135. package/skills/working-the-board/references/terms.md +3 -1
  136. package/skills/working-the-board/references/the-sentence.md +1 -1
  137. package/src/api.ts +21 -0
  138. package/src/cli/cli.ts +384 -51
  139. package/src/cli/credentials.ts +7 -0
  140. package/src/cli/directory.ts +9 -0
  141. package/src/cli/format.ts +83 -20
  142. package/src/cli/help.ts +50 -23
  143. package/src/cli/main.ts +1 -0
  144. package/src/client.ts +107 -10
  145. package/src/compat.ts +63 -14
  146. package/src/components/board.tsx +10 -4
  147. package/src/components/calendar.tsx +76 -34
  148. package/src/components/event-feed.tsx +2 -0
  149. package/src/components/pack-connect.tsx +179 -0
  150. package/src/components/pack-token.tsx +74 -0
  151. package/src/components/picker.tsx +297 -0
  152. package/src/components/projects.tsx +108 -9
  153. package/src/components/task-form.tsx +149 -63
  154. package/src/components/task-list.tsx +2 -2
  155. package/src/components/task-view.tsx +23 -9
  156. package/src/components/ui.tsx +25 -0
  157. package/src/components/work.tsx +10 -5
  158. package/src/components/workspace.tsx +298 -0
  159. package/src/console.tsx +202 -32
  160. package/src/data.ts +101 -1
  161. package/src/dates.ts +23 -2
  162. package/src/i18n/en.ts +92 -16
  163. package/src/i18n/ko.ts +84 -16
  164. package/src/index.ts +32 -1
  165. package/src/limits.ts +2 -0
  166. package/src/router.ts +17 -5
  167. package/src/routes.ts +4 -0
  168. package/src/schemas.ts +140 -19
  169. package/src/styles/console.css +319 -11
  170. package/src/vocabulary.ts +28 -0
package/dist/cli/cli.js CHANGED
@@ -1,15 +1,16 @@
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";
9
- import { COLOR_NAMES, GOAL_STATES, PERSON_ROLES, PROJECT_ROLES, PROJECT_VISIBILITIES, PROVIDER_KEYS, TASK_PRIORITIES, TASK_STATES, } from "../vocabulary.js";
9
+ import { COLOR_NAMES, GOAL_STATES, PEOPLE_VISIBILITIES, PERSON_ROLES, PROJECT_ROLES, PROJECT_VISIBILITIES, PROVIDER_KEYS, TASK_PRIORITIES, TASK_STATES, WEEK_STARTS, } from "../vocabulary.js";
10
10
  import { loadWebRoot, WebRootError } from "../web.js";
11
11
  import { CredentialsError, EMPTY_CREDENTIALS, loginsAt, resolveLogin, withLogin, withoutLogin, } from "./credentials.js";
12
- import { formatAnswer, formatCategoryLine, formatComment, formatDecision, formatDecisionLine, formatDocument, formatDocuments, formatGoal, formatGoalLine, formatListingLine, formatMemberLine, formatMore, formatOptions, formatPersonLine, formatPersonWork, formatProject, formatProjectLine, formatRules, formatRun, formatRunLine, formatTask, formatTaskLine, formatVersion, formatVersionLine, formatWork, } from "./format.js";
12
+ import { THE_SENTENCE } from "./directory.js";
13
+ import { formatAnswer, formatCategoryLine, formatComment, formatDecision, formatDecisionLine, formatDocument, formatDocuments, formatGoal, formatGoalLine, formatListingLine, formatMemberLine, formatMore, formatOptions, formatPersonLine, formatPersonWork, formatProject, formatProjectLine, formatRules, formatRun, formatRunLine, formatTask, formatTaskLine, formatVersion, formatVersionLine, formatWork, formatWorkspace, moment, setMomentZone, } from "./format.js";
13
14
  import { COMMAND_HELP, helpFor, ownSkillPath, usage } from "./help.js";
14
15
  import { describeInstall, updateCommandFor } from "./install.js";
15
16
  import { bodyOfFile, bodyOfFolder, bodyOfSkill, formatSkillBrowse, formatSkillFile, formatSkillFind, formatSkillLoad, formatSkills, joinFile, joinFolder, joinSkill, skillPathIn, summaryOf, } from "./pack.js";
@@ -149,6 +150,12 @@ async function open(io, values) {
149
150
  throw failed(wordsOf(resolved));
150
151
  }
151
152
  const { url, token } = resolved;
153
+ // Moments are printed in the workspace's time zone as the login kept it (ADR-0035); the
154
+ // environment's pair keeps nothing, and they stay as the API gives them.
155
+ setMomentZone(resolved.via === "login"
156
+ ? credentials?.logins.find((kept) => kept.url === url && kept.account === resolved.account)
157
+ ?.timeZone
158
+ : undefined);
152
159
  const client = clientOf(io, url, token);
153
160
  const projectKey = async () => text(values, "project") ?? io.env.CONSOLE_PROJECT ?? directory?.project;
154
161
  return {
@@ -291,15 +298,20 @@ async function personIdOf(session, value) {
291
298
  }
292
299
  return (await personOf(session, value)).id;
293
300
  }
294
- /** The person a login names, among the workspace's; the logins are as people sign in. */
301
+ /**
302
+ * The person a login names, among the workspace's, found by their exact login as people sign in
303
+ * (ADR-0035): whom the console shows the person, and, for an owner of a project, anyone of the
304
+ * workspace, so that someone the People page does not show can still be listed.
305
+ */
295
306
  async function personOf(session, login) {
296
- const { items } = await session.client.people();
297
- const found = items.find((person) => person.login.toLowerCase() === login.toLowerCase());
307
+ const found = await session.client.findPerson(login);
298
308
  if (found === undefined) {
299
- throw misuse(`Nobody of the workspace is called ${login}. console people lists everyone, by login.`);
309
+ throw misuse(`Nobody of the workspace is called ${login}, or they are not yours to see. console people lists whom you see, by login.`);
300
310
  }
301
311
  return found;
302
312
  }
313
+ /** Who sees whom, as the command takes it: with a hyphen or an underscore. */
314
+ const peopleVisibilityOf = (value) => wordOf(value.replaceAll("-", "_"), PEOPLE_VISIBILITIES, "who sees whom");
303
315
  /** One word of a vocabulary, or the refusal to understand. */
304
316
  function wordOf(value, words, what) {
305
317
  const found = words.find((word) => word === value);
@@ -413,7 +425,14 @@ async function keep(io, url, token, agent, asDefault) {
413
425
  .giveUpToken()
414
426
  .catch(() => undefined);
415
427
  }
416
- const next = withLogin(credentials, { url, account: person.login, token, agent, since: new Date(io.now()).toISOString() }, { default: asDefault });
428
+ const next = withLogin(credentials, {
429
+ url,
430
+ account: person.login,
431
+ token,
432
+ agent,
433
+ since: new Date(io.now()).toISOString(),
434
+ timeZone: me.workspace.timeZone ?? undefined,
435
+ }, { default: asDefault });
417
436
  const kept = await io.store.save(next);
418
437
  const standing = next.default === url ? ", the default console here" : "";
419
438
  io.stdout(`${agent === undefined ? "Signed in" : `Connected as "${agent}"`} to ${me.workspace.name} at ${url} as ${person.login} (${person.role})${standing}. The token is kept in ${kept}.\nNext, say which project this directory works in: console use <key> (console projects lists them).\n`);
@@ -557,6 +576,21 @@ function agentOf(env) {
557
576
  }
558
577
  return undefined;
559
578
  }
579
+ /**
580
+ * Keeps the workspace's time zone on the login the command used (ADR-0035), so that moments are
581
+ * printed in it from now on without asking; the environment's pair keeps nothing.
582
+ */
583
+ async function rememberZone(io, session, zone) {
584
+ setMomentZone(zone);
585
+ if (session.via !== "login" || session.credentials === undefined) {
586
+ return;
587
+ }
588
+ const kept = session.credentials.logins.find((login) => login.url === session.url && login.account === session.account);
589
+ if (kept === undefined || kept.timeZone === zone) {
590
+ return;
591
+ }
592
+ await io.store.save(withLogin(session.credentials, { ...kept, timeZone: zone }));
593
+ }
560
594
  const whoami = async (args, io) => {
561
595
  const { values } = parse(args, {}, false);
562
596
  const session = await open(io, values);
@@ -565,16 +599,20 @@ const whoami = async (args, io) => {
565
599
  if (person === null) {
566
600
  throw failed("The console does not know this token. Sign in again: console login");
567
601
  }
602
+ await rememberZone(io, session, me.workspace.timeZone ?? undefined);
568
603
  const key = await session.projectKey();
569
604
  const command = parseVersion(io.version);
570
605
  const server = parseVersion(me.console?.version);
571
606
  const verdict = command === undefined || server === undefined
572
607
  ? undefined
573
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);
574
612
  // A console from before the rule says nothing of itself; the command says so instead.
575
613
  const consoleWords = me.console === undefined
576
614
  ? "the console does not say its version, so it is older than 0.1.18"
577
- : `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`;
578
616
  answer(io, session, {
579
617
  ...me,
580
618
  console: me.console ?? null,
@@ -628,9 +666,25 @@ const use = async (args, io) => {
628
666
  const bound = pinUrl
629
667
  ? ` at ${session.url}${pinAccount && session.account !== undefined ? ` as ${session.account}` : ""}`
630
668
  : "";
631
- answer(io, session, project, () => `Working in ${project.name} (${project.key})${bound} from this directory on: kept in ${kept}, which is meant to be committed in a private repository and left out of a public one. What its people ask of agents: console rules.`);
669
+ answer(io, session, project, () => [
670
+ `Working in ${project.name} (${project.key})${bound} from this directory on: kept in ${kept}, which is meant to be committed in a private repository and left out of a public one. What its people ask of agents: console rules.`,
671
+ // Nothing sends an agent to the console until the checkout says so; the sentence is the
672
+ // skill's (references/the-sentence.md), printed here as `skillcdn use` prints its own.
673
+ "Then put this sentence into the checkout's agreement (AGENTS.md, or CLAUDE.md where that is the file agents read), as it is, so that every agent working here is sent to the console:",
674
+ "",
675
+ THE_SENTENCE,
676
+ ].join("\n"));
632
677
  return EXIT.ok;
633
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
+ }
634
688
  const tasks = async (args, io) => {
635
689
  const { values } = parse(args, {
636
690
  state: { type: "string" },
@@ -640,12 +694,13 @@ const tasks = async (args, io) => {
640
694
  category: { type: "string" },
641
695
  goal: { type: "string" },
642
696
  due: { type: "string" },
697
+ search: { type: "string" },
643
698
  archived: { type: "boolean" },
644
699
  ...PAGE_OPTIONS,
645
700
  }, false);
646
701
  const state = stateOf(text(values, "state"));
647
702
  if (on(values, "mine") && on(values, "unassigned")) {
648
- 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");
649
704
  }
650
705
  const page = pageOf(values);
651
706
  const session = await open(io, values);
@@ -658,19 +713,22 @@ const tasks = async (args, io) => {
658
713
  : due === "overdue"
659
714
  ? addDays(today, -1)
660
715
  : due === "week"
661
- ? addDays(today, 6)
716
+ ? await endOfWeek(io, session, today)
662
717
  : (dayOf(io, due) ?? undefined);
663
- const assignee = on(values, "mine")
664
- ? ((await personIdOf(session, "me")) ?? undefined)
665
- : 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
+ }
666
723
  const found = await project.tasks({
667
724
  ...(state === undefined ? {} : { state }),
668
725
  ...(on(values, "open") || due === "overdue" ? { open: true } : {}),
669
- ...(assignee === undefined ? {} : { assignee }),
726
+ ...(worker === undefined ? {} : { worker }),
670
727
  ...(on(values, "unassigned") ? { unassigned: true } : {}),
671
728
  ...(text(values, "category") === undefined ? {} : { category: text(values, "category") }),
672
729
  ...(text(values, "goal") === undefined ? {} : { goal: text(values, "goal") }),
673
730
  ...(dueBy === undefined ? {} : { dueBy }),
731
+ ...(search === undefined ? {} : { q: search }),
674
732
  ...(on(values, "archived") ? { archived: true } : {}),
675
733
  ...page,
676
734
  });
@@ -938,9 +996,37 @@ const TASK_FIELDS = {
938
996
  goal: { type: "string" },
939
997
  starts: { type: "string" },
940
998
  due: { type: "string" },
941
- assignee: { type: "string" },
999
+ worker: { type: "string", multiple: true },
942
1000
  parent: { type: "string" },
943
1001
  };
1002
+ /**
1003
+ * The people `--worker` names, each a login or `me`, given again or with commas between for
1004
+ * several, in place of those there were; `none` alone for nobody; nothing when the flag was
1005
+ * not given.
1006
+ */
1007
+ async function workerIdsOf(session, given) {
1008
+ if (given.length === 0) {
1009
+ return undefined;
1010
+ }
1011
+ const names = given
1012
+ .flatMap((value) => value.split(","))
1013
+ .map((name) => name.trim())
1014
+ .filter((name) => name.length > 0);
1015
+ if (names.includes("none")) {
1016
+ if (names.length > 1) {
1017
+ throw misuse("Say --worker none alone, for nobody; or the logins of the workers.");
1018
+ }
1019
+ return [];
1020
+ }
1021
+ const ids = [];
1022
+ for (const name of names) {
1023
+ const id = await personIdOf(session, name);
1024
+ if (typeof id === "string" && !ids.includes(id)) {
1025
+ ids.push(id);
1026
+ }
1027
+ }
1028
+ return ids;
1029
+ }
944
1030
  /** What the flags say of a task's fields, as the API takes them; `none` clears a field. */
945
1031
  async function taskFieldsOf(io, session, project, values) {
946
1032
  const state = stateOf(text(values, "state"));
@@ -949,7 +1035,7 @@ async function taskFieldsOf(io, session, project, values) {
949
1035
  const goalId = orNone(text(values, "goal"));
950
1036
  const startsOn = dayOf(io, text(values, "starts"), true);
951
1037
  const dueOn = dayOf(io, text(values, "due"), true);
952
- const assigneeId = await personIdOf(session, text(values, "assignee"));
1038
+ const workerIds = await workerIdsOf(session, texts(values, "worker"));
953
1039
  const parent = text(values, "parent");
954
1040
  const parentId = parent === undefined ? undefined : parent === "none" ? null : (await project.task(parent)).id;
955
1041
  return {
@@ -959,7 +1045,7 @@ async function taskFieldsOf(io, session, project, values) {
959
1045
  ...(goalId === undefined ? {} : { goalId }),
960
1046
  ...(startsOn === undefined ? {} : { startsOn }),
961
1047
  ...(dueOn === undefined ? {} : { dueOn }),
962
- ...(assigneeId === undefined ? {} : { assigneeId }),
1048
+ ...(workerIds === undefined ? {} : { workerIds }),
963
1049
  ...(parentId === undefined ? {} : { parentId }),
964
1050
  };
965
1051
  }
@@ -979,7 +1065,7 @@ const newTask = async (args, io) => {
979
1065
  ...(body === undefined ? {} : { body }),
980
1066
  ...fields,
981
1067
  });
982
- answer(io, session, made, () => `Task #${made.number} written: ${made.title}\nid: ${made.id}`);
1068
+ answer(io, session, made, () => `Task #${made.number} written: ${made.title}\ntask id: ${made.id}`);
983
1069
  return EXIT.ok;
984
1070
  };
985
1071
  const editTask = async (args, io) => {
@@ -1000,7 +1086,7 @@ const editTask = async (args, io) => {
1000
1086
  ...(await taskFieldsOf(io, session, project, values)),
1001
1087
  };
1002
1088
  if (Object.keys(patch).length === 0) {
1003
- throw misuse("Say what to change: --title, --body, --state, --priority, --category, --goal, --starts, --due, --assignee or --parent; none clears a field.");
1089
+ throw misuse("Say what to change: --title, --body, --state, --priority, --category, --goal, --starts, --due, --worker or --parent; none clears a field.");
1004
1090
  }
1005
1091
  const found = await project.task(ref);
1006
1092
  const changed = await project.updateTask(found.id, patch);
@@ -1169,6 +1255,9 @@ const take = async (args, io) => {
1169
1255
  };
1170
1256
  answer(io, session, { run, task: taken, comments: comments.items, rules }, () => [
1171
1257
  `Run ${run.id} began on #${found.number} ${found.title}, as "${run.agent}" for ${run.person.login}. The task is in progress and theirs.${tookOver}${elsewhere}`,
1258
+ // The ids on a line of their own, each named: a run's and a task's begin alike, and a
1259
+ // task has a number for people as well.
1260
+ `run id: ${run.id} task id: ${found.id} (task #${found.number})`,
1172
1261
  "",
1173
1262
  formatTask(taken, undefined, undefined, comments.items),
1174
1263
  "",
@@ -1283,7 +1372,7 @@ const ask = async (args, io) => {
1283
1372
  // The id is printed before any waiting, so that a wait cut short loses nothing.
1284
1373
  io.stdout(session.json
1285
1374
  ? `${JSON.stringify(raised, null, 2)}\n`
1286
- : `Decision ${raised.id} raised on run ${runId}; the run waits for a person.\n${formatOptions(raised)}\n`);
1375
+ : `Decision ${raised.id} raised on run ${runId}; the run waits for a person.\ndecision id: ${raised.id}\n${formatOptions(raised)}\n`);
1287
1376
  return settle(io, session, project, raised, seconds, true);
1288
1377
  };
1289
1378
  const decision = async (args, io) => {
@@ -1442,20 +1531,33 @@ const restore = async (args, io) => {
1442
1531
  return EXIT.ok;
1443
1532
  };
1444
1533
  const goals = async (args, io) => {
1445
- const { values } = parse(args, { category: { type: "string" }, state: { type: "string" } }, false);
1534
+ const { values } = parse(args, {
1535
+ category: { type: "string" },
1536
+ state: { type: "string" },
1537
+ search: { type: "string" },
1538
+ ...PAGE_OPTIONS,
1539
+ }, false);
1446
1540
  const state = goalStateOf(text(values, "state"));
1447
1541
  const category = text(values, "category");
1542
+ const search = text(values, "search")?.trim();
1543
+ if (search === "") {
1544
+ throw misuse("Say what to search for: console goals --search <words>");
1545
+ }
1546
+ const page = pageOf(values);
1448
1547
  const session = await open(io, values);
1449
1548
  const project = await session.project();
1450
1549
  const found = await project.goals({
1451
1550
  ...(state === undefined ? {} : { state }),
1452
1551
  ...(category === undefined ? {} : { category }),
1552
+ ...(search === undefined ? {} : { q: search }),
1553
+ ...page,
1453
1554
  });
1454
1555
  answer(io, session, found, () => found.items.length === 0
1455
- ? state === undefined && category === undefined
1556
+ ? state === undefined && category === undefined && search === undefined
1456
1557
  ? "No goals. An owner sets one: console goal new <title> --due <day>"
1457
1558
  : "No goal matches."
1458
- : found.items.map(formatGoalLine).join("\n"));
1559
+ : found.items.map(formatGoalLine).join("\n") +
1560
+ formatMore(found.more, page.limit, page.offset));
1459
1561
  return EXIT.ok;
1460
1562
  };
1461
1563
  const GOAL_FIELDS = {
@@ -1488,7 +1590,7 @@ const newGoal = async (args, io) => {
1488
1590
  ...(startsOn === undefined ? {} : { startsOn }),
1489
1591
  ...(dueOn === undefined ? {} : { dueOn }),
1490
1592
  });
1491
- answer(io, session, made, () => `Goal set: ${made.title}\nid: ${made.id}`);
1593
+ answer(io, session, made, () => `Goal set: ${made.title}\ngoal id: ${made.id}`);
1492
1594
  return EXIT.ok;
1493
1595
  };
1494
1596
  const editGoal = async (args, io) => {
@@ -1683,6 +1785,66 @@ const people = async (args, io) => {
1683
1785
  answer(io, session, found, () => [...found.items.map(formatPersonLine), ...found.listed.map(formatListingLine)].join("\n"));
1684
1786
  return EXIT.ok;
1685
1787
  };
1788
+ // The workspace's settings (ADR-0035): read by everyone, changed by an administrator, themselves
1789
+ // on the Settings page or through their agent here (ADR-0021).
1790
+ const WORKSPACE_FLAGS = "--people-visibility <everyone|shared-projects>, --week-start <auto|monday|sunday>, --time-zone <zone|none>";
1791
+ const WORKSPACE_USAGE = `Say what to do: console workspace | console workspace edit [${WORKSPACE_FLAGS.replaceAll(", ", "] [")}] | console workspace pack-token --connect|--stdin|--remove`;
1792
+ const workspace = async (args, io) => {
1793
+ const [verb, ...rest] = args;
1794
+ if (verb === undefined || verb.startsWith("--")) {
1795
+ const { values } = parse(args, {}, false);
1796
+ const session = await open(io, values);
1797
+ const me = await session.client.me();
1798
+ await rememberZone(io, session, me.workspace.timeZone ?? undefined);
1799
+ answer(io, session, me.workspace, () => formatWorkspace(me.workspace));
1800
+ return EXIT.ok;
1801
+ }
1802
+ if (verb === "edit") {
1803
+ const { values } = parse(rest, {
1804
+ "people-visibility": { type: "string" },
1805
+ "week-start": { type: "string" },
1806
+ "time-zone": { type: "string" },
1807
+ }, false);
1808
+ const visibility = text(values, "people-visibility");
1809
+ const weekStart = text(values, "week-start");
1810
+ const zone = text(values, "time-zone");
1811
+ if (visibility === undefined && weekStart === undefined && zone === undefined) {
1812
+ throw misuse(`Say what to change: ${WORKSPACE_FLAGS}`);
1813
+ }
1814
+ const session = await open(io, values);
1815
+ const changed = await session.client.updateWorkspace({
1816
+ ...(visibility === undefined ? {} : { peopleVisibility: peopleVisibilityOf(visibility) }),
1817
+ ...(weekStart === undefined
1818
+ ? {}
1819
+ : { weekStart: wordOf(weekStart, WEEK_STARTS, "a day the week begins on") }),
1820
+ ...(zone === undefined ? {} : { timeZone: zone === "none" ? null : zone }),
1821
+ });
1822
+ await rememberZone(io, session, changed.timeZone ?? undefined);
1823
+ answer(io, session, changed, () => formatWorkspace(changed));
1824
+ return EXIT.ok;
1825
+ }
1826
+ if (verb === "pack-token") {
1827
+ const { values, positionals } = parse(rest, PACK_TOKEN_FLAGS);
1828
+ if (positionals.length > 0) {
1829
+ throw misuse("The workspace's token names no project: console workspace pack-token --connect, --stdin or --remove");
1830
+ }
1831
+ const way = wayOf(values);
1832
+ const session = await open(io, values);
1833
+ const { client } = session;
1834
+ return packToken(io, session, way, {
1835
+ set: (token) => client.setWorkspacePackToken({ token }),
1836
+ remove: () => client.removeWorkspacePackToken(),
1837
+ connect: () => client.connectWorkspacePackToken(),
1838
+ async standing() {
1839
+ const state = await client.workspacePackTokenConnection();
1840
+ return state.status === "connected"
1841
+ ? { status: "connected", held: state.workspace }
1842
+ : state;
1843
+ },
1844
+ }, formatWorkspace);
1845
+ }
1846
+ throw misuse(WORKSPACE_USAGE);
1847
+ };
1686
1848
  const PERSON_USAGE = "Say what to do: console person role <login> <admin|member> | person add <login> [--host gh|google] | person remove <login> | person deactivate <login> | person reactivate <login>";
1687
1849
  const person = async (args, io) => {
1688
1850
  const what = args[0];
@@ -1780,30 +1942,79 @@ const editProject = async (args, io) => {
1780
1942
  answer(io, session, changed, () => formatProject(changed));
1781
1943
  return EXIT.ok;
1782
1944
  };
1783
- /** The pack token (ADR-0028): read from standard input and never from an argument, so that no shell keeps it. */
1784
- const projectPackToken = async (args, io) => {
1785
- const { values, positionals } = parse(args, {
1786
- stdin: { type: "boolean" },
1787
- remove: { type: "boolean" },
1788
- });
1789
- const key = theOne(positionals, "Say which project: console project pack-token <key> --stdin, or console project pack-token <key> --remove");
1790
- if (on(values, "stdin") === on(values, "remove")) {
1791
- throw misuse("Say one of --stdin, which reads the token from standard input, and --remove, which takes it away.");
1792
- }
1793
- const session = await open(io, values);
1794
- const project = session.client.project(key);
1795
- if (on(values, "remove")) {
1796
- const changed = await project.removePackToken();
1797
- answer(io, session, changed, () => formatProject(changed));
1945
+ /** How a pack's connection ended short of a token (ADR-0036), in words for the person. */
1946
+ const PACK_CONNECT_ENDED = {
1947
+ expired: "The code expired before anyone approved it. Run the command again for a new one.",
1948
+ declined: "The request was declined at SkillCDN, or the code was spent.",
1949
+ refused: "SkillCDN refused the request: the repository is not one the person may open, the pack is public and needs no token, or they hold as many tokens as one may.",
1950
+ };
1951
+ /** How a SkillCDN token is to change, one of the three the flags name. */
1952
+ const wayOf = (values) => {
1953
+ const ways = ["connect", "stdin", "remove"].filter((way) => on(values, way));
1954
+ const [way] = ways;
1955
+ if (way === undefined || ways.length !== 1) {
1956
+ throw misuse("Say one of --connect, which asks SkillCDN for a code a person approves; --stdin, which reads a token made by hand from standard input; and --remove, which takes the token away.");
1957
+ }
1958
+ return way;
1959
+ };
1960
+ /**
1961
+ * The SkillCDN token (ADR-0028): connected with a code a person approves at SkillCDN (ADR-0036),
1962
+ * or read from standard input and never from an argument, so that no shell keeps it; or taken
1963
+ * away. The same for a project's and for the workspace's.
1964
+ */
1965
+ async function packToken(io, session, way, holder, format) {
1966
+ if (way === "remove") {
1967
+ const changed = await holder.remove();
1968
+ answer(io, session, changed, () => format(changed));
1798
1969
  return EXIT.ok;
1799
1970
  }
1971
+ if (way === "connect") {
1972
+ const started = await holder.connect();
1973
+ io.stdout(`To connect the pack, a person who may open its repository opens ${started.url} signed in at SkillCDN, checks that the page shows the code ${started.code}, and approves; the console receives the SkillCDN token and keeps it sealed, shown to nobody.\nWaiting for that, until ${moment(started.expiresAt)}.\n`);
1974
+ for (;;) {
1975
+ await io.sleep(started.intervalSeconds * 1000);
1976
+ const state = await holder.standing();
1977
+ if (state.status === "pending") {
1978
+ continue;
1979
+ }
1980
+ if (state.status === "connected") {
1981
+ answer(io, session, state.held, () => `Connected.\n${format(state.held)}`);
1982
+ return EXIT.ok;
1983
+ }
1984
+ if (state.status === "ended") {
1985
+ throw failed(PACK_CONNECT_ENDED[state.reason]);
1986
+ }
1987
+ throw failed("The console no longer waits for the connection; run the command again for a new code.");
1988
+ }
1989
+ }
1800
1990
  const token = (await io.readStdin()).trim();
1801
1991
  if (token.length === 0) {
1802
1992
  throw failed("No token was given on standard input.");
1803
1993
  }
1804
- const changed = await project.setPackToken({ token });
1805
- answer(io, session, changed, () => formatProject(changed));
1994
+ const changed = await holder.set(token);
1995
+ answer(io, session, changed, () => format(changed));
1806
1996
  return EXIT.ok;
1997
+ }
1998
+ const PACK_TOKEN_FLAGS = {
1999
+ connect: { type: "boolean" },
2000
+ stdin: { type: "boolean" },
2001
+ remove: { type: "boolean" },
2002
+ };
2003
+ const projectPackToken = async (args, io) => {
2004
+ const { values, positionals } = parse(args, PACK_TOKEN_FLAGS);
2005
+ const key = theOne(positionals, "Say which project: console project pack-token <key> --connect, --stdin or --remove");
2006
+ const way = wayOf(values);
2007
+ const session = await open(io, values);
2008
+ const project = session.client.project(key);
2009
+ return packToken(io, session, way, {
2010
+ set: (token) => project.setPackToken({ token }),
2011
+ remove: () => project.removePackToken(),
2012
+ connect: () => project.connectPackToken(),
2013
+ async standing() {
2014
+ const state = await project.packTokenConnection();
2015
+ return state.status === "connected" ? { status: "connected", held: state.project } : state;
2016
+ },
2017
+ }, formatProject);
1807
2018
  };
1808
2019
  const projectIcon = async (args, io) => {
1809
2020
  const { values, positionals } = parse(args, { remove: { type: "boolean" } });
@@ -1990,6 +2201,7 @@ const COMMANDS = {
1990
2201
  rules,
1991
2202
  people,
1992
2203
  person,
2204
+ workspace,
1993
2205
  project,
1994
2206
  members,
1995
2207
  member,
@@ -2083,11 +2295,35 @@ async function noticeVersions(io, seen) {
2083
2295
  shownAt: new Date(now).toISOString(),
2084
2296
  });
2085
2297
  }
2298
+ /**
2299
+ * The refusal of a console older than this command works with (ADR-0038), in a console's own
2300
+ * form: a `426` the command reads as it reads the console's, before it acts on anything the
2301
+ * console answered.
2302
+ */
2303
+ function consoleRefusal(io, seen) {
2304
+ const command = parseVersion(io.version);
2305
+ const server = parseVersion(seen.version);
2306
+ if (command === undefined || server === undefined) {
2307
+ return undefined;
2308
+ }
2309
+ const { verdict, range } = judgeConsole(server, command, io.consoleAtLeast ?? CONSOLE_AT_LEAST);
2310
+ if (verdict !== "outdated") {
2311
+ return undefined;
2312
+ }
2313
+ return Response.json({
2314
+ error: {
2315
+ code: "console.outdated",
2316
+ message: `This console, ${seen.version}, is older than this command (${io.version}) works with (${formatRange(range)}).`,
2317
+ },
2318
+ }, { status: 426 });
2319
+ }
2086
2320
  /** What a refusal of the console is followed by, when there is something to do about it. */
2087
2321
  function hintFor(error, io, seen) {
2088
2322
  switch (error.code) {
2089
2323
  case "client.outdated":
2090
2324
  return `Update the command: ${updateWords(io, seen.version)}\n`;
2325
+ case "console.outdated":
2326
+ return `Install the console's version: ${updateWords(io, seen.version)}; or ask the console's operator to update the console.\n`;
2091
2327
  case "client.too_new":
2092
2328
  return `Install the console's version: ${updateWords(io, seen.version)}; or ask the console's operator to update the console.\n`;
2093
2329
  case "auth.required":
@@ -2124,6 +2360,8 @@ function hintFor(error, io, seen) {
2124
2360
  return "The person is listed already. console member role <login> <owner|member> changes what they are.\n";
2125
2361
  case "member.not_found":
2126
2362
  return "The person is not listed in the project. console members lists those who are.\n";
2363
+ case "member.open_project":
2364
+ return "The project is open to the workspace, so everyone of it is a member already. console member add <login> --role owner lists an owner; console member role <login> member takes one back to member.\n";
2127
2365
  case "category.key_taken":
2128
2366
  return "A category has that key already. console categories lists them.\n";
2129
2367
  case "category.not_found":
@@ -2160,7 +2398,11 @@ export async function runCli(argv, io) {
2160
2398
  const response = await io.fetch(input, init);
2161
2399
  seen.version = response.headers.get(VERSION_HEADER) ?? seen.version;
2162
2400
  seen.range = response.headers.get(CLIENT_RANGE_HEADER) ?? seen.range;
2163
- return response;
2401
+ // A console older than this command works with is refused here (ADR-0038); `serve`
2402
+ // carries the pages' own requests, which are not the command's to judge.
2403
+ // A refusal of the console's own is passed on as it is.
2404
+ return ((name === "serve" || response.status === 426 ? undefined : consoleRefusal(io, seen)) ??
2405
+ response);
2164
2406
  },
2165
2407
  };
2166
2408
  try {
@@ -2191,7 +2433,9 @@ export async function runCli(argv, io) {
2191
2433
  }
2192
2434
  if (error instanceof ApiError) {
2193
2435
  io.stderr(`${error.code}: ${error.message}\n${hintFor(error, io, seen)}`);
2194
- return error.code === "client.outdated" || error.code === "client.too_new"
2436
+ return error.code === "client.outdated" ||
2437
+ error.code === "client.too_new" ||
2438
+ error.code === "console.outdated"
2195
2439
  ? EXIT.incompatible
2196
2440
  : EXIT.failed;
2197
2441
  }