@skillcdn/console 0.4.1 → 0.4.3

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 (299) hide show
  1. package/README.md +19 -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 +569 -48
  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 +283 -7
  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 +44 -7
  16. package/dist/cli/help.js.map +1 -1
  17. package/dist/cli/plugin-check.d.ts +26 -0
  18. package/dist/cli/plugin-check.d.ts.map +1 -0
  19. package/dist/cli/plugin-check.js +396 -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 +189 -0
  24. package/dist/cli/plugin-cli.js.map +1 -0
  25. package/dist/cli/plugin-init.d.ts +37 -0
  26. package/dist/cli/plugin-init.d.ts.map +1 -0
  27. package/dist/cli/plugin-init.js +206 -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 +100 -3
  34. package/dist/client.d.ts.map +1 -1
  35. package/dist/client.js +79 -7
  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/comments.d.ts.map +1 -1
  44. package/dist/components/comments.js +1 -1
  45. package/dist/components/comments.js.map +1 -1
  46. package/dist/components/decision-form.d.ts +2 -1
  47. package/dist/components/decision-form.d.ts.map +1 -1
  48. package/dist/components/decision-form.js +36 -4
  49. package/dist/components/decision-form.js.map +1 -1
  50. package/dist/components/decision-list.d.ts +4 -0
  51. package/dist/components/decision-list.d.ts.map +1 -1
  52. package/dist/components/decision-list.js +29 -3
  53. package/dist/components/decision-list.js.map +1 -1
  54. package/dist/components/event-feed.d.ts +1 -1
  55. package/dist/components/event-feed.d.ts.map +1 -1
  56. package/dist/components/event-feed.js +33 -1
  57. package/dist/components/event-feed.js.map +1 -1
  58. package/dist/components/my-decisions.d.ts +2 -0
  59. package/dist/components/my-decisions.d.ts.map +1 -1
  60. package/dist/components/my-decisions.js +2 -2
  61. package/dist/components/my-decisions.js.map +1 -1
  62. package/dist/components/plugin-frame.d.ts +52 -0
  63. package/dist/components/plugin-frame.d.ts.map +1 -0
  64. package/dist/components/plugin-frame.js +314 -0
  65. package/dist/components/plugin-frame.js.map +1 -0
  66. package/dist/components/plugin-settings.d.ts +38 -0
  67. package/dist/components/plugin-settings.d.ts.map +1 -0
  68. package/dist/components/plugin-settings.js +348 -0
  69. package/dist/components/plugin-settings.js.map +1 -0
  70. package/dist/components/shell.d.ts +2 -0
  71. package/dist/components/shell.d.ts.map +1 -1
  72. package/dist/components/shell.js +26 -1
  73. package/dist/components/shell.js.map +1 -1
  74. package/dist/components/task-view.d.ts +2 -1
  75. package/dist/components/task-view.d.ts.map +1 -1
  76. package/dist/components/task-view.js.map +1 -1
  77. package/dist/components/workspace.d.ts +7 -1
  78. package/dist/components/workspace.d.ts.map +1 -1
  79. package/dist/components/workspace.js +1 -1
  80. package/dist/components/workspace.js.map +1 -1
  81. package/dist/console.css +330 -1
  82. package/dist/console.d.ts.map +1 -1
  83. package/dist/console.js +152 -41
  84. package/dist/console.js.map +1 -1
  85. package/dist/data.d.ts +9 -2
  86. package/dist/data.d.ts.map +1 -1
  87. package/dist/data.js +45 -9
  88. package/dist/data.js.map +1 -1
  89. package/dist/i18n/en.d.ts +155 -0
  90. package/dist/i18n/en.d.ts.map +1 -1
  91. package/dist/i18n/en.js +166 -0
  92. package/dist/i18n/en.js.map +1 -1
  93. package/dist/i18n/ko.d.ts.map +1 -1
  94. package/dist/i18n/ko.js +150 -0
  95. package/dist/i18n/ko.js.map +1 -1
  96. package/dist/index.d.ts +3 -1
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +3 -1
  99. package/dist/index.js.map +1 -1
  100. package/dist/limits.d.ts +2 -0
  101. package/dist/limits.d.ts.map +1 -1
  102. package/dist/limits.js +2 -0
  103. package/dist/limits.js.map +1 -1
  104. package/dist/plugin/args.d.ts +148 -0
  105. package/dist/plugin/args.d.ts.map +1 -0
  106. package/dist/plugin/args.js +369 -0
  107. package/dist/plugin/args.js.map +1 -0
  108. package/dist/plugin/bridge.d.ts +146 -0
  109. package/dist/plugin/bridge.d.ts.map +1 -0
  110. package/dist/plugin/bridge.js +125 -0
  111. package/dist/plugin/bridge.js.map +1 -0
  112. package/dist/plugin/capabilities.d.ts +20 -0
  113. package/dist/plugin/capabilities.d.ts.map +1 -0
  114. package/dist/plugin/capabilities.js +133 -0
  115. package/dist/plugin/capabilities.js.map +1 -0
  116. package/dist/plugin/context.d.ts +380 -0
  117. package/dist/plugin/context.d.ts.map +1 -0
  118. package/dist/plugin/context.js +2 -0
  119. package/dist/plugin/context.js.map +1 -0
  120. package/dist/plugin/define.d.ts +208 -0
  121. package/dist/plugin/define.d.ts.map +1 -0
  122. package/dist/plugin/define.js +482 -0
  123. package/dist/plugin/define.js.map +1 -0
  124. package/dist/plugin/definition-error.d.ts +11 -0
  125. package/dist/plugin/definition-error.d.ts.map +1 -0
  126. package/dist/plugin/definition-error.js +17 -0
  127. package/dist/plugin/definition-error.js.map +1 -0
  128. package/dist/plugin/limits.d.ts +67 -0
  129. package/dist/plugin/limits.d.ts.map +1 -0
  130. package/dist/plugin/limits.js +70 -0
  131. package/dist/plugin/limits.js.map +1 -0
  132. package/dist/plugin/load-worker.d.ts +7 -0
  133. package/dist/plugin/load-worker.d.ts.map +1 -0
  134. package/dist/plugin/load-worker.js +41 -0
  135. package/dist/plugin/load-worker.js.map +1 -0
  136. package/dist/plugin/load.d.ts +18 -0
  137. package/dist/plugin/load.d.ts.map +1 -0
  138. package/dist/plugin/load.js +62 -0
  139. package/dist/plugin/load.js.map +1 -0
  140. package/dist/plugin/manifest.d.ts +237 -0
  141. package/dist/plugin/manifest.d.ts.map +1 -0
  142. package/dist/plugin/manifest.js +411 -0
  143. package/dist/plugin/manifest.js.map +1 -0
  144. package/dist/plugin/refusals.d.ts +49 -0
  145. package/dist/plugin/refusals.d.ts.map +1 -0
  146. package/dist/plugin/refusals.js +79 -0
  147. package/dist/plugin/refusals.js.map +1 -0
  148. package/dist/plugin/rest-schemas.d.ts +556 -0
  149. package/dist/plugin/rest-schemas.d.ts.map +1 -0
  150. package/dist/plugin/rest-schemas.js +262 -0
  151. package/dist/plugin/rest-schemas.js.map +1 -0
  152. package/dist/plugin/sandbox.d.ts +23 -0
  153. package/dist/plugin/sandbox.d.ts.map +1 -0
  154. package/dist/plugin/sandbox.js +99 -0
  155. package/dist/plugin/sandbox.js.map +1 -0
  156. package/dist/plugin/testing.d.ts +45 -0
  157. package/dist/plugin/testing.d.ts.map +1 -0
  158. package/dist/plugin/testing.js +420 -0
  159. package/dist/plugin/testing.js.map +1 -0
  160. package/dist/plugin/web-react.d.ts +11 -0
  161. package/dist/plugin/web-react.d.ts.map +1 -0
  162. package/dist/plugin/web-react.js +34 -0
  163. package/dist/plugin/web-react.js.map +1 -0
  164. package/dist/plugin/web.d.ts +95 -0
  165. package/dist/plugin/web.d.ts.map +1 -0
  166. package/dist/plugin/web.js +245 -0
  167. package/dist/plugin/web.js.map +1 -0
  168. package/dist/plugin-nav.d.ts +29 -0
  169. package/dist/plugin-nav.d.ts.map +1 -0
  170. package/dist/plugin-nav.js +92 -0
  171. package/dist/plugin-nav.js.map +1 -0
  172. package/dist/plugin-testing.d.ts +2 -0
  173. package/dist/plugin-testing.d.ts.map +1 -0
  174. package/dist/plugin-testing.js +4 -0
  175. package/dist/plugin-testing.js.map +1 -0
  176. package/dist/plugin-web-react.d.ts +3 -0
  177. package/dist/plugin-web-react.d.ts.map +1 -0
  178. package/dist/plugin-web-react.js +3 -0
  179. package/dist/plugin-web-react.js.map +1 -0
  180. package/dist/plugin-web.d.ts +4 -0
  181. package/dist/plugin-web.d.ts.map +1 -0
  182. package/dist/plugin-web.js +5 -0
  183. package/dist/plugin-web.js.map +1 -0
  184. package/dist/plugin.d.ts +11 -0
  185. package/dist/plugin.d.ts.map +1 -0
  186. package/dist/plugin.js +15 -0
  187. package/dist/plugin.js.map +1 -0
  188. package/dist/router.d.ts +28 -0
  189. package/dist/router.d.ts.map +1 -1
  190. package/dist/router.js +47 -0
  191. package/dist/router.js.map +1 -1
  192. package/dist/routes.d.ts +4 -0
  193. package/dist/routes.d.ts.map +1 -1
  194. package/dist/routes.js +6 -0
  195. package/dist/routes.js.map +1 -1
  196. package/dist/schemas.d.ts +194 -9
  197. package/dist/schemas.d.ts.map +1 -1
  198. package/dist/schemas.js +98 -17
  199. package/dist/schemas.js.map +1 -1
  200. package/dist/tokens.d.ts +2 -0
  201. package/dist/tokens.d.ts.map +1 -0
  202. package/dist/tokens.js +101 -0
  203. package/dist/tokens.js.map +1 -0
  204. package/dist/vocabulary.d.ts +11 -1
  205. package/dist/vocabulary.d.ts.map +1 -1
  206. package/dist/vocabulary.js +24 -0
  207. package/dist/vocabulary.js.map +1 -1
  208. package/dist/web.d.ts.map +1 -1
  209. package/dist/web.js +2 -0
  210. package/dist/web.js.map +1 -1
  211. package/docs/specs/cli.md +12 -5
  212. package/docs/specs/plugins.md +488 -0
  213. package/docs/specs/rest.md +11 -9
  214. package/package.json +26 -8
  215. package/skills/building-a-console/SKILL.md +25 -26
  216. package/skills/building-a-console/references/configuring.md +40 -0
  217. package/skills/building-a-console/references/the-default-console.md +1 -1
  218. package/skills/building-a-console/references/the-look-from-a-website.md +5 -5
  219. package/skills/building-a-plugin/SKILL.md +137 -0
  220. package/skills/deploying-a-console/SKILL.md +24 -26
  221. package/skills/deploying-a-console/references/access.md +13 -9
  222. package/skills/deploying-a-console/references/resources.md +25 -10
  223. package/skills/deploying-a-console/references/running-it-again.md +15 -0
  224. package/skills/deploying-a-console/references/sizing.md +2 -0
  225. package/skills/planning-the-board/SKILL.md +96 -0
  226. package/skills/planning-the-board/references/a-task.md +36 -0
  227. package/skills/planning-the-board/references/setting-up.md +44 -0
  228. package/skills/working-the-board/SKILL.md +43 -34
  229. package/skills/working-the-board/references/the-pack.md +19 -0
  230. package/src/api.ts +12 -0
  231. package/src/cli/cli.ts +737 -55
  232. package/src/cli/format.ts +368 -10
  233. package/src/cli/help.ts +50 -6
  234. package/src/cli/plugin-check.ts +497 -0
  235. package/src/cli/plugin-cli.ts +237 -0
  236. package/src/cli/plugin-init.ts +257 -0
  237. package/src/cli/plugin-main.ts +30 -0
  238. package/src/client.ts +322 -9
  239. package/src/compat.ts +2 -2
  240. package/src/components/board.tsx +62 -14
  241. package/src/components/comments.tsx +1 -0
  242. package/src/components/decision-form.tsx +92 -11
  243. package/src/components/decision-list.tsx +71 -2
  244. package/src/components/event-feed.tsx +42 -1
  245. package/src/components/my-decisions.tsx +4 -1
  246. package/src/components/plugin-frame.tsx +437 -0
  247. package/src/components/plugin-settings.tsx +1111 -0
  248. package/src/components/shell.tsx +82 -14
  249. package/src/components/task-view.tsx +2 -1
  250. package/src/components/workspace.tsx +18 -0
  251. package/src/console.tsx +304 -31
  252. package/src/data.ts +59 -11
  253. package/src/i18n/en.ts +175 -0
  254. package/src/i18n/ko.ts +155 -0
  255. package/src/index.ts +12 -0
  256. package/src/limits.ts +2 -0
  257. package/src/plugin/args.ts +526 -0
  258. package/src/plugin/bridge.ts +314 -0
  259. package/src/plugin/capabilities.ts +150 -0
  260. package/src/plugin/context.ts +421 -0
  261. package/src/plugin/define.ts +792 -0
  262. package/src/plugin/definition-error.ts +19 -0
  263. package/src/plugin/limits.ts +70 -0
  264. package/src/plugin/load-worker.ts +50 -0
  265. package/src/plugin/load.ts +82 -0
  266. package/src/plugin/manifest.ts +561 -0
  267. package/src/plugin/refusals.ts +94 -0
  268. package/src/plugin/rest-schemas.ts +354 -0
  269. package/src/plugin/sandbox.ts +123 -0
  270. package/src/plugin/testing.ts +612 -0
  271. package/src/plugin/web-react.ts +45 -0
  272. package/src/plugin/web.ts +403 -0
  273. package/src/plugin-nav.ts +132 -0
  274. package/src/plugin-testing.ts +10 -0
  275. package/src/plugin-web-react.ts +4 -0
  276. package/src/plugin-web.ts +35 -0
  277. package/src/plugin.ts +126 -0
  278. package/src/router.ts +73 -0
  279. package/src/routes.ts +7 -0
  280. package/src/schemas.ts +115 -18
  281. package/src/styles/console.css +330 -1
  282. package/src/tokens.ts +101 -0
  283. package/src/vocabulary.ts +30 -0
  284. package/src/web.ts +2 -0
  285. package/template/plugin/.gitignore +2 -0
  286. package/template/plugin/README.md +26 -0
  287. package/template/plugin/SKILLCDN.md +13 -0
  288. package/template/plugin/build.mjs +40 -0
  289. package/template/plugin/console-plugin.json +50 -0
  290. package/template/plugin/package.json +27 -0
  291. package/template/plugin/skills/__key__/SKILL.md +23 -0
  292. package/template/plugin/src/check.test.ts +34 -0
  293. package/template/plugin/src/server/index.test.ts +41 -0
  294. package/template/plugin/src/server/index.ts +72 -0
  295. package/template/plugin/src/web/index.css +51 -0
  296. package/template/plugin/src/web/index.ts +115 -0
  297. package/template/plugin/tsconfig.json +17 -0
  298. package/skills/working-the-board/references/setting-up.md +0 -3
  299. /package/skills/{working-the-board → planning-the-board}/references/project-rules.md +0 -0
