@skillcdn/console 0.1.11 → 0.1.13
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 +8 -5
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +260 -6
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +7 -1
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +22 -0
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +69 -2
- package/dist/cli/help.js.map +1 -1
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +2 -2
- package/dist/components/board.js.map +1 -1
- package/dist/components/calendar.js +2 -2
- package/dist/components/calendar.js.map +1 -1
- package/dist/components/people.js +2 -2
- package/dist/components/people.js.map +1 -1
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +3 -3
- package/dist/components/projects.js.map +1 -1
- package/dist/components/runs.js +2 -2
- package/dist/components/runs.js.map +1 -1
- package/dist/components/shell.js +2 -2
- package/dist/components/shell.js.map +1 -1
- package/dist/components/task-form.js +2 -2
- package/dist/components/task-form.js.map +1 -1
- package/dist/components/ui.d.ts +7 -2
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +9 -4
- package/dist/components/ui.js.map +1 -1
- package/dist/console.d.ts +41 -6
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +61 -50
- package/dist/console.js.map +1 -1
- package/dist/i18n/en.d.ts +5 -5
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +8 -8
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +8 -8
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts +15 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +17 -0
- package/dist/router.js.map +1 -1
- package/docs/specs/cli.md +15 -2
- package/docs/specs/rest.md +13 -13
- package/package.json +1 -1
- package/skills/building-a-console/SKILL.md +4 -4
- package/skills/working-the-board/SKILL.md +5 -1
- package/src/cli/cli.ts +333 -5
- package/src/cli/format.ts +27 -0
- package/src/cli/help.ts +86 -1
- package/src/components/board.tsx +11 -2
- package/src/components/calendar.tsx +2 -2
- package/src/components/people.tsx +5 -5
- package/src/components/projects.tsx +5 -3
- package/src/components/runs.tsx +2 -2
- package/src/components/shell.tsx +3 -3
- package/src/components/task-form.tsx +2 -2
- package/src/components/ui.tsx +14 -5
- package/src/console.tsx +115 -54
- package/src/i18n/en.ts +9 -8
- package/src/i18n/ko.ts +9 -8
- package/src/index.ts +5 -0
- package/src/router.ts +26 -0
package/src/cli/cli.ts
CHANGED
|
@@ -9,10 +9,18 @@ import {
|
|
|
9
9
|
} from "../client.js";
|
|
10
10
|
import { addDays, dayOf as dayOfDate, isDay } from "../dates.js";
|
|
11
11
|
import { DEFAULT_PAGE_LIMIT, LIST_LIMIT, MAX_OPTIONS, MIN_OPTIONS } from "../limits.js";
|
|
12
|
-
import type { RestClaim, RestDecision, RestTaskPatch } from "../schemas.js";
|
|
12
|
+
import type { RestClaim, RestDecision, RestPerson, RestTaskPatch } from "../schemas.js";
|
|
13
13
|
import {
|
|
14
|
+
COLOR_NAMES,
|
|
15
|
+
type ColorName,
|
|
14
16
|
GOAL_STATES,
|
|
15
17
|
type GoalState,
|
|
18
|
+
PERSON_ROLES,
|
|
19
|
+
type PersonRole,
|
|
20
|
+
PROJECT_ROLES,
|
|
21
|
+
PROJECT_VISIBILITIES,
|
|
22
|
+
type ProjectRole,
|
|
23
|
+
type ProjectVisibility,
|
|
16
24
|
TASK_PRIORITIES,
|
|
17
25
|
TASK_STATES,
|
|
18
26
|
type TaskPriority,
|
|
@@ -30,8 +38,11 @@ import {
|
|
|
30
38
|
formatDocuments,
|
|
31
39
|
formatGoal,
|
|
32
40
|
formatGoalLine,
|
|
41
|
+
formatMemberLine,
|
|
33
42
|
formatMore,
|
|
34
43
|
formatOptions,
|
|
44
|
+
formatPersonLine,
|
|
45
|
+
formatProject,
|
|
35
46
|
formatProjectLine,
|
|
36
47
|
formatRun,
|
|
37
48
|
formatRunLine,
|
|
@@ -331,16 +342,41 @@ async function personIdOf(
|
|
|
331
342
|
}
|
|
332
343
|
return me.person.id;
|
|
333
344
|
}
|
|
345
|
+
return (await personOf(session, value)).id;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** The person a login names, among the workspace's; the logins are as people sign in. */
|
|
349
|
+
async function personOf(session: Session, login: string): Promise<RestPerson> {
|
|
334
350
|
const { items } = await session.client.people();
|
|
335
|
-
const found = items.find((person) => person.login.toLowerCase() ===
|
|
351
|
+
const found = items.find((person) => person.login.toLowerCase() === login.toLowerCase());
|
|
336
352
|
if (found === undefined) {
|
|
337
353
|
throw misuse(
|
|
338
|
-
`Nobody of the workspace is called ${
|
|
354
|
+
`Nobody of the workspace is called ${login}. console people lists everyone, by login.`,
|
|
339
355
|
);
|
|
340
356
|
}
|
|
341
|
-
return found
|
|
357
|
+
return found;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** One word of a vocabulary, or the refusal to understand. */
|
|
361
|
+
function wordOf<T extends string>(value: string, words: readonly T[], what: string): T {
|
|
362
|
+
const found = words.find((word) => word === value);
|
|
363
|
+
if (found === undefined) {
|
|
364
|
+
throw misuse(`Not ${what}: ${value}. One of ${words.join(", ")}`);
|
|
365
|
+
}
|
|
366
|
+
return found;
|
|
342
367
|
}
|
|
343
368
|
|
|
369
|
+
const personRoleOf = (value: string): PersonRole =>
|
|
370
|
+
wordOf(value, PERSON_ROLES, "a role in the workspace");
|
|
371
|
+
const projectRoleOf = (value: string): ProjectRole =>
|
|
372
|
+
wordOf(value, PROJECT_ROLES, "a role in a project");
|
|
373
|
+
const visibilityOf = (value: string): ProjectVisibility =>
|
|
374
|
+
wordOf(value, PROJECT_VISIBILITIES, "a visibility");
|
|
375
|
+
const colorOf = (value: string): ColorName => wordOf(value, COLOR_NAMES, "a colour");
|
|
376
|
+
/** A colour, or `none` for the one the pages choose, which is `null` on the wire. */
|
|
377
|
+
const colorOrNoneOf = (value: string | undefined): ColorName | null | undefined =>
|
|
378
|
+
value === undefined ? undefined : value === "none" ? null : colorOf(value);
|
|
379
|
+
|
|
344
380
|
function secondsOf(value: string | undefined, fallback: number): number {
|
|
345
381
|
if (value === undefined) {
|
|
346
382
|
return fallback;
|
|
@@ -381,6 +417,15 @@ function theOne(positionals: readonly string[], ask: string): string {
|
|
|
381
417
|
return first;
|
|
382
418
|
}
|
|
383
419
|
|
|
420
|
+
/** Exactly two words, for a command on a thing and a value. */
|
|
421
|
+
function theTwo(positionals: readonly string[], ask: string): readonly [string, string] {
|
|
422
|
+
const [first, second] = positionals;
|
|
423
|
+
if (first === undefined || second === undefined || positionals.length > 2) {
|
|
424
|
+
throw misuse(ask);
|
|
425
|
+
}
|
|
426
|
+
return [first, second];
|
|
427
|
+
}
|
|
428
|
+
|
|
384
429
|
/**
|
|
385
430
|
* The decision once answered, or as it stands when the wait is over. Each request asks the
|
|
386
431
|
* console to hold it for a while, so the answer arrives as soon as a person gives it.
|
|
@@ -535,7 +580,7 @@ const projects: Command = async (args, io) => {
|
|
|
535
580
|
const found = await session.client.projects();
|
|
536
581
|
answer(io, session, found, () =>
|
|
537
582
|
found.items.length === 0
|
|
538
|
-
?
|
|
583
|
+
? 'No projects you may work in. Make one: console project new <key> "<name>"'
|
|
539
584
|
: found.items.map(formatProjectLine).join("\n"),
|
|
540
585
|
);
|
|
541
586
|
return EXIT.ok;
|
|
@@ -1349,6 +1394,263 @@ const serve: Command = async (args, io) => {
|
|
|
1349
1394
|
return EXIT.ok;
|
|
1350
1395
|
};
|
|
1351
1396
|
|
|
1397
|
+
// Configuring, as the person (ADR-0021): what an administrator or an owner does on the pages,
|
|
1398
|
+
// their agent does here, attributed to them as the agent. The console decides on every request
|
|
1399
|
+
// with the same roles; what the person may not, the agent may not (auth.forbidden). The tokens
|
|
1400
|
+
// themselves have no command, by design.
|
|
1401
|
+
|
|
1402
|
+
const people: Command = async (args, io) => {
|
|
1403
|
+
const { values } = parse(args, {}, false);
|
|
1404
|
+
const session = await open(io, values);
|
|
1405
|
+
const found = await session.client.people();
|
|
1406
|
+
answer(io, session, found, () => found.items.map(formatPersonLine).join("\n"));
|
|
1407
|
+
return EXIT.ok;
|
|
1408
|
+
};
|
|
1409
|
+
|
|
1410
|
+
const person: Command = async (args, io) => {
|
|
1411
|
+
if (args[0] !== "role") {
|
|
1412
|
+
throw misuse("Say what to do: console person role <login> <admin|member>");
|
|
1413
|
+
}
|
|
1414
|
+
const { values, positionals } = parse(args.slice(1), {});
|
|
1415
|
+
const [login, role] = theTwo(
|
|
1416
|
+
positionals,
|
|
1417
|
+
"Say whom and what: console person role <login> <admin|member>",
|
|
1418
|
+
);
|
|
1419
|
+
const wanted = personRoleOf(role);
|
|
1420
|
+
const session = await open(io, values);
|
|
1421
|
+
const found = await personOf(session, login);
|
|
1422
|
+
const changed = await session.client.updatePerson(found.id, { role: wanted });
|
|
1423
|
+
answer(io, session, changed, () => formatPersonLine(changed));
|
|
1424
|
+
return EXIT.ok;
|
|
1425
|
+
};
|
|
1426
|
+
|
|
1427
|
+
/** The flags that set what a project is, shared by making one and changing one. */
|
|
1428
|
+
const PROJECT_FIELDS: OptionSpec = {
|
|
1429
|
+
description: { type: "string" },
|
|
1430
|
+
file: { type: "string" },
|
|
1431
|
+
visibility: { type: "string" },
|
|
1432
|
+
skills: { type: "string" },
|
|
1433
|
+
color: { type: "string" },
|
|
1434
|
+
symbol: { type: "string" },
|
|
1435
|
+
};
|
|
1436
|
+
|
|
1437
|
+
/** What the flags say of a project; `none` clears what may be cleared. */
|
|
1438
|
+
async function projectFieldsOf(io: CliIo, values: Values) {
|
|
1439
|
+
const description = await bodyOf(io, values, text(values, "description"));
|
|
1440
|
+
const visibility = text(values, "visibility");
|
|
1441
|
+
const skills = orNone(text(values, "skills"));
|
|
1442
|
+
const color = colorOrNoneOf(text(values, "color"));
|
|
1443
|
+
const symbol = orNone(text(values, "symbol"));
|
|
1444
|
+
return {
|
|
1445
|
+
...(description === undefined
|
|
1446
|
+
? {}
|
|
1447
|
+
: { description: description === "none" ? "" : description }),
|
|
1448
|
+
...(visibility === undefined ? {} : { visibility: visibilityOf(visibility) }),
|
|
1449
|
+
...(skills === undefined ? {} : { skillsAddress: skills }),
|
|
1450
|
+
...(color === undefined ? {} : { color }),
|
|
1451
|
+
...(symbol === undefined ? {} : { symbol }),
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
const newProject: Command = async (args, io) => {
|
|
1456
|
+
const { values, positionals } = parse(args, PROJECT_FIELDS);
|
|
1457
|
+
const [key, name] = theTwo(
|
|
1458
|
+
positionals,
|
|
1459
|
+
'Say the key and the name: console project new <key> "<name>" [--visibility workspace|private]',
|
|
1460
|
+
);
|
|
1461
|
+
const fields = await projectFieldsOf(io, values);
|
|
1462
|
+
const session = await open(io, values);
|
|
1463
|
+
const made = await session.client.createProject({ key, name, ...fields });
|
|
1464
|
+
answer(
|
|
1465
|
+
io,
|
|
1466
|
+
session,
|
|
1467
|
+
made,
|
|
1468
|
+
() =>
|
|
1469
|
+
`Project made: ${made.name} (${made.key}), with you as its owner.\nTo work in it from this directory: console use ${made.key}`,
|
|
1470
|
+
);
|
|
1471
|
+
return EXIT.ok;
|
|
1472
|
+
};
|
|
1473
|
+
|
|
1474
|
+
const editProject: Command = async (args, io) => {
|
|
1475
|
+
const { values, positionals } = parse(args, { ...PROJECT_FIELDS, name: { type: "string" } });
|
|
1476
|
+
const key = theOne(
|
|
1477
|
+
positionals,
|
|
1478
|
+
"Say which project: console project edit <key> --name <name> ...",
|
|
1479
|
+
);
|
|
1480
|
+
const name = text(values, "name");
|
|
1481
|
+
const patch = { ...(name === undefined ? {} : { name }), ...(await projectFieldsOf(io, values)) };
|
|
1482
|
+
if (Object.keys(patch).length === 0) {
|
|
1483
|
+
throw misuse(
|
|
1484
|
+
"Say what to change: --name, --description, --visibility, --skills, --color or --symbol; none clears the description, the skills address, the colour or the symbol.",
|
|
1485
|
+
);
|
|
1486
|
+
}
|
|
1487
|
+
const session = await open(io, values);
|
|
1488
|
+
const changed = await session.client.project(key).update(patch);
|
|
1489
|
+
answer(io, session, changed, () => formatProject(changed));
|
|
1490
|
+
return EXIT.ok;
|
|
1491
|
+
};
|
|
1492
|
+
|
|
1493
|
+
const projectIcon: Command = async (args, io) => {
|
|
1494
|
+
const { values, positionals } = parse(args, { remove: { type: "boolean" } });
|
|
1495
|
+
const [key, file] = positionals;
|
|
1496
|
+
const removing = on(values, "remove");
|
|
1497
|
+
if (key === undefined || positionals.length > 2 || (file === undefined) !== removing) {
|
|
1498
|
+
throw misuse(
|
|
1499
|
+
"Say which project and which image: console project icon <key> <file>, or console project icon <key> --remove",
|
|
1500
|
+
);
|
|
1501
|
+
}
|
|
1502
|
+
const session = await open(io, values);
|
|
1503
|
+
const project = session.client.project(key);
|
|
1504
|
+
const changed =
|
|
1505
|
+
file === undefined
|
|
1506
|
+
? await project.removeIcon()
|
|
1507
|
+
: await project.setIcon({
|
|
1508
|
+
name: basename(file),
|
|
1509
|
+
bytes: await io.readBytes(file),
|
|
1510
|
+
contentType: CONTENT_TYPES[extname(file).toLowerCase()] ?? "application/octet-stream",
|
|
1511
|
+
});
|
|
1512
|
+
answer(io, session, changed, () => formatProject(changed));
|
|
1513
|
+
return EXIT.ok;
|
|
1514
|
+
};
|
|
1515
|
+
|
|
1516
|
+
const project: Command = async (args, io) => {
|
|
1517
|
+
if (args[0] === "new") {
|
|
1518
|
+
return newProject(args.slice(1), io);
|
|
1519
|
+
}
|
|
1520
|
+
if (args[0] === "edit") {
|
|
1521
|
+
return editProject(args.slice(1), io);
|
|
1522
|
+
}
|
|
1523
|
+
if (args[0] === "icon") {
|
|
1524
|
+
return projectIcon(args.slice(1), io);
|
|
1525
|
+
}
|
|
1526
|
+
const { values, positionals } = parse(args, {});
|
|
1527
|
+
const key = theOne(
|
|
1528
|
+
positionals,
|
|
1529
|
+
"Say which project: console project <key>; or console project new, edit or icon",
|
|
1530
|
+
);
|
|
1531
|
+
const session = await open(io, values);
|
|
1532
|
+
const found = await session.client.project(key).get();
|
|
1533
|
+
answer(io, session, found, () => formatProject(found));
|
|
1534
|
+
return EXIT.ok;
|
|
1535
|
+
};
|
|
1536
|
+
|
|
1537
|
+
const members: Command = async (args, io) => {
|
|
1538
|
+
const { values } = parse(args, {}, false);
|
|
1539
|
+
const session = await open(io, values);
|
|
1540
|
+
const project = await session.project();
|
|
1541
|
+
const found = await project.members();
|
|
1542
|
+
answer(io, session, found, () =>
|
|
1543
|
+
found.items.length === 0
|
|
1544
|
+
? "Nobody is listed in the project."
|
|
1545
|
+
: found.items.map(formatMemberLine).join("\n"),
|
|
1546
|
+
);
|
|
1547
|
+
return EXIT.ok;
|
|
1548
|
+
};
|
|
1549
|
+
|
|
1550
|
+
const member: Command = async (args, io) => {
|
|
1551
|
+
const [verb, ...rest] = args;
|
|
1552
|
+
if (verb === "add") {
|
|
1553
|
+
const { values, positionals } = parse(rest, { role: { type: "string" } });
|
|
1554
|
+
const login = theOne(positionals, "Say whom: console member add <login> [--role owner|member]");
|
|
1555
|
+
const role = text(values, "role");
|
|
1556
|
+
const session = await open(io, values);
|
|
1557
|
+
const project = await session.project();
|
|
1558
|
+
const added = await project.addMember({
|
|
1559
|
+
personId: (await personOf(session, login)).id,
|
|
1560
|
+
...(role === undefined ? {} : { role: projectRoleOf(role) }),
|
|
1561
|
+
});
|
|
1562
|
+
answer(io, session, added, () => formatMemberLine(added));
|
|
1563
|
+
return EXIT.ok;
|
|
1564
|
+
}
|
|
1565
|
+
if (verb === "role") {
|
|
1566
|
+
const { values, positionals } = parse(rest, {});
|
|
1567
|
+
const [login, role] = theTwo(
|
|
1568
|
+
positionals,
|
|
1569
|
+
"Say whom and what: console member role <login> <owner|member>",
|
|
1570
|
+
);
|
|
1571
|
+
const wanted = projectRoleOf(role);
|
|
1572
|
+
const session = await open(io, values);
|
|
1573
|
+
const project = await session.project();
|
|
1574
|
+
const changed = await project.updateMember((await personOf(session, login)).id, {
|
|
1575
|
+
role: wanted,
|
|
1576
|
+
});
|
|
1577
|
+
answer(io, session, changed, () => formatMemberLine(changed));
|
|
1578
|
+
return EXIT.ok;
|
|
1579
|
+
}
|
|
1580
|
+
if (verb === "remove") {
|
|
1581
|
+
const { values, positionals } = parse(rest, {});
|
|
1582
|
+
const login = theOne(positionals, "Say whom: console member remove <login>");
|
|
1583
|
+
const session = await open(io, values);
|
|
1584
|
+
const project = await session.project();
|
|
1585
|
+
const found = await personOf(session, login);
|
|
1586
|
+
await project.removeMember(found.id);
|
|
1587
|
+
answer(
|
|
1588
|
+
io,
|
|
1589
|
+
session,
|
|
1590
|
+
{ removed: found.login },
|
|
1591
|
+
() => `${found.login} is no longer listed in the project.`,
|
|
1592
|
+
);
|
|
1593
|
+
return EXIT.ok;
|
|
1594
|
+
}
|
|
1595
|
+
throw misuse(
|
|
1596
|
+
"Say what to do: console member add <login> [--role owner|member], console member role <login> <owner|member>, or console member remove <login>",
|
|
1597
|
+
);
|
|
1598
|
+
};
|
|
1599
|
+
|
|
1600
|
+
const category: Command = async (args, io) => {
|
|
1601
|
+
const [verb, ...rest] = args;
|
|
1602
|
+
if (verb === "new") {
|
|
1603
|
+
const { values, positionals } = parse(rest, { color: { type: "string" } });
|
|
1604
|
+
const [key, name] = theTwo(
|
|
1605
|
+
positionals,
|
|
1606
|
+
'Say the key and the name: console category new <key> "<name>" [--color <name>]',
|
|
1607
|
+
);
|
|
1608
|
+
const color = text(values, "color");
|
|
1609
|
+
const session = await open(io, values);
|
|
1610
|
+
const made = await session.client.createCategory({
|
|
1611
|
+
key,
|
|
1612
|
+
name,
|
|
1613
|
+
...(color === undefined ? {} : { color: colorOf(color) }),
|
|
1614
|
+
});
|
|
1615
|
+
answer(io, session, made, () => formatCategoryLine(made));
|
|
1616
|
+
return EXIT.ok;
|
|
1617
|
+
}
|
|
1618
|
+
if (verb === "edit") {
|
|
1619
|
+
const { values, positionals } = parse(rest, {
|
|
1620
|
+
name: { type: "string" },
|
|
1621
|
+
color: { type: "string" },
|
|
1622
|
+
});
|
|
1623
|
+
const key = theOne(
|
|
1624
|
+
positionals,
|
|
1625
|
+
"Say which category: console category edit <key> [--name <name>] [--color <name>]",
|
|
1626
|
+
);
|
|
1627
|
+
const name = text(values, "name");
|
|
1628
|
+
const color = text(values, "color");
|
|
1629
|
+
const patch = {
|
|
1630
|
+
...(name === undefined ? {} : { name }),
|
|
1631
|
+
...(color === undefined ? {} : { color: colorOf(color) }),
|
|
1632
|
+
};
|
|
1633
|
+
if (Object.keys(patch).length === 0) {
|
|
1634
|
+
throw misuse("Say what to change: --name or --color.");
|
|
1635
|
+
}
|
|
1636
|
+
const session = await open(io, values);
|
|
1637
|
+
const changed = await session.client.updateCategory(key, patch);
|
|
1638
|
+
answer(io, session, changed, () => formatCategoryLine(changed));
|
|
1639
|
+
return EXIT.ok;
|
|
1640
|
+
}
|
|
1641
|
+
if (verb === "remove") {
|
|
1642
|
+
const { values, positionals } = parse(rest, {});
|
|
1643
|
+
const key = theOne(positionals, "Say which category: console category remove <key>");
|
|
1644
|
+
const session = await open(io, values);
|
|
1645
|
+
await session.client.removeCategory(key);
|
|
1646
|
+
answer(io, session, { removed: key }, () => `Category ${key} removed.`);
|
|
1647
|
+
return EXIT.ok;
|
|
1648
|
+
}
|
|
1649
|
+
throw misuse(
|
|
1650
|
+
'Say what to do: console category new <key> "<name>" [--color <name>], console category edit <key> [--name <name>] [--color <name>], or console category remove <key>',
|
|
1651
|
+
);
|
|
1652
|
+
};
|
|
1653
|
+
|
|
1352
1654
|
const COMMANDS: Readonly<Record<string, Command>> = {
|
|
1353
1655
|
login,
|
|
1354
1656
|
logout,
|
|
@@ -1363,6 +1665,12 @@ const COMMANDS: Readonly<Record<string, Command>> = {
|
|
|
1363
1665
|
goal,
|
|
1364
1666
|
categories,
|
|
1365
1667
|
skills,
|
|
1668
|
+
people,
|
|
1669
|
+
person,
|
|
1670
|
+
project,
|
|
1671
|
+
members,
|
|
1672
|
+
member,
|
|
1673
|
+
category,
|
|
1366
1674
|
take,
|
|
1367
1675
|
report,
|
|
1368
1676
|
"hand-in": handIn,
|
|
@@ -1416,6 +1724,26 @@ function hintFor(error: ApiError): string {
|
|
|
1416
1724
|
return "The due day is before the start. Say a due day on or after it.\n";
|
|
1417
1725
|
case "goal.not_found":
|
|
1418
1726
|
return "No goal of this project has that id. console goals lists them.\n";
|
|
1727
|
+
case "auth.forbidden":
|
|
1728
|
+
return "What was asked is an administrator's, or an owner's of the project, and the person this token acts as is not one here. Hand the step to them.\n";
|
|
1729
|
+
case "project.key_taken":
|
|
1730
|
+
return "A project has that key already. console projects lists them.\n";
|
|
1731
|
+
case "project.invalid_address":
|
|
1732
|
+
return "The skills address is not one as SkillCDN spells it; none makes the organization's skills the project's.\n";
|
|
1733
|
+
case "project.invalid_icon":
|
|
1734
|
+
return "The image must be a PNG, a JPEG, a GIF or a WebP, known by its bytes.\n";
|
|
1735
|
+
case "member.exists":
|
|
1736
|
+
return "The person is listed already. console member role <login> <owner|member> changes what they are.\n";
|
|
1737
|
+
case "member.not_found":
|
|
1738
|
+
return "The person is not listed in the project. console members lists those who are.\n";
|
|
1739
|
+
case "category.key_taken":
|
|
1740
|
+
return "A category has that key already. console categories lists them.\n";
|
|
1741
|
+
case "category.not_found":
|
|
1742
|
+
return "No category has that key. console categories lists them.\n";
|
|
1743
|
+
case "category.in_use":
|
|
1744
|
+
return "Tasks are sorted into the category. Move them out first: console task edit <number> --category <other|none>\n";
|
|
1745
|
+
case "person.last_admin":
|
|
1746
|
+
return "The board keeps at least one administrator. Make another one first.\n";
|
|
1419
1747
|
case "connect.too_many":
|
|
1420
1748
|
return "The console holds as many connections as it may at once. Try again in a few minutes.\n";
|
|
1421
1749
|
case "network":
|
package/src/cli/format.ts
CHANGED
|
@@ -6,6 +6,8 @@ import type {
|
|
|
6
6
|
RestDocumentSummary,
|
|
7
7
|
RestDocuments,
|
|
8
8
|
RestGoal,
|
|
9
|
+
RestMember,
|
|
10
|
+
RestPerson,
|
|
9
11
|
RestProject,
|
|
10
12
|
RestRun,
|
|
11
13
|
RestSkills,
|
|
@@ -42,6 +44,31 @@ export function formatProjectLine(project: RestProject): string {
|
|
|
42
44
|
return `${project.key} ${project.name} (${notes.join("; ")})`;
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
/** One project in full: what it is, how it is marked, and what the person is in it. */
|
|
48
|
+
export function formatProject(project: RestProject): string {
|
|
49
|
+
const lines = [
|
|
50
|
+
`project ${project.key}: ${project.name}`,
|
|
51
|
+
`visibility: ${project.visibility} role: ${project.role} skills: ${project.skillsAddress ?? "the organization's"}`,
|
|
52
|
+
`color: ${project.color ?? "none"} symbol: ${project.symbol ?? "none"} image: ${project.icon === null ? "none" : project.icon.contentType}`,
|
|
53
|
+
];
|
|
54
|
+
if (project.description.trim().length > 0) {
|
|
55
|
+
lines.push("", project.description.trimEnd());
|
|
56
|
+
}
|
|
57
|
+
return lines.join("\n");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** One person: their login, their name where the host gives one, and what they are in the workspace. */
|
|
61
|
+
export function formatPersonLine(person: RestPerson): string {
|
|
62
|
+
return person.name === null
|
|
63
|
+
? `${person.login} (${person.role})`
|
|
64
|
+
: `${person.login} ${person.name} (${person.role})`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** One member of a project: their login, what they are in it, and since when. */
|
|
68
|
+
export function formatMemberLine(member: RestMember): string {
|
|
69
|
+
return `${member.person.login} ${member.role} (since ${member.addedAt.slice(0, 10)})`;
|
|
70
|
+
}
|
|
71
|
+
|
|
45
72
|
/** The line under a page of a listing that has more after it: how to ask for the next page. */
|
|
46
73
|
export function formatMore(more: boolean, limit: number, offset: number): string {
|
|
47
74
|
return more ? `\n... more: --offset ${offset + limit}` : "";
|
package/src/cli/help.ts
CHANGED
|
@@ -3,6 +3,9 @@ import {
|
|
|
3
3
|
MAX_AGENT_LENGTH,
|
|
4
4
|
MAX_ARTIFACTS_PER_RUN,
|
|
5
5
|
MAX_BODY_LENGTH,
|
|
6
|
+
MAX_CATEGORIES,
|
|
7
|
+
MAX_CATEGORY_KEY_LENGTH,
|
|
8
|
+
MAX_CATEGORY_NAME_LENGTH,
|
|
6
9
|
MAX_DOCUMENT_LENGTH,
|
|
7
10
|
MAX_DOCUMENT_PATH_LENGTH,
|
|
8
11
|
MAX_FILE_BYTES,
|
|
@@ -10,6 +13,11 @@ import {
|
|
|
10
13
|
MAX_LINK_LABEL_LENGTH,
|
|
11
14
|
MAX_OPTION_LABEL_LENGTH,
|
|
12
15
|
MAX_OPTIONS,
|
|
16
|
+
MAX_PROJECT_DESCRIPTION_LENGTH,
|
|
17
|
+
MAX_PROJECT_ICON_BYTES,
|
|
18
|
+
MAX_PROJECT_KEY_LENGTH,
|
|
19
|
+
MAX_PROJECT_NAME_LENGTH,
|
|
20
|
+
MAX_PROJECT_SYMBOL_LENGTH,
|
|
13
21
|
MAX_QUESTION_LENGTH,
|
|
14
22
|
MAX_REPORTS_PER_RUN,
|
|
15
23
|
MAX_SUMMARY_LENGTH,
|
|
@@ -17,6 +25,7 @@ import {
|
|
|
17
25
|
MAX_VERSIONS_PER_DOCUMENT,
|
|
18
26
|
MIN_OPTIONS,
|
|
19
27
|
} from "../limits.js";
|
|
28
|
+
import { COLOR_NAMES } from "../vocabulary.js";
|
|
20
29
|
|
|
21
30
|
// What the command says about itself: for an agent that meets it for the first time, and for a
|
|
22
31
|
// person. One table for every command, from which the usage and each command's help are made:
|
|
@@ -53,6 +62,8 @@ const DOCUMENT_PATH = `a path of lowercase letters, digits and hyphens, with sla
|
|
|
53
62
|
const DOCUMENT_REFUSALS = ["document.not_found: no page has that path in this project"];
|
|
54
63
|
|
|
55
64
|
const DAY = "a day as 2026-10-24, today, tomorrow, or +7 for seven days from today";
|
|
65
|
+
const COLORS = `one of ${COLOR_NAMES.join(", ")}`;
|
|
66
|
+
const KEY = "lowercase letters, digits and hyphens";
|
|
56
67
|
const TASK_FIELD_FLAGS =
|
|
57
68
|
"[--state <state>] [--priority <priority>] [--category <key>] [--goal <id>] [--starts <day>] [--due <day>] [--assignee <login|me>] [--parent <number|id>]";
|
|
58
69
|
const TASK_FIELD_LIMITS = [
|
|
@@ -143,7 +154,76 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
143
154
|
categories: {
|
|
144
155
|
usage: "categories",
|
|
145
156
|
about:
|
|
146
|
-
"The categories tasks are sorted into, the same in every project: each with its key, which --category takes, its name and its colour. An administrator keeps them
|
|
157
|
+
"The categories tasks are sorted into, the same in every project: each with its key, which --category takes, its name and its colour. An administrator keeps them: console category, or the Categories page.",
|
|
158
|
+
},
|
|
159
|
+
people: {
|
|
160
|
+
usage: "people",
|
|
161
|
+
about:
|
|
162
|
+
"Everyone of the workspace, by login, with their name where the host gives one and what they are in it: admin, who configures the workspace, or member, who works in the projects they are in. The logins are what --assignee, member and person role take.",
|
|
163
|
+
},
|
|
164
|
+
person: {
|
|
165
|
+
usage: "person role <login> <admin|member>",
|
|
166
|
+
about:
|
|
167
|
+
"Says what a person is in the workspace, for an administrator: the person themselves on the People page, or their agent here, named on it. The board keeps at least one administrator.",
|
|
168
|
+
refusals: [
|
|
169
|
+
"auth.forbidden: only an administrator says what a person is",
|
|
170
|
+
"person.last_admin: the board keeps at least one administrator; make another one first",
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
project: {
|
|
174
|
+
usage:
|
|
175
|
+
'project <key> | project new <key> "<name>" [--description <markdown>|--file <path>] [--visibility workspace|private] [--skills <address>|none] [--color <name>] [--symbol <text>] | project edit <key> [--name "<name>"] [the same flags; none clears] | project icon <key> <file> | project icon <key> --remove',
|
|
176
|
+
about:
|
|
177
|
+
"One project in full; or a project made, with you as its owner, private unless --visibility workspace says everyone of the workspace works in it; or one changed, only in what the flags say, for an owner; or its image for its mark, set from a file of this machine or taken away. The organization's skills are the project's unless --skills names an address of its own.",
|
|
178
|
+
limits: [
|
|
179
|
+
`key: ${KEY}, up to ${MAX_PROJECT_KEY_LENGTH} characters; it does not change once made`,
|
|
180
|
+
`name: one line, up to ${MAX_PROJECT_NAME_LENGTH} characters; description: up to ${MAX_PROJECT_DESCRIPTION_LENGTH}`,
|
|
181
|
+
`color: ${COLORS}; symbol: up to ${MAX_PROJECT_SYMBOL_LENGTH} characters without spaces, shown on the colour; left out, the pages choose both from the key and the name`,
|
|
182
|
+
`image: a PNG, a JPEG, a GIF or a WebP of at most ${MAX_PROJECT_ICON_BYTES} bytes`,
|
|
183
|
+
],
|
|
184
|
+
refusals: [
|
|
185
|
+
"project.not_found: no project has that key, or it is not yours to see",
|
|
186
|
+
"auth.forbidden: only an owner of the project changes it",
|
|
187
|
+
"project.key_taken: a project has that key already",
|
|
188
|
+
"project.invalid_address: the skills address is not one as SkillCDN spells it",
|
|
189
|
+
"project.invalid_icon: the file is not an image of those kinds, by its bytes",
|
|
190
|
+
"request.too_large: the image is over the limit",
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
members: {
|
|
194
|
+
usage: "members",
|
|
195
|
+
about:
|
|
196
|
+
"Those listed in the project, by login, with what each is in it (owner, who configures it; member, who works on it) and since when. A project open to the workspace has everyone of it as a member besides, and an administrator owns every project.",
|
|
197
|
+
},
|
|
198
|
+
member: {
|
|
199
|
+
usage:
|
|
200
|
+
"member add <login> [--role owner|member] | member role <login> <owner|member> | member remove <login>",
|
|
201
|
+
about:
|
|
202
|
+
"Lists a person of the workspace in the project, as a member unless --role says owner; changes what they are in it; or takes them off the list. For an owner of the project.",
|
|
203
|
+
refusals: [
|
|
204
|
+
"auth.forbidden: only an owner of the project lists its members",
|
|
205
|
+
"member.invalid_person: the person is not of the workspace",
|
|
206
|
+
"member.exists: the person is listed already; member role changes what they are",
|
|
207
|
+
"member.not_found: the person is not listed in the project",
|
|
208
|
+
],
|
|
209
|
+
},
|
|
210
|
+
category: {
|
|
211
|
+
usage:
|
|
212
|
+
'category new <key> "<name>" [--color <name>] | category edit <key> [--name "<name>"] [--color <name>] | category remove <key>',
|
|
213
|
+
about:
|
|
214
|
+
"Makes a category tasks are sorted into, the same in every project; changes its name or its colour; or removes one no task is sorted into. For an administrator.",
|
|
215
|
+
limits: [
|
|
216
|
+
`key: ${KEY}, up to ${MAX_CATEGORY_KEY_LENGTH} characters; it does not change once made`,
|
|
217
|
+
`name: one line, up to ${MAX_CATEGORY_NAME_LENGTH} characters; color: ${COLORS}, gray when left out`,
|
|
218
|
+
`at most ${MAX_CATEGORIES} categories in the workspace`,
|
|
219
|
+
],
|
|
220
|
+
refusals: [
|
|
221
|
+
"auth.forbidden: only an administrator keeps the categories",
|
|
222
|
+
"category.key_taken: a category has that key already",
|
|
223
|
+
"category.not_found: no category has that key",
|
|
224
|
+
"category.in_use: tasks are sorted into it; move them out first (console task edit <number> --category <other|none>)",
|
|
225
|
+
"category.too_many: the workspace holds as many categories as one may",
|
|
226
|
+
],
|
|
147
227
|
},
|
|
148
228
|
skills: {
|
|
149
229
|
usage: "skills",
|
|
@@ -352,6 +432,11 @@ const SECTIONS: readonly { readonly title: string; readonly commands: readonly s
|
|
|
352
432
|
title: "The documents: the project's pages of Markdown, in folders, by path",
|
|
353
433
|
commands: ["docs", "doc", "write", "attach", "archive", "restore"],
|
|
354
434
|
},
|
|
435
|
+
{
|
|
436
|
+
title:
|
|
437
|
+
"Configuring, as the person: what an administrator or an owner does on the pages, their agent does here, named on it",
|
|
438
|
+
commands: ["people", "person", "project", "members", "member", "category"],
|
|
439
|
+
},
|
|
355
440
|
];
|
|
356
441
|
|
|
357
442
|
/** The whole usage, as `console help` prints it, naming the version when it is known. */
|
package/src/components/board.tsx
CHANGED
|
@@ -9,7 +9,16 @@ import {
|
|
|
9
9
|
} from "../api.js";
|
|
10
10
|
import { useWords } from "../i18n/index.js";
|
|
11
11
|
import type { BoardFilter } from "../router.js";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
Avatar,
|
|
14
|
+
Badge,
|
|
15
|
+
Button,
|
|
16
|
+
CategoryBadge,
|
|
17
|
+
cx,
|
|
18
|
+
DueBadge,
|
|
19
|
+
PriorityBadge,
|
|
20
|
+
personName,
|
|
21
|
+
} from "./ui.js";
|
|
13
22
|
|
|
14
23
|
// The board: one column per state, the tasks in it, the sooner due first and the newest after.
|
|
15
24
|
// A card says what a person needs at a glance and nothing more: the number and the title, the
|
|
@@ -256,7 +265,7 @@ export function BoardFilters(props: BoardFiltersProps) {
|
|
|
256
265
|
.filter((person) => person.id !== props.me?.id)
|
|
257
266
|
.map((person) => (
|
|
258
267
|
<option key={person.id} value={person.id}>
|
|
259
|
-
{person
|
|
268
|
+
{personName(person)}
|
|
260
269
|
</option>
|
|
261
270
|
))}
|
|
262
271
|
</select>
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
weekdays,
|
|
13
13
|
} from "../dates.js";
|
|
14
14
|
import { useLanguage, useWords } from "../i18n/index.js";
|
|
15
|
-
import { cx, PersonChip } from "./ui.js";
|
|
15
|
+
import { cx, PersonChip, personName } from "./ui.js";
|
|
16
16
|
|
|
17
17
|
// The calendar of a project (ADR-0019): the month as a grid. A task planned from a start to a
|
|
18
18
|
// due day lies across its days as a bar, in a lane of its own within each week so that the bars
|
|
@@ -313,7 +313,7 @@ export function workloadOf(tasks: readonly RestTask[], today: string): WorkloadR
|
|
|
313
313
|
if (b.person === undefined) {
|
|
314
314
|
return -1;
|
|
315
315
|
}
|
|
316
|
-
return a.person.
|
|
316
|
+
return personName(a.person).localeCompare(personName(b.person));
|
|
317
317
|
});
|
|
318
318
|
}
|
|
319
319
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { PersonRole, RestPerson } from "../api.js";
|
|
2
2
|
import { useWords } from "../i18n/index.js";
|
|
3
3
|
import { PERSON_ROLES } from "../vocabulary.js";
|
|
4
|
-
import { PersonChip, RoleBadge } from "./ui.js";
|
|
4
|
+
import { PersonChip, personName, RoleBadge } from "./ui.js";
|
|
5
5
|
|
|
6
6
|
// The people of the board, with what each may do. An administrator changes roles here; a
|
|
7
7
|
// member reads. Takes its data as props and nothing from the network.
|
|
@@ -24,15 +24,15 @@ export function PeopleList(props: PeopleListProps) {
|
|
|
24
24
|
{props.people.map((person) => (
|
|
25
25
|
<li key={person.id} className="sc-person-row">
|
|
26
26
|
<PersonChip person={person} size="md" />
|
|
27
|
-
<span className="sc-person-name">
|
|
28
|
-
{person.name
|
|
29
|
-
{props.me?.id === person.id && <span
|
|
27
|
+
<span className="sc-person-name sc-muted">
|
|
28
|
+
{person.name === null ? "" : person.login}
|
|
29
|
+
{props.me?.id === person.id && <span> {words.common.you}</span>}
|
|
30
30
|
</span>
|
|
31
31
|
{props.onChangeRole === undefined ? (
|
|
32
32
|
<RoleBadge role={person.role} />
|
|
33
33
|
) : (
|
|
34
34
|
<label className="sc-field sc-field-inline">
|
|
35
|
-
<span className="sc-visually-hidden">{words.people.roleOf(person
|
|
35
|
+
<span className="sc-visually-hidden">{words.people.roleOf(personName(person))}</span>
|
|
36
36
|
<select
|
|
37
37
|
className="sc-input"
|
|
38
38
|
value={person.role}
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
type RestProjectPatch,
|
|
24
24
|
} from "../api.js";
|
|
25
25
|
import { useWords } from "../i18n/index.js";
|
|
26
|
-
import { Badge, Button, cx, PersonChip, Time } from "./ui.js";
|
|
26
|
+
import { Badge, Button, cx, PersonChip, personName, Time } from "./ui.js";
|
|
27
27
|
|
|
28
28
|
/** A project's mark: its symbol on its colour, so that projects tell apart at a glance. */
|
|
29
29
|
export function ProjectMark(props: {
|
|
@@ -392,7 +392,9 @@ export function MemberList(props: MemberListProps) {
|
|
|
392
392
|
<ProjectRoleBadge role={member.role} />
|
|
393
393
|
) : (
|
|
394
394
|
<label className="sc-field sc-field-inline">
|
|
395
|
-
<span className="sc-visually-hidden">
|
|
395
|
+
<span className="sc-visually-hidden">
|
|
396
|
+
{members.roleOf(personName(member.person))}
|
|
397
|
+
</span>
|
|
396
398
|
<select
|
|
397
399
|
className="sc-input"
|
|
398
400
|
value={member.role}
|
|
@@ -446,7 +448,7 @@ export function MemberList(props: MemberListProps) {
|
|
|
446
448
|
<option value="">{members.chooseSomeone}</option>
|
|
447
449
|
{candidates.map((person) => (
|
|
448
450
|
<option key={person.id} value={person.id}>
|
|
449
|
-
{person
|
|
451
|
+
{personName(person)}
|
|
450
452
|
</option>
|
|
451
453
|
))}
|
|
452
454
|
</select>
|