@skillcdn/console 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/api.d.ts +4 -4
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +4 -4
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts +5 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +290 -46
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/credentials.d.ts +5 -0
- package/dist/cli/credentials.d.ts.map +1 -1
- package/dist/cli/credentials.js +2 -0
- package/dist/cli/credentials.js.map +1 -1
- package/dist/cli/directory.d.ts +7 -0
- package/dist/cli/directory.d.ts.map +1 -1
- package/dist/cli/directory.js +7 -0
- package/dist/cli/directory.js.map +1 -1
- package/dist/cli/format.d.ts +10 -1
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +76 -20
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +42 -23
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/main.js +1 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/client.d.ts +39 -9
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +24 -3
- package/dist/client.js.map +1 -1
- package/dist/compat.d.ts +28 -6
- package/dist/compat.d.ts.map +1 -1
- package/dist/compat.js +55 -14
- package/dist/compat.js.map +1 -1
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +6 -4
- package/dist/components/board.js.map +1 -1
- package/dist/components/calendar.d.ts +18 -8
- package/dist/components/calendar.d.ts.map +1 -1
- package/dist/components/calendar.js +39 -27
- package/dist/components/calendar.js.map +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +2 -0
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/pack-connect.d.ts +23 -0
- package/dist/components/pack-connect.d.ts.map +1 -0
- package/dist/components/pack-connect.js +86 -0
- package/dist/components/pack-connect.js.map +1 -0
- package/dist/components/pack-token.d.ts +27 -0
- package/dist/components/pack-token.d.ts.map +1 -0
- package/dist/components/pack-token.js +11 -0
- package/dist/components/pack-token.js.map +1 -0
- package/dist/components/picker.d.ts +39 -0
- package/dist/components/picker.d.ts.map +1 -0
- package/dist/components/picker.js +155 -0
- package/dist/components/picker.js.map +1 -0
- package/dist/components/projects.d.ts +13 -0
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +37 -5
- package/dist/components/projects.js.map +1 -1
- package/dist/components/task-form.d.ts +18 -4
- package/dist/components/task-form.d.ts.map +1 -1
- package/dist/components/task-form.js +56 -13
- package/dist/components/task-form.js.map +1 -1
- package/dist/components/task-list.js +2 -2
- package/dist/components/task-list.js.map +1 -1
- package/dist/components/task-view.d.ts +7 -1
- package/dist/components/task-view.d.ts.map +1 -1
- package/dist/components/task-view.js +6 -5
- package/dist/components/task-view.js.map +1 -1
- package/dist/components/ui.d.ts +7 -0
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +11 -0
- package/dist/components/ui.js.map +1 -1
- package/dist/components/work.d.ts +3 -1
- package/dist/components/work.d.ts.map +1 -1
- package/dist/components/work.js +6 -4
- package/dist/components/work.js.map +1 -1
- package/dist/components/workspace.d.ts +46 -0
- package/dist/components/workspace.d.ts.map +1 -0
- package/dist/components/workspace.js +124 -0
- package/dist/components/workspace.js.map +1 -0
- package/dist/console.css +319 -11
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +83 -24
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +26 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +59 -1
- package/dist/data.js.map +1 -1
- package/dist/dates.d.ts +12 -2
- package/dist/dates.d.ts.map +1 -1
- package/dist/dates.js +20 -2
- package/dist/dates.js.map +1 -1
- package/dist/i18n/en.d.ts +64 -3
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +81 -16
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +77 -16
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- 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/router.d.ts +4 -4
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +11 -2
- 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 +4 -0
- package/dist/routes.js.map +1 -1
- package/dist/schemas.d.ts +194 -13
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +121 -21
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +21 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +18 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +25 -20
- package/docs/specs/rest.md +42 -19
- package/package.json +2 -2
- package/skills/building-a-console/SKILL.md +6 -6
- package/skills/building-a-console/references/the-look-from-a-website.md +39 -0
- package/skills/deploying-a-console/SKILL.md +9 -8
- package/skills/deploying-a-console/references/resources.md +1 -1
- package/skills/working-the-board/SKILL.md +5 -5
- package/skills/working-the-board/references/setting-up.md +1 -1
- package/skills/working-the-board/references/terms.md +3 -1
- package/skills/working-the-board/references/the-sentence.md +1 -1
- package/src/api.ts +21 -0
- package/src/cli/cli.ts +384 -51
- package/src/cli/credentials.ts +7 -0
- package/src/cli/directory.ts +9 -0
- package/src/cli/format.ts +83 -20
- package/src/cli/help.ts +50 -23
- package/src/cli/main.ts +1 -0
- package/src/client.ts +107 -10
- package/src/compat.ts +63 -14
- package/src/components/board.tsx +10 -4
- package/src/components/calendar.tsx +76 -34
- package/src/components/event-feed.tsx +2 -0
- package/src/components/pack-connect.tsx +179 -0
- package/src/components/pack-token.tsx +74 -0
- package/src/components/picker.tsx +297 -0
- package/src/components/projects.tsx +108 -9
- package/src/components/task-form.tsx +149 -63
- package/src/components/task-list.tsx +2 -2
- package/src/components/task-view.tsx +23 -9
- package/src/components/ui.tsx +25 -0
- package/src/components/work.tsx +10 -5
- package/src/components/workspace.tsx +298 -0
- package/src/console.tsx +202 -32
- package/src/data.ts +101 -1
- package/src/dates.ts +23 -2
- package/src/i18n/en.ts +92 -16
- package/src/i18n/ko.ts +84 -16
- package/src/index.ts +32 -1
- package/src/limits.ts +2 -0
- package/src/router.ts +17 -5
- package/src/routes.ts +4 -0
- package/src/schemas.ts +140 -19
- package/src/styles/console.css +319 -11
- package/src/vocabulary.ts +28 -0
package/dist/cli/cli.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { basename, extname } from "node:path";
|
|
2
2
|
import { parseArgs } from "node:util";
|
|
3
3
|
import { ApiError, createClient, } from "../client.js";
|
|
4
|
-
import { CLIENT_RANGE_HEADER, judgeClient, parseVersion, VERSION_HEADER } from "../compat.js";
|
|
5
|
-
import { addDays, dayOf as dayOfDate, isDay } from "../dates.js";
|
|
4
|
+
import { CLIENT_RANGE_HEADER, CONSOLE_AT_LEAST, consoleRange, formatRange, judgeClient, judgeConsole, parseVersion, VERSION_HEADER, formatVersion as versionText, } from "../compat.js";
|
|
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
7
|
import { fillRules, RULES_PATH, RULES_TITLE } from "../rules.js";
|
|
8
8
|
import { hostOfLogin, } from "../schemas.js";
|
|
9
|
-
import { COLOR_NAMES, GOAL_STATES, PERSON_ROLES, PROJECT_ROLES, PROJECT_VISIBILITIES, PROVIDER_KEYS, TASK_PRIORITIES, TASK_STATES, } from "../vocabulary.js";
|
|
9
|
+
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
10
|
import { loadWebRoot, WebRootError } from "../web.js";
|
|
11
11
|
import { CredentialsError, EMPTY_CREDENTIALS, loginsAt, resolveLogin, withLogin, withoutLogin, } from "./credentials.js";
|
|
12
|
-
import {
|
|
12
|
+
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";
|
|
13
14
|
import { COMMAND_HELP, helpFor, ownSkillPath, usage } from "./help.js";
|
|
14
15
|
import { describeInstall, updateCommandFor } from "./install.js";
|
|
15
16
|
import { bodyOfFile, bodyOfFolder, bodyOfSkill, formatSkillBrowse, formatSkillFile, formatSkillFind, formatSkillLoad, formatSkills, joinFile, joinFolder, joinSkill, skillPathIn, summaryOf, } from "./pack.js";
|
|
@@ -149,6 +150,12 @@ async function open(io, values) {
|
|
|
149
150
|
throw failed(wordsOf(resolved));
|
|
150
151
|
}
|
|
151
152
|
const { url, token } = resolved;
|
|
153
|
+
// Moments are printed in the workspace's time zone as the login kept it (ADR-0035); the
|
|
154
|
+
// environment's pair keeps nothing, and they stay as the API gives them.
|
|
155
|
+
setMomentZone(resolved.via === "login"
|
|
156
|
+
? credentials?.logins.find((kept) => kept.url === url && kept.account === resolved.account)
|
|
157
|
+
?.timeZone
|
|
158
|
+
: undefined);
|
|
152
159
|
const client = clientOf(io, url, token);
|
|
153
160
|
const projectKey = async () => text(values, "project") ?? io.env.CONSOLE_PROJECT ?? directory?.project;
|
|
154
161
|
return {
|
|
@@ -291,15 +298,20 @@ async function personIdOf(session, value) {
|
|
|
291
298
|
}
|
|
292
299
|
return (await personOf(session, value)).id;
|
|
293
300
|
}
|
|
294
|
-
/**
|
|
301
|
+
/**
|
|
302
|
+
* The person a login names, among the workspace's, found by their exact login as people sign in
|
|
303
|
+
* (ADR-0035): whom the console shows the person, and, for an owner of a project, anyone of the
|
|
304
|
+
* workspace, so that someone the People page does not show can still be listed.
|
|
305
|
+
*/
|
|
295
306
|
async function personOf(session, login) {
|
|
296
|
-
const
|
|
297
|
-
const found = items.find((person) => person.login.toLowerCase() === login.toLowerCase());
|
|
307
|
+
const found = await session.client.findPerson(login);
|
|
298
308
|
if (found === undefined) {
|
|
299
|
-
throw misuse(`Nobody of the workspace is called ${login}. console people lists
|
|
309
|
+
throw misuse(`Nobody of the workspace is called ${login}, or they are not yours to see. console people lists whom you see, by login.`);
|
|
300
310
|
}
|
|
301
311
|
return found;
|
|
302
312
|
}
|
|
313
|
+
/** Who sees whom, as the command takes it: with a hyphen or an underscore. */
|
|
314
|
+
const peopleVisibilityOf = (value) => wordOf(value.replaceAll("-", "_"), PEOPLE_VISIBILITIES, "who sees whom");
|
|
303
315
|
/** One word of a vocabulary, or the refusal to understand. */
|
|
304
316
|
function wordOf(value, words, what) {
|
|
305
317
|
const found = words.find((word) => word === value);
|
|
@@ -413,7 +425,14 @@ async function keep(io, url, token, agent, asDefault) {
|
|
|
413
425
|
.giveUpToken()
|
|
414
426
|
.catch(() => undefined);
|
|
415
427
|
}
|
|
416
|
-
const next = withLogin(credentials, {
|
|
428
|
+
const next = withLogin(credentials, {
|
|
429
|
+
url,
|
|
430
|
+
account: person.login,
|
|
431
|
+
token,
|
|
432
|
+
agent,
|
|
433
|
+
since: new Date(io.now()).toISOString(),
|
|
434
|
+
timeZone: me.workspace.timeZone ?? undefined,
|
|
435
|
+
}, { default: asDefault });
|
|
417
436
|
const kept = await io.store.save(next);
|
|
418
437
|
const standing = next.default === url ? ", the default console here" : "";
|
|
419
438
|
io.stdout(`${agent === undefined ? "Signed in" : `Connected as "${agent}"`} to ${me.workspace.name} at ${url} as ${person.login} (${person.role})${standing}. The token is kept in ${kept}.\nNext, say which project this directory works in: console use <key> (console projects lists them).\n`);
|
|
@@ -557,6 +576,21 @@ function agentOf(env) {
|
|
|
557
576
|
}
|
|
558
577
|
return undefined;
|
|
559
578
|
}
|
|
579
|
+
/**
|
|
580
|
+
* Keeps the workspace's time zone on the login the command used (ADR-0035), so that moments are
|
|
581
|
+
* printed in it from now on without asking; the environment's pair keeps nothing.
|
|
582
|
+
*/
|
|
583
|
+
async function rememberZone(io, session, zone) {
|
|
584
|
+
setMomentZone(zone);
|
|
585
|
+
if (session.via !== "login" || session.credentials === undefined) {
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
const kept = session.credentials.logins.find((login) => login.url === session.url && login.account === session.account);
|
|
589
|
+
if (kept === undefined || kept.timeZone === zone) {
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
await io.store.save(withLogin(session.credentials, { ...kept, timeZone: zone }));
|
|
593
|
+
}
|
|
560
594
|
const whoami = async (args, io) => {
|
|
561
595
|
const { values } = parse(args, {}, false);
|
|
562
596
|
const session = await open(io, values);
|
|
@@ -565,16 +599,20 @@ const whoami = async (args, io) => {
|
|
|
565
599
|
if (person === null) {
|
|
566
600
|
throw failed("The console does not know this token. Sign in again: console login");
|
|
567
601
|
}
|
|
602
|
+
await rememberZone(io, session, me.workspace.timeZone ?? undefined);
|
|
568
603
|
const key = await session.projectKey();
|
|
569
604
|
const command = parseVersion(io.version);
|
|
570
605
|
const server = parseVersion(me.console?.version);
|
|
571
606
|
const verdict = command === undefined || server === undefined
|
|
572
607
|
? undefined
|
|
573
608
|
: judgeClient(command, server).verdict;
|
|
609
|
+
// The oldest console this command works with (ADR-0038), as it stands within the command's line.
|
|
610
|
+
const atLeast = io.consoleAtLeast ?? CONSOLE_AT_LEAST;
|
|
611
|
+
const floor = command === undefined ? atLeast : versionText(consoleRange(command, atLeast).atLeast);
|
|
574
612
|
// A console from before the rule says nothing of itself; the command says so instead.
|
|
575
613
|
const consoleWords = me.console === undefined
|
|
576
614
|
? "the console does not say its version, so it is older than 0.1.18"
|
|
577
|
-
: `the console is ${me.console.version} and takes ${me.console.clientRange}`;
|
|
615
|
+
: `the console is ${me.console.version} and takes ${me.console.clientRange}; this command works with a console from ${floor} on`;
|
|
578
616
|
answer(io, session, {
|
|
579
617
|
...me,
|
|
580
618
|
console: me.console ?? null,
|
|
@@ -628,9 +666,25 @@ const use = async (args, io) => {
|
|
|
628
666
|
const bound = pinUrl
|
|
629
667
|
? ` at ${session.url}${pinAccount && session.account !== undefined ? ` as ${session.account}` : ""}`
|
|
630
668
|
: "";
|
|
631
|
-
answer(io, session, project, () =>
|
|
669
|
+
answer(io, session, project, () => [
|
|
670
|
+
`Working in ${project.name} (${project.key})${bound} from this directory on: kept in ${kept}, which is meant to be committed in a private repository and left out of a public one. What its people ask of agents: console rules.`,
|
|
671
|
+
// Nothing sends an agent to the console until the checkout says so; the sentence is the
|
|
672
|
+
// skill's (references/the-sentence.md), printed here as `skillcdn use` prints its own.
|
|
673
|
+
"Then put this sentence into the checkout's agreement (AGENTS.md, or CLAUDE.md where that is the file agents read), as it is, so that every agent working here is sent to the console:",
|
|
674
|
+
"",
|
|
675
|
+
THE_SENTENCE,
|
|
676
|
+
].join("\n"));
|
|
632
677
|
return EXIT.ok;
|
|
633
678
|
};
|
|
679
|
+
/**
|
|
680
|
+
* The last day of this week (ADR-0039): the week begins as the workspace says, or, left to the
|
|
681
|
+
* language, as the person's language has it, else this machine's.
|
|
682
|
+
*/
|
|
683
|
+
async function endOfWeek(io, session, today) {
|
|
684
|
+
const me = await session.client.me();
|
|
685
|
+
const language = me.language ?? io.locale ?? "en";
|
|
686
|
+
return weekOf(today, firstDayOfWeek(language, me.workspace.weekStart ?? "auto")).at(-1) ?? today;
|
|
687
|
+
}
|
|
634
688
|
const tasks = async (args, io) => {
|
|
635
689
|
const { values } = parse(args, {
|
|
636
690
|
state: { type: "string" },
|
|
@@ -640,12 +694,13 @@ const tasks = async (args, io) => {
|
|
|
640
694
|
category: { type: "string" },
|
|
641
695
|
goal: { type: "string" },
|
|
642
696
|
due: { type: "string" },
|
|
697
|
+
search: { type: "string" },
|
|
643
698
|
archived: { type: "boolean" },
|
|
644
699
|
...PAGE_OPTIONS,
|
|
645
700
|
}, false);
|
|
646
701
|
const state = stateOf(text(values, "state"));
|
|
647
702
|
if (on(values, "mine") && on(values, "unassigned")) {
|
|
648
|
-
throw misuse("Say one: --mine for the tasks
|
|
703
|
+
throw misuse("Say one: --mine for the tasks you work on, or --unassigned for those nobody works on");
|
|
649
704
|
}
|
|
650
705
|
const page = pageOf(values);
|
|
651
706
|
const session = await open(io, values);
|
|
@@ -658,19 +713,22 @@ const tasks = async (args, io) => {
|
|
|
658
713
|
: due === "overdue"
|
|
659
714
|
? addDays(today, -1)
|
|
660
715
|
: due === "week"
|
|
661
|
-
?
|
|
716
|
+
? await endOfWeek(io, session, today)
|
|
662
717
|
: (dayOf(io, due) ?? undefined);
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
|
|
718
|
+
const worker = on(values, "mine") ? ((await personIdOf(session, "me")) ?? undefined) : undefined;
|
|
719
|
+
const search = text(values, "search")?.trim();
|
|
720
|
+
if (search === "") {
|
|
721
|
+
throw misuse("Say what to search for: console tasks --search <words>");
|
|
722
|
+
}
|
|
666
723
|
const found = await project.tasks({
|
|
667
724
|
...(state === undefined ? {} : { state }),
|
|
668
725
|
...(on(values, "open") || due === "overdue" ? { open: true } : {}),
|
|
669
|
-
...(
|
|
726
|
+
...(worker === undefined ? {} : { worker }),
|
|
670
727
|
...(on(values, "unassigned") ? { unassigned: true } : {}),
|
|
671
728
|
...(text(values, "category") === undefined ? {} : { category: text(values, "category") }),
|
|
672
729
|
...(text(values, "goal") === undefined ? {} : { goal: text(values, "goal") }),
|
|
673
730
|
...(dueBy === undefined ? {} : { dueBy }),
|
|
731
|
+
...(search === undefined ? {} : { q: search }),
|
|
674
732
|
...(on(values, "archived") ? { archived: true } : {}),
|
|
675
733
|
...page,
|
|
676
734
|
});
|
|
@@ -938,9 +996,37 @@ const TASK_FIELDS = {
|
|
|
938
996
|
goal: { type: "string" },
|
|
939
997
|
starts: { type: "string" },
|
|
940
998
|
due: { type: "string" },
|
|
941
|
-
|
|
999
|
+
worker: { type: "string", multiple: true },
|
|
942
1000
|
parent: { type: "string" },
|
|
943
1001
|
};
|
|
1002
|
+
/**
|
|
1003
|
+
* The people `--worker` names, each a login or `me`, given again or with commas between for
|
|
1004
|
+
* several, in place of those there were; `none` alone for nobody; nothing when the flag was
|
|
1005
|
+
* not given.
|
|
1006
|
+
*/
|
|
1007
|
+
async function workerIdsOf(session, given) {
|
|
1008
|
+
if (given.length === 0) {
|
|
1009
|
+
return undefined;
|
|
1010
|
+
}
|
|
1011
|
+
const names = given
|
|
1012
|
+
.flatMap((value) => value.split(","))
|
|
1013
|
+
.map((name) => name.trim())
|
|
1014
|
+
.filter((name) => name.length > 0);
|
|
1015
|
+
if (names.includes("none")) {
|
|
1016
|
+
if (names.length > 1) {
|
|
1017
|
+
throw misuse("Say --worker none alone, for nobody; or the logins of the workers.");
|
|
1018
|
+
}
|
|
1019
|
+
return [];
|
|
1020
|
+
}
|
|
1021
|
+
const ids = [];
|
|
1022
|
+
for (const name of names) {
|
|
1023
|
+
const id = await personIdOf(session, name);
|
|
1024
|
+
if (typeof id === "string" && !ids.includes(id)) {
|
|
1025
|
+
ids.push(id);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
return ids;
|
|
1029
|
+
}
|
|
944
1030
|
/** What the flags say of a task's fields, as the API takes them; `none` clears a field. */
|
|
945
1031
|
async function taskFieldsOf(io, session, project, values) {
|
|
946
1032
|
const state = stateOf(text(values, "state"));
|
|
@@ -949,7 +1035,7 @@ async function taskFieldsOf(io, session, project, values) {
|
|
|
949
1035
|
const goalId = orNone(text(values, "goal"));
|
|
950
1036
|
const startsOn = dayOf(io, text(values, "starts"), true);
|
|
951
1037
|
const dueOn = dayOf(io, text(values, "due"), true);
|
|
952
|
-
const
|
|
1038
|
+
const workerIds = await workerIdsOf(session, texts(values, "worker"));
|
|
953
1039
|
const parent = text(values, "parent");
|
|
954
1040
|
const parentId = parent === undefined ? undefined : parent === "none" ? null : (await project.task(parent)).id;
|
|
955
1041
|
return {
|
|
@@ -959,7 +1045,7 @@ async function taskFieldsOf(io, session, project, values) {
|
|
|
959
1045
|
...(goalId === undefined ? {} : { goalId }),
|
|
960
1046
|
...(startsOn === undefined ? {} : { startsOn }),
|
|
961
1047
|
...(dueOn === undefined ? {} : { dueOn }),
|
|
962
|
-
...(
|
|
1048
|
+
...(workerIds === undefined ? {} : { workerIds }),
|
|
963
1049
|
...(parentId === undefined ? {} : { parentId }),
|
|
964
1050
|
};
|
|
965
1051
|
}
|
|
@@ -979,7 +1065,7 @@ const newTask = async (args, io) => {
|
|
|
979
1065
|
...(body === undefined ? {} : { body }),
|
|
980
1066
|
...fields,
|
|
981
1067
|
});
|
|
982
|
-
answer(io, session, made, () => `Task #${made.number} written: ${made.title}\
|
|
1068
|
+
answer(io, session, made, () => `Task #${made.number} written: ${made.title}\ntask id: ${made.id}`);
|
|
983
1069
|
return EXIT.ok;
|
|
984
1070
|
};
|
|
985
1071
|
const editTask = async (args, io) => {
|
|
@@ -1000,7 +1086,7 @@ const editTask = async (args, io) => {
|
|
|
1000
1086
|
...(await taskFieldsOf(io, session, project, values)),
|
|
1001
1087
|
};
|
|
1002
1088
|
if (Object.keys(patch).length === 0) {
|
|
1003
|
-
throw misuse("Say what to change: --title, --body, --state, --priority, --category, --goal, --starts, --due, --
|
|
1089
|
+
throw misuse("Say what to change: --title, --body, --state, --priority, --category, --goal, --starts, --due, --worker or --parent; none clears a field.");
|
|
1004
1090
|
}
|
|
1005
1091
|
const found = await project.task(ref);
|
|
1006
1092
|
const changed = await project.updateTask(found.id, patch);
|
|
@@ -1169,6 +1255,9 @@ const take = async (args, io) => {
|
|
|
1169
1255
|
};
|
|
1170
1256
|
answer(io, session, { run, task: taken, comments: comments.items, rules }, () => [
|
|
1171
1257
|
`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}`,
|
|
1258
|
+
// The ids on a line of their own, each named: a run's and a task's begin alike, and a
|
|
1259
|
+
// task has a number for people as well.
|
|
1260
|
+
`run id: ${run.id} task id: ${found.id} (task #${found.number})`,
|
|
1172
1261
|
"",
|
|
1173
1262
|
formatTask(taken, undefined, undefined, comments.items),
|
|
1174
1263
|
"",
|
|
@@ -1283,7 +1372,7 @@ const ask = async (args, io) => {
|
|
|
1283
1372
|
// The id is printed before any waiting, so that a wait cut short loses nothing.
|
|
1284
1373
|
io.stdout(session.json
|
|
1285
1374
|
? `${JSON.stringify(raised, null, 2)}\n`
|
|
1286
|
-
: `Decision ${raised.id} raised on run ${runId}; the run waits for a person.\n${formatOptions(raised)}\n`);
|
|
1375
|
+
: `Decision ${raised.id} raised on run ${runId}; the run waits for a person.\ndecision id: ${raised.id}\n${formatOptions(raised)}\n`);
|
|
1287
1376
|
return settle(io, session, project, raised, seconds, true);
|
|
1288
1377
|
};
|
|
1289
1378
|
const decision = async (args, io) => {
|
|
@@ -1442,20 +1531,33 @@ const restore = async (args, io) => {
|
|
|
1442
1531
|
return EXIT.ok;
|
|
1443
1532
|
};
|
|
1444
1533
|
const goals = async (args, io) => {
|
|
1445
|
-
const { values } = parse(args, {
|
|
1534
|
+
const { values } = parse(args, {
|
|
1535
|
+
category: { type: "string" },
|
|
1536
|
+
state: { type: "string" },
|
|
1537
|
+
search: { type: "string" },
|
|
1538
|
+
...PAGE_OPTIONS,
|
|
1539
|
+
}, false);
|
|
1446
1540
|
const state = goalStateOf(text(values, "state"));
|
|
1447
1541
|
const category = text(values, "category");
|
|
1542
|
+
const search = text(values, "search")?.trim();
|
|
1543
|
+
if (search === "") {
|
|
1544
|
+
throw misuse("Say what to search for: console goals --search <words>");
|
|
1545
|
+
}
|
|
1546
|
+
const page = pageOf(values);
|
|
1448
1547
|
const session = await open(io, values);
|
|
1449
1548
|
const project = await session.project();
|
|
1450
1549
|
const found = await project.goals({
|
|
1451
1550
|
...(state === undefined ? {} : { state }),
|
|
1452
1551
|
...(category === undefined ? {} : { category }),
|
|
1552
|
+
...(search === undefined ? {} : { q: search }),
|
|
1553
|
+
...page,
|
|
1453
1554
|
});
|
|
1454
1555
|
answer(io, session, found, () => found.items.length === 0
|
|
1455
|
-
? state === undefined && category === undefined
|
|
1556
|
+
? state === undefined && category === undefined && search === undefined
|
|
1456
1557
|
? "No goals. An owner sets one: console goal new <title> --due <day>"
|
|
1457
1558
|
: "No goal matches."
|
|
1458
|
-
: found.items.map(formatGoalLine).join("\n")
|
|
1559
|
+
: found.items.map(formatGoalLine).join("\n") +
|
|
1560
|
+
formatMore(found.more, page.limit, page.offset));
|
|
1459
1561
|
return EXIT.ok;
|
|
1460
1562
|
};
|
|
1461
1563
|
const GOAL_FIELDS = {
|
|
@@ -1488,7 +1590,7 @@ const newGoal = async (args, io) => {
|
|
|
1488
1590
|
...(startsOn === undefined ? {} : { startsOn }),
|
|
1489
1591
|
...(dueOn === undefined ? {} : { dueOn }),
|
|
1490
1592
|
});
|
|
1491
|
-
answer(io, session, made, () => `Goal set: ${made.title}\
|
|
1593
|
+
answer(io, session, made, () => `Goal set: ${made.title}\ngoal id: ${made.id}`);
|
|
1492
1594
|
return EXIT.ok;
|
|
1493
1595
|
};
|
|
1494
1596
|
const editGoal = async (args, io) => {
|
|
@@ -1683,6 +1785,66 @@ const people = async (args, io) => {
|
|
|
1683
1785
|
answer(io, session, found, () => [...found.items.map(formatPersonLine), ...found.listed.map(formatListingLine)].join("\n"));
|
|
1684
1786
|
return EXIT.ok;
|
|
1685
1787
|
};
|
|
1788
|
+
// The workspace's settings (ADR-0035): read by everyone, changed by an administrator, themselves
|
|
1789
|
+
// on the Settings page or through their agent here (ADR-0021).
|
|
1790
|
+
const WORKSPACE_FLAGS = "--people-visibility <everyone|shared-projects>, --week-start <auto|monday|sunday>, --time-zone <zone|none>";
|
|
1791
|
+
const WORKSPACE_USAGE = `Say what to do: console workspace | console workspace edit [${WORKSPACE_FLAGS.replaceAll(", ", "] [")}] | console workspace pack-token --connect|--stdin|--remove`;
|
|
1792
|
+
const workspace = async (args, io) => {
|
|
1793
|
+
const [verb, ...rest] = args;
|
|
1794
|
+
if (verb === undefined || verb.startsWith("--")) {
|
|
1795
|
+
const { values } = parse(args, {}, false);
|
|
1796
|
+
const session = await open(io, values);
|
|
1797
|
+
const me = await session.client.me();
|
|
1798
|
+
await rememberZone(io, session, me.workspace.timeZone ?? undefined);
|
|
1799
|
+
answer(io, session, me.workspace, () => formatWorkspace(me.workspace));
|
|
1800
|
+
return EXIT.ok;
|
|
1801
|
+
}
|
|
1802
|
+
if (verb === "edit") {
|
|
1803
|
+
const { values } = parse(rest, {
|
|
1804
|
+
"people-visibility": { type: "string" },
|
|
1805
|
+
"week-start": { type: "string" },
|
|
1806
|
+
"time-zone": { type: "string" },
|
|
1807
|
+
}, false);
|
|
1808
|
+
const visibility = text(values, "people-visibility");
|
|
1809
|
+
const weekStart = text(values, "week-start");
|
|
1810
|
+
const zone = text(values, "time-zone");
|
|
1811
|
+
if (visibility === undefined && weekStart === undefined && zone === undefined) {
|
|
1812
|
+
throw misuse(`Say what to change: ${WORKSPACE_FLAGS}`);
|
|
1813
|
+
}
|
|
1814
|
+
const session = await open(io, values);
|
|
1815
|
+
const changed = await session.client.updateWorkspace({
|
|
1816
|
+
...(visibility === undefined ? {} : { peopleVisibility: peopleVisibilityOf(visibility) }),
|
|
1817
|
+
...(weekStart === undefined
|
|
1818
|
+
? {}
|
|
1819
|
+
: { weekStart: wordOf(weekStart, WEEK_STARTS, "a day the week begins on") }),
|
|
1820
|
+
...(zone === undefined ? {} : { timeZone: zone === "none" ? null : zone }),
|
|
1821
|
+
});
|
|
1822
|
+
await rememberZone(io, session, changed.timeZone ?? undefined);
|
|
1823
|
+
answer(io, session, changed, () => formatWorkspace(changed));
|
|
1824
|
+
return EXIT.ok;
|
|
1825
|
+
}
|
|
1826
|
+
if (verb === "pack-token") {
|
|
1827
|
+
const { values, positionals } = parse(rest, PACK_TOKEN_FLAGS);
|
|
1828
|
+
if (positionals.length > 0) {
|
|
1829
|
+
throw misuse("The workspace's token names no project: console workspace pack-token --connect, --stdin or --remove");
|
|
1830
|
+
}
|
|
1831
|
+
const way = wayOf(values);
|
|
1832
|
+
const session = await open(io, values);
|
|
1833
|
+
const { client } = session;
|
|
1834
|
+
return packToken(io, session, way, {
|
|
1835
|
+
set: (token) => client.setWorkspacePackToken({ token }),
|
|
1836
|
+
remove: () => client.removeWorkspacePackToken(),
|
|
1837
|
+
connect: () => client.connectWorkspacePackToken(),
|
|
1838
|
+
async standing() {
|
|
1839
|
+
const state = await client.workspacePackTokenConnection();
|
|
1840
|
+
return state.status === "connected"
|
|
1841
|
+
? { status: "connected", held: state.workspace }
|
|
1842
|
+
: state;
|
|
1843
|
+
},
|
|
1844
|
+
}, formatWorkspace);
|
|
1845
|
+
}
|
|
1846
|
+
throw misuse(WORKSPACE_USAGE);
|
|
1847
|
+
};
|
|
1686
1848
|
const PERSON_USAGE = "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>";
|
|
1687
1849
|
const person = async (args, io) => {
|
|
1688
1850
|
const what = args[0];
|
|
@@ -1780,30 +1942,79 @@ const editProject = async (args, io) => {
|
|
|
1780
1942
|
answer(io, session, changed, () => formatProject(changed));
|
|
1781
1943
|
return EXIT.ok;
|
|
1782
1944
|
};
|
|
1783
|
-
/**
|
|
1784
|
-
const
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1945
|
+
/** How a pack's connection ended short of a token (ADR-0036), in words for the person. */
|
|
1946
|
+
const PACK_CONNECT_ENDED = {
|
|
1947
|
+
expired: "The code expired before anyone approved it. Run the command again for a new one.",
|
|
1948
|
+
declined: "The request was declined at SkillCDN, or the code was spent.",
|
|
1949
|
+
refused: "SkillCDN refused the request: the repository is not one the person may open, the pack is public and needs no token, or they hold as many tokens as one may.",
|
|
1950
|
+
};
|
|
1951
|
+
/** How a SkillCDN token is to change, one of the three the flags name. */
|
|
1952
|
+
const wayOf = (values) => {
|
|
1953
|
+
const ways = ["connect", "stdin", "remove"].filter((way) => on(values, way));
|
|
1954
|
+
const [way] = ways;
|
|
1955
|
+
if (way === undefined || ways.length !== 1) {
|
|
1956
|
+
throw misuse("Say one of --connect, which asks SkillCDN for a code a person approves; --stdin, which reads a token made by hand from standard input; and --remove, which takes the token away.");
|
|
1957
|
+
}
|
|
1958
|
+
return way;
|
|
1959
|
+
};
|
|
1960
|
+
/**
|
|
1961
|
+
* The SkillCDN token (ADR-0028): connected with a code a person approves at SkillCDN (ADR-0036),
|
|
1962
|
+
* or read from standard input and never from an argument, so that no shell keeps it; or taken
|
|
1963
|
+
* away. The same for a project's and for the workspace's.
|
|
1964
|
+
*/
|
|
1965
|
+
async function packToken(io, session, way, holder, format) {
|
|
1966
|
+
if (way === "remove") {
|
|
1967
|
+
const changed = await holder.remove();
|
|
1968
|
+
answer(io, session, changed, () => format(changed));
|
|
1798
1969
|
return EXIT.ok;
|
|
1799
1970
|
}
|
|
1971
|
+
if (way === "connect") {
|
|
1972
|
+
const started = await holder.connect();
|
|
1973
|
+
io.stdout(`To connect the pack, a person who may open its repository opens ${started.url} signed in at SkillCDN, checks that the page shows the code ${started.code}, and approves; the console receives the SkillCDN token and keeps it sealed, shown to nobody.\nWaiting for that, until ${moment(started.expiresAt)}.\n`);
|
|
1974
|
+
for (;;) {
|
|
1975
|
+
await io.sleep(started.intervalSeconds * 1000);
|
|
1976
|
+
const state = await holder.standing();
|
|
1977
|
+
if (state.status === "pending") {
|
|
1978
|
+
continue;
|
|
1979
|
+
}
|
|
1980
|
+
if (state.status === "connected") {
|
|
1981
|
+
answer(io, session, state.held, () => `Connected.\n${format(state.held)}`);
|
|
1982
|
+
return EXIT.ok;
|
|
1983
|
+
}
|
|
1984
|
+
if (state.status === "ended") {
|
|
1985
|
+
throw failed(PACK_CONNECT_ENDED[state.reason]);
|
|
1986
|
+
}
|
|
1987
|
+
throw failed("The console no longer waits for the connection; run the command again for a new code.");
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1800
1990
|
const token = (await io.readStdin()).trim();
|
|
1801
1991
|
if (token.length === 0) {
|
|
1802
1992
|
throw failed("No token was given on standard input.");
|
|
1803
1993
|
}
|
|
1804
|
-
const changed = await
|
|
1805
|
-
answer(io, session, changed, () =>
|
|
1994
|
+
const changed = await holder.set(token);
|
|
1995
|
+
answer(io, session, changed, () => format(changed));
|
|
1806
1996
|
return EXIT.ok;
|
|
1997
|
+
}
|
|
1998
|
+
const PACK_TOKEN_FLAGS = {
|
|
1999
|
+
connect: { type: "boolean" },
|
|
2000
|
+
stdin: { type: "boolean" },
|
|
2001
|
+
remove: { type: "boolean" },
|
|
2002
|
+
};
|
|
2003
|
+
const projectPackToken = async (args, io) => {
|
|
2004
|
+
const { values, positionals } = parse(args, PACK_TOKEN_FLAGS);
|
|
2005
|
+
const key = theOne(positionals, "Say which project: console project pack-token <key> --connect, --stdin or --remove");
|
|
2006
|
+
const way = wayOf(values);
|
|
2007
|
+
const session = await open(io, values);
|
|
2008
|
+
const project = session.client.project(key);
|
|
2009
|
+
return packToken(io, session, way, {
|
|
2010
|
+
set: (token) => project.setPackToken({ token }),
|
|
2011
|
+
remove: () => project.removePackToken(),
|
|
2012
|
+
connect: () => project.connectPackToken(),
|
|
2013
|
+
async standing() {
|
|
2014
|
+
const state = await project.packTokenConnection();
|
|
2015
|
+
return state.status === "connected" ? { status: "connected", held: state.project } : state;
|
|
2016
|
+
},
|
|
2017
|
+
}, formatProject);
|
|
1807
2018
|
};
|
|
1808
2019
|
const projectIcon = async (args, io) => {
|
|
1809
2020
|
const { values, positionals } = parse(args, { remove: { type: "boolean" } });
|
|
@@ -1990,6 +2201,7 @@ const COMMANDS = {
|
|
|
1990
2201
|
rules,
|
|
1991
2202
|
people,
|
|
1992
2203
|
person,
|
|
2204
|
+
workspace,
|
|
1993
2205
|
project,
|
|
1994
2206
|
members,
|
|
1995
2207
|
member,
|
|
@@ -2083,11 +2295,35 @@ async function noticeVersions(io, seen) {
|
|
|
2083
2295
|
shownAt: new Date(now).toISOString(),
|
|
2084
2296
|
});
|
|
2085
2297
|
}
|
|
2298
|
+
/**
|
|
2299
|
+
* The refusal of a console older than this command works with (ADR-0038), in a console's own
|
|
2300
|
+
* form: a `426` the command reads as it reads the console's, before it acts on anything the
|
|
2301
|
+
* console answered.
|
|
2302
|
+
*/
|
|
2303
|
+
function consoleRefusal(io, seen) {
|
|
2304
|
+
const command = parseVersion(io.version);
|
|
2305
|
+
const server = parseVersion(seen.version);
|
|
2306
|
+
if (command === undefined || server === undefined) {
|
|
2307
|
+
return undefined;
|
|
2308
|
+
}
|
|
2309
|
+
const { verdict, range } = judgeConsole(server, command, io.consoleAtLeast ?? CONSOLE_AT_LEAST);
|
|
2310
|
+
if (verdict !== "outdated") {
|
|
2311
|
+
return undefined;
|
|
2312
|
+
}
|
|
2313
|
+
return Response.json({
|
|
2314
|
+
error: {
|
|
2315
|
+
code: "console.outdated",
|
|
2316
|
+
message: `This console, ${seen.version}, is older than this command (${io.version}) works with (${formatRange(range)}).`,
|
|
2317
|
+
},
|
|
2318
|
+
}, { status: 426 });
|
|
2319
|
+
}
|
|
2086
2320
|
/** What a refusal of the console is followed by, when there is something to do about it. */
|
|
2087
2321
|
function hintFor(error, io, seen) {
|
|
2088
2322
|
switch (error.code) {
|
|
2089
2323
|
case "client.outdated":
|
|
2090
2324
|
return `Update the command: ${updateWords(io, seen.version)}\n`;
|
|
2325
|
+
case "console.outdated":
|
|
2326
|
+
return `Install the console's version: ${updateWords(io, seen.version)}; or ask the console's operator to update the console.\n`;
|
|
2091
2327
|
case "client.too_new":
|
|
2092
2328
|
return `Install the console's version: ${updateWords(io, seen.version)}; or ask the console's operator to update the console.\n`;
|
|
2093
2329
|
case "auth.required":
|
|
@@ -2124,6 +2360,8 @@ function hintFor(error, io, seen) {
|
|
|
2124
2360
|
return "The person is listed already. console member role <login> <owner|member> changes what they are.\n";
|
|
2125
2361
|
case "member.not_found":
|
|
2126
2362
|
return "The person is not listed in the project. console members lists those who are.\n";
|
|
2363
|
+
case "member.open_project":
|
|
2364
|
+
return "The project is open to the workspace, so everyone of it is a member already. console member add <login> --role owner lists an owner; console member role <login> member takes one back to member.\n";
|
|
2127
2365
|
case "category.key_taken":
|
|
2128
2366
|
return "A category has that key already. console categories lists them.\n";
|
|
2129
2367
|
case "category.not_found":
|
|
@@ -2160,7 +2398,11 @@ export async function runCli(argv, io) {
|
|
|
2160
2398
|
const response = await io.fetch(input, init);
|
|
2161
2399
|
seen.version = response.headers.get(VERSION_HEADER) ?? seen.version;
|
|
2162
2400
|
seen.range = response.headers.get(CLIENT_RANGE_HEADER) ?? seen.range;
|
|
2163
|
-
|
|
2401
|
+
// A console older than this command works with is refused here (ADR-0038); `serve`
|
|
2402
|
+
// carries the pages' own requests, which are not the command's to judge.
|
|
2403
|
+
// A refusal of the console's own is passed on as it is.
|
|
2404
|
+
return ((name === "serve" || response.status === 426 ? undefined : consoleRefusal(io, seen)) ??
|
|
2405
|
+
response);
|
|
2164
2406
|
},
|
|
2165
2407
|
};
|
|
2166
2408
|
try {
|
|
@@ -2191,7 +2433,9 @@ export async function runCli(argv, io) {
|
|
|
2191
2433
|
}
|
|
2192
2434
|
if (error instanceof ApiError) {
|
|
2193
2435
|
io.stderr(`${error.code}: ${error.message}\n${hintFor(error, io, seen)}`);
|
|
2194
|
-
return error.code === "client.outdated" ||
|
|
2436
|
+
return error.code === "client.outdated" ||
|
|
2437
|
+
error.code === "client.too_new" ||
|
|
2438
|
+
error.code === "console.outdated"
|
|
2195
2439
|
? EXIT.incompatible
|
|
2196
2440
|
: EXIT.failed;
|
|
2197
2441
|
}
|