@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/README.md
CHANGED
|
@@ -33,13 +33,14 @@ const { items } = await client.tasks({ state: "ready" });
|
|
|
33
33
|
- **The bounds** every input is held to (`MAX_TITLE_LENGTH`, `MAX_BODY_LENGTH`, `MAX_OPTIONS`, ...), so that a page can say so before a request is made.
|
|
34
34
|
- **The routes:** `REST_ROUTES` (the workspace's resources), `AUTH_ROUTES`, and the helpers that build a path (`projectPath(key, collection, id)` for everything under a project, `projectIconPath(key, icon)` for a project's image, `restPath`, `loginPath`, `signInPath`). The API itself is described in [docs/specs/rest.md](https://github.com/skillcdn/console/blob/main/docs/specs/rest.md).
|
|
35
35
|
- **The schemas,** in the mini build of zod, which runs in a browser: what the server answers (`restProjectSchema`, `restMemberSchema`, `restTaskSchema`, `restDecisionSchema`, `restEventSchema`, `restMeSchema`, ...) and what it is sent (`restProjectInputSchema`, `restTaskInputSchema`, `restTaskPatchSchema`, `restDecisionInputSchema`, `restAnswerInputSchema`, ...). Absent values are `null` on the wire, never missing keys. The server validates with the input schemas and is tested against the others.
|
|
36
|
-
- **The client:** `createClient({ baseUrl, fetch })` answers typed values parsed with those schemas, and throws an `ApiError` with the server's stable `code` (or `network`, `invalid_response`) for anything else. It takes a `fetch` and reads nothing else. With `token`, a token a person made on their Agents page, every request carries `Authorization: Bearer` and no cookie: how a script or a console of a person's own acts as that person. The board is a project's: `client.projects()` lists those the person may see, and `client.project(key)` is the client of one, with its tasks, decisions, goals, runs, files, events, skills, documents, settings, members and image (`setIcon`, `removeIcon`). `tasks(filter)` narrows the board (`TaskFilter`: a state, the open ones, a category's key, a goal, an assignee, a day tasks are due by) and pages it (`Page`: `limit` and `offset`; the answer carries `more`), as `decisions(filter)` and `runs(filter)` are paged; a task carries its `category`, `goalId`, its plan (`startsOn`, `dueOn`) and its moments (`startedAt`, `doneAt`). The roadmap is `goals()`, `goal(id)`, `createGoal` and `updateGoal`; the workspace's categories are `client.categories()`, `createCategory`, `updateCategory` and `removeCategory`; `client.work()` is what is on the person's plate across their projects ([ADR-0019](https://github.com/skillcdn/console/blob/main/docs/adr/0019-a-task-is-planned-in-days-the-board-keeps-its-moments-and-goals-and-categories-give-the-work-its-shape.md)). The documents go by path: `documents({ folder })` lists a folder, `documents({ q })` finds pages by their words, `document(path)` reads one with its links both ways and its files, `writeDocument(path, { title, body, baseVersion })` writes a version, `archiveDocument`, `restoreDocument`, `documentVersions`, `documentVersion(path, n)`, `attachFile(path, upload)` and `documentFileUrl(path, id)` do the rest; `updateDecision(id, { outcome })` grows a decision's record. `connect`, `connectRequest`, `approveConnection`, `denyConnection` and `claimConnection` are the two sides of an agent connecting with a code a person approves (ADR-0011), and `personTokens` and `revokePersonToken` an administrator's sight of everyone's agents. An `EventSource` cannot carry a header, so a stream read with a token is a `fetch` of `eventStreamUrl(after)`. `handInFile` sends a file as a form, and `fileUrl` says where its bytes are read back. `events(after, { task })` is everything that happened to one task.
|
|
36
|
+
- **The client:** `createClient({ baseUrl, fetch })` answers typed values parsed with those schemas, and throws an `ApiError` with the server's stable `code` (or `network`, `invalid_response`) for anything else. It takes a `fetch` and reads nothing else. With `token`, a token a person made on their Agents page, every request carries `Authorization: Bearer` and no cookie: how a script or a console of a person's own acts as that person, configuring included (`createProject`, `updatePerson`, a project's `update`, `setIcon`, `addMember`, `updateMember` and `removeMember`, `createCategory`, `updateCategory` and `removeCategory`), within the person's own roles; only the tokens themselves and the approval of a connection are a session's ([ADR-0021](https://github.com/skillcdn/console/blob/main/docs/adr/0021-a-token-configures-as-its-person-does.md)). The board is a project's: `client.projects()` lists those the person may see, and `client.project(key)` is the client of one, with its tasks, decisions, goals, runs, files, events, skills, documents, settings, members and image (`setIcon`, `removeIcon`). `tasks(filter)` narrows the board (`TaskFilter`: a state, the open ones, a category's key, a goal, an assignee, a day tasks are due by) and pages it (`Page`: `limit` and `offset`; the answer carries `more`), as `decisions(filter)` and `runs(filter)` are paged; a task carries its `category`, `goalId`, its plan (`startsOn`, `dueOn`) and its moments (`startedAt`, `doneAt`). The roadmap is `goals()`, `goal(id)`, `createGoal` and `updateGoal`; the workspace's categories are `client.categories()`, `createCategory`, `updateCategory` and `removeCategory`; `client.work()` is what is on the person's plate across their projects ([ADR-0019](https://github.com/skillcdn/console/blob/main/docs/adr/0019-a-task-is-planned-in-days-the-board-keeps-its-moments-and-goals-and-categories-give-the-work-its-shape.md)). The documents go by path: `documents({ folder })` lists a folder, `documents({ q })` finds pages by their words, `document(path)` reads one with its links both ways and its files, `writeDocument(path, { title, body, baseVersion })` writes a version, `archiveDocument`, `restoreDocument`, `documentVersions`, `documentVersion(path, n)`, `attachFile(path, upload)` and `documentFileUrl(path, id)` do the rest; `updateDecision(id, { outcome })` grows a decision's record. `connect`, `connectRequest`, `approveConnection`, `denyConnection` and `claimConnection` are the two sides of an agent connecting with a code a person approves (ADR-0011), and `personTokens` and `revokePersonToken` an administrator's sight of everyone's agents. An `EventSource` cannot carry a header, so a stream read with a token is a `fetch` of `eventStreamUrl(after)`. `handInFile` sends a file as a form, and `fileUrl` says where its bytes are read back. `events(after, { task })` is everything that happened to one task.
|
|
37
37
|
|
|
38
38
|
### The command line
|
|
39
39
|
|
|
40
40
|
```sh
|
|
41
41
|
npm install -g @skillcdn/console # where the agent runs
|
|
42
42
|
console login --url https://console.example # shows an address and a code; a person approves, and the token arrives here
|
|
43
|
+
console project new web "The web app" # a project of your own, as on the pages; member, category and person role do the rest
|
|
43
44
|
console use web # the project this directory works in; .skillcdn-console.json, committed
|
|
44
45
|
console take 7 # a run begins on task #7 of the project, for the person the token is
|
|
45
46
|
console report "Found the cause: the parser trusts its input."
|
|
@@ -52,8 +53,9 @@ console finish --summary "Done: the parser refuses empty input."
|
|
|
52
53
|
console serve dist # a console of your own, built from this package, served for you
|
|
53
54
|
```
|
|
54
55
|
|
|
55
|
-
- **`console`** is the agent's side of the console ([docs/specs/cli.md](https://github.com/skillcdn/console/blob/main/docs/specs/cli.md)): a thin client of the REST API with no logic of its own, no dependencies beyond the package's, and no cost to an agent until it is used. `console help` says everything an agent needs and `console --version` which version this is; `--json` answers with the console's own JSON; the exit code says whether the console did it (`0`), refused or could not be reached (`1`), did not understand (`2`), or a decision still waits (`3`).
|
|
56
|
+
- **`console`** is the agent's side of the console ([docs/specs/cli.md](https://github.com/skillcdn/console/blob/main/docs/specs/cli.md)): a thin client of the REST API with no logic of its own, no dependencies beyond the package's, and no cost to an agent until it is used. The package states Node.js 24 as its floor and is tested on it; 22 runs the command today, checked by hand on each release. `console help` says everything an agent needs and `console --version` which version this is; `--json` answers with the console's own JSON; the exit code says whether the console did it (`0`), refused or could not be reached (`1`), did not understand (`2`), or a decision still waits (`3`).
|
|
56
57
|
- **A person signs it in once:** `login` checks the token against the console and keeps it, with the console's address, in `$XDG_CONFIG_HOME/skillcdn-console/credentials.json` (`~/.config` when unset). `CONSOLE_URL` and `CONSOLE_TOKEN` in the environment win over the file, for a hook or a job. A token is never taken on the command line.
|
|
58
|
+
- **Configuring, as the person** ([ADR-0021](https://github.com/skillcdn/console/blob/main/docs/adr/0021-a-token-configures-as-its-person-does.md)): `people`, `person role`, `project new`, `project edit`, `project icon`, `members`, `member add`, `member role`, `member remove`, `category new`, `category edit` and `category remove` do what an administrator or an owner does on the pages, with the same refusals (`auth.forbidden` where the person may not), and the event names the agent beside the person. The tokens themselves have no command: they are made and removed on the Agents page, signed in.
|
|
57
59
|
- **The project's documents** are the agent's to read and write: `docs`, `doc`, `write`, `attach`, `archive` and `restore`, by path; a link in a body to a page's path refers to the page.
|
|
58
60
|
- **Every command on the board works in one project:** `--project <key>`, else `CONSOLE_PROJECT`, else what `console use <key>` wrote to `.skillcdn-console.json` in the working directory or one above it, a file meant to be committed. `console projects` lists the projects the person may work in.
|
|
59
61
|
- **`console serve [<dir>] [--port <n>]`** is for a person: it serves [a console of your own](#a-console-of-your-own) from your machine, the build in `<dir>` at `http://127.0.0.1:11197/`, and carries everything under `/api/` to the console the command is signed in to, with your token, which the pages never see ([ADR-0014](https://github.com/skillcdn/console/blob/main/docs/adr/0014-a-persons-own-console-is-served-from-their-machine-by-the-command.md)). The loopback only; a request from another site's page is refused. Without a directory only the API is served, for a development server.
|
|
@@ -71,10 +73,11 @@ createConsole({
|
|
|
71
73
|
}).mount(document.getElementById("root"));
|
|
72
74
|
```
|
|
73
75
|
|
|
74
|
-
- **The components** take their data as props and nothing from the network: `Board` (one column per state, the sooner due first), `TaskCard`, `BoardFilters` with `filterTasks` and `byDue` (the board narrowed by category, goal, person and one's own), `TaskList` (tasks as rows, a page at a time), `TaskView` (one task with its plan, its moments, its decisions and its parts), `TaskForm` (with the `categories` and the `goals` to choose from), `Calendar` and `Workload` with `workloadOf` (a month with each task across its planned days, a bar in a lane of its own, or on its due day, with a legend of the colours of the `categories` given, and who has what; `weekSegments` lays the bars; the composition narrows the page with `BoardFilters` as it does the board, the address carrying the filter with the month), `GoalList`, `GoalView` and `GoalForm` (the roadmap, one goal with its tasks by state, and the way to set one), `WorkList` (a person's own work across projects), `CategoryList` (the categories, with the ways an administrator keeps them), `DecisionList` and `DecisionCard` (the waiting ones as cards, each with the way to answer; the answered ones as the record, `DecisionLog`, a row each, a page at a time), `DecisionForm`, `EventFeed` (a sentence per event, newest first, a page at a time; `describeEvent` makes the sentence), `Shell` (the header with the workspace, the project, the tabs of the pages and the person's menu), `SignIn` (one button per provider the deployment offers), `TokenList`, `TokenForm` and `NewToken` (the agents a person connected, as the tokens they hold, the way to make one by hand, and the one just made with its secret shown once), `ConnectCodeForm`, `ConnectApproval` and `ConnectWords` (an agent connecting with a code: where the code is typed, what asks under it with the way to approve or refuse it, and what to tell the agent; `ConnectPage` composes the first two for a code), `PeopleList` (everyone, with what each is, and the way for an administrator to change it), `ProjectMark` (a project's mark: its own image when it has one, else its symbol on its colour, or the ones chosen from its key and its name; `sm`, `md` or `lg`), `ProjectList`, `ProjectForm` and `MemberList` (the projects a person may see with what they are in each, the way to make or change one, its mark included, with an image chosen for it (`onSubmit` gets the file, or `null` to take the image away), and those listed in a project with the way for an owner to add, change and remove them; `ProjectRoleBadge`, `PROJECT_ROLE_LABELS`, `VISIBILITY_LABELS` and `keyOf` with them), `SkillList` (a project's skills as SkillCDN serves them at its address, each with where a person reads it and what an agent loads it by), `FolderView`, `DocumentList`, `DocumentSearch`, `DocumentView`, `DocumentForm`, `DocumentFiles` and `DocumentCrumbs` (a project's documents: a folder's folders and pages, the way to find pages by their words, one page with its versions, what it links to, what refers to it and its files, and the forms to write and attach; `DocsPage` composes them for a path), `RunList` and `RunCard` (an agent's run on a task: what it reported, what it handed in, links and files alike, what it waits for, and when an open one was last heard from; `RunStatusBadge` and `RUN_STATUS_LABELS` with them, and `fileHref` for where a file is read), and `Markdown`, which renders what people and agents wrote as elements, never as HTML, with links to the web only and pictures over https only, and, given `docHref`, a link to a page's path as a link to the page. `DecisionCard` reads as a record, with the context, the rationale and what followed, which `onUpdate` lets a person write. The small blocks (`Button`, `Badge`, `Avatar`, `PersonChip
|
|
76
|
+
- **The components** take their data as props and nothing from the network: `Board` (one column per state, the sooner due first), `TaskCard`, `BoardFilters` with `filterTasks` and `byDue` (the board narrowed by category, goal, person and one's own), `TaskList` (tasks as rows, a page at a time), `TaskView` (one task with its plan, its moments, its decisions and its parts), `TaskForm` (with the `categories` and the `goals` to choose from), `Calendar` and `Workload` with `workloadOf` (a month with each task across its planned days, a bar in a lane of its own, or on its due day, with a legend of the colours of the `categories` given, and who has what; `weekSegments` lays the bars; the composition narrows the page with `BoardFilters` as it does the board, the address carrying the filter with the month), `GoalList`, `GoalView` and `GoalForm` (the roadmap, one goal with its tasks by state, and the way to set one), `WorkList` (a person's own work across projects), `CategoryList` (the categories, with the ways an administrator keeps them), `DecisionList` and `DecisionCard` (the waiting ones as cards, each with the way to answer; the answered ones as the record, `DecisionLog`, a row each, a page at a time), `DecisionForm`, `EventFeed` (a sentence per event, newest first, a page at a time; `describeEvent` makes the sentence), `Shell` (the header with the workspace, the project, the tabs of the pages and the person's menu), `SignIn` (one button per provider the deployment offers), `TokenList`, `TokenForm` and `NewToken` (the agents a person connected, as the tokens they hold, the way to make one by hand, and the one just made with its secret shown once), `ConnectCodeForm`, `ConnectApproval` and `ConnectWords` (an agent connecting with a code: where the code is typed, what asks under it with the way to approve or refuse it, and what to tell the agent; `ConnectPage` composes the first two for a code), `PeopleList` (everyone, with what each is, and the way for an administrator to change it), `ProjectMark` (a project's mark: its own image when it has one, else its symbol on its colour, or the ones chosen from its key and its name; `sm`, `md` or `lg`), `ProjectList`, `ProjectForm` and `MemberList` (the projects a person may see with what they are in each, the way to make or change one, its mark included, with an image chosen for it (`onSubmit` gets the file, or `null` to take the image away), and those listed in a project with the way for an owner to add, change and remove them; `ProjectRoleBadge`, `PROJECT_ROLE_LABELS`, `VISIBILITY_LABELS` and `keyOf` with them), `SkillList` (a project's skills as SkillCDN serves them at its address, each with where a person reads it and what an agent loads it by), `FolderView`, `DocumentList`, `DocumentSearch`, `DocumentView`, `DocumentForm`, `DocumentFiles` and `DocumentCrumbs` (a project's documents: a folder's folders and pages, the way to find pages by their words, one page with its versions, what it links to, what refers to it and its files, and the forms to write and attach; `DocsPage` composes them for a path), `RunList` and `RunCard` (an agent's run on a task: what it reported, what it handed in, links and files alike, what it waits for, and when an open one was last heard from; `RunStatusBadge` and `RUN_STATUS_LABELS` with them, and `fileHref` for where a file is read), and `Markdown`, which renders what people and agents wrote as elements, never as HTML, with links to the web only and pictures over https only, and, given `docHref`, a link to a page's path as a link to the page. `DecisionCard` reads as a record, with the context, the rationale and what followed, which `onUpdate` lets a person write. The small blocks (`Button`, `Badge`, `Avatar`, `PersonChip` with `personName` (a person by the name the host gives, the login where there is none), `Callout`, `EmptyState`, `Spinner`, `Time`, `Day`, `CategoryBadge`, `DueBadge`, `GoalStateBadge`, `ProgressBar`, and `ShowMore` with `usePaged` and `PAGE_SIZE`, a long list a page at a time), the words of the vocabulary (`STATE_LABELS`, `PRIORITY_LABELS`, `ROLE_LABELS`, with `RoleBadge`) and the day helpers (`today`, `addDays`, `daysBetween`, `monthGrid`, `formatDay`, `formatMonth` and the rest, days being the strings the API spells them as) are exported too.
|
|
75
77
|
- **The composition:** `createConsole(config)` returns `{ App, mount }`. `App` is the whole console as one component; `mount(container)` renders it and returns the way to take it down. `config.baseUrl` is the origin of the API (the page's own when left out), `config.components` the pieces to replace (`ConsoleComponents` names them with the props each takes), `config.languages` the packs the pages speak, `config.brand` the addresses of a symbol and a wordmark to show beside the name (ADR-0013; the default console passes those of `@skillcdn/brand`, the main repository's package of its marks), `config.client` a client of your own, for tests. The SkillCDN name and marks are trademarks of KDX Labs under the main repository's trademark policy, not under this package's MIT license; a custom console brings its own marks through the same slot. A console that holds a token rather than a session (one of your own, served by `console serve`) is told so by the API (`me.agent`, the token's name) and offers nothing a token cannot do: no agents page, no new project, no settings, no changes to members or roles, no sign-out; it keeps the choice of language in the browser. The pages route in the browser and every form has an address (ADR-0010): the projects at `/`, the person's own work at `/work`, the categories at `/categories`, a project's pages under `/projects/<key>` (its roadmap and calendar among them), a task by its number, a goal and a decision by their ids, a document by its path, and the query for how a page is shown, the board's filters and the calendar's month among it (`matchRoute(pathname, search)`, `PATHS`, `projectHref`, `boardHref`, `taskHref`, `goalHref`, `calendarHref`, `decisionHref`, `docHref`, `newTaskHref`, `newGoalHref`, `newDecisionHref`, and `movedFrom` for the addresses of before); every link to one of them is followed in place, with one history entry per step; the workspace is loaded once and the project the page is on whole, kept current by the project's stream (`useConsoleData(client, projectKey)`, for a composition of your own).
|
|
78
|
+
- **Pages of your own:** `config.pages` adds tabs beside the console's. Each is a `CustomPage`: a `key` (what the address says: `/pages/<key>` on the workspace, `/projects/<project>/pages/<key>` on a project), a `label` (one word, or a word by language tag with `en` standing in), a `level` (`workspace`, or `project` for a tab on every project), `wide` for a page that takes the whole window, and `render(context)`, which gets what the default pages get: the `data` of `useConsoleData` with its actions, the `project` the page is on, the `client`, the `navigation`, the `words` and the `language`. `customPageHref` names the address and `customPageLabel` the tab's word. A page that is not among the pages given shows nothing there.
|
|
76
79
|
- **The languages:** every word the pages show is in a pack (`Messages`, keyed as `en` is), English and Korean ship (`ENGLISH`, `KOREAN`, `DEFAULT_LANGUAGES`), and a component takes its words from the language above it (`useWords`, `useLanguage`, `LanguageContext`), English where there is none. The person's choice is kept on them (`client.updateMe({ language })`) and the browser's languages decide before one (`chooseLanguage`, `packFor`). `createConsole({ languages })` replaces the packs the default console speaks, so that a custom console adds a language of its own; a pack is complete by type.
|
|
77
|
-
- **The styles** are one file, `@skillcdn/console/console.css`: the tokens (`--sc-*`), a small reset, and one class per block, all prefixed `sc-`. The look is SkillCDN's own: dark only, one accent, the page a dark field with a light in it (the shell, `sc-shell`, paints it) and the surfaces panes of glass over it (`--sc-glass-*`). Re-skinning starts and mostly ends with the tokens; a custom console that wants a flat page sets the field's colours to one and the glass to none. The faces are the page's to provide: the stylesheet names Pretendard first for the Korean and falls through to the system's faces, and the default console loads it; a custom console loads what it likes.
|
|
80
|
+
- **The styles** are one file, `@skillcdn/console/console.css`: the tokens (`--sc-*`), a small reset, and one class per block, all prefixed `sc-`. The look is SkillCDN's own: dark only, one accent, the page a dark field with a light in it (the shell, `sc-shell`, paints it) and the surfaces panes of glass over it (`--sc-glass-*`). Re-skinning starts and mostly ends with the tokens; a custom console that wants a flat page sets the field's colours to one and the glass to none. A stylesheet of your own, loaded after this one, sets them on `:root`: the surfaces (`--sc-color-bg`, `--sc-color-bg-subtle`, `--sc-color-bg-inset`, `--sc-color-bg-raised`, the borders), the text (`--sc-color-text`, `--sc-color-text-muted`, `--sc-color-text-faint`), the accent and the point (`--sc-color-accent*`, `--sc-color-point*`), the tones (`--sc-color-success`, `--sc-color-warning`, `--sc-color-danger`, each with a `-subtle`), the field and its light (`--sc-color-sky`, `--sc-color-far*`, `--sc-color-light-*`, `--sc-color-vignette`), the glass (`--sc-color-glass*`, `--sc-color-sheet*`, `--sc-color-field`), the fonts (`--sc-font-sans`, `--sc-font-mono`) and the radii (`--sc-radius-*`); the categories' colours are `--sc-category-line` and `--sc-category-fill` under `.sc-category-<name>`. Beyond the tokens, every block has its class (`sc-panel`, `sc-card`, `sc-column`, `sc-header`, ...), which a stylesheet of your own may style; the classes are part of the package's surface, and a change to one is in the changelog. The faces are the page's to provide: the stylesheet names Pretendard first for the Korean and falls through to the system's faces, and the default console loads it; a custom console loads what it likes.
|
|
78
81
|
- React 19 is a peer dependency; `react-markdown` and `remark-gfm` are the package's own.
|
|
79
82
|
|
|
80
83
|
A custom console is a small repository that depends on this package, holds a configuration and a CI job, and builds to static files: the organization's, served by the image from `WEB_ROOT` in place of the default UI, or your own, served by `console serve` on your machine. Nothing of the server is in the package but the serving of a build, `@skillcdn/console/web`, which the image and the command share.
|
|
@@ -118,7 +121,7 @@ npx vite # your pages, reloading as you
|
|
|
118
121
|
npx vite build && console serve dist # the build served whole, at http://127.0.0.1:11197/
|
|
119
122
|
```
|
|
120
123
|
|
|
121
|
-
The pages run on your token: what you may do on the board, they may do, and no more. The token never reaches the browser; the command adds it on the way, on the loopback only, and refuses a request from another site's page. The tokens themselves
|
|
124
|
+
The pages run on your token: what you may do on the board, they may do, and no more. The token never reaches the browser; the command adds it on the way, on the loopback only, and refuses a request from another site's page. The tokens themselves are made and removed on the organization's console, signed in, and the pages say so there; everything else, configuring included, the pages do on your token as the organization's console does on your session. Replace a component, pass your own language pack or your own marks, or compose pages of your own from the components and `useConsoleData`: the sections above say what each piece takes.
|
|
122
125
|
|
|
123
126
|
### The skills, for an agent
|
|
124
127
|
|
package/dist/cli/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/cli/cli.ts"],"names":[],"mappings":"AAEA,OAAO,EAIL,KAAK,SAAS,EAEf,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/cli/cli.ts"],"names":[],"mappings":"AAEA,OAAO,EAIL,KAAK,SAAS,EAEf,MAAM,cAAc,CAAC;AAqBtB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AA0BrD,OAAO,EAA0C,KAAK,MAAM,EAAE,MAAM,YAAY,CAAC;AAQjF,qFAAqF;AACrF,eAAO,MAAM,IAAI;aACf,EAAE,EAAE,CAAC;IACL,6EAA6E;aAC7E,MAAM,EAAE,CAAC;IACT,sCAAsC;aACtC,KAAK,EAAE,CAAC;IACR,6CAA6C;aAC7C,OAAO,EAAE,CAAC;CACF,CAAC;AAWX,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IAC3D,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,iFAAiF;IACjF,QAAQ,CAAC,SAAS,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1C,yFAAyF;IACzF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,2CAA2C;IAC3C,QAAQ,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,kCAAkC;IAClC,QAAQ,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;IAC1D,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;IAChC,+EAA+E;IAC/E,QAAQ,CAAC,SAAS,EAAE,cAAc,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,QAAQ,CAAC,GAAG,EAAE,MAAM,MAAM,CAAC;IAC3B,uFAAuF;IACvF,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,0FAA0F;IAC1F,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACtC;AAinDD,yFAAyF;AACzF,wBAAsB,MAAM,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,EAAE,EAAE,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CA6BhF"}
|
package/dist/cli/cli.js
CHANGED
|
@@ -3,9 +3,9 @@ import { parseArgs } from "node:util";
|
|
|
3
3
|
import { ApiError, createClient, } from "../client.js";
|
|
4
4
|
import { addDays, dayOf as dayOfDate, isDay } from "../dates.js";
|
|
5
5
|
import { DEFAULT_PAGE_LIMIT, LIST_LIMIT, MAX_OPTIONS, MIN_OPTIONS } from "../limits.js";
|
|
6
|
-
import { GOAL_STATES, TASK_PRIORITIES, TASK_STATES, } from "../vocabulary.js";
|
|
6
|
+
import { COLOR_NAMES, GOAL_STATES, PERSON_ROLES, PROJECT_ROLES, PROJECT_VISIBILITIES, TASK_PRIORITIES, TASK_STATES, } from "../vocabulary.js";
|
|
7
7
|
import { loadWebRoot, WebRootError } from "../web.js";
|
|
8
|
-
import { formatAnswer, formatCategoryLine, formatDecision, formatDecisionLine, formatDocument, formatDocuments, formatGoal, formatGoalLine, formatMore, formatOptions, formatProjectLine, formatRun, formatRunLine, formatSkills, formatTask, formatTaskLine, formatVersion, formatVersionLine, formatWork, } from "./format.js";
|
|
8
|
+
import { formatAnswer, formatCategoryLine, formatDecision, formatDecisionLine, formatDocument, formatDocuments, formatGoal, formatGoalLine, formatMemberLine, formatMore, formatOptions, formatPersonLine, formatProject, formatProjectLine, formatRun, formatRunLine, formatSkills, formatTask, formatTaskLine, formatVersion, formatVersionLine, formatWork, } from "./format.js";
|
|
9
9
|
import { COMMAND_HELP, helpFor, usage } from "./help.js";
|
|
10
10
|
import { createServeHandler, DEFAULT_SERVE_PORT } from "./serve.js";
|
|
11
11
|
// The `console` command (docs/specs/cli.md): the agent's side of the console. A thin client of
|
|
@@ -212,13 +212,31 @@ async function personIdOf(session, value) {
|
|
|
212
212
|
}
|
|
213
213
|
return me.person.id;
|
|
214
214
|
}
|
|
215
|
+
return (await personOf(session, value)).id;
|
|
216
|
+
}
|
|
217
|
+
/** The person a login names, among the workspace's; the logins are as people sign in. */
|
|
218
|
+
async function personOf(session, login) {
|
|
215
219
|
const { items } = await session.client.people();
|
|
216
|
-
const found = items.find((person) => person.login.toLowerCase() ===
|
|
220
|
+
const found = items.find((person) => person.login.toLowerCase() === login.toLowerCase());
|
|
217
221
|
if (found === undefined) {
|
|
218
|
-
throw misuse(`Nobody of the workspace is called ${
|
|
222
|
+
throw misuse(`Nobody of the workspace is called ${login}. console people lists everyone, by login.`);
|
|
219
223
|
}
|
|
220
|
-
return found
|
|
224
|
+
return found;
|
|
221
225
|
}
|
|
226
|
+
/** One word of a vocabulary, or the refusal to understand. */
|
|
227
|
+
function wordOf(value, words, what) {
|
|
228
|
+
const found = words.find((word) => word === value);
|
|
229
|
+
if (found === undefined) {
|
|
230
|
+
throw misuse(`Not ${what}: ${value}. One of ${words.join(", ")}`);
|
|
231
|
+
}
|
|
232
|
+
return found;
|
|
233
|
+
}
|
|
234
|
+
const personRoleOf = (value) => wordOf(value, PERSON_ROLES, "a role in the workspace");
|
|
235
|
+
const projectRoleOf = (value) => wordOf(value, PROJECT_ROLES, "a role in a project");
|
|
236
|
+
const visibilityOf = (value) => wordOf(value, PROJECT_VISIBILITIES, "a visibility");
|
|
237
|
+
const colorOf = (value) => wordOf(value, COLOR_NAMES, "a colour");
|
|
238
|
+
/** A colour, or `none` for the one the pages choose, which is `null` on the wire. */
|
|
239
|
+
const colorOrNoneOf = (value) => value === undefined ? undefined : value === "none" ? null : colorOf(value);
|
|
222
240
|
function secondsOf(value, fallback) {
|
|
223
241
|
if (value === undefined) {
|
|
224
242
|
return fallback;
|
|
@@ -255,6 +273,14 @@ function theOne(positionals, ask) {
|
|
|
255
273
|
}
|
|
256
274
|
return first;
|
|
257
275
|
}
|
|
276
|
+
/** Exactly two words, for a command on a thing and a value. */
|
|
277
|
+
function theTwo(positionals, ask) {
|
|
278
|
+
const [first, second] = positionals;
|
|
279
|
+
if (first === undefined || second === undefined || positionals.length > 2) {
|
|
280
|
+
throw misuse(ask);
|
|
281
|
+
}
|
|
282
|
+
return [first, second];
|
|
283
|
+
}
|
|
258
284
|
/**
|
|
259
285
|
* The decision once answered, or as it stands when the wait is over. Each request asks the
|
|
260
286
|
* console to hold it for a while, so the answer arrives as soon as a person gives it.
|
|
@@ -366,7 +392,7 @@ const projects = async (args, io) => {
|
|
|
366
392
|
const session = await open(io, values);
|
|
367
393
|
const found = await session.client.projects();
|
|
368
394
|
answer(io, session, found, () => found.items.length === 0
|
|
369
|
-
?
|
|
395
|
+
? 'No projects you may work in. Make one: console project new <key> "<name>"'
|
|
370
396
|
: found.items.map(formatProjectLine).join("\n"));
|
|
371
397
|
return EXIT.ok;
|
|
372
398
|
};
|
|
@@ -1032,6 +1058,208 @@ const serve = async (args, io) => {
|
|
|
1032
1058
|
await listening.closed;
|
|
1033
1059
|
return EXIT.ok;
|
|
1034
1060
|
};
|
|
1061
|
+
// Configuring, as the person (ADR-0021): what an administrator or an owner does on the pages,
|
|
1062
|
+
// their agent does here, attributed to them as the agent. The console decides on every request
|
|
1063
|
+
// with the same roles; what the person may not, the agent may not (auth.forbidden). The tokens
|
|
1064
|
+
// themselves have no command, by design.
|
|
1065
|
+
const people = async (args, io) => {
|
|
1066
|
+
const { values } = parse(args, {}, false);
|
|
1067
|
+
const session = await open(io, values);
|
|
1068
|
+
const found = await session.client.people();
|
|
1069
|
+
answer(io, session, found, () => found.items.map(formatPersonLine).join("\n"));
|
|
1070
|
+
return EXIT.ok;
|
|
1071
|
+
};
|
|
1072
|
+
const person = async (args, io) => {
|
|
1073
|
+
if (args[0] !== "role") {
|
|
1074
|
+
throw misuse("Say what to do: console person role <login> <admin|member>");
|
|
1075
|
+
}
|
|
1076
|
+
const { values, positionals } = parse(args.slice(1), {});
|
|
1077
|
+
const [login, role] = theTwo(positionals, "Say whom and what: console person role <login> <admin|member>");
|
|
1078
|
+
const wanted = personRoleOf(role);
|
|
1079
|
+
const session = await open(io, values);
|
|
1080
|
+
const found = await personOf(session, login);
|
|
1081
|
+
const changed = await session.client.updatePerson(found.id, { role: wanted });
|
|
1082
|
+
answer(io, session, changed, () => formatPersonLine(changed));
|
|
1083
|
+
return EXIT.ok;
|
|
1084
|
+
};
|
|
1085
|
+
/** The flags that set what a project is, shared by making one and changing one. */
|
|
1086
|
+
const PROJECT_FIELDS = {
|
|
1087
|
+
description: { type: "string" },
|
|
1088
|
+
file: { type: "string" },
|
|
1089
|
+
visibility: { type: "string" },
|
|
1090
|
+
skills: { type: "string" },
|
|
1091
|
+
color: { type: "string" },
|
|
1092
|
+
symbol: { type: "string" },
|
|
1093
|
+
};
|
|
1094
|
+
/** What the flags say of a project; `none` clears what may be cleared. */
|
|
1095
|
+
async function projectFieldsOf(io, values) {
|
|
1096
|
+
const description = await bodyOf(io, values, text(values, "description"));
|
|
1097
|
+
const visibility = text(values, "visibility");
|
|
1098
|
+
const skills = orNone(text(values, "skills"));
|
|
1099
|
+
const color = colorOrNoneOf(text(values, "color"));
|
|
1100
|
+
const symbol = orNone(text(values, "symbol"));
|
|
1101
|
+
return {
|
|
1102
|
+
...(description === undefined
|
|
1103
|
+
? {}
|
|
1104
|
+
: { description: description === "none" ? "" : description }),
|
|
1105
|
+
...(visibility === undefined ? {} : { visibility: visibilityOf(visibility) }),
|
|
1106
|
+
...(skills === undefined ? {} : { skillsAddress: skills }),
|
|
1107
|
+
...(color === undefined ? {} : { color }),
|
|
1108
|
+
...(symbol === undefined ? {} : { symbol }),
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
const newProject = async (args, io) => {
|
|
1112
|
+
const { values, positionals } = parse(args, PROJECT_FIELDS);
|
|
1113
|
+
const [key, name] = theTwo(positionals, 'Say the key and the name: console project new <key> "<name>" [--visibility workspace|private]');
|
|
1114
|
+
const fields = await projectFieldsOf(io, values);
|
|
1115
|
+
const session = await open(io, values);
|
|
1116
|
+
const made = await session.client.createProject({ key, name, ...fields });
|
|
1117
|
+
answer(io, session, made, () => `Project made: ${made.name} (${made.key}), with you as its owner.\nTo work in it from this directory: console use ${made.key}`);
|
|
1118
|
+
return EXIT.ok;
|
|
1119
|
+
};
|
|
1120
|
+
const editProject = async (args, io) => {
|
|
1121
|
+
const { values, positionals } = parse(args, { ...PROJECT_FIELDS, name: { type: "string" } });
|
|
1122
|
+
const key = theOne(positionals, "Say which project: console project edit <key> --name <name> ...");
|
|
1123
|
+
const name = text(values, "name");
|
|
1124
|
+
const patch = { ...(name === undefined ? {} : { name }), ...(await projectFieldsOf(io, values)) };
|
|
1125
|
+
if (Object.keys(patch).length === 0) {
|
|
1126
|
+
throw misuse("Say what to change: --name, --description, --visibility, --skills, --color or --symbol; none clears the description, the skills address, the colour or the symbol.");
|
|
1127
|
+
}
|
|
1128
|
+
const session = await open(io, values);
|
|
1129
|
+
const changed = await session.client.project(key).update(patch);
|
|
1130
|
+
answer(io, session, changed, () => formatProject(changed));
|
|
1131
|
+
return EXIT.ok;
|
|
1132
|
+
};
|
|
1133
|
+
const projectIcon = async (args, io) => {
|
|
1134
|
+
const { values, positionals } = parse(args, { remove: { type: "boolean" } });
|
|
1135
|
+
const [key, file] = positionals;
|
|
1136
|
+
const removing = on(values, "remove");
|
|
1137
|
+
if (key === undefined || positionals.length > 2 || (file === undefined) !== removing) {
|
|
1138
|
+
throw misuse("Say which project and which image: console project icon <key> <file>, or console project icon <key> --remove");
|
|
1139
|
+
}
|
|
1140
|
+
const session = await open(io, values);
|
|
1141
|
+
const project = session.client.project(key);
|
|
1142
|
+
const changed = file === undefined
|
|
1143
|
+
? await project.removeIcon()
|
|
1144
|
+
: await project.setIcon({
|
|
1145
|
+
name: basename(file),
|
|
1146
|
+
bytes: await io.readBytes(file),
|
|
1147
|
+
contentType: CONTENT_TYPES[extname(file).toLowerCase()] ?? "application/octet-stream",
|
|
1148
|
+
});
|
|
1149
|
+
answer(io, session, changed, () => formatProject(changed));
|
|
1150
|
+
return EXIT.ok;
|
|
1151
|
+
};
|
|
1152
|
+
const project = async (args, io) => {
|
|
1153
|
+
if (args[0] === "new") {
|
|
1154
|
+
return newProject(args.slice(1), io);
|
|
1155
|
+
}
|
|
1156
|
+
if (args[0] === "edit") {
|
|
1157
|
+
return editProject(args.slice(1), io);
|
|
1158
|
+
}
|
|
1159
|
+
if (args[0] === "icon") {
|
|
1160
|
+
return projectIcon(args.slice(1), io);
|
|
1161
|
+
}
|
|
1162
|
+
const { values, positionals } = parse(args, {});
|
|
1163
|
+
const key = theOne(positionals, "Say which project: console project <key>; or console project new, edit or icon");
|
|
1164
|
+
const session = await open(io, values);
|
|
1165
|
+
const found = await session.client.project(key).get();
|
|
1166
|
+
answer(io, session, found, () => formatProject(found));
|
|
1167
|
+
return EXIT.ok;
|
|
1168
|
+
};
|
|
1169
|
+
const members = async (args, io) => {
|
|
1170
|
+
const { values } = parse(args, {}, false);
|
|
1171
|
+
const session = await open(io, values);
|
|
1172
|
+
const project = await session.project();
|
|
1173
|
+
const found = await project.members();
|
|
1174
|
+
answer(io, session, found, () => found.items.length === 0
|
|
1175
|
+
? "Nobody is listed in the project."
|
|
1176
|
+
: found.items.map(formatMemberLine).join("\n"));
|
|
1177
|
+
return EXIT.ok;
|
|
1178
|
+
};
|
|
1179
|
+
const member = async (args, io) => {
|
|
1180
|
+
const [verb, ...rest] = args;
|
|
1181
|
+
if (verb === "add") {
|
|
1182
|
+
const { values, positionals } = parse(rest, { role: { type: "string" } });
|
|
1183
|
+
const login = theOne(positionals, "Say whom: console member add <login> [--role owner|member]");
|
|
1184
|
+
const role = text(values, "role");
|
|
1185
|
+
const session = await open(io, values);
|
|
1186
|
+
const project = await session.project();
|
|
1187
|
+
const added = await project.addMember({
|
|
1188
|
+
personId: (await personOf(session, login)).id,
|
|
1189
|
+
...(role === undefined ? {} : { role: projectRoleOf(role) }),
|
|
1190
|
+
});
|
|
1191
|
+
answer(io, session, added, () => formatMemberLine(added));
|
|
1192
|
+
return EXIT.ok;
|
|
1193
|
+
}
|
|
1194
|
+
if (verb === "role") {
|
|
1195
|
+
const { values, positionals } = parse(rest, {});
|
|
1196
|
+
const [login, role] = theTwo(positionals, "Say whom and what: console member role <login> <owner|member>");
|
|
1197
|
+
const wanted = projectRoleOf(role);
|
|
1198
|
+
const session = await open(io, values);
|
|
1199
|
+
const project = await session.project();
|
|
1200
|
+
const changed = await project.updateMember((await personOf(session, login)).id, {
|
|
1201
|
+
role: wanted,
|
|
1202
|
+
});
|
|
1203
|
+
answer(io, session, changed, () => formatMemberLine(changed));
|
|
1204
|
+
return EXIT.ok;
|
|
1205
|
+
}
|
|
1206
|
+
if (verb === "remove") {
|
|
1207
|
+
const { values, positionals } = parse(rest, {});
|
|
1208
|
+
const login = theOne(positionals, "Say whom: console member remove <login>");
|
|
1209
|
+
const session = await open(io, values);
|
|
1210
|
+
const project = await session.project();
|
|
1211
|
+
const found = await personOf(session, login);
|
|
1212
|
+
await project.removeMember(found.id);
|
|
1213
|
+
answer(io, session, { removed: found.login }, () => `${found.login} is no longer listed in the project.`);
|
|
1214
|
+
return EXIT.ok;
|
|
1215
|
+
}
|
|
1216
|
+
throw misuse("Say what to do: console member add <login> [--role owner|member], console member role <login> <owner|member>, or console member remove <login>");
|
|
1217
|
+
};
|
|
1218
|
+
const category = async (args, io) => {
|
|
1219
|
+
const [verb, ...rest] = args;
|
|
1220
|
+
if (verb === "new") {
|
|
1221
|
+
const { values, positionals } = parse(rest, { color: { type: "string" } });
|
|
1222
|
+
const [key, name] = theTwo(positionals, 'Say the key and the name: console category new <key> "<name>" [--color <name>]');
|
|
1223
|
+
const color = text(values, "color");
|
|
1224
|
+
const session = await open(io, values);
|
|
1225
|
+
const made = await session.client.createCategory({
|
|
1226
|
+
key,
|
|
1227
|
+
name,
|
|
1228
|
+
...(color === undefined ? {} : { color: colorOf(color) }),
|
|
1229
|
+
});
|
|
1230
|
+
answer(io, session, made, () => formatCategoryLine(made));
|
|
1231
|
+
return EXIT.ok;
|
|
1232
|
+
}
|
|
1233
|
+
if (verb === "edit") {
|
|
1234
|
+
const { values, positionals } = parse(rest, {
|
|
1235
|
+
name: { type: "string" },
|
|
1236
|
+
color: { type: "string" },
|
|
1237
|
+
});
|
|
1238
|
+
const key = theOne(positionals, "Say which category: console category edit <key> [--name <name>] [--color <name>]");
|
|
1239
|
+
const name = text(values, "name");
|
|
1240
|
+
const color = text(values, "color");
|
|
1241
|
+
const patch = {
|
|
1242
|
+
...(name === undefined ? {} : { name }),
|
|
1243
|
+
...(color === undefined ? {} : { color: colorOf(color) }),
|
|
1244
|
+
};
|
|
1245
|
+
if (Object.keys(patch).length === 0) {
|
|
1246
|
+
throw misuse("Say what to change: --name or --color.");
|
|
1247
|
+
}
|
|
1248
|
+
const session = await open(io, values);
|
|
1249
|
+
const changed = await session.client.updateCategory(key, patch);
|
|
1250
|
+
answer(io, session, changed, () => formatCategoryLine(changed));
|
|
1251
|
+
return EXIT.ok;
|
|
1252
|
+
}
|
|
1253
|
+
if (verb === "remove") {
|
|
1254
|
+
const { values, positionals } = parse(rest, {});
|
|
1255
|
+
const key = theOne(positionals, "Say which category: console category remove <key>");
|
|
1256
|
+
const session = await open(io, values);
|
|
1257
|
+
await session.client.removeCategory(key);
|
|
1258
|
+
answer(io, session, { removed: key }, () => `Category ${key} removed.`);
|
|
1259
|
+
return EXIT.ok;
|
|
1260
|
+
}
|
|
1261
|
+
throw misuse('Say what to do: console category new <key> "<name>" [--color <name>], console category edit <key> [--name <name>] [--color <name>], or console category remove <key>');
|
|
1262
|
+
};
|
|
1035
1263
|
const COMMANDS = {
|
|
1036
1264
|
login,
|
|
1037
1265
|
logout,
|
|
@@ -1046,6 +1274,12 @@ const COMMANDS = {
|
|
|
1046
1274
|
goal,
|
|
1047
1275
|
categories,
|
|
1048
1276
|
skills,
|
|
1277
|
+
people,
|
|
1278
|
+
person,
|
|
1279
|
+
project,
|
|
1280
|
+
members,
|
|
1281
|
+
member,
|
|
1282
|
+
category,
|
|
1049
1283
|
take,
|
|
1050
1284
|
report,
|
|
1051
1285
|
"hand-in": handIn,
|
|
@@ -1097,6 +1331,26 @@ function hintFor(error) {
|
|
|
1097
1331
|
return "The due day is before the start. Say a due day on or after it.\n";
|
|
1098
1332
|
case "goal.not_found":
|
|
1099
1333
|
return "No goal of this project has that id. console goals lists them.\n";
|
|
1334
|
+
case "auth.forbidden":
|
|
1335
|
+
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";
|
|
1336
|
+
case "project.key_taken":
|
|
1337
|
+
return "A project has that key already. console projects lists them.\n";
|
|
1338
|
+
case "project.invalid_address":
|
|
1339
|
+
return "The skills address is not one as SkillCDN spells it; none makes the organization's skills the project's.\n";
|
|
1340
|
+
case "project.invalid_icon":
|
|
1341
|
+
return "The image must be a PNG, a JPEG, a GIF or a WebP, known by its bytes.\n";
|
|
1342
|
+
case "member.exists":
|
|
1343
|
+
return "The person is listed already. console member role <login> <owner|member> changes what they are.\n";
|
|
1344
|
+
case "member.not_found":
|
|
1345
|
+
return "The person is not listed in the project. console members lists those who are.\n";
|
|
1346
|
+
case "category.key_taken":
|
|
1347
|
+
return "A category has that key already. console categories lists them.\n";
|
|
1348
|
+
case "category.not_found":
|
|
1349
|
+
return "No category has that key. console categories lists them.\n";
|
|
1350
|
+
case "category.in_use":
|
|
1351
|
+
return "Tasks are sorted into the category. Move them out first: console task edit <number> --category <other|none>\n";
|
|
1352
|
+
case "person.last_admin":
|
|
1353
|
+
return "The board keeps at least one administrator. Make another one first.\n";
|
|
1100
1354
|
case "connect.too_many":
|
|
1101
1355
|
return "The console holds as many connections as it may at once. Try again in a few minutes.\n";
|
|
1102
1356
|
case "network":
|