@skillcdn/console 0.1.14 → 0.1.16

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 (165) hide show
  1. package/README.md +5 -4
  2. package/SKILLCDN.md +2 -0
  3. package/dist/agent-name.d.ts +3 -0
  4. package/dist/agent-name.d.ts.map +1 -0
  5. package/dist/agent-name.js +9 -0
  6. package/dist/agent-name.js.map +1 -0
  7. package/dist/api.d.ts +6 -4
  8. package/dist/api.d.ts.map +1 -1
  9. package/dist/api.js +6 -4
  10. package/dist/api.js.map +1 -1
  11. package/dist/cli/cli.d.ts +2 -0
  12. package/dist/cli/cli.d.ts.map +1 -1
  13. package/dist/cli/cli.js +225 -24
  14. package/dist/cli/cli.js.map +1 -1
  15. package/dist/cli/format.d.ts +14 -4
  16. package/dist/cli/format.d.ts.map +1 -1
  17. package/dist/cli/format.js +59 -10
  18. package/dist/cli/format.js.map +1 -1
  19. package/dist/cli/help.d.ts.map +1 -1
  20. package/dist/cli/help.js +28 -12
  21. package/dist/cli/help.js.map +1 -1
  22. package/dist/cli/main.js +5 -0
  23. package/dist/cli/main.js.map +1 -1
  24. package/dist/client.d.ts +20 -2
  25. package/dist/client.d.ts.map +1 -1
  26. package/dist/client.js +11 -2
  27. package/dist/client.js.map +1 -1
  28. package/dist/components/calendar.d.ts +35 -2
  29. package/dist/components/calendar.d.ts.map +1 -1
  30. package/dist/components/calendar.js +80 -31
  31. package/dist/components/calendar.js.map +1 -1
  32. package/dist/components/connect.d.ts +4 -1
  33. package/dist/components/connect.d.ts.map +1 -1
  34. package/dist/components/connect.js +7 -3
  35. package/dist/components/connect.js.map +1 -1
  36. package/dist/components/decision-list.d.ts.map +1 -1
  37. package/dist/components/decision-list.js +2 -2
  38. package/dist/components/decision-list.js.map +1 -1
  39. package/dist/components/documents.d.ts +5 -0
  40. package/dist/components/documents.d.ts.map +1 -1
  41. package/dist/components/documents.js +3 -2
  42. package/dist/components/documents.js.map +1 -1
  43. package/dist/components/event-feed.d.ts +1 -1
  44. package/dist/components/event-feed.d.ts.map +1 -1
  45. package/dist/components/event-feed.js +10 -1
  46. package/dist/components/event-feed.js.map +1 -1
  47. package/dist/components/people.d.ts +24 -1
  48. package/dist/components/people.d.ts.map +1 -1
  49. package/dist/components/people.js +35 -3
  50. package/dist/components/people.js.map +1 -1
  51. package/dist/components/projects.d.ts +10 -1
  52. package/dist/components/projects.d.ts.map +1 -1
  53. package/dist/components/projects.js +8 -5
  54. package/dist/components/projects.js.map +1 -1
  55. package/dist/components/rules.d.ts +40 -0
  56. package/dist/components/rules.d.ts.map +1 -0
  57. package/dist/components/rules.js +46 -0
  58. package/dist/components/rules.js.map +1 -0
  59. package/dist/components/runs.d.ts +1 -1
  60. package/dist/components/runs.d.ts.map +1 -1
  61. package/dist/components/runs.js +2 -1
  62. package/dist/components/runs.js.map +1 -1
  63. package/dist/components/skills.d.ts.map +1 -1
  64. package/dist/components/skills.js +5 -1
  65. package/dist/components/skills.js.map +1 -1
  66. package/dist/components/task-form.d.ts.map +1 -1
  67. package/dist/components/task-form.js +1 -1
  68. package/dist/components/task-form.js.map +1 -1
  69. package/dist/components/tokens.d.ts.map +1 -1
  70. package/dist/components/tokens.js +2 -9
  71. package/dist/components/tokens.js.map +1 -1
  72. package/dist/components/ui.d.ts +7 -2
  73. package/dist/components/ui.d.ts.map +1 -1
  74. package/dist/components/ui.js +13 -0
  75. package/dist/components/ui.js.map +1 -1
  76. package/dist/components/work.d.ts +5 -1
  77. package/dist/components/work.d.ts.map +1 -1
  78. package/dist/components/work.js +1 -1
  79. package/dist/components/work.js.map +1 -1
  80. package/dist/console.css +263 -17
  81. package/dist/console.d.ts.map +1 -1
  82. package/dist/console.js +197 -43
  83. package/dist/console.js.map +1 -1
  84. package/dist/data.d.ts +19 -2
  85. package/dist/data.d.ts.map +1 -1
  86. package/dist/data.js +27 -1
  87. package/dist/data.js.map +1 -1
  88. package/dist/i18n/en.d.ts +84 -9
  89. package/dist/i18n/en.d.ts.map +1 -1
  90. package/dist/i18n/en.js +84 -13
  91. package/dist/i18n/en.js.map +1 -1
  92. package/dist/i18n/ko.d.ts.map +1 -1
  93. package/dist/i18n/ko.js +84 -13
  94. package/dist/i18n/ko.js.map +1 -1
  95. package/dist/index.d.ts +4 -3
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +4 -3
  98. package/dist/index.js.map +1 -1
  99. package/dist/limits.d.ts +4 -0
  100. package/dist/limits.d.ts.map +1 -1
  101. package/dist/limits.js +4 -0
  102. package/dist/limits.js.map +1 -1
  103. package/dist/router.d.ts +30 -2
  104. package/dist/router.d.ts.map +1 -1
  105. package/dist/router.js +50 -10
  106. package/dist/router.js.map +1 -1
  107. package/dist/routes.d.ts +14 -4
  108. package/dist/routes.d.ts.map +1 -1
  109. package/dist/routes.js +17 -2
  110. package/dist/routes.js.map +1 -1
  111. package/dist/rules.d.ts +28 -0
  112. package/dist/rules.d.ts.map +1 -0
  113. package/dist/rules.js +102 -0
  114. package/dist/rules.js.map +1 -0
  115. package/dist/schemas.d.ts +652 -4
  116. package/dist/schemas.d.ts.map +1 -1
  117. package/dist/schemas.js +96 -6
  118. package/dist/schemas.js.map +1 -1
  119. package/dist/vocabulary.d.ts +21 -1
  120. package/dist/vocabulary.d.ts.map +1 -1
  121. package/dist/vocabulary.js +21 -0
  122. package/dist/vocabulary.js.map +1 -1
  123. package/docs/specs/cli.md +13 -9
  124. package/docs/specs/rest.md +33 -9
  125. package/package.json +1 -1
  126. package/skills/building-a-console/SKILL.md +1 -1
  127. package/skills/deploying-a-console/SKILL.md +5 -5
  128. package/skills/deploying-a-console/assets/secrets.example.json +2 -1
  129. package/skills/deploying-a-console/references/access.md +10 -6
  130. package/skills/deploying-a-console/references/resources.md +10 -7
  131. package/skills/working-the-board/SKILL.md +10 -9
  132. package/skills/working-the-board/references/project-rules.md +36 -0
  133. package/src/agent-name.ts +9 -0
  134. package/src/api.ts +37 -0
  135. package/src/cli/cli.ts +264 -26
  136. package/src/cli/format.ts +74 -10
  137. package/src/cli/help.ts +31 -11
  138. package/src/cli/main.ts +5 -0
  139. package/src/client.ts +47 -1
  140. package/src/components/calendar.tsx +252 -51
  141. package/src/components/connect.tsx +12 -6
  142. package/src/components/decision-list.tsx +2 -1
  143. package/src/components/documents.tsx +7 -3
  144. package/src/components/event-feed.tsx +16 -3
  145. package/src/components/people.tsx +244 -35
  146. package/src/components/projects.tsx +37 -14
  147. package/src/components/rules.tsx +167 -0
  148. package/src/components/runs.tsx +4 -2
  149. package/src/components/skills.tsx +22 -10
  150. package/src/components/task-form.tsx +1 -0
  151. package/src/components/tokens.tsx +2 -14
  152. package/src/components/ui.tsx +25 -2
  153. package/src/components/work.tsx +34 -28
  154. package/src/console.tsx +524 -45
  155. package/src/data.ts +52 -2
  156. package/src/i18n/en.ts +94 -14
  157. package/src/i18n/ko.ts +90 -14
  158. package/src/index.ts +16 -1
  159. package/src/limits.ts +4 -0
  160. package/src/router.ts +67 -12
  161. package/src/routes.ts +23 -3
  162. package/src/rules.ts +109 -0
  163. package/src/schemas.ts +121 -4
  164. package/src/styles/console.css +263 -17
  165. package/src/vocabulary.ts +30 -0
