@skillcdn/console 0.4.0 → 0.4.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 (308) hide show
  1. package/README.md +20 -2
  2. package/SKILLCDN.md +7 -5
  3. package/dist/api.d.ts +5 -4
  4. package/dist/api.d.ts.map +1 -1
  5. package/dist/api.js +5 -4
  6. package/dist/api.js.map +1 -1
  7. package/dist/cli/cli.d.ts.map +1 -1
  8. package/dist/cli/cli.js +579 -45
  9. package/dist/cli/cli.js.map +1 -1
  10. package/dist/cli/format.d.ts +34 -3
  11. package/dist/cli/format.d.ts.map +1 -1
  12. package/dist/cli/format.js +277 -6
  13. package/dist/cli/format.js.map +1 -1
  14. package/dist/cli/help.d.ts.map +1 -1
  15. package/dist/cli/help.js +60 -7
  16. package/dist/cli/help.js.map +1 -1
  17. package/dist/cli/plugin-check.d.ts +21 -0
  18. package/dist/cli/plugin-check.d.ts.map +1 -0
  19. package/dist/cli/plugin-check.js +374 -0
  20. package/dist/cli/plugin-check.js.map +1 -0
  21. package/dist/cli/plugin-cli.d.ts +20 -0
  22. package/dist/cli/plugin-cli.d.ts.map +1 -0
  23. package/dist/cli/plugin-cli.js +123 -0
  24. package/dist/cli/plugin-cli.js.map +1 -0
  25. package/dist/cli/plugin-init.d.ts +20 -0
  26. package/dist/cli/plugin-init.d.ts.map +1 -0
  27. package/dist/cli/plugin-init.js +74 -0
  28. package/dist/cli/plugin-init.js.map +1 -0
  29. package/dist/cli/plugin-main.d.ts +3 -0
  30. package/dist/cli/plugin-main.d.ts.map +1 -0
  31. package/dist/cli/plugin-main.js +28 -0
  32. package/dist/cli/plugin-main.js.map +1 -0
  33. package/dist/client.d.ts +109 -2
  34. package/dist/client.d.ts.map +1 -1
  35. package/dist/client.js +67 -2
  36. package/dist/client.js.map +1 -1
  37. package/dist/compat.d.ts +1 -1
  38. package/dist/compat.js +2 -2
  39. package/dist/components/board.d.ts +8 -0
  40. package/dist/components/board.d.ts.map +1 -1
  41. package/dist/components/board.js +59 -15
  42. package/dist/components/board.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 +33 -1
  46. package/dist/components/event-feed.js.map +1 -1
  47. package/dist/components/markdown.d.ts.map +1 -1
  48. package/dist/components/markdown.js +6 -3
  49. package/dist/components/markdown.js.map +1 -1
  50. package/dist/components/pack-connect.d.ts +3 -1
  51. package/dist/components/pack-connect.d.ts.map +1 -1
  52. package/dist/components/pack-connect.js +4 -4
  53. package/dist/components/pack-connect.js.map +1 -1
  54. package/dist/components/pack-token.d.ts +3 -1
  55. package/dist/components/pack-token.d.ts.map +1 -1
  56. package/dist/components/pack-token.js +1 -1
  57. package/dist/components/pack-token.js.map +1 -1
  58. package/dist/components/people.d.ts.map +1 -1
  59. package/dist/components/people.js +14 -3
  60. package/dist/components/people.js.map +1 -1
  61. package/dist/components/plugin-frame.d.ts +52 -0
  62. package/dist/components/plugin-frame.d.ts.map +1 -0
  63. package/dist/components/plugin-frame.js +314 -0
  64. package/dist/components/plugin-frame.js.map +1 -0
  65. package/dist/components/plugin-settings.d.ts +38 -0
  66. package/dist/components/plugin-settings.d.ts.map +1 -0
  67. package/dist/components/plugin-settings.js +348 -0
  68. package/dist/components/plugin-settings.js.map +1 -0
  69. package/dist/components/remote-pictures.d.ts +6 -0
  70. package/dist/components/remote-pictures.d.ts.map +1 -0
  71. package/dist/components/remote-pictures.js +8 -0
  72. package/dist/components/remote-pictures.js.map +1 -0
  73. package/dist/components/shell.d.ts +2 -0
  74. package/dist/components/shell.d.ts.map +1 -1
  75. package/dist/components/shell.js +26 -1
  76. package/dist/components/shell.js.map +1 -1
  77. package/dist/components/sign-in.d.ts.map +1 -1
  78. package/dist/components/sign-in.js +49 -2
  79. package/dist/components/sign-in.js.map +1 -1
  80. package/dist/components/ui.d.ts +3 -2
  81. package/dist/components/ui.d.ts.map +1 -1
  82. package/dist/components/ui.js +6 -3
  83. package/dist/components/ui.js.map +1 -1
  84. package/dist/components/workspace.d.ts +7 -1
  85. package/dist/components/workspace.d.ts.map +1 -1
  86. package/dist/components/workspace.js +1 -1
  87. package/dist/components/workspace.js.map +1 -1
  88. package/dist/console.css +384 -5
  89. package/dist/console.d.ts.map +1 -1
  90. package/dist/console.js +146 -29
  91. package/dist/console.js.map +1 -1
  92. package/dist/data.d.ts +11 -2
  93. package/dist/data.d.ts.map +1 -1
  94. package/dist/data.js +35 -21
  95. package/dist/data.js.map +1 -1
  96. package/dist/i18n/en.d.ts +163 -1
  97. package/dist/i18n/en.d.ts.map +1 -1
  98. package/dist/i18n/en.js +174 -1
  99. package/dist/i18n/en.js.map +1 -1
  100. package/dist/i18n/ko.d.ts.map +1 -1
  101. package/dist/i18n/ko.js +154 -1
  102. package/dist/i18n/ko.js.map +1 -1
  103. package/dist/index.d.ts +3 -1
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +3 -1
  106. package/dist/index.js.map +1 -1
  107. package/dist/limits.d.ts +14 -0
  108. package/dist/limits.d.ts.map +1 -1
  109. package/dist/limits.js +14 -0
  110. package/dist/limits.js.map +1 -1
  111. package/dist/plugin/args.d.ts +148 -0
  112. package/dist/plugin/args.d.ts.map +1 -0
  113. package/dist/plugin/args.js +369 -0
  114. package/dist/plugin/args.js.map +1 -0
  115. package/dist/plugin/bridge.d.ts +146 -0
  116. package/dist/plugin/bridge.d.ts.map +1 -0
  117. package/dist/plugin/bridge.js +125 -0
  118. package/dist/plugin/bridge.js.map +1 -0
  119. package/dist/plugin/capabilities.d.ts +20 -0
  120. package/dist/plugin/capabilities.d.ts.map +1 -0
  121. package/dist/plugin/capabilities.js +133 -0
  122. package/dist/plugin/capabilities.js.map +1 -0
  123. package/dist/plugin/context.d.ts +374 -0
  124. package/dist/plugin/context.d.ts.map +1 -0
  125. package/dist/plugin/context.js +2 -0
  126. package/dist/plugin/context.js.map +1 -0
  127. package/dist/plugin/define.d.ts +208 -0
  128. package/dist/plugin/define.d.ts.map +1 -0
  129. package/dist/plugin/define.js +482 -0
  130. package/dist/plugin/define.js.map +1 -0
  131. package/dist/plugin/definition-error.d.ts +11 -0
  132. package/dist/plugin/definition-error.d.ts.map +1 -0
  133. package/dist/plugin/definition-error.js +17 -0
  134. package/dist/plugin/definition-error.js.map +1 -0
  135. package/dist/plugin/limits.d.ts +67 -0
  136. package/dist/plugin/limits.d.ts.map +1 -0
  137. package/dist/plugin/limits.js +70 -0
  138. package/dist/plugin/limits.js.map +1 -0
  139. package/dist/plugin/load-worker.d.ts +7 -0
  140. package/dist/plugin/load-worker.d.ts.map +1 -0
  141. package/dist/plugin/load-worker.js +41 -0
  142. package/dist/plugin/load-worker.js.map +1 -0
  143. package/dist/plugin/load.d.ts +18 -0
  144. package/dist/plugin/load.d.ts.map +1 -0
  145. package/dist/plugin/load.js +62 -0
  146. package/dist/plugin/load.js.map +1 -0
  147. package/dist/plugin/manifest.d.ts +237 -0
  148. package/dist/plugin/manifest.d.ts.map +1 -0
  149. package/dist/plugin/manifest.js +411 -0
  150. package/dist/plugin/manifest.js.map +1 -0
  151. package/dist/plugin/refusals.d.ts +49 -0
  152. package/dist/plugin/refusals.d.ts.map +1 -0
  153. package/dist/plugin/refusals.js +79 -0
  154. package/dist/plugin/refusals.js.map +1 -0
  155. package/dist/plugin/rest-schemas.d.ts +556 -0
  156. package/dist/plugin/rest-schemas.d.ts.map +1 -0
  157. package/dist/plugin/rest-schemas.js +262 -0
  158. package/dist/plugin/rest-schemas.js.map +1 -0
  159. package/dist/plugin/sandbox.d.ts +23 -0
  160. package/dist/plugin/sandbox.d.ts.map +1 -0
  161. package/dist/plugin/sandbox.js +99 -0
  162. package/dist/plugin/sandbox.js.map +1 -0
  163. package/dist/plugin/testing.d.ts +45 -0
  164. package/dist/plugin/testing.d.ts.map +1 -0
  165. package/dist/plugin/testing.js +420 -0
  166. package/dist/plugin/testing.js.map +1 -0
  167. package/dist/plugin/web-react.d.ts +11 -0
  168. package/dist/plugin/web-react.d.ts.map +1 -0
  169. package/dist/plugin/web-react.js +34 -0
  170. package/dist/plugin/web-react.js.map +1 -0
  171. package/dist/plugin/web.d.ts +95 -0
  172. package/dist/plugin/web.d.ts.map +1 -0
  173. package/dist/plugin/web.js +245 -0
  174. package/dist/plugin/web.js.map +1 -0
  175. package/dist/plugin-nav.d.ts +29 -0
  176. package/dist/plugin-nav.d.ts.map +1 -0
  177. package/dist/plugin-nav.js +92 -0
  178. package/dist/plugin-nav.js.map +1 -0
  179. package/dist/plugin-testing.d.ts +2 -0
  180. package/dist/plugin-testing.d.ts.map +1 -0
  181. package/dist/plugin-testing.js +4 -0
  182. package/dist/plugin-testing.js.map +1 -0
  183. package/dist/plugin-web-react.d.ts +3 -0
  184. package/dist/plugin-web-react.d.ts.map +1 -0
  185. package/dist/plugin-web-react.js +3 -0
  186. package/dist/plugin-web-react.js.map +1 -0
  187. package/dist/plugin-web.d.ts +4 -0
  188. package/dist/plugin-web.d.ts.map +1 -0
  189. package/dist/plugin-web.js +5 -0
  190. package/dist/plugin-web.js.map +1 -0
  191. package/dist/plugin.d.ts +11 -0
  192. package/dist/plugin.d.ts.map +1 -0
  193. package/dist/plugin.js +15 -0
  194. package/dist/plugin.js.map +1 -0
  195. package/dist/router.d.ts +28 -0
  196. package/dist/router.d.ts.map +1 -1
  197. package/dist/router.js +47 -0
  198. package/dist/router.js.map +1 -1
  199. package/dist/routes.d.ts +6 -0
  200. package/dist/routes.d.ts.map +1 -1
  201. package/dist/routes.js +8 -0
  202. package/dist/routes.js.map +1 -1
  203. package/dist/schemas.d.ts +147 -1
  204. package/dist/schemas.d.ts.map +1 -1
  205. package/dist/schemas.js +55 -5
  206. package/dist/schemas.js.map +1 -1
  207. package/dist/tokens.d.ts +2 -0
  208. package/dist/tokens.d.ts.map +1 -0
  209. package/dist/tokens.js +101 -0
  210. package/dist/tokens.js.map +1 -0
  211. package/dist/vocabulary.d.ts +11 -1
  212. package/dist/vocabulary.d.ts.map +1 -1
  213. package/dist/vocabulary.js +24 -0
  214. package/dist/vocabulary.js.map +1 -1
  215. package/dist/web.d.ts.map +1 -1
  216. package/dist/web.js +5 -2
  217. package/dist/web.js.map +1 -1
  218. package/docs/specs/cli.md +16 -9
  219. package/docs/specs/plugins.md +487 -0
  220. package/docs/specs/rest.md +25 -13
  221. package/package.json +21 -3
  222. package/skills/building-a-console/SKILL.md +25 -26
  223. package/skills/building-a-console/references/configuring.md +40 -0
  224. package/skills/building-a-console/references/the-default-console.md +1 -1
  225. package/skills/building-a-console/references/the-look-from-a-website.md +5 -5
  226. package/skills/building-a-plugin/SKILL.md +137 -0
  227. package/skills/deploying-a-console/SKILL.md +24 -26
  228. package/skills/deploying-a-console/references/access.md +13 -9
  229. package/skills/deploying-a-console/references/resources.md +25 -10
  230. package/skills/deploying-a-console/references/running-it-again.md +15 -0
  231. package/skills/deploying-a-console/references/sizing.md +2 -0
  232. package/skills/planning-the-board/SKILL.md +96 -0
  233. package/skills/planning-the-board/references/a-task.md +36 -0
  234. package/skills/planning-the-board/references/setting-up.md +44 -0
  235. package/skills/working-the-board/SKILL.md +43 -34
  236. package/skills/working-the-board/references/the-pack.md +19 -0
  237. package/src/api.ts +16 -0
  238. package/src/cli/cli.ts +736 -45
  239. package/src/cli/format.ts +356 -9
  240. package/src/cli/help.ts +71 -6
  241. package/src/cli/plugin-check.ts +470 -0
  242. package/src/cli/plugin-cli.ts +148 -0
  243. package/src/cli/plugin-init.ts +90 -0
  244. package/src/cli/plugin-main.ts +30 -0
  245. package/src/client.ts +324 -1
  246. package/src/compat.ts +2 -2
  247. package/src/components/board.tsx +62 -14
  248. package/src/components/event-feed.tsx +42 -1
  249. package/src/components/markdown.tsx +6 -3
  250. package/src/components/pack-connect.tsx +10 -7
  251. package/src/components/pack-token.tsx +4 -1
  252. package/src/components/people.tsx +17 -7
  253. package/src/components/plugin-frame.tsx +437 -0
  254. package/src/components/plugin-settings.tsx +1111 -0
  255. package/src/components/remote-pictures.ts +19 -0
  256. package/src/components/shell.tsx +82 -14
  257. package/src/components/sign-in.tsx +114 -13
  258. package/src/components/ui.tsx +6 -3
  259. package/src/components/workspace.tsx +18 -0
  260. package/src/console.tsx +309 -23
  261. package/src/data.ts +47 -22
  262. package/src/i18n/en.ts +183 -1
  263. package/src/i18n/ko.ts +159 -1
  264. package/src/index.ts +12 -0
  265. package/src/limits.ts +14 -0
  266. package/src/plugin/args.ts +526 -0
  267. package/src/plugin/bridge.ts +314 -0
  268. package/src/plugin/capabilities.ts +150 -0
  269. package/src/plugin/context.ts +416 -0
  270. package/src/plugin/define.ts +792 -0
  271. package/src/plugin/definition-error.ts +19 -0
  272. package/src/plugin/limits.ts +70 -0
  273. package/src/plugin/load-worker.ts +50 -0
  274. package/src/plugin/load.ts +82 -0
  275. package/src/plugin/manifest.ts +561 -0
  276. package/src/plugin/refusals.ts +94 -0
  277. package/src/plugin/rest-schemas.ts +354 -0
  278. package/src/plugin/sandbox.ts +123 -0
  279. package/src/plugin/testing.ts +612 -0
  280. package/src/plugin/web-react.ts +45 -0
  281. package/src/plugin/web.ts +403 -0
  282. package/src/plugin-nav.ts +132 -0
  283. package/src/plugin-testing.ts +10 -0
  284. package/src/plugin-web-react.ts +4 -0
  285. package/src/plugin-web.ts +35 -0
  286. package/src/plugin.ts +126 -0
  287. package/src/router.ts +73 -0
  288. package/src/routes.ts +9 -0
  289. package/src/schemas.ts +60 -4
  290. package/src/styles/console.css +384 -5
  291. package/src/tokens.ts +101 -0
  292. package/src/vocabulary.ts +30 -0
  293. package/src/web.ts +5 -2
  294. package/template/plugin/.gitignore +2 -0
  295. package/template/plugin/README.md +26 -0
  296. package/template/plugin/SKILLCDN.md +13 -0
  297. package/template/plugin/build.mjs +40 -0
  298. package/template/plugin/console-plugin.json +50 -0
  299. package/template/plugin/package.json +27 -0
  300. package/template/plugin/skills/__key__/SKILL.md +23 -0
  301. package/template/plugin/src/check.test.ts +34 -0
  302. package/template/plugin/src/server/index.test.ts +41 -0
  303. package/template/plugin/src/server/index.ts +72 -0
  304. package/template/plugin/src/web/index.css +51 -0
  305. package/template/plugin/src/web/index.ts +115 -0
  306. package/template/plugin/tsconfig.json +17 -0
  307. package/skills/working-the-board/references/setting-up.md +0 -3
  308. /package/skills/{working-the-board → planning-the-board}/references/project-rules.md +0 -0