@@ -0,0 +1,237 @@
1
+ // The `console-plugin` command (docs/specs/plugins.md, "Authoring"): what a plugin's author runs.
2
+ // `init` writes a plugin from the template; `check` reads one as the console would. Everything it
3
+ // touches comes through `io`, so that the tests run it with no process of its own.
4
+ import { resolve } from "node:path";
5
+ import { parseArgs } from "node:util";
6
+ import type { ServerLoad } from "../plugin/load.js";
7
+ import { isPluginKey, RESERVED_PLUGIN_KEYS } from "../plugin/manifest.js";
8
+ import { checkPlugin } from "./plugin-check.js";
9
+ import { LANGUAGE_TAG, nameFromKey, PACKAGE_NAME, writePluginFromTemplate } from "./plugin-init.js";
10
+
11
+ export interface PluginCliIo {
12
+ stdout(text: string): void;
13
+ stderr(text: string): void;
14
+ readonly cwd: string;
15
+ /** The template the package ships, `template/plugin` beside `dist/`. */
16
+ readonly templateDir: string;
17
+ readonly version: string | undefined;
18
+ /** Loads a server module in a hardened thread and describes it. */
19
+ loadServer(file: string): Promise<ServerLoad>;
20
+ }
21
+
22
+ /** The exit codes: done; refused or failed; not understood. */
23
+ export const PLUGIN_EXIT = { ok: 0, failed: 1, usage: 2 } as const;
24
+
25
+ export const PLUGIN_USAGE = `console-plugin: writes and checks a plugin of the SkillCDN Console.
26
+
27
+ console-plugin init <dir> --key <key> [--name <name>] [--console <dependency>]
28
+ [--package <name>] [--repository <url> [--directory <path>]] [--languages <tags>]
29
+ Writes a plugin into <dir>, which must not exist or be empty, from the template this
30
+ package ships: the manifest, a server module with one collection, one command and one
31
+ query, the source of a web bundle with one page and one widget, one skill with its
32
+ manifest, tests, and the build. <key> is the plugin's key, lowercase letters, digits and
33
+ hyphens; --name its name for people, from the key unless said; --console what the
34
+ plugin's devDependency on @skillcdn/console says, ^<this version> unless said;
35
+ --package the package's name, console-plugin-<key> unless said, a scoped one
36
+ (@scope/console-plugin-<key>) published as public; --repository the https address of
37
+ the repository it lives in, with --directory its path there in a repository of several,
38
+ which fill repository, homepage and bugs and the manifest's store; --languages the
39
+ languages of its words with commas between, en first (en,ko), each language map and
40
+ both manifests' translations given the others as English words to translate, of which
41
+ check warns while they stay English.
42
+
43
+ console-plugin check [<dir>]
44
+ Reads the plugin in <dir>, the working directory unless said, as a console would, built:
45
+ the manifest, package.json (a permissive license, no install scripts, files that ship
46
+ what runs), the server module loaded under the host's hardening and held to the manifest,
47
+ the web bundle self-contained and its classes prefixed, the skills, the migrations.
48
+ Exits 0 with a summary, 1 with every problem found.
49
+
50
+ console-plugin help | --version
51
+
52
+ The contract is docs/specs/plugins.md of the console's repository, shipped with this package.
53
+ `;
54
+
55
+ function wordsOf(items: readonly string[], prefix: string): string {
56
+ return items.map((item) => ` ${prefix}: ${item}\n`).join("");
57
+ }
58
+
59
+ export async function runPluginCli(argv: readonly string[], io: PluginCliIo): Promise<number> {
60
+ const [command, ...rest] = argv;
61
+ if (command === undefined || command === "help" || command === "--help" || command === "-h") {
62
+ io.stdout(PLUGIN_USAGE);
63
+ return PLUGIN_EXIT.ok;
64
+ }
65
+ if (command === "--version" || command === "version") {
66
+ io.stdout(`${io.version ?? "unknown"}\n`);
67
+ return PLUGIN_EXIT.ok;
68
+ }
69
+ if (command === "init") {
70
+ return init(rest, io);
71
+ }
72
+ if (command === "check") {
73
+ return check(rest, io);
74
+ }
75
+ io.stderr(`console-plugin: ${command} is not a command; console-plugin help lists them\n`);
76
+ return PLUGIN_EXIT.usage;
77
+ }
78
+
79
+ const INIT_USAGE =
80
+ "usage: console-plugin init <dir> --key <key> [--name <name>] [--console <dependency>] [--package <name>] [--repository <url> [--directory <path>]] [--languages <tags>]\n";
81
+
82
+ async function init(args: readonly string[], io: PluginCliIo): Promise<number> {
83
+ let values: {
84
+ key?: string;
85
+ name?: string;
86
+ console?: string;
87
+ package?: string;
88
+ repository?: string;
89
+ directory?: string;
90
+ languages?: string;
91
+ };
92
+ let positionals: string[];
93
+ try {
94
+ ({ values, positionals } = parseArgs({
95
+ args: [...args],
96
+ options: {
97
+ key: { type: "string" },
98
+ name: { type: "string" },
99
+ console: { type: "string" },
100
+ package: { type: "string" },
101
+ repository: { type: "string" },
102
+ directory: { type: "string" },
103
+ languages: { type: "string" },
104
+ },
105
+ allowPositionals: true,
106
+ strict: true,
107
+ }));
108
+ } catch (error) {
109
+ io.stderr(`console-plugin init: ${error instanceof Error ? error.message : String(error)}\n`);
110
+ return PLUGIN_EXIT.usage;
111
+ }
112
+ const [dir, ...extra] = positionals;
113
+ const key = values.key;
114
+ if (dir === undefined || extra.length > 0 || key === undefined) {
115
+ io.stderr(INIT_USAGE);
116
+ return PLUGIN_EXIT.usage;
117
+ }
118
+ const packageName = values.package;
119
+ if (packageName !== undefined && (packageName.length > 214 || !PACKAGE_NAME.test(packageName))) {
120
+ io.stderr(
121
+ `console-plugin init: ${packageName} is not a package name: lowercase letters, digits, hyphens, dots and underscores, with a scope as @scope/name\n`,
122
+ );
123
+ return PLUGIN_EXIT.usage;
124
+ }
125
+ if (values.directory !== undefined && values.repository === undefined) {
126
+ io.stderr("console-plugin init: --directory goes with --repository\n");
127
+ return PLUGIN_EXIT.usage;
128
+ }
129
+ if (values.repository !== undefined && !isRepositoryAddress(values.repository)) {
130
+ io.stderr(
131
+ `console-plugin init: ${values.repository} is not the https address of a repository\n`,
132
+ );
133
+ return PLUGIN_EXIT.usage;
134
+ }
135
+ if (
136
+ values.directory !== undefined &&
137
+ !/^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(values.directory)
138
+ ) {
139
+ io.stderr(
140
+ "console-plugin init: --directory is a path within the repository, with forward slashes\n",
141
+ );
142
+ return PLUGIN_EXIT.usage;
143
+ }
144
+ const languages = values.languages
145
+ ?.split(",")
146
+ .map((language) => language.trim())
147
+ .filter((language) => language.length > 0);
148
+ if (
149
+ languages !== undefined &&
150
+ (languages.length === 0 || languages.some((language) => !LANGUAGE_TAG.test(language)))
151
+ ) {
152
+ io.stderr(
153
+ "console-plugin init: --languages is a list of language tags with commas between, en first: en,ko\n",
154
+ );
155
+ return PLUGIN_EXIT.usage;
156
+ }
157
+ if (languages !== undefined && languages[0] !== "en") {
158
+ io.stderr(
159
+ "console-plugin init: --languages begins with en, the language the template is written in\n",
160
+ );
161
+ return PLUGIN_EXIT.usage;
162
+ }
163
+ if (!isPluginKey(key)) {
164
+ io.stderr(
165
+ `console-plugin init: ${key} is not a key: lowercase letters, digits and hyphens, 2 to 32 long, starting with a letter\n`,
166
+ );
167
+ return PLUGIN_EXIT.usage;
168
+ }
169
+ if (RESERVED_PLUGIN_KEYS.includes(key)) {
170
+ io.stderr(`console-plugin init: ${key} is a word the console reserves; choose another key\n`);
171
+ return PLUGIN_EXIT.usage;
172
+ }
173
+ const name = values.name ?? nameFromKey(key);
174
+ if (name.length === 0 || name.length > 40 || /[\r\n]/.test(name)) {
175
+ io.stderr("console-plugin init: --name is one line of up to 40 characters\n");
176
+ return PLUGIN_EXIT.usage;
177
+ }
178
+ const consoleDependency = values.console ?? (io.version === undefined ? "*" : `^${io.version}`);
179
+ const target = resolve(io.cwd, dir);
180
+ try {
181
+ const written = await writePluginFromTemplate(io.templateDir, target, {
182
+ key,
183
+ name,
184
+ consoleDependency,
185
+ packageName,
186
+ repository:
187
+ values.repository === undefined
188
+ ? undefined
189
+ : { url: values.repository, directory: values.directory },
190
+ languages,
191
+ });
192
+ io.stdout(`Wrote the plugin ${key} into ${target}: ${written.length} files.\n`);
193
+ io.stdout(
194
+ `Next: cd ${dir}, install the dependencies (npm install), build (npm run build), run the tests (npm test), and check it as a console would (npm run check). The contract is docs/specs/plugins.md in node_modules/@skillcdn/console/.\n`,
195
+ );
196
+ return PLUGIN_EXIT.ok;
197
+ } catch (error) {
198
+ io.stderr(`console-plugin init: ${error instanceof Error ? error.message : String(error)}\n`);
199
+ return PLUGIN_EXIT.failed;
200
+ }
201
+ }
202
+
203
+ /** An https address of a repository, as git names it or as a browser opens it. */
204
+ function isRepositoryAddress(value: string): boolean {
205
+ const address = value.replace(/^git\+/, "");
206
+ if (!/^https:\/\/\S+$/.test(address)) {
207
+ return false;
208
+ }
209
+ try {
210
+ return new URL(address).hostname.length > 0;
211
+ } catch {
212
+ return false;
213
+ }
214
+ }
215
+
216
+ async function check(args: readonly string[], io: PluginCliIo): Promise<number> {
217
+ const [dir, ...extra] = args;
218
+ if (extra.length > 0 || dir?.startsWith("-")) {
219
+ io.stderr("usage: console-plugin check [<dir>]\n");
220
+ return PLUGIN_EXIT.usage;
221
+ }
222
+ const root = resolve(io.cwd, dir ?? ".");
223
+ const report = await checkPlugin(root, (file) => io.loadServer(file));
224
+ io.stdout(`console-plugin check ${root}\n`);
225
+ io.stdout(wordsOf(report.errors, "error"));
226
+ io.stdout(wordsOf(report.warnings, "warning"));
227
+ if (report.ok) {
228
+ io.stdout(
229
+ `ok: ${report.summary ?? "a plugin"}${report.warnings.length > 0 ? `; ${report.warnings.length} warning${report.warnings.length === 1 ? "" : "s"}` : ""}\n`,
230
+ );
231
+ return PLUGIN_EXIT.ok;
232
+ }
233
+ io.stdout(
234
+ `${report.errors.length} problem${report.errors.length === 1 ? "" : "s"}${report.summary === null ? "" : ` in ${report.summary}`}\n`,
235
+ );
236
+ return PLUGIN_EXIT.failed;
237
+ }
@@ -0,0 +1,257 @@
1
+ // `console-plugin init` (docs/specs/plugins.md, "Authoring"): writes a plugin from the template
2
+ // the package ships, the words `__package__`, `__key__`, `__name__` and `__console__` in the
3
+ // template's files and their names replaced; then what the template cannot say in a word, where
4
+ // it was asked for: the fields a published or a repository-bound package carries, and the
5
+ // languages beyond English. `examples/plugin-hello` in the console's repository is what it writes
6
+ // for the key `hello` with nothing else said, and a test holds the two equal.
7
+ import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
8
+ import { join, relative, sep } from "node:path";
9
+
10
+ export interface InitOptions {
11
+ readonly key: string;
12
+ /** The plugin's name for people, in English. */
13
+ readonly name: string;
14
+ /** What the plugin's devDependency on `@skillcdn/console` says: `^<version>` of the command, unless said. */
15
+ readonly consoleDependency: string;
16
+ /** The package's name: `console-plugin-<key>` unless said. A scoped name is published with `publishConfig.access` `public`. */
17
+ readonly packageName?: string | undefined;
18
+ /** The repository the plugin lives in and, in a repository of several, its directory: `repository`, `homepage`, `bugs` and the manifest's `store`. */
19
+ readonly repository?:
20
+ | { readonly url: string; readonly directory?: string | undefined }
21
+ | undefined;
22
+ /** The languages the plugin's words are in, `en` first; `en` alone unless said. Every language map and both manifests' translations get the others, copied from English for the author to translate. */
23
+ readonly languages?: readonly string[] | undefined;
24
+ }
25
+
26
+ /** A language tag as the manifest's language maps are keyed: `en`, `ko`, `pt-BR`. */
27
+ export const LANGUAGE_TAG = /^[a-z]{2,3}(?:-[A-Za-z0-9]+)*$/;
28
+
29
+ /** A package's name as the registry takes one: lowercase, with a scope as `@scope/name`. */
30
+ export const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
31
+
32
+ /** The package's name from the options: what was said, else `console-plugin-<key>`. */
33
+ export function packageNameOf(options: InitOptions): string {
34
+ return options.packageName ?? `console-plugin-${options.key}`;
35
+ }
36
+
37
+ /** The words the template uses, and what each becomes. */
38
+ export function substitutions(options: InitOptions): readonly [string, string][] {
39
+ return [
40
+ ["__package__", packageNameOf(options)],
41
+ ["__key__", options.key],
42
+ ["__name__", options.name],
43
+ ["__console__", options.consoleDependency],
44
+ ];
45
+ }
46
+
47
+ export function substitute(text: string, options: InitOptions): string {
48
+ let result = text;
49
+ for (const [word, value] of substitutions(options)) {
50
+ result = result.replaceAll(word, value);
51
+ }
52
+ return result;
53
+ }
54
+
55
+ /** A name for people from a key: `leave-requests` is "Leave Requests". */
56
+ export function nameFromKey(key: string): string {
57
+ return key
58
+ .split("-")
59
+ .filter((word) => word.length > 0)
60
+ .map((word) => `${word[0]?.toUpperCase() ?? ""}${word.slice(1)}`)
61
+ .join(" ");
62
+ }
63
+
64
+ async function* walk(dir: string): AsyncGenerator<string> {
65
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
66
+ const path = join(dir, entry.name);
67
+ if (entry.isDirectory()) {
68
+ yield* walk(path);
69
+ } else if (entry.isFile()) {
70
+ yield path;
71
+ }
72
+ }
73
+ }
74
+
75
+ /** The files of the template, as paths relative to it with forward slashes. */
76
+ export async function templateFiles(templateDir: string): Promise<string[]> {
77
+ const files: string[] = [];
78
+ for await (const path of walk(templateDir)) {
79
+ files.push(relative(templateDir, path).split(sep).join("/"));
80
+ }
81
+ return files.sort();
82
+ }
83
+
84
+ /**
85
+ * Writes the template into `targetDir`, which must not exist or be an empty directory, and
86
+ * answers the files written, relative to it.
87
+ */
88
+ export async function writePluginFromTemplate(
89
+ templateDir: string,
90
+ targetDir: string,
91
+ options: InitOptions,
92
+ ): Promise<string[]> {
93
+ const found = await stat(targetDir).catch(() => null);
94
+ if (found !== null) {
95
+ if (!found.isDirectory()) {
96
+ throw new Error(`${targetDir} exists and is not a directory`);
97
+ }
98
+ if ((await readdir(targetDir)).length > 0) {
99
+ throw new Error(`${targetDir} is not empty`);
100
+ }
101
+ }
102
+ const written: string[] = [];
103
+ for (const file of await templateFiles(templateDir)) {
104
+ const target = substitute(file, options);
105
+ const text = await readFile(join(templateDir, file), "utf8");
106
+ const path = join(targetDir, ...target.split("/"));
107
+ await mkdir(join(path, ".."), { recursive: true });
108
+ await writeFile(path, substitute(text, options), "utf8");
109
+ written.push(target);
110
+ }
111
+ await adaptPlugin(targetDir, options);
112
+ return written;
113
+ }
114
+
115
+ /** Whether a value of the manifest is a language map: words per language, `en` among them. */
116
+ export function isLanguageMap(value: unknown): value is Record<string, string> {
117
+ return (
118
+ typeof value === "object" &&
119
+ value !== null &&
120
+ !Array.isArray(value) &&
121
+ "en" in value &&
122
+ Object.entries(value).every(
123
+ ([language, words]) => LANGUAGE_TAG.test(language) && typeof words === "string",
124
+ )
125
+ );
126
+ }
127
+
128
+ /** Every language map of the manifest, wherever it is, gets `languages`, each copied from English where it is missing. */
129
+ function addLanguages(value: unknown, languages: readonly string[]): void {
130
+ if (Array.isArray(value)) {
131
+ for (const item of value) {
132
+ addLanguages(item, languages);
133
+ }
134
+ return;
135
+ }
136
+ if (typeof value !== "object" || value === null) {
137
+ return;
138
+ }
139
+ if (isLanguageMap(value)) {
140
+ const english = value.en ?? "";
141
+ for (const language of languages) {
142
+ if (!(language in value)) {
143
+ value[language] = english;
144
+ }
145
+ }
146
+ return;
147
+ }
148
+ for (const inner of Object.values(value)) {
149
+ addLanguages(inner, languages);
150
+ }
151
+ }
152
+
153
+ /** The page of a repository at its host, from the address git names it by. */
154
+ function pageOf(url: string): string {
155
+ return url
156
+ .replace(/^git\+/, "")
157
+ .replace(/\.git$/, "")
158
+ .replace(/\/$/, "");
159
+ }
160
+
161
+ /**
162
+ * The manifest's front-matter with `translations` for `languages`, each the English words
163
+ * again, inserted before the front-matter's end; under `parent` where the format nests them
164
+ * (`skillcdn.translations` in a skill). The description is the one the front-matter has, as
165
+ * written; the title is the plugin's name.
166
+ */
167
+ function withTranslations(
168
+ text: string,
169
+ languages: readonly string[],
170
+ titleKey: "name" | "title",
171
+ title: string,
172
+ parent?: "skillcdn",
173
+ ): string {
174
+ const newline = text.includes("\r\n") ? "\r\n" : "\n";
175
+ const lines = text.split(/\r?\n/);
176
+ const end = lines.indexOf("---", 1);
177
+ if (lines[0] !== "---" || end === -1) {
178
+ return text;
179
+ }
180
+ const described = lines
181
+ .slice(1, end)
182
+ .find((line) => line.startsWith("description:"))
183
+ ?.slice("description:".length)
184
+ .trim();
185
+ const indent = parent === undefined ? "" : " ";
186
+ const block = [
187
+ ...(parent === undefined ? [] : [`${parent}:`]),
188
+ `${indent}translations:`,
189
+ ...languages.flatMap((language) => [
190
+ `${indent} ${language}:`,
191
+ `${indent} ${titleKey}: ${JSON.stringify(title)}`,
192
+ ...(described === undefined ? [] : [`${indent} description: ${described}`]),
193
+ ]),
194
+ ];
195
+ return [...lines.slice(0, end), ...block, ...lines.slice(end)].join(newline);
196
+ }
197
+
198
+ /**
199
+ * What the template cannot say in a word, where it was asked for: `publishConfig` for a scoped
200
+ * name, `repository`, `homepage` and `bugs` with the manifest's `store` for a repository, and
201
+ * the languages beyond English in every language map and both manifests' translations, each
202
+ * copied from the English words for the author to translate (`console-plugin check` warns of
203
+ * one left as it was). Nothing is rewritten when nothing was asked, so that the example stays
204
+ * what the template says.
205
+ */
206
+ async function adaptPlugin(targetDir: string, options: InitOptions): Promise<void> {
207
+ const name = packageNameOf(options);
208
+ const repository = options.repository;
209
+ const languages = (options.languages ?? []).filter((language) => language !== "en");
210
+ if (!name.startsWith("@") && repository === undefined && languages.length === 0) {
211
+ return;
212
+ }
213
+ const packagePath = join(targetDir, "package.json");
214
+ const pkg = JSON.parse(await readFile(packagePath, "utf8")) as Record<string, unknown>;
215
+ const manifestPath = join(targetDir, "console-plugin.json");
216
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8")) as Record<string, unknown>;
217
+ if (name.startsWith("@")) {
218
+ pkg.publishConfig = { access: "public" };
219
+ }
220
+ if (repository !== undefined) {
221
+ const page = pageOf(repository.url);
222
+ // The paths a git host has for a directory and for issues are the host's; only the one
223
+ // whose paths are known gets them.
224
+ const atGitHub = new URL(page).hostname === "github.com";
225
+ const homepage =
226
+ repository.directory !== undefined && atGitHub
227
+ ? `${page}/tree/HEAD/${repository.directory}`
228
+ : page;
229
+ pkg.repository = {
230
+ type: "git",
231
+ url: repository.url,
232
+ ...(repository.directory === undefined ? {} : { directory: repository.directory }),
233
+ };
234
+ pkg.homepage = homepage;
235
+ if (atGitHub) {
236
+ pkg.bugs = { url: `${page}/issues` };
237
+ }
238
+ manifest.store = { homepage, repository: page };
239
+ }
240
+ if (languages.length > 0) {
241
+ addLanguages(manifest, languages);
242
+ const skillcdn = join(targetDir, "SKILLCDN.md");
243
+ await writeFile(
244
+ skillcdn,
245
+ withTranslations(await readFile(skillcdn, "utf8"), languages, "name", options.name),
246
+ "utf8",
247
+ );
248
+ const skill = join(targetDir, "skills", options.key, "SKILL.md");
249
+ await writeFile(
250
+ skill,
251
+ withTranslations(await readFile(skill, "utf8"), languages, "title", options.name, "skillcdn"),
252
+ "utf8",
253
+ );
254
+ }
255
+ await writeFile(packagePath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8");
256
+ await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
257
+ }
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ import { join } from "node:path";
4
+ import process from "node:process";
5
+ import { fileURLToPath } from "node:url";
6
+ import * as z from "zod/mini";
7
+ import { loadServerModule } from "../plugin/load.js";
8
+ import { runPluginCli } from "./plugin-cli.js";
9
+
10
+ // The `console-plugin` command, the package's second bin: the only place it reads the process.
11
+ // Everything it does is in plugin-cli.ts, which takes what it needs and is what the tests run.
12
+
13
+ /** The package's own directory, two levels above dist/cli: where the template is. */
14
+ const packageDir = fileURLToPath(new URL("../../", import.meta.url));
15
+ const manifest = z
16
+ .object({ version: z.string() })
17
+ .safeParse(createRequire(import.meta.url)("../../package.json"));
18
+
19
+ process.exitCode = await runPluginCli(process.argv.slice(2), {
20
+ stdout: (text) => {
21
+ process.stdout.write(text);
22
+ },
23
+ stderr: (text) => {
24
+ process.stderr.write(text);
25
+ },
26
+ cwd: process.cwd(),
27
+ templateDir: join(packageDir, "template", "plugin"),
28
+ version: manifest.success ? manifest.data.version : undefined,
29
+ loadServer: (file) => loadServerModule(file),
30
+ });