package/src/api.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  // custom UI talks to any console with types, and so that the contract has one source. No React
4
4
  // here: the server and the command line import this entry point, and nothing of the pages.
5
5
 
6
+ export { agentLabel } from "./agent-name.js";
6
7
  export {
7
8
  ApiError,
8
9
  type ClientOptions,
@@ -51,6 +52,7 @@ export {
51
52
  MAX_LANGUAGE_TAG_LENGTH,
52
53
  MAX_LINK_LABEL_LENGTH,
53
54
  MAX_LINKS,
55
+ MAX_LOGIN_LENGTH,
54
56
  MAX_NOTE_LENGTH,
55
57
  MAX_OPTION_LABEL_LENGTH,
56
58
  MAX_OPTIONS,
@@ -61,6 +63,7 @@ export {
61
63
  MAX_PROJECT_SYMBOL_LENGTH,
62
64
  MAX_QUESTION_LENGTH,
63
65
  MAX_REPORTS_PER_RUN,
66
+ MAX_RULES_LENGTH,
64
67
  MAX_SEARCH_LENGTH,
65
68
  MAX_SECRET_LENGTH,
66
69
  MAX_SKILLS_ADDRESS_LENGTH,
@@ -84,17 +87,31 @@ export {
84
87
  PROJECT_COLLECTIONS,
85
88
  type ProjectCollection,
86
89
  personTokensPath,
90
+ personWorkPath,
87
91
  projectIconPath,
88
92
  projectPath,
89
93
  REST_ROUTES,
90
94
  RETURN_TO_PARAM,
91
95
  restPath,
96
+ rulesPath,
92
97
  SIGN_IN_FAILURES,
93
98
  SIGN_IN_PARAM,
94
99
  type SignInFailure,
95
100
  signInPath,
96
101
  } from "./routes.js";
97
102
  export {
103
+ fillRules,
104
+ isReferenceRules,
105
+ languageNameOf,
106
+ PROJECT_RULES_TEMPLATE,
107
+ REFERENCE_RULES,
108
+ RULES_LANGUAGE_PLACEHOLDER,
109
+ RULES_PATH,
110
+ RULES_TITLE,
111
+ } from "./rules.js";
112
+ export {
113
+ hostOfLogin,
114
+ isLoginAt,
98
115
  type RestAnswerInput,
99
116
  type RestArtifact,
100
117
  type RestArtifactInput,
@@ -129,23 +146,30 @@ export {
129
146
  type RestGoalInput,
130
147
  type RestGoalPatch,
131
148
  type RestGoals,
149
+ type RestListing,
150
+ type RestListingInput,
132
151
  type RestMe,
133
152
  type RestMember,
134
153
  type RestMemberInput,
135
154
  type RestMemberPatch,
136
155
  type RestMembers,
137
156
  type RestMePatch,
157
+ type RestOrganization,
138
158
  type RestPeople,
139
159
  type RestPerson,
140
160
  type RestPersonPatch,
161
+ type RestPersonWork,
141
162
  type RestProject,
142
163
  type RestProjectIcon,
143
164
  type RestProjectInput,
144
165
  type RestProjectPatch,
166
+ type RestProjectRules,
145
167
  type RestProjects,
146
168
  type RestProvider,
147
169
  type RestReport,
148
170
  type RestReportInput,
171
+ type RestRules,
172
+ type RestRulesInput,
149
173
  type RestRun,
150
174
  type RestRunEndInput,
151
175
  type RestRunInput,
@@ -200,23 +224,30 @@ export {
200
224
  restGoalPatchSchema,
201
225
  restGoalSchema,
202
226
  restGoalsSchema,
227
+ restListingInputSchema,
228
+ restListingSchema,
203
229
  restMemberInputSchema,
204
230
  restMemberPatchSchema,
205
231
  restMemberSchema,
206
232
  restMembersSchema,
207
233
  restMePatchSchema,
208
234
  restMeSchema,
235
+ restOrganizationSchema,
209
236
  restPeopleSchema,
210
237
  restPersonPatchSchema,
211
238
  restPersonSchema,
239
+ restPersonWorkSchema,
212
240
  restProjectIconSchema,
213
241
  restProjectInputSchema,
214
242
  restProjectPatchSchema,
243
+ restProjectRulesSchema,
215
244
  restProjectSchema,
216
245
  restProjectsSchema,
217
246
  restProviderSchema,
218
247
  restReportInputSchema,
219
248
  restReportSchema,
249
+ restRulesInputSchema,
250
+ restRulesSchema,
220
251
  restRunEndInputSchema,
221
252
  restRunInputSchema,
222
253
  restRunSchema,
@@ -253,9 +284,13 @@ export {
253
284
  isProviderKey,
254
285
  LINK_SOURCES,
255
286
  type LinkSource,
287
+ MEMBERSHIP_SOURCES,
288
+ type MembershipSource,
256
289
  OPEN_TASK_STATES,
257
290
  PERSON_ROLES,
291
+ PERSON_STATUSES,
258
292
  type PersonRole,
293
+ type PersonStatus,
259
294
  PROJECT_ICON_TYPES,
260
295
  PROJECT_ROLES,
261
296
  PROJECT_VISIBILITIES,
@@ -264,8 +299,10 @@ export {
264
299
  type ProjectRole,
265
300
  type ProjectVisibility,
266
301
  type ProviderKey,
302
+ RULES_SOURCES,
267
303
  RUN_ENDINGS,
268
304
  RUN_STATUSES,
305
+ type RulesSource,
269
306
  type RunEnding,
270
307
  type RunStatus,
271
308
  SKILLS_STATUSES,
package/src/cli/cli.ts CHANGED
@@ -9,7 +9,14 @@ import {
9
9
  } from "../client.js";
10
10
  import { addDays, dayOf as dayOfDate, isDay } from "../dates.js";
11
11
  import { DEFAULT_PAGE_LIMIT, LIST_LIMIT, MAX_OPTIONS, MIN_OPTIONS } from "../limits.js";
12
- import type { RestClaim, RestDecision, RestPerson, RestTaskPatch } from "../schemas.js";
12
+ import { fillRules, RULES_PATH, RULES_TITLE } from "../rules.js";
13
+ import {
14
+ hostOfLogin,
15
+ type RestClaim,
16
+ type RestDecision,
17
+ type RestPerson,
18
+ type RestTaskPatch,
19
+ } from "../schemas.js";
13
20
  import {
14
21
  COLOR_NAMES,
15
22
  type ColorName,
@@ -19,8 +26,10 @@ import {
19
26
  type PersonRole,
20
27
  PROJECT_ROLES,
21
28
  PROJECT_VISIBILITIES,
29
+ PROVIDER_KEYS,
22
30
  type ProjectRole,
23
31
  type ProjectVisibility,
32
+ type ProviderKey,
24
33
  TASK_PRIORITIES,
25
34
  TASK_STATES,
26
35
  type TaskPriority,
@@ -38,12 +47,15 @@ import {
38
47
  formatDocuments,
39
48
  formatGoal,
40
49
  formatGoalLine,
50
+ formatListingLine,
41
51
  formatMemberLine,
42
52
  formatMore,
43
53
  formatOptions,
44
54
  formatPersonLine,
55
+ formatPersonWork,
45
56
  formatProject,
46
57
  formatProjectLine,
58
+ formatRules,
47
59
  formatRun,
48
60
  formatRunLine,
49
61
  formatSkills,
@@ -102,6 +114,8 @@ export interface CliIo {
102
114
  readonly now: () => number;
103
115
  /** The version of the package the command runs from, when the entry point knows it. */
104
116
  readonly version?: string | undefined;
117
+ /** Where the console's own skills are on this machine, shipped with the package; nothing when they are not there. */
118
+ readonly skillsDir?: string | undefined;
105
119
  /** How `serve` listens on the loopback (ADR-0014); left out, the command cannot serve. */
106
120
  readonly listen?: Listen | undefined;
107
121
  }
@@ -368,6 +382,8 @@ function wordOf<T extends string>(value: string, words: readonly T[], what: stri
368
382
 
369
383
  const personRoleOf = (value: string): PersonRole =>
370
384
  wordOf(value, PERSON_ROLES, "a role in the workspace");
385
+ const providerOf = (value: string): ProviderKey =>
386
+ wordOf(value, PROVIDER_KEYS, "a provider people sign in through");
371
387
  const projectRoleOf = (value: string): ProjectRole =>
372
388
  wordOf(value, PROJECT_ROLES, "a role in a project");
373
389
  const visibilityOf = (value: string): ProjectVisibility =>
@@ -522,7 +538,8 @@ const login: Command = async (args, io) => {
522
538
  // A connection (ADR-0011): a code a person approves on the console's own pages, and the token
523
539
  // handed over here once, never shown.
524
540
  const client = createClient({ baseUrl: url, fetch: io.fetch });
525
- const agent = text(values, "agent") ?? "The console command";
541
+ // "<agent> on <machine>": the pages say the agent and keep the machine for a hover.
542
+ const agent = text(values, "agent") ?? agentOf(io.env) ?? "An agent";
526
543
  const begun = await client.connect({
527
544
  agent: io.hostname === undefined ? agent : `${agent} on ${io.hostname}`,
528
545
  });
@@ -550,11 +567,47 @@ const login: Command = async (args, io) => {
550
567
 
551
568
  const logout: Command = async (args, io) => {
552
569
  parse(args, {}, false);
570
+ const stored = await io.store.load();
571
+ const url = io.env.CONSOLE_URL ?? stored?.url;
572
+ const token = io.env.CONSOLE_TOKEN ?? stored?.token;
573
+ if (url === undefined || token === undefined) {
574
+ io.stdout("Not signed in here: nothing to forget.\n");
575
+ return EXIT.ok;
576
+ }
577
+ // The token is given up at the console first, so that it is good for nothing from now on;
578
+ // one the console no longer knows is gone already. A console out of reach leaves it to the
579
+ // Agents page, and the person is told so.
580
+ let gone = true;
581
+ try {
582
+ await createClient({ baseUrl: url, token, fetch: io.fetch }).giveUpToken();
583
+ } catch (error) {
584
+ if (!(error instanceof ApiError && error.code === "auth.required")) {
585
+ gone = false;
586
+ }
587
+ }
553
588
  await io.store.clear();
554
- io.stdout("Signed out here. The token itself is removed on your Agents page.\n");
589
+ io.stdout(
590
+ gone
591
+ ? "Signed out: the token is given up at the console and forgotten here.\n"
592
+ : `Signed out here, but the console at ${url} could not be reached to give the token up: a person removes it on the Agents page.\n`,
593
+ );
594
+ if (io.env.CONSOLE_TOKEN !== undefined) {
595
+ io.stdout("CONSOLE_TOKEN in the environment still holds it: unset it.\n");
596
+ }
555
597
  return EXIT.ok;
556
598
  };
557
599
 
600
+ /** The agent the command runs under, where its environment says so: what a connection is named by default. */
601
+ function agentOf(env: CliIo["env"]): string | undefined {
602
+ if (env.CLAUDECODE !== undefined) {
603
+ return "Claude Code";
604
+ }
605
+ if (Object.keys(env).some((key) => key.startsWith("CODEX_"))) {
606
+ return "Codex";
607
+ }
608
+ return undefined;
609
+ }
610
+
558
611
  const whoami: Command = async (args, io) => {
559
612
  const { values } = parse(args, {}, false);
560
613
  const session = await open(io, values);
@@ -597,7 +650,7 @@ const use: Command = async (args, io) => {
597
650
  session,
598
651
  project,
599
652
  () =>
600
- `Working in ${project.name} (${project.key}) from this directory on: kept in ${kept}, which is meant to be committed.`,
653
+ `Working in ${project.name} (${project.key}) from this directory on: kept in ${kept}, which is meant to be committed. What its people ask of agents: console rules.`,
601
654
  );
602
655
  return EXIT.ok;
603
656
  };
@@ -653,10 +706,18 @@ const tasks: Command = async (args, io) => {
653
706
  };
654
707
 
655
708
  const work: Command = async (args, io) => {
656
- const { values } = parse(args, {}, false);
709
+ const { values } = parse(args, { person: { type: "string" } }, false);
657
710
  const session = await open(io, values);
658
- const found = await session.client.work();
659
- answer(io, session, found, () => formatWork(found));
711
+ const login = text(values, "person");
712
+ if (login === undefined) {
713
+ const found = await session.client.work();
714
+ answer(io, session, found, () => formatWork(found));
715
+ return EXIT.ok;
716
+ }
717
+ // Another person's plate: the console decides whether it is the asker's to see.
718
+ const whose = await personOf(session, login);
719
+ const found = await session.client.personWork(whose.id);
720
+ answer(io, session, found, () => formatPersonWork(whose, found));
660
721
  return EXIT.ok;
661
722
  };
662
723
 
@@ -675,10 +736,118 @@ const skills: Command = async (args, io) => {
675
736
  const session = await open(io, values);
676
737
  const project = await session.project();
677
738
  const found = await project.skills();
678
- answer(io, session, found, () => formatSkills(found));
739
+ answer(io, session, found, () => formatSkills(found, io.skillsDir));
679
740
  return EXIT.ok;
680
741
  };
681
742
 
743
+ /** A version number given as a flag, or nothing; anything else is not understood. */
744
+ function versionOf(given: string | undefined): number | undefined {
745
+ if (given === undefined) {
746
+ return undefined;
747
+ }
748
+ const version = Number(given);
749
+ if (!Number.isInteger(version) || version < 1) {
750
+ throw misuse(`Not a version number: ${given}`);
751
+ }
752
+ return version;
753
+ }
754
+
755
+ // The rules of the board (ADR-0023): read before a task is taken, written by an owner for the
756
+ // project and by an administrator for the workspace, through the same page the console keeps.
757
+ const rules: Command = async (args, io) => {
758
+ const [verb, ...rest] = args;
759
+ if (verb === "write") {
760
+ const { values, positionals } = parse(rest, {
761
+ body: { type: "string" },
762
+ file: { type: "string" },
763
+ workspace: { type: "boolean" },
764
+ base: { type: "string" },
765
+ });
766
+ if (positionals.length > 1) {
767
+ throw misuse("Say the rules as one argument: console rules write --body <markdown>");
768
+ }
769
+ const given = positionals[0] ?? text(values, "body");
770
+ const none = given === "none";
771
+ const body = none ? undefined : await bodyOf(io, values, given);
772
+ if (!none && body === undefined) {
773
+ throw misuse(
774
+ "Say what the rules are to say: --body <markdown>, --file <path>, - to read standard input, or none to put the written rules away.",
775
+ );
776
+ }
777
+ const baseVersion = versionOf(text(values, "base"));
778
+ const session = await open(io, values);
779
+ if (on(values, "workspace")) {
780
+ if (none) {
781
+ const put = await session.client.archiveRules();
782
+ answer(
783
+ io,
784
+ session,
785
+ put,
786
+ () => "The organization's rules are put away: the console's own hold.",
787
+ );
788
+ return EXIT.ok;
789
+ }
790
+ const written = await session.client.writeRules({
791
+ body: body ?? "",
792
+ ...(baseVersion === undefined ? {} : { baseVersion }),
793
+ });
794
+ answer(io, session, written, () =>
795
+ written.page === null
796
+ ? "Nothing written: these are the console's own rules as they stand, and they hold already."
797
+ : `Wrote the workspace's rules, version ${written.page.version}.`,
798
+ );
799
+ return EXIT.ok;
800
+ }
801
+ const project = await session.project();
802
+ if (none) {
803
+ try {
804
+ const put = await project.archiveDocument(RULES_PATH);
805
+ answer(io, session, put, () => "This project's rules are put away: it adds none now.");
806
+ } catch (error) {
807
+ if (!(error instanceof ApiError) || error.code !== "document.not_found") {
808
+ throw error;
809
+ }
810
+ answer(io, session, null, () => "This project has no rules of its own to put away.");
811
+ }
812
+ return EXIT.ok;
813
+ }
814
+ const written = await project.writeDocument(RULES_PATH, {
815
+ title: RULES_TITLE,
816
+ body: body ?? "",
817
+ ...(baseVersion === undefined ? {} : { baseVersion }),
818
+ });
819
+ answer(io, session, written, () => `Wrote this project's rules, version ${written.version}.`);
820
+ return EXIT.ok;
821
+ }
822
+ const { values } = parse(args, {}, false);
823
+ const session = await open(io, values);
824
+ const key = await session.projectKey();
825
+ const [found, me] = await Promise.all([
826
+ key === undefined ? session.client.rules() : session.client.project(key).rules(),
827
+ session.client.me(),
828
+ ]);
829
+ const read =
830
+ "workspace" in found
831
+ ? {
832
+ workspace: forPerson(found.workspace, me.language),
833
+ project: forPerson(found.project, me.language),
834
+ }
835
+ : forPerson(found, me.language);
836
+ answer(io, session, read, () => formatRules(read));
837
+ return EXIT.ok;
838
+ };
839
+
840
+ /**
841
+ * The rules as they read for the person whose token the command holds: `{language}` filled
842
+ * with the language they chose for the pages, or with the task's when they chose none.
843
+ */
844
+ function forPerson<T extends { readonly body: string | null }>(
845
+ half: T,
846
+ language: string | null,
847
+ ): T {
848
+ return half.body === null ? half : { ...half, body: fillRules(half.body, language) };
849
+ }
850
+
682
851
  /** The flags that set a task's fields, shared by writing one and changing one. */
683
852
  const TASK_FIELDS: OptionSpec = {
684
853
  state: { type: "string" },
@@ -804,11 +973,29 @@ const take: Command = async (args, io) => {
804
973
  taskId: found.id,
805
974
  ...(agent === undefined ? {} : { agent }),
806
975
  });
807
- answer(io, session, { run, task: found }, () =>
976
+ // The rules come with the run, as they read for the person: everything from here acts
977
+ // within them (ADR-0023); the task is read again, as it stands now that it is taken.
978
+ const [ruling, me, taken] = await Promise.all([
979
+ project.rules(),
980
+ session.client.me(),
981
+ project.task(ref),
982
+ ]);
983
+ // Taking a task with a run open on it ends that run (ADR-0020): said when it happened.
984
+ const tookOver =
985
+ found.openRuns > 0
986
+ ? " A run that was open on it is over now: the board cannot know whether its agent still ran, so taking it was deciding."
987
+ : "";
988
+ const rules = {
989
+ workspace: forPerson(ruling.workspace, me.language),
990
+ project: forPerson(ruling.project, me.language),
991
+ };
992
+ answer(io, session, { run, task: taken, rules }, () =>
808
993
  [
809
- `Run ${run.id} began on #${found.number} ${found.title}, as "${run.agent}" for ${run.person.login}. The task is in progress and theirs. A run that was open on it is over now: the board cannot know whether its agent still ran, so taking it was deciding.`,
994
+ `Run ${run.id} began on #${found.number} ${found.title}, as "${run.agent}" for ${run.person.login}. The task is in progress and theirs.${tookOver}`,
995
+ "",
996
+ formatTask(taken),
810
997
  "",
811
- formatTask(found),
998
+ formatRules(rules),
812
999
  "",
813
1000
  'Say how it goes: console report "<what you found or did>". Hand in what you make: console hand-in <https url | file path>.',
814
1001
  'Ask when a person must decide: console ask "<question>" --option "..." --option "...". End with: console finish --summary "<what was done, what is left>".',
@@ -1403,25 +1590,67 @@ const people: Command = async (args, io) => {
1403
1590
  const { values } = parse(args, {}, false);
1404
1591
  const session = await open(io, values);
1405
1592
  const found = await session.client.people();
1406
- answer(io, session, found, () => found.items.map(formatPersonLine).join("\n"));
1593
+ answer(io, session, found, () =>
1594
+ [...found.items.map(formatPersonLine), ...found.listed.map(formatListingLine)].join("\n"),
1595
+ );
1407
1596
  return EXIT.ok;
1408
1597
  };
1409
1598
 
1599
+ const PERSON_USAGE =
1600
+ "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>";
1601
+
1410
1602
  const person: Command = async (args, io) => {
1411
- if (args[0] !== "role") {
1412
- throw misuse("Say what to do: console person role <login> <admin|member>");
1603
+ const what = args[0];
1604
+ if (what === "role") {
1605
+ const { values, positionals } = parse(args.slice(1), {});
1606
+ const [login, role] = theTwo(
1607
+ positionals,
1608
+ "Say whom and what: console person role <login> <admin|member>",
1609
+ );
1610
+ const wanted = personRoleOf(role);
1611
+ const session = await open(io, values);
1612
+ const found = await personOf(session, login);
1613
+ const changed = await session.client.updatePerson(found.id, { role: wanted });
1614
+ answer(io, session, changed, () => formatPersonLine(changed));
1615
+ return EXIT.ok;
1413
1616
  }
1414
- const { values, positionals } = parse(args.slice(1), {});
1415
- const [login, role] = theTwo(
1416
- positionals,
1417
- "Say whom and what: console person role <login> <admin|member>",
1418
- );
1419
- const wanted = personRoleOf(role);
1420
- const session = await open(io, values);
1421
- const found = await personOf(session, login);
1422
- const changed = await session.client.updatePerson(found.id, { role: wanted });
1423
- answer(io, session, changed, () => formatPersonLine(changed));
1424
- return EXIT.ok;
1617
+ if (what === "add") {
1618
+ const { values, positionals } = parse(args.slice(1), { host: { type: "string" } });
1619
+ const login = theOne(positionals, "Say whom: console person add <login> [--host gh|google]");
1620
+ const chosen = text(values, "host");
1621
+ const host = chosen === undefined ? hostOfLogin(login) : providerOf(chosen);
1622
+ const session = await open(io, values);
1623
+ const made = await session.client.createListing({ host, login });
1624
+ answer(io, session, made, () => formatListingLine(made));
1625
+ return EXIT.ok;
1626
+ }
1627
+ if (what === "remove") {
1628
+ const { values, positionals } = parse(args.slice(1), {});
1629
+ const login = theOne(positionals, "Say whom: console person remove <login>");
1630
+ const session = await open(io, values);
1631
+ const { listed } = await session.client.people();
1632
+ const found = listed.find((listing) => listing.login.toLowerCase() === login.toLowerCase());
1633
+ if (found === undefined) {
1634
+ throw misuse(
1635
+ `Nobody called ${login} is let in and not signed in yet. console people lists them; a person who signed in is taken out with console person deactivate <login>.`,
1636
+ );
1637
+ }
1638
+ await session.client.removeListing(found.id);
1639
+ answer(io, session, { removed: found }, () => `${found.login} is no longer let in.`);
1640
+ return EXIT.ok;
1641
+ }
1642
+ if (what === "deactivate" || what === "reactivate") {
1643
+ const { values, positionals } = parse(args.slice(1), {});
1644
+ const login = theOne(positionals, `Say whom: console person ${what} <login>`);
1645
+ const session = await open(io, values);
1646
+ const found = await personOf(session, login);
1647
+ const changed = await session.client.updatePerson(found.id, {
1648
+ status: what === "deactivate" ? "deactivated" : "active",
1649
+ });
1650
+ answer(io, session, changed, () => formatPersonLine(changed));
1651
+ return EXIT.ok;
1652
+ }
1653
+ throw misuse(PERSON_USAGE);
1425
1654
  };
1426
1655
 
1427
1656
  /** The flags that set what a project is, shared by making one and changing one. */
@@ -1665,6 +1894,7 @@ const COMMANDS: Readonly<Record<string, Command>> = {
1665
1894
  goal,
1666
1895
  categories,
1667
1896
  skills,
1897
+ rules,
1668
1898
  people,
1669
1899
  person,
1670
1900
  project,
@@ -1743,7 +1973,15 @@ function hintFor(error: ApiError): string {
1743
1973
  case "category.in_use":
1744
1974
  return "Tasks are sorted into the category. Move them out first: console task edit <number> --category <other|none>\n";
1745
1975
  case "person.last_admin":
1746
- return "The board keeps at least one administrator. Make another one first.\n";
1976
+ return "The board keeps at least one administrator in. Make another one first.\n";
1977
+ case "person.of_organization":
1978
+ return "The organization says who is in: a person it let in comes and goes with it, and is listed by login to stay.\n";
1979
+ case "listing.exists":
1980
+ return "The login is let in already, or is a person's who is in. console people lists both.\n";
1981
+ case "listing.not_found":
1982
+ return "No login let in and not signed in yet has that id. console people lists them.\n";
1983
+ case "listing.unknown_host":
1984
+ return "Nobody signs in through that provider here. console whoami --json names the ways in.\n";
1747
1985
  case "connect.too_many":
1748
1986
  return "The console holds as many connections as it may at once. Try again in a few minutes.\n";
1749
1987
  case "network":
package/src/cli/format.ts CHANGED
@@ -6,9 +6,13 @@ import type {
6
6
  RestDocumentSummary,
7
7
  RestDocuments,
8
8
  RestGoal,
9
+ RestListing,
9
10
  RestMember,
10
11
  RestPerson,
12
+ RestPersonWork,
11
13
  RestProject,
14
+ RestProjectRules,
15
+ RestRules,
12
16
  RestRun,
13
17
  RestSkills,
14
18
  RestTask,
@@ -57,11 +61,20 @@ export function formatProject(project: RestProject): string {
57
61
  return lines.join("\n");
58
62
  }
59
63
 
60
- /** One person: their login, their name where the host gives one, and what they are in the workspace. */
64
+ /** One person: their login, their name where the host gives one, what they are in the workspace, and that they are out when they are. */
61
65
  export function formatPersonLine(person: RestPerson): string {
66
+ const standing = person.status === "active" ? person.role : `${person.role}, deactivated`;
62
67
  return person.name === null
63
- ? `${person.login} (${person.role})`
64
- : `${person.login} ${person.name} (${person.role})`;
68
+ ? `${person.login} (${standing})`
69
+ : `${person.login} ${person.name} (${standing})`;
70
+ }
71
+
72
+ /** The provider a listing names, as people call it. */
73
+ const HOST_NAMES = { gh: "GitHub", google: "Google" } as const;
74
+
75
+ /** One login let in that nobody has signed in with yet (ADR-0022). */
76
+ export function formatListingLine(listing: RestListing): string {
77
+ return `${listing.login} (let in at ${HOST_NAMES[listing.host]}, not signed in yet)`;
65
78
  }
66
79
 
67
80
  /** One member of a project: their login, what they are in it, and since when. */
@@ -203,17 +216,27 @@ const SKILLS_WORDS: Readonly<Record<Exclude<RestSkills["status"], "none" | "read
203
216
  unavailable: "SkillCDN could not be reached; ask again later.",
204
217
  };
205
218
 
206
- /** The organization's skills: where they are, and each with what an agent loads it by. */
207
- export function formatSkills(skills: RestSkills): string {
208
- if (skills.status === "none" || skills.address === null) {
209
- return "No skills address: neither this project nor the console names one.";
219
+ /**
220
+ * The organization's skills: where they are, each with what an agent loads it by, where a
221
+ * person connects an agent that has no connection to the address, and where the console's own
222
+ * skills are on this machine, when the command knows.
223
+ */
224
+ export function formatSkills(skills: RestSkills, ownSkills?: string): string {
225
+ const own =
226
+ ownSkills === undefined
227
+ ? []
228
+ : ["", `The console's own skills, for working the board: ${ownSkills}`];
229
+ if (skills.status === "none" || skills.address === null || skills.page === null) {
230
+ return ["No skills address: neither this project nor the console names one.", ...own].join(
231
+ "\n",
232
+ );
210
233
  }
211
234
  const head = `skills at ${skills.address}, served by ${skills.source}: ${skills.status}`;
212
235
  if (skills.status !== "ready") {
213
- return `${head}\n${SKILLS_WORDS[skills.status]}`;
236
+ return [head, SKILLS_WORDS[skills.status], ...own].join("\n");
214
237
  }
215
238
  if (skills.items.length === 0) {
216
- return `${head}\nNo skills at this address.`;
239
+ return [head, "No skills at this address.", ...own].join("\n");
217
240
  }
218
241
  const lines = [head];
219
242
  for (const skill of skills.items) {
@@ -221,11 +244,41 @@ export function formatSkills(skills: RestSkills): string {
221
244
  }
222
245
  lines.push(
223
246
  "",
224
- "Load a skill by its URI through your SkillCDN connection. --json adds the page of each, for a person.",
247
+ `Load a skill by its URI through your SkillCDN connection to this address; an agent without one is connected by a person at ${skills.page}. --json adds the page of each, for a person.`,
248
+ ...own,
225
249
  );
226
250
  return lines.join("\n");
227
251
  }
228
252
 
253
+ /** One half of the rules: where it comes from, and the text as it stands. */
254
+ function formatRulesHalf(where: "the workspace" | "this project", rules: RestRules): string {
255
+ const page = rules.page;
256
+ const facts =
257
+ page === null
258
+ ? undefined
259
+ : `v${page.version}, by ${writer(page.updatedBy, page.agent)}; ${page.updatedAt}`;
260
+ if (rules.source === "none" || rules.body === null) {
261
+ return `Rules of ${where}: none of its own; the workspace's hold.`;
262
+ }
263
+ const head =
264
+ rules.source === "reference"
265
+ ? `Rules of ${where}: the console's own, until an administrator writes the organization's (console rules write --workspace).`
266
+ : `Rules of ${where} (${facts ?? "written"}):`;
267
+ return `${head}\n\n${rules.body.trimEnd()}`;
268
+ }
269
+
270
+ /** The rules that hold: the workspace's, and what the project adds when the command works in one. */
271
+ export function formatRules(rules: RestProjectRules | RestRules): string {
272
+ if ("workspace" in rules) {
273
+ return [
274
+ formatRulesHalf("the workspace", rules.workspace),
275
+ "",
276
+ formatRulesHalf("this project", rules.project),
277
+ ].join("\n");
278
+ }
279
+ return formatRulesHalf("the workspace", rules);
280
+ }
281
+
229
282
  /** How far a goal is, from its tasks: the done, the at work, and the rest, in one phrase. */
230
283
  function progressOf(goal: RestGoal): string {
231
284
  const counts = goal.taskCounts;
@@ -282,6 +335,17 @@ export function formatWork(work: RestWork): string {
282
335
  return lines.join("\n");
283
336
  }
284
337
 
338
+ /** Another person's plate: their open tasks, each with its project; no decisions, which wait for anyone. */
339
+ export function formatPersonWork(person: RestPerson, work: RestPersonWork): string {
340
+ if (work.tasks.length === 0) {
341
+ return `No open task is assigned to ${person.login}.`;
342
+ }
343
+ return [
344
+ `tasks assigned to ${person.login}:`,
345
+ ...work.tasks.map(({ project, task }) => ` ${project.key} ${formatTaskLine(task)}`),
346
+ ].join("\n");
347
+ }
348
+
285
349
  export function formatDecisionLine(decision: RestDecision): string {
286
350
  const state = decision.answer === null ? "waiting" : `answered ${decision.answer.option}`;
287
351
  return `${decision.id} ${state} ${decision.question} (asked by ${asker(decision)})`;