@@ -0,0 +1,526 @@
1
+ // The arguments of a plugin's commands and queries (docs/specs/plugins.md, "The server module"):
2
+ // each defined once, as data the console parses command lines against, writes help from and draws
3
+ // forms from, and as a type the command's `run` receives. A definer yields a plain object; what
4
+ // crosses from the plugin's thread to the console is exactly that object.
5
+ import { PluginDefinitionError } from "./definition-error.js";
6
+ import { MAX_PLUGIN_ABOUT_LENGTH, MAX_PLUGIN_OPTIONS } from "./limits.js";
7
+ import { PLUGIN_KEY_PATTERN } from "./manifest.js";
8
+
9
+ export const ARG_TYPES = [
10
+ "text",
11
+ "markdown",
12
+ "day",
13
+ "number",
14
+ "boolean",
15
+ "enum",
16
+ "person",
17
+ "project",
18
+ "record",
19
+ "list",
20
+ "file",
21
+ ] as const;
22
+
23
+ export type ArgType = (typeof ARG_TYPES)[number];
24
+
25
+ /** An argument as the console reads it: what every definer yields, and what help and the parser go by. */
26
+ export interface ArgDescription {
27
+ readonly type: ArgType;
28
+ /** One line, shown in help. */
29
+ readonly about?: string;
30
+ /** Left out, the argument is `undefined`; else the command refuses without it. */
31
+ readonly optional: boolean;
32
+ /** Given by position, in declaration order, before the flags; else as `--<name> <value>`. */
33
+ readonly positional: boolean;
34
+ /** A text hidden in the event the command leaves. */
35
+ readonly secret?: boolean;
36
+ /** The most characters of a text, the most of a number, or the most items of a list. */
37
+ readonly max?: number;
38
+ /** The least of a number. */
39
+ readonly min?: number;
40
+ /** A number that is whole. */
41
+ readonly integer?: boolean;
42
+ /** The words an `enum` takes. */
43
+ readonly options?: readonly string[];
44
+ /** The collection a `record` names an id of. */
45
+ readonly collection?: string;
46
+ /** What each item of a `list` is. */
47
+ readonly of?: ArgDescription;
48
+ }
49
+
50
+ /**
51
+ * A description that also carries, in its type alone, the value `run` receives and whether it
52
+ * may be left out: `__value` and `__optional` are never set. They are kept apart from the fields
53
+ * the console reads, so that a definer's result infers from what was asked of it and not from
54
+ * where it is placed.
55
+ */
56
+ export interface ArgDefinition<T = unknown, Optional extends boolean = boolean>
57
+ extends ArgDescription {
58
+ readonly __value?: T;
59
+ readonly __optional?: Optional;
60
+ }
61
+
62
+ export type ArgDefinitions = Readonly<Record<string, ArgDescription>>;
63
+
64
+ /** The value an argument's definition stands for. */
65
+ export type ArgValue<D> = D extends ArgDefinition<infer T, boolean> ? T : unknown;
66
+
67
+ /** True for a definer's result with `optional: true`; a bare description is required. */
68
+ type IsOptional<D> =
69
+ D extends ArgDefinition<unknown, true>
70
+ ? D extends ArgDefinition<unknown, false>
71
+ ? false
72
+ : true
73
+ : false;
74
+
75
+ type RequiredKeys<A extends ArgDefinitions> = {
76
+ [K in keyof A]: IsOptional<A[K]> extends true ? never : K;
77
+ }[keyof A];
78
+ type OptionalKeys<A extends ArgDefinitions> = {
79
+ [K in keyof A]: IsOptional<A[K]> extends true ? K : never;
80
+ }[keyof A];
81
+
82
+ /** The arguments `run` receives, from their definitions: the optional ones may be left out. */
83
+ export type ArgsOf<A extends ArgDefinitions> = {
84
+ readonly [K in RequiredKeys<A>]: ArgValue<A[K]>;
85
+ } & {
86
+ readonly [K in OptionalKeys<A>]?: ArgValue<A[K]>;
87
+ };
88
+
89
+ /** A person as an argument resolves to: the console found them among those the caller may see. */
90
+ export interface PluginPersonRef {
91
+ readonly id: string;
92
+ readonly login: string;
93
+ readonly name: string | null;
94
+ }
95
+
96
+ /** A project as an argument resolves to: one the caller may see. */
97
+ export interface PluginProjectRef {
98
+ readonly id: string;
99
+ readonly key: string;
100
+ readonly name: string;
101
+ }
102
+
103
+ /** A file as a command receives it: uploaded first, held for an hour, attached to a record by the plugin. */
104
+ export interface PluginFileHandle {
105
+ readonly handle: string;
106
+ readonly name: string;
107
+ readonly type: string;
108
+ readonly bytes: number;
109
+ }
110
+
111
+ interface CommonOptions<O extends boolean> {
112
+ readonly about?: string;
113
+ readonly positional?: boolean;
114
+ readonly optional?: O;
115
+ }
116
+
117
+ function common<O extends boolean>(
118
+ options: CommonOptions<O> | undefined,
119
+ ): {
120
+ about?: string;
121
+ optional: O;
122
+ positional: boolean;
123
+ } {
124
+ const about = options?.about;
125
+ if (
126
+ about !== undefined &&
127
+ (about.length === 0 || about.length > MAX_PLUGIN_ABOUT_LENGTH || /[\r\n]/.test(about))
128
+ ) {
129
+ throw new PluginDefinitionError([
130
+ `an argument's about is one line of 1 to ${MAX_PLUGIN_ABOUT_LENGTH} characters`,
131
+ ]);
132
+ }
133
+ return {
134
+ ...(about === undefined ? {} : { about }),
135
+ optional: (options?.optional ?? false) as O,
136
+ positional: options?.positional ?? false,
137
+ };
138
+ }
139
+
140
+ function bound(name: string, value: number | undefined, integer: boolean): void {
141
+ if (value !== undefined && (!Number.isFinite(value) || (integer && !Number.isInteger(value)))) {
142
+ throw new PluginDefinitionError([`${name} is not a ${integer ? "whole " : ""}number`]);
143
+ }
144
+ }
145
+
146
+ /** The definers: `arg.text({ max: 200 })`, `arg.day({ positional: true })`, `arg.list(arg.person())`. */
147
+ export const arg = {
148
+ /** A line of text, up to `max` characters; `secret` hides it in the event the command leaves. */
149
+ text<O extends boolean = false>(
150
+ options?: CommonOptions<O> & { readonly max?: number; readonly secret?: boolean },
151
+ ): ArgDefinition<string, O> {
152
+ bound("max", options?.max, true);
153
+ return {
154
+ type: "text",
155
+ ...common(options),
156
+ ...(options?.max === undefined ? {} : { max: options.max }),
157
+ ...(options?.secret === true ? { secret: true } : {}),
158
+ };
159
+ },
160
+ /** Markdown, with its line breaks, up to `max` characters. */
161
+ markdown<O extends boolean = false>(
162
+ options?: CommonOptions<O> & { readonly max?: number },
163
+ ): ArgDefinition<string, O> {
164
+ bound("max", options?.max, true);
165
+ return {
166
+ type: "markdown",
167
+ ...common(options),
168
+ ...(options?.max === undefined ? {} : { max: options.max }),
169
+ };
170
+ },
171
+ /** A day as `YYYY-MM-DD`. */
172
+ day<O extends boolean = false>(options?: CommonOptions<O>): ArgDefinition<string, O> {
173
+ return { type: "day", ...common(options) };
174
+ },
175
+ /** A number, within `min` and `max`, whole where `integer`. */
176
+ number<O extends boolean = false>(
177
+ options?: CommonOptions<O> & {
178
+ readonly min?: number;
179
+ readonly max?: number;
180
+ readonly integer?: boolean;
181
+ },
182
+ ): ArgDefinition<number, O> {
183
+ bound("min", options?.min, false);
184
+ bound("max", options?.max, false);
185
+ if (options?.min !== undefined && options.max !== undefined && options.min > options.max) {
186
+ throw new PluginDefinitionError(["min is above max"]);
187
+ }
188
+ return {
189
+ type: "number",
190
+ ...common(options),
191
+ ...(options?.min === undefined ? {} : { min: options.min }),
192
+ ...(options?.max === undefined ? {} : { max: options.max }),
193
+ ...(options?.integer === true ? { integer: true } : {}),
194
+ };
195
+ },
196
+ /** `true` or `false`; on the command line, `--<name>` alone is `true`. */
197
+ boolean<O extends boolean = false>(options?: CommonOptions<O>): ArgDefinition<boolean, O> {
198
+ return { type: "boolean", ...common(options) };
199
+ },
200
+ /** One of the words given. */
201
+ enum<const V extends readonly [string, ...string[]], O extends boolean = false>(
202
+ values: V,
203
+ options?: CommonOptions<O>,
204
+ ): ArgDefinition<V[number], O> {
205
+ if (
206
+ values.length === 0 ||
207
+ values.length > MAX_PLUGIN_OPTIONS ||
208
+ new Set(values).size !== values.length
209
+ ) {
210
+ throw new PluginDefinitionError([`an enum takes 1 to ${MAX_PLUGIN_OPTIONS} different words`]);
211
+ }
212
+ for (const value of values) {
213
+ if (value.length === 0 || value.length > 64 || /\s/.test(value)) {
214
+ throw new PluginDefinitionError([
215
+ `the enum word "${value}" is not one word of up to 64 characters`,
216
+ ]);
217
+ }
218
+ }
219
+ return { type: "enum", ...common(options), options: [...values] };
220
+ },
221
+ /** A person: a login or `me` on the command line, resolved by the console to one the caller may see. */
222
+ person<O extends boolean = false>(options?: CommonOptions<O>): ArgDefinition<PluginPersonRef, O> {
223
+ return { type: "person", ...common(options) };
224
+ },
225
+ /** A project: a key the caller may see. */
226
+ project<O extends boolean = false>(
227
+ options?: CommonOptions<O>,
228
+ ): ArgDefinition<PluginProjectRef, O> {
229
+ return { type: "project", ...common(options) };
230
+ },
231
+ /** The id of a record of the collection named, one of this plugin's. */
232
+ record<O extends boolean = false>(
233
+ collection: string,
234
+ options?: CommonOptions<O>,
235
+ ): ArgDefinition<string, O> {
236
+ if (!PLUGIN_KEY_PATTERN.test(collection)) {
237
+ throw new PluginDefinitionError([
238
+ `arg.record names the collection "${collection}", which is not a key`,
239
+ ]);
240
+ }
241
+ return { type: "record", ...common(options), collection };
242
+ },
243
+ /** Several of the inner argument: the flag repeated, or commas between; up to `max` items. */
244
+ list<D extends ArgDefinition, O extends boolean = false>(
245
+ of: D,
246
+ options?: CommonOptions<O> & { readonly max?: number },
247
+ ): ArgDefinition<ArgValue<D>[], O> {
248
+ if (of.type === "list" || of.type === "file" || of.type === "markdown") {
249
+ throw new PluginDefinitionError([`a list of ${of.type} is not an argument`]);
250
+ }
251
+ if (of.optional || of.positional) {
252
+ throw new PluginDefinitionError([
253
+ "the items of a list are neither optional nor positional themselves",
254
+ ]);
255
+ }
256
+ bound("max", options?.max, true);
257
+ return {
258
+ type: "list",
259
+ ...common(options),
260
+ ...(options?.max === undefined ? {} : { max: options.max }),
261
+ of,
262
+ };
263
+ },
264
+ /** A file: what `--file <path>` or `-` carried, uploaded first and handed as a handle. */
265
+ file<O extends boolean = false>(
266
+ options?: Omit<CommonOptions<O>, "positional">,
267
+ ): ArgDefinition<PluginFileHandle, O> {
268
+ return { type: "file", ...common(options) };
269
+ },
270
+ };
271
+
272
+ const DAY = /^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/;
273
+ const DEFAULT_TEXT_MAX = 2000;
274
+ const DEFAULT_MARKDOWN_MAX = 20_000;
275
+ const DEFAULT_LIST_MAX = 100;
276
+
277
+ /**
278
+ * The problem with a value for an argument, as it arrives on the wire before the console
279
+ * resolves it: a person is a login or `me`, a project a key, a record an id, a file a handle, each
280
+ * a string. Nothing when the value fits.
281
+ */
282
+ export function argProblem(
283
+ name: string,
284
+ description: ArgDescription,
285
+ value: unknown,
286
+ ): string | undefined {
287
+ if (value === undefined) {
288
+ return description.optional ? undefined : `${name} is required`;
289
+ }
290
+ switch (description.type) {
291
+ case "text":
292
+ case "markdown": {
293
+ if (typeof value !== "string") {
294
+ return `${name} is not text`;
295
+ }
296
+ const max =
297
+ description.max ?? (description.type === "text" ? DEFAULT_TEXT_MAX : DEFAULT_MARKDOWN_MAX);
298
+ if (value.length > max) {
299
+ return `${name} is over ${max} characters`;
300
+ }
301
+ if (description.type === "text" && /[\r\n]/.test(value)) {
302
+ return `${name} is one line`;
303
+ }
304
+ return undefined;
305
+ }
306
+ case "day":
307
+ return typeof value === "string" && DAY.test(value)
308
+ ? undefined
309
+ : `${name} is not a day as YYYY-MM-DD`;
310
+ case "number": {
311
+ if (typeof value !== "number" || !Number.isFinite(value)) {
312
+ return `${name} is not a number`;
313
+ }
314
+ if (description.integer === true && !Number.isInteger(value)) {
315
+ return `${name} is not a whole number`;
316
+ }
317
+ if (description.min !== undefined && value < description.min) {
318
+ return `${name} is below ${description.min}`;
319
+ }
320
+ if (description.max !== undefined && value > description.max) {
321
+ return `${name} is above ${description.max}`;
322
+ }
323
+ return undefined;
324
+ }
325
+ case "boolean":
326
+ return typeof value === "boolean" ? undefined : `${name} is true or false`;
327
+ case "enum":
328
+ return typeof value === "string" && (description.options ?? []).includes(value)
329
+ ? undefined
330
+ : `${name} is one of ${(description.options ?? []).join(", ")}`;
331
+ case "person":
332
+ case "project":
333
+ case "record":
334
+ case "file":
335
+ return typeof value === "string" && value.length > 0 && value.length <= 254
336
+ ? undefined
337
+ : `${name} is not valid`;
338
+ case "list": {
339
+ if (!Array.isArray(value)) {
340
+ return `${name} is a list`;
341
+ }
342
+ const max = description.max ?? DEFAULT_LIST_MAX;
343
+ if (value.length > max) {
344
+ return `${name} has more than ${max} items`;
345
+ }
346
+ const of = description.of;
347
+ if (of === undefined) {
348
+ return `${name} has no item type`;
349
+ }
350
+ for (const [index, item] of value.entries()) {
351
+ const problem = argProblem(`${name}[${index}]`, of, item);
352
+ if (problem !== undefined) {
353
+ return problem;
354
+ }
355
+ }
356
+ return undefined;
357
+ }
358
+ default:
359
+ return `${name} has a type the console does not know`;
360
+ }
361
+ }
362
+
363
+ /** The first problem with a set of values against their definitions, unknown names included; nothing when all fit. */
364
+ export function argsProblem(
365
+ definitions: Readonly<Record<string, ArgDescription>>,
366
+ values: Readonly<Record<string, unknown>>,
367
+ ): string | undefined {
368
+ for (const name of Object.keys(values)) {
369
+ if (!(name in definitions)) {
370
+ return `${name} is not an argument of this command`;
371
+ }
372
+ }
373
+ for (const [name, description] of Object.entries(definitions)) {
374
+ const problem = argProblem(name, description, values[name]);
375
+ if (problem !== undefined) {
376
+ return problem;
377
+ }
378
+ }
379
+ return undefined;
380
+ }
381
+
382
+ /**
383
+ * A word from the command line as the value an argument takes: a number read, a boolean read,
384
+ * a list split at commas; text as it is. Nothing the word cannot be is a problem `argProblem`
385
+ * then names.
386
+ */
387
+ export function argValueFromWord(description: ArgDescription, word: string): unknown {
388
+ switch (description.type) {
389
+ case "number": {
390
+ const trimmed = word.trim();
391
+ return /^-?\d+(?:\.\d+)?$/.test(trimmed) ? Number(trimmed) : word;
392
+ }
393
+ case "boolean":
394
+ return word === "true" || word === "" ? true : word === "false" ? false : word;
395
+ case "list": {
396
+ const of = description.of;
397
+ return word
398
+ .split(",")
399
+ .map((item) => item.trim())
400
+ .filter((item) => item.length > 0)
401
+ .map((item) => (of === undefined ? item : argValueFromWord(of, item)));
402
+ }
403
+ default:
404
+ return word;
405
+ }
406
+ }
407
+
408
+ /** The words of a command line as the values of a command's arguments, or the first word that does not fit. */
409
+ export type ParsedArgv =
410
+ | { readonly values: Record<string, unknown>; readonly problem?: undefined }
411
+ | { readonly problem: string; readonly values?: undefined };
412
+
413
+ /**
414
+ * The words of a command line as the arguments of a command or a query, by its definitions
415
+ * (docs/specs/plugins.md, "The command"): the positional arguments in declaration order, one word
416
+ * each, among the flags, `--name value` or `--name=value`; a boolean flag alone means true; a
417
+ * list is a flag given again, or commas between its items; `--` ends the flags. An argument of
418
+ * type `file` takes the handles of the files the command uploaded, one each in declaration
419
+ * order, where none is named. The values are then checked with `argsProblem`.
420
+ */
421
+ export function parseArgv(
422
+ definitions: Readonly<Record<string, ArgDescription>>,
423
+ argv: readonly string[],
424
+ files: readonly string[] = [],
425
+ ): ParsedArgv {
426
+ const values: Record<string, unknown> = {};
427
+ const positionals = Object.entries(definitions).filter(
428
+ ([, description]) => description.positional,
429
+ );
430
+ let position = 0;
431
+ let flagsEnded = false;
432
+ const put = (name: string, description: ArgDescription, word: string): string | undefined => {
433
+ const value = argValueFromWord(description, word);
434
+ if (description.type === "list") {
435
+ const current = values[name];
436
+ values[name] = Array.isArray(current) ? [...current, ...(value as unknown[])] : value;
437
+ return undefined;
438
+ }
439
+ if (Object.hasOwn(values, name)) {
440
+ return `${name} is given more than once`;
441
+ }
442
+ values[name] = value;
443
+ return undefined;
444
+ };
445
+ for (let index = 0; index < argv.length; index += 1) {
446
+ const word = argv[index] ?? "";
447
+ if (!flagsEnded && word === "--") {
448
+ flagsEnded = true;
449
+ continue;
450
+ }
451
+ if (!flagsEnded && word.startsWith("--") && word.length > 2) {
452
+ const equals = word.indexOf("=");
453
+ const name = equals < 0 ? word.slice(2) : word.slice(2, equals);
454
+ const description = Object.hasOwn(definitions, name) ? definitions[name] : undefined;
455
+ if (description === undefined) {
456
+ return { problem: `--${name} is not an argument of this command` };
457
+ }
458
+ let given: string;
459
+ if (equals >= 0) {
460
+ given = word.slice(equals + 1);
461
+ } else if (description.type === "boolean") {
462
+ given = "";
463
+ } else {
464
+ const next = argv[index + 1];
465
+ if (next === undefined) {
466
+ return { problem: `--${name} needs a value` };
467
+ }
468
+ given = next;
469
+ index += 1;
470
+ }
471
+ const problem = put(name, description, given);
472
+ if (problem !== undefined) {
473
+ return { problem };
474
+ }
475
+ continue;
476
+ }
477
+ const slot = positionals[position];
478
+ if (slot === undefined) {
479
+ return {
480
+ problem:
481
+ positionals.length === 0
482
+ ? `${JSON.stringify(word)} is one word too many: the command takes no positional argument, only flags`
483
+ : `${JSON.stringify(word)} is one word too many: the command takes ${positionals.length} positional argument${positionals.length === 1 ? "" : "s"}`,
484
+ };
485
+ }
486
+ const problem = put(slot[0], slot[1], word);
487
+ if (problem !== undefined) {
488
+ return { problem };
489
+ }
490
+ position += 1;
491
+ }
492
+ let taken = 0;
493
+ let slots = 0;
494
+ for (const [name, description] of Object.entries(definitions)) {
495
+ if (description.type !== "file") {
496
+ continue;
497
+ }
498
+ slots += 1;
499
+ if (!Object.hasOwn(values, name) && taken < files.length) {
500
+ values[name] = files[taken];
501
+ taken += 1;
502
+ }
503
+ }
504
+ if (taken < files.length) {
505
+ return {
506
+ problem:
507
+ slots === 0
508
+ ? "the command takes no file"
509
+ : `the command takes ${slots} file${slots === 1 ? "" : "s"}, not ${files.length}`,
510
+ };
511
+ }
512
+ return { values };
513
+ }
514
+
515
+ /** The usage of an argument, as help prints it: `<from>` for a positional, `--to <day>` for a flag, in brackets where optional. */
516
+ export function argUsage(name: string, description: ArgDescription): string {
517
+ const placeholder =
518
+ description.type === "list" ? `${description.of?.type ?? "item"},...` : description.type;
519
+ const form =
520
+ description.type === "boolean" && !description.positional
521
+ ? `--${name}`
522
+ : description.positional
523
+ ? `<${name}>`
524
+ : `--${name} <${placeholder}>`;
525
+ return description.optional ? `[${form}]` : form;
526
+ }