@skillcdn/console 0.1.18 → 0.1.19
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 +1 -1
- package/SKILLCDN.md +1 -1
- package/dist/api.d.ts +1 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +1 -1
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +353 -39
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/credentials.d.ts +102 -3
- package/dist/cli/credentials.d.ts.map +1 -1
- package/dist/cli/credentials.js +177 -8
- package/dist/cli/credentials.js.map +1 -1
- package/dist/cli/directory.d.ts +6 -0
- package/dist/cli/directory.d.ts.map +1 -1
- package/dist/cli/directory.js +37 -6
- 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 +106 -2
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +42 -20
- package/dist/cli/help.js.map +1 -1
- package/dist/client.d.ts +21 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +14 -1
- package/dist/client.js.map +1 -1
- package/dist/components/board.d.ts +9 -1
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +82 -5
- package/dist/components/board.js.map +1 -1
- package/dist/components/comments.d.ts.map +1 -1
- package/dist/components/comments.js +12 -4
- package/dist/components/comments.js.map +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +4 -0
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/goals.d.ts +17 -1
- package/dist/components/goals.d.ts.map +1 -1
- package/dist/components/goals.js +34 -9
- package/dist/components/goals.js.map +1 -1
- package/dist/components/markdown.d.ts +6 -0
- package/dist/components/markdown.d.ts.map +1 -1
- package/dist/components/markdown.js +7 -0
- package/dist/components/markdown.js.map +1 -1
- package/dist/components/pictures.d.ts +68 -0
- package/dist/components/pictures.d.ts.map +1 -0
- package/dist/components/pictures.js +213 -0
- package/dist/components/pictures.js.map +1 -0
- package/dist/components/projects.d.ts +12 -0
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +15 -0
- package/dist/components/projects.js.map +1 -1
- package/dist/components/task-view.d.ts +3 -0
- package/dist/components/task-view.d.ts.map +1 -1
- package/dist/components/task-view.js +6 -4
- package/dist/components/task-view.js.map +1 -1
- package/dist/console.css +282 -3
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +124 -10
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +16 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +47 -2
- package/dist/data.js.map +1 -1
- package/dist/i18n/en.d.ts +52 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +50 -2
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +50 -2
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts +25 -6
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +38 -7
- package/dist/router.js.map +1 -1
- package/dist/rules.js +5 -5
- package/dist/rules.js.map +1 -1
- package/dist/schemas.d.ts +158 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +78 -0
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +2 -2
- 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 +17 -9
- package/docs/specs/rest.md +22 -4
- package/package.json +1 -1
- package/skills/working-the-board/SKILL.md +10 -4
- package/src/api.ts +13 -0
- package/src/cli/cli.ts +441 -42
- package/src/cli/credentials.ts +276 -14
- package/src/cli/directory.ts +43 -6
- package/src/cli/format.ts +138 -2
- package/src/cli/help.ts +44 -20
- package/src/client.ts +60 -0
- package/src/components/board.tsx +154 -8
- package/src/components/comments.tsx +20 -10
- package/src/components/event-feed.tsx +4 -0
- package/src/components/goals.tsx +173 -106
- package/src/components/markdown.tsx +19 -0
- package/src/components/pictures.tsx +389 -0
- package/src/components/projects.tsx +68 -0
- package/src/components/task-view.tsx +43 -2
- package/src/console.tsx +259 -4
- package/src/data.ts +72 -1
- package/src/i18n/en.ts +53 -2
- package/src/i18n/ko.ts +53 -2
- package/src/index.ts +27 -0
- package/src/router.ts +57 -9
- package/src/rules.ts +5 -5
- package/src/schemas.ts +97 -0
- package/src/styles/console.css +282 -3
- package/src/vocabulary.ts +3 -0
package/src/cli/cli.ts
CHANGED
|
@@ -15,7 +15,11 @@ import {
|
|
|
15
15
|
hostOfLogin,
|
|
16
16
|
type RestClaim,
|
|
17
17
|
type RestDecision,
|
|
18
|
+
type RestMember,
|
|
18
19
|
type RestPerson,
|
|
20
|
+
type RestSkillFile,
|
|
21
|
+
type RestSkillLoad,
|
|
22
|
+
type RestSkillPage,
|
|
19
23
|
type RestTaskPatch,
|
|
20
24
|
} from "../schemas.js";
|
|
21
25
|
import {
|
|
@@ -37,8 +41,18 @@ import {
|
|
|
37
41
|
type TaskState,
|
|
38
42
|
} from "../vocabulary.js";
|
|
39
43
|
import { loadWebRoot, type WebRoot, WebRootError } from "../web.js";
|
|
40
|
-
import
|
|
41
|
-
|
|
44
|
+
import {
|
|
45
|
+
type CredentialStore,
|
|
46
|
+
type Credentials,
|
|
47
|
+
CredentialsError,
|
|
48
|
+
EMPTY_CREDENTIALS,
|
|
49
|
+
loginsAt,
|
|
50
|
+
type Resolution,
|
|
51
|
+
resolveLogin,
|
|
52
|
+
withLogin,
|
|
53
|
+
withoutLogin,
|
|
54
|
+
} from "./credentials.js";
|
|
55
|
+
import type { DirectorySettings, DirectoryStore } from "./directory.js";
|
|
42
56
|
import {
|
|
43
57
|
formatAnswer,
|
|
44
58
|
formatCategoryLine,
|
|
@@ -60,6 +74,8 @@ import {
|
|
|
60
74
|
formatRules,
|
|
61
75
|
formatRun,
|
|
62
76
|
formatRunLine,
|
|
77
|
+
formatSkillFile,
|
|
78
|
+
formatSkillLoad,
|
|
63
79
|
formatSkills,
|
|
64
80
|
formatTask,
|
|
65
81
|
formatTaskLine,
|
|
@@ -158,6 +174,7 @@ type OptionSpec = Readonly<
|
|
|
158
174
|
const GLOBAL_OPTIONS: OptionSpec = {
|
|
159
175
|
json: { type: "boolean" },
|
|
160
176
|
url: { type: "string" },
|
|
177
|
+
account: { type: "string" },
|
|
161
178
|
project: { type: "string" },
|
|
162
179
|
};
|
|
163
180
|
|
|
@@ -194,6 +211,12 @@ interface Session {
|
|
|
194
211
|
readonly url: string;
|
|
195
212
|
/** The token the client holds: what `serve` carries the page's requests with. */
|
|
196
213
|
readonly token: string;
|
|
214
|
+
/** The person's login at the console, when the login kept knows it. */
|
|
215
|
+
readonly account: string | undefined;
|
|
216
|
+
/** Where the token came from: the environment's pair, or a login kept here. */
|
|
217
|
+
readonly via: "environment" | "login";
|
|
218
|
+
/** What the command keeps, as it was read for this command. */
|
|
219
|
+
readonly credentials: Credentials | undefined;
|
|
197
220
|
readonly json: boolean;
|
|
198
221
|
/** The project the command works in: named by `--project`, the environment, or the working directory. */
|
|
199
222
|
project(): Promise<ProjectClient>;
|
|
@@ -204,23 +227,85 @@ interface Session {
|
|
|
204
227
|
const NO_PROJECT =
|
|
205
228
|
"Say which project: --project <key>, CONSOLE_PROJECT in the environment, or console use <key> once in this directory. console projects lists the projects you may work in.";
|
|
206
229
|
|
|
207
|
-
/**
|
|
230
|
+
/** What the command keeps, or nothing; a file that cannot be read stops the command, and is never written over. */
|
|
231
|
+
async function loadCredentials(io: CliIo): Promise<Credentials | undefined> {
|
|
232
|
+
try {
|
|
233
|
+
return await io.store.load();
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (error instanceof CredentialsError) {
|
|
236
|
+
throw failed(
|
|
237
|
+
`${error.message} Move it away, then sign in again: console login --url <the console's origin>`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** The console a directory is bound to, as an origin, when it names one. */
|
|
245
|
+
function directoryOrigin(settings: DirectorySettings | undefined): string | undefined {
|
|
246
|
+
if (settings?.url === undefined) {
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
return originOf(settings.url);
|
|
251
|
+
} catch {
|
|
252
|
+
throw failed(`.skillcdn-console.json names a console that is not an origin: ${settings.url}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Why no console could be talked to, for the person who reads it. */
|
|
257
|
+
function wordsOf(failure: Exclude<Resolution, { ok: true }>): string {
|
|
258
|
+
switch (failure.reason) {
|
|
259
|
+
case "nothing":
|
|
260
|
+
return "Not signed in. A person signs the command in: console login --url <the console's origin>";
|
|
261
|
+
case "environment_half":
|
|
262
|
+
return `Both CONSOLE_URL and CONSOLE_TOKEN are needed in the environment, and only ${failure.have} is set: set both, or neither.`;
|
|
263
|
+
case "no_login": {
|
|
264
|
+
const whom = failure.account === undefined ? "" : ` as ${failure.account}`;
|
|
265
|
+
const where =
|
|
266
|
+
failure.source === "directory"
|
|
267
|
+
? `This directory works at ${failure.url}${whom}, where nobody is signed in here`
|
|
268
|
+
: `Nobody is signed in here at ${failure.url}${whom}`;
|
|
269
|
+
return `${where}: console login --url ${failure.url}. console logins lists the consoles signed in.`;
|
|
270
|
+
}
|
|
271
|
+
case "ambiguous":
|
|
272
|
+
return `Several accounts are signed in at ${failure.url} (${failure.accounts.join(", ")}): say which with --account <login>, or bind this directory to one with console use <key> --account <login>.`;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* The console a command talks to, and with what (ADR-0027): the address from the flag, the
|
|
278
|
+
* environment, the directory or the default login, in that order, and then the token kept for
|
|
279
|
+
* that address and no other. A token never travels to a console it was not made at.
|
|
280
|
+
*/
|
|
208
281
|
async function open(io: CliIo, values: Values): Promise<Session> {
|
|
209
|
-
const
|
|
210
|
-
const
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
)
|
|
282
|
+
const directory = await io.directory.load();
|
|
283
|
+
const flag = text(values, "url");
|
|
284
|
+
const envUrl = io.env.CONSOLE_URL;
|
|
285
|
+
const credentials = await loadCredentials(io);
|
|
286
|
+
const resolved = resolveLogin({
|
|
287
|
+
flagUrl: flag === undefined ? undefined : originOf(flag),
|
|
288
|
+
flagAccount: text(values, "account"),
|
|
289
|
+
envUrl: envUrl === undefined ? undefined : originOf(envUrl),
|
|
290
|
+
envToken: io.env.CONSOLE_TOKEN,
|
|
291
|
+
directoryUrl: directoryOrigin(directory),
|
|
292
|
+
directoryAccount: directory?.account,
|
|
293
|
+
credentials,
|
|
294
|
+
});
|
|
295
|
+
if (!resolved.ok) {
|
|
296
|
+
throw failed(wordsOf(resolved));
|
|
216
297
|
}
|
|
298
|
+
const { url, token } = resolved;
|
|
217
299
|
const client = clientOf(io, url, token);
|
|
218
300
|
const projectKey = async () =>
|
|
219
|
-
text(values, "project") ?? io.env.CONSOLE_PROJECT ??
|
|
301
|
+
text(values, "project") ?? io.env.CONSOLE_PROJECT ?? directory?.project;
|
|
220
302
|
return {
|
|
221
303
|
client,
|
|
222
304
|
url,
|
|
223
305
|
token,
|
|
306
|
+
account: resolved.account,
|
|
307
|
+
via: resolved.via,
|
|
308
|
+
credentials,
|
|
224
309
|
json: on(values, "json"),
|
|
225
310
|
projectKey,
|
|
226
311
|
async project() {
|
|
@@ -272,6 +357,9 @@ async function bodyOf(
|
|
|
272
357
|
return given === "-" ? io.readStdin() : given;
|
|
273
358
|
}
|
|
274
359
|
|
|
360
|
+
/** The hosts a token may travel to in the clear: this machine, and no other. */
|
|
361
|
+
const LOOPBACK_HOSTS: ReadonlySet<string> = new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
362
|
+
|
|
275
363
|
function originOf(value: string): string {
|
|
276
364
|
const url = URL.canParse(value) ? new URL(value) : undefined;
|
|
277
365
|
if (
|
|
@@ -285,6 +373,12 @@ function originOf(value: string): string {
|
|
|
285
373
|
`Not an origin: ${value}. Say the console's address without a path, such as https://console.example.com`,
|
|
286
374
|
);
|
|
287
375
|
}
|
|
376
|
+
// A token travels over https; in the clear only to this machine, where a console is run for development.
|
|
377
|
+
if (url.protocol === "http:" && !LOOPBACK_HOSTS.has(url.hostname)) {
|
|
378
|
+
throw misuse(
|
|
379
|
+
`Not a safe origin: ${value}. A token travels over https only; http is for a console on this machine (localhost).`,
|
|
380
|
+
);
|
|
381
|
+
}
|
|
288
382
|
return url.origin;
|
|
289
383
|
}
|
|
290
384
|
|
|
@@ -517,8 +611,17 @@ async function settle(
|
|
|
517
611
|
|
|
518
612
|
type Command = (args: readonly string[], io: CliIo) => Promise<number>;
|
|
519
613
|
|
|
520
|
-
/**
|
|
521
|
-
|
|
614
|
+
/**
|
|
615
|
+
* Checks a token against the console, keeps it as the login at that console for that account,
|
|
616
|
+
* and says who it is. A token the login replaces is given up at the console on the way.
|
|
617
|
+
*/
|
|
618
|
+
async function keep(
|
|
619
|
+
io: CliIo,
|
|
620
|
+
url: string,
|
|
621
|
+
token: string,
|
|
622
|
+
agent: string | undefined,
|
|
623
|
+
asDefault: boolean,
|
|
624
|
+
): Promise<number> {
|
|
522
625
|
const me = await clientOf(io, url, token).me();
|
|
523
626
|
const person = me.person;
|
|
524
627
|
if (person === null) {
|
|
@@ -526,9 +629,22 @@ async function keep(io: CliIo, url: string, token: string, agent?: string): Prom
|
|
|
526
629
|
"The console does not know this token. Make one on your Agents page and try again.",
|
|
527
630
|
);
|
|
528
631
|
}
|
|
529
|
-
const
|
|
632
|
+
const credentials = (await loadCredentials(io)) ?? EMPTY_CREDENTIALS;
|
|
633
|
+
const replaced = loginsAt(credentials, url).find((login) => login.account === person.login);
|
|
634
|
+
if (replaced !== undefined && replaced.token !== token) {
|
|
635
|
+
await clientOf(io, url, replaced.token)
|
|
636
|
+
.giveUpToken()
|
|
637
|
+
.catch(() => undefined);
|
|
638
|
+
}
|
|
639
|
+
const next = withLogin(
|
|
640
|
+
credentials,
|
|
641
|
+
{ url, account: person.login, token, agent, since: new Date(io.now()).toISOString() },
|
|
642
|
+
{ default: asDefault },
|
|
643
|
+
);
|
|
644
|
+
const kept = await io.store.save(next);
|
|
645
|
+
const standing = next.default === url ? ", the default console here" : "";
|
|
530
646
|
io.stdout(
|
|
531
|
-
`${agent === undefined ? "Signed in" : `Connected as "${agent}"`} to ${me.workspace.name} at ${url} as ${person.login} (${person.role}). The token is kept in ${kept}.\nNext, say which project this directory works in: console use <key> (console projects lists them).\n`,
|
|
647
|
+
`${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`,
|
|
532
648
|
);
|
|
533
649
|
return EXIT.ok;
|
|
534
650
|
}
|
|
@@ -536,21 +652,22 @@ async function keep(io: CliIo, url: string, token: string, agent?: string): Prom
|
|
|
536
652
|
const login: Command = async (args, io) => {
|
|
537
653
|
const { values } = parse(
|
|
538
654
|
args,
|
|
539
|
-
{ "token-stdin": { type: "boolean" }, agent: { type: "string" } },
|
|
655
|
+
{ "token-stdin": { type: "boolean" }, agent: { type: "string" }, default: { type: "boolean" } },
|
|
540
656
|
false,
|
|
541
657
|
);
|
|
542
|
-
const
|
|
543
|
-
const wanted = text(values, "url") ?? io.env.CONSOLE_URL ??
|
|
658
|
+
const credentials = await loadCredentials(io);
|
|
659
|
+
const wanted = text(values, "url") ?? io.env.CONSOLE_URL ?? credentials?.default;
|
|
544
660
|
if (wanted === undefined) {
|
|
545
661
|
throw misuse("Say where the console is: console login --url https://console.example.com");
|
|
546
662
|
}
|
|
547
663
|
const url = originOf(wanted);
|
|
664
|
+
const asDefault = on(values, "default");
|
|
548
665
|
if (on(values, "token-stdin")) {
|
|
549
666
|
const token = (await io.readStdin()).trim();
|
|
550
667
|
if (token.length === 0) {
|
|
551
668
|
throw failed("No token was given.");
|
|
552
669
|
}
|
|
553
|
-
return keep(io, url, token);
|
|
670
|
+
return keep(io, url, token, undefined, asDefault);
|
|
554
671
|
}
|
|
555
672
|
// A connection (ADR-0011): a code a person approves on the console's own pages, and the token
|
|
556
673
|
// handed over here once, never shown.
|
|
@@ -577,40 +694,110 @@ const login: Command = async (args, io) => {
|
|
|
577
694
|
throw error;
|
|
578
695
|
}
|
|
579
696
|
if (claim.status === "connected") {
|
|
580
|
-
return keep(io, url, claim.secret, claim.token.name);
|
|
697
|
+
return keep(io, url, claim.secret, claim.token.name, asDefault);
|
|
581
698
|
}
|
|
582
699
|
}
|
|
583
700
|
};
|
|
584
701
|
|
|
585
702
|
const logout: Command = async (args, io) => {
|
|
586
|
-
parse(args, {}, false);
|
|
587
|
-
const
|
|
588
|
-
const
|
|
589
|
-
const
|
|
590
|
-
|
|
703
|
+
const { values } = parse(args, {}, false);
|
|
704
|
+
const credentials = await loadCredentials(io);
|
|
705
|
+
const directory = await io.directory.load();
|
|
706
|
+
const envNote = () => {
|
|
707
|
+
if (io.env.CONSOLE_TOKEN !== undefined) {
|
|
708
|
+
io.stdout("CONSOLE_TOKEN in the environment still holds a token: unset it.\n");
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
if (credentials === undefined || credentials.logins.length === 0) {
|
|
591
712
|
io.stdout("Not signed in here: nothing to forget.\n");
|
|
713
|
+
envNote();
|
|
592
714
|
return EXIT.ok;
|
|
593
715
|
}
|
|
716
|
+
// Which login: the console the flag names, else the directory's, else the default, else the
|
|
717
|
+
// one console signed in to; and the account the same way, where the console has several.
|
|
718
|
+
const consoles = [...new Set(credentials.logins.map((login) => login.url))];
|
|
719
|
+
const flag = text(values, "url");
|
|
720
|
+
const url =
|
|
721
|
+
flag === undefined
|
|
722
|
+
? (directoryOrigin(directory) ??
|
|
723
|
+
credentials.default ??
|
|
724
|
+
(consoles.length === 1 ? consoles[0] : undefined))
|
|
725
|
+
: originOf(flag);
|
|
726
|
+
if (url === undefined) {
|
|
727
|
+
throw failed(
|
|
728
|
+
`Several consoles are signed in here; say which: console logout --url <origin>. console logins lists them.`,
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
const account = text(values, "account") ?? directory?.account;
|
|
732
|
+
const at = loginsAt(credentials, url);
|
|
733
|
+
const chosen = account === undefined ? at : at.filter((login) => login.account === account);
|
|
734
|
+
const [login] = chosen;
|
|
735
|
+
if (login === undefined) {
|
|
736
|
+
throw failed(
|
|
737
|
+
`Nobody is signed in here at ${url}${account === undefined ? "" : ` as ${account}`}: nothing to forget. console logins lists the consoles signed in.`,
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
if (chosen.length > 1) {
|
|
741
|
+
throw failed(
|
|
742
|
+
`Several accounts are signed in at ${url} (${chosen.map((kept) => kept.account ?? "?").join(", ")}); say which: console logout --url ${url} --account <login>`,
|
|
743
|
+
);
|
|
744
|
+
}
|
|
594
745
|
// The token is given up at the console first, so that it is good for nothing from now on;
|
|
595
746
|
// one the console no longer knows is gone already. A console out of reach leaves it to the
|
|
596
747
|
// Agents page, and the person is told so.
|
|
597
748
|
let gone = true;
|
|
598
749
|
try {
|
|
599
|
-
await clientOf(io, url, token).giveUpToken();
|
|
750
|
+
await clientOf(io, url, login.token).giveUpToken();
|
|
600
751
|
} catch (error) {
|
|
601
752
|
if (!(error instanceof ApiError && error.code === "auth.required")) {
|
|
602
753
|
gone = false;
|
|
603
754
|
}
|
|
604
755
|
}
|
|
605
|
-
|
|
756
|
+
const next = withoutLogin(credentials, url, login.account);
|
|
757
|
+
if (next.logins.length === 0) {
|
|
758
|
+
await io.store.clear();
|
|
759
|
+
} else {
|
|
760
|
+
await io.store.save(next);
|
|
761
|
+
}
|
|
762
|
+
const whom = login.account === undefined ? "" : ` as ${login.account}`;
|
|
606
763
|
io.stdout(
|
|
607
764
|
gone
|
|
608
|
-
?
|
|
609
|
-
: `Signed out here, but the console
|
|
765
|
+
? `Signed out of ${url}${whom}: the token is given up at the console and forgotten here.\n`
|
|
766
|
+
: `Signed out of ${url}${whom} here, but the console could not be reached to give the token up: a person removes it on the Agents page.\n`,
|
|
610
767
|
);
|
|
611
|
-
|
|
612
|
-
|
|
768
|
+
envNote();
|
|
769
|
+
return EXIT.ok;
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
/** The consoles this machine is signed in to, with the account at each; never a token. */
|
|
773
|
+
const logins: Command = async (args, io) => {
|
|
774
|
+
const { values } = parse(args, {}, false);
|
|
775
|
+
const credentials = await loadCredentials(io);
|
|
776
|
+
const kept = (credentials?.logins ?? []).map((login) => ({
|
|
777
|
+
url: login.url,
|
|
778
|
+
account: login.account ?? null,
|
|
779
|
+
agent: login.agent ?? null,
|
|
780
|
+
since: login.since ?? null,
|
|
781
|
+
default: credentials?.default === login.url,
|
|
782
|
+
}));
|
|
783
|
+
if (on(values, "json")) {
|
|
784
|
+
io.stdout(
|
|
785
|
+
`${JSON.stringify({ default: credentials?.default ?? null, logins: kept }, null, 2)}\n`,
|
|
786
|
+
);
|
|
787
|
+
return EXIT.ok;
|
|
613
788
|
}
|
|
789
|
+
if (kept.length === 0) {
|
|
790
|
+
io.stdout("Not signed in anywhere here: console login --url <the console's origin>\n");
|
|
791
|
+
return EXIT.ok;
|
|
792
|
+
}
|
|
793
|
+
io.stdout(
|
|
794
|
+
`${kept
|
|
795
|
+
.map(
|
|
796
|
+
(login) =>
|
|
797
|
+
`${login.url} ${login.account ?? "(account unknown)"}${login.agent === null ? "" : ` ${login.agent}`}${login.since === null ? "" : ` since ${login.since.slice(0, 10)}`}${login.default ? " (default)" : ""}`,
|
|
798
|
+
)
|
|
799
|
+
.join("\n")}\n`,
|
|
800
|
+
);
|
|
614
801
|
return EXIT.ok;
|
|
615
802
|
};
|
|
616
803
|
|
|
@@ -684,16 +871,35 @@ const projects: Command = async (args, io) => {
|
|
|
684
871
|
|
|
685
872
|
const use: Command = async (args, io) => {
|
|
686
873
|
const { values, positionals } = parse(args, {});
|
|
687
|
-
const key = theOne(
|
|
874
|
+
const key = theOne(
|
|
875
|
+
positionals,
|
|
876
|
+
"Say which project: console use <key> [--url <origin>] [--account <login>]",
|
|
877
|
+
);
|
|
688
878
|
const session = await open(io, values);
|
|
689
879
|
const project = await session.client.project(key).get();
|
|
690
|
-
|
|
880
|
+
// The directory names the console where that is not the default login here, or when asked,
|
|
881
|
+
// and the account where the console has several, or when asked; never a token.
|
|
882
|
+
const credentials = session.credentials ?? EMPTY_CREDENTIALS;
|
|
883
|
+
const pinUrl =
|
|
884
|
+
text(values, "url") !== undefined ||
|
|
885
|
+
(session.via === "login" && credentials.default !== session.url);
|
|
886
|
+
const pinAccount =
|
|
887
|
+
text(values, "account") !== undefined ||
|
|
888
|
+
(session.via === "login" && loginsAt(credentials, session.url).length > 1);
|
|
889
|
+
const kept = await io.directory.save({
|
|
890
|
+
project: project.key,
|
|
891
|
+
...(pinUrl ? { url: session.url } : {}),
|
|
892
|
+
...(pinAccount && session.account !== undefined ? { account: session.account } : {}),
|
|
893
|
+
});
|
|
894
|
+
const bound = pinUrl
|
|
895
|
+
? ` at ${session.url}${pinAccount && session.account !== undefined ? ` as ${session.account}` : ""}`
|
|
896
|
+
: "";
|
|
691
897
|
answer(
|
|
692
898
|
io,
|
|
693
899
|
session,
|
|
694
900
|
project,
|
|
695
901
|
() =>
|
|
696
|
-
`Working in ${project.name} (${project.key}) from this directory on: kept in ${kept}, which is meant to be committed. What its people ask of agents: console rules.`,
|
|
902
|
+
`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.`,
|
|
697
903
|
);
|
|
698
904
|
return EXIT.ok;
|
|
699
905
|
};
|
|
@@ -705,14 +911,21 @@ const tasks: Command = async (args, io) => {
|
|
|
705
911
|
state: { type: "string" },
|
|
706
912
|
open: { type: "boolean" },
|
|
707
913
|
mine: { type: "boolean" },
|
|
914
|
+
unassigned: { type: "boolean" },
|
|
708
915
|
category: { type: "string" },
|
|
709
916
|
goal: { type: "string" },
|
|
710
917
|
due: { type: "string" },
|
|
918
|
+
archived: { type: "boolean" },
|
|
711
919
|
...PAGE_OPTIONS,
|
|
712
920
|
},
|
|
713
921
|
false,
|
|
714
922
|
);
|
|
715
923
|
const state = stateOf(text(values, "state"));
|
|
924
|
+
if (on(values, "mine") && on(values, "unassigned")) {
|
|
925
|
+
throw misuse(
|
|
926
|
+
"Say one: --mine for the tasks assigned to you, or --unassigned for those assigned to nobody",
|
|
927
|
+
);
|
|
928
|
+
}
|
|
716
929
|
const page = pageOf(values);
|
|
717
930
|
const session = await open(io, values);
|
|
718
931
|
const project = await session.project();
|
|
@@ -734,9 +947,11 @@ const tasks: Command = async (args, io) => {
|
|
|
734
947
|
...(state === undefined ? {} : { state }),
|
|
735
948
|
...(on(values, "open") || due === "overdue" ? { open: true } : {}),
|
|
736
949
|
...(assignee === undefined ? {} : { assignee }),
|
|
950
|
+
...(on(values, "unassigned") ? { unassigned: true } : {}),
|
|
737
951
|
...(text(values, "category") === undefined ? {} : { category: text(values, "category") }),
|
|
738
952
|
...(text(values, "goal") === undefined ? {} : { goal: text(values, "goal") }),
|
|
739
953
|
...(dueBy === undefined ? {} : { dueBy }),
|
|
954
|
+
...(on(values, "archived") ? { archived: true } : {}),
|
|
740
955
|
...page,
|
|
741
956
|
});
|
|
742
957
|
answer(io, session, found, () =>
|
|
@@ -783,6 +998,65 @@ const skills: Command = async (args, io) => {
|
|
|
783
998
|
return EXIT.ok;
|
|
784
999
|
};
|
|
785
1000
|
|
|
1001
|
+
/** How many pages of a skill, or of a file, the command follows before it stops: far above what one holds. */
|
|
1002
|
+
const MAX_SKILL_PAGES = 64;
|
|
1003
|
+
|
|
1004
|
+
const skill: Command = async (args, io) => {
|
|
1005
|
+
const { values, positionals } = parse(args, { file: { type: "string" } });
|
|
1006
|
+
const path = theOne(
|
|
1007
|
+
positionals,
|
|
1008
|
+
"Say which skill: console skill <path of its SKILL.md> [--file <path>]",
|
|
1009
|
+
);
|
|
1010
|
+
const session = await open(io, values);
|
|
1011
|
+
const project = await session.project();
|
|
1012
|
+
const file = text(values, "file");
|
|
1013
|
+
if (file !== undefined) {
|
|
1014
|
+
// The file whole, page after page, as the console reads it from the deployment.
|
|
1015
|
+
const parts: string[] = [];
|
|
1016
|
+
let offset: number | undefined;
|
|
1017
|
+
let last: RestSkillFile | undefined;
|
|
1018
|
+
for (let pages = 0; pages < MAX_SKILL_PAGES; pages += 1) {
|
|
1019
|
+
last = await project.skillFile(file, offset === undefined ? {} : { offset });
|
|
1020
|
+
if (last.status !== "ready" || last.file === null) {
|
|
1021
|
+
break;
|
|
1022
|
+
}
|
|
1023
|
+
parts.push(last.file.content);
|
|
1024
|
+
if (last.file.nextOffset === null) {
|
|
1025
|
+
break;
|
|
1026
|
+
}
|
|
1027
|
+
offset = last.file.nextOffset;
|
|
1028
|
+
}
|
|
1029
|
+
if (last === undefined) {
|
|
1030
|
+
throw failed("The file could not be read.");
|
|
1031
|
+
}
|
|
1032
|
+
const read = last;
|
|
1033
|
+
const content = parts.join("");
|
|
1034
|
+
answer(io, session, { ...read, content }, () => formatSkillFile(read, content));
|
|
1035
|
+
return EXIT.ok;
|
|
1036
|
+
}
|
|
1037
|
+
// The skill whole: its pages until complete, the rules first, as load_skill lays them out.
|
|
1038
|
+
const pages: RestSkillPage[] = [];
|
|
1039
|
+
let cursor: string | undefined;
|
|
1040
|
+
let last: RestSkillLoad | undefined;
|
|
1041
|
+
for (let n = 0; n < MAX_SKILL_PAGES; n += 1) {
|
|
1042
|
+
last = await project.skill(path, cursor);
|
|
1043
|
+
if (last.status !== "ready" || last.skill === null) {
|
|
1044
|
+
break;
|
|
1045
|
+
}
|
|
1046
|
+
pages.push(last.skill);
|
|
1047
|
+
if (last.skill.complete || last.skill.nextCursor === null) {
|
|
1048
|
+
break;
|
|
1049
|
+
}
|
|
1050
|
+
cursor = last.skill.nextCursor;
|
|
1051
|
+
}
|
|
1052
|
+
if (last === undefined) {
|
|
1053
|
+
throw failed("The skill could not be read.");
|
|
1054
|
+
}
|
|
1055
|
+
const loaded = last;
|
|
1056
|
+
answer(io, session, { ...loaded, pages }, () => formatSkillLoad(loaded, pages));
|
|
1057
|
+
return EXIT.ok;
|
|
1058
|
+
};
|
|
1059
|
+
|
|
786
1060
|
/** A version number given as a flag, or nothing; anything else is not understood. */
|
|
787
1061
|
function versionOf(given: string | undefined): number | undefined {
|
|
788
1062
|
if (given === undefined) {
|
|
@@ -983,6 +1257,45 @@ const editTask: Command = async (args, io) => {
|
|
|
983
1257
|
return EXIT.ok;
|
|
984
1258
|
};
|
|
985
1259
|
|
|
1260
|
+
/** Puts a task away, or every done or dropped task of a goal (ADR-0029); the task by its number or its id. */
|
|
1261
|
+
const taskArchive: Command = async (args, io) => {
|
|
1262
|
+
const { values, positionals } = parse(args, { goal: { type: "string" } });
|
|
1263
|
+
const goal = text(values, "goal");
|
|
1264
|
+
if (positionals.length > 1 || (positionals.length === 0) === (goal === undefined)) {
|
|
1265
|
+
throw misuse(
|
|
1266
|
+
"Say which: console task archive <number|id>, or console task archive --goal <id> for every done or dropped task of a goal",
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
const session = await open(io, values);
|
|
1270
|
+
const project = await session.project();
|
|
1271
|
+
if (goal !== undefined) {
|
|
1272
|
+
const { archived } = await project.archiveTasksOfGoal(goal);
|
|
1273
|
+
answer(
|
|
1274
|
+
io,
|
|
1275
|
+
session,
|
|
1276
|
+
{ archived },
|
|
1277
|
+
() => `${archived} ${archived === 1 ? "task" : "tasks"} of the goal put away.`,
|
|
1278
|
+
);
|
|
1279
|
+
return EXIT.ok;
|
|
1280
|
+
}
|
|
1281
|
+
const found = await project.task(positionals[0] ?? "");
|
|
1282
|
+
const archived = await project.archiveTask(found.id);
|
|
1283
|
+
answer(io, session, archived, () => formatTaskLine(archived));
|
|
1284
|
+
return EXIT.ok;
|
|
1285
|
+
};
|
|
1286
|
+
|
|
1287
|
+
/** Brings a task back from the archive. */
|
|
1288
|
+
const taskRestore: Command = async (args, io) => {
|
|
1289
|
+
const { values, positionals } = parse(args, {});
|
|
1290
|
+
const ref = theOne(positionals, "Say which task: console task restore <number|id>");
|
|
1291
|
+
const session = await open(io, values);
|
|
1292
|
+
const project = await session.project();
|
|
1293
|
+
const found = await project.task(ref);
|
|
1294
|
+
const restored = await project.restoreTask(found.id);
|
|
1295
|
+
answer(io, session, restored, () => formatTaskLine(restored));
|
|
1296
|
+
return EXIT.ok;
|
|
1297
|
+
};
|
|
1298
|
+
|
|
986
1299
|
const task: Command = async (args, io) => {
|
|
987
1300
|
if (args[0] === "new") {
|
|
988
1301
|
return newTask(args.slice(1), io);
|
|
@@ -990,6 +1303,12 @@ const task: Command = async (args, io) => {
|
|
|
990
1303
|
if (args[0] === "edit") {
|
|
991
1304
|
return editTask(args.slice(1), io);
|
|
992
1305
|
}
|
|
1306
|
+
if (args[0] === "archive") {
|
|
1307
|
+
return taskArchive(args.slice(1), io);
|
|
1308
|
+
}
|
|
1309
|
+
if (args[0] === "restore") {
|
|
1310
|
+
return taskRestore(args.slice(1), io);
|
|
1311
|
+
}
|
|
993
1312
|
const { values, positionals } = parse(args, {});
|
|
994
1313
|
const ref = theOne(positionals, "Say which task: console task <number|id>");
|
|
995
1314
|
const session = await open(io, values);
|
|
@@ -1871,6 +2190,37 @@ const editProject: Command = async (args, io) => {
|
|
|
1871
2190
|
return EXIT.ok;
|
|
1872
2191
|
};
|
|
1873
2192
|
|
|
2193
|
+
/** The pack token (ADR-0028): read from standard input and never from an argument, so that no shell keeps it. */
|
|
2194
|
+
const projectPackToken: Command = async (args, io) => {
|
|
2195
|
+
const { values, positionals } = parse(args, {
|
|
2196
|
+
stdin: { type: "boolean" },
|
|
2197
|
+
remove: { type: "boolean" },
|
|
2198
|
+
});
|
|
2199
|
+
const key = theOne(
|
|
2200
|
+
positionals,
|
|
2201
|
+
"Say which project: console project pack-token <key> --stdin, or console project pack-token <key> --remove",
|
|
2202
|
+
);
|
|
2203
|
+
if (on(values, "stdin") === on(values, "remove")) {
|
|
2204
|
+
throw misuse(
|
|
2205
|
+
"Say one of --stdin, which reads the token from standard input, and --remove, which takes it away.",
|
|
2206
|
+
);
|
|
2207
|
+
}
|
|
2208
|
+
const session = await open(io, values);
|
|
2209
|
+
const project = session.client.project(key);
|
|
2210
|
+
if (on(values, "remove")) {
|
|
2211
|
+
const changed = await project.removePackToken();
|
|
2212
|
+
answer(io, session, changed, () => formatProject(changed));
|
|
2213
|
+
return EXIT.ok;
|
|
2214
|
+
}
|
|
2215
|
+
const token = (await io.readStdin()).trim();
|
|
2216
|
+
if (token.length === 0) {
|
|
2217
|
+
throw failed("No token was given on standard input.");
|
|
2218
|
+
}
|
|
2219
|
+
const changed = await project.setPackToken({ token });
|
|
2220
|
+
answer(io, session, changed, () => formatProject(changed));
|
|
2221
|
+
return EXIT.ok;
|
|
2222
|
+
};
|
|
2223
|
+
|
|
1874
2224
|
const projectIcon: Command = async (args, io) => {
|
|
1875
2225
|
const { values, positionals } = parse(args, { remove: { type: "boolean" } });
|
|
1876
2226
|
const [key, file] = positionals;
|
|
@@ -1904,10 +2254,13 @@ const project: Command = async (args, io) => {
|
|
|
1904
2254
|
if (args[0] === "icon") {
|
|
1905
2255
|
return projectIcon(args.slice(1), io);
|
|
1906
2256
|
}
|
|
2257
|
+
if (args[0] === "pack-token") {
|
|
2258
|
+
return projectPackToken(args.slice(1), io);
|
|
2259
|
+
}
|
|
1907
2260
|
const { values, positionals } = parse(args, {});
|
|
1908
2261
|
const key = theOne(
|
|
1909
2262
|
positionals,
|
|
1910
|
-
"Say which project: console project <key>; or console project new, edit or
|
|
2263
|
+
"Say which project: console project <key>; or console project new, edit, icon or pack-token",
|
|
1911
2264
|
);
|
|
1912
2265
|
const session = await open(io, values);
|
|
1913
2266
|
const found = await session.client.project(key).get();
|
|
@@ -1928,18 +2281,59 @@ const members: Command = async (args, io) => {
|
|
|
1928
2281
|
return EXIT.ok;
|
|
1929
2282
|
};
|
|
1930
2283
|
|
|
2284
|
+
/** The keys --project names with commas, each once, or nothing when it names one project or none. */
|
|
2285
|
+
function projectKeysOf(value: string | undefined): readonly string[] | undefined {
|
|
2286
|
+
if (value === undefined || !value.includes(",")) {
|
|
2287
|
+
return undefined;
|
|
2288
|
+
}
|
|
2289
|
+
const keys = [...new Set(value.split(",").map((key) => key.trim()))].filter((key) => key !== "");
|
|
2290
|
+
if (keys.length === 0) {
|
|
2291
|
+
throw misuse("Say which projects: --project <key>[,<key>...]");
|
|
2292
|
+
}
|
|
2293
|
+
return keys;
|
|
2294
|
+
}
|
|
2295
|
+
|
|
1931
2296
|
const member: Command = async (args, io) => {
|
|
1932
2297
|
const [verb, ...rest] = args;
|
|
1933
2298
|
if (verb === "add") {
|
|
1934
2299
|
const { values, positionals } = parse(rest, { role: { type: "string" } });
|
|
1935
|
-
const login = theOne(
|
|
2300
|
+
const login = theOne(
|
|
2301
|
+
positionals,
|
|
2302
|
+
"Say whom: console member add <login> [--role owner|member] [--project <key>[,<key>...]]",
|
|
2303
|
+
);
|
|
1936
2304
|
const role = text(values, "role");
|
|
1937
2305
|
const session = await open(io, values);
|
|
1938
|
-
const
|
|
1939
|
-
const added = await project.addMember({
|
|
2306
|
+
const input = {
|
|
1940
2307
|
personId: (await personOf(session, login)).id,
|
|
1941
2308
|
...(role === undefined ? {} : { role: projectRoleOf(role) }),
|
|
1942
|
-
}
|
|
2309
|
+
};
|
|
2310
|
+
// Several projects at once, where --project names them with commas: the person is listed
|
|
2311
|
+
// in each in turn, one listed already is said so, and the rest go on.
|
|
2312
|
+
const keys = projectKeysOf(text(values, "project"));
|
|
2313
|
+
if (keys !== undefined) {
|
|
2314
|
+
const added: { readonly project: string; readonly member: RestMember | null }[] = [];
|
|
2315
|
+
for (const key of keys) {
|
|
2316
|
+
try {
|
|
2317
|
+
added.push({ project: key, member: await session.client.project(key).addMember(input) });
|
|
2318
|
+
} catch (error) {
|
|
2319
|
+
if (!(error instanceof ApiError && error.code === "member.exists")) {
|
|
2320
|
+
throw error;
|
|
2321
|
+
}
|
|
2322
|
+
added.push({ project: key, member: null });
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
answer(io, session, added, () =>
|
|
2326
|
+
added
|
|
2327
|
+
.map(
|
|
2328
|
+
({ project, member }) =>
|
|
2329
|
+
`${project} ${member === null ? `${login} is listed already` : formatMemberLine(member)}`,
|
|
2330
|
+
)
|
|
2331
|
+
.join("\n"),
|
|
2332
|
+
);
|
|
2333
|
+
return EXIT.ok;
|
|
2334
|
+
}
|
|
2335
|
+
const project = await session.project();
|
|
2336
|
+
const added = await project.addMember(input);
|
|
1943
2337
|
answer(io, session, added, () => formatMemberLine(added));
|
|
1944
2338
|
return EXIT.ok;
|
|
1945
2339
|
}
|
|
@@ -1974,7 +2368,7 @@ const member: Command = async (args, io) => {
|
|
|
1974
2368
|
return EXIT.ok;
|
|
1975
2369
|
}
|
|
1976
2370
|
throw misuse(
|
|
1977
|
-
"Say what to do: console member add <login> [--role owner|member], console member role <login> <owner|member>, or console member remove <login>",
|
|
2371
|
+
"Say what to do: console member add <login> [--role owner|member] [--project <key>[,<key>...]], console member role <login> <owner|member>, or console member remove <login>",
|
|
1978
2372
|
);
|
|
1979
2373
|
};
|
|
1980
2374
|
|
|
@@ -2034,6 +2428,7 @@ const category: Command = async (args, io) => {
|
|
|
2034
2428
|
|
|
2035
2429
|
const COMMANDS: Readonly<Record<string, Command>> = {
|
|
2036
2430
|
login,
|
|
2431
|
+
logins,
|
|
2037
2432
|
logout,
|
|
2038
2433
|
whoami,
|
|
2039
2434
|
serve,
|
|
@@ -2046,6 +2441,7 @@ const COMMANDS: Readonly<Record<string, Command>> = {
|
|
|
2046
2441
|
goal,
|
|
2047
2442
|
categories,
|
|
2048
2443
|
skills,
|
|
2444
|
+
skill,
|
|
2049
2445
|
rules,
|
|
2050
2446
|
people,
|
|
2051
2447
|
person,
|
|
@@ -2087,11 +2483,14 @@ function help(io: CliIo, name: string | undefined): number {
|
|
|
2087
2483
|
return EXIT.ok;
|
|
2088
2484
|
}
|
|
2089
2485
|
|
|
2090
|
-
/**
|
|
2486
|
+
/**
|
|
2487
|
+
* A client of the console at `url`, naming the command on every request (ADR-0025). A redirect
|
|
2488
|
+
* is not followed: the token goes to the address the command resolved, and nowhere it is sent on to.
|
|
2489
|
+
*/
|
|
2091
2490
|
function clientOf(io: CliIo, url: string, token?: string): ConsoleClient {
|
|
2092
2491
|
return createClient({
|
|
2093
2492
|
baseUrl: url,
|
|
2094
|
-
fetch: io.fetch,
|
|
2493
|
+
fetch: (input, init) => io.fetch(input, { ...init, redirect: "manual" }),
|
|
2095
2494
|
...(token === undefined ? {} : { token }),
|
|
2096
2495
|
...(io.userAgent === undefined ? {} : { userAgent: io.userAgent }),
|
|
2097
2496
|
});
|