@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.
- package/README.md +19 -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 +569 -48
- 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 +283 -7
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +44 -7
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/plugin-check.d.ts +26 -0
- package/dist/cli/plugin-check.d.ts.map +1 -0
- package/dist/cli/plugin-check.js +396 -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 +189 -0
- package/dist/cli/plugin-cli.js.map +1 -0
- package/dist/cli/plugin-init.d.ts +37 -0
- package/dist/cli/plugin-init.d.ts.map +1 -0
- package/dist/cli/plugin-init.js +206 -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 +100 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +79 -7
- 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/comments.d.ts.map +1 -1
- package/dist/components/comments.js +1 -1
- package/dist/components/comments.js.map +1 -1
- package/dist/components/decision-form.d.ts +2 -1
- package/dist/components/decision-form.d.ts.map +1 -1
- package/dist/components/decision-form.js +36 -4
- package/dist/components/decision-form.js.map +1 -1
- package/dist/components/decision-list.d.ts +4 -0
- package/dist/components/decision-list.d.ts.map +1 -1
- package/dist/components/decision-list.js +29 -3
- package/dist/components/decision-list.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/my-decisions.d.ts +2 -0
- package/dist/components/my-decisions.d.ts.map +1 -1
- package/dist/components/my-decisions.js +2 -2
- package/dist/components/my-decisions.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/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/task-view.d.ts +2 -1
- package/dist/components/task-view.d.ts.map +1 -1
- package/dist/components/task-view.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 +330 -1
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +152 -41
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +9 -2
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +45 -9
- package/dist/data.js.map +1 -1
- package/dist/i18n/en.d.ts +155 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +166 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +150 -0
- 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 +2 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +2 -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 +380 -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 +4 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +6 -0
- package/dist/routes.js.map +1 -1
- package/dist/schemas.d.ts +194 -9
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +98 -17
- 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 +2 -0
- package/dist/web.js.map +1 -1
- package/docs/specs/cli.md +12 -5
- package/docs/specs/plugins.md +488 -0
- package/docs/specs/rest.md +11 -9
- package/package.json +26 -8
- 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 +12 -0
- package/src/cli/cli.ts +737 -55
- package/src/cli/format.ts +368 -10
- package/src/cli/help.ts +50 -6
- package/src/cli/plugin-check.ts +497 -0
- package/src/cli/plugin-cli.ts +237 -0
- package/src/cli/plugin-init.ts +257 -0
- package/src/cli/plugin-main.ts +30 -0
- package/src/client.ts +322 -9
- package/src/compat.ts +2 -2
- package/src/components/board.tsx +62 -14
- package/src/components/comments.tsx +1 -0
- package/src/components/decision-form.tsx +92 -11
- package/src/components/decision-list.tsx +71 -2
- package/src/components/event-feed.tsx +42 -1
- package/src/components/my-decisions.tsx +4 -1
- package/src/components/plugin-frame.tsx +437 -0
- package/src/components/plugin-settings.tsx +1111 -0
- package/src/components/shell.tsx +82 -14
- package/src/components/task-view.tsx +2 -1
- package/src/components/workspace.tsx +18 -0
- package/src/console.tsx +304 -31
- package/src/data.ts +59 -11
- package/src/i18n/en.ts +175 -0
- package/src/i18n/ko.ts +155 -0
- package/src/index.ts +12 -0
- package/src/limits.ts +2 -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 +421 -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 +7 -0
- package/src/schemas.ts +115 -18
- package/src/styles/console.css +330 -1
- package/src/tokens.ts +101 -0
- package/src/vocabulary.ts +30 -0
- package/src/web.ts +2 -0
- 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
package/dist/cli/cli.js
CHANGED
|
@@ -4,13 +4,15 @@ import { ApiError, createClient, } from "../client.js";
|
|
|
4
4
|
import { CLIENT_RANGE_HEADER, CONSOLE_AT_LEAST, consoleRange, formatRange, judgeClient, judgeConsole, parseVersion, VERSION_HEADER, formatVersion as versionText, } from "../compat.js";
|
|
5
5
|
import { addDays, dayOf as dayOfDate, firstDayOfWeek, isDay, weekOf } from "../dates.js";
|
|
6
6
|
import { DEFAULT_PAGE_LIMIT, LIST_LIMIT, MAX_OPTIONS, MIN_OPTIONS } from "../limits.js";
|
|
7
|
+
import { capabilityWords } from "../plugin/capabilities.js";
|
|
8
|
+
import { PLUGIN_LIST_LIMIT } from "../plugin/limits.js";
|
|
7
9
|
import { fillRules, RULES_PATH, RULES_TITLE } from "../rules.js";
|
|
8
10
|
import { hostOfLogin, } from "../schemas.js";
|
|
9
11
|
import { COLOR_NAMES, GOAL_STATES, PEOPLE_VISIBILITIES, PERSON_ROLES, PROJECT_ROLES, PROJECT_VISIBILITIES, PROVIDER_KEYS, TASK_PRIORITIES, TASK_STATES, WEEK_STARTS, } from "../vocabulary.js";
|
|
10
12
|
import { loadWebRoot, WebRootError } from "../web.js";
|
|
11
13
|
import { CredentialsError, EMPTY_CREDENTIALS, loginsAt, resolveLogin, withLogin, withoutLogin, } from "./credentials.js";
|
|
12
14
|
import { THE_SENTENCE } from "./directory.js";
|
|
13
|
-
import { formatAnswer, formatCategoryLine, formatComment, formatDecision, formatDecisionLine, formatDocument, formatDocuments, formatGoal, formatGoalLine, formatListingLine, formatMemberLine, formatMore, formatOptions, formatPersonLine, formatPersonWork, formatProject, formatProjectLine, formatRules, formatRun, formatRunLine, formatTask, formatTaskLine, formatVersion, formatVersionLine, formatWork, formatWorkspace, moment, setMomentZone, } from "./format.js";
|
|
15
|
+
import { formatAnswer, formatCategoryLine, formatComment, formatDecision, formatDecisionLine, formatDocument, formatDocuments, formatGoal, formatGoalLine, formatInvocation, formatListingLine, formatMemberLine, formatMore, formatOptions, formatPersonLine, formatPersonWork, formatPlugin, formatPluginGrantLine, formatPluginHelp, formatPluginRecord, formatPluginRecords, formatPluginRecordVersions, formatPluginSettingLine, formatPlugins, formatProject, formatProjectLine, formatRules, formatRun, formatRunLine, formatTask, formatTaskLine, formatVersion, formatVersionLine, formatWork, formatWorkspace, localized, moment, setMomentZone, } from "./format.js";
|
|
14
16
|
import { COMMAND_HELP, helpFor, ownSkillPath, usage } from "./help.js";
|
|
15
17
|
import { describeInstall, updateCommandFor } from "./install.js";
|
|
16
18
|
import { bodyOfFile, bodyOfFolder, bodyOfSkill, formatSkillBrowse, formatSkillFile, formatSkillFind, formatSkillLoad, formatSkills, joinFile, joinFolder, joinSkill, skillPathIn, summaryOf, } from "./pack.js";
|
|
@@ -772,18 +774,29 @@ const MAX_SKILL_PAGES = 64;
|
|
|
772
774
|
/** How many results a search answers at most: what the deployment allows on one page. */
|
|
773
775
|
const MAX_FIND_LIMIT = 25;
|
|
774
776
|
/** The pack of the project as the console lists it, and what its reads are rendered at; none when the pack did not answer. */
|
|
775
|
-
async function packOf(
|
|
776
|
-
const listing = await
|
|
777
|
+
async function packOf(reader) {
|
|
778
|
+
const listing = await reader.skills();
|
|
777
779
|
return { listing, summary: summaryOf(listing) };
|
|
778
780
|
}
|
|
779
|
-
const
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
781
|
+
const SKILLS_OPTIONS = {
|
|
782
|
+
search: { type: "string" },
|
|
783
|
+
limit: { type: "string" },
|
|
784
|
+
cursor: { type: "string" },
|
|
785
|
+
};
|
|
786
|
+
const SKILL_OPTIONS = {
|
|
787
|
+
file: { type: "string" },
|
|
788
|
+
offset: { type: "string" },
|
|
789
|
+
limit: { type: "string" },
|
|
790
|
+
};
|
|
791
|
+
/** The line `console skills` ends with: where the plugins' own skills are. */
|
|
792
|
+
const PLUGIN_SKILLS_LINE = "The plugins of this console have skills of their own: console plugins lists the plugins, console plugin <key> skills the skills of one.";
|
|
793
|
+
/**
|
|
794
|
+
* The pack as the words say: whole, a folder of it, a file's outline, or what words find; for
|
|
795
|
+
* the project's pack as `console skills`, and for a plugin's as `console plugin <key> skills`.
|
|
796
|
+
*/
|
|
797
|
+
async function readSkills(io, session, reader, values, positionals, extras) {
|
|
785
798
|
if (positionals.length > 1) {
|
|
786
|
-
throw misuse(
|
|
799
|
+
throw misuse(`Say one folder: ${extras.command} [<folder>] [--search <words>]`);
|
|
787
800
|
}
|
|
788
801
|
// The root of the pack is the mounted folder, which the deployment reads as no path at all.
|
|
789
802
|
const given = positionals[0];
|
|
@@ -792,24 +805,24 @@ const skills = async (args, io) => {
|
|
|
792
805
|
const limit = text(values, "limit");
|
|
793
806
|
const cursor = text(values, "cursor");
|
|
794
807
|
if (words === undefined && (limit !== undefined || cursor !== undefined)) {
|
|
795
|
-
throw misuse(
|
|
808
|
+
throw misuse(`--limit and --cursor go with --search: ${extras.command} --search <words> [--limit <n>] [--cursor <cursor>]`);
|
|
796
809
|
}
|
|
797
810
|
const atMost = limit === undefined ? undefined : Number(limit);
|
|
798
811
|
if (atMost !== undefined &&
|
|
799
812
|
(!Number.isInteger(atMost) || atMost < 1 || atMost > MAX_FIND_LIMIT)) {
|
|
800
813
|
throw misuse(`Not a number of results, 1 to ${MAX_FIND_LIMIT}: ${limit}`);
|
|
801
814
|
}
|
|
802
|
-
const
|
|
803
|
-
const project = await session.project();
|
|
804
|
-
const { listing, summary } = await packOf(project);
|
|
815
|
+
const { listing, summary } = await packOf(reader);
|
|
805
816
|
// The pack as it stands; or why it does not answer, which a folder or a search would meet too.
|
|
806
817
|
if (summary === undefined || (words === undefined && given === undefined)) {
|
|
807
|
-
answer(io, session, listing, () => formatSkills(listing,
|
|
818
|
+
answer(io, session, listing, () => [formatSkills(listing, extras.own), extras.afterwards]
|
|
819
|
+
.filter((part) => part !== undefined)
|
|
820
|
+
.join("\n"));
|
|
808
821
|
return EXIT.ok;
|
|
809
822
|
}
|
|
810
823
|
if (words !== undefined) {
|
|
811
824
|
// One page of what the words find; more is said, and asked for with --cursor.
|
|
812
|
-
const found = await
|
|
825
|
+
const found = await reader.skillFind(words, { path: folder, limit: atMost, cursor });
|
|
813
826
|
answer(io, session, found, () => formatSkillFind(found, summary));
|
|
814
827
|
return EXIT.ok;
|
|
815
828
|
}
|
|
@@ -818,7 +831,7 @@ const skills = async (args, io) => {
|
|
|
818
831
|
let next;
|
|
819
832
|
let last;
|
|
820
833
|
for (let n = 0; n < MAX_SKILL_PAGES; n += 1) {
|
|
821
|
-
last = await
|
|
834
|
+
last = await reader.skillBrowse({ path: folder, cursor: next });
|
|
822
835
|
if (last.read?.status !== "ready") {
|
|
823
836
|
break;
|
|
824
837
|
}
|
|
@@ -835,6 +848,16 @@ const skills = async (args, io) => {
|
|
|
835
848
|
const whole = joinFolder(pages, summary);
|
|
836
849
|
answer(io, session, { ...browsed, read: whole === undefined ? null : bodyOfFolder(whole) }, () => formatSkillBrowse(browsed, whole));
|
|
837
850
|
return EXIT.ok;
|
|
851
|
+
}
|
|
852
|
+
const skills = async (args, io) => {
|
|
853
|
+
const { values, positionals } = parse(args, SKILLS_OPTIONS);
|
|
854
|
+
const session = await open(io, values);
|
|
855
|
+
const project = await session.project();
|
|
856
|
+
return readSkills(io, session, project, values, positionals, {
|
|
857
|
+
command: "console skills",
|
|
858
|
+
own: io.skillsDir,
|
|
859
|
+
afterwards: PLUGIN_SKILLS_LINE,
|
|
860
|
+
});
|
|
838
861
|
};
|
|
839
862
|
/** The most characters one read of a file asks for: what the console's API allows on one page. */
|
|
840
863
|
const MAX_FILE_LIMIT = 100_000;
|
|
@@ -852,28 +875,25 @@ function countOf(given, name, bounds) {
|
|
|
852
875
|
}
|
|
853
876
|
return count;
|
|
854
877
|
}
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
});
|
|
878
|
+
/**
|
|
879
|
+
* One skill of the pack whole, a file of it, or one page of a file; for the project's pack as
|
|
880
|
+
* `console skill`, and for a plugin's as `console plugin <key> skill`.
|
|
881
|
+
*/
|
|
882
|
+
async function readSkill(io, session, reader, values, positionals, command) {
|
|
861
883
|
const file = text(values, "file");
|
|
862
884
|
// A file is read by its own path from the repository's root; a skill's path beside it would be
|
|
863
885
|
// ignored, which reads as if it mattered, so the two together are not understood.
|
|
864
886
|
if (file !== undefined && positionals.length > 0) {
|
|
865
|
-
throw misuse(
|
|
887
|
+
throw misuse(`Say one: ${command} <path of its SKILL.md> for a skill, or ${command} --file <path> for a file of the pack.`);
|
|
866
888
|
}
|
|
867
889
|
// A section is a page of a file read as asked (SkillCDN 0.4): the flags mean nothing without one.
|
|
868
890
|
if (file === undefined &&
|
|
869
891
|
(text(values, "offset") !== undefined || text(values, "limit") !== undefined)) {
|
|
870
|
-
throw misuse(
|
|
892
|
+
throw misuse(`--offset and --limit go with --file: ${command} --file <path> [--offset <n>] [--limit <n>]`);
|
|
871
893
|
}
|
|
872
894
|
const offset = countOf(text(values, "offset"), "offset", { least: 0 });
|
|
873
895
|
const limit = countOf(text(values, "limit"), "limit", { least: 1, most: MAX_FILE_LIMIT });
|
|
874
|
-
const
|
|
875
|
-
const project = await session.project();
|
|
876
|
-
const { listing, summary } = await packOf(project);
|
|
896
|
+
const { listing, summary } = await packOf(reader);
|
|
877
897
|
if (summary === undefined) {
|
|
878
898
|
answer(io, session, listing, () => formatSkills(listing, io.skillsDir));
|
|
879
899
|
return EXIT.ok;
|
|
@@ -881,7 +901,7 @@ const skill = async (args, io) => {
|
|
|
881
901
|
if (file !== undefined && (offset !== undefined || limit !== undefined)) {
|
|
882
902
|
// One page as asked, the section a search or an outline named; the result says where the
|
|
883
903
|
// file continues, and asks for nothing.
|
|
884
|
-
const page = await
|
|
904
|
+
const page = await reader.skillFile(file, { offset, limit });
|
|
885
905
|
const section = page.read === null ? undefined : joinFile([page], summary, limit);
|
|
886
906
|
answer(io, session, { ...page, read: section === undefined ? null : bodyOfFile(section) }, () => formatSkillFile(page, section));
|
|
887
907
|
return EXIT.ok;
|
|
@@ -892,7 +912,7 @@ const skill = async (args, io) => {
|
|
|
892
912
|
let offset;
|
|
893
913
|
let last;
|
|
894
914
|
for (let n = 0; n < MAX_SKILL_PAGES; n += 1) {
|
|
895
|
-
last = await
|
|
915
|
+
last = await reader.skillFile(file, offset === undefined ? {} : { offset });
|
|
896
916
|
if (last.read === null) {
|
|
897
917
|
break;
|
|
898
918
|
}
|
|
@@ -910,7 +930,7 @@ const skill = async (args, io) => {
|
|
|
910
930
|
answer(io, session, { ...fileRead, read: whole === undefined ? null : bodyOfFile(whole) }, () => formatSkillFile(fileRead, whole));
|
|
911
931
|
return EXIT.ok;
|
|
912
932
|
}
|
|
913
|
-
const wanted = skillPathIn(theOne(positionals,
|
|
933
|
+
const wanted = skillPathIn(theOne(positionals, `Say which skill: ${command} <path of its SKILL.md>, or ${command} --file <path> for a file of the pack`), listing.address);
|
|
914
934
|
if ("error" in wanted) {
|
|
915
935
|
throw misuse(wanted.error);
|
|
916
936
|
}
|
|
@@ -919,7 +939,7 @@ const skill = async (args, io) => {
|
|
|
919
939
|
let cursor;
|
|
920
940
|
let last;
|
|
921
941
|
for (let n = 0; n < MAX_SKILL_PAGES; n += 1) {
|
|
922
|
-
last = await
|
|
942
|
+
last = await reader.skill(wanted.path, cursor);
|
|
923
943
|
if (last.read?.status !== "ready") {
|
|
924
944
|
break;
|
|
925
945
|
}
|
|
@@ -936,7 +956,492 @@ const skill = async (args, io) => {
|
|
|
936
956
|
const whole = joinSkill(pages, summary);
|
|
937
957
|
answer(io, session, { ...loaded, read: whole === undefined ? null : bodyOfSkill(whole) }, () => formatSkillLoad(loaded, whole));
|
|
938
958
|
return EXIT.ok;
|
|
959
|
+
}
|
|
960
|
+
const skill = async (args, io) => {
|
|
961
|
+
const { values, positionals } = parse(args, SKILL_OPTIONS);
|
|
962
|
+
const session = await open(io, values);
|
|
963
|
+
const project = await session.project();
|
|
964
|
+
return readSkill(io, session, project, values, positionals, "console skill");
|
|
965
|
+
};
|
|
966
|
+
// ---- The plugins (docs/specs/plugins.md, "The command") ----
|
|
967
|
+
// `console plugins` and `console plugin <key> ...` are the two entries: the command sends the
|
|
968
|
+
// words as typed and renders the console's generic shapes, and knows nothing of any plugin.
|
|
969
|
+
const plugins = async (args, io) => {
|
|
970
|
+
const { values } = parse(args, {}, false);
|
|
971
|
+
const session = await open(io, values);
|
|
972
|
+
const [listed, me] = await Promise.all([session.client.plugins(), session.client.me()]);
|
|
973
|
+
answer(io, session, listed, () => formatPlugins(listed, me.language));
|
|
974
|
+
return EXIT.ok;
|
|
975
|
+
};
|
|
976
|
+
/** The flags of the command's own on `console plugin ...`, wherever they stand among the words. */
|
|
977
|
+
const PLUGIN_GLOBAL_FLAGS = {
|
|
978
|
+
json: "boolean",
|
|
979
|
+
url: "string",
|
|
980
|
+
account: "string",
|
|
981
|
+
project: "string",
|
|
982
|
+
};
|
|
983
|
+
/** The flags of the command's own on a plugin's command: the file it uploads first, the run it is for. */
|
|
984
|
+
const PLUGIN_COMMAND_FLAGS = {
|
|
985
|
+
file: "string",
|
|
986
|
+
run: "string",
|
|
987
|
+
};
|
|
988
|
+
/**
|
|
989
|
+
* The words with the flags named in `own` taken out, wherever they stand: the rest is the
|
|
990
|
+
* plugin's, and goes to the console as typed. `--` ends the flags, and stays among the words.
|
|
991
|
+
*/
|
|
992
|
+
function splitWords(args, own) {
|
|
993
|
+
const values = {};
|
|
994
|
+
const words = [];
|
|
995
|
+
let ended = false;
|
|
996
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
997
|
+
const word = args[index] ?? "";
|
|
998
|
+
if (ended || !word.startsWith("--") || word.length === 2) {
|
|
999
|
+
ended = ended || word === "--";
|
|
1000
|
+
words.push(word);
|
|
1001
|
+
continue;
|
|
1002
|
+
}
|
|
1003
|
+
const equals = word.indexOf("=");
|
|
1004
|
+
const name = equals < 0 ? word.slice(2) : word.slice(2, equals);
|
|
1005
|
+
const type = Object.hasOwn(own, name) ? own[name] : undefined;
|
|
1006
|
+
if (type === undefined) {
|
|
1007
|
+
words.push(word);
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
if (type === "boolean") {
|
|
1011
|
+
if (equals >= 0) {
|
|
1012
|
+
throw misuse(`--${name} takes no value`);
|
|
1013
|
+
}
|
|
1014
|
+
values[name] = true;
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
if (equals >= 0) {
|
|
1018
|
+
values[name] = word.slice(equals + 1);
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
const next = args[index + 1];
|
|
1022
|
+
if (next === undefined) {
|
|
1023
|
+
throw misuse(`--${name} needs a value`);
|
|
1024
|
+
}
|
|
1025
|
+
values[name] = next;
|
|
1026
|
+
index += 1;
|
|
1027
|
+
}
|
|
1028
|
+
return { values, words };
|
|
1029
|
+
}
|
|
1030
|
+
const PLUGIN_USAGE = "console plugin <key> [help [<command>] | <command> [args] | records <collection> | skills | skill <path> | enable | disable | settings | secret <name> | grant <role> | revoke <id>]. console plugins lists the plugins.";
|
|
1031
|
+
const plugin = async (args, io) => {
|
|
1032
|
+
const { values, words } = splitWords(args, PLUGIN_GLOBAL_FLAGS);
|
|
1033
|
+
const [key, verb, ...rest] = words;
|
|
1034
|
+
if (key === undefined || key.startsWith("-")) {
|
|
1035
|
+
throw misuse(`Say which plugin: ${PLUGIN_USAGE}`);
|
|
1036
|
+
}
|
|
1037
|
+
const session = await open(io, values);
|
|
1038
|
+
const client = session.client.plugin(key);
|
|
1039
|
+
switch (verb) {
|
|
1040
|
+
case undefined: {
|
|
1041
|
+
const [detail, me] = await Promise.all([client.get(), session.client.me()]);
|
|
1042
|
+
answer(io, session, detail, () => formatPlugin(detail, me.language));
|
|
1043
|
+
return EXIT.ok;
|
|
1044
|
+
}
|
|
1045
|
+
case "help": {
|
|
1046
|
+
if (rest.length > 1) {
|
|
1047
|
+
throw misuse(`Say one command: console plugin ${key} help [<command>]`);
|
|
1048
|
+
}
|
|
1049
|
+
const name = rest[0];
|
|
1050
|
+
const detail = await client.get();
|
|
1051
|
+
const operation = name === undefined
|
|
1052
|
+
? undefined
|
|
1053
|
+
: detail.operations.find((candidate) => candidate.name === name);
|
|
1054
|
+
if (name !== undefined && operation === undefined) {
|
|
1055
|
+
throw misuse(`Not a command or a query of ${key} you may run: ${name}. console plugin ${key} lists them.`);
|
|
1056
|
+
}
|
|
1057
|
+
answer(io, session, operation ?? detail.operations, () => formatPluginHelp(detail, name));
|
|
1058
|
+
return EXIT.ok;
|
|
1059
|
+
}
|
|
1060
|
+
case "records":
|
|
1061
|
+
return pluginRecords(io, session, client, rest, text(values, "project"));
|
|
1062
|
+
case "skills": {
|
|
1063
|
+
const parsed = parse(rest, SKILLS_OPTIONS);
|
|
1064
|
+
return readSkills(io, session, client, parsed.values, parsed.positionals, {
|
|
1065
|
+
command: `console plugin ${key} skills`,
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
case "skill": {
|
|
1069
|
+
const parsed = parse(rest, SKILL_OPTIONS);
|
|
1070
|
+
return readSkill(io, session, client, parsed.values, parsed.positionals, `console plugin ${key} skill`);
|
|
1071
|
+
}
|
|
1072
|
+
case "enable":
|
|
1073
|
+
return pluginEnable(io, session, client, rest);
|
|
1074
|
+
case "disable": {
|
|
1075
|
+
parse(rest, {}, false);
|
|
1076
|
+
const detail = await client.disable();
|
|
1077
|
+
answer(io, session, detail, () => `Disabled ${detail.key}: its commands are refused until it is enabled again; what the console kept of it stays.`);
|
|
1078
|
+
return EXIT.ok;
|
|
1079
|
+
}
|
|
1080
|
+
case "settings":
|
|
1081
|
+
return pluginSettings(io, session, client, rest);
|
|
1082
|
+
case "secret":
|
|
1083
|
+
return pluginSecret(io, session, client, rest);
|
|
1084
|
+
case "grant":
|
|
1085
|
+
return pluginGrant(io, session, client, rest, text(values, "project"));
|
|
1086
|
+
case "revoke": {
|
|
1087
|
+
const { positionals } = parse(rest, {});
|
|
1088
|
+
const id = theOne(positionals, `Say which grant: console plugin ${key} revoke <grant id>`);
|
|
1089
|
+
const revoked = await client.revoke(id);
|
|
1090
|
+
answer(io, session, revoked, () => `Revoked: ${formatPluginGrantLine(revoked)}`);
|
|
1091
|
+
return EXIT.ok;
|
|
1092
|
+
}
|
|
1093
|
+
default:
|
|
1094
|
+
return pluginInvoke(io, session, client, verb, rest);
|
|
1095
|
+
}
|
|
939
1096
|
};
|
|
1097
|
+
/** A query's answer that is a file, as the console carries one: its name, its type, its bytes as base64. */
|
|
1098
|
+
function queryFileOf(result) {
|
|
1099
|
+
if (result.kind !== "query" || typeof result.data !== "object" || result.data === null) {
|
|
1100
|
+
return undefined;
|
|
1101
|
+
}
|
|
1102
|
+
const file = result.data.file;
|
|
1103
|
+
if (typeof file !== "object" ||
|
|
1104
|
+
file === null ||
|
|
1105
|
+
typeof file.name !== "string" ||
|
|
1106
|
+
typeof file.type !== "string" ||
|
|
1107
|
+
typeof file.base64 !== "string") {
|
|
1108
|
+
return undefined;
|
|
1109
|
+
}
|
|
1110
|
+
const named = file;
|
|
1111
|
+
return {
|
|
1112
|
+
name: basename(named.name) || "answer",
|
|
1113
|
+
type: named.type,
|
|
1114
|
+
bytes: Uint8Array.from(Buffer.from(named.base64, "base64")),
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* A command or a query of the plugin, by the words as typed: a file named with --file, or read
|
|
1119
|
+
* from standard input for -, is uploaded first and goes as a handle; the console parses the rest.
|
|
1120
|
+
*/
|
|
1121
|
+
async function pluginInvoke(io, session, client, name, rest) {
|
|
1122
|
+
const { values, words } = splitWords(rest, PLUGIN_COMMAND_FLAGS);
|
|
1123
|
+
const stdin = words.includes("-");
|
|
1124
|
+
const filePath = text(values, "file");
|
|
1125
|
+
if (filePath !== undefined && stdin) {
|
|
1126
|
+
throw misuse("Say one file: --file <path>, or - for standard input.");
|
|
1127
|
+
}
|
|
1128
|
+
const files = [];
|
|
1129
|
+
if (filePath !== undefined) {
|
|
1130
|
+
let bytes;
|
|
1131
|
+
try {
|
|
1132
|
+
bytes = await io.readBytes(filePath);
|
|
1133
|
+
}
|
|
1134
|
+
catch {
|
|
1135
|
+
throw misuse(`The file could not be read: ${filePath}`);
|
|
1136
|
+
}
|
|
1137
|
+
const fileName = basename(filePath);
|
|
1138
|
+
const uploaded = await client.upload({
|
|
1139
|
+
name: fileName,
|
|
1140
|
+
bytes,
|
|
1141
|
+
contentType: CONTENT_TYPES[extname(fileName).toLowerCase()] ?? "application/octet-stream",
|
|
1142
|
+
});
|
|
1143
|
+
files.push(uploaded.handle);
|
|
1144
|
+
}
|
|
1145
|
+
else if (stdin) {
|
|
1146
|
+
const uploaded = await client.upload({
|
|
1147
|
+
name: "stdin.txt",
|
|
1148
|
+
bytes: new TextEncoder().encode(await io.readStdin()),
|
|
1149
|
+
contentType: "text/plain",
|
|
1150
|
+
});
|
|
1151
|
+
files.push(uploaded.handle);
|
|
1152
|
+
}
|
|
1153
|
+
const project = await session.projectKey();
|
|
1154
|
+
const run = text(values, "run");
|
|
1155
|
+
const result = await client.invoke({
|
|
1156
|
+
argv: [name, ...words.filter((word) => word !== "-")],
|
|
1157
|
+
...(files.length === 0 ? {} : { files }),
|
|
1158
|
+
...(project === undefined ? {} : { project }),
|
|
1159
|
+
...(run === undefined ? {} : { run }),
|
|
1160
|
+
});
|
|
1161
|
+
// A query that answers a file: saved under its name in the working directory, as console file saves one.
|
|
1162
|
+
const file = queryFileOf(result);
|
|
1163
|
+
let saved;
|
|
1164
|
+
if (file !== undefined && !session.json) {
|
|
1165
|
+
try {
|
|
1166
|
+
saved = await io.writeBytes(file.name, file.bytes, file.name);
|
|
1167
|
+
}
|
|
1168
|
+
catch {
|
|
1169
|
+
throw failed(`The file could not be written at ${file.name}.`);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
answer(io, session, result, () => file !== undefined && saved !== undefined
|
|
1173
|
+
? `Saved ${file.name} (${file.bytes.byteLength} bytes, ${file.type}) at ${saved}`
|
|
1174
|
+
: formatInvocation(result));
|
|
1175
|
+
return result.waiting === null ? EXIT.ok : EXIT.waiting;
|
|
1176
|
+
}
|
|
1177
|
+
/** A condition's value as typed: a number, true, false and null read as such; JSON where it begins as JSON; text otherwise. */
|
|
1178
|
+
function whereValueOf(word) {
|
|
1179
|
+
if (word === "true") {
|
|
1180
|
+
return true;
|
|
1181
|
+
}
|
|
1182
|
+
if (word === "false") {
|
|
1183
|
+
return false;
|
|
1184
|
+
}
|
|
1185
|
+
if (word === "null") {
|
|
1186
|
+
return null;
|
|
1187
|
+
}
|
|
1188
|
+
if (/^-?\d+(?:\.\d+)?$/.test(word)) {
|
|
1189
|
+
return Number(word);
|
|
1190
|
+
}
|
|
1191
|
+
if (word.startsWith("{") || word.startsWith("[")) {
|
|
1192
|
+
try {
|
|
1193
|
+
return JSON.parse(word);
|
|
1194
|
+
}
|
|
1195
|
+
catch {
|
|
1196
|
+
return word;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
return word;
|
|
1200
|
+
}
|
|
1201
|
+
/** The records of a collection one may read, a page of them; or one record, or its versions. */
|
|
1202
|
+
async function pluginRecords(io, session, client, rest, project) {
|
|
1203
|
+
const { values, positionals } = parse(rest, {
|
|
1204
|
+
where: { type: "string", multiple: true },
|
|
1205
|
+
search: { type: "string" },
|
|
1206
|
+
order: { type: "string" },
|
|
1207
|
+
limit: { type: "string" },
|
|
1208
|
+
after: { type: "string" },
|
|
1209
|
+
archived: { type: "boolean" },
|
|
1210
|
+
versions: { type: "boolean" },
|
|
1211
|
+
});
|
|
1212
|
+
const [collection, id, ...more] = positionals;
|
|
1213
|
+
if (collection === undefined || more.length > 0) {
|
|
1214
|
+
throw misuse(`Say which collection: console plugin ${client.key} records <collection> [<id>] [--where <field>=<value>]... [--search <words>] [--order <field>[:desc]] [--limit <n>] [--after <cursor>] [--archived] [--versions]`);
|
|
1215
|
+
}
|
|
1216
|
+
if (id !== undefined) {
|
|
1217
|
+
if (on(values, "versions")) {
|
|
1218
|
+
const versions = await client.recordVersions(collection, id);
|
|
1219
|
+
answer(io, session, versions, () => formatPluginRecordVersions(versions));
|
|
1220
|
+
return EXIT.ok;
|
|
1221
|
+
}
|
|
1222
|
+
const record = await client.record(collection, id);
|
|
1223
|
+
answer(io, session, record, () => formatPluginRecord(record));
|
|
1224
|
+
return EXIT.ok;
|
|
1225
|
+
}
|
|
1226
|
+
if (on(values, "versions")) {
|
|
1227
|
+
throw misuse("--versions goes with a record's id: console plugin <key> records <collection> <id> --versions");
|
|
1228
|
+
}
|
|
1229
|
+
const where = {};
|
|
1230
|
+
for (const pair of texts(values, "where")) {
|
|
1231
|
+
const equals = pair.indexOf("=");
|
|
1232
|
+
if (equals <= 0) {
|
|
1233
|
+
throw misuse(`Not a condition, as <field>=<value>: ${pair}`);
|
|
1234
|
+
}
|
|
1235
|
+
where[pair.slice(0, equals)] = whereValueOf(pair.slice(equals + 1));
|
|
1236
|
+
}
|
|
1237
|
+
const order = text(values, "order");
|
|
1238
|
+
const page = await client.records(collection, {
|
|
1239
|
+
...(Object.keys(where).length === 0 ? {} : { where }),
|
|
1240
|
+
q: text(values, "search"),
|
|
1241
|
+
order: order === undefined ? undefined : order.endsWith(":desc") ? `-${order.slice(0, -5)}` : order,
|
|
1242
|
+
limit: countOf(text(values, "limit"), "limit", { least: 1, most: PLUGIN_LIST_LIMIT }),
|
|
1243
|
+
after: text(values, "after"),
|
|
1244
|
+
project,
|
|
1245
|
+
archived: on(values, "archived") ? true : undefined,
|
|
1246
|
+
});
|
|
1247
|
+
answer(io, session, page, () => formatPluginRecords(page));
|
|
1248
|
+
return EXIT.ok;
|
|
1249
|
+
}
|
|
1250
|
+
/** The values `--set <key>=<value>` give, each read as its setting's type; an empty value clears one. */
|
|
1251
|
+
function settingValuesOf(pairs, detail) {
|
|
1252
|
+
const values = {};
|
|
1253
|
+
const read = (type, word, key) => {
|
|
1254
|
+
switch (type) {
|
|
1255
|
+
case "number": {
|
|
1256
|
+
const value = Number(word);
|
|
1257
|
+
if (!Number.isFinite(value) || word.trim().length === 0) {
|
|
1258
|
+
throw misuse(`Not a number for the setting ${key}: ${word}`);
|
|
1259
|
+
}
|
|
1260
|
+
return value;
|
|
1261
|
+
}
|
|
1262
|
+
case "boolean":
|
|
1263
|
+
if (word !== "true" && word !== "false") {
|
|
1264
|
+
throw misuse(`The setting ${key} is true or false: ${word}`);
|
|
1265
|
+
}
|
|
1266
|
+
return word === "true";
|
|
1267
|
+
default:
|
|
1268
|
+
return word;
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
for (const pair of pairs) {
|
|
1272
|
+
const equals = pair.indexOf("=");
|
|
1273
|
+
if (equals <= 0) {
|
|
1274
|
+
throw misuse(`Not a setting, as <key>=<value>: ${pair}`);
|
|
1275
|
+
}
|
|
1276
|
+
const key = pair.slice(0, equals);
|
|
1277
|
+
const word = pair.slice(equals + 1);
|
|
1278
|
+
const setting = detail.settings.find((candidate) => candidate.key === key);
|
|
1279
|
+
if (setting === undefined) {
|
|
1280
|
+
throw misuse(`Not a setting of ${detail.key}: ${key}. Its settings: ${detail.settings.map((candidate) => candidate.key).join(", ") || "none"}`);
|
|
1281
|
+
}
|
|
1282
|
+
if (setting.type === "secret") {
|
|
1283
|
+
throw misuse(`${key} is a secret: console plugin ${detail.key} secret ${key} reads it from standard input.`);
|
|
1284
|
+
}
|
|
1285
|
+
if (word.length === 0) {
|
|
1286
|
+
values[key] = null;
|
|
1287
|
+
}
|
|
1288
|
+
else if (setting.type === "list") {
|
|
1289
|
+
values[key] = word
|
|
1290
|
+
.split(",")
|
|
1291
|
+
.map((item) => item.trim())
|
|
1292
|
+
.filter((item) => item.length > 0)
|
|
1293
|
+
.map((item) => read(setting.of ?? "text", item, key));
|
|
1294
|
+
}
|
|
1295
|
+
else {
|
|
1296
|
+
values[key] = read(setting.type, word, key);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
return values;
|
|
1300
|
+
}
|
|
1301
|
+
/** The capabilities a plugin asks, each with what it opens and what it risks, for the administrator to read before consenting. */
|
|
1302
|
+
function capabilityLines(capabilities) {
|
|
1303
|
+
return capabilities.map((capability) => {
|
|
1304
|
+
const words = capabilityWords(capability);
|
|
1305
|
+
return words === undefined
|
|
1306
|
+
? ` ${capability}: a capability this command does not know`
|
|
1307
|
+
: ` ${capability}: opens ${words.opens}; a malicious plugin reaches ${words.reach}`;
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
/** Enables a plugin, with consent to every capability it asks, named again; an administrator's. */
|
|
1311
|
+
async function pluginEnable(io, session, client, rest) {
|
|
1312
|
+
const { values, positionals } = parse(rest, {
|
|
1313
|
+
consent: { type: "string", multiple: true },
|
|
1314
|
+
board: { type: "string" },
|
|
1315
|
+
projects: { type: "string", multiple: true },
|
|
1316
|
+
set: { type: "string", multiple: true },
|
|
1317
|
+
});
|
|
1318
|
+
if (positionals.length > 0) {
|
|
1319
|
+
throw misuse(`Nothing follows enable but its flags: console plugin ${client.key} enable [--consent <capability>,...] [--board new|<key>] [--projects all|<key>,...] [--set <key>=<value>]...`);
|
|
1320
|
+
}
|
|
1321
|
+
const [detail, me] = await Promise.all([client.get(), session.client.me()]);
|
|
1322
|
+
const asked = detail.capabilities;
|
|
1323
|
+
const consent = texts(values, "consent")
|
|
1324
|
+
.flatMap((value) => value.split(","))
|
|
1325
|
+
.map((capability) => capability.trim())
|
|
1326
|
+
.filter((capability) => capability.length > 0);
|
|
1327
|
+
if (asked.length > 0 && !session.json) {
|
|
1328
|
+
io.stdout(`${detail.key} asks ${asked.length === 1 ? "one capability" : `${asked.length} capabilities`}:\n${capabilityLines(asked).join("\n")}\n`);
|
|
1329
|
+
}
|
|
1330
|
+
const missing = asked.filter((capability) => !consent.includes(capability));
|
|
1331
|
+
if (missing.length > 0) {
|
|
1332
|
+
throw misuse(`Enabling ${detail.key} needs consent to every capability it asks, named again: --consent ${asked.join(",")}. Not named: ${missing.join(", ")}.`);
|
|
1333
|
+
}
|
|
1334
|
+
const boardFlag = text(values, "board");
|
|
1335
|
+
let board = null;
|
|
1336
|
+
if (boardFlag === "new") {
|
|
1337
|
+
if (detail.boardDefaults === null) {
|
|
1338
|
+
throw misuse(`${detail.key} names no defaults for a board project: choose one with --board <key>.`);
|
|
1339
|
+
}
|
|
1340
|
+
board = {
|
|
1341
|
+
create: {
|
|
1342
|
+
key: detail.boardDefaults.key,
|
|
1343
|
+
name: localized(detail.boardDefaults.name, me.language),
|
|
1344
|
+
description: localized(detail.boardDefaults.description, me.language),
|
|
1345
|
+
},
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
else if (boardFlag !== undefined) {
|
|
1349
|
+
board = { project: boardFlag };
|
|
1350
|
+
}
|
|
1351
|
+
const named = texts(values, "projects")
|
|
1352
|
+
.flatMap((value) => value.split(","))
|
|
1353
|
+
.map((key) => key.trim())
|
|
1354
|
+
.filter((key) => key.length > 0);
|
|
1355
|
+
const settings = settingValuesOf(texts(values, "set"), detail);
|
|
1356
|
+
const enabled = await client.enable({
|
|
1357
|
+
consent,
|
|
1358
|
+
board,
|
|
1359
|
+
projects: named.length === 0 || named.includes("all") ? "all" : named,
|
|
1360
|
+
...(Object.keys(settings).length === 0 ? {} : { settings }),
|
|
1361
|
+
});
|
|
1362
|
+
answer(io, session, enabled, () => [
|
|
1363
|
+
`Enabled ${enabled.key}${enabled.board === null ? "" : `, with the board project ${enabled.board.key}`}; on ${enabled.projects === "all" || enabled.projects === null ? "every project" : enabled.projects.join(", ") || "no project"}.`,
|
|
1364
|
+
...(enabled.settings.length === 0
|
|
1365
|
+
? []
|
|
1366
|
+
: [
|
|
1367
|
+
"Settings:",
|
|
1368
|
+
...enabled.settings.map((setting) => formatPluginSettingLine(setting, me.language)),
|
|
1369
|
+
]),
|
|
1370
|
+
].join("\n"));
|
|
1371
|
+
return EXIT.ok;
|
|
1372
|
+
}
|
|
1373
|
+
/** The settings as they stand, or changed as `--set` says; an administrator's. */
|
|
1374
|
+
async function pluginSettings(io, session, client, rest) {
|
|
1375
|
+
const { values, positionals } = parse(rest, { set: { type: "string", multiple: true } });
|
|
1376
|
+
if (positionals.length > 0) {
|
|
1377
|
+
throw misuse(`Nothing follows settings but --set: console plugin ${client.key} settings [--set <key>=<value>]...`);
|
|
1378
|
+
}
|
|
1379
|
+
const [detail, me] = await Promise.all([client.get(), session.client.me()]);
|
|
1380
|
+
const sets = texts(values, "set");
|
|
1381
|
+
if (sets.length === 0) {
|
|
1382
|
+
answer(io, session, detail.settings, () => detail.settings.length === 0
|
|
1383
|
+
? `${detail.key} has no settings.`
|
|
1384
|
+
: [
|
|
1385
|
+
`Settings of ${detail.key}:`,
|
|
1386
|
+
...detail.settings.map((setting) => formatPluginSettingLine(setting, me.language)),
|
|
1387
|
+
].join("\n"));
|
|
1388
|
+
return EXIT.ok;
|
|
1389
|
+
}
|
|
1390
|
+
const changed = await client.updateSettings({ values: settingValuesOf(sets, detail) });
|
|
1391
|
+
answer(io, session, changed.settings, () => [
|
|
1392
|
+
`Changed the settings of ${changed.key}:`,
|
|
1393
|
+
...changed.settings.map((setting) => formatPluginSettingLine(setting, me.language)),
|
|
1394
|
+
].join("\n"));
|
|
1395
|
+
return EXIT.ok;
|
|
1396
|
+
}
|
|
1397
|
+
/** Seals a secret of the plugin, read from standard input and shown never; or takes it away. */
|
|
1398
|
+
async function pluginSecret(io, session, client, rest) {
|
|
1399
|
+
const { values, positionals } = parse(rest, { remove: { type: "boolean" } });
|
|
1400
|
+
const name = theOne(positionals, `Say which secret: console plugin ${client.key} secret <name> [--remove]; the value is read from standard input.`);
|
|
1401
|
+
if (on(values, "remove")) {
|
|
1402
|
+
await client.removeSecret(name);
|
|
1403
|
+
answer(io, session, { name, set: false }, () => `Removed the secret ${name} of ${client.key}.`);
|
|
1404
|
+
return EXIT.ok;
|
|
1405
|
+
}
|
|
1406
|
+
const value = (await io.readStdin()).replace(/\r?\n$/, "");
|
|
1407
|
+
if (value.length === 0) {
|
|
1408
|
+
throw misuse(`Pipe the value in: console plugin ${client.key} secret ${name} < file. It is sealed by the console and shown never.`);
|
|
1409
|
+
}
|
|
1410
|
+
await client.setSecret(name, value);
|
|
1411
|
+
answer(io, session, { name, set: true }, () => `Set the secret ${name} of ${client.key}: sealed, and shown by nothing from now on.`);
|
|
1412
|
+
return EXIT.ok;
|
|
1413
|
+
}
|
|
1414
|
+
/** Grants a role of the plugin to a person, to the members of a project, or to everyone; an administrator's. */
|
|
1415
|
+
async function pluginGrant(io, session, client, rest, projectKey) {
|
|
1416
|
+
const { values, positionals } = parse(rest, {
|
|
1417
|
+
person: { type: "string" },
|
|
1418
|
+
everyone: { type: "boolean" },
|
|
1419
|
+
});
|
|
1420
|
+
const usage = `console plugin ${client.key} grant <role> --person <login> | --project <key> | --everyone`;
|
|
1421
|
+
const role = theOne(positionals, `Say which role: ${usage}`);
|
|
1422
|
+
const login = text(values, "person");
|
|
1423
|
+
const everyone = on(values, "everyone");
|
|
1424
|
+
if ([login !== undefined, projectKey !== undefined, everyone].filter(Boolean).length !== 1) {
|
|
1425
|
+
throw misuse(`Say whom, one of the three: ${usage}`);
|
|
1426
|
+
}
|
|
1427
|
+
let input;
|
|
1428
|
+
if (login !== undefined) {
|
|
1429
|
+
const person = await session.client.findPerson(login);
|
|
1430
|
+
if (person === undefined) {
|
|
1431
|
+
throw failed(`No person has the login ${login} among those you may see.`);
|
|
1432
|
+
}
|
|
1433
|
+
input = { role, person: person.id };
|
|
1434
|
+
}
|
|
1435
|
+
else if (projectKey !== undefined) {
|
|
1436
|
+
input = { role, project: projectKey };
|
|
1437
|
+
}
|
|
1438
|
+
else {
|
|
1439
|
+
input = { role, everyone: true };
|
|
1440
|
+
}
|
|
1441
|
+
const granted = await client.grant(input);
|
|
1442
|
+
answer(io, session, granted, () => `Granted: ${formatPluginGrantLine(granted)}`);
|
|
1443
|
+
return EXIT.ok;
|
|
1444
|
+
}
|
|
940
1445
|
/** A version number given as a flag, or nothing; anything else is not understood. */
|
|
941
1446
|
function versionOf(given) {
|
|
942
1447
|
if (given === undefined) {
|
|
@@ -1018,9 +1523,10 @@ const rules = async (args, io) => {
|
|
|
1018
1523
|
? {
|
|
1019
1524
|
workspace: forPerson(found.workspace, me.language),
|
|
1020
1525
|
project: forPerson(found.project, me.language),
|
|
1526
|
+
...(found.plugins === undefined ? {} : { plugins: found.plugins }),
|
|
1021
1527
|
}
|
|
1022
1528
|
: forPerson(found, me.language);
|
|
1023
|
-
answer(io, session, read, () => formatRules(read));
|
|
1529
|
+
answer(io, session, read, () => formatRules(read, me.language));
|
|
1024
1530
|
return EXIT.ok;
|
|
1025
1531
|
};
|
|
1026
1532
|
/**
|
|
@@ -1210,6 +1716,22 @@ const comment = async (args, io) => {
|
|
|
1210
1716
|
if (body.trim().length === 0 && paths.length === 0) {
|
|
1211
1717
|
throw misuse(`Say something, or attach a file. ${ask}`);
|
|
1212
1718
|
}
|
|
1719
|
+
const files = await attachmentsOf(io, paths);
|
|
1720
|
+
const session = await open(io, values);
|
|
1721
|
+
const project = await session.project();
|
|
1722
|
+
const task = await project.task(ref);
|
|
1723
|
+
const written = await project.comment(task.id, { body, files });
|
|
1724
|
+
answer(io, session, written, () => [
|
|
1725
|
+
`Commented on #${task.number} ${task.title}${written.files.length === 0 ? "" : `, with ${written.files.length} file${written.files.length === 1 ? "" : "s"}`}.`,
|
|
1726
|
+
formatComment(written),
|
|
1727
|
+
].join("\n"));
|
|
1728
|
+
return EXIT.ok;
|
|
1729
|
+
};
|
|
1730
|
+
/**
|
|
1731
|
+
* The files named by --attach, read from this machine: each called by its name, without the
|
|
1732
|
+
* path, and typed by its extension where the extension says.
|
|
1733
|
+
*/
|
|
1734
|
+
async function attachmentsOf(io, paths) {
|
|
1213
1735
|
const files = [];
|
|
1214
1736
|
for (const path of paths) {
|
|
1215
1737
|
let bytes;
|
|
@@ -1223,20 +1745,12 @@ const comment = async (args, io) => {
|
|
|
1223
1745
|
const contentType = CONTENT_TYPES[extname(name).toLowerCase()];
|
|
1224
1746
|
files.push({ name, bytes, ...(contentType === undefined ? {} : { contentType }) });
|
|
1225
1747
|
}
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
const task = await project.task(ref);
|
|
1229
|
-
const written = await project.comment(task.id, { body, files });
|
|
1230
|
-
answer(io, session, written, () => [
|
|
1231
|
-
`Commented on #${task.number} ${task.title}${written.files.length === 0 ? "" : `, with ${written.files.length} file${written.files.length === 1 ? "" : "s"}`}.`,
|
|
1232
|
-
formatComment(written),
|
|
1233
|
-
].join("\n"));
|
|
1234
|
-
return EXIT.ok;
|
|
1235
|
-
};
|
|
1748
|
+
return files;
|
|
1749
|
+
}
|
|
1236
1750
|
/**
|
|
1237
1751
|
* A file of the project, read from the console and written on this machine: one a run handed
|
|
1238
|
-
* in, one put in a comment, or one attached to a page, by its id, so that an
|
|
1239
|
-
* a picture a person put on the board.
|
|
1752
|
+
* in, one put in a comment or on a decision, or one attached to a page, by its id, so that an
|
|
1753
|
+
* agent can look at a picture a person put on the board.
|
|
1240
1754
|
*/
|
|
1241
1755
|
const file = async (args, io) => {
|
|
1242
1756
|
const { values, positionals } = parse(args, { out: { type: "string" } });
|
|
@@ -1294,6 +1808,7 @@ const take = async (args, io) => {
|
|
|
1294
1808
|
const rules = {
|
|
1295
1809
|
workspace: forPerson(ruling.workspace, me.language),
|
|
1296
1810
|
project: forPerson(ruling.project, me.language),
|
|
1811
|
+
...(ruling.plugins === undefined ? {} : { plugins: ruling.plugins }),
|
|
1297
1812
|
};
|
|
1298
1813
|
answer(io, session, { run, task: taken, comments: comments.items, rules }, () => [
|
|
1299
1814
|
`Run ${run.id} began on #${found.number} ${found.title}, as "${run.agent}" for ${run.person.login}. The task is in progress and theirs.${tookOver}${elsewhere}`,
|
|
@@ -1303,7 +1818,7 @@ const take = async (args, io) => {
|
|
|
1303
1818
|
"",
|
|
1304
1819
|
formatTask(taken, undefined, undefined, comments.items),
|
|
1305
1820
|
"",
|
|
1306
|
-
formatRules(rules),
|
|
1821
|
+
formatRules(rules, me.language),
|
|
1307
1822
|
"",
|
|
1308
1823
|
'Say how it goes: console report "<what you found or did>". Hand in what you make: console hand-in <https url | file path>.',
|
|
1309
1824
|
'Ask when a person must decide: console ask "<question>" --option "..." --option "...". A word for the task\'s people that is not a report: console comment <number> "<markdown>". End with: console finish --summary "<what was done, what is left>".',
|
|
@@ -1395,6 +1910,7 @@ const ask = async (args, io) => {
|
|
|
1395
1910
|
run: { type: "string" },
|
|
1396
1911
|
wait: { type: "string" },
|
|
1397
1912
|
to: { type: "string" },
|
|
1913
|
+
attach: { type: "string", multiple: true },
|
|
1398
1914
|
});
|
|
1399
1915
|
const question = theOne(positionals, 'Say the question: console ask "<question>" --option "<label>" --option "<label>"');
|
|
1400
1916
|
const to = text(values, "to");
|
|
@@ -1407,6 +1923,8 @@ const ask = async (args, io) => {
|
|
|
1407
1923
|
}
|
|
1408
1924
|
const seconds = secondsOf(text(values, "wait"), DEFAULT_ASK_WAIT_SECONDS);
|
|
1409
1925
|
const body = await bodyOf(io, values, text(values, "body"));
|
|
1926
|
+
// The files put on the decision (ADR-0055): a picture of what is to be chosen between, for one.
|
|
1927
|
+
const files = await attachmentsOf(io, texts(values, "attach"));
|
|
1410
1928
|
const session = await open(io, values);
|
|
1411
1929
|
const project = await session.project();
|
|
1412
1930
|
const runId = await currentRun(project, values);
|
|
@@ -1418,11 +1936,12 @@ const ask = async (args, io) => {
|
|
|
1418
1936
|
runId,
|
|
1419
1937
|
...(body === undefined ? {} : { body }),
|
|
1420
1938
|
...(typeof deciderId === "string" ? { deciderId } : {}),
|
|
1421
|
-
});
|
|
1939
|
+
}, files);
|
|
1940
|
+
const withFiles = files.length === 0 ? "" : `, with ${files.length} file${files.length === 1 ? "" : "s"}`;
|
|
1422
1941
|
// The id is printed before any waiting, so that a wait cut short loses nothing.
|
|
1423
1942
|
io.stdout(session.json
|
|
1424
1943
|
? `${JSON.stringify(raised, null, 2)}\n`
|
|
1425
|
-
: `Decision ${raised.id} raised on run ${runId}; the run waits for ${raised.decider.login}.\ndecision id: ${raised.id}\n${formatOptions(raised)}\n`);
|
|
1944
|
+
: `Decision ${raised.id} raised on run ${runId}${withFiles}; the run waits for ${raised.decider.login}.\ndecision id: ${raised.id}\n${formatOptions(raised)}\n`);
|
|
1426
1945
|
return settle(io, session, project, raised, seconds, true);
|
|
1427
1946
|
};
|
|
1428
1947
|
/**
|
|
@@ -2294,6 +2813,8 @@ const COMMANDS = {
|
|
|
2294
2813
|
skills,
|
|
2295
2814
|
skill,
|
|
2296
2815
|
rules,
|
|
2816
|
+
plugins,
|
|
2817
|
+
plugin,
|
|
2297
2818
|
people,
|
|
2298
2819
|
person,
|
|
2299
2820
|
workspace,
|