@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/dist/cli/cli.js
CHANGED
|
@@ -8,7 +8,8 @@ import { fillRules, RULES_PATH, RULES_TITLE } from "../rules.js";
|
|
|
8
8
|
import { hostOfLogin, } from "../schemas.js";
|
|
9
9
|
import { COLOR_NAMES, GOAL_STATES, PERSON_ROLES, PROJECT_ROLES, PROJECT_VISIBILITIES, PROVIDER_KEYS, TASK_PRIORITIES, TASK_STATES, } from "../vocabulary.js";
|
|
10
10
|
import { loadWebRoot, WebRootError } from "../web.js";
|
|
11
|
-
import {
|
|
11
|
+
import { CredentialsError, EMPTY_CREDENTIALS, loginsAt, resolveLogin, withLogin, withoutLogin, } from "./credentials.js";
|
|
12
|
+
import { formatAnswer, formatCategoryLine, formatComment, formatDecision, formatDecisionLine, formatDocument, formatDocuments, formatGoal, formatGoalLine, formatListingLine, formatMemberLine, formatMore, formatOptions, formatPersonLine, formatPersonWork, formatProject, formatProjectLine, formatRules, formatRun, formatRunLine, formatSkillFile, formatSkillLoad, formatSkills, formatTask, formatTaskLine, formatVersion, formatVersionLine, formatWork, } from "./format.js";
|
|
12
13
|
import { COMMAND_HELP, helpFor, usage } from "./help.js";
|
|
13
14
|
import { describeInstall, updateCommandFor } from "./install.js";
|
|
14
15
|
import { createServeHandler, DEFAULT_SERVE_PORT } from "./serve.js";
|
|
@@ -53,6 +54,7 @@ const misuse = (message) => new CliExit(EXIT.usage, message);
|
|
|
53
54
|
const GLOBAL_OPTIONS = {
|
|
54
55
|
json: { type: "boolean" },
|
|
55
56
|
url: { type: "string" },
|
|
57
|
+
account: { type: "string" },
|
|
56
58
|
project: { type: "string" },
|
|
57
59
|
};
|
|
58
60
|
function parse(args, options, allowPositionals = true) {
|
|
@@ -81,20 +83,80 @@ const texts = (values, name) => {
|
|
|
81
83
|
return typeof value === "string" ? [value] : [];
|
|
82
84
|
};
|
|
83
85
|
const NO_PROJECT = "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.";
|
|
84
|
-
/**
|
|
86
|
+
/** What the command keeps, or nothing; a file that cannot be read stops the command, and is never written over. */
|
|
87
|
+
async function loadCredentials(io) {
|
|
88
|
+
try {
|
|
89
|
+
return await io.store.load();
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (error instanceof CredentialsError) {
|
|
93
|
+
throw failed(`${error.message} Move it away, then sign in again: console login --url <the console's origin>`);
|
|
94
|
+
}
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** The console a directory is bound to, as an origin, when it names one. */
|
|
99
|
+
function directoryOrigin(settings) {
|
|
100
|
+
if (settings?.url === undefined) {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
return originOf(settings.url);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
throw failed(`.skillcdn-console.json names a console that is not an origin: ${settings.url}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** Why no console could be talked to, for the person who reads it. */
|
|
111
|
+
function wordsOf(failure) {
|
|
112
|
+
switch (failure.reason) {
|
|
113
|
+
case "nothing":
|
|
114
|
+
return "Not signed in. A person signs the command in: console login --url <the console's origin>";
|
|
115
|
+
case "environment_half":
|
|
116
|
+
return `Both CONSOLE_URL and CONSOLE_TOKEN are needed in the environment, and only ${failure.have} is set: set both, or neither.`;
|
|
117
|
+
case "no_login": {
|
|
118
|
+
const whom = failure.account === undefined ? "" : ` as ${failure.account}`;
|
|
119
|
+
const where = failure.source === "directory"
|
|
120
|
+
? `This directory works at ${failure.url}${whom}, where nobody is signed in here`
|
|
121
|
+
: `Nobody is signed in here at ${failure.url}${whom}`;
|
|
122
|
+
return `${where}: console login --url ${failure.url}. console logins lists the consoles signed in.`;
|
|
123
|
+
}
|
|
124
|
+
case "ambiguous":
|
|
125
|
+
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>.`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The console a command talks to, and with what (ADR-0027): the address from the flag, the
|
|
130
|
+
* environment, the directory or the default login, in that order, and then the token kept for
|
|
131
|
+
* that address and no other. A token never travels to a console it was not made at.
|
|
132
|
+
*/
|
|
85
133
|
async function open(io, values) {
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
134
|
+
const directory = await io.directory.load();
|
|
135
|
+
const flag = text(values, "url");
|
|
136
|
+
const envUrl = io.env.CONSOLE_URL;
|
|
137
|
+
const credentials = await loadCredentials(io);
|
|
138
|
+
const resolved = resolveLogin({
|
|
139
|
+
flagUrl: flag === undefined ? undefined : originOf(flag),
|
|
140
|
+
flagAccount: text(values, "account"),
|
|
141
|
+
envUrl: envUrl === undefined ? undefined : originOf(envUrl),
|
|
142
|
+
envToken: io.env.CONSOLE_TOKEN,
|
|
143
|
+
directoryUrl: directoryOrigin(directory),
|
|
144
|
+
directoryAccount: directory?.account,
|
|
145
|
+
credentials,
|
|
146
|
+
});
|
|
147
|
+
if (!resolved.ok) {
|
|
148
|
+
throw failed(wordsOf(resolved));
|
|
91
149
|
}
|
|
150
|
+
const { url, token } = resolved;
|
|
92
151
|
const client = clientOf(io, url, token);
|
|
93
|
-
const projectKey = async () => text(values, "project") ?? io.env.CONSOLE_PROJECT ??
|
|
152
|
+
const projectKey = async () => text(values, "project") ?? io.env.CONSOLE_PROJECT ?? directory?.project;
|
|
94
153
|
return {
|
|
95
154
|
client,
|
|
96
155
|
url,
|
|
97
156
|
token,
|
|
157
|
+
account: resolved.account,
|
|
158
|
+
via: resolved.via,
|
|
159
|
+
credentials,
|
|
98
160
|
json: on(values, "json"),
|
|
99
161
|
projectKey,
|
|
100
162
|
async project() {
|
|
@@ -134,6 +196,8 @@ async function bodyOf(io, values, given) {
|
|
|
134
196
|
}
|
|
135
197
|
return given === "-" ? io.readStdin() : given;
|
|
136
198
|
}
|
|
199
|
+
/** The hosts a token may travel to in the clear: this machine, and no other. */
|
|
200
|
+
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
137
201
|
function originOf(value) {
|
|
138
202
|
const url = URL.canParse(value) ? new URL(value) : undefined;
|
|
139
203
|
if (url === undefined ||
|
|
@@ -143,6 +207,10 @@ function originOf(value) {
|
|
|
143
207
|
url.hash !== "") {
|
|
144
208
|
throw misuse(`Not an origin: ${value}. Say the console's address without a path, such as https://console.example.com`);
|
|
145
209
|
}
|
|
210
|
+
// A token travels over https; in the clear only to this machine, where a console is run for development.
|
|
211
|
+
if (url.protocol === "http:" && !LOOPBACK_HOSTS.has(url.hostname)) {
|
|
212
|
+
throw misuse(`Not a safe origin: ${value}. A token travels over https only; http is for a console on this machine (localhost).`);
|
|
213
|
+
}
|
|
146
214
|
return url.origin;
|
|
147
215
|
}
|
|
148
216
|
function stateOf(value) {
|
|
@@ -327,31 +395,44 @@ async function settle(io, session, project, decision, seconds, shown = false) {
|
|
|
327
395
|
}
|
|
328
396
|
return outcome.answer === null ? EXIT.waiting : EXIT.ok;
|
|
329
397
|
}
|
|
330
|
-
/**
|
|
331
|
-
|
|
398
|
+
/**
|
|
399
|
+
* Checks a token against the console, keeps it as the login at that console for that account,
|
|
400
|
+
* and says who it is. A token the login replaces is given up at the console on the way.
|
|
401
|
+
*/
|
|
402
|
+
async function keep(io, url, token, agent, asDefault) {
|
|
332
403
|
const me = await clientOf(io, url, token).me();
|
|
333
404
|
const person = me.person;
|
|
334
405
|
if (person === null) {
|
|
335
406
|
throw failed("The console does not know this token. Make one on your Agents page and try again.");
|
|
336
407
|
}
|
|
337
|
-
const
|
|
338
|
-
|
|
408
|
+
const credentials = (await loadCredentials(io)) ?? EMPTY_CREDENTIALS;
|
|
409
|
+
const replaced = loginsAt(credentials, url).find((login) => login.account === person.login);
|
|
410
|
+
if (replaced !== undefined && replaced.token !== token) {
|
|
411
|
+
await clientOf(io, url, replaced.token)
|
|
412
|
+
.giveUpToken()
|
|
413
|
+
.catch(() => undefined);
|
|
414
|
+
}
|
|
415
|
+
const next = withLogin(credentials, { url, account: person.login, token, agent, since: new Date(io.now()).toISOString() }, { default: asDefault });
|
|
416
|
+
const kept = await io.store.save(next);
|
|
417
|
+
const standing = next.default === url ? ", the default console here" : "";
|
|
418
|
+
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`);
|
|
339
419
|
return EXIT.ok;
|
|
340
420
|
}
|
|
341
421
|
const login = async (args, io) => {
|
|
342
|
-
const { values } = parse(args, { "token-stdin": { type: "boolean" }, agent: { type: "string" } }, false);
|
|
343
|
-
const
|
|
344
|
-
const wanted = text(values, "url") ?? io.env.CONSOLE_URL ??
|
|
422
|
+
const { values } = parse(args, { "token-stdin": { type: "boolean" }, agent: { type: "string" }, default: { type: "boolean" } }, false);
|
|
423
|
+
const credentials = await loadCredentials(io);
|
|
424
|
+
const wanted = text(values, "url") ?? io.env.CONSOLE_URL ?? credentials?.default;
|
|
345
425
|
if (wanted === undefined) {
|
|
346
426
|
throw misuse("Say where the console is: console login --url https://console.example.com");
|
|
347
427
|
}
|
|
348
428
|
const url = originOf(wanted);
|
|
429
|
+
const asDefault = on(values, "default");
|
|
349
430
|
if (on(values, "token-stdin")) {
|
|
350
431
|
const token = (await io.readStdin()).trim();
|
|
351
432
|
if (token.length === 0) {
|
|
352
433
|
throw failed("No token was given.");
|
|
353
434
|
}
|
|
354
|
-
return keep(io, url, token);
|
|
435
|
+
return keep(io, url, token, undefined, asDefault);
|
|
355
436
|
}
|
|
356
437
|
// A connection (ADR-0011): a code a person approves on the console's own pages, and the token
|
|
357
438
|
// handed over here once, never shown.
|
|
@@ -375,38 +456,94 @@ const login = async (args, io) => {
|
|
|
375
456
|
throw error;
|
|
376
457
|
}
|
|
377
458
|
if (claim.status === "connected") {
|
|
378
|
-
return keep(io, url, claim.secret, claim.token.name);
|
|
459
|
+
return keep(io, url, claim.secret, claim.token.name, asDefault);
|
|
379
460
|
}
|
|
380
461
|
}
|
|
381
462
|
};
|
|
382
463
|
const logout = async (args, io) => {
|
|
383
|
-
parse(args, {}, false);
|
|
384
|
-
const
|
|
385
|
-
const
|
|
386
|
-
const
|
|
387
|
-
|
|
464
|
+
const { values } = parse(args, {}, false);
|
|
465
|
+
const credentials = await loadCredentials(io);
|
|
466
|
+
const directory = await io.directory.load();
|
|
467
|
+
const envNote = () => {
|
|
468
|
+
if (io.env.CONSOLE_TOKEN !== undefined) {
|
|
469
|
+
io.stdout("CONSOLE_TOKEN in the environment still holds a token: unset it.\n");
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
if (credentials === undefined || credentials.logins.length === 0) {
|
|
388
473
|
io.stdout("Not signed in here: nothing to forget.\n");
|
|
474
|
+
envNote();
|
|
389
475
|
return EXIT.ok;
|
|
390
476
|
}
|
|
477
|
+
// Which login: the console the flag names, else the directory's, else the default, else the
|
|
478
|
+
// one console signed in to; and the account the same way, where the console has several.
|
|
479
|
+
const consoles = [...new Set(credentials.logins.map((login) => login.url))];
|
|
480
|
+
const flag = text(values, "url");
|
|
481
|
+
const url = flag === undefined
|
|
482
|
+
? (directoryOrigin(directory) ??
|
|
483
|
+
credentials.default ??
|
|
484
|
+
(consoles.length === 1 ? consoles[0] : undefined))
|
|
485
|
+
: originOf(flag);
|
|
486
|
+
if (url === undefined) {
|
|
487
|
+
throw failed(`Several consoles are signed in here; say which: console logout --url <origin>. console logins lists them.`);
|
|
488
|
+
}
|
|
489
|
+
const account = text(values, "account") ?? directory?.account;
|
|
490
|
+
const at = loginsAt(credentials, url);
|
|
491
|
+
const chosen = account === undefined ? at : at.filter((login) => login.account === account);
|
|
492
|
+
const [login] = chosen;
|
|
493
|
+
if (login === undefined) {
|
|
494
|
+
throw failed(`Nobody is signed in here at ${url}${account === undefined ? "" : ` as ${account}`}: nothing to forget. console logins lists the consoles signed in.`);
|
|
495
|
+
}
|
|
496
|
+
if (chosen.length > 1) {
|
|
497
|
+
throw failed(`Several accounts are signed in at ${url} (${chosen.map((kept) => kept.account ?? "?").join(", ")}); say which: console logout --url ${url} --account <login>`);
|
|
498
|
+
}
|
|
391
499
|
// The token is given up at the console first, so that it is good for nothing from now on;
|
|
392
500
|
// one the console no longer knows is gone already. A console out of reach leaves it to the
|
|
393
501
|
// Agents page, and the person is told so.
|
|
394
502
|
let gone = true;
|
|
395
503
|
try {
|
|
396
|
-
await clientOf(io, url, token).giveUpToken();
|
|
504
|
+
await clientOf(io, url, login.token).giveUpToken();
|
|
397
505
|
}
|
|
398
506
|
catch (error) {
|
|
399
507
|
if (!(error instanceof ApiError && error.code === "auth.required")) {
|
|
400
508
|
gone = false;
|
|
401
509
|
}
|
|
402
510
|
}
|
|
403
|
-
|
|
511
|
+
const next = withoutLogin(credentials, url, login.account);
|
|
512
|
+
if (next.logins.length === 0) {
|
|
513
|
+
await io.store.clear();
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
await io.store.save(next);
|
|
517
|
+
}
|
|
518
|
+
const whom = login.account === undefined ? "" : ` as ${login.account}`;
|
|
404
519
|
io.stdout(gone
|
|
405
|
-
?
|
|
406
|
-
: `Signed out here, but the console
|
|
407
|
-
|
|
408
|
-
|
|
520
|
+
? `Signed out of ${url}${whom}: the token is given up at the console and forgotten here.\n`
|
|
521
|
+
: `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`);
|
|
522
|
+
envNote();
|
|
523
|
+
return EXIT.ok;
|
|
524
|
+
};
|
|
525
|
+
/** The consoles this machine is signed in to, with the account at each; never a token. */
|
|
526
|
+
const logins = async (args, io) => {
|
|
527
|
+
const { values } = parse(args, {}, false);
|
|
528
|
+
const credentials = await loadCredentials(io);
|
|
529
|
+
const kept = (credentials?.logins ?? []).map((login) => ({
|
|
530
|
+
url: login.url,
|
|
531
|
+
account: login.account ?? null,
|
|
532
|
+
agent: login.agent ?? null,
|
|
533
|
+
since: login.since ?? null,
|
|
534
|
+
default: credentials?.default === login.url,
|
|
535
|
+
}));
|
|
536
|
+
if (on(values, "json")) {
|
|
537
|
+
io.stdout(`${JSON.stringify({ default: credentials?.default ?? null, logins: kept }, null, 2)}\n`);
|
|
538
|
+
return EXIT.ok;
|
|
409
539
|
}
|
|
540
|
+
if (kept.length === 0) {
|
|
541
|
+
io.stdout("Not signed in anywhere here: console login --url <the console's origin>\n");
|
|
542
|
+
return EXIT.ok;
|
|
543
|
+
}
|
|
544
|
+
io.stdout(`${kept
|
|
545
|
+
.map((login) => `${login.url} ${login.account ?? "(account unknown)"}${login.agent === null ? "" : ` ${login.agent}`}${login.since === null ? "" : ` since ${login.since.slice(0, 10)}`}${login.default ? " (default)" : ""}`)
|
|
546
|
+
.join("\n")}\n`);
|
|
410
547
|
return EXIT.ok;
|
|
411
548
|
};
|
|
412
549
|
/** The agent the command runs under, where its environment says so: what a connection is named by default. */
|
|
@@ -466,11 +603,25 @@ const projects = async (args, io) => {
|
|
|
466
603
|
};
|
|
467
604
|
const use = async (args, io) => {
|
|
468
605
|
const { values, positionals } = parse(args, {});
|
|
469
|
-
const key = theOne(positionals, "Say which project: console use <key>");
|
|
606
|
+
const key = theOne(positionals, "Say which project: console use <key> [--url <origin>] [--account <login>]");
|
|
470
607
|
const session = await open(io, values);
|
|
471
608
|
const project = await session.client.project(key).get();
|
|
472
|
-
|
|
473
|
-
|
|
609
|
+
// The directory names the console where that is not the default login here, or when asked,
|
|
610
|
+
// and the account where the console has several, or when asked; never a token.
|
|
611
|
+
const credentials = session.credentials ?? EMPTY_CREDENTIALS;
|
|
612
|
+
const pinUrl = text(values, "url") !== undefined ||
|
|
613
|
+
(session.via === "login" && credentials.default !== session.url);
|
|
614
|
+
const pinAccount = text(values, "account") !== undefined ||
|
|
615
|
+
(session.via === "login" && loginsAt(credentials, session.url).length > 1);
|
|
616
|
+
const kept = await io.directory.save({
|
|
617
|
+
project: project.key,
|
|
618
|
+
...(pinUrl ? { url: session.url } : {}),
|
|
619
|
+
...(pinAccount && session.account !== undefined ? { account: session.account } : {}),
|
|
620
|
+
});
|
|
621
|
+
const bound = pinUrl
|
|
622
|
+
? ` at ${session.url}${pinAccount && session.account !== undefined ? ` as ${session.account}` : ""}`
|
|
623
|
+
: "";
|
|
624
|
+
answer(io, session, project, () => `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.`);
|
|
474
625
|
return EXIT.ok;
|
|
475
626
|
};
|
|
476
627
|
const tasks = async (args, io) => {
|
|
@@ -478,12 +629,17 @@ const tasks = async (args, io) => {
|
|
|
478
629
|
state: { type: "string" },
|
|
479
630
|
open: { type: "boolean" },
|
|
480
631
|
mine: { type: "boolean" },
|
|
632
|
+
unassigned: { type: "boolean" },
|
|
481
633
|
category: { type: "string" },
|
|
482
634
|
goal: { type: "string" },
|
|
483
635
|
due: { type: "string" },
|
|
636
|
+
archived: { type: "boolean" },
|
|
484
637
|
...PAGE_OPTIONS,
|
|
485
638
|
}, false);
|
|
486
639
|
const state = stateOf(text(values, "state"));
|
|
640
|
+
if (on(values, "mine") && on(values, "unassigned")) {
|
|
641
|
+
throw misuse("Say one: --mine for the tasks assigned to you, or --unassigned for those assigned to nobody");
|
|
642
|
+
}
|
|
487
643
|
const page = pageOf(values);
|
|
488
644
|
const session = await open(io, values);
|
|
489
645
|
const project = await session.project();
|
|
@@ -504,9 +660,11 @@ const tasks = async (args, io) => {
|
|
|
504
660
|
...(state === undefined ? {} : { state }),
|
|
505
661
|
...(on(values, "open") || due === "overdue" ? { open: true } : {}),
|
|
506
662
|
...(assignee === undefined ? {} : { assignee }),
|
|
663
|
+
...(on(values, "unassigned") ? { unassigned: true } : {}),
|
|
507
664
|
...(text(values, "category") === undefined ? {} : { category: text(values, "category") }),
|
|
508
665
|
...(text(values, "goal") === undefined ? {} : { goal: text(values, "goal") }),
|
|
509
666
|
...(dueBy === undefined ? {} : { dueBy }),
|
|
667
|
+
...(on(values, "archived") ? { archived: true } : {}),
|
|
510
668
|
...page,
|
|
511
669
|
});
|
|
512
670
|
answer(io, session, found, () => found.items.length === 0
|
|
@@ -545,6 +703,60 @@ const skills = async (args, io) => {
|
|
|
545
703
|
answer(io, session, found, () => formatSkills(found, io.skillsDir));
|
|
546
704
|
return EXIT.ok;
|
|
547
705
|
};
|
|
706
|
+
/** How many pages of a skill, or of a file, the command follows before it stops: far above what one holds. */
|
|
707
|
+
const MAX_SKILL_PAGES = 64;
|
|
708
|
+
const skill = async (args, io) => {
|
|
709
|
+
const { values, positionals } = parse(args, { file: { type: "string" } });
|
|
710
|
+
const path = theOne(positionals, "Say which skill: console skill <path of its SKILL.md> [--file <path>]");
|
|
711
|
+
const session = await open(io, values);
|
|
712
|
+
const project = await session.project();
|
|
713
|
+
const file = text(values, "file");
|
|
714
|
+
if (file !== undefined) {
|
|
715
|
+
// The file whole, page after page, as the console reads it from the deployment.
|
|
716
|
+
const parts = [];
|
|
717
|
+
let offset;
|
|
718
|
+
let last;
|
|
719
|
+
for (let pages = 0; pages < MAX_SKILL_PAGES; pages += 1) {
|
|
720
|
+
last = await project.skillFile(file, offset === undefined ? {} : { offset });
|
|
721
|
+
if (last.status !== "ready" || last.file === null) {
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
parts.push(last.file.content);
|
|
725
|
+
if (last.file.nextOffset === null) {
|
|
726
|
+
break;
|
|
727
|
+
}
|
|
728
|
+
offset = last.file.nextOffset;
|
|
729
|
+
}
|
|
730
|
+
if (last === undefined) {
|
|
731
|
+
throw failed("The file could not be read.");
|
|
732
|
+
}
|
|
733
|
+
const read = last;
|
|
734
|
+
const content = parts.join("");
|
|
735
|
+
answer(io, session, { ...read, content }, () => formatSkillFile(read, content));
|
|
736
|
+
return EXIT.ok;
|
|
737
|
+
}
|
|
738
|
+
// The skill whole: its pages until complete, the rules first, as load_skill lays them out.
|
|
739
|
+
const pages = [];
|
|
740
|
+
let cursor;
|
|
741
|
+
let last;
|
|
742
|
+
for (let n = 0; n < MAX_SKILL_PAGES; n += 1) {
|
|
743
|
+
last = await project.skill(path, cursor);
|
|
744
|
+
if (last.status !== "ready" || last.skill === null) {
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
pages.push(last.skill);
|
|
748
|
+
if (last.skill.complete || last.skill.nextCursor === null) {
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
cursor = last.skill.nextCursor;
|
|
752
|
+
}
|
|
753
|
+
if (last === undefined) {
|
|
754
|
+
throw failed("The skill could not be read.");
|
|
755
|
+
}
|
|
756
|
+
const loaded = last;
|
|
757
|
+
answer(io, session, { ...loaded, pages }, () => formatSkillLoad(loaded, pages));
|
|
758
|
+
return EXIT.ok;
|
|
759
|
+
};
|
|
548
760
|
/** A version number given as a flag, or nothing; anything else is not understood. */
|
|
549
761
|
function versionOf(given) {
|
|
550
762
|
if (given === undefined) {
|
|
@@ -715,6 +927,36 @@ const editTask = async (args, io) => {
|
|
|
715
927
|
answer(io, session, changed, () => formatTask(changed));
|
|
716
928
|
return EXIT.ok;
|
|
717
929
|
};
|
|
930
|
+
/** Puts a task away, or every done or dropped task of a goal (ADR-0029); the task by its number or its id. */
|
|
931
|
+
const taskArchive = async (args, io) => {
|
|
932
|
+
const { values, positionals } = parse(args, { goal: { type: "string" } });
|
|
933
|
+
const goal = text(values, "goal");
|
|
934
|
+
if (positionals.length > 1 || (positionals.length === 0) === (goal === undefined)) {
|
|
935
|
+
throw misuse("Say which: console task archive <number|id>, or console task archive --goal <id> for every done or dropped task of a goal");
|
|
936
|
+
}
|
|
937
|
+
const session = await open(io, values);
|
|
938
|
+
const project = await session.project();
|
|
939
|
+
if (goal !== undefined) {
|
|
940
|
+
const { archived } = await project.archiveTasksOfGoal(goal);
|
|
941
|
+
answer(io, session, { archived }, () => `${archived} ${archived === 1 ? "task" : "tasks"} of the goal put away.`);
|
|
942
|
+
return EXIT.ok;
|
|
943
|
+
}
|
|
944
|
+
const found = await project.task(positionals[0] ?? "");
|
|
945
|
+
const archived = await project.archiveTask(found.id);
|
|
946
|
+
answer(io, session, archived, () => formatTaskLine(archived));
|
|
947
|
+
return EXIT.ok;
|
|
948
|
+
};
|
|
949
|
+
/** Brings a task back from the archive. */
|
|
950
|
+
const taskRestore = async (args, io) => {
|
|
951
|
+
const { values, positionals } = parse(args, {});
|
|
952
|
+
const ref = theOne(positionals, "Say which task: console task restore <number|id>");
|
|
953
|
+
const session = await open(io, values);
|
|
954
|
+
const project = await session.project();
|
|
955
|
+
const found = await project.task(ref);
|
|
956
|
+
const restored = await project.restoreTask(found.id);
|
|
957
|
+
answer(io, session, restored, () => formatTaskLine(restored));
|
|
958
|
+
return EXIT.ok;
|
|
959
|
+
};
|
|
718
960
|
const task = async (args, io) => {
|
|
719
961
|
if (args[0] === "new") {
|
|
720
962
|
return newTask(args.slice(1), io);
|
|
@@ -722,6 +964,12 @@ const task = async (args, io) => {
|
|
|
722
964
|
if (args[0] === "edit") {
|
|
723
965
|
return editTask(args.slice(1), io);
|
|
724
966
|
}
|
|
967
|
+
if (args[0] === "archive") {
|
|
968
|
+
return taskArchive(args.slice(1), io);
|
|
969
|
+
}
|
|
970
|
+
if (args[0] === "restore") {
|
|
971
|
+
return taskRestore(args.slice(1), io);
|
|
972
|
+
}
|
|
725
973
|
const { values, positionals } = parse(args, {});
|
|
726
974
|
const ref = theOne(positionals, "Say which task: console task <number|id>");
|
|
727
975
|
const session = await open(io, values);
|
|
@@ -1445,6 +1693,31 @@ const editProject = async (args, io) => {
|
|
|
1445
1693
|
answer(io, session, changed, () => formatProject(changed));
|
|
1446
1694
|
return EXIT.ok;
|
|
1447
1695
|
};
|
|
1696
|
+
/** The pack token (ADR-0028): read from standard input and never from an argument, so that no shell keeps it. */
|
|
1697
|
+
const projectPackToken = async (args, io) => {
|
|
1698
|
+
const { values, positionals } = parse(args, {
|
|
1699
|
+
stdin: { type: "boolean" },
|
|
1700
|
+
remove: { type: "boolean" },
|
|
1701
|
+
});
|
|
1702
|
+
const key = theOne(positionals, "Say which project: console project pack-token <key> --stdin, or console project pack-token <key> --remove");
|
|
1703
|
+
if (on(values, "stdin") === on(values, "remove")) {
|
|
1704
|
+
throw misuse("Say one of --stdin, which reads the token from standard input, and --remove, which takes it away.");
|
|
1705
|
+
}
|
|
1706
|
+
const session = await open(io, values);
|
|
1707
|
+
const project = session.client.project(key);
|
|
1708
|
+
if (on(values, "remove")) {
|
|
1709
|
+
const changed = await project.removePackToken();
|
|
1710
|
+
answer(io, session, changed, () => formatProject(changed));
|
|
1711
|
+
return EXIT.ok;
|
|
1712
|
+
}
|
|
1713
|
+
const token = (await io.readStdin()).trim();
|
|
1714
|
+
if (token.length === 0) {
|
|
1715
|
+
throw failed("No token was given on standard input.");
|
|
1716
|
+
}
|
|
1717
|
+
const changed = await project.setPackToken({ token });
|
|
1718
|
+
answer(io, session, changed, () => formatProject(changed));
|
|
1719
|
+
return EXIT.ok;
|
|
1720
|
+
};
|
|
1448
1721
|
const projectIcon = async (args, io) => {
|
|
1449
1722
|
const { values, positionals } = parse(args, { remove: { type: "boolean" } });
|
|
1450
1723
|
const [key, file] = positionals;
|
|
@@ -1474,8 +1747,11 @@ const project = async (args, io) => {
|
|
|
1474
1747
|
if (args[0] === "icon") {
|
|
1475
1748
|
return projectIcon(args.slice(1), io);
|
|
1476
1749
|
}
|
|
1750
|
+
if (args[0] === "pack-token") {
|
|
1751
|
+
return projectPackToken(args.slice(1), io);
|
|
1752
|
+
}
|
|
1477
1753
|
const { values, positionals } = parse(args, {});
|
|
1478
|
-
const key = theOne(positionals, "Say which project: console project <key>; or console project new, edit or
|
|
1754
|
+
const key = theOne(positionals, "Say which project: console project <key>; or console project new, edit, icon or pack-token");
|
|
1479
1755
|
const session = await open(io, values);
|
|
1480
1756
|
const found = await session.client.project(key).get();
|
|
1481
1757
|
answer(io, session, found, () => formatProject(found));
|
|
@@ -1491,18 +1767,51 @@ const members = async (args, io) => {
|
|
|
1491
1767
|
: found.items.map(formatMemberLine).join("\n"));
|
|
1492
1768
|
return EXIT.ok;
|
|
1493
1769
|
};
|
|
1770
|
+
/** The keys --project names with commas, each once, or nothing when it names one project or none. */
|
|
1771
|
+
function projectKeysOf(value) {
|
|
1772
|
+
if (value === undefined || !value.includes(",")) {
|
|
1773
|
+
return undefined;
|
|
1774
|
+
}
|
|
1775
|
+
const keys = [...new Set(value.split(",").map((key) => key.trim()))].filter((key) => key !== "");
|
|
1776
|
+
if (keys.length === 0) {
|
|
1777
|
+
throw misuse("Say which projects: --project <key>[,<key>...]");
|
|
1778
|
+
}
|
|
1779
|
+
return keys;
|
|
1780
|
+
}
|
|
1494
1781
|
const member = async (args, io) => {
|
|
1495
1782
|
const [verb, ...rest] = args;
|
|
1496
1783
|
if (verb === "add") {
|
|
1497
1784
|
const { values, positionals } = parse(rest, { role: { type: "string" } });
|
|
1498
|
-
const login = theOne(positionals, "Say whom: console member add <login> [--role owner|member]");
|
|
1785
|
+
const login = theOne(positionals, "Say whom: console member add <login> [--role owner|member] [--project <key>[,<key>...]]");
|
|
1499
1786
|
const role = text(values, "role");
|
|
1500
1787
|
const session = await open(io, values);
|
|
1501
|
-
const
|
|
1502
|
-
const added = await project.addMember({
|
|
1788
|
+
const input = {
|
|
1503
1789
|
personId: (await personOf(session, login)).id,
|
|
1504
1790
|
...(role === undefined ? {} : { role: projectRoleOf(role) }),
|
|
1505
|
-
}
|
|
1791
|
+
};
|
|
1792
|
+
// Several projects at once, where --project names them with commas: the person is listed
|
|
1793
|
+
// in each in turn, one listed already is said so, and the rest go on.
|
|
1794
|
+
const keys = projectKeysOf(text(values, "project"));
|
|
1795
|
+
if (keys !== undefined) {
|
|
1796
|
+
const added = [];
|
|
1797
|
+
for (const key of keys) {
|
|
1798
|
+
try {
|
|
1799
|
+
added.push({ project: key, member: await session.client.project(key).addMember(input) });
|
|
1800
|
+
}
|
|
1801
|
+
catch (error) {
|
|
1802
|
+
if (!(error instanceof ApiError && error.code === "member.exists")) {
|
|
1803
|
+
throw error;
|
|
1804
|
+
}
|
|
1805
|
+
added.push({ project: key, member: null });
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
answer(io, session, added, () => added
|
|
1809
|
+
.map(({ project, member }) => `${project} ${member === null ? `${login} is listed already` : formatMemberLine(member)}`)
|
|
1810
|
+
.join("\n"));
|
|
1811
|
+
return EXIT.ok;
|
|
1812
|
+
}
|
|
1813
|
+
const project = await session.project();
|
|
1814
|
+
const added = await project.addMember(input);
|
|
1506
1815
|
answer(io, session, added, () => formatMemberLine(added));
|
|
1507
1816
|
return EXIT.ok;
|
|
1508
1817
|
}
|
|
@@ -1528,7 +1837,7 @@ const member = async (args, io) => {
|
|
|
1528
1837
|
answer(io, session, { removed: found.login }, () => `${found.login} is no longer listed in the project.`);
|
|
1529
1838
|
return EXIT.ok;
|
|
1530
1839
|
}
|
|
1531
|
-
throw misuse("Say what to do: console member add <login> [--role owner|member], console member role <login> <owner|member>, or console member remove <login>");
|
|
1840
|
+
throw misuse("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>");
|
|
1532
1841
|
};
|
|
1533
1842
|
const category = async (args, io) => {
|
|
1534
1843
|
const [verb, ...rest] = args;
|
|
@@ -1577,6 +1886,7 @@ const category = async (args, io) => {
|
|
|
1577
1886
|
};
|
|
1578
1887
|
const COMMANDS = {
|
|
1579
1888
|
login,
|
|
1889
|
+
logins,
|
|
1580
1890
|
logout,
|
|
1581
1891
|
whoami,
|
|
1582
1892
|
serve,
|
|
@@ -1589,6 +1899,7 @@ const COMMANDS = {
|
|
|
1589
1899
|
goal,
|
|
1590
1900
|
categories,
|
|
1591
1901
|
skills,
|
|
1902
|
+
skill,
|
|
1592
1903
|
rules,
|
|
1593
1904
|
people,
|
|
1594
1905
|
person,
|
|
@@ -1628,11 +1939,14 @@ function help(io, name) {
|
|
|
1628
1939
|
io.stdout(found);
|
|
1629
1940
|
return EXIT.ok;
|
|
1630
1941
|
}
|
|
1631
|
-
/**
|
|
1942
|
+
/**
|
|
1943
|
+
* A client of the console at `url`, naming the command on every request (ADR-0025). A redirect
|
|
1944
|
+
* is not followed: the token goes to the address the command resolved, and nowhere it is sent on to.
|
|
1945
|
+
*/
|
|
1632
1946
|
function clientOf(io, url, token) {
|
|
1633
1947
|
return createClient({
|
|
1634
1948
|
baseUrl: url,
|
|
1635
|
-
fetch: io.fetch,
|
|
1949
|
+
fetch: (input, init) => io.fetch(input, { ...init, redirect: "manual" }),
|
|
1636
1950
|
...(token === undefined ? {} : { token }),
|
|
1637
1951
|
...(io.userAgent === undefined ? {} : { userAgent: io.userAgent }),
|
|
1638
1952
|
});
|