@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/src/cli/cli.ts CHANGED
@@ -7,8 +7,18 @@ import {
7
7
  type FetchLike,
8
8
  type ProjectClient,
9
9
  } from "../client.js";
10
- import { CLIENT_RANGE_HEADER, judgeClient, parseVersion, VERSION_HEADER } from "../compat.js";
11
- import { addDays, dayOf as dayOfDate, isDay } from "../dates.js";
10
+ import {
11
+ CLIENT_RANGE_HEADER,
12
+ CONSOLE_AT_LEAST,
13
+ consoleRange,
14
+ formatRange,
15
+ judgeClient,
16
+ judgeConsole,
17
+ parseVersion,
18
+ VERSION_HEADER,
19
+ formatVersion as versionText,
20
+ } from "../compat.js";
21
+ import { addDays, dayOf as dayOfDate, firstDayOfWeek, isDay, weekOf } from "../dates.js";
12
22
  import { DEFAULT_PAGE_LIMIT, LIST_LIMIT, MAX_OPTIONS, MIN_OPTIONS } from "../limits.js";
13
23
  import { fillRules, RULES_PATH, RULES_TITLE } from "../rules.js";
14
24
  import {
@@ -16,6 +26,7 @@ import {
16
26
  type RestClaim,
17
27
  type RestDecision,
18
28
  type RestMember,
29
+ type RestPackConnectStarted,
19
30
  type RestPerson,
20
31
  type RestSkillBrowse,
21
32
  type RestSkillFile,
@@ -28,7 +39,9 @@ import {
28
39
  type ColorName,
29
40
  GOAL_STATES,
30
41
  type GoalState,
42
+ PEOPLE_VISIBILITIES,
31
43
  PERSON_ROLES,
44
+ type PeopleVisibility,
32
45
  type PersonRole,
33
46
  PROJECT_ROLES,
34
47
  PROJECT_VISIBILITIES,
@@ -40,6 +53,7 @@ import {
40
53
  TASK_STATES,
41
54
  type TaskPriority,
42
55
  type TaskState,
56
+ WEEK_STARTS,
43
57
  } from "../vocabulary.js";
44
58
  import { loadWebRoot, type WebRoot, WebRootError } from "../web.js";
45
59
  import {
@@ -53,7 +67,7 @@ import {
53
67
  withLogin,
54
68
  withoutLogin,
55
69
  } from "./credentials.js";
56
- import type { DirectorySettings, DirectoryStore } from "./directory.js";
70
+ import { type DirectorySettings, type DirectoryStore, THE_SENTENCE } from "./directory.js";
57
71
  import {
58
72
  formatAnswer,
59
73
  formatCategoryLine,
@@ -80,6 +94,9 @@ import {
80
94
  formatVersion,
81
95
  formatVersionLine,
82
96
  formatWork,
97
+ formatWorkspace,
98
+ moment,
99
+ setMomentZone,
83
100
  } from "./format.js";
84
101
  import { COMMAND_HELP, helpFor, ownSkillPath, usage } from "./help.js";
85
102
  import { describeInstall, type Install, updateCommandFor } from "./install.js";
@@ -161,6 +178,10 @@ export interface CliIo {
161
178
  readonly install?: Install | undefined;
162
179
  /** What the command last said of the versions; left out, it says it every time. */
163
180
  readonly notices?: NoticeStore | undefined;
181
+ /** The oldest console this command works with (ADR-0038); left out, the package's own floor. */
182
+ readonly consoleAtLeast?: string | undefined;
183
+ /** The language and region of this machine (`ko-KR`), for what follows a language where the person chose none; left out, English. */
184
+ readonly locale?: string | undefined;
164
185
  /** Where the console's own skills are on this machine, shipped with the package; nothing when they are not there. */
165
186
  readonly skillsDir?: string | undefined;
166
187
  /** How `serve` listens on the loopback (ADR-0014); left out, the command cannot serve. */
@@ -310,6 +331,14 @@ async function open(io: CliIo, values: Values): Promise<Session> {
310
331
  throw failed(wordsOf(resolved));
311
332
  }
312
333
  const { url, token } = resolved;
334
+ // Moments are printed in the workspace's time zone as the login kept it (ADR-0035); the
335
+ // environment's pair keeps nothing, and they stay as the API gives them.
336
+ setMomentZone(
337
+ resolved.via === "login"
338
+ ? credentials?.logins.find((kept) => kept.url === url && kept.account === resolved.account)
339
+ ?.timeZone
340
+ : undefined,
341
+ );
313
342
  const client = clientOf(io, url, token);
314
343
  const projectKey = async () =>
315
344
  text(values, "project") ?? io.env.CONSOLE_PROJECT ?? directory?.project;
@@ -484,18 +513,25 @@ async function personIdOf(
484
513
  return (await personOf(session, value)).id;
485
514
  }
486
515
 
487
- /** The person a login names, among the workspace's; the logins are as people sign in. */
516
+ /**
517
+ * The person a login names, among the workspace's, found by their exact login as people sign in
518
+ * (ADR-0035): whom the console shows the person, and, for an owner of a project, anyone of the
519
+ * workspace, so that someone the People page does not show can still be listed.
520
+ */
488
521
  async function personOf(session: Session, login: string): Promise<RestPerson> {
489
- const { items } = await session.client.people();
490
- const found = items.find((person) => person.login.toLowerCase() === login.toLowerCase());
522
+ const found = await session.client.findPerson(login);
491
523
  if (found === undefined) {
492
524
  throw misuse(
493
- `Nobody of the workspace is called ${login}. console people lists everyone, by login.`,
525
+ `Nobody of the workspace is called ${login}, or they are not yours to see. console people lists whom you see, by login.`,
494
526
  );
495
527
  }
496
528
  return found;
497
529
  }
498
530
 
531
+ /** Who sees whom, as the command takes it: with a hyphen or an underscore. */
532
+ const peopleVisibilityOf = (value: string): PeopleVisibility =>
533
+ wordOf(value.replaceAll("-", "_"), PEOPLE_VISIBILITIES, "who sees whom");
534
+
499
535
  /** One word of a vocabulary, or the refusal to understand. */
500
536
  function wordOf<T extends string>(value: string, words: readonly T[], what: string): T {
501
537
  const found = words.find((word) => word === value);
@@ -652,7 +688,14 @@ async function keep(
652
688
  }
653
689
  const next = withLogin(
654
690
  credentials,
655
- { url, account: person.login, token, agent, since: new Date(io.now()).toISOString() },
691
+ {
692
+ url,
693
+ account: person.login,
694
+ token,
695
+ agent,
696
+ since: new Date(io.now()).toISOString(),
697
+ timeZone: me.workspace.timeZone ?? undefined,
698
+ },
656
699
  { default: asDefault },
657
700
  );
658
701
  const kept = await io.store.save(next);
@@ -826,6 +869,24 @@ function agentOf(env: CliIo["env"]): string | undefined {
826
869
  return undefined;
827
870
  }
828
871
 
872
+ /**
873
+ * Keeps the workspace's time zone on the login the command used (ADR-0035), so that moments are
874
+ * printed in it from now on without asking; the environment's pair keeps nothing.
875
+ */
876
+ async function rememberZone(io: CliIo, session: Session, zone: string | undefined): Promise<void> {
877
+ setMomentZone(zone);
878
+ if (session.via !== "login" || session.credentials === undefined) {
879
+ return;
880
+ }
881
+ const kept = session.credentials.logins.find(
882
+ (login) => login.url === session.url && login.account === session.account,
883
+ );
884
+ if (kept === undefined || kept.timeZone === zone) {
885
+ return;
886
+ }
887
+ await io.store.save(withLogin(session.credentials, { ...kept, timeZone: zone }));
888
+ }
889
+
829
890
  const whoami: Command = async (args, io) => {
830
891
  const { values } = parse(args, {}, false);
831
892
  const session = await open(io, values);
@@ -834,6 +895,7 @@ const whoami: Command = async (args, io) => {
834
895
  if (person === null) {
835
896
  throw failed("The console does not know this token. Sign in again: console login");
836
897
  }
898
+ await rememberZone(io, session, me.workspace.timeZone ?? undefined);
837
899
  const key = await session.projectKey();
838
900
  const command = parseVersion(io.version);
839
901
  const server = parseVersion(me.console?.version);
@@ -841,11 +903,15 @@ const whoami: Command = async (args, io) => {
841
903
  command === undefined || server === undefined
842
904
  ? undefined
843
905
  : judgeClient(command, server).verdict;
906
+ // The oldest console this command works with (ADR-0038), as it stands within the command's line.
907
+ const atLeast = io.consoleAtLeast ?? CONSOLE_AT_LEAST;
908
+ const floor =
909
+ command === undefined ? atLeast : versionText(consoleRange(command, atLeast).atLeast);
844
910
  // A console from before the rule says nothing of itself; the command says so instead.
845
911
  const consoleWords =
846
912
  me.console === undefined
847
913
  ? "the console does not say its version, so it is older than 0.1.18"
848
- : `the console is ${me.console.version} and takes ${me.console.clientRange}`;
914
+ : `the console is ${me.console.version} and takes ${me.console.clientRange}; this command works with a console from ${floor} on`;
849
915
  answer(
850
916
  io,
851
917
  session,
@@ -914,16 +980,29 @@ const use: Command = async (args, io) => {
914
980
  const bound = pinUrl
915
981
  ? ` at ${session.url}${pinAccount && session.account !== undefined ? ` as ${session.account}` : ""}`
916
982
  : "";
917
- answer(
918
- io,
919
- session,
920
- project,
921
- () =>
983
+ answer(io, session, project, () =>
984
+ [
922
985
  `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.`,
986
+ // Nothing sends an agent to the console until the checkout says so; the sentence is the
987
+ // skill's (references/the-sentence.md), printed here as `skillcdn use` prints its own.
988
+ "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:",
989
+ "",
990
+ THE_SENTENCE,
991
+ ].join("\n"),
923
992
  );
924
993
  return EXIT.ok;
925
994
  };
926
995
 
996
+ /**
997
+ * The last day of this week (ADR-0039): the week begins as the workspace says, or, left to the
998
+ * language, as the person's language has it, else this machine's.
999
+ */
1000
+ async function endOfWeek(io: CliIo, session: Session, today: string): Promise<string> {
1001
+ const me = await session.client.me();
1002
+ const language = me.language ?? io.locale ?? "en";
1003
+ return weekOf(today, firstDayOfWeek(language, me.workspace.weekStart ?? "auto")).at(-1) ?? today;
1004
+ }
1005
+
927
1006
  const tasks: Command = async (args, io) => {
928
1007
  const { values } = parse(
929
1008
  args,
@@ -935,6 +1014,7 @@ const tasks: Command = async (args, io) => {
935
1014
  category: { type: "string" },
936
1015
  goal: { type: "string" },
937
1016
  due: { type: "string" },
1017
+ search: { type: "string" },
938
1018
  archived: { type: "boolean" },
939
1019
  ...PAGE_OPTIONS,
940
1020
  },
@@ -943,7 +1023,7 @@ const tasks: Command = async (args, io) => {
943
1023
  const state = stateOf(text(values, "state"));
944
1024
  if (on(values, "mine") && on(values, "unassigned")) {
945
1025
  throw misuse(
946
- "Say one: --mine for the tasks assigned to you, or --unassigned for those assigned to nobody",
1026
+ "Say one: --mine for the tasks you work on, or --unassigned for those nobody works on",
947
1027
  );
948
1028
  }
949
1029
  const page = pageOf(values);
@@ -958,19 +1038,22 @@ const tasks: Command = async (args, io) => {
958
1038
  : due === "overdue"
959
1039
  ? addDays(today, -1)
960
1040
  : due === "week"
961
- ? addDays(today, 6)
1041
+ ? await endOfWeek(io, session, today)
962
1042
  : (dayOf(io, due) ?? undefined);
963
- const assignee = on(values, "mine")
964
- ? ((await personIdOf(session, "me")) ?? undefined)
965
- : undefined;
1043
+ const worker = on(values, "mine") ? ((await personIdOf(session, "me")) ?? undefined) : undefined;
1044
+ const search = text(values, "search")?.trim();
1045
+ if (search === "") {
1046
+ throw misuse("Say what to search for: console tasks --search <words>");
1047
+ }
966
1048
  const found = await project.tasks({
967
1049
  ...(state === undefined ? {} : { state }),
968
1050
  ...(on(values, "open") || due === "overdue" ? { open: true } : {}),
969
- ...(assignee === undefined ? {} : { assignee }),
1051
+ ...(worker === undefined ? {} : { worker }),
970
1052
  ...(on(values, "unassigned") ? { unassigned: true } : {}),
971
1053
  ...(text(values, "category") === undefined ? {} : { category: text(values, "category") }),
972
1054
  ...(text(values, "goal") === undefined ? {} : { goal: text(values, "goal") }),
973
1055
  ...(dueBy === undefined ? {} : { dueBy }),
1056
+ ...(search === undefined ? {} : { q: search }),
974
1057
  ...(on(values, "archived") ? { archived: true } : {}),
975
1058
  ...page,
976
1059
  });
@@ -1284,10 +1367,42 @@ const TASK_FIELDS: OptionSpec = {
1284
1367
  goal: { type: "string" },
1285
1368
  starts: { type: "string" },
1286
1369
  due: { type: "string" },
1287
- assignee: { type: "string" },
1370
+ worker: { type: "string", multiple: true },
1288
1371
  parent: { type: "string" },
1289
1372
  };
1290
1373
 
1374
+ /**
1375
+ * The people `--worker` names, each a login or `me`, given again or with commas between for
1376
+ * several, in place of those there were; `none` alone for nobody; nothing when the flag was
1377
+ * not given.
1378
+ */
1379
+ async function workerIdsOf(
1380
+ session: Session,
1381
+ given: readonly string[],
1382
+ ): Promise<string[] | undefined> {
1383
+ if (given.length === 0) {
1384
+ return undefined;
1385
+ }
1386
+ const names = given
1387
+ .flatMap((value) => value.split(","))
1388
+ .map((name) => name.trim())
1389
+ .filter((name) => name.length > 0);
1390
+ if (names.includes("none")) {
1391
+ if (names.length > 1) {
1392
+ throw misuse("Say --worker none alone, for nobody; or the logins of the workers.");
1393
+ }
1394
+ return [];
1395
+ }
1396
+ const ids: string[] = [];
1397
+ for (const name of names) {
1398
+ const id = await personIdOf(session, name);
1399
+ if (typeof id === "string" && !ids.includes(id)) {
1400
+ ids.push(id);
1401
+ }
1402
+ }
1403
+ return ids;
1404
+ }
1405
+
1291
1406
  /** What the flags say of a task's fields, as the API takes them; `none` clears a field. */
1292
1407
  async function taskFieldsOf(
1293
1408
  io: CliIo,
@@ -1301,7 +1416,7 @@ async function taskFieldsOf(
1301
1416
  const goalId = orNone(text(values, "goal"));
1302
1417
  const startsOn = dayOf(io, text(values, "starts"), true);
1303
1418
  const dueOn = dayOf(io, text(values, "due"), true);
1304
- const assigneeId = await personIdOf(session, text(values, "assignee"));
1419
+ const workerIds = await workerIdsOf(session, texts(values, "worker"));
1305
1420
  const parent = text(values, "parent");
1306
1421
  const parentId =
1307
1422
  parent === undefined ? undefined : parent === "none" ? null : (await project.task(parent)).id;
@@ -1312,7 +1427,7 @@ async function taskFieldsOf(
1312
1427
  ...(goalId === undefined ? {} : { goalId }),
1313
1428
  ...(startsOn === undefined ? {} : { startsOn }),
1314
1429
  ...(dueOn === undefined ? {} : { dueOn }),
1315
- ...(assigneeId === undefined ? {} : { assigneeId }),
1430
+ ...(workerIds === undefined ? {} : { workerIds }),
1316
1431
  ...(parentId === undefined ? {} : { parentId }),
1317
1432
  };
1318
1433
  }
@@ -1333,7 +1448,12 @@ const newTask: Command = async (args, io) => {
1333
1448
  ...(body === undefined ? {} : { body }),
1334
1449
  ...fields,
1335
1450
  });
1336
- answer(io, session, made, () => `Task #${made.number} written: ${made.title}\nid: ${made.id}`);
1451
+ answer(
1452
+ io,
1453
+ session,
1454
+ made,
1455
+ () => `Task #${made.number} written: ${made.title}\ntask id: ${made.id}`,
1456
+ );
1337
1457
  return EXIT.ok;
1338
1458
  };
1339
1459
 
@@ -1359,7 +1479,7 @@ const editTask: Command = async (args, io) => {
1359
1479
  };
1360
1480
  if (Object.keys(patch).length === 0) {
1361
1481
  throw misuse(
1362
- "Say what to change: --title, --body, --state, --priority, --category, --goal, --starts, --due, --assignee or --parent; none clears a field.",
1482
+ "Say what to change: --title, --body, --state, --priority, --category, --goal, --starts, --due, --worker or --parent; none clears a field.",
1363
1483
  );
1364
1484
  }
1365
1485
  const found = await project.task(ref);
@@ -1556,6 +1676,9 @@ const take: Command = async (args, io) => {
1556
1676
  answer(io, session, { run, task: taken, comments: comments.items, rules }, () =>
1557
1677
  [
1558
1678
  `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}`,
1679
+ // The ids on a line of their own, each named: a run's and a task's begin alike, and a
1680
+ // task has a number for people as well.
1681
+ `run id: ${run.id} task id: ${found.id} (task #${found.number})`,
1559
1682
  "",
1560
1683
  formatTask(taken, undefined, undefined, comments.items),
1561
1684
  "",
@@ -1701,7 +1824,7 @@ const ask: Command = async (args, io) => {
1701
1824
  io.stdout(
1702
1825
  session.json
1703
1826
  ? `${JSON.stringify(raised, null, 2)}\n`
1704
- : `Decision ${raised.id} raised on run ${runId}; the run waits for a person.\n${formatOptions(raised)}\n`,
1827
+ : `Decision ${raised.id} raised on run ${runId}; the run waits for a person.\ndecision id: ${raised.id}\n${formatOptions(raised)}\n`,
1705
1828
  );
1706
1829
  return settle(io, session, project, raised, seconds, true);
1707
1830
  };
@@ -1897,23 +2020,36 @@ const restore: Command = async (args, io) => {
1897
2020
  const goals: Command = async (args, io) => {
1898
2021
  const { values } = parse(
1899
2022
  args,
1900
- { category: { type: "string" }, state: { type: "string" } },
2023
+ {
2024
+ category: { type: "string" },
2025
+ state: { type: "string" },
2026
+ search: { type: "string" },
2027
+ ...PAGE_OPTIONS,
2028
+ },
1901
2029
  false,
1902
2030
  );
1903
2031
  const state = goalStateOf(text(values, "state"));
1904
2032
  const category = text(values, "category");
2033
+ const search = text(values, "search")?.trim();
2034
+ if (search === "") {
2035
+ throw misuse("Say what to search for: console goals --search <words>");
2036
+ }
2037
+ const page = pageOf(values);
1905
2038
  const session = await open(io, values);
1906
2039
  const project = await session.project();
1907
2040
  const found = await project.goals({
1908
2041
  ...(state === undefined ? {} : { state }),
1909
2042
  ...(category === undefined ? {} : { category }),
2043
+ ...(search === undefined ? {} : { q: search }),
2044
+ ...page,
1910
2045
  });
1911
2046
  answer(io, session, found, () =>
1912
2047
  found.items.length === 0
1913
- ? state === undefined && category === undefined
2048
+ ? state === undefined && category === undefined && search === undefined
1914
2049
  ? "No goals. An owner sets one: console goal new <title> --due <day>"
1915
2050
  : "No goal matches."
1916
- : found.items.map(formatGoalLine).join("\n"),
2051
+ : found.items.map(formatGoalLine).join("\n") +
2052
+ formatMore(found.more, page.limit, page.offset),
1917
2053
  );
1918
2054
  return EXIT.ok;
1919
2055
  };
@@ -1950,7 +2086,7 @@ const newGoal: Command = async (args, io) => {
1950
2086
  ...(startsOn === undefined ? {} : { startsOn }),
1951
2087
  ...(dueOn === undefined ? {} : { dueOn }),
1952
2088
  });
1953
- answer(io, session, made, () => `Goal set: ${made.title}\nid: ${made.id}`);
2089
+ answer(io, session, made, () => `Goal set: ${made.title}\ngoal id: ${made.id}`);
1954
2090
  return EXIT.ok;
1955
2091
  };
1956
2092
 
@@ -2186,6 +2322,81 @@ const people: Command = async (args, io) => {
2186
2322
  return EXIT.ok;
2187
2323
  };
2188
2324
 
2325
+ // The workspace's settings (ADR-0035): read by everyone, changed by an administrator, themselves
2326
+ // on the Settings page or through their agent here (ADR-0021).
2327
+ const WORKSPACE_FLAGS =
2328
+ "--people-visibility <everyone|shared-projects>, --week-start <auto|monday|sunday>, --time-zone <zone|none>";
2329
+ const WORKSPACE_USAGE = `Say what to do: console workspace | console workspace edit [${WORKSPACE_FLAGS.replaceAll(", ", "] [")}] | console workspace pack-token --connect|--stdin|--remove`;
2330
+
2331
+ const workspace: Command = async (args, io) => {
2332
+ const [verb, ...rest] = args;
2333
+ if (verb === undefined || verb.startsWith("--")) {
2334
+ const { values } = parse(args, {}, false);
2335
+ const session = await open(io, values);
2336
+ const me = await session.client.me();
2337
+ await rememberZone(io, session, me.workspace.timeZone ?? undefined);
2338
+ answer(io, session, me.workspace, () => formatWorkspace(me.workspace));
2339
+ return EXIT.ok;
2340
+ }
2341
+ if (verb === "edit") {
2342
+ const { values } = parse(
2343
+ rest,
2344
+ {
2345
+ "people-visibility": { type: "string" },
2346
+ "week-start": { type: "string" },
2347
+ "time-zone": { type: "string" },
2348
+ },
2349
+ false,
2350
+ );
2351
+ const visibility = text(values, "people-visibility");
2352
+ const weekStart = text(values, "week-start");
2353
+ const zone = text(values, "time-zone");
2354
+ if (visibility === undefined && weekStart === undefined && zone === undefined) {
2355
+ throw misuse(`Say what to change: ${WORKSPACE_FLAGS}`);
2356
+ }
2357
+ const session = await open(io, values);
2358
+ const changed = await session.client.updateWorkspace({
2359
+ ...(visibility === undefined ? {} : { peopleVisibility: peopleVisibilityOf(visibility) }),
2360
+ ...(weekStart === undefined
2361
+ ? {}
2362
+ : { weekStart: wordOf(weekStart, WEEK_STARTS, "a day the week begins on") }),
2363
+ ...(zone === undefined ? {} : { timeZone: zone === "none" ? null : zone }),
2364
+ });
2365
+ await rememberZone(io, session, changed.timeZone ?? undefined);
2366
+ answer(io, session, changed, () => formatWorkspace(changed));
2367
+ return EXIT.ok;
2368
+ }
2369
+ if (verb === "pack-token") {
2370
+ const { values, positionals } = parse(rest, PACK_TOKEN_FLAGS);
2371
+ if (positionals.length > 0) {
2372
+ throw misuse(
2373
+ "The workspace's token names no project: console workspace pack-token --connect, --stdin or --remove",
2374
+ );
2375
+ }
2376
+ const way = wayOf(values);
2377
+ const session = await open(io, values);
2378
+ const { client } = session;
2379
+ return packToken(
2380
+ io,
2381
+ session,
2382
+ way,
2383
+ {
2384
+ set: (token) => client.setWorkspacePackToken({ token }),
2385
+ remove: () => client.removeWorkspacePackToken(),
2386
+ connect: () => client.connectWorkspacePackToken(),
2387
+ async standing() {
2388
+ const state = await client.workspacePackTokenConnection();
2389
+ return state.status === "connected"
2390
+ ? { status: "connected", held: state.workspace }
2391
+ : state;
2392
+ },
2393
+ },
2394
+ formatWorkspace,
2395
+ );
2396
+ }
2397
+ throw misuse(WORKSPACE_USAGE);
2398
+ };
2399
+
2189
2400
  const PERSON_USAGE =
2190
2401
  "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>";
2191
2402
 
@@ -2309,35 +2520,118 @@ const editProject: Command = async (args, io) => {
2309
2520
  return EXIT.ok;
2310
2521
  };
2311
2522
 
2312
- /** The pack token (ADR-0028): read from standard input and never from an argument, so that no shell keeps it. */
2313
- const projectPackToken: Command = async (args, io) => {
2314
- const { values, positionals } = parse(args, {
2315
- stdin: { type: "boolean" },
2316
- remove: { type: "boolean" },
2317
- });
2318
- const key = theOne(
2319
- positionals,
2320
- "Say which project: console project pack-token <key> --stdin, or console project pack-token <key> --remove",
2321
- );
2322
- if (on(values, "stdin") === on(values, "remove")) {
2523
+ /** How a pack's connection ended short of a token (ADR-0036), in words for the person. */
2524
+ const PACK_CONNECT_ENDED = {
2525
+ expired: "The code expired before anyone approved it. Run the command again for a new one.",
2526
+ declined: "The request was declined at SkillCDN, or the code was spent.",
2527
+ refused:
2528
+ "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.",
2529
+ } as const;
2530
+
2531
+ /** How a SkillCDN token is to change, one of the three the flags name. */
2532
+ const wayOf = (values: Values): "connect" | "stdin" | "remove" => {
2533
+ const ways = (["connect", "stdin", "remove"] as const).filter((way) => on(values, way));
2534
+ const [way] = ways;
2535
+ if (way === undefined || ways.length !== 1) {
2323
2536
  throw misuse(
2324
- "Say one of --stdin, which reads the token from standard input, and --remove, which takes it away.",
2537
+ "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.",
2325
2538
  );
2326
2539
  }
2327
- const session = await open(io, values);
2328
- const project = session.client.project(key);
2329
- if (on(values, "remove")) {
2330
- const changed = await project.removePackToken();
2331
- answer(io, session, changed, () => formatProject(changed));
2540
+ return way;
2541
+ };
2542
+
2543
+ /** Whose SkillCDN token the command works: a project's, or the workspace's for the organization's pack. */
2544
+ interface TokenHolder<T> {
2545
+ set(token: string): Promise<T>;
2546
+ remove(): Promise<T>;
2547
+ connect(): Promise<RestPackConnectStarted>;
2548
+ standing(): Promise<
2549
+ | { readonly status: "none" }
2550
+ | { readonly status: "pending" }
2551
+ | { readonly status: "ended"; readonly reason: keyof typeof PACK_CONNECT_ENDED }
2552
+ | { readonly status: "connected"; readonly held: T }
2553
+ >;
2554
+ }
2555
+
2556
+ /**
2557
+ * The SkillCDN token (ADR-0028): connected with a code a person approves at SkillCDN (ADR-0036),
2558
+ * or read from standard input and never from an argument, so that no shell keeps it; or taken
2559
+ * away. The same for a project's and for the workspace's.
2560
+ */
2561
+ async function packToken<T>(
2562
+ io: CliIo,
2563
+ session: Session,
2564
+ way: "connect" | "stdin" | "remove",
2565
+ holder: TokenHolder<T>,
2566
+ format: (held: T) => string,
2567
+ ): Promise<number> {
2568
+ if (way === "remove") {
2569
+ const changed = await holder.remove();
2570
+ answer(io, session, changed, () => format(changed));
2332
2571
  return EXIT.ok;
2333
2572
  }
2573
+ if (way === "connect") {
2574
+ const started = await holder.connect();
2575
+ io.stdout(
2576
+ `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`,
2577
+ );
2578
+ for (;;) {
2579
+ await io.sleep(started.intervalSeconds * 1000);
2580
+ const state = await holder.standing();
2581
+ if (state.status === "pending") {
2582
+ continue;
2583
+ }
2584
+ if (state.status === "connected") {
2585
+ answer(io, session, state.held, () => `Connected.\n${format(state.held)}`);
2586
+ return EXIT.ok;
2587
+ }
2588
+ if (state.status === "ended") {
2589
+ throw failed(PACK_CONNECT_ENDED[state.reason]);
2590
+ }
2591
+ throw failed(
2592
+ "The console no longer waits for the connection; run the command again for a new code.",
2593
+ );
2594
+ }
2595
+ }
2334
2596
  const token = (await io.readStdin()).trim();
2335
2597
  if (token.length === 0) {
2336
2598
  throw failed("No token was given on standard input.");
2337
2599
  }
2338
- const changed = await project.setPackToken({ token });
2339
- answer(io, session, changed, () => formatProject(changed));
2600
+ const changed = await holder.set(token);
2601
+ answer(io, session, changed, () => format(changed));
2340
2602
  return EXIT.ok;
2603
+ }
2604
+
2605
+ const PACK_TOKEN_FLAGS = {
2606
+ connect: { type: "boolean" },
2607
+ stdin: { type: "boolean" },
2608
+ remove: { type: "boolean" },
2609
+ } as const;
2610
+
2611
+ const projectPackToken: Command = async (args, io) => {
2612
+ const { values, positionals } = parse(args, PACK_TOKEN_FLAGS);
2613
+ const key = theOne(
2614
+ positionals,
2615
+ "Say which project: console project pack-token <key> --connect, --stdin or --remove",
2616
+ );
2617
+ const way = wayOf(values);
2618
+ const session = await open(io, values);
2619
+ const project = session.client.project(key);
2620
+ return packToken(
2621
+ io,
2622
+ session,
2623
+ way,
2624
+ {
2625
+ set: (token) => project.setPackToken({ token }),
2626
+ remove: () => project.removePackToken(),
2627
+ connect: () => project.connectPackToken(),
2628
+ async standing() {
2629
+ const state = await project.packTokenConnection();
2630
+ return state.status === "connected" ? { status: "connected", held: state.project } : state;
2631
+ },
2632
+ },
2633
+ formatProject,
2634
+ );
2341
2635
  };
2342
2636
 
2343
2637
  const projectIcon: Command = async (args, io) => {
@@ -2564,6 +2858,7 @@ const COMMANDS: Readonly<Record<string, Command>> = {
2564
2858
  rules,
2565
2859
  people,
2566
2860
  person,
2861
+ workspace,
2567
2862
  project,
2568
2863
  members,
2569
2864
  member,
@@ -2674,11 +2969,39 @@ async function noticeVersions(io: CliIo, seen: ConsoleSeen): Promise<void> {
2674
2969
  });
2675
2970
  }
2676
2971
 
2972
+ /**
2973
+ * The refusal of a console older than this command works with (ADR-0038), in a console's own
2974
+ * form: a `426` the command reads as it reads the console's, before it acts on anything the
2975
+ * console answered.
2976
+ */
2977
+ function consoleRefusal(io: CliIo, seen: ConsoleSeen): Response | undefined {
2978
+ const command = parseVersion(io.version);
2979
+ const server = parseVersion(seen.version);
2980
+ if (command === undefined || server === undefined) {
2981
+ return undefined;
2982
+ }
2983
+ const { verdict, range } = judgeConsole(server, command, io.consoleAtLeast ?? CONSOLE_AT_LEAST);
2984
+ if (verdict !== "outdated") {
2985
+ return undefined;
2986
+ }
2987
+ return Response.json(
2988
+ {
2989
+ error: {
2990
+ code: "console.outdated",
2991
+ message: `This console, ${seen.version}, is older than this command (${io.version}) works with (${formatRange(range)}).`,
2992
+ },
2993
+ },
2994
+ { status: 426 },
2995
+ );
2996
+ }
2997
+
2677
2998
  /** What a refusal of the console is followed by, when there is something to do about it. */
2678
2999
  function hintFor(error: ApiError, io: CliIo, seen: ConsoleSeen): string {
2679
3000
  switch (error.code) {
2680
3001
  case "client.outdated":
2681
3002
  return `Update the command: ${updateWords(io, seen.version)}\n`;
3003
+ case "console.outdated":
3004
+ return `Install the console's version: ${updateWords(io, seen.version)}; or ask the console's operator to update the console.\n`;
2682
3005
  case "client.too_new":
2683
3006
  return `Install the console's version: ${updateWords(io, seen.version)}; or ask the console's operator to update the console.\n`;
2684
3007
  case "auth.required":
@@ -2715,6 +3038,8 @@ function hintFor(error: ApiError, io: CliIo, seen: ConsoleSeen): string {
2715
3038
  return "The person is listed already. console member role <login> <owner|member> changes what they are.\n";
2716
3039
  case "member.not_found":
2717
3040
  return "The person is not listed in the project. console members lists those who are.\n";
3041
+ case "member.open_project":
3042
+ 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";
2718
3043
  case "category.key_taken":
2719
3044
  return "A category has that key already. console categories lists them.\n";
2720
3045
  case "category.not_found":
@@ -2752,7 +3077,13 @@ export async function runCli(argv: readonly string[], io: CliIo): Promise<number
2752
3077
  const response = await io.fetch(input, init);
2753
3078
  seen.version = response.headers.get(VERSION_HEADER) ?? seen.version;
2754
3079
  seen.range = response.headers.get(CLIENT_RANGE_HEADER) ?? seen.range;
2755
- return response;
3080
+ // A console older than this command works with is refused here (ADR-0038); `serve`
3081
+ // carries the pages' own requests, which are not the command's to judge.
3082
+ // A refusal of the console's own is passed on as it is.
3083
+ return (
3084
+ (name === "serve" || response.status === 426 ? undefined : consoleRefusal(io, seen)) ??
3085
+ response
3086
+ );
2756
3087
  },
2757
3088
  };
2758
3089
  try {
@@ -2784,7 +3115,9 @@ export async function runCli(argv: readonly string[], io: CliIo): Promise<number
2784
3115
  }
2785
3116
  if (error instanceof ApiError) {
2786
3117
  io.stderr(`${error.code}: ${error.message}\n${hintFor(error, io, seen)}`);
2787
- return error.code === "client.outdated" || error.code === "client.too_new"
3118
+ return error.code === "client.outdated" ||
3119
+ error.code === "client.too_new" ||
3120
+ error.code === "console.outdated"
2788
3121
  ? EXIT.incompatible
2789
3122
  : EXIT.failed;
2790
3123
  }