@skillcdn/console 0.1.8 → 0.1.10
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 +5 -5
- package/dist/api.d.ts +5 -5
- 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.map +1 -1
- package/dist/cli/cli.js +255 -10
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +9 -1
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +80 -0
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +62 -7
- package/dist/cli/help.js.map +1 -1
- package/dist/client.d.ts +31 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +19 -3
- package/dist/client.js.map +1 -1
- package/dist/components/board.d.ts +27 -1
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +75 -7
- package/dist/components/board.js.map +1 -1
- package/dist/components/calendar.d.ts +34 -0
- package/dist/components/calendar.d.ts.map +1 -0
- package/dist/components/calendar.js +96 -0
- package/dist/components/calendar.js.map +1 -0
- package/dist/components/categories.d.ts +13 -0
- package/dist/components/categories.d.ts.map +1 -0
- package/dist/components/categories.js +52 -0
- package/dist/components/categories.js.map +1 -0
- package/dist/components/decision-list.d.ts +10 -0
- package/dist/components/decision-list.d.ts.map +1 -1
- package/dist/components/decision-list.js +14 -1
- 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 +12 -0
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/goals.d.ts +35 -0
- package/dist/components/goals.d.ts.map +1 -0
- package/dist/components/goals.js +45 -0
- package/dist/components/goals.js.map +1 -0
- package/dist/components/task-form.d.ts +5 -1
- package/dist/components/task-form.d.ts.map +1 -1
- package/dist/components/task-form.js +13 -1
- package/dist/components/task-form.js.map +1 -1
- package/dist/components/task-list.d.ts +16 -0
- package/dist/components/task-list.d.ts.map +1 -0
- package/dist/components/task-list.js +14 -0
- package/dist/components/task-list.js.map +1 -0
- package/dist/components/task-view.d.ts +9 -1
- package/dist/components/task-view.d.ts.map +1 -1
- package/dist/components/task-view.js +10 -4
- package/dist/components/task-view.js.map +1 -1
- package/dist/components/ui.d.ts +30 -1
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +60 -0
- package/dist/components/ui.js.map +1 -1
- package/dist/components/work.d.ts +10 -0
- package/dist/components/work.d.ts.map +1 -0
- package/dist/components/work.js +29 -0
- package/dist/components/work.js.map +1 -0
- package/dist/console.css +511 -0
- package/dist/console.d.ts +8 -0
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +124 -8
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +14 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +36 -2
- package/dist/data.js.map +1 -1
- package/dist/dates.d.ts +39 -0
- package/dist/dates.d.ts.map +1 -0
- package/dist/dates.js +120 -0
- package/dist/dates.js.map +1 -0
- package/dist/i18n/en.d.ts +162 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +174 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +169 -1
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -4
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +5 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +5 -0
- package/dist/limits.js.map +1 -1
- package/dist/router.d.ts +50 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +74 -1
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +7 -1
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +9 -0
- package/dist/routes.js.map +1 -1
- package/dist/schemas.d.ts +366 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +112 -3
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +15 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +26 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +11 -4
- package/docs/specs/rest.md +33 -8
- package/package.json +1 -1
- package/skills/building-a-console/SKILL.md +4 -4
- package/skills/building-a-console/references/the-default-console.md +1 -1
- package/skills/deploying-a-console/SKILL.md +11 -10
- package/skills/deploying-a-console/assets/deployer-policy.json +13 -12
- package/skills/deploying-a-console/assets/secrets.example.json +4 -4
- package/skills/deploying-a-console/references/access.md +10 -9
- package/skills/deploying-a-console/references/resources.md +107 -50
- package/skills/deploying-a-console/references/sizing.md +1 -1
- package/skills/deploying-a-console/scripts/store-secrets.mjs +1 -15
- package/skills/working-the-board/SKILL.md +7 -6
- package/src/api.ts +30 -0
- package/src/cli/cli.ts +311 -10
- package/src/cli/format.ts +88 -0
- package/src/cli/help.ts +69 -7
- package/src/client.ts +70 -2
- package/src/components/board.tsx +205 -12
- package/src/components/calendar.tsx +255 -0
- package/src/components/categories.tsx +209 -0
- package/src/components/decision-list.tsx +62 -5
- package/src/components/event-feed.tsx +12 -0
- package/src/components/goals.tsx +264 -0
- package/src/components/task-form.tsx +78 -1
- package/src/components/task-list.tsx +61 -0
- package/src/components/task-view.tsx +74 -3
- package/src/components/ui.tsx +116 -1
- package/src/components/work.tsx +83 -0
- package/src/console.tsx +337 -4
- package/src/data.ts +63 -8
- package/src/dates.ts +143 -0
- package/src/i18n/en.ts +184 -1
- package/src/i18n/ko.ts +175 -1
- package/src/index.ts +58 -1
- package/src/limits.ts +5 -0
- package/src/router.ts +119 -6
- package/src/routes.ts +10 -0
- package/src/schemas.ts +141 -1
- package/src/styles/console.css +511 -0
- package/src/vocabulary.ts +33 -0
|
@@ -78,7 +78,7 @@ Part of the plan, in plain words, one line per item, the chosen size first:
|
|
|
78
78
|
## Options the plan offers
|
|
79
79
|
|
|
80
80
|
- **The database as serverless capacity.** Aurora Serverless v2 for PostgreSQL scales its capacity with load, down to nothing when idle (0 ACU with automatic pause, since 2024-11). The console holds a listening connection open and sweeps every fifteen minutes, so it never idles and the database never pauses: the floor is 0.5 ACU, about $44 a month at $0.12 per ACU-hour (read 2026-10), more than the `small` and `medium` instances. Offer it when the organization wants capacity that follows load without resizing, and say the floor.
|
|
81
|
-
- **The bucket on Cloudflare R2** instead of S3: the same S3 API,
|
|
81
|
+
- **The bucket on Cloudflare R2** instead of S3: the same S3 API, `S3_ENDPOINT=https://<account id>.r2.cloudflarestorage.com`, `S3_REGION=auto`, path style; no egress charges. Its keys are made in the Cloudflare dashboard (R2, Manage API tokens, Object Read & Write on the one bucket), saved in the secrets file as `S3_ACCESS_KEY_ID` and `S3_SECRET_ACCESS_KEY`, stored as parameters and carried by the `api` task as secrets, with no `S3_CREDENTIALS` line: the task's role cannot reach R2. Offer it when the organization keeps its files at Cloudflare already; S3 stays the default because the task's own role reaches it with no key to keep, and the budget and the audit are then in one account.
|
|
82
82
|
- **A load balancer instead of the tunnel.** ECS Express Mode creates an Application Load Balancer with HTTPS on an AWS domain in one command, at about $16 to $23 a month for the balancer (read 2026-10); the custom domain is a manual step on the balancer, and Cloudflare then proxies to it. Offer it only when the organization does not want its traffic through the tunnel; the tunnel is the default because it costs nothing, exposes nothing, and brings the client's address to the console cleanly.
|
|
83
83
|
- **ARM tasks** when the organization has ARM runners for its builds (building for ARM on an x86 runner takes many times longer): a fifth off the task line.
|
|
84
84
|
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
// node store-secrets.mjs set <file> <NAME> the value is standard input
|
|
6
6
|
// node store-secrets.mjs derive <file> <NAME> <template> {{OTHER}} in the template is replaced
|
|
7
7
|
// node store-secrets.mjs render <file> <in> <out> a JSON file with {{NAME}} placeholders filled
|
|
8
|
-
// node store-secrets.mjs key <file> <iam user> creates an access key for the user and stores both halves
|
|
9
8
|
// node store-secrets.mjs store <file> <prefix> every value as a SecureString parameter <prefix>/<NAME>
|
|
10
9
|
import { execFileSync } from "node:child_process";
|
|
11
10
|
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
@@ -15,7 +14,7 @@ import process from "node:process";
|
|
|
15
14
|
|
|
16
15
|
const [mode, file, ...rest] = process.argv.slice(2);
|
|
17
16
|
const usage = () => {
|
|
18
|
-
process.stderr.write("usage: node store-secrets.mjs set|derive|render|
|
|
17
|
+
process.stderr.write("usage: node store-secrets.mjs set|derive|render|store <file> ...\n");
|
|
19
18
|
process.exit(2);
|
|
20
19
|
};
|
|
21
20
|
if (mode === undefined || file === undefined) {
|
|
@@ -73,19 +72,6 @@ switch (mode) {
|
|
|
73
72
|
writeFileSync(output, fill(read(), readFileSync(input, "utf8")), { mode: 0o600 });
|
|
74
73
|
break;
|
|
75
74
|
}
|
|
76
|
-
case "key": {
|
|
77
|
-
const [user] = rest;
|
|
78
|
-
if (user === undefined) {
|
|
79
|
-
usage();
|
|
80
|
-
}
|
|
81
|
-
const answer = JSON.parse(aws(["iam", "create-access-key"], { UserName: user }));
|
|
82
|
-
write({
|
|
83
|
-
...read(),
|
|
84
|
-
S3_ACCESS_KEY_ID: answer.AccessKey.AccessKeyId,
|
|
85
|
-
S3_SECRET_ACCESS_KEY: answer.AccessKey.SecretAccessKey,
|
|
86
|
-
});
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
89
75
|
case "store": {
|
|
90
76
|
const [prefix] = rest;
|
|
91
77
|
if (prefix === undefined) {
|
|
@@ -5,7 +5,7 @@ license: MIT
|
|
|
5
5
|
compatibility: Needs a shell with the console command on the PATH (npm install -g @skillcdn/console, Node.js 24 or newer), signed in by a person. Nothing else; the command needs no key of its own and the agent never sees a token.
|
|
6
6
|
metadata:
|
|
7
7
|
author: skillcdn
|
|
8
|
-
version: "1.
|
|
8
|
+
version: "1.2"
|
|
9
9
|
skillcdn:
|
|
10
10
|
translations:
|
|
11
11
|
ko:
|
|
@@ -37,8 +37,8 @@ The full contract is the [specification of the command line](/docs/specs/cli.md)
|
|
|
37
37
|
|
|
38
38
|
| Input | Source |
|
|
39
39
|
|---|---|
|
|
40
|
-
| The task | Its number (`#7`) or its id, from what the person said. Told to pick one, `console tasks --state ready`
|
|
41
|
-
| The work | The task's body, read with `console task <number>`: what to make, where, the milestones, what to ask. The pages it links to (`console doc <path>`) and the project's skills (`console skills`, each loaded through the agent's own SkillCDN connection) say the rest. |
|
|
40
|
+
| The task | Its number (`#7`) or its id, from what the person said. Told to pick one, `console work` lists what is on the person's plate across their projects, the sooner due first, and `console tasks --state ready --mine` what is ready and theirs in this one (`--due overdue`, `--due week`, `--category <key>` and `--goal <id>` narrow it further); a task is never taken without being told which, or that any ready one may be. |
|
|
41
|
+
| The work | The task's body, read with `console task <number>`: what to make, where, the milestones, what to ask; with its plan, the category, the goal it works towards (`console goal <id>` says what reaching it means) and the days it starts and is due, which the reports and the summary keep to. The pages it links to (`console doc <path>`) and the project's skills (`console skills`, each loaded through the agent's own SkillCDN connection) say the rest. |
|
|
42
42
|
| The organization's way | Its own skill for its console, when it has one, adds which tasks to take, what to report and when to ask; it comes through the same kind of address as its other skills. |
|
|
43
43
|
|
|
44
44
|
## Workflow
|
|
@@ -47,7 +47,7 @@ Each phase ends with something on the board. Only phase 4 waits for a person.
|
|
|
47
47
|
|
|
48
48
|
### Phase 1: Read
|
|
49
49
|
|
|
50
|
-
`console task <number>` shows the task in full, the decisions about it and the runs on it. A run open on it by another agent means the task is taken: say so and stop. Read what the body links to, with `console doc <path>` for the project's pages and `console skills` for the organization's skills, before anything is made.
|
|
50
|
+
`console task <number>` shows the task in full, with its plan, the decisions about it and the runs on it. A run open on it by another agent means the task is taken: say so and stop. A task that is past its due day is still the task; the first report says so, and what the day was for is kept in mind. Read what the body links to, with `console doc <path>` for the project's pages and `console skills` for the organization's skills, before anything is made.
|
|
51
51
|
|
|
52
52
|
### Phase 2: Take
|
|
53
53
|
|
|
@@ -77,10 +77,11 @@ After the last report, `console finish --summary "<what was done, and what is le
|
|
|
77
77
|
|
|
78
78
|
- Never look for, read, print or send a token. The rules of the repository say the rest.
|
|
79
79
|
- One run at a time: a command on a run acts on the one open run of this token in the project; working two tasks at once, say which with `--run <id>`.
|
|
80
|
-
- The board's words are the vocabulary's own: states `idea`, `ready`, `in_progress`, `in_review`, `done` and `dropped`; priorities `low`, `normal`, `high` and `urgent`. Taking a task moves it, finishing moves it; it is not moved by hand around a run.
|
|
80
|
+
- The board's words are the vocabulary's own: states `idea`, `ready`, `in_progress`, `in_review`, `done` and `dropped`; priorities `low`, `normal`, `high` and `urgent`; a day is `2026-10-24`. Taking a task moves it, finishing moves it; it is not moved by hand around a run. When a task is started and when it is done, the board writes down itself: never typed.
|
|
81
|
+
- The plan is the person's: a task's days, its category and its goal are changed (`console task edit`) only when the task says so or a person asks, and a goal is set or moved (`console goal new`, `console goal edit`) only when the person is an owner and said to.
|
|
81
82
|
- Exit codes: `0` done; `1` refused or unreachable, the reason on standard error as a code and words, which `console help <command>` names; `2` not understood; `3` a decision still waits.
|
|
82
83
|
- Everything sent is held to limits the console states (`console help <command>`); a refusal `request.invalid` names the field.
|
|
83
84
|
|
|
84
85
|
## Terms
|
|
85
86
|
|
|
86
|
-
- **Task**: what is to be done, numbered per project. **Run**: one agent at work on one task for one person, with its reports, what it handed in, and what it waits for. **Decision**: a question with options, which a person answers on the board. **Page**: a document of the project, Markdown at a path that does not change. **Project**: a board of its own, with its members and its skills address.
|
|
87
|
+
- **Task**: what is to be done, numbered per project, with its plan (a category, a goal, the days it starts and is due) and its moments (when it was started and done, which the board keeps). **Goal**: what the project works towards, on its roadmap, with the day it is meant to be reached by; its progress is its tasks. **Category**: a kind of work, the workspace's word for it, by key. **Run**: one agent at work on one task for one person, with its reports, what it handed in, and what it waits for. **Decision**: a question with options, which a person answers on the board. **Page**: a document of the project, Markdown at a path that does not change. **Project**: a board of its own, with its members and its skills address.
|
package/src/api.ts
CHANGED
|
@@ -13,6 +13,7 @@ export {
|
|
|
13
13
|
type FetchLike,
|
|
14
14
|
type FileUpload,
|
|
15
15
|
type ProjectClient,
|
|
16
|
+
type TaskFilter,
|
|
16
17
|
} from "./client.js";
|
|
17
18
|
export {
|
|
18
19
|
CONNECT_CODE_ALPHABET,
|
|
@@ -36,6 +37,9 @@ export {
|
|
|
36
37
|
MAX_AGENT_LENGTH,
|
|
37
38
|
MAX_ARTIFACTS_PER_RUN,
|
|
38
39
|
MAX_BODY_LENGTH,
|
|
40
|
+
MAX_CATEGORIES,
|
|
41
|
+
MAX_CATEGORY_KEY_LENGTH,
|
|
42
|
+
MAX_CATEGORY_NAME_LENGTH,
|
|
39
43
|
MAX_DOCUMENT_LENGTH,
|
|
40
44
|
MAX_DOCUMENT_PATH_LENGTH,
|
|
41
45
|
MAX_DOCUMENT_SEGMENT_LENGTH,
|
|
@@ -68,6 +72,7 @@ export {
|
|
|
68
72
|
export {
|
|
69
73
|
AUTH_ROUTES,
|
|
70
74
|
CLAIM_PATH,
|
|
75
|
+
categoryPath,
|
|
71
76
|
connectPath,
|
|
72
77
|
isSignInFailure,
|
|
73
78
|
loginPath,
|
|
@@ -88,6 +93,10 @@ export {
|
|
|
88
93
|
type RestArtifact,
|
|
89
94
|
type RestArtifactInput,
|
|
90
95
|
type RestBacklink,
|
|
96
|
+
type RestCategories,
|
|
97
|
+
type RestCategory,
|
|
98
|
+
type RestCategoryInput,
|
|
99
|
+
type RestCategoryPatch,
|
|
91
100
|
type RestClaim,
|
|
92
101
|
type RestClaimInput,
|
|
93
102
|
type RestConnectInput,
|
|
@@ -110,6 +119,10 @@ export {
|
|
|
110
119
|
type RestEvents,
|
|
111
120
|
type RestFile,
|
|
112
121
|
type RestFileInput,
|
|
122
|
+
type RestGoal,
|
|
123
|
+
type RestGoalInput,
|
|
124
|
+
type RestGoalPatch,
|
|
125
|
+
type RestGoals,
|
|
113
126
|
type RestMe,
|
|
114
127
|
type RestMember,
|
|
115
128
|
type RestMemberInput,
|
|
@@ -133,6 +146,7 @@ export {
|
|
|
133
146
|
type RestSkill,
|
|
134
147
|
type RestSkills,
|
|
135
148
|
type RestTask,
|
|
149
|
+
type RestTaskCounts,
|
|
136
150
|
type RestTaskInput,
|
|
137
151
|
type RestTaskLink,
|
|
138
152
|
type RestTaskPatch,
|
|
@@ -144,10 +158,15 @@ export {
|
|
|
144
158
|
type RestVersion,
|
|
145
159
|
type RestVersionSummary,
|
|
146
160
|
type RestVersions,
|
|
161
|
+
type RestWork,
|
|
147
162
|
restAnswerInputSchema,
|
|
148
163
|
restArtifactInputSchema,
|
|
149
164
|
restArtifactSchema,
|
|
150
165
|
restBacklinkSchema,
|
|
166
|
+
restCategoriesSchema,
|
|
167
|
+
restCategoryInputSchema,
|
|
168
|
+
restCategoryPatchSchema,
|
|
169
|
+
restCategorySchema,
|
|
151
170
|
restClaimInputSchema,
|
|
152
171
|
restClaimSchema,
|
|
153
172
|
restConnectInputSchema,
|
|
@@ -170,6 +189,10 @@ export {
|
|
|
170
189
|
restEventsSchema,
|
|
171
190
|
restFileInputSchema,
|
|
172
191
|
restFileSchema,
|
|
192
|
+
restGoalInputSchema,
|
|
193
|
+
restGoalPatchSchema,
|
|
194
|
+
restGoalSchema,
|
|
195
|
+
restGoalsSchema,
|
|
173
196
|
restMemberInputSchema,
|
|
174
197
|
restMemberPatchSchema,
|
|
175
198
|
restMemberSchema,
|
|
@@ -192,6 +215,7 @@ export {
|
|
|
192
215
|
restRunsSchema,
|
|
193
216
|
restSkillSchema,
|
|
194
217
|
restSkillsSchema,
|
|
218
|
+
restTaskCountsSchema,
|
|
195
219
|
restTaskInputSchema,
|
|
196
220
|
restTaskLinkSchema,
|
|
197
221
|
restTaskPatchSchema,
|
|
@@ -204,16 +228,22 @@ export {
|
|
|
204
228
|
restVersionSchema,
|
|
205
229
|
restVersionSummarySchema,
|
|
206
230
|
restVersionsSchema,
|
|
231
|
+
restWorkSchema,
|
|
207
232
|
} from "./schemas.js";
|
|
208
233
|
export { hasForbiddenCodePoint } from "./text.js";
|
|
209
234
|
export {
|
|
210
235
|
ARTIFACT_KINDS,
|
|
211
236
|
type ArtifactKind,
|
|
237
|
+
CATEGORY_COLORS,
|
|
238
|
+
type CategoryColor,
|
|
212
239
|
EVENT_KINDS,
|
|
213
240
|
type EventKind,
|
|
241
|
+
GOAL_STATES,
|
|
242
|
+
type GoalState,
|
|
214
243
|
isProviderKey,
|
|
215
244
|
LINK_SOURCES,
|
|
216
245
|
type LinkSource,
|
|
246
|
+
OPEN_TASK_STATES,
|
|
217
247
|
PERSON_ROLES,
|
|
218
248
|
type PersonRole,
|
|
219
249
|
PROJECT_ROLES,
|
package/src/cli/cli.ts
CHANGED
|
@@ -7,18 +7,29 @@ import {
|
|
|
7
7
|
type FetchLike,
|
|
8
8
|
type ProjectClient,
|
|
9
9
|
} from "../client.js";
|
|
10
|
+
import { addDays, dayOf as dayOfDate, isDay } from "../dates.js";
|
|
10
11
|
import { MAX_OPTIONS, MIN_OPTIONS } from "../limits.js";
|
|
11
|
-
import type { RestClaim, RestDecision } from "../schemas.js";
|
|
12
|
-
import {
|
|
12
|
+
import type { RestClaim, RestDecision, RestTaskPatch } from "../schemas.js";
|
|
13
|
+
import {
|
|
14
|
+
GOAL_STATES,
|
|
15
|
+
type GoalState,
|
|
16
|
+
TASK_PRIORITIES,
|
|
17
|
+
TASK_STATES,
|
|
18
|
+
type TaskPriority,
|
|
19
|
+
type TaskState,
|
|
20
|
+
} from "../vocabulary.js";
|
|
13
21
|
import { loadWebRoot, type WebRoot, WebRootError } from "../web.js";
|
|
14
22
|
import type { CredentialStore } from "./credentials.js";
|
|
15
23
|
import type { DirectoryStore } from "./directory.js";
|
|
16
24
|
import {
|
|
17
25
|
formatAnswer,
|
|
26
|
+
formatCategoryLine,
|
|
18
27
|
formatDecision,
|
|
19
28
|
formatDecisionLine,
|
|
20
29
|
formatDocument,
|
|
21
30
|
formatDocuments,
|
|
31
|
+
formatGoal,
|
|
32
|
+
formatGoalLine,
|
|
22
33
|
formatOptions,
|
|
23
34
|
formatProjectLine,
|
|
24
35
|
formatRun,
|
|
@@ -28,6 +39,7 @@ import {
|
|
|
28
39
|
formatTaskLine,
|
|
29
40
|
formatVersion,
|
|
30
41
|
formatVersionLine,
|
|
42
|
+
formatWork,
|
|
31
43
|
} from "./format.js";
|
|
32
44
|
import { COMMAND_HELP, helpFor, usage } from "./help.js";
|
|
33
45
|
import { createServeHandler, DEFAULT_SERVE_PORT, type Listen } from "./serve.js";
|
|
@@ -255,6 +267,79 @@ function priorityOf(value: string | undefined): TaskPriority | undefined {
|
|
|
255
267
|
return found;
|
|
256
268
|
}
|
|
257
269
|
|
|
270
|
+
function goalStateOf(value: string | undefined): GoalState | undefined {
|
|
271
|
+
if (value === undefined) {
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
const found = GOAL_STATES.find((state) => state === value);
|
|
275
|
+
if (found === undefined) {
|
|
276
|
+
throw misuse(`Not a state of a goal: ${value}. One of ${GOAL_STATES.join(", ")}`);
|
|
277
|
+
}
|
|
278
|
+
return found;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Today by this machine's clock, as a day. */
|
|
282
|
+
const todayOf = (io: CliIo): string => dayOfDate(new Date(io.now()));
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* A day as a flag takes one: `2026-10-24`, `today`, `tomorrow`, or `+<n>` days from today; and
|
|
286
|
+
* `none` where a field may be cleared, which is `null` on the wire.
|
|
287
|
+
*/
|
|
288
|
+
function dayOf(io: CliIo, value: string | undefined, clearable = false): string | null | undefined {
|
|
289
|
+
if (value === undefined) {
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
if (clearable && value === "none") {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
if (value === "today") {
|
|
296
|
+
return todayOf(io);
|
|
297
|
+
}
|
|
298
|
+
if (value === "tomorrow") {
|
|
299
|
+
return addDays(todayOf(io), 1);
|
|
300
|
+
}
|
|
301
|
+
const ahead = /^\+(\d{1,3})$/.exec(value);
|
|
302
|
+
if (ahead !== null) {
|
|
303
|
+
return addDays(todayOf(io), Number(ahead[1]));
|
|
304
|
+
}
|
|
305
|
+
if (!isDay(value)) {
|
|
306
|
+
throw misuse(`Not a day: ${value}. Say it as 2026-10-24, today, tomorrow, or +7`);
|
|
307
|
+
}
|
|
308
|
+
return value;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** A value a field is set to, or `none` to clear it, which is `null` on the wire. */
|
|
312
|
+
const orNone = (value: string | undefined): string | null | undefined =>
|
|
313
|
+
value === "none" ? null : value;
|
|
314
|
+
|
|
315
|
+
/** The id of the person a flag names: `me`, or a login of the workspace. */
|
|
316
|
+
async function personIdOf(
|
|
317
|
+
session: Session,
|
|
318
|
+
value: string | undefined,
|
|
319
|
+
): Promise<string | null | undefined> {
|
|
320
|
+
if (value === undefined) {
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
if (value === "none") {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
if (value === "me") {
|
|
327
|
+
const me = await session.client.me();
|
|
328
|
+
if (me.person === null) {
|
|
329
|
+
throw failed("The console does not know this token. Sign in again: console login");
|
|
330
|
+
}
|
|
331
|
+
return me.person.id;
|
|
332
|
+
}
|
|
333
|
+
const { items } = await session.client.people();
|
|
334
|
+
const found = items.find((person) => person.login.toLowerCase() === value.toLowerCase());
|
|
335
|
+
if (found === undefined) {
|
|
336
|
+
throw misuse(
|
|
337
|
+
`Nobody of the workspace is called ${value}. console people is not a command; the logins are as people sign in.`,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
return found.id;
|
|
341
|
+
}
|
|
342
|
+
|
|
258
343
|
function secondsOf(value: string | undefined, fallback: number): number {
|
|
259
344
|
if (value === undefined) {
|
|
260
345
|
return fallback;
|
|
@@ -452,17 +537,67 @@ const use: Command = async (args, io) => {
|
|
|
452
537
|
};
|
|
453
538
|
|
|
454
539
|
const tasks: Command = async (args, io) => {
|
|
455
|
-
const { values } = parse(
|
|
540
|
+
const { values } = parse(
|
|
541
|
+
args,
|
|
542
|
+
{
|
|
543
|
+
state: { type: "string" },
|
|
544
|
+
open: { type: "boolean" },
|
|
545
|
+
mine: { type: "boolean" },
|
|
546
|
+
category: { type: "string" },
|
|
547
|
+
goal: { type: "string" },
|
|
548
|
+
due: { type: "string" },
|
|
549
|
+
},
|
|
550
|
+
false,
|
|
551
|
+
);
|
|
456
552
|
const state = stateOf(text(values, "state"));
|
|
457
553
|
const session = await open(io, values);
|
|
458
554
|
const project = await session.project();
|
|
459
|
-
|
|
555
|
+
// What is due: a day, or the words for the usual windows; overdue keeps only the open ones.
|
|
556
|
+
const due = text(values, "due");
|
|
557
|
+
const today = todayOf(io);
|
|
558
|
+
const dueBy =
|
|
559
|
+
due === undefined
|
|
560
|
+
? undefined
|
|
561
|
+
: due === "overdue"
|
|
562
|
+
? addDays(today, -1)
|
|
563
|
+
: due === "week"
|
|
564
|
+
? addDays(today, 6)
|
|
565
|
+
: (dayOf(io, due) ?? undefined);
|
|
566
|
+
const assignee = on(values, "mine")
|
|
567
|
+
? ((await personIdOf(session, "me")) ?? undefined)
|
|
568
|
+
: undefined;
|
|
569
|
+
const found = await project.tasks({
|
|
570
|
+
...(state === undefined ? {} : { state }),
|
|
571
|
+
...(on(values, "open") || due === "overdue" ? { open: true } : {}),
|
|
572
|
+
...(assignee === undefined ? {} : { assignee }),
|
|
573
|
+
...(text(values, "category") === undefined ? {} : { category: text(values, "category") }),
|
|
574
|
+
...(text(values, "goal") === undefined ? {} : { goal: text(values, "goal") }),
|
|
575
|
+
...(dueBy === undefined ? {} : { dueBy }),
|
|
576
|
+
});
|
|
460
577
|
answer(io, session, found, () =>
|
|
461
578
|
found.items.length === 0 ? "No tasks." : found.items.map(formatTaskLine).join("\n"),
|
|
462
579
|
);
|
|
463
580
|
return EXIT.ok;
|
|
464
581
|
};
|
|
465
582
|
|
|
583
|
+
const work: Command = async (args, io) => {
|
|
584
|
+
const { values } = parse(args, {}, false);
|
|
585
|
+
const session = await open(io, values);
|
|
586
|
+
const found = await session.client.work();
|
|
587
|
+
answer(io, session, found, () => formatWork(found));
|
|
588
|
+
return EXIT.ok;
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
const categories: Command = async (args, io) => {
|
|
592
|
+
const { values } = parse(args, {}, false);
|
|
593
|
+
const session = await open(io, values);
|
|
594
|
+
const found = await session.client.categories();
|
|
595
|
+
answer(io, session, found, () =>
|
|
596
|
+
found.items.length === 0 ? "No categories." : found.items.map(formatCategoryLine).join("\n"),
|
|
597
|
+
);
|
|
598
|
+
return EXIT.ok;
|
|
599
|
+
};
|
|
600
|
+
|
|
466
601
|
const skills: Command = async (args, io) => {
|
|
467
602
|
const { values } = parse(args, {}, false);
|
|
468
603
|
const session = await open(io, values);
|
|
@@ -472,33 +607,105 @@ const skills: Command = async (args, io) => {
|
|
|
472
607
|
return EXIT.ok;
|
|
473
608
|
};
|
|
474
609
|
|
|
610
|
+
/** The flags that set a task's fields, shared by writing one and changing one. */
|
|
611
|
+
const TASK_FIELDS: OptionSpec = {
|
|
612
|
+
state: { type: "string" },
|
|
613
|
+
priority: { type: "string" },
|
|
614
|
+
category: { type: "string" },
|
|
615
|
+
goal: { type: "string" },
|
|
616
|
+
starts: { type: "string" },
|
|
617
|
+
due: { type: "string" },
|
|
618
|
+
assignee: { type: "string" },
|
|
619
|
+
parent: { type: "string" },
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
/** What the flags say of a task's fields, as the API takes them; `none` clears a field. */
|
|
623
|
+
async function taskFieldsOf(
|
|
624
|
+
io: CliIo,
|
|
625
|
+
session: Session,
|
|
626
|
+
project: ProjectClient,
|
|
627
|
+
values: Values,
|
|
628
|
+
): Promise<RestTaskPatch> {
|
|
629
|
+
const state = stateOf(text(values, "state"));
|
|
630
|
+
const priority = priorityOf(text(values, "priority"));
|
|
631
|
+
const category = orNone(text(values, "category"));
|
|
632
|
+
const goalId = orNone(text(values, "goal"));
|
|
633
|
+
const startsOn = dayOf(io, text(values, "starts"), true);
|
|
634
|
+
const dueOn = dayOf(io, text(values, "due"), true);
|
|
635
|
+
const assigneeId = await personIdOf(session, text(values, "assignee"));
|
|
636
|
+
const parent = text(values, "parent");
|
|
637
|
+
const parentId =
|
|
638
|
+
parent === undefined ? undefined : parent === "none" ? null : (await project.task(parent)).id;
|
|
639
|
+
return {
|
|
640
|
+
...(state === undefined ? {} : { state }),
|
|
641
|
+
...(priority === undefined ? {} : { priority }),
|
|
642
|
+
...(category === undefined ? {} : { category }),
|
|
643
|
+
...(goalId === undefined ? {} : { goalId }),
|
|
644
|
+
...(startsOn === undefined ? {} : { startsOn }),
|
|
645
|
+
...(dueOn === undefined ? {} : { dueOn }),
|
|
646
|
+
...(assigneeId === undefined ? {} : { assigneeId }),
|
|
647
|
+
...(parentId === undefined ? {} : { parentId }),
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
475
651
|
const newTask: Command = async (args, io) => {
|
|
476
652
|
const { values, positionals } = parse(args, {
|
|
653
|
+
...TASK_FIELDS,
|
|
477
654
|
body: { type: "string" },
|
|
478
655
|
file: { type: "string" },
|
|
479
|
-
state: { type: "string" },
|
|
480
|
-
priority: { type: "string" },
|
|
481
656
|
});
|
|
482
657
|
const title = theOne(positionals, 'Say the title: console task new "<title>"');
|
|
483
|
-
const state = stateOf(text(values, "state"));
|
|
484
|
-
const priority = priorityOf(text(values, "priority"));
|
|
485
658
|
const body = await bodyOf(io, values, text(values, "body"));
|
|
486
659
|
const session = await open(io, values);
|
|
487
660
|
const project = await session.project();
|
|
661
|
+
const fields = await taskFieldsOf(io, session, project, values);
|
|
488
662
|
const made = await project.createTask({
|
|
489
663
|
title,
|
|
490
664
|
...(body === undefined ? {} : { body }),
|
|
491
|
-
...
|
|
492
|
-
...(priority === undefined ? {} : { priority }),
|
|
665
|
+
...fields,
|
|
493
666
|
});
|
|
494
667
|
answer(io, session, made, () => `Task #${made.number} written: ${made.title}\nid: ${made.id}`);
|
|
495
668
|
return EXIT.ok;
|
|
496
669
|
};
|
|
497
670
|
|
|
671
|
+
const editTask: Command = async (args, io) => {
|
|
672
|
+
const { values, positionals } = parse(args, {
|
|
673
|
+
...TASK_FIELDS,
|
|
674
|
+
title: { type: "string" },
|
|
675
|
+
body: { type: "string" },
|
|
676
|
+
file: { type: "string" },
|
|
677
|
+
});
|
|
678
|
+
const ref = theOne(
|
|
679
|
+
positionals,
|
|
680
|
+
"Say which task: console task edit <number|id> --due <day> [--category <key>] ...",
|
|
681
|
+
);
|
|
682
|
+
const body = await bodyOf(io, values, text(values, "body"));
|
|
683
|
+
const session = await open(io, values);
|
|
684
|
+
const project = await session.project();
|
|
685
|
+
const title = text(values, "title");
|
|
686
|
+
const patch: RestTaskPatch = {
|
|
687
|
+
...(title === undefined ? {} : { title }),
|
|
688
|
+
...(body === undefined ? {} : { body }),
|
|
689
|
+
...(await taskFieldsOf(io, session, project, values)),
|
|
690
|
+
};
|
|
691
|
+
if (Object.keys(patch).length === 0) {
|
|
692
|
+
throw misuse(
|
|
693
|
+
"Say what to change: --title, --body, --state, --priority, --category, --goal, --starts, --due, --assignee or --parent; none clears a field.",
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
const found = await project.task(ref);
|
|
697
|
+
const changed = await project.updateTask(found.id, patch);
|
|
698
|
+
answer(io, session, changed, () => formatTask(changed));
|
|
699
|
+
return EXIT.ok;
|
|
700
|
+
};
|
|
701
|
+
|
|
498
702
|
const task: Command = async (args, io) => {
|
|
499
703
|
if (args[0] === "new") {
|
|
500
704
|
return newTask(args.slice(1), io);
|
|
501
705
|
}
|
|
706
|
+
if (args[0] === "edit") {
|
|
707
|
+
return editTask(args.slice(1), io);
|
|
708
|
+
}
|
|
502
709
|
const { values, positionals } = parse(args, {});
|
|
503
710
|
const ref = theOne(positionals, "Say which task: console task <number|id>");
|
|
504
711
|
const session = await open(io, values);
|
|
@@ -864,6 +1071,88 @@ const restore: Command = async (args, io) => {
|
|
|
864
1071
|
return EXIT.ok;
|
|
865
1072
|
};
|
|
866
1073
|
|
|
1074
|
+
const goals: Command = async (args, io) => {
|
|
1075
|
+
const { values } = parse(args, {}, false);
|
|
1076
|
+
const session = await open(io, values);
|
|
1077
|
+
const project = await session.project();
|
|
1078
|
+
const found = await project.goals();
|
|
1079
|
+
answer(io, session, found, () =>
|
|
1080
|
+
found.items.length === 0
|
|
1081
|
+
? "No goals. An owner sets one: console goal new <title> --due <day>"
|
|
1082
|
+
: found.items.map(formatGoalLine).join("\n"),
|
|
1083
|
+
);
|
|
1084
|
+
return EXIT.ok;
|
|
1085
|
+
};
|
|
1086
|
+
|
|
1087
|
+
const GOAL_FIELDS: OptionSpec = {
|
|
1088
|
+
state: { type: "string" },
|
|
1089
|
+
due: { type: "string" },
|
|
1090
|
+
body: { type: "string" },
|
|
1091
|
+
file: { type: "string" },
|
|
1092
|
+
};
|
|
1093
|
+
|
|
1094
|
+
const newGoal: Command = async (args, io) => {
|
|
1095
|
+
const { values, positionals } = parse(args, GOAL_FIELDS);
|
|
1096
|
+
const title = theOne(positionals, 'Say the title: console goal new "<title>" [--due <day>]');
|
|
1097
|
+
const state = goalStateOf(text(values, "state"));
|
|
1098
|
+
const dueOn = dayOf(io, text(values, "due"));
|
|
1099
|
+
const body = await bodyOf(io, values, text(values, "body"));
|
|
1100
|
+
const session = await open(io, values);
|
|
1101
|
+
const project = await session.project();
|
|
1102
|
+
const made = await project.createGoal({
|
|
1103
|
+
title,
|
|
1104
|
+
...(body === undefined ? {} : { body }),
|
|
1105
|
+
...(state === undefined ? {} : { state }),
|
|
1106
|
+
...(dueOn === undefined ? {} : { dueOn }),
|
|
1107
|
+
});
|
|
1108
|
+
answer(io, session, made, () => `Goal set: ${made.title}\nid: ${made.id}`);
|
|
1109
|
+
return EXIT.ok;
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1112
|
+
const editGoal: Command = async (args, io) => {
|
|
1113
|
+
const { values, positionals } = parse(args, { ...GOAL_FIELDS, title: { type: "string" } });
|
|
1114
|
+
const id = theOne(positionals, "Say which goal: console goal edit <id> --state <state> ...");
|
|
1115
|
+
const title = text(values, "title");
|
|
1116
|
+
const state = goalStateOf(text(values, "state"));
|
|
1117
|
+
const dueOn = dayOf(io, text(values, "due"), true);
|
|
1118
|
+
const body = await bodyOf(io, values, text(values, "body"));
|
|
1119
|
+
const patch = {
|
|
1120
|
+
...(title === undefined ? {} : { title }),
|
|
1121
|
+
...(body === undefined ? {} : { body }),
|
|
1122
|
+
...(state === undefined ? {} : { state }),
|
|
1123
|
+
...(dueOn === undefined ? {} : { dueOn }),
|
|
1124
|
+
};
|
|
1125
|
+
if (Object.keys(patch).length === 0) {
|
|
1126
|
+
throw misuse(
|
|
1127
|
+
"Say what to change: --title, --body, --state or --due; --due none clears the day.",
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
const session = await open(io, values);
|
|
1131
|
+
const project = await session.project();
|
|
1132
|
+
const changed = await project.updateGoal(id, patch);
|
|
1133
|
+
answer(io, session, changed, () => formatGoal(changed));
|
|
1134
|
+
return EXIT.ok;
|
|
1135
|
+
};
|
|
1136
|
+
|
|
1137
|
+
const goal: Command = async (args, io) => {
|
|
1138
|
+
if (args[0] === "new") {
|
|
1139
|
+
return newGoal(args.slice(1), io);
|
|
1140
|
+
}
|
|
1141
|
+
if (args[0] === "edit") {
|
|
1142
|
+
return editGoal(args.slice(1), io);
|
|
1143
|
+
}
|
|
1144
|
+
const { values, positionals } = parse(args, {});
|
|
1145
|
+
const id = theOne(positionals, "Say which goal: console goal <id>");
|
|
1146
|
+
const session = await open(io, values);
|
|
1147
|
+
const project = await session.project();
|
|
1148
|
+
const found = await project.goal(id);
|
|
1149
|
+
const towards = await project.tasks({ goal: found.id });
|
|
1150
|
+
answer(io, session, { goal: found, tasks: towards.items }, () =>
|
|
1151
|
+
formatGoal(found, towards.items),
|
|
1152
|
+
);
|
|
1153
|
+
return EXIT.ok;
|
|
1154
|
+
};
|
|
1155
|
+
|
|
867
1156
|
const decisions: Command = async (args, io) => {
|
|
868
1157
|
const { values } = parse(args, { open: { type: "boolean" } }, false);
|
|
869
1158
|
const session = await open(io, values);
|
|
@@ -1024,8 +1313,12 @@ const COMMANDS: Readonly<Record<string, Command>> = {
|
|
|
1024
1313
|
serve,
|
|
1025
1314
|
projects,
|
|
1026
1315
|
use,
|
|
1316
|
+
work,
|
|
1027
1317
|
tasks,
|
|
1028
1318
|
task,
|
|
1319
|
+
goals,
|
|
1320
|
+
goal,
|
|
1321
|
+
categories,
|
|
1029
1322
|
skills,
|
|
1030
1323
|
take,
|
|
1031
1324
|
report,
|
|
@@ -1072,6 +1365,14 @@ function hintFor(error: ApiError): string {
|
|
|
1072
1365
|
return "The page has moved on since the version you started from. Read it again, and write from there.\n";
|
|
1073
1366
|
case "document.archived":
|
|
1074
1367
|
return "The page is archived. console restore <path> brings it back first.\n";
|
|
1368
|
+
case "task.invalid_category":
|
|
1369
|
+
return "No category has that key. console categories lists them.\n";
|
|
1370
|
+
case "task.invalid_goal":
|
|
1371
|
+
return "No goal of this project has that id. console goals lists them.\n";
|
|
1372
|
+
case "task.invalid_dates":
|
|
1373
|
+
return "The due day is before the start. Say a due day on or after it.\n";
|
|
1374
|
+
case "goal.not_found":
|
|
1375
|
+
return "No goal of this project has that id. console goals lists them.\n";
|
|
1075
1376
|
case "connect.too_many":
|
|
1076
1377
|
return "The console holds as many connections as it may at once. Try again in a few minutes.\n";
|
|
1077
1378
|
case "network":
|