@skillcdn/console 0.4.3 → 0.4.4
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 +4 -4
- package/dist/api.d.ts +3 -3
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +3 -3
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +95 -20
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +4 -2
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +32 -9
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +17 -14
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/plugin-check.d.ts +5 -1
- package/dist/cli/plugin-check.d.ts.map +1 -1
- package/dist/cli/plugin-check.js +23 -4
- package/dist/cli/plugin-check.js.map +1 -1
- package/dist/cli/plugin-cli.d.ts.map +1 -1
- package/dist/cli/plugin-cli.js +10 -1
- package/dist/cli/plugin-cli.js.map +1 -1
- package/dist/cli/plugin-init.d.ts +2 -0
- package/dist/cli/plugin-init.d.ts.map +1 -1
- package/dist/cli/plugin-init.js +6 -0
- package/dist/cli/plugin-init.js.map +1 -1
- package/dist/client.d.ts +4 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +2 -0
- package/dist/client.js.map +1 -1
- package/dist/compat.js +1 -1
- package/dist/components/board.d.ts +8 -0
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +3 -3
- package/dist/components/board.js.map +1 -1
- package/dist/components/decision-form.d.ts +6 -0
- package/dist/components/decision-form.d.ts.map +1 -1
- package/dist/components/decision-form.js +23 -5
- package/dist/components/decision-form.js.map +1 -1
- package/dist/components/decision-list.d.ts +5 -1
- package/dist/components/decision-list.d.ts.map +1 -1
- package/dist/components/decision-list.js +25 -5
- package/dist/components/decision-list.js.map +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +7 -1
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/goals.d.ts.map +1 -1
- package/dist/components/goals.js +2 -2
- package/dist/components/goals.js.map +1 -1
- package/dist/components/home.d.ts +35 -0
- package/dist/components/home.d.ts.map +1 -0
- package/dist/components/home.js +326 -0
- package/dist/components/home.js.map +1 -0
- package/dist/components/pack-connect.d.ts.map +1 -1
- package/dist/components/pack-connect.js +5 -4
- package/dist/components/pack-connect.js.map +1 -1
- package/dist/components/people.d.ts +2 -0
- package/dist/components/people.d.ts.map +1 -1
- package/dist/components/people.js +15 -2
- package/dist/components/people.js.map +1 -1
- package/dist/components/plugin-frame.d.ts +3 -12
- package/dist/components/plugin-frame.d.ts.map +1 -1
- package/dist/components/plugin-frame.js +53 -20
- package/dist/components/plugin-frame.js.map +1 -1
- package/dist/components/plugin-settings.d.ts.map +1 -1
- package/dist/components/plugin-settings.js +151 -26
- package/dist/components/plugin-settings.js.map +1 -1
- package/dist/components/projects.d.ts +0 -8
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +1 -13
- package/dist/components/projects.js.map +1 -1
- package/dist/components/task-view.d.ts.map +1 -1
- package/dist/components/task-view.js +2 -1
- package/dist/components/task-view.js.map +1 -1
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +5 -2
- package/dist/components/ui.js.map +1 -1
- package/dist/connect-page.d.ts.map +1 -1
- package/dist/connect-page.js +5 -6
- package/dist/connect-page.js.map +1 -1
- package/dist/console.css +761 -191
- package/dist/console.d.ts +10 -3
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +33 -13
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +4 -2
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +5 -0
- package/dist/data.js.map +1 -1
- package/dist/docs-page.d.ts.map +1 -1
- package/dist/docs-page.js +6 -5
- package/dist/docs-page.js.map +1 -1
- package/dist/home-tiles.d.ts +92 -0
- package/dist/home-tiles.d.ts.map +1 -0
- package/dist/home-tiles.js +294 -0
- package/dist/home-tiles.js.map +1 -0
- package/dist/i18n/en.d.ts +174 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +175 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/index.d.ts +4 -2
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/index.js +10 -11
- package/dist/i18n/index.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +170 -3
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +6 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +6 -0
- package/dist/limits.js.map +1 -1
- package/dist/plugin/bridge.d.ts +44 -3
- package/dist/plugin/bridge.d.ts.map +1 -1
- package/dist/plugin/bridge.js +26 -0
- package/dist/plugin/bridge.js.map +1 -1
- package/dist/plugin/capabilities.d.ts +7 -1
- package/dist/plugin/capabilities.d.ts.map +1 -1
- package/dist/plugin/capabilities.js +14 -1
- package/dist/plugin/capabilities.js.map +1 -1
- package/dist/plugin/context.d.ts +18 -1
- package/dist/plugin/context.d.ts.map +1 -1
- package/dist/plugin/define.d.ts +1 -0
- package/dist/plugin/define.d.ts.map +1 -1
- package/dist/plugin/define.js +1 -1
- package/dist/plugin/define.js.map +1 -1
- package/dist/plugin/limits.d.ts +5 -2
- package/dist/plugin/limits.d.ts.map +1 -1
- package/dist/plugin/limits.js +5 -2
- package/dist/plugin/limits.js.map +1 -1
- package/dist/plugin/manifest.d.ts +83 -6
- package/dist/plugin/manifest.d.ts.map +1 -1
- package/dist/plugin/manifest.js +111 -7
- package/dist/plugin/manifest.js.map +1 -1
- package/dist/plugin/rest-schemas.d.ts +76 -0
- package/dist/plugin/rest-schemas.d.ts.map +1 -1
- package/dist/plugin/rest-schemas.js +23 -2
- package/dist/plugin/rest-schemas.js.map +1 -1
- package/dist/plugin/sandbox.d.ts +4 -3
- package/dist/plugin/sandbox.d.ts.map +1 -1
- package/dist/plugin/sandbox.js +33 -6
- package/dist/plugin/sandbox.js.map +1 -1
- package/dist/plugin/semver.d.ts +26 -0
- package/dist/plugin/semver.d.ts.map +1 -0
- package/dist/plugin/semver.js +76 -0
- package/dist/plugin/semver.js.map +1 -0
- package/dist/plugin/testing.d.ts.map +1 -1
- package/dist/plugin/testing.js +2 -1
- package/dist/plugin/testing.js.map +1 -1
- package/dist/plugin/web.d.ts +10 -1
- package/dist/plugin/web.d.ts.map +1 -1
- package/dist/plugin/web.js +34 -0
- package/dist/plugin/web.js.map +1 -1
- package/dist/plugin.d.ts +3 -2
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +3 -2
- package/dist/plugin.js.map +1 -1
- package/dist/rules.d.ts +2 -2
- package/dist/rules.js +3 -3
- package/dist/schemas.d.ts +497 -2
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +96 -9
- package/dist/schemas.js.map +1 -1
- package/dist/use-action.d.ts +8 -2
- package/dist/use-action.d.ts.map +1 -1
- package/dist/use-action.js +13 -6
- package/dist/use-action.js.map +1 -1
- package/dist/vocabulary.d.ts +3 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +3 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +4 -3
- package/docs/specs/plugins.md +46 -33
- package/docs/specs/rest.md +16 -15
- package/package.json +1 -1
- package/skills/building-a-plugin/SKILL.md +8 -7
- package/skills/planning-the-board/references/setting-up.md +2 -1
- package/skills/working-the-board/SKILL.md +3 -3
- package/src/api.ts +6 -0
- package/src/cli/cli.ts +124 -19
- package/src/cli/format.ts +42 -9
- package/src/cli/help.ts +17 -13
- package/src/cli/plugin-check.ts +41 -2
- package/src/cli/plugin-cli.ts +10 -1
- package/src/cli/plugin-init.ts +8 -0
- package/src/client.ts +8 -0
- package/src/compat.ts +1 -1
- package/src/components/board.tsx +3 -3
- package/src/components/decision-form.tsx +75 -19
- package/src/components/decision-list.tsx +45 -13
- package/src/components/event-feed.tsx +7 -1
- package/src/components/goals.tsx +4 -1
- package/src/components/home.tsx +615 -0
- package/src/components/pack-connect.tsx +5 -4
- package/src/components/people.tsx +136 -54
- package/src/components/plugin-frame.tsx +62 -63
- package/src/components/plugin-settings.tsx +428 -87
- package/src/components/projects.tsx +0 -55
- package/src/components/task-view.tsx +7 -0
- package/src/components/ui.tsx +6 -2
- package/src/connect-page.tsx +5 -6
- package/src/console.tsx +80 -30
- package/src/data.ts +9 -2
- package/src/docs-page.tsx +6 -5
- package/src/home-tiles.ts +438 -0
- package/src/i18n/en.ts +178 -0
- package/src/i18n/index.ts +17 -14
- package/src/i18n/ko.ts +174 -3
- package/src/index.ts +7 -5
- package/src/limits.ts +6 -0
- package/src/plugin/bridge.ts +77 -1
- package/src/plugin/capabilities.ts +17 -1
- package/src/plugin/context.ts +18 -1
- package/src/plugin/define.ts +4 -1
- package/src/plugin/limits.ts +5 -2
- package/src/plugin/manifest.ts +154 -10
- package/src/plugin/rest-schemas.ts +27 -1
- package/src/plugin/sandbox.ts +36 -8
- package/src/plugin/semver.ts +89 -0
- package/src/plugin/testing.ts +2 -1
- package/src/plugin/web.ts +49 -0
- package/src/plugin.ts +18 -0
- package/src/rules.ts +3 -3
- package/src/schemas.ts +118 -10
- package/src/styles/console.css +761 -191
- package/src/use-action.ts +16 -7
- package/src/vocabulary.ts +4 -0
- package/template/plugin/console-plugin.json +3 -1
package/src/cli/cli.ts
CHANGED
|
@@ -21,7 +21,13 @@ import {
|
|
|
21
21
|
formatVersion as versionText,
|
|
22
22
|
} from "../compat.js";
|
|
23
23
|
import { addDays, dayOf as dayOfDate, firstDayOfWeek, isDay, weekOf } from "../dates.js";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
DEFAULT_PAGE_LIMIT,
|
|
26
|
+
LIST_LIMIT,
|
|
27
|
+
MAX_DECIDERS,
|
|
28
|
+
MAX_OPTIONS,
|
|
29
|
+
MIN_OPTIONS,
|
|
30
|
+
} from "../limits.js";
|
|
25
31
|
import { capabilityWords } from "../plugin/capabilities.js";
|
|
26
32
|
import { PLUGIN_LIST_LIMIT } from "../plugin/limits.js";
|
|
27
33
|
import { fillRules, RULES_PATH, RULES_TITLE } from "../rules.js";
|
|
@@ -77,6 +83,7 @@ import {
|
|
|
77
83
|
} from "./credentials.js";
|
|
78
84
|
import { type DirectorySettings, type DirectoryStore, THE_SENTENCE } from "./directory.js";
|
|
79
85
|
import {
|
|
86
|
+
deciderLogins,
|
|
80
87
|
formatAnswer,
|
|
81
88
|
formatCategoryLine,
|
|
82
89
|
formatComment,
|
|
@@ -1526,6 +1533,8 @@ const plugin: Command = async (args, io) => {
|
|
|
1526
1533
|
);
|
|
1527
1534
|
return EXIT.ok;
|
|
1528
1535
|
}
|
|
1536
|
+
case "capabilities":
|
|
1537
|
+
return pluginCapabilities(io, session, client, rest);
|
|
1529
1538
|
case "settings":
|
|
1530
1539
|
return pluginSettings(io, session, client, rest);
|
|
1531
1540
|
case "secret":
|
|
@@ -1787,7 +1796,60 @@ function capabilityLines(capabilities: readonly string[]): string[] {
|
|
|
1787
1796
|
});
|
|
1788
1797
|
}
|
|
1789
1798
|
|
|
1790
|
-
/**
|
|
1799
|
+
/** The capabilities `--consent` names, given again or with commas between. */
|
|
1800
|
+
function capabilitiesNamed(values: Values): string[] {
|
|
1801
|
+
return texts(values, "consent")
|
|
1802
|
+
.flatMap((value) => value.split(","))
|
|
1803
|
+
.map((capability) => capability.trim())
|
|
1804
|
+
.filter((capability) => capability.length > 0);
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
/** What a plugin requires, and which of what it can go without it holds, as `capabilities` prints it. */
|
|
1808
|
+
function capabilitiesHeld(detail: RestPluginDetail): string {
|
|
1809
|
+
const held = detail.consent ?? [];
|
|
1810
|
+
const optional = detail.optionalCapabilities ?? [];
|
|
1811
|
+
const named = (capabilities: readonly string[]) =>
|
|
1812
|
+
capabilities.length === 0 ? "none" : capabilities.join(", ");
|
|
1813
|
+
return [
|
|
1814
|
+
`Capabilities of ${detail.key}:`,
|
|
1815
|
+
` required: ${named(detail.capabilities)}`,
|
|
1816
|
+
` optional, on: ${named(optional.filter((capability) => held.includes(capability)))}`,
|
|
1817
|
+
` optional, off: ${named(optional.filter((capability) => !held.includes(capability)))}`,
|
|
1818
|
+
].join("\n");
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
/**
|
|
1822
|
+
* What a plugin holds; with `--consent`, which of the optional capabilities it holds from now on,
|
|
1823
|
+
* `none` for none of them. An administrator's.
|
|
1824
|
+
*/
|
|
1825
|
+
async function pluginCapabilities(
|
|
1826
|
+
io: CliIo,
|
|
1827
|
+
session: Session,
|
|
1828
|
+
client: PluginClient,
|
|
1829
|
+
rest: readonly string[],
|
|
1830
|
+
): Promise<number> {
|
|
1831
|
+
const { values, positionals } = parse(rest, { consent: { type: "string", multiple: true } });
|
|
1832
|
+
if (positionals.length > 0) {
|
|
1833
|
+
throw misuse(
|
|
1834
|
+
`Nothing follows capabilities but --consent: console plugin ${client.key} capabilities [--consent <capability>,...|none]`,
|
|
1835
|
+
);
|
|
1836
|
+
}
|
|
1837
|
+
const detail = await client.get();
|
|
1838
|
+
const named = capabilitiesNamed(values);
|
|
1839
|
+
if (named.length === 0) {
|
|
1840
|
+
answer(io, session, detail, () => capabilitiesHeld(detail));
|
|
1841
|
+
return EXIT.ok;
|
|
1842
|
+
}
|
|
1843
|
+
const chosen = named.filter((capability) => capability !== "none");
|
|
1844
|
+
const changed = await client.updateConsent([
|
|
1845
|
+
...detail.capabilities,
|
|
1846
|
+
...chosen.filter((capability) => !detail.capabilities.includes(capability)),
|
|
1847
|
+
]);
|
|
1848
|
+
answer(io, session, changed, () => capabilitiesHeld(changed));
|
|
1849
|
+
return EXIT.ok;
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
/** Enables a plugin, with consent to every capability it requires, named again; an administrator's. */
|
|
1791
1853
|
async function pluginEnable(
|
|
1792
1854
|
io: CliIo,
|
|
1793
1855
|
session: Session,
|
|
@@ -1807,19 +1869,22 @@ async function pluginEnable(
|
|
|
1807
1869
|
}
|
|
1808
1870
|
const [detail, me] = await Promise.all([client.get(), session.client.me()]);
|
|
1809
1871
|
const asked = detail.capabilities;
|
|
1810
|
-
const
|
|
1811
|
-
|
|
1812
|
-
.map((capability) => capability.trim())
|
|
1813
|
-
.filter((capability) => capability.length > 0);
|
|
1872
|
+
const optional = detail.optionalCapabilities ?? [];
|
|
1873
|
+
const consent = capabilitiesNamed(values);
|
|
1814
1874
|
if (asked.length > 0 && !session.json) {
|
|
1815
1875
|
io.stdout(
|
|
1816
|
-
`${detail.key}
|
|
1876
|
+
`${detail.key} requires ${asked.length === 1 ? "one capability" : `${asked.length} capabilities`}:\n${capabilityLines(asked).join("\n")}\n`,
|
|
1877
|
+
);
|
|
1878
|
+
}
|
|
1879
|
+
if (optional.length > 0 && !session.json) {
|
|
1880
|
+
io.stdout(
|
|
1881
|
+
`It can go without these, which it holds if --consent names them too:\n${capabilityLines(optional).join("\n")}\n`,
|
|
1817
1882
|
);
|
|
1818
1883
|
}
|
|
1819
1884
|
const missing = asked.filter((capability) => !consent.includes(capability));
|
|
1820
1885
|
if (missing.length > 0) {
|
|
1821
1886
|
throw misuse(
|
|
1822
|
-
`Enabling ${detail.key} needs consent to every capability it
|
|
1887
|
+
`Enabling ${detail.key} needs consent to every capability it requires, named again: --consent ${asked.join(",")}. Not named: ${missing.join(", ")}.`,
|
|
1823
1888
|
);
|
|
1824
1889
|
}
|
|
1825
1890
|
const boardFlag = text(values, "board");
|
|
@@ -2070,7 +2135,7 @@ const rules: Command = async (args, io) => {
|
|
|
2070
2135
|
|
|
2071
2136
|
/**
|
|
2072
2137
|
* The rules as they read for the person whose token the command holds: `{language}` filled
|
|
2073
|
-
* with the language they
|
|
2138
|
+
* with the language they read in, or with the task's while they have none.
|
|
2074
2139
|
*/
|
|
2075
2140
|
function forPerson<T extends { readonly body: string | null }>(
|
|
2076
2141
|
half: T,
|
|
@@ -2530,16 +2595,19 @@ const ask: Command = async (args, io) => {
|
|
|
2530
2595
|
file: { type: "string" },
|
|
2531
2596
|
run: { type: "string" },
|
|
2532
2597
|
wait: { type: "string" },
|
|
2533
|
-
to: { type: "string" },
|
|
2598
|
+
to: { type: "string", multiple: true },
|
|
2534
2599
|
attach: { type: "string", multiple: true },
|
|
2535
2600
|
});
|
|
2536
2601
|
const question = theOne(
|
|
2537
2602
|
positionals,
|
|
2538
2603
|
'Say the question: console ask "<question>" --option "<label>" --option "<label>"',
|
|
2539
2604
|
);
|
|
2540
|
-
const
|
|
2541
|
-
if (
|
|
2542
|
-
throw misuse("Say whom the decision is for: --to <login|me
|
|
2605
|
+
const tos = texts(values, "to");
|
|
2606
|
+
if (tos.includes("none")) {
|
|
2607
|
+
throw misuse("Say whom the decision is for: --to <login|me>, given again for several.");
|
|
2608
|
+
}
|
|
2609
|
+
if (tos.length > MAX_DECIDERS) {
|
|
2610
|
+
throw misuse(`Name up to ${MAX_DECIDERS} people with --to.`);
|
|
2543
2611
|
}
|
|
2544
2612
|
const options = texts(values, "option");
|
|
2545
2613
|
if (options.length < MIN_OPTIONS || options.length > MAX_OPTIONS) {
|
|
@@ -2552,15 +2620,26 @@ const ask: Command = async (args, io) => {
|
|
|
2552
2620
|
const session = await open(io, values);
|
|
2553
2621
|
const project = await session.project();
|
|
2554
2622
|
const runId = await currentRun(project, values);
|
|
2555
|
-
// Whom the decision is for (ADR-0042): the person the agent works for, unless --to
|
|
2556
|
-
|
|
2623
|
+
// Whom the decision is for (ADR-0042, ADR-0057): the person the agent works for, unless --to
|
|
2624
|
+
// names others of the project, each once; several, and any of them may answer.
|
|
2625
|
+
const deciderIds: string[] = [];
|
|
2626
|
+
for (const to of tos) {
|
|
2627
|
+
const id = await personIdOf(session, to);
|
|
2628
|
+
if (typeof id === "string" && !deciderIds.includes(id)) {
|
|
2629
|
+
deciderIds.push(id);
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2557
2632
|
const raised = await project.raiseDecision(
|
|
2558
2633
|
{
|
|
2559
2634
|
question,
|
|
2560
2635
|
options: options.map((label) => ({ label })),
|
|
2561
2636
|
runId,
|
|
2562
2637
|
...(body === undefined ? {} : { body }),
|
|
2563
|
-
...(
|
|
2638
|
+
...(deciderIds.length === 0
|
|
2639
|
+
? {}
|
|
2640
|
+
: deciderIds.length === 1
|
|
2641
|
+
? { deciderId: deciderIds[0] }
|
|
2642
|
+
: { deciderId: deciderIds[0], deciderIds }),
|
|
2564
2643
|
},
|
|
2565
2644
|
files,
|
|
2566
2645
|
);
|
|
@@ -2570,7 +2649,7 @@ const ask: Command = async (args, io) => {
|
|
|
2570
2649
|
io.stdout(
|
|
2571
2650
|
session.json
|
|
2572
2651
|
? `${JSON.stringify(raised, null, 2)}\n`
|
|
2573
|
-
: `Decision ${raised.id} raised on run ${runId}${withFiles}; the run waits for ${raised
|
|
2652
|
+
: `Decision ${raised.id} raised on run ${runId}${withFiles}; the run waits for ${deciderLogins(raised)}.\ndecision id: ${raised.id}\n${formatOptions(raised)}\n`,
|
|
2574
2653
|
);
|
|
2575
2654
|
return settle(io, session, project, raised, seconds, true);
|
|
2576
2655
|
};
|
|
@@ -3119,8 +3198,19 @@ const people: Command = async (args, io) => {
|
|
|
3119
3198
|
const { values } = parse(args, {}, false);
|
|
3120
3199
|
const session = await open(io, values);
|
|
3121
3200
|
const found = await session.client.people();
|
|
3201
|
+
const loginOf = new Map(found.items.map((person) => [person.id, person.login]));
|
|
3122
3202
|
answer(io, session, found, () =>
|
|
3123
|
-
[
|
|
3203
|
+
[
|
|
3204
|
+
...found.items.map((person) =>
|
|
3205
|
+
formatPersonLine(
|
|
3206
|
+
person,
|
|
3207
|
+
person.managerId === null || person.managerId === undefined
|
|
3208
|
+
? undefined
|
|
3209
|
+
: (loginOf.get(person.managerId) ?? person.managerId),
|
|
3210
|
+
),
|
|
3211
|
+
),
|
|
3212
|
+
...found.listed.map(formatListingLine),
|
|
3213
|
+
].join("\n"),
|
|
3124
3214
|
);
|
|
3125
3215
|
return EXIT.ok;
|
|
3126
3216
|
};
|
|
@@ -3201,7 +3291,7 @@ const workspace: Command = async (args, io) => {
|
|
|
3201
3291
|
};
|
|
3202
3292
|
|
|
3203
3293
|
const PERSON_USAGE =
|
|
3204
|
-
"Say what to do: console person role <login> <admin|member> | person add <login> [--host gh|google] | person remove <login> | person deactivate <login> | person reactivate <login>";
|
|
3294
|
+
"Say what to do: console person role <login> <admin|member> | person manager <login> <manager login|none> | person add <login> [--host gh|google] | person remove <login> | person deactivate <login> | person reactivate <login>";
|
|
3205
3295
|
|
|
3206
3296
|
const person: Command = async (args, io) => {
|
|
3207
3297
|
const what = args[0];
|
|
@@ -3218,6 +3308,21 @@ const person: Command = async (args, io) => {
|
|
|
3218
3308
|
answer(io, session, changed, () => formatPersonLine(changed));
|
|
3219
3309
|
return EXIT.ok;
|
|
3220
3310
|
}
|
|
3311
|
+
if (what === "manager") {
|
|
3312
|
+
const { values, positionals } = parse(args.slice(1), {});
|
|
3313
|
+
const [login, manager] = theTwo(
|
|
3314
|
+
positionals,
|
|
3315
|
+
"Say whom and whom they report to: console person manager <login> <manager login|none>",
|
|
3316
|
+
);
|
|
3317
|
+
const session = await open(io, values);
|
|
3318
|
+
const found = await personOf(session, login);
|
|
3319
|
+
const managerId = manager === "none" ? null : (await personOf(session, manager)).id;
|
|
3320
|
+
const changed = await session.client.updatePerson(found.id, { managerId });
|
|
3321
|
+
answer(io, session, changed, () =>
|
|
3322
|
+
formatPersonLine(changed, managerId === null ? undefined : manager),
|
|
3323
|
+
);
|
|
3324
|
+
return EXIT.ok;
|
|
3325
|
+
}
|
|
3221
3326
|
if (what === "add") {
|
|
3222
3327
|
const { values, positionals } = parse(args.slice(1), { host: { type: "string" } });
|
|
3223
3328
|
const login = theOne(positionals, "Say whom: console person add <login> [--host gh|google]");
|
package/src/cli/format.ts
CHANGED
|
@@ -119,12 +119,13 @@ export function formatProject(project: RestProject): string {
|
|
|
119
119
|
return lines.join("\n");
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
/** One person: their login, their name where the host gives one, what they are in the workspace,
|
|
123
|
-
export function formatPersonLine(person: RestPerson): string {
|
|
122
|
+
/** One person: their login, their name where the host gives one, what they are in the workspace, that they are out when they are, and whom they report to where known. */
|
|
123
|
+
export function formatPersonLine(person: RestPerson, reportsTo?: string | undefined): string {
|
|
124
124
|
const standing = person.status === "active" ? person.role : `${person.role}, deactivated`;
|
|
125
|
+
const line = reportsTo === undefined ? "" : `, reports to ${reportsTo}`;
|
|
125
126
|
return person.name === null
|
|
126
|
-
? `${person.login} (${standing})`
|
|
127
|
-
: `${person.login} ${person.name} (${standing})`;
|
|
127
|
+
? `${person.login} (${standing}${line})`
|
|
128
|
+
: `${person.login} ${person.name} (${standing}${line})`;
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
/** The provider a listing names, as people call it. */
|
|
@@ -219,6 +220,7 @@ export function formatTask(
|
|
|
219
220
|
`#${task.number} ${task.title}`,
|
|
220
221
|
`state: ${task.state} priority: ${task.priority} owner: ${task.owner.login} workers: ${task.workers.length === 0 ? "nobody" : logins(task.workers)}`,
|
|
221
222
|
`id: ${task.id}${task.parentId === null ? "" : ` part of: ${task.parentId}`}`,
|
|
223
|
+
...(task.agent == null ? [] : [`written through: ${task.agent}`]),
|
|
222
224
|
...scheduleLines(task),
|
|
223
225
|
];
|
|
224
226
|
if (task.links.length > 0) {
|
|
@@ -379,7 +381,7 @@ export function formatGoalLine(goal: RestGoal): string {
|
|
|
379
381
|
export function formatGoal(goal: RestGoal, tasks?: readonly RestTask[]): string {
|
|
380
382
|
const lines = [
|
|
381
383
|
`goal ${goal.id}: ${goal.title}`,
|
|
382
|
-
`state: ${goal.state} category: ${goal.category?.key ?? "none"} starts: ${goal.startsOn ?? "none"} due: ${goal.dueOn ?? "none"} set by: ${goal.createdBy.login}`,
|
|
384
|
+
`state: ${goal.state} category: ${goal.category?.key ?? "none"} starts: ${goal.startsOn ?? "none"} due: ${goal.dueOn ?? "none"} set by: ${goal.createdBy.login}${through(goal.agent)}`,
|
|
383
385
|
`progress: ${progressOf(goal)}`,
|
|
384
386
|
];
|
|
385
387
|
if (goal.body.trim().length > 0) {
|
|
@@ -431,9 +433,27 @@ export function formatPersonWork(person: RestPerson, work: RestPersonWork): stri
|
|
|
431
433
|
].join("\n");
|
|
432
434
|
}
|
|
433
435
|
|
|
434
|
-
/**
|
|
436
|
+
/** The agent a thing came through, beside the person (ADR-0056); nothing when they did it themselves. */
|
|
437
|
+
const through = (agent: string | null | undefined): string =>
|
|
438
|
+
agent === null || agent === undefined ? "" : ` through ${agent}`;
|
|
439
|
+
|
|
440
|
+
/** Everyone a decision is addressed to (ADR-0057): `deciders` where the console answers it, else the one decider. */
|
|
441
|
+
const decidersOf = (decision: RestDecision): readonly RestPerson[] =>
|
|
442
|
+
decision.deciders !== undefined && decision.deciders.length > 0
|
|
443
|
+
? decision.deciders
|
|
444
|
+
: [decision.decider];
|
|
445
|
+
|
|
446
|
+
/** The logins of everyone a decision is addressed to, with commas between. */
|
|
447
|
+
export const deciderLogins = (decision: RestDecision): string =>
|
|
448
|
+
decidersOf(decision)
|
|
449
|
+
.map((person) => person.login)
|
|
450
|
+
.join(", ");
|
|
451
|
+
|
|
452
|
+
/** Whom a decision is for, where that is not the one who asked alone (ADR-0042, ADR-0057). */
|
|
435
453
|
const addressed = (decision: RestDecision): string =>
|
|
436
|
-
decision.
|
|
454
|
+
decidersOf(decision).every((person) => person.id === decision.raisedBy.id)
|
|
455
|
+
? ""
|
|
456
|
+
: `, for ${deciderLogins(decision)}`;
|
|
437
457
|
|
|
438
458
|
export function formatDecisionLine(decision: RestDecision): string {
|
|
439
459
|
const state = decision.answer === null ? "waiting" : `answered ${decision.answer.option}`;
|
|
@@ -454,11 +474,11 @@ export function formatOptions(decision: RestDecision): string {
|
|
|
454
474
|
export function formatAnswer(decision: RestDecision): string {
|
|
455
475
|
const { answer } = decision;
|
|
456
476
|
if (answer === null) {
|
|
457
|
-
return `waits for ${decision
|
|
477
|
+
return `waits for ${deciderLogins(decision)}.`;
|
|
458
478
|
}
|
|
459
479
|
const chosen = decision.options.find((option) => option.id === answer.option);
|
|
460
480
|
const lines = [
|
|
461
|
-
`Answered: ${answer.option}) ${chosen?.label ?? answer.option}${chosen?.own === true ? ", in their own words" : ""}, by ${answer.by.login} at ${answer.at}`,
|
|
481
|
+
`Answered: ${answer.option}) ${chosen?.label ?? answer.option}${chosen?.own === true ? ", in their own words" : ""}, by ${answer.by.login}${through(answer.agent)} at ${answer.at}`,
|
|
462
482
|
];
|
|
463
483
|
if (answer.note !== null && answer.note.length > 0) {
|
|
464
484
|
lines.push(`Note: ${answer.note}`);
|
|
@@ -488,6 +508,13 @@ export function formatDecision(decision: RestDecision): string {
|
|
|
488
508
|
lines.push("", "options:", formatOptions(decision), "", formatAnswer(decision));
|
|
489
509
|
if (decision.outcome !== null && decision.outcome.trim().length > 0) {
|
|
490
510
|
lines.push("", "what followed:", indent(decision.outcome));
|
|
511
|
+
if (decision.outcomeBy != null && decision.outcomeAt != null) {
|
|
512
|
+
lines.push(
|
|
513
|
+
indent(
|
|
514
|
+
`written by ${decision.outcomeBy.login}${through(decision.outcomeAgent)} at ${decision.outcomeAt}`,
|
|
515
|
+
),
|
|
516
|
+
);
|
|
517
|
+
}
|
|
491
518
|
}
|
|
492
519
|
return lines.join("\n");
|
|
493
520
|
}
|
|
@@ -590,6 +617,9 @@ export function localized(
|
|
|
590
617
|
export function formatPluginLine(plugin: RestPlugin, language: string | null | undefined): string {
|
|
591
618
|
const notes = [
|
|
592
619
|
plugin.roles.length === 0 ? undefined : `roles: ${plugin.roles.join(", ")}`,
|
|
620
|
+
plugin.console === null || plugin.console === undefined
|
|
621
|
+
? undefined
|
|
622
|
+
: `console: ${plugin.console}`,
|
|
593
623
|
plugin.problems.length === 0 ? undefined : `problems: ${plugin.problems.join("; ")}`,
|
|
594
624
|
].filter((note): note is string => note !== undefined);
|
|
595
625
|
return `${plugin.key} ${localized(plugin.name, language)} ${plugin.version} ${plugin.state}${notes.length === 0 ? "" : ` ${notes.join(" ")}`}`;
|
|
@@ -761,6 +791,9 @@ export function formatPlugin(
|
|
|
761
791
|
if (detail.problems.length > 0) {
|
|
762
792
|
lines.push(`problems: ${detail.problems.join("; ")}`);
|
|
763
793
|
}
|
|
794
|
+
if (detail.console !== null && detail.console !== undefined) {
|
|
795
|
+
lines.push(`Runs on consoles ${detail.console}.`);
|
|
796
|
+
}
|
|
764
797
|
lines.push(
|
|
765
798
|
detail.roles.length === 0
|
|
766
799
|
? "You hold no role in it."
|
package/src/cli/help.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
MAX_CATEGORY_KEY_LENGTH,
|
|
8
8
|
MAX_CATEGORY_NAME_LENGTH,
|
|
9
9
|
MAX_COMMENTS_PER_TASK,
|
|
10
|
+
MAX_DECIDERS,
|
|
10
11
|
MAX_DECISIONS_PER_PROJECT,
|
|
11
12
|
MAX_DOCUMENT_LENGTH,
|
|
12
13
|
MAX_DOCUMENT_PATH_LENGTH,
|
|
@@ -247,14 +248,15 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
247
248
|
},
|
|
248
249
|
person: {
|
|
249
250
|
usage:
|
|
250
|
-
"person role <login> <admin|member> | person add <login> [--host gh|google] | person remove <login> | person deactivate <login> | person reactivate <login>",
|
|
251
|
+
"person role <login> <admin|member> | person manager <login> <manager login|none> | person add <login> [--host gh|google] | person remove <login> | person deactivate <login> | person reactivate <login>",
|
|
251
252
|
synopsis:
|
|
252
|
-
"Says what a person is in the workspace, lets a login in or takes one out; for an administrator.",
|
|
253
|
+
"Says what a person is in the workspace and whom they report to, lets a login in or takes one out; for an administrator.",
|
|
253
254
|
about:
|
|
254
|
-
"For an administrator: the person themselves on the People page, or their agent here, named on it. role says what a person is in the workspace; the board keeps at least one administrator in. add lets a login in: whoever signs in with that account at that host is a person from then on; the host is read off the login (an address is Google's, anything else GitHub's) unless --host says. remove takes a login away that nobody has signed in with yet. deactivate takes a person out: refused on every request at once, their agents with them, kept with everything they did, and listed nowhere; reactivate takes them back in. A person the organization let in comes and goes with it, and is neither.",
|
|
255
|
+
"For an administrator: the person themselves on the People page, or their agent here, named on it. role says what a person is in the workspace; the board keeps at least one administrator in. manager says whom a person reports to, the reporting line of the directory, which the plugins read as a default (the approver of their leave, say) in place of a line of their own: another active person of the workspace who does not report to them, directly or through others, or none; console people shows it. add lets a login in: whoever signs in with that account at that host is a person from then on; the host is read off the login (an address is Google's, anything else GitHub's) unless --host says. remove takes a login away that nobody has signed in with yet. deactivate takes a person out: refused on every request at once, their agents with them, kept with everything they did, and listed nowhere; reactivate takes them back in. A person the organization let in comes and goes with it, and is neither.",
|
|
255
256
|
refusals: [
|
|
256
257
|
"auth.forbidden: only an administrator says who is in and what they are",
|
|
257
258
|
"person.last_admin: the board keeps at least one administrator in; make another one first",
|
|
259
|
+
"person.invalid_manager: the manager is not another active person of the workspace, or reports to this person already",
|
|
258
260
|
"person.of_organization: the organization says whether that person is in",
|
|
259
261
|
"listing.exists: the login is let in already, or is a person's who is in",
|
|
260
262
|
"listing.unknown_host: nobody signs in through that provider here",
|
|
@@ -378,9 +380,9 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
378
380
|
},
|
|
379
381
|
plugin: {
|
|
380
382
|
usage:
|
|
381
|
-
"plugin <key> | plugin <key> help [<command>] | plugin <key> <command> [<args>] [--<name> <value>]... [--file <path> | -] [--run <id>] | plugin <key> records <collection> [<id>] [--where <field>=<value>]... [--search <words>] [--project <key>] [--order <field>[:desc]] [--limit <n>] [--after <cursor>] [--archived] [--versions] | plugin <key> skills [<folder>|<file>] [--search <words> [--limit <n>] [--cursor <cursor>]] | plugin <key> skill <path> | plugin <key> skill --file <path> [--offset <n>] [--limit <n>] | plugin <key> enable --consent <capability>,... [--board new|<key>] [--projects all|<key>,...] [--set <key>=<value>]... | plugin <key> disable | plugin <key> settings [--set <key>=<value>]... | plugin <key> secret <name> [--remove] | plugin <key> grant <role> --person <login>|--project <key>|--everyone | plugin <key> revoke <grant id>",
|
|
383
|
+
"plugin <key> | plugin <key> help [<command>] | plugin <key> <command> [<args>] [--<name> <value>]... [--file <path> | -] [--run <id>] | plugin <key> records <collection> [<id>] [--where <field>=<value>]... [--search <words>] [--project <key>] [--order <field>[:desc]] [--limit <n>] [--after <cursor>] [--archived] [--versions] | plugin <key> skills [<folder>|<file>] [--search <words> [--limit <n>] [--cursor <cursor>]] | plugin <key> skill <path> | plugin <key> skill --file <path> [--offset <n>] [--limit <n>] | plugin <key> enable --consent <capability>,... [--board new|<key>] [--projects all|<key>,...] [--set <key>=<value>]... | plugin <key> disable | plugin <key> capabilities [--consent <capability>,...|none] | plugin <key> settings [--set <key>=<value>]... | plugin <key> secret <name> [--remove] | plugin <key> grant <role> --person <login>|--project <key>|--everyone | plugin <key> revoke <grant id>",
|
|
382
384
|
overview:
|
|
383
|
-
"plugin <key> [help [<command>] | <command> [<args>] | records <collection> | skills | skill <path> | enable | disable | settings | secret | grant | revoke]",
|
|
385
|
+
"plugin <key> [help [<command>] | <command> [<args>] | records <collection> | skills | skill <path> | enable | disable | capabilities | settings | secret | grant | revoke]",
|
|
384
386
|
synopsis:
|
|
385
387
|
"One plugin: what it is for and its commands; a command run as you; its records, its skills; its settings, for an administrator.",
|
|
386
388
|
about:
|
|
@@ -388,7 +390,8 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
388
390
|
limits: [
|
|
389
391
|
"a command line: up to 200 words of up to 50,000 characters each; one file, up to the file bound (console help hand-in), with the plugin's files capability",
|
|
390
392
|
"records: a page of up to 200; where: a field the collection indexes",
|
|
391
|
-
"enable: consent names every capability the manifest
|
|
393
|
+
"enable: consent names every capability the manifest requires, and the optional ones to turn on; settings and secrets as the manifest bounds them",
|
|
394
|
+
"capabilities: --consent names the optional capabilities the plugin holds from now on, none for none of them; those it requires stay",
|
|
392
395
|
],
|
|
393
396
|
refusals: [
|
|
394
397
|
"plugin.not_found: no plugin has that key, or you may not know of it; or the plugin has no such command or query",
|
|
@@ -451,25 +454,26 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
451
454
|
},
|
|
452
455
|
ask: {
|
|
453
456
|
usage:
|
|
454
|
-
'ask "<question>" --option "<label>" --option "<label>" [...] [--body <markdown>|--file <path>] [--attach <path>]... [--to <login|me>] [--wait <seconds>] [--run <id>]',
|
|
457
|
+
'ask "<question>" --option "<label>" --option "<label>" [...] [--body <markdown>|--file <path>] [--attach <path>]... [--to <login|me>]... [--wait <seconds>] [--run <id>]',
|
|
455
458
|
overview:
|
|
456
|
-
'ask "<question>" --option "<label>" --option "<label>" [...] [--body <markdown>|--file <path>] [--attach <path>]... [--to <login|me>] [--wait <seconds>]',
|
|
459
|
+
'ask "<question>" --option "<label>" --option "<label>" [...] [--body <markdown>|--file <path>] [--attach <path>]... [--to <login|me>]... [--wait <seconds>]',
|
|
457
460
|
synopsis:
|
|
458
461
|
"Raises a decision a person must make, with options, and waits a while for the answer.",
|
|
459
|
-
about: `Raises a decision a person must make: the question, the options to choose from, and what they need to know. --attach puts a file on the decision, in the one request: a picture of what is to be chosen between, a screenshot of what was made; a picture the body names as  is shown there, the rest after the body, and the person reads each back with \`console file <id>\` (ADR-0055). It is for the person you work for unless --to names another of the project, who then finds it among their decisions (ADR-0042); anyone of the project may still answer. The run waits for the answer. The command waits 60 seconds for it unless --wait says otherwise, and exits with 3 if it is still to come: keep waiting with \`console decision <id> --wait 100\`, within what your shell gives one command. The decision's id is printed as soon as it is raised. The person chooses one of the options, or answers in their own words: the answer then names one more option, their words, marked own; act on it as given. ${ON_THE_RUN}`,
|
|
462
|
+
about: `Raises a decision a person must make: the question, the options to choose from, and what they need to know. --attach puts a file on the decision, in the one request: a picture of what is to be chosen between, a screenshot of what was made; a picture the body names as  is shown there, the rest after the body, and the person reads each back with \`console file <id>\` (ADR-0055). It is for the person you work for unless --to names another of the project, who then finds it among their decisions (ADR-0042); --to given again names several, each of whom finds it among theirs, and the first answer ends it (ADR-0057); anyone of the project may still answer. The run waits for the answer. The command waits 60 seconds for it unless --wait says otherwise, and exits with 3 if it is still to come: keep waiting with \`console decision <id> --wait 100\`, within what your shell gives one command. The decision's id is printed as soon as it is raised. The person chooses one of the options, or answers in their own words: the answer then names one more option, their words, marked own; act on it as given. ${ON_THE_RUN}`,
|
|
460
463
|
limits: [
|
|
461
464
|
`question: one line, up to ${MAX_QUESTION_LENGTH} characters`,
|
|
462
465
|
`options: ${MIN_OPTIONS} to ${MAX_OPTIONS}, each one line up to ${MAX_OPTION_LABEL_LENGTH} characters`,
|
|
463
466
|
`body: ${MARKDOWN}`,
|
|
464
467
|
`attach: a file: up to ${MAX_FILE_BYTES} bytes, not empty; called by its name, without the path; a picture is placed in the body as `,
|
|
465
468
|
`at most ${MAX_FILES_PER_DECISION} files on one decision, together up to ${MAX_FILE_BYTES} bytes`,
|
|
466
|
-
|
|
469
|
+
`to: a login of the project, or me; up to ${MAX_DECIDERS} of them`,
|
|
467
470
|
`at most ${MAX_DECISIONS_PER_PROJECT} decisions in one project`,
|
|
468
471
|
],
|
|
469
472
|
refusals: [
|
|
470
473
|
...RUN_CHOICE,
|
|
471
474
|
"decision.invalid_run: the run is not yours, or is over",
|
|
472
|
-
"decision.invalid_decider:
|
|
475
|
+
"decision.invalid_decider: a person named may not work in the project",
|
|
476
|
+
"decision.too_many_deciders: more people named than one decision takes",
|
|
473
477
|
"decision.too_many: the project holds as many decisions as one may",
|
|
474
478
|
"decision.too_many_files: more files than one decision carries",
|
|
475
479
|
"request.too_large: a file, or the files together, over the bound",
|
|
@@ -479,7 +483,7 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
479
483
|
usage: 'answer <id> --option <n>|--own "<answer>" [--note <markdown>|--file <path>]',
|
|
480
484
|
synopsis: "Writes a person's answer on a decision: the option they chose, or their own words.",
|
|
481
485
|
about:
|
|
482
|
-
"Writes on the board the answer a person gave: one of the decision's options by its number, or their own words, which become one more option of the decision marked as theirs; and a note about it. The answer is the person's, written in their name through you (ADR-0021):
|
|
486
|
+
"Writes on the board the answer a person gave: one of the decision's options by its number, or their own words, which become one more option of the decision marked as theirs; and a note about it. The answer is the person's, written in their name through you (ADR-0021), and the record says so: the decision names you beside them as the agent the answer came through, where an answer given on the pages names the person alone (ADR-0056). A choice they made in chat, in a meeting or by changing the task is written here, with where and when they made it in the note, so that the record is whole and a run that waited goes on. A decision is answered once; what followed is written with console decision <id> --outcome.",
|
|
483
487
|
limits: [
|
|
484
488
|
`own: one line, up to ${MAX_OPTION_LABEL_LENGTH} characters`,
|
|
485
489
|
`note: Markdown, up to ${MAX_NOTE_LENGTH} characters`,
|
|
@@ -495,7 +499,7 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
495
499
|
synopsis:
|
|
496
500
|
"One decision in full; waits for its answer with --wait; writes what followed with --outcome.",
|
|
497
501
|
about:
|
|
498
|
-
"One decision, with its context, its options, its answer (one of the options, or the person's own words as one more, marked own) and what followed. With --wait, holds on for the answer that long, and exits with 3 if it is still to come. With --outcome, writes down what followed the decision, so that it reads as a record.",
|
|
502
|
+
"One decision, with its context, its options, its answer (one of the options, or the person's own words as one more, marked own) and what followed. With --wait, holds on for the answer that long, and exits with 3 if it is still to come. With --outcome, writes down what followed the decision, so that it reads as a record; the record names who wrote it, you beside them, and when (ADR-0056).",
|
|
499
503
|
limits: [`outcome: ${MARKDOWN}`],
|
|
500
504
|
refusals: ["decision.not_found: no decision has that id"],
|
|
501
505
|
},
|
package/src/cli/plugin-check.ts
CHANGED
|
@@ -8,11 +8,13 @@ import { descriptionProblems, type PluginDescription } from "../plugin/define.js
|
|
|
8
8
|
import type { ServerLoad } from "../plugin/load.js";
|
|
9
9
|
import {
|
|
10
10
|
CURRENT_PLUGIN_API,
|
|
11
|
+
capabilitiesAsked,
|
|
11
12
|
PLUGIN_API_MAJORS,
|
|
12
13
|
type PluginManifest,
|
|
13
14
|
parsePluginManifest,
|
|
14
15
|
} from "../plugin/manifest.js";
|
|
15
16
|
import { forbiddenImportsIn, removedGlobalsIn } from "../plugin/sandbox.js";
|
|
17
|
+
import { formatTriple, satisfiesRange, versionOfDependency } from "../plugin/semver.js";
|
|
16
18
|
import { isLanguageMap } from "./plugin-init.js";
|
|
17
19
|
|
|
18
20
|
export interface CheckReport {
|
|
@@ -330,10 +332,16 @@ export function sameAsEnglish(value: unknown, at = ""): string[] {
|
|
|
330
332
|
);
|
|
331
333
|
}
|
|
332
334
|
|
|
335
|
+
export interface CheckOptions {
|
|
336
|
+
/** The version of the console this command belongs to, held against the manifest's `console`; left out, not compared. */
|
|
337
|
+
readonly consoleVersion?: string | undefined;
|
|
338
|
+
}
|
|
339
|
+
|
|
333
340
|
/** Reads the plugin at `root` as a console would, and says what it would refuse. */
|
|
334
341
|
export async function checkPlugin(
|
|
335
342
|
root: string,
|
|
336
343
|
loadServer: (file: string) => Promise<ServerLoad>,
|
|
344
|
+
options: CheckOptions = {},
|
|
337
345
|
): Promise<CheckReport> {
|
|
338
346
|
const errors: string[] = [];
|
|
339
347
|
const warnings: string[] = [];
|
|
@@ -365,6 +373,15 @@ export async function checkPlugin(
|
|
|
365
373
|
`console-plugin.json: ${path} is the same as en: translate it, or leave the language out`,
|
|
366
374
|
);
|
|
367
375
|
}
|
|
376
|
+
if (
|
|
377
|
+
manifest.console !== undefined &&
|
|
378
|
+
options.consoleVersion !== undefined &&
|
|
379
|
+
!satisfiesRange(options.consoleVersion, manifest.console)
|
|
380
|
+
) {
|
|
381
|
+
warnings.push(
|
|
382
|
+
`console-plugin.json: console ${manifest.console} leaves out ${options.consoleVersion}, the console this command belongs to`,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
368
385
|
|
|
369
386
|
const packageRead = await readJson(join(root, "package.json"));
|
|
370
387
|
let version = "0.0.0";
|
|
@@ -393,6 +410,25 @@ export async function checkPlugin(
|
|
|
393
410
|
if (pkg.type !== "module") {
|
|
394
411
|
errors.push('package.json: type is not "module"; the server module is ESM');
|
|
395
412
|
}
|
|
413
|
+
// The console the plugin is built against is the one its types come from: a devDependency
|
|
414
|
+
// outside the consoles it declares is a plugin built for a console it says it does not run on.
|
|
415
|
+
const devDependencies = (
|
|
416
|
+
typeof pkg.devDependencies === "object" && pkg.devDependencies !== null
|
|
417
|
+
? pkg.devDependencies
|
|
418
|
+
: {}
|
|
419
|
+
) as Record<string, unknown>;
|
|
420
|
+
const dependency = devDependencies["@skillcdn/console"];
|
|
421
|
+
const builtAgainst =
|
|
422
|
+
typeof dependency === "string" ? versionOfDependency(dependency) : undefined;
|
|
423
|
+
if (
|
|
424
|
+
manifest.console !== undefined &&
|
|
425
|
+
builtAgainst !== undefined &&
|
|
426
|
+
!satisfiesRange(formatTriple(builtAgainst), manifest.console)
|
|
427
|
+
) {
|
|
428
|
+
errors.push(
|
|
429
|
+
`package.json: devDependencies names @skillcdn/console ${String(dependency)}, outside the consoles the manifest declares (console ${manifest.console})`,
|
|
430
|
+
);
|
|
431
|
+
}
|
|
396
432
|
const scripts = (
|
|
397
433
|
typeof pkg.scripts === "object" && pkg.scripts !== null ? pkg.scripts : {}
|
|
398
434
|
) as Record<string, unknown>;
|
|
@@ -456,14 +492,17 @@ export async function checkPlugin(
|
|
|
456
492
|
errors.push(`${manifest.server}: ${problem}`);
|
|
457
493
|
}
|
|
458
494
|
if (
|
|
459
|
-
manifest.
|
|
495
|
+
capabilitiesAsked(manifest).includes("endpoints") &&
|
|
460
496
|
Object.keys(description.endpoints).length === 0
|
|
461
497
|
) {
|
|
462
498
|
warnings.push(
|
|
463
499
|
"console-plugin.json: asks the capability endpoints and the server module defines no endpoint",
|
|
464
500
|
);
|
|
465
501
|
}
|
|
466
|
-
if (
|
|
502
|
+
if (
|
|
503
|
+
capabilitiesAsked(manifest).includes("jobs") &&
|
|
504
|
+
Object.keys(description.jobs).length === 0
|
|
505
|
+
) {
|
|
467
506
|
warnings.push(
|
|
468
507
|
"console-plugin.json: asks the capability jobs and the server module defines no job",
|
|
469
508
|
);
|
package/src/cli/plugin-cli.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { resolve } from "node:path";
|
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
6
6
|
import type { ServerLoad } from "../plugin/load.js";
|
|
7
7
|
import { isPluginKey, RESERVED_PLUGIN_KEYS } from "../plugin/manifest.js";
|
|
8
|
+
import { lineRangeFrom, versionOfDependency } from "../plugin/semver.js";
|
|
8
9
|
import { checkPlugin } from "./plugin-check.js";
|
|
9
10
|
import { LANGUAGE_TAG, nameFromKey, PACKAGE_NAME, writePluginFromTemplate } from "./plugin-init.js";
|
|
10
11
|
|
|
@@ -176,12 +177,18 @@ async function init(args: readonly string[], io: PluginCliIo): Promise<number> {
|
|
|
176
177
|
return PLUGIN_EXIT.usage;
|
|
177
178
|
}
|
|
178
179
|
const consoleDependency = values.console ?? (io.version === undefined ? "*" : `^${io.version}`);
|
|
180
|
+
// The consoles the plugin runs on: the line of the version it is written against, read from
|
|
181
|
+
// the dependency where it names one (`^0.4.3`, `0.4.3`); a dependency that names none
|
|
182
|
+
// (`workspace:*`) leaves the manifest naming no console.
|
|
183
|
+
const writtenAgainst = versionOfDependency(consoleDependency);
|
|
184
|
+
const consoleRange = writtenAgainst === undefined ? undefined : lineRangeFrom(writtenAgainst);
|
|
179
185
|
const target = resolve(io.cwd, dir);
|
|
180
186
|
try {
|
|
181
187
|
const written = await writePluginFromTemplate(io.templateDir, target, {
|
|
182
188
|
key,
|
|
183
189
|
name,
|
|
184
190
|
consoleDependency,
|
|
191
|
+
consoleRange,
|
|
185
192
|
packageName,
|
|
186
193
|
repository:
|
|
187
194
|
values.repository === undefined
|
|
@@ -220,7 +227,9 @@ async function check(args: readonly string[], io: PluginCliIo): Promise<number>
|
|
|
220
227
|
return PLUGIN_EXIT.usage;
|
|
221
228
|
}
|
|
222
229
|
const root = resolve(io.cwd, dir ?? ".");
|
|
223
|
-
const report = await checkPlugin(root, (file) => io.loadServer(file)
|
|
230
|
+
const report = await checkPlugin(root, (file) => io.loadServer(file), {
|
|
231
|
+
consoleVersion: io.version,
|
|
232
|
+
});
|
|
224
233
|
io.stdout(`console-plugin check ${root}\n`);
|
|
225
234
|
io.stdout(wordsOf(report.errors, "error"));
|
|
226
235
|
io.stdout(wordsOf(report.warnings, "warning"));
|
package/src/cli/plugin-init.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface InitOptions {
|
|
|
13
13
|
readonly name: string;
|
|
14
14
|
/** What the plugin's devDependency on `@skillcdn/console` says: `^<version>` of the command, unless said. */
|
|
15
15
|
readonly consoleDependency: string;
|
|
16
|
+
/** The consoles the plugin runs on, the manifest's `console`: the line of the version it is written against; left out, the manifest names none. */
|
|
17
|
+
readonly consoleRange?: string | undefined;
|
|
16
18
|
/** The package's name: `console-plugin-<key>` unless said. A scoped name is published with `publishConfig.access` `public`. */
|
|
17
19
|
readonly packageName?: string | undefined;
|
|
18
20
|
/** The repository the plugin lives in and, in a repository of several, its directory: `repository`, `homepage`, `bugs` and the manifest's `store`. */
|
|
@@ -40,12 +42,18 @@ export function substitutions(options: InitOptions): readonly [string, string][]
|
|
|
40
42
|
["__package__", packageNameOf(options)],
|
|
41
43
|
["__key__", options.key],
|
|
42
44
|
["__name__", options.name],
|
|
45
|
+
["__consoleRange__", options.consoleRange ?? "*"],
|
|
43
46
|
["__console__", options.consoleDependency],
|
|
44
47
|
];
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
export function substitute(text: string, options: InitOptions): string {
|
|
48
51
|
let result = text;
|
|
52
|
+
if (options.consoleRange === undefined) {
|
|
53
|
+
// No console to name: the manifest's line goes, and any console that serves the plugin API
|
|
54
|
+
// runs the plugin, as before the field. Taken out as text, so that the file keeps its shape.
|
|
55
|
+
result = result.replace(/^[ \t]*"console": "__consoleRange__",\r?\n/m, "");
|
|
56
|
+
}
|
|
49
57
|
for (const [word, value] of substitutions(options)) {
|
|
50
58
|
result = result.replaceAll(word, value);
|
|
51
59
|
}
|