@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.
- package/README.md +20 -2
- package/SKILLCDN.md +7 -5
- package/dist/api.d.ts +5 -4
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +5 -4
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +579 -45
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +34 -3
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +277 -6
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +60 -7
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/plugin-check.d.ts +21 -0
- package/dist/cli/plugin-check.d.ts.map +1 -0
- package/dist/cli/plugin-check.js +374 -0
- package/dist/cli/plugin-check.js.map +1 -0
- package/dist/cli/plugin-cli.d.ts +20 -0
- package/dist/cli/plugin-cli.d.ts.map +1 -0
- package/dist/cli/plugin-cli.js +123 -0
- package/dist/cli/plugin-cli.js.map +1 -0
- package/dist/cli/plugin-init.d.ts +20 -0
- package/dist/cli/plugin-init.d.ts.map +1 -0
- package/dist/cli/plugin-init.js +74 -0
- package/dist/cli/plugin-init.js.map +1 -0
- package/dist/cli/plugin-main.d.ts +3 -0
- package/dist/cli/plugin-main.d.ts.map +1 -0
- package/dist/cli/plugin-main.js +28 -0
- package/dist/cli/plugin-main.js.map +1 -0
- package/dist/client.d.ts +109 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +67 -2
- package/dist/client.js.map +1 -1
- package/dist/compat.d.ts +1 -1
- package/dist/compat.js +2 -2
- package/dist/components/board.d.ts +8 -0
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +59 -15
- package/dist/components/board.js.map +1 -1
- package/dist/components/event-feed.d.ts +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +33 -1
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/markdown.d.ts.map +1 -1
- package/dist/components/markdown.js +6 -3
- package/dist/components/markdown.js.map +1 -1
- package/dist/components/pack-connect.d.ts +3 -1
- package/dist/components/pack-connect.d.ts.map +1 -1
- package/dist/components/pack-connect.js +4 -4
- package/dist/components/pack-connect.js.map +1 -1
- package/dist/components/pack-token.d.ts +3 -1
- package/dist/components/pack-token.d.ts.map +1 -1
- package/dist/components/pack-token.js +1 -1
- package/dist/components/pack-token.js.map +1 -1
- package/dist/components/people.d.ts.map +1 -1
- package/dist/components/people.js +14 -3
- package/dist/components/people.js.map +1 -1
- package/dist/components/plugin-frame.d.ts +52 -0
- package/dist/components/plugin-frame.d.ts.map +1 -0
- package/dist/components/plugin-frame.js +314 -0
- package/dist/components/plugin-frame.js.map +1 -0
- package/dist/components/plugin-settings.d.ts +38 -0
- package/dist/components/plugin-settings.d.ts.map +1 -0
- package/dist/components/plugin-settings.js +348 -0
- package/dist/components/plugin-settings.js.map +1 -0
- package/dist/components/remote-pictures.d.ts +6 -0
- package/dist/components/remote-pictures.d.ts.map +1 -0
- package/dist/components/remote-pictures.js +8 -0
- package/dist/components/remote-pictures.js.map +1 -0
- package/dist/components/shell.d.ts +2 -0
- package/dist/components/shell.d.ts.map +1 -1
- package/dist/components/shell.js +26 -1
- package/dist/components/shell.js.map +1 -1
- package/dist/components/sign-in.d.ts.map +1 -1
- package/dist/components/sign-in.js +49 -2
- package/dist/components/sign-in.js.map +1 -1
- package/dist/components/ui.d.ts +3 -2
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +6 -3
- package/dist/components/ui.js.map +1 -1
- package/dist/components/workspace.d.ts +7 -1
- package/dist/components/workspace.d.ts.map +1 -1
- package/dist/components/workspace.js +1 -1
- package/dist/components/workspace.js.map +1 -1
- package/dist/console.css +384 -5
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +146 -29
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +11 -2
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +35 -21
- package/dist/data.js.map +1 -1
- package/dist/i18n/en.d.ts +163 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +174 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +154 -1
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +14 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +14 -0
- package/dist/limits.js.map +1 -1
- package/dist/plugin/args.d.ts +148 -0
- package/dist/plugin/args.d.ts.map +1 -0
- package/dist/plugin/args.js +369 -0
- package/dist/plugin/args.js.map +1 -0
- package/dist/plugin/bridge.d.ts +146 -0
- package/dist/plugin/bridge.d.ts.map +1 -0
- package/dist/plugin/bridge.js +125 -0
- package/dist/plugin/bridge.js.map +1 -0
- package/dist/plugin/capabilities.d.ts +20 -0
- package/dist/plugin/capabilities.d.ts.map +1 -0
- package/dist/plugin/capabilities.js +133 -0
- package/dist/plugin/capabilities.js.map +1 -0
- package/dist/plugin/context.d.ts +374 -0
- package/dist/plugin/context.d.ts.map +1 -0
- package/dist/plugin/context.js +2 -0
- package/dist/plugin/context.js.map +1 -0
- package/dist/plugin/define.d.ts +208 -0
- package/dist/plugin/define.d.ts.map +1 -0
- package/dist/plugin/define.js +482 -0
- package/dist/plugin/define.js.map +1 -0
- package/dist/plugin/definition-error.d.ts +11 -0
- package/dist/plugin/definition-error.d.ts.map +1 -0
- package/dist/plugin/definition-error.js +17 -0
- package/dist/plugin/definition-error.js.map +1 -0
- package/dist/plugin/limits.d.ts +67 -0
- package/dist/plugin/limits.d.ts.map +1 -0
- package/dist/plugin/limits.js +70 -0
- package/dist/plugin/limits.js.map +1 -0
- package/dist/plugin/load-worker.d.ts +7 -0
- package/dist/plugin/load-worker.d.ts.map +1 -0
- package/dist/plugin/load-worker.js +41 -0
- package/dist/plugin/load-worker.js.map +1 -0
- package/dist/plugin/load.d.ts +18 -0
- package/dist/plugin/load.d.ts.map +1 -0
- package/dist/plugin/load.js +62 -0
- package/dist/plugin/load.js.map +1 -0
- package/dist/plugin/manifest.d.ts +237 -0
- package/dist/plugin/manifest.d.ts.map +1 -0
- package/dist/plugin/manifest.js +411 -0
- package/dist/plugin/manifest.js.map +1 -0
- package/dist/plugin/refusals.d.ts +49 -0
- package/dist/plugin/refusals.d.ts.map +1 -0
- package/dist/plugin/refusals.js +79 -0
- package/dist/plugin/refusals.js.map +1 -0
- package/dist/plugin/rest-schemas.d.ts +556 -0
- package/dist/plugin/rest-schemas.d.ts.map +1 -0
- package/dist/plugin/rest-schemas.js +262 -0
- package/dist/plugin/rest-schemas.js.map +1 -0
- package/dist/plugin/sandbox.d.ts +23 -0
- package/dist/plugin/sandbox.d.ts.map +1 -0
- package/dist/plugin/sandbox.js +99 -0
- package/dist/plugin/sandbox.js.map +1 -0
- package/dist/plugin/testing.d.ts +45 -0
- package/dist/plugin/testing.d.ts.map +1 -0
- package/dist/plugin/testing.js +420 -0
- package/dist/plugin/testing.js.map +1 -0
- package/dist/plugin/web-react.d.ts +11 -0
- package/dist/plugin/web-react.d.ts.map +1 -0
- package/dist/plugin/web-react.js +34 -0
- package/dist/plugin/web-react.js.map +1 -0
- package/dist/plugin/web.d.ts +95 -0
- package/dist/plugin/web.d.ts.map +1 -0
- package/dist/plugin/web.js +245 -0
- package/dist/plugin/web.js.map +1 -0
- package/dist/plugin-nav.d.ts +29 -0
- package/dist/plugin-nav.d.ts.map +1 -0
- package/dist/plugin-nav.js +92 -0
- package/dist/plugin-nav.js.map +1 -0
- package/dist/plugin-testing.d.ts +2 -0
- package/dist/plugin-testing.d.ts.map +1 -0
- package/dist/plugin-testing.js +4 -0
- package/dist/plugin-testing.js.map +1 -0
- package/dist/plugin-web-react.d.ts +3 -0
- package/dist/plugin-web-react.d.ts.map +1 -0
- package/dist/plugin-web-react.js +3 -0
- package/dist/plugin-web-react.js.map +1 -0
- package/dist/plugin-web.d.ts +4 -0
- package/dist/plugin-web.d.ts.map +1 -0
- package/dist/plugin-web.js +5 -0
- package/dist/plugin-web.js.map +1 -0
- package/dist/plugin.d.ts +11 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +15 -0
- package/dist/plugin.js.map +1 -0
- package/dist/router.d.ts +28 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +47 -0
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +6 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +8 -0
- package/dist/routes.js.map +1 -1
- package/dist/schemas.d.ts +147 -1
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +55 -5
- package/dist/schemas.js.map +1 -1
- package/dist/tokens.d.ts +2 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +101 -0
- package/dist/tokens.js.map +1 -0
- package/dist/vocabulary.d.ts +11 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +24 -0
- package/dist/vocabulary.js.map +1 -1
- package/dist/web.d.ts.map +1 -1
- package/dist/web.js +5 -2
- package/dist/web.js.map +1 -1
- package/docs/specs/cli.md +16 -9
- package/docs/specs/plugins.md +487 -0
- package/docs/specs/rest.md +25 -13
- package/package.json +21 -3
- package/skills/building-a-console/SKILL.md +25 -26
- package/skills/building-a-console/references/configuring.md +40 -0
- package/skills/building-a-console/references/the-default-console.md +1 -1
- package/skills/building-a-console/references/the-look-from-a-website.md +5 -5
- package/skills/building-a-plugin/SKILL.md +137 -0
- package/skills/deploying-a-console/SKILL.md +24 -26
- package/skills/deploying-a-console/references/access.md +13 -9
- package/skills/deploying-a-console/references/resources.md +25 -10
- package/skills/deploying-a-console/references/running-it-again.md +15 -0
- package/skills/deploying-a-console/references/sizing.md +2 -0
- package/skills/planning-the-board/SKILL.md +96 -0
- package/skills/planning-the-board/references/a-task.md +36 -0
- package/skills/planning-the-board/references/setting-up.md +44 -0
- package/skills/working-the-board/SKILL.md +43 -34
- package/skills/working-the-board/references/the-pack.md +19 -0
- package/src/api.ts +16 -0
- package/src/cli/cli.ts +736 -45
- package/src/cli/format.ts +356 -9
- package/src/cli/help.ts +71 -6
- package/src/cli/plugin-check.ts +470 -0
- package/src/cli/plugin-cli.ts +148 -0
- package/src/cli/plugin-init.ts +90 -0
- package/src/cli/plugin-main.ts +30 -0
- package/src/client.ts +324 -1
- package/src/compat.ts +2 -2
- package/src/components/board.tsx +62 -14
- package/src/components/event-feed.tsx +42 -1
- package/src/components/markdown.tsx +6 -3
- package/src/components/pack-connect.tsx +10 -7
- package/src/components/pack-token.tsx +4 -1
- package/src/components/people.tsx +17 -7
- package/src/components/plugin-frame.tsx +437 -0
- package/src/components/plugin-settings.tsx +1111 -0
- package/src/components/remote-pictures.ts +19 -0
- package/src/components/shell.tsx +82 -14
- package/src/components/sign-in.tsx +114 -13
- package/src/components/ui.tsx +6 -3
- package/src/components/workspace.tsx +18 -0
- package/src/console.tsx +309 -23
- package/src/data.ts +47 -22
- package/src/i18n/en.ts +183 -1
- package/src/i18n/ko.ts +159 -1
- package/src/index.ts +12 -0
- package/src/limits.ts +14 -0
- package/src/plugin/args.ts +526 -0
- package/src/plugin/bridge.ts +314 -0
- package/src/plugin/capabilities.ts +150 -0
- package/src/plugin/context.ts +416 -0
- package/src/plugin/define.ts +792 -0
- package/src/plugin/definition-error.ts +19 -0
- package/src/plugin/limits.ts +70 -0
- package/src/plugin/load-worker.ts +50 -0
- package/src/plugin/load.ts +82 -0
- package/src/plugin/manifest.ts +561 -0
- package/src/plugin/refusals.ts +94 -0
- package/src/plugin/rest-schemas.ts +354 -0
- package/src/plugin/sandbox.ts +123 -0
- package/src/plugin/testing.ts +612 -0
- package/src/plugin/web-react.ts +45 -0
- package/src/plugin/web.ts +403 -0
- package/src/plugin-nav.ts +132 -0
- package/src/plugin-testing.ts +10 -0
- package/src/plugin-web-react.ts +4 -0
- package/src/plugin-web.ts +35 -0
- package/src/plugin.ts +126 -0
- package/src/router.ts +73 -0
- package/src/routes.ts +9 -0
- package/src/schemas.ts +60 -4
- package/src/styles/console.css +384 -5
- package/src/tokens.ts +101 -0
- package/src/vocabulary.ts +30 -0
- package/src/web.ts +5 -2
- package/template/plugin/.gitignore +2 -0
- package/template/plugin/README.md +26 -0
- package/template/plugin/SKILLCDN.md +13 -0
- package/template/plugin/build.mjs +40 -0
- package/template/plugin/console-plugin.json +50 -0
- package/template/plugin/package.json +27 -0
- package/template/plugin/skills/__key__/SKILL.md +23 -0
- package/template/plugin/src/check.test.ts +34 -0
- package/template/plugin/src/server/index.test.ts +41 -0
- package/template/plugin/src/server/index.ts +72 -0
- package/template/plugin/src/web/index.css +51 -0
- package/template/plugin/src/web/index.ts +115 -0
- package/template/plugin/tsconfig.json +17 -0
- package/skills/working-the-board/references/setting-up.md +0 -3
- /package/skills/{working-the-board → planning-the-board}/references/project-rules.md +0 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
// `console-plugin check` (docs/specs/plugins.md, "Authoring"): reads a plugin's package as the
|
|
2
|
+
// console would and names everything a console, or a store, would refuse it for: the manifest,
|
|
3
|
+
// the package's own manifest, the server module under the host's hardening and against the
|
|
4
|
+
// manifest, the web bundle's self-containment and its classes, the skills, the migrations.
|
|
5
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
6
|
+
import { join, posix } from "node:path";
|
|
7
|
+
import { descriptionProblems, type PluginDescription } from "../plugin/define.js";
|
|
8
|
+
import type { ServerLoad } from "../plugin/load.js";
|
|
9
|
+
import {
|
|
10
|
+
CURRENT_PLUGIN_API,
|
|
11
|
+
PLUGIN_API_MAJORS,
|
|
12
|
+
type PluginManifest,
|
|
13
|
+
parsePluginManifest,
|
|
14
|
+
} from "../plugin/manifest.js";
|
|
15
|
+
import { forbiddenImportsIn, removedGlobalsIn } from "../plugin/sandbox.js";
|
|
16
|
+
|
|
17
|
+
export interface CheckReport {
|
|
18
|
+
readonly ok: boolean;
|
|
19
|
+
readonly errors: readonly string[];
|
|
20
|
+
readonly warnings: readonly string[];
|
|
21
|
+
/** One line about what was found, when the package read at all. */
|
|
22
|
+
readonly summary: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** SPDX identifiers under which a plugin may ship into an image: permissive only. */
|
|
26
|
+
export const PERMISSIVE_LICENSES: readonly string[] = [
|
|
27
|
+
"MIT",
|
|
28
|
+
"MIT-0",
|
|
29
|
+
"ISC",
|
|
30
|
+
"0BSD",
|
|
31
|
+
"BSD-2-Clause",
|
|
32
|
+
"BSD-3-Clause",
|
|
33
|
+
"Apache-2.0",
|
|
34
|
+
"BlueOak-1.0.0",
|
|
35
|
+
"CC0-1.0",
|
|
36
|
+
"Unlicense",
|
|
37
|
+
"Zlib",
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
/** `(MIT OR Apache-2.0)` is fine when one side is; `MIT AND X` only when both are. */
|
|
41
|
+
export function isPermissiveLicense(expression: string): boolean {
|
|
42
|
+
const trimmed = expression.trim().replace(/^\(|\)$/g, "");
|
|
43
|
+
if (/\bOR\b/i.test(trimmed)) {
|
|
44
|
+
return trimmed.split(/\s+OR\s+/i).some(isPermissiveLicense);
|
|
45
|
+
}
|
|
46
|
+
if (/\bAND\b/i.test(trimmed)) {
|
|
47
|
+
return trimmed.split(/\s+AND\s+/i).every(isPermissiveLicense);
|
|
48
|
+
}
|
|
49
|
+
return PERMISSIVE_LICENSES.includes(trimmed);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** True when npm's `files` list ships the path: named, under a named directory, or matched by a simple glob. */
|
|
53
|
+
export function coveredByFiles(files: readonly string[], path: string): boolean {
|
|
54
|
+
const target = posix.normalize(path).replace(/^\.\//, "");
|
|
55
|
+
return files.some((entry) => {
|
|
56
|
+
const pattern = posix.normalize(entry).replace(/^\.\//, "").replace(/\/$/, "");
|
|
57
|
+
if (pattern.startsWith("!")) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (pattern === target || target.startsWith(`${pattern}/`)) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
if (!/[*?]/.test(pattern)) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const regex = new RegExp(
|
|
67
|
+
`^${pattern
|
|
68
|
+
.split("**")
|
|
69
|
+
.map((part) =>
|
|
70
|
+
part
|
|
71
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
72
|
+
.replace(/\*/g, "[^/]*")
|
|
73
|
+
.replace(/\?/g, "[^/]"),
|
|
74
|
+
)
|
|
75
|
+
.join(".*")}(?:/.*)?$`,
|
|
76
|
+
);
|
|
77
|
+
return regex.test(target);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A class selector: a dot not after a digit or another dot (0.5em is a number), then a name. */
|
|
82
|
+
const CLASS_SELECTOR = /(?<![\d.])\.(-?[A-Za-z_][\w-]*)/g;
|
|
83
|
+
|
|
84
|
+
/** The classes a stylesheet styles, each once; comments and quoted strings are not read. */
|
|
85
|
+
export function classesIn(css: string): string[] {
|
|
86
|
+
const withoutComments = css.replace(/\/\*[\s\S]*?\*\//g, "").replace(/"[^"]*"|'[^']*'/g, '""');
|
|
87
|
+
const found = new Set<string>();
|
|
88
|
+
for (const match of withoutComments.matchAll(CLASS_SELECTOR)) {
|
|
89
|
+
if (match[1] !== undefined && !/^\d/.test(match[1])) {
|
|
90
|
+
found.add(match[1]);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return [...found];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const WEB_IMPORT = /(?:\bfrom\s*|\bimport\s*\(?\s*|\bexport\s+\*\s+from\s*)["'`]([^"'`]+)["'`]/g;
|
|
97
|
+
const WEB_FORBIDDEN =
|
|
98
|
+
/\b(fetch|XMLHttpRequest|WebSocket|EventSource|importScripts|sendBeacon)\s*\(|document\.cookie|\bnavigator\.serviceWorker\b/g;
|
|
99
|
+
|
|
100
|
+
/** What a web bundle reaches for that a frame has no business with, or imports from elsewhere. */
|
|
101
|
+
export function webBundleProblems(source: string): string[] {
|
|
102
|
+
const problems = new Set<string>();
|
|
103
|
+
for (const match of source.matchAll(WEB_IMPORT)) {
|
|
104
|
+
const specifier = match[1] ?? "";
|
|
105
|
+
if (
|
|
106
|
+
!specifier.startsWith("./") &&
|
|
107
|
+
!specifier.startsWith("../") &&
|
|
108
|
+
!specifier.startsWith("data:")
|
|
109
|
+
) {
|
|
110
|
+
problems.add(`imports ${specifier} from elsewhere: a bundle is self-contained`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
for (const match of source.matchAll(WEB_FORBIDDEN)) {
|
|
114
|
+
problems.add(
|
|
115
|
+
`uses ${match[1] ?? match[0]}: a frame speaks to the console through the bridge alone`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return [...problems];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function exists(path: string): Promise<"file" | "directory" | null> {
|
|
122
|
+
try {
|
|
123
|
+
const found = await stat(path);
|
|
124
|
+
return found.isDirectory() ? "directory" : "file";
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function readJson(path: string): Promise<{ value: unknown } | { problem: string }> {
|
|
131
|
+
let text: string;
|
|
132
|
+
try {
|
|
133
|
+
text = await readFile(path, "utf8");
|
|
134
|
+
} catch {
|
|
135
|
+
return { problem: `${posix.basename(path)} is missing` };
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
return { value: JSON.parse(text) as unknown };
|
|
139
|
+
} catch (error) {
|
|
140
|
+
return {
|
|
141
|
+
problem: `${posix.basename(path)} is not JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
interface FrontMatter {
|
|
147
|
+
readonly scalars: Readonly<Record<string, string>>;
|
|
148
|
+
readonly body: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function frontMatter(text: string): FrontMatter | null {
|
|
152
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/.exec(text);
|
|
153
|
+
if (match === null) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const scalars: Record<string, string> = {};
|
|
157
|
+
for (const line of (match[1] ?? "").split(/\r?\n/)) {
|
|
158
|
+
const key = /^([a-z][a-zA-Z]*):(?: (.*))?$/.exec(line);
|
|
159
|
+
if (key?.[1] !== undefined && key[2] !== undefined) {
|
|
160
|
+
scalars[key[1]] = key[2].replace(/^"(.*)"$/, "$1");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return { scalars, body: match[2] ?? "" };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function checkSkills(
|
|
167
|
+
root: string,
|
|
168
|
+
skillsDir: string,
|
|
169
|
+
errors: string[],
|
|
170
|
+
warnings: string[],
|
|
171
|
+
): Promise<number> {
|
|
172
|
+
const manifestPath = join(root, "SKILLCDN.md");
|
|
173
|
+
const manifestText = await readFile(manifestPath, "utf8").catch(() => null);
|
|
174
|
+
if (manifestText === null) {
|
|
175
|
+
errors.push(
|
|
176
|
+
"SKILLCDN.md is missing at the package root: a plugin with skills has their manifest there",
|
|
177
|
+
);
|
|
178
|
+
} else {
|
|
179
|
+
const matter = frontMatter(manifestText);
|
|
180
|
+
if (matter === null || !matter.scalars.name || !matter.scalars.description) {
|
|
181
|
+
errors.push("SKILLCDN.md has no front-matter with name and description");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const dir = join(root, skillsDir);
|
|
185
|
+
if ((await exists(dir)) !== "directory") {
|
|
186
|
+
errors.push(`skills names ${skillsDir}, which is not a directory`);
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
let count = 0;
|
|
190
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
191
|
+
if (!entry.isDirectory()) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const file = posix.join(skillsDir, entry.name, "SKILL.md");
|
|
195
|
+
const text = await readFile(join(root, file), "utf8").catch(() => null);
|
|
196
|
+
if (text === null) {
|
|
197
|
+
errors.push(`${file} is missing: a skill directory holds a SKILL.md`);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
count += 1;
|
|
201
|
+
const matter = frontMatter(text);
|
|
202
|
+
if (matter === null) {
|
|
203
|
+
errors.push(`${file} has no front-matter`);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (matter.scalars.name !== entry.name) {
|
|
207
|
+
errors.push(
|
|
208
|
+
`${file} is named ${matter.scalars.name ?? "(nothing)"}, not ${entry.name} as its directory is`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
const description = matter.scalars.description ?? "";
|
|
212
|
+
if (description.length === 0 || description.length > 1024) {
|
|
213
|
+
errors.push(
|
|
214
|
+
`${file} has a description of ${description.length} characters; 1 to 1024 are allowed`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
if (!/^\s*# /.test(matter.body)) {
|
|
218
|
+
errors.push(`${file} does not start its body with a level-one heading`);
|
|
219
|
+
}
|
|
220
|
+
if (matter.scalars.license === undefined) {
|
|
221
|
+
warnings.push(`${file} names no license`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (count === 0) {
|
|
225
|
+
warnings.push(`${skillsDir} holds no skill`);
|
|
226
|
+
}
|
|
227
|
+
return count;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function checkMigrations(
|
|
231
|
+
root: string,
|
|
232
|
+
migrationsDir: string,
|
|
233
|
+
errors: string[],
|
|
234
|
+
): Promise<void> {
|
|
235
|
+
const dir = join(root, migrationsDir);
|
|
236
|
+
if ((await exists(dir)) !== "directory") {
|
|
237
|
+
errors.push(`migrations names ${migrationsDir}, which is not a directory`);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const names = (await readdir(dir)).filter((name) => name.endsWith(".sql")).sort();
|
|
241
|
+
if (names.length === 0) {
|
|
242
|
+
errors.push(`${migrationsDir} holds no .sql file`);
|
|
243
|
+
}
|
|
244
|
+
for (const name of names) {
|
|
245
|
+
if (!/^\d{4}-[a-z0-9-]+\.sql$/.test(name)) {
|
|
246
|
+
errors.push(`${migrationsDir}/${name} is not named NNNN-<words>.sql`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function checkWeb(
|
|
252
|
+
root: string,
|
|
253
|
+
manifest: PluginManifest,
|
|
254
|
+
webDir: string,
|
|
255
|
+
errors: string[],
|
|
256
|
+
warnings: string[],
|
|
257
|
+
): Promise<void> {
|
|
258
|
+
const dir = join(root, webDir);
|
|
259
|
+
if ((await exists(dir)) !== "directory") {
|
|
260
|
+
errors.push(`web names ${webDir}, which is not a directory: build the bundle first`);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const script = await readFile(join(dir, "index.js"), "utf8").catch(() => null);
|
|
264
|
+
if (script === null) {
|
|
265
|
+
errors.push(`${webDir}/index.js is missing: the frame loads the bundle from there`);
|
|
266
|
+
} else {
|
|
267
|
+
for (const problem of webBundleProblems(script)) {
|
|
268
|
+
errors.push(`${webDir}/index.js ${problem}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const css = await readFile(join(dir, "index.css"), "utf8").catch(() => null);
|
|
272
|
+
if (css !== null) {
|
|
273
|
+
if (
|
|
274
|
+
/@import\s+(?:url\()?["']?(?:https?:)?\/\//.test(css) ||
|
|
275
|
+
/url\(\s*["']?(?:https?:)?\/\//.test(css)
|
|
276
|
+
) {
|
|
277
|
+
errors.push(`${webDir}/index.css loads from elsewhere: a bundle is self-contained`);
|
|
278
|
+
}
|
|
279
|
+
const prefix = `pl-${manifest.key}-`;
|
|
280
|
+
const strangers = classesIn(css).filter(
|
|
281
|
+
(name) => !name.startsWith(prefix) && !name.startsWith("sc-"),
|
|
282
|
+
);
|
|
283
|
+
if (strangers.length > 0) {
|
|
284
|
+
warnings.push(
|
|
285
|
+
`${webDir}/index.css styles ${strangers.slice(0, 5).join(", ")}${strangers.length > 5 ? ", ..." : ""}: a plugin's classes begin with ${prefix}`,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function describeCounts(manifest: PluginManifest, description: PluginDescription | null): string {
|
|
292
|
+
const counts = [
|
|
293
|
+
`${manifest.pages.length} pages`,
|
|
294
|
+
`${manifest.widgets.length} widgets`,
|
|
295
|
+
`${manifest.settings.length} settings`,
|
|
296
|
+
`${Object.keys(manifest.roles).length} roles`,
|
|
297
|
+
];
|
|
298
|
+
if (description !== null) {
|
|
299
|
+
counts.unshift(
|
|
300
|
+
`${description.collections.length} collections`,
|
|
301
|
+
`${Object.keys(description.commands).length} commands`,
|
|
302
|
+
`${Object.keys(description.queries).length} queries`,
|
|
303
|
+
`${Object.keys(description.endpoints).length} endpoints`,
|
|
304
|
+
`${description.hooks.length} hooks`,
|
|
305
|
+
`${Object.keys(description.jobs).length} jobs`,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
return counts.join(", ");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Reads the plugin at `root` as a console would, and says what it would refuse. */
|
|
312
|
+
export async function checkPlugin(
|
|
313
|
+
root: string,
|
|
314
|
+
loadServer: (file: string) => Promise<ServerLoad>,
|
|
315
|
+
): Promise<CheckReport> {
|
|
316
|
+
const errors: string[] = [];
|
|
317
|
+
const warnings: string[] = [];
|
|
318
|
+
if ((await exists(root)) !== "directory") {
|
|
319
|
+
return { ok: false, errors: [`${root} is not a directory`], warnings, summary: null };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const manifestRead = await readJson(join(root, "console-plugin.json"));
|
|
323
|
+
if ("problem" in manifestRead) {
|
|
324
|
+
return { ok: false, errors: [manifestRead.problem], warnings, summary: null };
|
|
325
|
+
}
|
|
326
|
+
const parsed = parsePluginManifest(manifestRead.value);
|
|
327
|
+
if (!parsed.ok) {
|
|
328
|
+
return {
|
|
329
|
+
ok: false,
|
|
330
|
+
errors: parsed.problems.map((problem) => `console-plugin.json: ${problem}`),
|
|
331
|
+
warnings,
|
|
332
|
+
summary: null,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
const manifest = parsed.manifest;
|
|
336
|
+
if (!(PLUGIN_API_MAJORS as readonly number[]).includes(manifest.pluginApi)) {
|
|
337
|
+
errors.push(
|
|
338
|
+
`console-plugin.json: pluginApi ${manifest.pluginApi} is not one this command knows (${PLUGIN_API_MAJORS.join(", ")}); write against ${CURRENT_PLUGIN_API}`,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const packageRead = await readJson(join(root, "package.json"));
|
|
343
|
+
let version = "0.0.0";
|
|
344
|
+
if ("problem" in packageRead) {
|
|
345
|
+
errors.push(packageRead.problem);
|
|
346
|
+
} else {
|
|
347
|
+
const pkg = (
|
|
348
|
+
typeof packageRead.value === "object" && packageRead.value !== null ? packageRead.value : {}
|
|
349
|
+
) as Record<string, unknown>;
|
|
350
|
+
if (typeof pkg.name !== "string" || pkg.name.length === 0) {
|
|
351
|
+
errors.push("package.json: name is missing");
|
|
352
|
+
}
|
|
353
|
+
if (
|
|
354
|
+
typeof pkg.version !== "string" ||
|
|
355
|
+
!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(pkg.version)
|
|
356
|
+
) {
|
|
357
|
+
errors.push("package.json: version is not a semver version");
|
|
358
|
+
} else {
|
|
359
|
+
version = pkg.version;
|
|
360
|
+
}
|
|
361
|
+
if (typeof pkg.license !== "string" || !isPermissiveLicense(pkg.license)) {
|
|
362
|
+
errors.push(
|
|
363
|
+
`package.json: license ${typeof pkg.license === "string" ? pkg.license : "(none)"} is not a permissive license (${PERMISSIVE_LICENSES.join(", ")})`,
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
if (pkg.type !== "module") {
|
|
367
|
+
errors.push('package.json: type is not "module"; the server module is ESM');
|
|
368
|
+
}
|
|
369
|
+
const scripts = (
|
|
370
|
+
typeof pkg.scripts === "object" && pkg.scripts !== null ? pkg.scripts : {}
|
|
371
|
+
) as Record<string, unknown>;
|
|
372
|
+
for (const hook of ["preinstall", "install", "postinstall"]) {
|
|
373
|
+
if (hook in scripts) {
|
|
374
|
+
errors.push(
|
|
375
|
+
`package.json: has a ${hook} script; a plugin is installed with no install scripts`,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (Array.isArray(pkg.files)) {
|
|
380
|
+
const files = pkg.files.filter((entry): entry is string => typeof entry === "string");
|
|
381
|
+
const shipped = [
|
|
382
|
+
"console-plugin.json",
|
|
383
|
+
manifest.server,
|
|
384
|
+
...(manifest.web === undefined ? [] : [manifest.web]),
|
|
385
|
+
...(manifest.skills === undefined ? [] : [manifest.skills, "SKILLCDN.md"]),
|
|
386
|
+
...(manifest.migrations === undefined ? [] : [manifest.migrations]),
|
|
387
|
+
];
|
|
388
|
+
for (const path of shipped) {
|
|
389
|
+
if (!coveredByFiles(files, path)) {
|
|
390
|
+
errors.push(`package.json: files leaves out ${path}, which the console reads`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
} else {
|
|
394
|
+
warnings.push("package.json: has no files list; everything in the directory is published");
|
|
395
|
+
}
|
|
396
|
+
if (typeof pkg.private === "boolean" && pkg.private) {
|
|
397
|
+
warnings.push("package.json: is private; take that out to publish");
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
let description: PluginDescription | null = null;
|
|
402
|
+
const serverPath = join(root, manifest.server);
|
|
403
|
+
if ((await exists(serverPath)) !== "file") {
|
|
404
|
+
errors.push(`server names ${manifest.server}, which is missing: build the server module first`);
|
|
405
|
+
} else {
|
|
406
|
+
const source = await readFile(serverPath, "utf8");
|
|
407
|
+
for (const specifier of forbiddenImportsIn(source)) {
|
|
408
|
+
errors.push(`${manifest.server} imports ${specifier}, which the host refuses`);
|
|
409
|
+
}
|
|
410
|
+
for (const name of removedGlobalsIn(source)) {
|
|
411
|
+
errors.push(
|
|
412
|
+
`${manifest.server} calls ${name}, which the host removes; the console is reached through ctx`,
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
const load = await loadServer(serverPath);
|
|
416
|
+
if (!load.ok) {
|
|
417
|
+
errors.push(`${manifest.server} does not load under the host's hardening: ${load.error}`);
|
|
418
|
+
for (const problem of load.problems) {
|
|
419
|
+
errors.push(`${manifest.server}: ${problem}`);
|
|
420
|
+
}
|
|
421
|
+
} else {
|
|
422
|
+
description = load.description;
|
|
423
|
+
if (description.pluginApi !== manifest.pluginApi) {
|
|
424
|
+
errors.push(
|
|
425
|
+
`${manifest.server} was built against plugin API ${description.pluginApi}; the manifest says ${manifest.pluginApi}`,
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
for (const problem of descriptionProblems(description, manifest)) {
|
|
429
|
+
errors.push(`${manifest.server}: ${problem}`);
|
|
430
|
+
}
|
|
431
|
+
if (
|
|
432
|
+
manifest.capabilities.includes("endpoints") &&
|
|
433
|
+
Object.keys(description.endpoints).length === 0
|
|
434
|
+
) {
|
|
435
|
+
warnings.push(
|
|
436
|
+
"console-plugin.json: asks the capability endpoints and the server module defines no endpoint",
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
if (manifest.capabilities.includes("jobs") && Object.keys(description.jobs).length === 0) {
|
|
440
|
+
warnings.push(
|
|
441
|
+
"console-plugin.json: asks the capability jobs and the server module defines no job",
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (manifest.web !== undefined) {
|
|
448
|
+
await checkWeb(root, manifest, manifest.web, errors, warnings);
|
|
449
|
+
}
|
|
450
|
+
let skills = 0;
|
|
451
|
+
if (manifest.skills !== undefined) {
|
|
452
|
+
skills = await checkSkills(root, manifest.skills, errors, warnings);
|
|
453
|
+
} else {
|
|
454
|
+
warnings.push(
|
|
455
|
+
"console-plugin.json: names no skills; an agent learns the plugin from its command's help alone",
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
if (manifest.migrations !== undefined) {
|
|
459
|
+
await checkMigrations(root, manifest.migrations, errors);
|
|
460
|
+
} else if (manifest.capabilities.includes("storage:sql")) {
|
|
461
|
+
warnings.push("console-plugin.json: asks storage:sql and names no migrations");
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
ok: errors.length === 0,
|
|
466
|
+
errors,
|
|
467
|
+
warnings,
|
|
468
|
+
summary: `${manifest.key} ${version}: ${describeCounts(manifest, description)}, ${skills} skills`,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
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 { nameFromKey, 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
|
+
Writes a plugin into <dir>, which must not exist or be empty, from the template this
|
|
29
|
+
package ships: the manifest, a server module with one collection, one command and one
|
|
30
|
+
query, the source of a web bundle with one page and one widget, one skill with its
|
|
31
|
+
manifest, tests, and the build. <key> is the plugin's key, lowercase letters, digits and
|
|
32
|
+
hyphens; --name its name for people, from the key unless said; --console what the
|
|
33
|
+
plugin's devDependency on @skillcdn/console says, ^<this version> unless said.
|
|
34
|
+
|
|
35
|
+
console-plugin check [<dir>]
|
|
36
|
+
Reads the plugin in <dir>, the working directory unless said, as a console would, built:
|
|
37
|
+
the manifest, package.json (a permissive license, no install scripts, files that ship
|
|
38
|
+
what runs), the server module loaded under the host's hardening and held to the manifest,
|
|
39
|
+
the web bundle self-contained and its classes prefixed, the skills, the migrations.
|
|
40
|
+
Exits 0 with a summary, 1 with every problem found.
|
|
41
|
+
|
|
42
|
+
console-plugin help | --version
|
|
43
|
+
|
|
44
|
+
The contract is docs/specs/plugins.md of the console's repository, shipped with this package.
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
function wordsOf(items: readonly string[], prefix: string): string {
|
|
48
|
+
return items.map((item) => ` ${prefix}: ${item}\n`).join("");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function runPluginCli(argv: readonly string[], io: PluginCliIo): Promise<number> {
|
|
52
|
+
const [command, ...rest] = argv;
|
|
53
|
+
if (command === undefined || command === "help" || command === "--help" || command === "-h") {
|
|
54
|
+
io.stdout(PLUGIN_USAGE);
|
|
55
|
+
return PLUGIN_EXIT.ok;
|
|
56
|
+
}
|
|
57
|
+
if (command === "--version" || command === "version") {
|
|
58
|
+
io.stdout(`${io.version ?? "unknown"}\n`);
|
|
59
|
+
return PLUGIN_EXIT.ok;
|
|
60
|
+
}
|
|
61
|
+
if (command === "init") {
|
|
62
|
+
return init(rest, io);
|
|
63
|
+
}
|
|
64
|
+
if (command === "check") {
|
|
65
|
+
return check(rest, io);
|
|
66
|
+
}
|
|
67
|
+
io.stderr(`console-plugin: ${command} is not a command; console-plugin help lists them\n`);
|
|
68
|
+
return PLUGIN_EXIT.usage;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function init(args: readonly string[], io: PluginCliIo): Promise<number> {
|
|
72
|
+
let values: { key?: string; name?: string; console?: string };
|
|
73
|
+
let positionals: string[];
|
|
74
|
+
try {
|
|
75
|
+
({ values, positionals } = parseArgs({
|
|
76
|
+
args: [...args],
|
|
77
|
+
options: { key: { type: "string" }, name: { type: "string" }, console: { type: "string" } },
|
|
78
|
+
allowPositionals: true,
|
|
79
|
+
strict: true,
|
|
80
|
+
}));
|
|
81
|
+
} catch (error) {
|
|
82
|
+
io.stderr(`console-plugin init: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
83
|
+
return PLUGIN_EXIT.usage;
|
|
84
|
+
}
|
|
85
|
+
const [dir, ...extra] = positionals;
|
|
86
|
+
const key = values.key;
|
|
87
|
+
if (dir === undefined || extra.length > 0 || key === undefined) {
|
|
88
|
+
io.stderr(
|
|
89
|
+
"usage: console-plugin init <dir> --key <key> [--name <name>] [--console <dependency>]\n",
|
|
90
|
+
);
|
|
91
|
+
return PLUGIN_EXIT.usage;
|
|
92
|
+
}
|
|
93
|
+
if (!isPluginKey(key)) {
|
|
94
|
+
io.stderr(
|
|
95
|
+
`console-plugin init: ${key} is not a key: lowercase letters, digits and hyphens, 2 to 32 long, starting with a letter\n`,
|
|
96
|
+
);
|
|
97
|
+
return PLUGIN_EXIT.usage;
|
|
98
|
+
}
|
|
99
|
+
if (RESERVED_PLUGIN_KEYS.includes(key)) {
|
|
100
|
+
io.stderr(`console-plugin init: ${key} is a word the console reserves; choose another key\n`);
|
|
101
|
+
return PLUGIN_EXIT.usage;
|
|
102
|
+
}
|
|
103
|
+
const name = values.name ?? nameFromKey(key);
|
|
104
|
+
if (name.length === 0 || name.length > 40 || /[\r\n]/.test(name)) {
|
|
105
|
+
io.stderr("console-plugin init: --name is one line of up to 40 characters\n");
|
|
106
|
+
return PLUGIN_EXIT.usage;
|
|
107
|
+
}
|
|
108
|
+
const consoleDependency = values.console ?? (io.version === undefined ? "*" : `^${io.version}`);
|
|
109
|
+
const target = resolve(io.cwd, dir);
|
|
110
|
+
try {
|
|
111
|
+
const written = await writePluginFromTemplate(io.templateDir, target, {
|
|
112
|
+
key,
|
|
113
|
+
name,
|
|
114
|
+
consoleDependency,
|
|
115
|
+
});
|
|
116
|
+
io.stdout(`Wrote the plugin ${key} into ${target}: ${written.length} files.\n`);
|
|
117
|
+
io.stdout(
|
|
118
|
+
`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`,
|
|
119
|
+
);
|
|
120
|
+
return PLUGIN_EXIT.ok;
|
|
121
|
+
} catch (error) {
|
|
122
|
+
io.stderr(`console-plugin init: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
123
|
+
return PLUGIN_EXIT.failed;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function check(args: readonly string[], io: PluginCliIo): Promise<number> {
|
|
128
|
+
const [dir, ...extra] = args;
|
|
129
|
+
if (extra.length > 0 || dir?.startsWith("-")) {
|
|
130
|
+
io.stderr("usage: console-plugin check [<dir>]\n");
|
|
131
|
+
return PLUGIN_EXIT.usage;
|
|
132
|
+
}
|
|
133
|
+
const root = resolve(io.cwd, dir ?? ".");
|
|
134
|
+
const report = await checkPlugin(root, (file) => io.loadServer(file));
|
|
135
|
+
io.stdout(`console-plugin check ${root}\n`);
|
|
136
|
+
io.stdout(wordsOf(report.errors, "error"));
|
|
137
|
+
io.stdout(wordsOf(report.warnings, "warning"));
|
|
138
|
+
if (report.ok) {
|
|
139
|
+
io.stdout(
|
|
140
|
+
`ok: ${report.summary ?? "a plugin"}${report.warnings.length > 0 ? `; ${report.warnings.length} warning${report.warnings.length === 1 ? "" : "s"}` : ""}\n`,
|
|
141
|
+
);
|
|
142
|
+
return PLUGIN_EXIT.ok;
|
|
143
|
+
}
|
|
144
|
+
io.stdout(
|
|
145
|
+
`${report.errors.length} problem${report.errors.length === 1 ? "" : "s"}${report.summary === null ? "" : ` in ${report.summary}`}\n`,
|
|
146
|
+
);
|
|
147
|
+
return PLUGIN_EXIT.failed;
|
|
148
|
+
}
|