@skillcdn/console 0.1.24 → 0.3.1
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 -7
- package/dist/api.d.ts +2 -2
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +2 -2
- 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 +248 -92
- 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 +13 -28
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +74 -274
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +36 -19
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/pack.d.ts +46 -0
- package/dist/cli/pack.d.ts.map +1 -0
- package/dist/cli/pack.js +250 -0
- package/dist/cli/pack.js.map +1 -0
- package/dist/client.d.ts +26 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +14 -1
- package/dist/client.js.map +1 -1
- package/dist/compat.d.ts +1 -1
- package/dist/compat.js +1 -1
- package/dist/components/calendar.d.ts +5 -1
- package/dist/components/calendar.d.ts.map +1 -1
- package/dist/components/calendar.js +3 -3
- package/dist/components/calendar.js.map +1 -1
- package/dist/components/decision-list.d.ts +13 -1
- package/dist/components/decision-list.d.ts.map +1 -1
- package/dist/components/decision-list.js +74 -29
- package/dist/components/decision-list.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/paged.d.ts +24 -0
- package/dist/components/paged.d.ts.map +1 -1
- package/dist/components/paged.js +40 -4
- package/dist/components/paged.js.map +1 -1
- 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/skills.d.ts.map +1 -1
- package/dist/components/skills.js +8 -4
- package/dist/components/skills.js.map +1 -1
- package/dist/components/ui.d.ts +2 -0
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +7 -2
- 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 +1 -1
- 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 +268 -5
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +62 -23
- 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 +64 -2
- 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 +66 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +80 -8
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +75 -7
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts +10 -5
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +24 -9
- 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 +500 -158
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +169 -165
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +19 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +16 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +19 -14
- package/docs/specs/rest.md +34 -19
- package/package.json +2 -1
- package/skills/building-a-console/SKILL.md +5 -5
- 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/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 +2 -0
- package/skills/working-the-board/references/the-sentence.md +1 -1
- package/src/api.ts +18 -7
- package/src/cli/cli.ts +345 -109
- package/src/cli/credentials.ts +7 -0
- package/src/cli/directory.ts +9 -0
- package/src/cli/format.ts +82 -341
- package/src/cli/help.ts +43 -19
- package/src/cli/pack.ts +356 -0
- package/src/client.ts +84 -0
- package/src/compat.ts +1 -1
- package/src/components/calendar.tsx +8 -2
- package/src/components/decision-list.tsx +227 -94
- 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/paged.tsx +72 -3
- package/src/components/projects.tsx +108 -9
- package/src/components/skills.tsx +51 -33
- package/src/components/ui.tsx +16 -3
- package/src/components/work.tsx +4 -1
- package/src/components/workspace.tsx +298 -0
- package/src/console.tsx +176 -29
- package/src/data.ts +89 -2
- package/src/dates.ts +23 -2
- package/src/i18n/en.ts +94 -8
- package/src/i18n/ko.ts +84 -7
- package/src/index.ts +25 -1
- package/src/router.ts +37 -14
- package/src/routes.ts +4 -0
- package/src/schemas.ts +202 -189
- package/src/styles/console.css +268 -5
- package/src/vocabulary.ts +25 -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
|
@@ -15,17 +15,11 @@ import type {
|
|
|
15
15
|
RestProjectRules,
|
|
16
16
|
RestRules,
|
|
17
17
|
RestRun,
|
|
18
|
-
RestSkillBrowse,
|
|
19
|
-
RestSkillBrowseEntry,
|
|
20
|
-
RestSkillFile,
|
|
21
|
-
RestSkillFind,
|
|
22
|
-
RestSkillLoad,
|
|
23
|
-
RestSkillPage,
|
|
24
|
-
RestSkills,
|
|
25
18
|
RestTask,
|
|
26
19
|
RestVersion,
|
|
27
20
|
RestVersionSummary,
|
|
28
21
|
RestWork,
|
|
22
|
+
RestWorkspace,
|
|
29
23
|
} from "../schemas.js";
|
|
30
24
|
|
|
31
25
|
// What the command prints: one line per thing in a list, a few lines for one thing in full.
|
|
@@ -34,6 +28,48 @@ import type {
|
|
|
34
28
|
|
|
35
29
|
const plural = (count: number, word: string): string => `${count} ${word}${count === 1 ? "" : "s"}`;
|
|
36
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
|
+
|
|
37
73
|
const indent = (text: string): string =>
|
|
38
74
|
text
|
|
39
75
|
.trimEnd()
|
|
@@ -61,7 +97,7 @@ export function formatProject(project: RestProject): string {
|
|
|
61
97
|
`project ${project.key}: ${project.name}`,
|
|
62
98
|
`visibility: ${project.visibility} role: ${project.role} skills: ${project.skillsAddress ?? "the organization's"}`,
|
|
63
99
|
`color: ${project.color ?? "none"} symbol: ${project.symbol ?? "none"} image: ${project.icon === null ? "none" : project.icon.contentType}`,
|
|
64
|
-
`pack token: ${project.packToken === null ? "none" : `set ${project.packToken.setAt}`}`,
|
|
100
|
+
`pack token: ${project.packToken === null ? "none" : `set ${moment(project.packToken.setAt)}`}`,
|
|
65
101
|
];
|
|
66
102
|
if (project.description.trim().length > 0) {
|
|
67
103
|
lines.push("", project.description.trimEnd());
|
|
@@ -85,9 +121,25 @@ export function formatListingLine(listing: RestListing): string {
|
|
|
85
121
|
return `${listing.login} (let in at ${HOST_NAMES[listing.host]}, not signed in yet)`;
|
|
86
122
|
}
|
|
87
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
|
+
|
|
88
140
|
/** One member of a project: their login, what they are in it, and since when. */
|
|
89
141
|
export function formatMemberLine(member: RestMember): string {
|
|
90
|
-
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)})`;
|
|
91
143
|
}
|
|
92
144
|
|
|
93
145
|
/** The line under a page of a listing that has more after it: how to ask for the next page. */
|
|
@@ -128,9 +180,9 @@ function scheduleLines(task: RestTask): string[] {
|
|
|
128
180
|
lines.push(plan.join(" "));
|
|
129
181
|
}
|
|
130
182
|
const happened = [
|
|
131
|
-
task.startedAt === null ? undefined : `started: ${task.startedAt}`,
|
|
132
|
-
task.doneAt === null ? undefined : `done: ${task.doneAt}`,
|
|
133
|
-
task.archivedAt === null ? undefined : `archived: ${task.archivedAt}`,
|
|
183
|
+
task.startedAt === null ? undefined : `started: ${moment(task.startedAt)}`,
|
|
184
|
+
task.doneAt === null ? undefined : `done: ${moment(task.doneAt)}`,
|
|
185
|
+
task.archivedAt === null ? undefined : `archived: ${moment(task.archivedAt)}`,
|
|
134
186
|
].filter((part) => part !== undefined);
|
|
135
187
|
if (happened.length > 0) {
|
|
136
188
|
lines.push(happened.join(" "));
|
|
@@ -188,7 +240,7 @@ export function formatTask(
|
|
|
188
240
|
export function formatComment(comment: RestComment): string {
|
|
189
241
|
const who =
|
|
190
242
|
comment.agent === null ? comment.author.login : `${comment.author.login} as ${comment.agent}`;
|
|
191
|
-
const lines = [` ${who}, ${comment.createdAt}, comment ${comment.id}:`];
|
|
243
|
+
const lines = [` ${who}, ${moment(comment.createdAt)}, comment ${comment.id}:`];
|
|
192
244
|
if (comment.body.trim().length > 0) {
|
|
193
245
|
lines.push(indent(comment.body));
|
|
194
246
|
}
|
|
@@ -206,7 +258,7 @@ export function formatRunLine(run: RestRun): string {
|
|
|
206
258
|
notes.push(`waiting for decision ${run.waitingFor}`);
|
|
207
259
|
}
|
|
208
260
|
if (run.endedAt === null) {
|
|
209
|
-
notes.push(`last heard ${run.lastHeardAt}`);
|
|
261
|
+
notes.push(`last heard ${moment(run.lastHeardAt)}`);
|
|
210
262
|
}
|
|
211
263
|
return `${run.id} ${run.status} ${run.agent} for ${run.person.login} task #${run.taskNumber} (${notes.join(", ")})`;
|
|
212
264
|
}
|
|
@@ -215,7 +267,7 @@ export function formatRunLine(run: RestRun): string {
|
|
|
215
267
|
export function formatRun(run: RestRun, fileUrl?: (artifactId: string) => string): string {
|
|
216
268
|
const lines = [
|
|
217
269
|
formatRunLine(run),
|
|
218
|
-
`started: ${run.startedAt}${run.endedAt === null ? "" : ` ended: ${run.endedAt}`}`,
|
|
270
|
+
`started: ${moment(run.startedAt)}${run.endedAt === null ? "" : ` ended: ${moment(run.endedAt)}`}`,
|
|
219
271
|
];
|
|
220
272
|
if (run.summary !== null) {
|
|
221
273
|
lines.push("", "summary:", indent(run.summary));
|
|
@@ -223,7 +275,7 @@ export function formatRun(run: RestRun, fileUrl?: (artifactId: string) => string
|
|
|
223
275
|
if (run.reports.length > 0) {
|
|
224
276
|
lines.push("", "reports:");
|
|
225
277
|
for (const report of run.reports) {
|
|
226
|
-
lines.push(` [${report.createdAt}]`, indent(report.body));
|
|
278
|
+
lines.push(` [${moment(report.createdAt)}]`, indent(report.body));
|
|
227
279
|
}
|
|
228
280
|
}
|
|
229
281
|
if (run.artifacts.length > 0) {
|
|
@@ -247,77 +299,13 @@ export function formatArtifact(
|
|
|
247
299
|
return artifact.label === null ? what : `${artifact.label}: ${what}`;
|
|
248
300
|
}
|
|
249
301
|
|
|
250
|
-
const SKILLS_WORDS: Readonly<Record<Exclude<RestSkills["status"], "none" | "ready">, string>> = {
|
|
251
|
-
indexing: "SkillCDN is still indexing the repository; ask again in a moment.",
|
|
252
|
-
failed: "SkillCDN could not index the repository.",
|
|
253
|
-
not_found:
|
|
254
|
-
"SkillCDN does not serve this address to the console: nothing is there, or the pack is private and the project holds no pack token for it (console project pack-token <key> --stdin, for an owner).",
|
|
255
|
-
token_invalid:
|
|
256
|
-
"The pack token the project holds no longer works at SkillCDN: an owner sets a new one (console project pack-token <key> --stdin).",
|
|
257
|
-
unavailable: "SkillCDN could not be reached; ask again later.",
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
/** A count with its noun: `1 skill`, `2 skills`. */
|
|
261
|
-
const counted = (n: number, noun: string): string => `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* The organization's skills: where they are, the pack by name and by count, each skill with
|
|
265
|
-
* what an agent loads it by, where a person connects an agent that has no connection to the
|
|
266
|
-
* address, and where the console's own skills are on this machine, when the command knows.
|
|
267
|
-
*/
|
|
268
|
-
export function formatSkills(skills: RestSkills, ownSkills?: string): string {
|
|
269
|
-
const own =
|
|
270
|
-
ownSkills === undefined
|
|
271
|
-
? []
|
|
272
|
-
: ["", `The console's own skills, for working the board: ${ownSkills}`];
|
|
273
|
-
if (skills.status === "none" || skills.address === null || skills.page === null) {
|
|
274
|
-
return ["No skills address: neither this project nor the console names one.", ...own].join(
|
|
275
|
-
"\n",
|
|
276
|
-
);
|
|
277
|
-
}
|
|
278
|
-
const head = `skills at ${skills.address}, served by ${skills.source}: ${skills.status}`;
|
|
279
|
-
if (skills.status !== "ready") {
|
|
280
|
-
return [head, SKILLS_WORDS[skills.status], ...own].join("\n");
|
|
281
|
-
}
|
|
282
|
-
const lines = [head];
|
|
283
|
-
if (skills.pack !== undefined) {
|
|
284
|
-
const named = [skills.pack.name, skills.pack.description]
|
|
285
|
-
.filter((text): text is string => text !== null)
|
|
286
|
-
.join(" ");
|
|
287
|
-
if (named !== "") {
|
|
288
|
-
lines.push(named);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
if (skills.counts !== undefined) {
|
|
292
|
-
lines.push(
|
|
293
|
-
`${counted(skills.counts.skills, "skill")}, ${counted(skills.counts.documents, "document")}: console skills <folder> shows what a folder holds (. for the root), console skills --search <words> finds skills and documents by words.`,
|
|
294
|
-
);
|
|
295
|
-
}
|
|
296
|
-
if (skills.items.length === 0) {
|
|
297
|
-
lines.push("No skills at this address.");
|
|
298
|
-
}
|
|
299
|
-
// The path, which console skill takes, and the URI beside it, which a host's own connection takes.
|
|
300
|
-
for (const skill of skills.items) {
|
|
301
|
-
lines.push(
|
|
302
|
-
`${skill.name} ${skill.description}`,
|
|
303
|
-
` ${skill.path}${skill.uri === null ? "" : ` ${skill.uri}`}`,
|
|
304
|
-
);
|
|
305
|
-
}
|
|
306
|
-
lines.push(
|
|
307
|
-
"",
|
|
308
|
-
`Read a skill whole with console skill <path>, its rules first, and a file of the pack with console skill --file <path>; or load it by its URI through your own SkillCDN connection to this address, which a person makes at ${skills.page}. --json adds the page of each, for a person.`,
|
|
309
|
-
...own,
|
|
310
|
-
);
|
|
311
|
-
return lines.join("\n");
|
|
312
|
-
}
|
|
313
|
-
|
|
314
302
|
/** One half of the rules: where it comes from, and the text as it stands. */
|
|
315
303
|
function formatRulesHalf(where: "the workspace" | "this project", rules: RestRules): string {
|
|
316
304
|
const page = rules.page;
|
|
317
305
|
const facts =
|
|
318
306
|
page === null
|
|
319
307
|
? undefined
|
|
320
|
-
: `v${page.version}, by ${writer(page.updatedBy, page.agent)}; ${page.updatedAt}`;
|
|
308
|
+
: `v${page.version}, by ${writer(page.updatedBy, page.agent)}; ${moment(page.updatedAt)}`;
|
|
321
309
|
if (rules.source === "none" || rules.body === null) {
|
|
322
310
|
return `Rules of ${where}: none of its own; the workspace's hold.`;
|
|
323
311
|
}
|
|
@@ -417,19 +405,25 @@ export function formatDecisionLine(decision: RestDecision): string {
|
|
|
417
405
|
return `${decision.id} ${state} ${decision.question} (asked by ${asker(decision)})`;
|
|
418
406
|
}
|
|
419
407
|
|
|
408
|
+
/** The options, one a line; the person's own answer, when they wrote one, is the last and says so. */
|
|
420
409
|
export function formatOptions(decision: RestDecision): string {
|
|
421
|
-
return decision.options
|
|
410
|
+
return decision.options
|
|
411
|
+
.map(
|
|
412
|
+
(option) =>
|
|
413
|
+
` ${option.id}) ${option.label}${option.own === true ? " (the person's own answer, not one given)" : ""}`,
|
|
414
|
+
)
|
|
415
|
+
.join("\n");
|
|
422
416
|
}
|
|
423
417
|
|
|
424
|
-
/** The answer, with who gave it; or that there is none yet. */
|
|
418
|
+
/** The answer, with who gave it, and that it is in their own words when it is; or that there is none yet. */
|
|
425
419
|
export function formatAnswer(decision: RestDecision): string {
|
|
426
420
|
const { answer } = decision;
|
|
427
421
|
if (answer === null) {
|
|
428
422
|
return "waits for a person.";
|
|
429
423
|
}
|
|
430
|
-
const
|
|
424
|
+
const chosen = decision.options.find((option) => option.id === answer.option);
|
|
431
425
|
const lines = [
|
|
432
|
-
`Answered: ${answer.option}) ${label ?? answer.option}, by ${answer.by.login} at ${answer.at}`,
|
|
426
|
+
`Answered: ${answer.option}) ${chosen?.label ?? answer.option}${chosen?.own === true ? ", in their own words" : ""}, by ${answer.by.login} at ${answer.at}`,
|
|
433
427
|
];
|
|
434
428
|
if (answer.note !== null && answer.note.length > 0) {
|
|
435
429
|
lines.push(`Note: ${answer.note}`);
|
|
@@ -462,7 +456,7 @@ export function formatDocumentLine(document: RestDocumentSummary): string {
|
|
|
462
456
|
if (document.archivedAt !== null) {
|
|
463
457
|
notes.push("archived");
|
|
464
458
|
}
|
|
465
|
-
return `${document.path} ${document.title} (${notes.join(", ")}; ${document.updatedAt})`;
|
|
459
|
+
return `${document.path} ${document.title} (${notes.join(", ")}; ${moment(document.updatedAt)})`;
|
|
466
460
|
}
|
|
467
461
|
|
|
468
462
|
/** A folder's folders and pages, or the pages a search found. */
|
|
@@ -484,7 +478,7 @@ export function formatDocument(
|
|
|
484
478
|
): string {
|
|
485
479
|
const lines = [
|
|
486
480
|
`${document.path}: ${document.title}`,
|
|
487
|
-
`version ${document.version}, by ${writer(document.updatedBy, document.agent)} at ${document.updatedAt}${document.archivedAt === null ? "" : `; archived ${document.archivedAt}`}`,
|
|
481
|
+
`version ${document.version}, by ${writer(document.updatedBy, document.agent)} at ${moment(document.updatedAt)}${document.archivedAt === null ? "" : `; archived ${moment(document.archivedAt)}`}`,
|
|
488
482
|
`id: ${document.id}`,
|
|
489
483
|
];
|
|
490
484
|
if (document.body.trim().length > 0) {
|
|
@@ -518,7 +512,7 @@ export function formatDocument(
|
|
|
518
512
|
}
|
|
519
513
|
|
|
520
514
|
export function formatVersionLine(version: RestVersionSummary): string {
|
|
521
|
-
return `v${version.number} ${version.title} (by ${writer(version.author, version.agent)}; ${version.createdAt})`;
|
|
515
|
+
return `v${version.number} ${version.title} (by ${writer(version.author, version.agent)}; ${moment(version.createdAt)})`;
|
|
522
516
|
}
|
|
523
517
|
|
|
524
518
|
/** One version of a document, with its body as it was. */
|
|
@@ -531,256 +525,3 @@ export function formatVersion(path: string, version: RestVersion): string {
|
|
|
531
525
|
}
|
|
532
526
|
|
|
533
527
|
/** The text of one path across the pages a skill came in: the fragments joined in order. */
|
|
534
|
-
function joinTexts(
|
|
535
|
-
pages: readonly RestSkillPage[],
|
|
536
|
-
pick: (page: RestSkillPage) => readonly { readonly path: string; readonly body: string }[],
|
|
537
|
-
): { readonly path: string; readonly body: string }[] {
|
|
538
|
-
const joined = new Map<string, string>();
|
|
539
|
-
for (const page of pages) {
|
|
540
|
-
for (const text of pick(page)) {
|
|
541
|
-
joined.set(text.path, (joined.get(text.path) ?? "") + text.body);
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
return [...joined.entries()].map(([path, body]) => ({ path, body }));
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
const NO_ADDRESS = "No skills address: neither this project nor the console names one.";
|
|
548
|
-
|
|
549
|
-
/**
|
|
550
|
-
* One skill, as load_skill lays it out for a model (ADR-0028): the rules from the repository's
|
|
551
|
-
* root to the nearest manifest, the body, the included files, each under a line that names its
|
|
552
|
-
* path; then the files the skill has and what it refers to. A skill described only is named and
|
|
553
|
-
* pointed at, and nothing of its body is shown.
|
|
554
|
-
*/
|
|
555
|
-
export function formatSkillLoad(loaded: RestSkillLoad, pages: readonly RestSkillPage[]): string {
|
|
556
|
-
if (loaded.status === "none" || loaded.address === null) {
|
|
557
|
-
return NO_ADDRESS;
|
|
558
|
-
}
|
|
559
|
-
const head = `skill at ${loaded.address}, served by ${loaded.source}: ${loaded.status}`;
|
|
560
|
-
if (loaded.status !== "ready") {
|
|
561
|
-
return [head, SKILLS_WORDS[loaded.status]].join("\n");
|
|
562
|
-
}
|
|
563
|
-
const [first] = pages;
|
|
564
|
-
if (first === undefined) {
|
|
565
|
-
return [head, "Nothing was read."].join("\n");
|
|
566
|
-
}
|
|
567
|
-
const last = pages[pages.length - 1] ?? first;
|
|
568
|
-
const lines = [
|
|
569
|
-
`${first.name} ${first.description}`,
|
|
570
|
-
`skill ${first.path} at ${loaded.address}, served by ${loaded.source}`,
|
|
571
|
-
];
|
|
572
|
-
if (first.serving.license !== null) {
|
|
573
|
-
lines.push(`license: ${first.serving.license}`);
|
|
574
|
-
}
|
|
575
|
-
if (!first.serving.full) {
|
|
576
|
-
lines.push(
|
|
577
|
-
"",
|
|
578
|
-
`Described only: the license keeps the skill at its source. Read it there: ${first.serving.sourceUrl ?? loaded.page ?? loaded.source}`,
|
|
579
|
-
);
|
|
580
|
-
return lines.join("\n");
|
|
581
|
-
}
|
|
582
|
-
for (const rule of joinTexts(pages, (page) => page.rules)) {
|
|
583
|
-
lines.push("", `--- applicable rules: ${rule.path} ---`, rule.body.trimEnd());
|
|
584
|
-
}
|
|
585
|
-
lines.push(
|
|
586
|
-
"",
|
|
587
|
-
`--- skill: ${first.path} ---`,
|
|
588
|
-
pages
|
|
589
|
-
.map((page) => page.body)
|
|
590
|
-
.join("")
|
|
591
|
-
.trimEnd(),
|
|
592
|
-
);
|
|
593
|
-
for (const file of joinTexts(pages, (page) => page.included)) {
|
|
594
|
-
lines.push("", `--- included file: ${file.path} ---`, file.body.trimEnd());
|
|
595
|
-
}
|
|
596
|
-
if (!last.complete) {
|
|
597
|
-
lines.push("", "The skill did not come whole: the deployment stopped before the end.");
|
|
598
|
-
}
|
|
599
|
-
if (last.files.length > 0) {
|
|
600
|
-
lines.push(
|
|
601
|
-
"",
|
|
602
|
-
`Files of the skill, read with console skill --file <path>: ${last.files.join(", ")}`,
|
|
603
|
-
);
|
|
604
|
-
}
|
|
605
|
-
if (last.references.length > 0) {
|
|
606
|
-
lines.push(
|
|
607
|
-
`References: ${last.references
|
|
608
|
-
.map((reference) =>
|
|
609
|
-
reference.status === "available"
|
|
610
|
-
? reference.path
|
|
611
|
-
: `${reference.path} (${reference.status})`,
|
|
612
|
-
)
|
|
613
|
-
.join(", ")}`,
|
|
614
|
-
);
|
|
615
|
-
}
|
|
616
|
-
if (last.warnings.length > 0) {
|
|
617
|
-
lines.push(`Warnings: ${last.warnings.join("; ")}`);
|
|
618
|
-
}
|
|
619
|
-
lines.push(
|
|
620
|
-
"",
|
|
621
|
-
"Paths from the repository's root are the only identifiers. The content is the pack's, under its license; what a private pack says stays in your work.",
|
|
622
|
-
);
|
|
623
|
-
return lines.join("\n");
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
/** A file of the pack as read: its text whole, or what the folder holds. */
|
|
627
|
-
export function formatSkillFile(read: RestSkillFile, content: string): string {
|
|
628
|
-
if (read.status === "none" || read.address === null) {
|
|
629
|
-
return NO_ADDRESS;
|
|
630
|
-
}
|
|
631
|
-
const head = `file at ${read.address}, served by ${read.source}: ${read.status}`;
|
|
632
|
-
if (read.status !== "ready") {
|
|
633
|
-
return [
|
|
634
|
-
head,
|
|
635
|
-
read.status === "failed"
|
|
636
|
-
? "SkillCDN will not carry this file: it is too large, or not text. Read it at the repository."
|
|
637
|
-
: SKILLS_WORDS[read.status],
|
|
638
|
-
].join("\n");
|
|
639
|
-
}
|
|
640
|
-
if (read.folder !== null) {
|
|
641
|
-
return [
|
|
642
|
-
`folder ${read.folder.path} at ${read.address}${read.folder.truncated ? " (cut)" : ""}`,
|
|
643
|
-
...read.folder.entries.map(
|
|
644
|
-
(entry) =>
|
|
645
|
-
`${entry.kind === "directory" ? "d" : "f"} ${entry.path}${entry.size === null ? "" : ` ${entry.size}`}`,
|
|
646
|
-
),
|
|
647
|
-
"",
|
|
648
|
-
"console skills <folder> shows a folder with its skills marked, what each folder holds and the README that introduces it.",
|
|
649
|
-
].join("\n");
|
|
650
|
-
}
|
|
651
|
-
if (read.file === null) {
|
|
652
|
-
return `${head}: nothing was read.`;
|
|
653
|
-
}
|
|
654
|
-
return [
|
|
655
|
-
`--- file: ${read.file.path} (${read.file.totalLength} characters) ---`,
|
|
656
|
-
content.trimEnd(),
|
|
657
|
-
].join("\n");
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
/** One entry of a browsed folder: what it is, its path, its words, and the facts of its kind. */
|
|
661
|
-
function formatBrowseEntry(entry: RestSkillBrowseEntry): string {
|
|
662
|
-
const facts: string[] = [];
|
|
663
|
-
if (entry.kind === "directory") {
|
|
664
|
-
if (entry.skillCount > 0) {
|
|
665
|
-
facts.push(counted(entry.skillCount, "skill"));
|
|
666
|
-
}
|
|
667
|
-
if (entry.documentCount > 0) {
|
|
668
|
-
facts.push(counted(entry.documentCount, "document"));
|
|
669
|
-
}
|
|
670
|
-
if (entry.overviewPath !== null) {
|
|
671
|
-
facts.push(`README ${entry.overviewPath}`);
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
if (entry.kind === "skill") {
|
|
675
|
-
if (entry.license !== null) {
|
|
676
|
-
facts.push(entry.license);
|
|
677
|
-
}
|
|
678
|
-
if (entry.describedOnly) {
|
|
679
|
-
facts.push("described only");
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
if (entry.kind === "file" && entry.size !== null) {
|
|
683
|
-
facts.push(`${entry.size} bytes`);
|
|
684
|
-
}
|
|
685
|
-
const words = [entry.name, entry.description]
|
|
686
|
-
.filter((text): text is string => text !== null && text !== "")
|
|
687
|
-
.join(" ");
|
|
688
|
-
const mark = entry.kind === "skill" ? "skill" : entry.kind === "directory" ? "d" : "f";
|
|
689
|
-
return `${mark} ${entry.path}${words === "" ? "" : ` ${words}`}${facts.length === 0 ? "" : ` (${facts.join("; ")})`}`;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
/**
|
|
693
|
-
* What a folder of the pack holds, as browse_repo lays it out: the README that introduces the
|
|
694
|
-
* folder, then its skills, its folders with what each holds, and its files, across the pages the
|
|
695
|
-
* listing came in.
|
|
696
|
-
*/
|
|
697
|
-
export function formatSkillBrowse(
|
|
698
|
-
browsed: RestSkillBrowse,
|
|
699
|
-
folders: readonly NonNullable<RestSkillBrowse["folder"]>[],
|
|
700
|
-
): string {
|
|
701
|
-
if (browsed.status === "none" || browsed.address === null) {
|
|
702
|
-
return NO_ADDRESS;
|
|
703
|
-
}
|
|
704
|
-
const head = `folder at ${browsed.address}, served by ${browsed.source}: ${browsed.status}`;
|
|
705
|
-
if (browsed.status !== "ready") {
|
|
706
|
-
return [head, SKILLS_WORDS[browsed.status]].join("\n");
|
|
707
|
-
}
|
|
708
|
-
const [first] = folders;
|
|
709
|
-
if (first === undefined) {
|
|
710
|
-
return [head, "Nothing was read."].join("\n");
|
|
711
|
-
}
|
|
712
|
-
const last = folders[folders.length - 1] ?? first;
|
|
713
|
-
const lines = [`folder ${first.path === "" ? "/" : first.path} at ${browsed.address}`];
|
|
714
|
-
if (first.overview !== null) {
|
|
715
|
-
const words = [first.overview.title, first.overview.description]
|
|
716
|
-
.filter((text): text is string => text !== null && text !== "")
|
|
717
|
-
.join(" ");
|
|
718
|
-
lines.push(` ${words === "" ? "" : `${words} `}(README: ${first.overview.path})`);
|
|
719
|
-
}
|
|
720
|
-
const entries = folders.flatMap((folder) => folder.entries);
|
|
721
|
-
if (entries.length === 0) {
|
|
722
|
-
lines.push("Nothing in this folder.");
|
|
723
|
-
}
|
|
724
|
-
for (const entry of entries) {
|
|
725
|
-
lines.push(formatBrowseEntry(entry));
|
|
726
|
-
}
|
|
727
|
-
if (last.nextCursor !== null) {
|
|
728
|
-
lines.push("More entries: the listing stopped before the end of the folder.");
|
|
729
|
-
}
|
|
730
|
-
lines.push(
|
|
731
|
-
"",
|
|
732
|
-
"Read a skill with console skill <path>; a file, or a README named above, with console skill --file <path>; a folder with console skills <folder>.",
|
|
733
|
-
);
|
|
734
|
-
return lines.join("\n");
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
/**
|
|
738
|
-
* What words found in the pack, as search_repo lays it out: skills with the files of theirs that
|
|
739
|
-
* matched, documents on their own, in relevance order; words, not meaning.
|
|
740
|
-
*/
|
|
741
|
-
export function formatSkillFind(found: RestSkillFind): string {
|
|
742
|
-
if (found.status === "none" || found.address === null) {
|
|
743
|
-
return NO_ADDRESS;
|
|
744
|
-
}
|
|
745
|
-
const head = `search at ${found.address}, served by ${found.source}: ${found.status}`;
|
|
746
|
-
if (found.status !== "ready") {
|
|
747
|
-
return [head, SKILLS_WORDS[found.status]].join("\n");
|
|
748
|
-
}
|
|
749
|
-
if (found.found === null) {
|
|
750
|
-
return [head, "Nothing was read."].join("\n");
|
|
751
|
-
}
|
|
752
|
-
const { query, path, items, nextCursor } = found.found;
|
|
753
|
-
const where = path === null || path === "" ? "" : ` within ${path}`;
|
|
754
|
-
const lines = [
|
|
755
|
-
`found for "${query}"${where} at ${found.address}: ${counted(items.length, "result")}`,
|
|
756
|
-
];
|
|
757
|
-
for (const item of items) {
|
|
758
|
-
if (item.kind === "skill") {
|
|
759
|
-
lines.push(
|
|
760
|
-
`skill ${item.path} ${item.name} ${item.description}${item.describedOnly ? " (described only)" : ""}`,
|
|
761
|
-
);
|
|
762
|
-
for (const file of item.files) {
|
|
763
|
-
lines.push(` file ${file.path}${file.title === null ? "" : ` ${file.title}`}`);
|
|
764
|
-
}
|
|
765
|
-
if (item.moreFiles > 0) {
|
|
766
|
-
lines.push(` and ${counted(item.moreFiles, "more file")}`);
|
|
767
|
-
}
|
|
768
|
-
} else {
|
|
769
|
-
lines.push(
|
|
770
|
-
`document ${item.path}${item.title === null ? "" : ` ${item.title}`}${item.summary === null ? "" : ` ${item.summary}`}`,
|
|
771
|
-
);
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
if (nextCursor !== null) {
|
|
775
|
-
lines.push(
|
|
776
|
-
"More matched: narrow the words, or search within a folder (console skills <folder> --search <words>).",
|
|
777
|
-
);
|
|
778
|
-
}
|
|
779
|
-
if (items.length > 0) {
|
|
780
|
-
lines.push(
|
|
781
|
-
"",
|
|
782
|
-
"Read a skill with console skill <path>, a file or a document with console skill --file <path>.",
|
|
783
|
-
);
|
|
784
|
-
}
|
|
785
|
-
return lines.join("\n");
|
|
786
|
-
}
|