@skillcdn/console 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/api.d.ts +4 -4
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +4 -4
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts +5 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +290 -46
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/credentials.d.ts +5 -0
- package/dist/cli/credentials.d.ts.map +1 -1
- package/dist/cli/credentials.js +2 -0
- package/dist/cli/credentials.js.map +1 -1
- package/dist/cli/directory.d.ts +7 -0
- package/dist/cli/directory.d.ts.map +1 -1
- package/dist/cli/directory.js +7 -0
- package/dist/cli/directory.js.map +1 -1
- package/dist/cli/format.d.ts +10 -1
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +76 -20
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +42 -23
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/main.js +1 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/client.d.ts +39 -9
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +24 -3
- package/dist/client.js.map +1 -1
- package/dist/compat.d.ts +28 -6
- package/dist/compat.d.ts.map +1 -1
- package/dist/compat.js +55 -14
- package/dist/compat.js.map +1 -1
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +6 -4
- package/dist/components/board.js.map +1 -1
- package/dist/components/calendar.d.ts +18 -8
- package/dist/components/calendar.d.ts.map +1 -1
- package/dist/components/calendar.js +39 -27
- package/dist/components/calendar.js.map +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +2 -0
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/pack-connect.d.ts +23 -0
- package/dist/components/pack-connect.d.ts.map +1 -0
- package/dist/components/pack-connect.js +86 -0
- package/dist/components/pack-connect.js.map +1 -0
- package/dist/components/pack-token.d.ts +27 -0
- package/dist/components/pack-token.d.ts.map +1 -0
- package/dist/components/pack-token.js +11 -0
- package/dist/components/pack-token.js.map +1 -0
- package/dist/components/picker.d.ts +39 -0
- package/dist/components/picker.d.ts.map +1 -0
- package/dist/components/picker.js +155 -0
- package/dist/components/picker.js.map +1 -0
- package/dist/components/projects.d.ts +13 -0
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +37 -5
- package/dist/components/projects.js.map +1 -1
- package/dist/components/task-form.d.ts +18 -4
- package/dist/components/task-form.d.ts.map +1 -1
- package/dist/components/task-form.js +56 -13
- package/dist/components/task-form.js.map +1 -1
- package/dist/components/task-list.js +2 -2
- package/dist/components/task-list.js.map +1 -1
- package/dist/components/task-view.d.ts +7 -1
- package/dist/components/task-view.d.ts.map +1 -1
- package/dist/components/task-view.js +6 -5
- package/dist/components/task-view.js.map +1 -1
- package/dist/components/ui.d.ts +7 -0
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +11 -0
- package/dist/components/ui.js.map +1 -1
- package/dist/components/work.d.ts +3 -1
- package/dist/components/work.d.ts.map +1 -1
- package/dist/components/work.js +6 -4
- package/dist/components/work.js.map +1 -1
- package/dist/components/workspace.d.ts +46 -0
- package/dist/components/workspace.d.ts.map +1 -0
- package/dist/components/workspace.js +124 -0
- package/dist/components/workspace.js.map +1 -0
- package/dist/console.css +319 -11
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +83 -24
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +26 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +59 -1
- package/dist/data.js.map +1 -1
- package/dist/dates.d.ts +12 -2
- package/dist/dates.d.ts.map +1 -1
- package/dist/dates.js +20 -2
- package/dist/dates.js.map +1 -1
- package/dist/i18n/en.d.ts +64 -3
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +81 -16
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +77 -16
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +2 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +2 -0
- package/dist/limits.js.map +1 -1
- package/dist/router.d.ts +4 -4
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +11 -2
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +4 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +4 -0
- package/dist/routes.js.map +1 -1
- package/dist/schemas.d.ts +194 -13
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +121 -21
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +21 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +18 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +25 -20
- package/docs/specs/rest.md +42 -19
- package/package.json +2 -2
- package/skills/building-a-console/SKILL.md +6 -6
- package/skills/building-a-console/references/the-look-from-a-website.md +39 -0
- package/skills/deploying-a-console/SKILL.md +9 -8
- package/skills/deploying-a-console/references/resources.md +1 -1
- package/skills/working-the-board/SKILL.md +5 -5
- package/skills/working-the-board/references/setting-up.md +1 -1
- package/skills/working-the-board/references/terms.md +3 -1
- package/skills/working-the-board/references/the-sentence.md +1 -1
- package/src/api.ts +21 -0
- package/src/cli/cli.ts +384 -51
- package/src/cli/credentials.ts +7 -0
- package/src/cli/directory.ts +9 -0
- package/src/cli/format.ts +83 -20
- package/src/cli/help.ts +50 -23
- package/src/cli/main.ts +1 -0
- package/src/client.ts +107 -10
- package/src/compat.ts +63 -14
- package/src/components/board.tsx +10 -4
- package/src/components/calendar.tsx +76 -34
- package/src/components/event-feed.tsx +2 -0
- package/src/components/pack-connect.tsx +179 -0
- package/src/components/pack-token.tsx +74 -0
- package/src/components/picker.tsx +297 -0
- package/src/components/projects.tsx +108 -9
- package/src/components/task-form.tsx +149 -63
- package/src/components/task-list.tsx +2 -2
- package/src/components/task-view.tsx +23 -9
- package/src/components/ui.tsx +25 -0
- package/src/components/work.tsx +10 -5
- package/src/components/workspace.tsx +298 -0
- package/src/console.tsx +202 -32
- package/src/data.ts +101 -1
- package/src/dates.ts +23 -2
- package/src/i18n/en.ts +92 -16
- package/src/i18n/ko.ts +84 -16
- package/src/index.ts +32 -1
- package/src/limits.ts +2 -0
- package/src/router.ts +17 -5
- package/src/routes.ts +4 -0
- package/src/schemas.ts +140 -19
- package/src/styles/console.css +319 -11
- package/src/vocabulary.ts +28 -0
package/src/cli/credentials.ts
CHANGED
|
@@ -22,6 +22,11 @@ export interface Login {
|
|
|
22
22
|
readonly agent: string | undefined;
|
|
23
23
|
/** When the command was signed in, when known. */
|
|
24
24
|
readonly since: string | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* The workspace's time zone as the console last said it, at login or whoami (ADR-0035): what
|
|
27
|
+
* the command prints moments in, without asking again; none when the workspace names none.
|
|
28
|
+
*/
|
|
29
|
+
readonly timeZone?: string | undefined;
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
/** Everything the command keeps: the logins, and the console a directory that names none works at. */
|
|
@@ -71,6 +76,7 @@ const loginSchema = z.object({
|
|
|
71
76
|
token: z.string().check(z.minLength(1), z.maxLength(1024)),
|
|
72
77
|
agent: z.optional(z.string().check(z.maxLength(200))),
|
|
73
78
|
since: z.optional(z.string().check(z.maxLength(40))),
|
|
79
|
+
timeZone: z.optional(z.string().check(z.minLength(1), z.maxLength(64))),
|
|
74
80
|
});
|
|
75
81
|
const fileSchema = z.object({
|
|
76
82
|
version: z.literal(2),
|
|
@@ -95,6 +101,7 @@ export function parseCredentials(json: unknown): Credentials | undefined {
|
|
|
95
101
|
token: login.token,
|
|
96
102
|
agent: login.agent,
|
|
97
103
|
since: login.since,
|
|
104
|
+
...(login.timeZone === undefined ? {} : { timeZone: login.timeZone }),
|
|
98
105
|
})),
|
|
99
106
|
};
|
|
100
107
|
}
|
package/src/cli/directory.ts
CHANGED
|
@@ -15,6 +15,15 @@ import * as z from "zod/mini";
|
|
|
15
15
|
/** The file's name, in the working directory or one above it. */
|
|
16
16
|
export const DIRECTORY_FILE = ".skillcdn-console.json";
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* The sentence a checkout's agreement carries to send the agents working there to the console:
|
|
20
|
+
* the one the skill working-the-board keeps (references/the-sentence.md) and this repository's
|
|
21
|
+
* own agreement carries. `console use` prints it, since nothing tells an agent to look until the
|
|
22
|
+
* checkout says so, as `skillcdn use` prints the sentence for a pack.
|
|
23
|
+
*/
|
|
24
|
+
export const THE_SENTENCE =
|
|
25
|
+
"**Bound to a console?** When `.skillcdn-console.json` is in this checkout, or `console whoami` names a project, work the task through that SkillCDN console (`console help` says how).";
|
|
26
|
+
|
|
18
27
|
/** How large the file may be before it is not read: a few names, never more. */
|
|
19
28
|
const MAX_FILE_CHARS = 16 * 1024;
|
|
20
29
|
|
package/src/cli/format.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
RestVersion,
|
|
20
20
|
RestVersionSummary,
|
|
21
21
|
RestWork,
|
|
22
|
+
RestWorkspace,
|
|
22
23
|
} from "../schemas.js";
|
|
23
24
|
|
|
24
25
|
// What the command prints: one line per thing in a list, a few lines for one thing in full.
|
|
@@ -27,6 +28,48 @@ import type {
|
|
|
27
28
|
|
|
28
29
|
const plural = (count: number, word: string): string => `${count} ${word}${count === 1 ? "" : "s"}`;
|
|
29
30
|
|
|
31
|
+
/** The time zone moments are printed in: the workspace's, as the command last learned it (ADR-0035); none leaves them as the API gives them. */
|
|
32
|
+
let momentZone: string | undefined;
|
|
33
|
+
|
|
34
|
+
/** Says which time zone the moments printed from now on are in, or none. */
|
|
35
|
+
export function setMomentZone(zone: string | undefined): void {
|
|
36
|
+
momentZone = zone;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* An instant as the command prints it: in the workspace's time zone, as ISO 8601 with its
|
|
41
|
+
* offset (`2026-10-18T15:04:17+09:00`), when a zone is known; else as the API gave it, in UTC.
|
|
42
|
+
*/
|
|
43
|
+
export function moment(iso: string): string {
|
|
44
|
+
if (momentZone === undefined) {
|
|
45
|
+
return iso;
|
|
46
|
+
}
|
|
47
|
+
const date = new Date(iso);
|
|
48
|
+
if (Number.isNaN(date.getTime())) {
|
|
49
|
+
return iso;
|
|
50
|
+
}
|
|
51
|
+
let parts: Intl.DateTimeFormatPart[];
|
|
52
|
+
try {
|
|
53
|
+
parts = new Intl.DateTimeFormat("en-US", {
|
|
54
|
+
timeZone: momentZone,
|
|
55
|
+
hourCycle: "h23",
|
|
56
|
+
year: "numeric",
|
|
57
|
+
month: "2-digit",
|
|
58
|
+
day: "2-digit",
|
|
59
|
+
hour: "2-digit",
|
|
60
|
+
minute: "2-digit",
|
|
61
|
+
second: "2-digit",
|
|
62
|
+
timeZoneName: "longOffset",
|
|
63
|
+
}).formatToParts(date);
|
|
64
|
+
} catch {
|
|
65
|
+
return iso;
|
|
66
|
+
}
|
|
67
|
+
const of = (type: Intl.DateTimeFormatPartTypes): string =>
|
|
68
|
+
parts.find((part) => part.type === type)?.value ?? "";
|
|
69
|
+
const offset = of("timeZoneName").replace("GMT", "");
|
|
70
|
+
return `${of("year")}-${of("month")}-${of("day")}T${of("hour")}:${of("minute")}:${of("second")}${offset === "" ? "Z" : offset}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
30
73
|
const indent = (text: string): string =>
|
|
31
74
|
text
|
|
32
75
|
.trimEnd()
|
|
@@ -54,7 +97,7 @@ export function formatProject(project: RestProject): string {
|
|
|
54
97
|
`project ${project.key}: ${project.name}`,
|
|
55
98
|
`visibility: ${project.visibility} role: ${project.role} skills: ${project.skillsAddress ?? "the organization's"}`,
|
|
56
99
|
`color: ${project.color ?? "none"} symbol: ${project.symbol ?? "none"} image: ${project.icon === null ? "none" : project.icon.contentType}`,
|
|
57
|
-
`pack token: ${project.packToken === null ? "none" : `set ${project.packToken.setAt}`}`,
|
|
100
|
+
`pack token: ${project.packToken === null ? "none" : `set ${moment(project.packToken.setAt)}`}`,
|
|
58
101
|
];
|
|
59
102
|
if (project.description.trim().length > 0) {
|
|
60
103
|
lines.push("", project.description.trimEnd());
|
|
@@ -78,9 +121,25 @@ export function formatListingLine(listing: RestListing): string {
|
|
|
78
121
|
return `${listing.login} (let in at ${HOST_NAMES[listing.host]}, not signed in yet)`;
|
|
79
122
|
}
|
|
80
123
|
|
|
124
|
+
/** The workspace as its Settings page shows it: what it is called, who sees whom, the week, the time zone, where the skills come from, and what tokens may last. */
|
|
125
|
+
export function formatWorkspace(workspace: RestWorkspace): string {
|
|
126
|
+
const visibility = workspace.peopleVisibility ?? "everyone";
|
|
127
|
+
const zone = workspace.timeZone ?? null;
|
|
128
|
+
const token = workspace.skills.packToken ?? null;
|
|
129
|
+
return [
|
|
130
|
+
`workspace ${workspace.name}`,
|
|
131
|
+
`people visibility: ${visibility} (${visibility === "everyone" ? "everyone sees everyone" : "a member sees only the people they share a project with; an administrator sees everyone"})`,
|
|
132
|
+
`week begins on: ${workspace.weekStart ?? "auto"} (auto: the system default, as each device has it)`,
|
|
133
|
+
`time zone: ${zone ?? "none"} (${zone === null ? "moments are printed as the API gives them, in UTC" : "moments are printed in it, as ISO 8601 with its offset"})`,
|
|
134
|
+
`skills: ${workspace.skills.address ?? "no address of the organization's"} at ${workspace.skills.source}`,
|
|
135
|
+
`SkillCDN token: ${token === null ? "none" : `set ${moment(token.setAt)}`} (${token === null ? "the organization's pack is read with none" : "the organization's pack is read with it by a project with no address and no token of its own"})`,
|
|
136
|
+
`token days at most: ${workspace.tokenDaysAtMost ?? "a person chooses"}`,
|
|
137
|
+
].join("\n");
|
|
138
|
+
}
|
|
139
|
+
|
|
81
140
|
/** One member of a project: their login, what they are in it, and since when. */
|
|
82
141
|
export function formatMemberLine(member: RestMember): string {
|
|
83
|
-
return `${member.person.login} ${member.role} (since ${member.addedAt.slice(0, 10)})`;
|
|
142
|
+
return `${member.person.login} ${member.role} (since ${moment(member.addedAt).slice(0, 10)})`;
|
|
84
143
|
}
|
|
85
144
|
|
|
86
145
|
/** The line under a page of a listing that has more after it: how to ask for the next page. */
|
|
@@ -88,10 +147,14 @@ export function formatMore(more: boolean, limit: number, offset: number): string
|
|
|
88
147
|
return more ? `\n... more: --offset ${offset + limit}` : "";
|
|
89
148
|
}
|
|
90
149
|
|
|
150
|
+
/** The logins of the people at work on a task, in the order they came to it. */
|
|
151
|
+
const logins = (people: readonly RestPerson[]): string =>
|
|
152
|
+
people.map((person) => person.login).join(", ");
|
|
153
|
+
|
|
91
154
|
export function formatTaskLine(task: RestTask): string {
|
|
92
155
|
const notes = [`owner ${task.owner.login}`];
|
|
93
|
-
if (task.
|
|
94
|
-
notes.push(`
|
|
156
|
+
if (task.workers.length > 0) {
|
|
157
|
+
notes.push(`workers ${logins(task.workers)}`);
|
|
95
158
|
}
|
|
96
159
|
if (task.category !== null) {
|
|
97
160
|
notes.push(task.category.key);
|
|
@@ -121,9 +184,9 @@ function scheduleLines(task: RestTask): string[] {
|
|
|
121
184
|
lines.push(plan.join(" "));
|
|
122
185
|
}
|
|
123
186
|
const happened = [
|
|
124
|
-
task.startedAt === null ? undefined : `started: ${task.startedAt}`,
|
|
125
|
-
task.doneAt === null ? undefined : `done: ${task.doneAt}`,
|
|
126
|
-
task.archivedAt === null ? undefined : `archived: ${task.archivedAt}`,
|
|
187
|
+
task.startedAt === null ? undefined : `started: ${moment(task.startedAt)}`,
|
|
188
|
+
task.doneAt === null ? undefined : `done: ${moment(task.doneAt)}`,
|
|
189
|
+
task.archivedAt === null ? undefined : `archived: ${moment(task.archivedAt)}`,
|
|
127
190
|
].filter((part) => part !== undefined);
|
|
128
191
|
if (happened.length > 0) {
|
|
129
192
|
lines.push(happened.join(" "));
|
|
@@ -140,7 +203,7 @@ export function formatTask(
|
|
|
140
203
|
): string {
|
|
141
204
|
const lines = [
|
|
142
205
|
`#${task.number} ${task.title}`,
|
|
143
|
-
`state: ${task.state} priority: ${task.priority} owner: ${task.owner.login}
|
|
206
|
+
`state: ${task.state} priority: ${task.priority} owner: ${task.owner.login} workers: ${task.workers.length === 0 ? "nobody" : logins(task.workers)}`,
|
|
144
207
|
`id: ${task.id}${task.parentId === null ? "" : ` part of: ${task.parentId}`}`,
|
|
145
208
|
...scheduleLines(task),
|
|
146
209
|
];
|
|
@@ -181,7 +244,7 @@ export function formatTask(
|
|
|
181
244
|
export function formatComment(comment: RestComment): string {
|
|
182
245
|
const who =
|
|
183
246
|
comment.agent === null ? comment.author.login : `${comment.author.login} as ${comment.agent}`;
|
|
184
|
-
const lines = [` ${who}, ${comment.createdAt}, comment ${comment.id}:`];
|
|
247
|
+
const lines = [` ${who}, ${moment(comment.createdAt)}, comment ${comment.id}:`];
|
|
185
248
|
if (comment.body.trim().length > 0) {
|
|
186
249
|
lines.push(indent(comment.body));
|
|
187
250
|
}
|
|
@@ -199,7 +262,7 @@ export function formatRunLine(run: RestRun): string {
|
|
|
199
262
|
notes.push(`waiting for decision ${run.waitingFor}`);
|
|
200
263
|
}
|
|
201
264
|
if (run.endedAt === null) {
|
|
202
|
-
notes.push(`last heard ${run.lastHeardAt}`);
|
|
265
|
+
notes.push(`last heard ${moment(run.lastHeardAt)}`);
|
|
203
266
|
}
|
|
204
267
|
return `${run.id} ${run.status} ${run.agent} for ${run.person.login} task #${run.taskNumber} (${notes.join(", ")})`;
|
|
205
268
|
}
|
|
@@ -208,7 +271,7 @@ export function formatRunLine(run: RestRun): string {
|
|
|
208
271
|
export function formatRun(run: RestRun, fileUrl?: (artifactId: string) => string): string {
|
|
209
272
|
const lines = [
|
|
210
273
|
formatRunLine(run),
|
|
211
|
-
`started: ${run.startedAt}${run.endedAt === null ? "" : ` ended: ${run.endedAt}`}`,
|
|
274
|
+
`started: ${moment(run.startedAt)}${run.endedAt === null ? "" : ` ended: ${moment(run.endedAt)}`}`,
|
|
212
275
|
];
|
|
213
276
|
if (run.summary !== null) {
|
|
214
277
|
lines.push("", "summary:", indent(run.summary));
|
|
@@ -216,7 +279,7 @@ export function formatRun(run: RestRun, fileUrl?: (artifactId: string) => string
|
|
|
216
279
|
if (run.reports.length > 0) {
|
|
217
280
|
lines.push("", "reports:");
|
|
218
281
|
for (const report of run.reports) {
|
|
219
|
-
lines.push(` [${report.createdAt}]`, indent(report.body));
|
|
282
|
+
lines.push(` [${moment(report.createdAt)}]`, indent(report.body));
|
|
220
283
|
}
|
|
221
284
|
}
|
|
222
285
|
if (run.artifacts.length > 0) {
|
|
@@ -246,7 +309,7 @@ function formatRulesHalf(where: "the workspace" | "this project", rules: RestRul
|
|
|
246
309
|
const facts =
|
|
247
310
|
page === null
|
|
248
311
|
? undefined
|
|
249
|
-
: `v${page.version}, by ${writer(page.updatedBy, page.agent)}; ${page.updatedAt}`;
|
|
312
|
+
: `v${page.version}, by ${writer(page.updatedBy, page.agent)}; ${moment(page.updatedAt)}`;
|
|
250
313
|
if (rules.source === "none" || rules.body === null) {
|
|
251
314
|
return `Rules of ${where}: none of its own; the workspace's hold.`;
|
|
252
315
|
}
|
|
@@ -316,9 +379,9 @@ export function formatCategoryLine(category: RestCategory): string {
|
|
|
316
379
|
export function formatWork(work: RestWork): string {
|
|
317
380
|
const lines: string[] = [];
|
|
318
381
|
if (work.tasks.length === 0) {
|
|
319
|
-
lines.push("No open task is
|
|
382
|
+
lines.push("No open task is on your plate.");
|
|
320
383
|
} else {
|
|
321
|
-
lines.push("tasks
|
|
384
|
+
lines.push("tasks you work on:");
|
|
322
385
|
for (const { project, task } of work.tasks) {
|
|
323
386
|
lines.push(` ${project.key} ${formatTaskLine(task)}`);
|
|
324
387
|
}
|
|
@@ -333,10 +396,10 @@ export function formatWork(work: RestWork): string {
|
|
|
333
396
|
/** Another person's plate: their open tasks, each with its project; no decisions, which wait for anyone. */
|
|
334
397
|
export function formatPersonWork(person: RestPerson, work: RestPersonWork): string {
|
|
335
398
|
if (work.tasks.length === 0) {
|
|
336
|
-
return `No open task is
|
|
399
|
+
return `No open task is on ${person.login}'s plate.`;
|
|
337
400
|
}
|
|
338
401
|
return [
|
|
339
|
-
`tasks
|
|
402
|
+
`tasks ${person.login} works on:`,
|
|
340
403
|
...work.tasks.map(({ project, task }) => ` ${project.key} ${formatTaskLine(task)}`),
|
|
341
404
|
].join("\n");
|
|
342
405
|
}
|
|
@@ -397,7 +460,7 @@ export function formatDocumentLine(document: RestDocumentSummary): string {
|
|
|
397
460
|
if (document.archivedAt !== null) {
|
|
398
461
|
notes.push("archived");
|
|
399
462
|
}
|
|
400
|
-
return `${document.path} ${document.title} (${notes.join(", ")}; ${document.updatedAt})`;
|
|
463
|
+
return `${document.path} ${document.title} (${notes.join(", ")}; ${moment(document.updatedAt)})`;
|
|
401
464
|
}
|
|
402
465
|
|
|
403
466
|
/** A folder's folders and pages, or the pages a search found. */
|
|
@@ -419,7 +482,7 @@ export function formatDocument(
|
|
|
419
482
|
): string {
|
|
420
483
|
const lines = [
|
|
421
484
|
`${document.path}: ${document.title}`,
|
|
422
|
-
`version ${document.version}, by ${writer(document.updatedBy, document.agent)} at ${document.updatedAt}${document.archivedAt === null ? "" : `; archived ${document.archivedAt}`}`,
|
|
485
|
+
`version ${document.version}, by ${writer(document.updatedBy, document.agent)} at ${moment(document.updatedAt)}${document.archivedAt === null ? "" : `; archived ${moment(document.archivedAt)}`}`,
|
|
423
486
|
`id: ${document.id}`,
|
|
424
487
|
];
|
|
425
488
|
if (document.body.trim().length > 0) {
|
|
@@ -453,7 +516,7 @@ export function formatDocument(
|
|
|
453
516
|
}
|
|
454
517
|
|
|
455
518
|
export function formatVersionLine(version: RestVersionSummary): string {
|
|
456
|
-
return `v${version.number} ${version.title} (by ${writer(version.author, version.agent)}; ${version.createdAt})`;
|
|
519
|
+
return `v${version.number} ${version.title} (by ${writer(version.author, version.agent)}; ${moment(version.createdAt)})`;
|
|
457
520
|
}
|
|
458
521
|
|
|
459
522
|
/** One version of a document, with its body as it was. */
|
package/src/cli/help.ts
CHANGED
|
@@ -23,9 +23,11 @@ import {
|
|
|
23
23
|
MAX_QUESTION_LENGTH,
|
|
24
24
|
MAX_REPORTS_PER_RUN,
|
|
25
25
|
MAX_RULES_LENGTH,
|
|
26
|
+
MAX_SEARCH_LENGTH,
|
|
26
27
|
MAX_SUMMARY_LENGTH,
|
|
27
28
|
MAX_TITLE_LENGTH,
|
|
28
29
|
MAX_VERSIONS_PER_DOCUMENT,
|
|
30
|
+
MAX_WORKERS,
|
|
29
31
|
MIN_OPTIONS,
|
|
30
32
|
} from "../limits.js";
|
|
31
33
|
import { COLOR_NAMES } from "../vocabulary.js";
|
|
@@ -84,17 +86,17 @@ const DAY = "a day as 2026-10-24, today, tomorrow, or +7 for seven days from tod
|
|
|
84
86
|
const COLORS = `one of ${COLOR_NAMES.join(", ")}`;
|
|
85
87
|
const KEY = "lowercase letters, digits and hyphens";
|
|
86
88
|
const TASK_FIELD_FLAGS =
|
|
87
|
-
"[--state <state>] [--priority <priority>] [--category <key>] [--goal <id>] [--starts <day>] [--due <day>] [--
|
|
89
|
+
"[--state <state>] [--priority <priority>] [--category <key>] [--goal <id>] [--starts <day>] [--due <day>] [--worker <login|me|none>]... [--parent <number|id>]";
|
|
88
90
|
const TASK_FIELD_LIMITS = [
|
|
89
91
|
`starts, due: ${DAY}; the due day is not before the start`,
|
|
90
92
|
"category: the key of one of the workspace's categories (console categories); goal: the id of one of the project's goals (console goals)",
|
|
91
|
-
|
|
93
|
+
`worker: a login of the workspace, or me; given again, or with commas between, for several, up to ${MAX_WORKERS}, in place of the workers there were; none alone for nobody. parent: a task of the project, by its number or its id; console tasks --search <words> finds one by its title`,
|
|
92
94
|
];
|
|
93
95
|
const TASK_FIELD_REFUSALS = [
|
|
94
96
|
"task.invalid_category: no category has that key",
|
|
95
97
|
"task.invalid_goal: the goal is not one of this project",
|
|
96
98
|
"task.invalid_dates: the due day is before the start",
|
|
97
|
-
"task.
|
|
99
|
+
"task.invalid_worker: a person named as a worker may not work in the project",
|
|
98
100
|
"task.invalid_parent: the parent is not a task of the project, or would make a loop",
|
|
99
101
|
];
|
|
100
102
|
|
|
@@ -136,9 +138,10 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
136
138
|
},
|
|
137
139
|
use: {
|
|
138
140
|
usage: "use <key> [--url <origin>] [--account <login>]",
|
|
139
|
-
synopsis:
|
|
141
|
+
synopsis:
|
|
142
|
+
"Binds this directory to a project: writes .skillcdn-console.json here, and prints the sentence for the checkout's agreement.",
|
|
140
143
|
about:
|
|
141
|
-
"Says which project this directory works in: writes .skillcdn-console.json here, which the commands read from this directory or one above it, and which is meant to be committed in a private repository and left out of a public one. The file names the project; and the console, where that is not the default login here or --url says so; and the account, where the console has several logins or --account says so. Never a token. --project <key> on any command, or CONSOLE_PROJECT in the environment, wins over the project it names.",
|
|
144
|
+
"Says which project this directory works in: writes .skillcdn-console.json here, which the commands read from this directory or one above it, and which is meant to be committed in a private repository and left out of a public one. The file names the project; and the console, where that is not the default login here or --url says so; and the account, where the console has several logins or --account says so. Never a token. --project <key> on any command, or CONSOLE_PROJECT in the environment, wins over the project it names. Then prints the sentence for the checkout's agreement (AGENTS.md, or CLAUDE.md where that is the file agents read), to be copied as it is, since nothing sends an agent to the console until the checkout says so.",
|
|
142
145
|
refusals: ["project.not_found: no project has the key, or it is not yours to see"],
|
|
143
146
|
},
|
|
144
147
|
work: {
|
|
@@ -146,14 +149,15 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
146
149
|
synopsis:
|
|
147
150
|
"What is on your plate across your projects: your open tasks, and the decisions waiting.",
|
|
148
151
|
about:
|
|
149
|
-
"What is on your plate across the projects you may see: the open tasks
|
|
152
|
+
"What is on your plate across the projects you may see: the open tasks you work on, the sooner due first, each with its project; and the decisions waiting for a person. Needs no project. --person <login> shows another person's plate, for an administrator or for an owner of a project they are in: the open tasks they work on within the projects you may see, without the decisions, which wait for anyone.",
|
|
150
153
|
},
|
|
151
154
|
tasks: {
|
|
152
155
|
usage:
|
|
153
|
-
"tasks [--state <state>] [--open] [--mine] [--category <key>] [--goal <id>] [--due <day|today|week|overdue>] [--unassigned] [--archived] [--limit <n>] [--offset <n>]",
|
|
156
|
+
"tasks [--state <state>] [--open] [--mine] [--category <key>] [--goal <id>] [--due <day|today|week|overdue>] [--unassigned] [--search <words>] [--archived] [--limit <n>] [--offset <n>]",
|
|
154
157
|
synopsis:
|
|
155
|
-
"The tasks on the board, newest first, narrowed by state,
|
|
156
|
-
about: `The tasks on the board, newest first: number, state, priority, title, who owns and works on each, its category and its due day, and what waits on it. --open keeps the ones not done or dropped, --mine those
|
|
158
|
+
"The tasks on the board, newest first, narrowed by state, worker, category, goal, due day or words of the title.",
|
|
159
|
+
about: `The tasks on the board, newest first: number, state, priority, title, who owns and who works on each, its category and its due day, and what waits on it. --open keeps the ones not done or dropped, --mine those you work on, --unassigned those nobody works on (not with --mine), --category and --goal those sorted into one, --due those due by a day: today, this week (through its last day, the week beginning as the workspace says, console workspace, or as your language has it), or overdue (the open ones past their day), --search those whose title has every word given, whatever the case, and the task of that number for a number (12 or #12): how a task is found by its title, as a parent or otherwise. The tasks put away (ADR-0029) are left out unless --archived asks for them alone. A page at a time: ${DEFAULT_PAGE_LIMIT} unless --limit says otherwise, from --offset on; a last line says when there is more. States: idea, ready, in_progress, in_review, done, dropped.`,
|
|
160
|
+
limits: [`search: 1 to ${MAX_SEARCH_LENGTH} characters`],
|
|
157
161
|
},
|
|
158
162
|
task: {
|
|
159
163
|
usage: `task <number|id> | task new <title> [--body <markdown>|--file <path>] ${TASK_FIELD_FLAGS} | task edit <number|id> [--title <title>] [--body <markdown>|--file <path>] ${TASK_FIELD_FLAGS} | task archive <number|id> | task archive --goal <id> | task restore <number|id>`,
|
|
@@ -170,10 +174,11 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
170
174
|
refusals: ["task.not_found: no task has that number or id", ...TASK_FIELD_REFUSALS],
|
|
171
175
|
},
|
|
172
176
|
goals: {
|
|
173
|
-
usage:
|
|
177
|
+
usage:
|
|
178
|
+
"goals [--category <key>] [--state <state>] [--search <words>] [--limit <n>] [--offset <n>]",
|
|
174
179
|
synopsis: "The roadmap: the project's goals, with their state, their days and how far each is.",
|
|
175
|
-
about:
|
|
176
|
-
|
|
180
|
+
about: `The roadmap of the project: its goals, the active first, each with its id, its state (planned, active, done, dropped), its category, the days it starts and is meant to be reached by, and how far it is, counted from the tasks that work towards it. --category keeps those sorted into one, --state those in one state, --search those whose title has every word given, whatever the case: how a goal's id is found for --goal. A page at a time: ${DEFAULT_PAGE_LIMIT} unless --limit says otherwise, from --offset on; a last line says when there is more.`,
|
|
181
|
+
limits: [`search: 1 to ${MAX_SEARCH_LENGTH} characters`],
|
|
177
182
|
},
|
|
178
183
|
goal: {
|
|
179
184
|
usage:
|
|
@@ -203,9 +208,26 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
203
208
|
},
|
|
204
209
|
people: {
|
|
205
210
|
usage: "people",
|
|
206
|
-
synopsis: "
|
|
211
|
+
synopsis: "The people you see, by login, with what each is in the workspace.",
|
|
207
212
|
about:
|
|
208
|
-
"
|
|
213
|
+
"The people of the workspace you see, 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; a person taken out is marked deactivated. Everyone, unless the workspace shows a member only the people they share a project with (console workspace says which; an administrator sees everyone). Then, where you see everyone, the logins an administrator let in that nobody has signed in with yet. The logins are what --worker, member and person take; a login is found exactly, so an owner lists someone the list does not show.",
|
|
214
|
+
},
|
|
215
|
+
workspace: {
|
|
216
|
+
usage:
|
|
217
|
+
"workspace | workspace edit [--people-visibility <everyone|shared-projects>] [--week-start <auto|monday|sunday>] [--time-zone <zone|none>] | workspace pack-token --connect | workspace pack-token --stdin | workspace pack-token --remove",
|
|
218
|
+
overview:
|
|
219
|
+
"workspace | workspace edit [flags] | workspace pack-token --connect|--stdin|--remove",
|
|
220
|
+
synopsis:
|
|
221
|
+
"The workspace's settings; or some changed, or its SkillCDN token set, for an administrator.",
|
|
222
|
+
about:
|
|
223
|
+
"What the workspace is called; who sees whom on the People page and in the pickers (everyone; or shared-projects: a member sees only the people they share a project with, an administrator everyone; a project open to the workspace has everyone in it, so a member of one sees everyone); the day the calendars begin a week on (auto: the system default, as each device has it); the workspace's time zone, an IANA name such as Asia/Seoul, in which this command prints moments as ISO 8601 with its offset, as the login kept it at login or whoami, or none, which leaves them as the API gives them, in UTC; where the organization's skills come from; and the most days a token may last. edit changes any of the first three, for an administrator; --json leaves moments as the API gives them. pack-token is the SkillCDN token the organization's pack is read with, by every project that names no address of its own and holds no token of its own (ADR-0036), for an administrator: with --connect, the console asks SkillCDN for a code, this command prints the page and the code, a person who may open the repository approves there, signed in at SkillCDN, and the console receives the token, shown to nobody; with --stdin, a token made by hand at SkillCDN is read from standard input and never an argument; --remove takes the token away.",
|
|
224
|
+
refusals: [
|
|
225
|
+
"auth.forbidden: only an administrator changes the workspace's settings or its token",
|
|
226
|
+
"request.invalid: the time zone is not a name the console knows",
|
|
227
|
+
"workspace.pack_token_refused: SkillCDN does not serve the organization's pack to the token, or the console names no address for the organization's skills",
|
|
228
|
+
"workspace.pack_connect_refused: the console names no address for the organization's skills, the pack is on the console's machine, or SkillCDN refused to begin",
|
|
229
|
+
"workspace.pack_token_unsupported, workspace.pack_connect_unsupported: the console keeps no secret to seal a token with (AUTH_SECRET), or reaches no deployment",
|
|
230
|
+
],
|
|
209
231
|
},
|
|
210
232
|
person: {
|
|
211
233
|
usage:
|
|
@@ -224,12 +246,13 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
224
246
|
},
|
|
225
247
|
project: {
|
|
226
248
|
usage:
|
|
227
|
-
'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 | project pack-token <key> --stdin | project pack-token <key> --remove',
|
|
249
|
+
'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 | project pack-token <key> --connect | project pack-token <key> --stdin | project pack-token <key> --remove',
|
|
228
250
|
overview:
|
|
229
|
-
"project <key> | project new <key> <name> [flags] | project edit <key> [flags] | project icon <key> <file>|--remove | project pack-token <key> --stdin|--remove",
|
|
230
|
-
synopsis:
|
|
251
|
+
"project <key> | project new <key> <name> [flags] | project edit <key> [flags] | project icon <key> <file>|--remove | project pack-token <key> --connect|--stdin|--remove",
|
|
252
|
+
synopsis:
|
|
253
|
+
"One project in full; or a project made, changed, given its mark or its SkillCDN token.",
|
|
231
254
|
about:
|
|
232
|
-
"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; or its
|
|
255
|
+
"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; or its SkillCDN token (pack-token, ADR-0028), which the console keeps sealed to read a private pack with, and which a project that reads the organization's pack may leave to the workspace's (console workspace pack-token): with --connect, the console asks SkillCDN for a code, this command prints the page and the code, a person who may open the repository approves there, signed in at SkillCDN, and the console receives the token, shown to nobody (ADR-0036); with --stdin, a token a person made by hand at SkillCDN is read from standard input and never an argument; --remove takes the token away. The organization's skills are the project's unless --skills names an address of its own.",
|
|
233
256
|
limits: [
|
|
234
257
|
`key: ${KEY}, up to ${MAX_PROJECT_KEY_LENGTH} characters; it does not change once made`,
|
|
235
258
|
`name: one line, up to ${MAX_PROJECT_NAME_LENGTH} characters; description: up to ${MAX_PROJECT_DESCRIPTION_LENGTH}`,
|
|
@@ -243,13 +266,16 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
243
266
|
"project.invalid_address: the skills address is not one as SkillCDN spells it",
|
|
244
267
|
"project.invalid_icon: the file is not an image of those kinds, by its bytes",
|
|
245
268
|
"request.too_large: the image is over the limit",
|
|
269
|
+
"project.pack_connect_unsupported: the console reaches no deployment, or keeps no secret to seal a token with",
|
|
270
|
+
"project.pack_connect_refused: the project names no pack, the pack is on the console's machine, or SkillCDN refused to begin",
|
|
271
|
+
"skills.unavailable: SkillCDN could not be reached; try again later",
|
|
246
272
|
],
|
|
247
273
|
},
|
|
248
274
|
members: {
|
|
249
275
|
usage: "members",
|
|
250
|
-
synopsis: "
|
|
276
|
+
synopsis: "The members of the project, with what each is in it.",
|
|
251
277
|
about:
|
|
252
|
-
"
|
|
278
|
+
"The members of the project, by login, with what each is in it (owner, who configures it; member, who works on it) and since when. In a private project, those listed; in one open to the workspace, everyone of it, an owner where listed as one or an administrator, a member otherwise. An administrator owns every project.",
|
|
253
279
|
},
|
|
254
280
|
member: {
|
|
255
281
|
usage:
|
|
@@ -257,11 +283,12 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
257
283
|
synopsis:
|
|
258
284
|
"Lists a person in the project, changes what they are in it, or takes them off; for an owner.",
|
|
259
285
|
about:
|
|
260
|
-
"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. With --project naming several keys with commas, member add lists the person in each of them in turn, one line per project; one listed already is said so, and the rest go on.",
|
|
286
|
+
"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. In a project open to the workspace everyone is a member already: only an owner is listed (member add --role owner, or member role <login> owner), and an owner made a member again stays the member everyone is. With --project naming several keys with commas, member add lists the person in each of them in turn, one line per project; one listed already is said so, and the rest go on.",
|
|
261
287
|
refusals: [
|
|
262
288
|
"auth.forbidden: only an owner of the project lists its members",
|
|
263
289
|
"member.invalid_person: the person is not of the workspace",
|
|
264
290
|
"member.exists: the person is listed already; member role changes what they are",
|
|
291
|
+
"member.open_project: the project is open to the workspace, so everyone is a member already; list an owner with --role owner",
|
|
265
292
|
"member.not_found: the person is not listed in the project",
|
|
266
293
|
],
|
|
267
294
|
},
|
|
@@ -328,7 +355,7 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
328
355
|
usage: "take <number|id> [--agent <name>]",
|
|
329
356
|
synopsis: "Takes a task: a run begins, and the task is in progress and the person's.",
|
|
330
357
|
about:
|
|
331
|
-
"Takes a task: a run begins, and the task is in progress and the person's; the
|
|
358
|
+
"Takes a task: a run begins, and the task is in progress and the person's; the run's id and the task's id are printed on a line of their own, then the task, then the rules of the board. A run still open on the task is taken over, since the board cannot know whether its agent still runs: it ends as abandoned, the feed says who took over, and that agent is refused on its next command; console task <number> shows the runs on a task, with when each was last heard from, before it is taken. --agent is what people see at work; left out, it is what the person called the token.",
|
|
332
359
|
limits: [`agent: one line, up to ${MAX_AGENT_LENGTH} characters`],
|
|
333
360
|
refusals: [
|
|
334
361
|
"task.not_found: no task has that number or id",
|
|
@@ -604,7 +631,7 @@ const SECTIONS: readonly { readonly title: string; readonly commands: readonly s
|
|
|
604
631
|
{
|
|
605
632
|
title:
|
|
606
633
|
"Configuring, as the person: what an administrator or an owner does on the pages, their agent does here, named on it",
|
|
607
|
-
commands: ["people", "person", "project", "members", "member", "category"],
|
|
634
|
+
commands: ["workspace", "people", "person", "project", "members", "member", "category"],
|
|
608
635
|
},
|
|
609
636
|
];
|
|
610
637
|
|
package/src/cli/main.ts
CHANGED
|
@@ -60,6 +60,7 @@ process.exitCode = await runCli(process.argv.slice(2), {
|
|
|
60
60
|
exists: existsSync,
|
|
61
61
|
}),
|
|
62
62
|
notices: createNoticeStore(configDir),
|
|
63
|
+
locale: Intl.DateTimeFormat().resolvedOptions().locale,
|
|
63
64
|
skillsDir: existsSync(skillsDir) ? skillsDir : undefined,
|
|
64
65
|
listen: listenOn,
|
|
65
66
|
});
|