toolcraft 0.0.16 → 0.0.18
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/dist/cli.d.ts +2 -0
- package/dist/cli.js +833 -124
- package/dist/error-report.d.ts +39 -0
- package/dist/error-report.js +330 -0
- package/dist/human-in-loop/approval-tasks.js +11 -8
- package/dist/human-in-loop/approvals-commands.js +21 -20
- package/dist/human-in-loop/default-provider.js +5 -3
- package/dist/human-in-loop/runner.js +45 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +55 -35
- package/dist/json-schema-converter.d.ts +1 -0
- package/dist/json-schema-converter.js +102 -52
- package/dist/mcp-proxy.d.ts +1 -0
- package/dist/mcp-proxy.js +13 -6
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.js +131 -55
- package/dist/sdk.d.ts +4 -2
- package/dist/sdk.js +132 -48
- package/dist/source-snippet.d.ts +8 -0
- package/dist/source-snippet.js +42 -0
- package/dist/stack-trim.d.ts +4 -0
- package/dist/stack-trim.js +70 -0
- package/dist/suggest.d.ts +4 -0
- package/dist/suggest.js +46 -0
- package/dist/user-error.d.ts +3 -0
- package/dist/user-error.js +7 -1
- package/dist/validation-errors.d.ts +5 -0
- package/dist/validation-errors.js +18 -0
- package/node_modules/@poe-code/design-system/dist/components/help-formatter-plain.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/components/help-formatter-plain.js +1 -1
- package/node_modules/@poe-code/design-system/dist/components/text.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/components/text.js +8 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/buffer.js +8 -1
- package/node_modules/@poe-code/design-system/dist/dashboard/keymap.d.ts +5 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/keymap.js +146 -12
- package/node_modules/@poe-code/design-system/dist/dashboard/terminal.js +31 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/types.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/explorer/actions.d.ts +16 -0
- package/node_modules/@poe-code/design-system/dist/explorer/actions.js +39 -0
- package/node_modules/@poe-code/design-system/dist/explorer/demo.d.ts +13 -0
- package/node_modules/@poe-code/design-system/dist/explorer/demo.js +297 -0
- package/node_modules/@poe-code/design-system/dist/explorer/events.d.ts +61 -0
- package/node_modules/@poe-code/design-system/dist/explorer/events.js +1 -0
- package/node_modules/@poe-code/design-system/dist/explorer/filter.d.ts +10 -0
- package/node_modules/@poe-code/design-system/dist/explorer/filter.js +95 -0
- package/node_modules/@poe-code/design-system/dist/explorer/index.d.ts +8 -0
- package/node_modules/@poe-code/design-system/dist/explorer/index.js +8 -0
- package/node_modules/@poe-code/design-system/dist/explorer/jobs.d.ts +7 -0
- package/node_modules/@poe-code/design-system/dist/explorer/jobs.js +59 -0
- package/node_modules/@poe-code/design-system/dist/explorer/keymap.d.ts +21 -0
- package/node_modules/@poe-code/design-system/dist/explorer/keymap.js +363 -0
- package/node_modules/@poe-code/design-system/dist/explorer/layout.d.ts +20 -0
- package/node_modules/@poe-code/design-system/dist/explorer/layout.js +73 -0
- package/node_modules/@poe-code/design-system/dist/explorer/reducer.d.ts +9 -0
- package/node_modules/@poe-code/design-system/dist/explorer/reducer.js +704 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/detail.d.ts +4 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/detail.js +96 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/footer.d.ts +4 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/footer.js +49 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/header.d.ts +4 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/header.js +56 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/index.d.ts +8 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/index.js +61 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/list.d.ts +4 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/list.js +106 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/modal.d.ts +3 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/modal.js +91 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/test-fixtures.d.ts +8 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/test-fixtures.js +156 -0
- package/node_modules/@poe-code/design-system/dist/explorer/runtime.d.ts +2 -0
- package/node_modules/@poe-code/design-system/dist/explorer/runtime.js +282 -0
- package/node_modules/@poe-code/design-system/dist/explorer/runtime.test-helpers.d.ts +50 -0
- package/node_modules/@poe-code/design-system/dist/explorer/runtime.test-helpers.js +101 -0
- package/node_modules/@poe-code/design-system/dist/explorer/state.d.ts +130 -0
- package/node_modules/@poe-code/design-system/dist/explorer/state.js +87 -0
- package/node_modules/@poe-code/design-system/dist/explorer/theme.d.ts +27 -0
- package/node_modules/@poe-code/design-system/dist/explorer/theme.js +97 -0
- package/node_modules/@poe-code/design-system/dist/index.d.ts +3 -0
- package/node_modules/@poe-code/design-system/dist/index.js +3 -0
- package/node_modules/@poe-code/design-system/package.json +1 -0
- package/node_modules/@poe-code/task-list/README.md +98 -0
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues-sync.d.ts +46 -0
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues-sync.js +309 -0
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues.d.ts +2 -0
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues.js +22 -2
- package/node_modules/@poe-code/task-list/dist/backends/markdown-dir.js +266 -99
- package/node_modules/@poe-code/task-list/dist/index.d.ts +1 -0
- package/node_modules/@poe-code/task-list/dist/index.js +1 -0
- package/node_modules/@poe-code/task-list/dist/open.js +3 -0
- package/node_modules/@poe-code/task-list/dist/types.d.ts +4 -0
- package/package.json +6 -2
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export function getExplorerTheme() {
|
|
2
|
+
return {
|
|
3
|
+
accent: accent,
|
|
4
|
+
muted: muted,
|
|
5
|
+
border: muted,
|
|
6
|
+
borderFocused: accent,
|
|
7
|
+
badge: (text, tone) => tonePainter(tone)(` ${text} `),
|
|
8
|
+
matchHighlight: (text) => accent(`\u001b[4m${text}\u001b[24m`)
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function getExplorerStyles() {
|
|
12
|
+
return resolveThemeName() === "light"
|
|
13
|
+
? {
|
|
14
|
+
accent: { fg: "#006699", bold: true },
|
|
15
|
+
muted: { fg: "#666666" },
|
|
16
|
+
border: { fg: "#666666" },
|
|
17
|
+
borderFocused: { fg: "#006699", bold: true },
|
|
18
|
+
matchHighlight: { fg: "#006699", bold: true, underline: true },
|
|
19
|
+
tones: {
|
|
20
|
+
success: { fg: "#008800" },
|
|
21
|
+
warning: { fg: "#cc6600" },
|
|
22
|
+
error: { fg: "#cc0000" },
|
|
23
|
+
info: { fg: "#a200ff" },
|
|
24
|
+
muted: { fg: "#666666" }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
: {
|
|
28
|
+
accent: { fg: "cyan", bold: true },
|
|
29
|
+
muted: { dim: true },
|
|
30
|
+
border: { dim: true },
|
|
31
|
+
borderFocused: { fg: "cyan", bold: true },
|
|
32
|
+
matchHighlight: { fg: "cyan", bold: true, underline: true },
|
|
33
|
+
tones: {
|
|
34
|
+
success: { fg: "green" },
|
|
35
|
+
warning: { fg: "yellow" },
|
|
36
|
+
error: { fg: "red" },
|
|
37
|
+
info: { fg: "magenta" },
|
|
38
|
+
muted: { dim: true }
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function tonePainter(tone) {
|
|
43
|
+
const light = resolveThemeName() === "light";
|
|
44
|
+
if (tone === "success") {
|
|
45
|
+
return light ? hex(0, 136, 0) : ansi(32);
|
|
46
|
+
}
|
|
47
|
+
if (tone === "warning") {
|
|
48
|
+
return light ? hex(204, 102, 0) : ansi(33);
|
|
49
|
+
}
|
|
50
|
+
if (tone === "error") {
|
|
51
|
+
return light ? hex(204, 0, 0) : ansi(31);
|
|
52
|
+
}
|
|
53
|
+
if (tone === "info") {
|
|
54
|
+
return light ? hex(162, 0, 255) : ansi(35);
|
|
55
|
+
}
|
|
56
|
+
return muted;
|
|
57
|
+
}
|
|
58
|
+
function accent(text) {
|
|
59
|
+
return resolveThemeName() === "light" ? ansi(38, 2, 0, 102, 153, 1)(text) : ansi(36)(text);
|
|
60
|
+
}
|
|
61
|
+
function muted(text) {
|
|
62
|
+
return resolveThemeName() === "light" ? hex(102, 102, 102)(text) : ansi(2)(text);
|
|
63
|
+
}
|
|
64
|
+
function hex(red, green, blue) {
|
|
65
|
+
return ansi(38, 2, red, green, blue);
|
|
66
|
+
}
|
|
67
|
+
function ansi(...codes) {
|
|
68
|
+
return (text) => `\u001b[${codes.join(";")}m${text}\u001b[0m`;
|
|
69
|
+
}
|
|
70
|
+
function resolveThemeName(env = process.env) {
|
|
71
|
+
const raw = (env.POE_CODE_THEME ?? env.POE_THEME)?.toLowerCase();
|
|
72
|
+
if (raw === "light" || raw === "dark") {
|
|
73
|
+
return raw;
|
|
74
|
+
}
|
|
75
|
+
const apple = env.APPLE_INTERFACE_STYLE;
|
|
76
|
+
if (typeof apple === "string") {
|
|
77
|
+
return apple.toLowerCase() === "dark" ? "dark" : "light";
|
|
78
|
+
}
|
|
79
|
+
const vscodeKind = env.VSCODE_COLOR_THEME_KIND;
|
|
80
|
+
if (typeof vscodeKind === "string") {
|
|
81
|
+
const normalized = vscodeKind.toLowerCase();
|
|
82
|
+
if (normalized.includes("light")) {
|
|
83
|
+
return "light";
|
|
84
|
+
}
|
|
85
|
+
if (normalized.includes("dark")) {
|
|
86
|
+
return "dark";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const colorFGBG = env.COLORFGBG;
|
|
90
|
+
if (typeof colorFGBG === "string") {
|
|
91
|
+
const background = Number.parseInt(colorFGBG.split(";").at(-1) ?? "", 10);
|
|
92
|
+
if (Number.isFinite(background)) {
|
|
93
|
+
return background >= 8 ? "light" : "dark";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return "dark";
|
|
97
|
+
}
|
|
@@ -19,6 +19,9 @@ export * as acp from "./acp/index.js";
|
|
|
19
19
|
export * as dashboard from "./dashboard/index.js";
|
|
20
20
|
export { createDashboard, shouldUseInteractiveDashboard } from "./dashboard/index.js";
|
|
21
21
|
export type { Dashboard, DashboardOptions } from "./dashboard/index.js";
|
|
22
|
+
export * as explorer from "./explorer/index.js";
|
|
23
|
+
export { runExplorer, singleDetail } from "./explorer/index.js";
|
|
24
|
+
export type { Row, DetailItem, Detail, DetailCtx, Action, ActionContext, ExplorerConfig, Tone, } from "./explorer/index.js";
|
|
22
25
|
export * as prompts from "./prompts/index.js";
|
|
23
26
|
export { intro, introPlain, outro, note, select, multiselect, text as promptText, confirm, confirmOrCancel, password, spinner, withSpinner, isCancel, cancel, log, PromptCancelledError } from "./prompts/index.js";
|
|
24
27
|
export type { SelectOptions, MultiselectOptions, TextOptions, ConfirmOptions, PasswordOptions, SpinnerOptions, WithSpinnerOptions } from "./prompts/index.js";
|
|
@@ -18,6 +18,9 @@ export * as acp from "./acp/index.js";
|
|
|
18
18
|
// Dashboard
|
|
19
19
|
export * as dashboard from "./dashboard/index.js";
|
|
20
20
|
export { createDashboard, shouldUseInteractiveDashboard } from "./dashboard/index.js";
|
|
21
|
+
// Explorer
|
|
22
|
+
export * as explorer from "./explorer/index.js";
|
|
23
|
+
export { runExplorer, singleDetail } from "./explorer/index.js";
|
|
21
24
|
// Prompts
|
|
22
25
|
export * as prompts from "./prompts/index.js";
|
|
23
26
|
export { intro, introPlain, outro, note, select, multiselect, text as promptText, confirm, confirmOrCancel, password, spinner, withSpinner, isCancel, cancel, log, PromptCancelledError } from "./prompts/index.js";
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "tsc",
|
|
9
|
+
"postbuild": "node scripts/smoke-built-exports.cjs",
|
|
9
10
|
"lint": "cd ../.. && eslint packages/design-system --ext ts && tsc -p packages/design-system/tsconfig.json --noEmit",
|
|
10
11
|
"test": "cd ../.. && vitest run $(rg --files packages/design-system/src -g '*.test.ts' | sort | tr '\\n' ' ')",
|
|
11
12
|
"demo": "tsx scripts/demo.ts",
|
|
@@ -140,6 +140,104 @@ await project.move(created.id, { after: "42" });
|
|
|
140
140
|
|
|
141
141
|
`gh-issues` exposes one list named `${project.owner}/${project.number}`. `create()` ignores `TaskCreate.id` because GitHub assigns issue numbers. `fire(state)` writes the Project v2 `Status` field to the matching single-select option. `move()` reorders project items.
|
|
142
142
|
|
|
143
|
+
If the GitHub Project v2 board has not been set up manually, run `poe-code tasks sync <list>` before opening the backend. `openTaskList({ type: "gh-issues" })` no longer requires manual board setup when the board was provisioned with `tasks sync` first.
|
|
144
|
+
|
|
145
|
+
## Verifying and provisioning the GitHub Project v2 board
|
|
146
|
+
|
|
147
|
+
Use `verifyGhProject` to check whether a GitHub Project v2 board has the required Project and `Status` single-select options:
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
import { verifyGhProject } from "@poe-code/task-list";
|
|
151
|
+
|
|
152
|
+
const report = await verifyGhProject({
|
|
153
|
+
owner: "octo-org",
|
|
154
|
+
number: 7,
|
|
155
|
+
requiredStates: ["queued", "agent-running", "human-review", "done", "failed", "archived"],
|
|
156
|
+
auth: { token: "github-token" }
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`VerifyGhProjectOptions` has this shape:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
interface VerifyGhProjectOptions {
|
|
164
|
+
owner: string;
|
|
165
|
+
number: number;
|
|
166
|
+
requiredStates: readonly string[];
|
|
167
|
+
client?: GhClient;
|
|
168
|
+
fetch?: typeof fetch;
|
|
169
|
+
auth?: { token: string };
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`verifyGhProject` returns `Promise<VerifyGhProjectReport>`:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
interface VerifyGhProjectReport {
|
|
177
|
+
ok: boolean;
|
|
178
|
+
project: { id: string; number: number; owner: string } | null;
|
|
179
|
+
statusField: { id: string; options: readonly string[] } | null;
|
|
180
|
+
missingProject: boolean;
|
|
181
|
+
missingStatusField: boolean;
|
|
182
|
+
missingOptions: readonly string[];
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Use `syncGhProject` to provision anything missing:
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
import { syncGhProject } from "@poe-code/task-list";
|
|
190
|
+
|
|
191
|
+
const report = await syncGhProject({
|
|
192
|
+
owner: "octo-org",
|
|
193
|
+
number: 7,
|
|
194
|
+
requiredStates: ["queued", "agent-running", "human-review", "done", "failed", "archived"],
|
|
195
|
+
title: "Delivery Board",
|
|
196
|
+
yes: true,
|
|
197
|
+
auth: { token: "github-token" }
|
|
198
|
+
});
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`SyncGhProjectOptions` extends `VerifyGhProjectOptions`:
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
interface SyncGhProjectOptions extends VerifyGhProjectOptions {
|
|
205
|
+
title?: string;
|
|
206
|
+
yes?: boolean;
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
`syncGhProject` returns `Promise<SyncGhProjectReport>`:
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
interface SyncGhProjectReport extends VerifyGhProjectReport {
|
|
214
|
+
created: readonly string[];
|
|
215
|
+
updated: readonly string[];
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The CLI exposes the same verification and provisioning flow:
|
|
220
|
+
|
|
221
|
+
```sh
|
|
222
|
+
poe-code tasks verify <list> --workflow ./WORKFLOW.md --repo octo-org/octo-repo --project octo-org/7 --states queued,agent-running,human-review,done,failed,archived --json
|
|
223
|
+
poe-code tasks sync <list> --workflow ./WORKFLOW.md --repo octo-org/octo-repo --project octo-org/7 --states queued,agent-running,human-review,done,failed,archived --json --yes
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`<list>` and `--project` both use `<owner>/<number>` project syntax. `--workflow` defaults to `./WORKFLOW.md`. `--repo` overrides the task repository from workflow frontmatter. `--states` overrides the required state list from workflow frontmatter. `--json` prints the report object as JSON. `--yes` confirms non-interactive sync; it is only used by `poe-code tasks sync`.
|
|
227
|
+
|
|
228
|
+
| Option | Commands | Behavior |
|
|
229
|
+
| --- | --- | --- |
|
|
230
|
+
| `--workflow <path>` | `verify`, `sync` | Workflow file path. Defaults to `./WORKFLOW.md`. |
|
|
231
|
+
| `--repo <owner/name>` | `verify`, `sync` | GitHub repository owner/name. |
|
|
232
|
+
| `--project <owner/number>` | `verify`, `sync` | GitHub Project v2 owner/number. Overrides `<list>`. |
|
|
233
|
+
| `--states <csv>` | `verify`, `sync` | Required task state names. |
|
|
234
|
+
| `--json` | `verify`, `sync` | Prints the report as JSON. |
|
|
235
|
+
| `--yes` | `sync` | Confirms non-interactive provisioning. |
|
|
236
|
+
|
|
237
|
+
The `Status` field name and option names are matched case-sensitively. The field must be named `Status`, and required option names must match exactly. For example, `status` is treated as a missing field, and `Done` is treated as missing when the required state is `done`.
|
|
238
|
+
|
|
239
|
+
In v1, if sync creates a new GitHub Project v2 board, the CLI prints the new project number and does not rewrite `WORKFLOW.md`. The operator must update `WORKFLOW.md` by hand with the printed `<owner>/<number>`.
|
|
240
|
+
|
|
143
241
|
## Notes
|
|
144
242
|
|
|
145
243
|
The package never overwrites existing task files or store files. `defaults.metadata` is applied only when creating new tasks and does not retroactively update existing tasks.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type GhClient } from "./gh-issues-client.js";
|
|
2
|
+
export interface VerifyGhProjectOptions {
|
|
3
|
+
owner: string;
|
|
4
|
+
number: number;
|
|
5
|
+
requiredStates: readonly string[];
|
|
6
|
+
client?: GhClient;
|
|
7
|
+
fetch?: typeof fetch;
|
|
8
|
+
auth?: {
|
|
9
|
+
token: string;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export interface VerifyGhProjectReport {
|
|
13
|
+
ok: boolean;
|
|
14
|
+
project: {
|
|
15
|
+
id: string;
|
|
16
|
+
number: number;
|
|
17
|
+
owner: string;
|
|
18
|
+
} | null;
|
|
19
|
+
statusField: {
|
|
20
|
+
id: string;
|
|
21
|
+
options: readonly string[];
|
|
22
|
+
} | null;
|
|
23
|
+
missingProject: boolean;
|
|
24
|
+
missingStatusField: boolean;
|
|
25
|
+
missingOptions: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
export interface SyncGhProjectOptions extends VerifyGhProjectOptions {
|
|
28
|
+
title?: string;
|
|
29
|
+
yes?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface SyncGhProjectReport extends VerifyGhProjectReport {
|
|
32
|
+
created: readonly string[];
|
|
33
|
+
updated: readonly string[];
|
|
34
|
+
}
|
|
35
|
+
export declare class GhProjectSyncError extends Error {
|
|
36
|
+
readonly op: "lookup" | "createProject" | "createField" | "createOption";
|
|
37
|
+
readonly target: string;
|
|
38
|
+
constructor(options: {
|
|
39
|
+
op: "lookup" | "createProject" | "createField" | "createOption";
|
|
40
|
+
target: string;
|
|
41
|
+
cause?: unknown;
|
|
42
|
+
message: string;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
export declare function verifyGhProject(opts: VerifyGhProjectOptions): Promise<VerifyGhProjectReport>;
|
|
46
|
+
export declare function syncGhProject(opts: SyncGhProjectOptions): Promise<SyncGhProjectReport>;
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { PROJECT_ORGANIZATION_QUERY, PROJECT_USER_QUERY } from "./gh-issues.js";
|
|
2
|
+
import { createGhClient } from "./gh-issues-client.js";
|
|
3
|
+
const OWNER_ORGANIZATION_QUERY = `query ProjectOwner($owner: String!) {
|
|
4
|
+
organization(login: $owner) {
|
|
5
|
+
id
|
|
6
|
+
}
|
|
7
|
+
}`;
|
|
8
|
+
const OWNER_USER_QUERY = `query ProjectOwner($owner: String!) {
|
|
9
|
+
user(login: $owner) {
|
|
10
|
+
id
|
|
11
|
+
}
|
|
12
|
+
}`;
|
|
13
|
+
const CREATE_PROJECT_MUTATION = `mutation CreateProject($input: CreateProjectV2Input!) {
|
|
14
|
+
createProjectV2(input: $input) {
|
|
15
|
+
projectV2 {
|
|
16
|
+
id
|
|
17
|
+
number
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}`;
|
|
21
|
+
const CREATE_STATUS_FIELD_MUTATION = `mutation CreateStatusField($input: CreateProjectV2FieldInput!) {
|
|
22
|
+
createProjectV2Field(input: $input) {
|
|
23
|
+
projectV2Field {
|
|
24
|
+
... on ProjectV2SingleSelectField {
|
|
25
|
+
id
|
|
26
|
+
name
|
|
27
|
+
options { id name }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}`;
|
|
32
|
+
const CREATE_STATUS_OPTION_MUTATION = `mutation CreateStatusOption($input: CreateProjectV2SingleSelectFieldOptionInput!) {
|
|
33
|
+
createProjectV2SingleSelectFieldOption(input: $input) {
|
|
34
|
+
singleSelectFieldOption {
|
|
35
|
+
id
|
|
36
|
+
name
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}`;
|
|
40
|
+
export class GhProjectSyncError extends Error {
|
|
41
|
+
op;
|
|
42
|
+
target;
|
|
43
|
+
constructor(options) {
|
|
44
|
+
super(options.message, { cause: options.cause });
|
|
45
|
+
this.name = "GhProjectSyncError";
|
|
46
|
+
this.op = options.op;
|
|
47
|
+
this.target = options.target;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export async function verifyGhProject(opts) {
|
|
51
|
+
const client = resolveGhClient(opts);
|
|
52
|
+
const target = `project:${opts.owner}/${opts.number}`;
|
|
53
|
+
const variables = {
|
|
54
|
+
owner: opts.owner,
|
|
55
|
+
number: opts.number
|
|
56
|
+
};
|
|
57
|
+
let project;
|
|
58
|
+
try {
|
|
59
|
+
const organizationResult = await client.graphql(PROJECT_ORGANIZATION_QUERY, variables);
|
|
60
|
+
project = organizationResult.organization?.projectV2 ?? null;
|
|
61
|
+
if (project === null) {
|
|
62
|
+
const userResult = await client.graphql(PROJECT_USER_QUERY, variables);
|
|
63
|
+
project = userResult.user?.projectV2 ?? null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
throw new GhProjectSyncError({
|
|
68
|
+
op: "lookup",
|
|
69
|
+
target,
|
|
70
|
+
cause: error,
|
|
71
|
+
message: "lookup_failed"
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (project === null) {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
project: null,
|
|
78
|
+
statusField: null,
|
|
79
|
+
missingProject: true,
|
|
80
|
+
missingStatusField: true,
|
|
81
|
+
missingOptions: opts.requiredStates
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const statusField = selectStatusField(project);
|
|
85
|
+
if (statusField === null) {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
project: {
|
|
89
|
+
id: project.id,
|
|
90
|
+
number: opts.number,
|
|
91
|
+
owner: opts.owner
|
|
92
|
+
},
|
|
93
|
+
statusField: null,
|
|
94
|
+
missingProject: false,
|
|
95
|
+
missingStatusField: true,
|
|
96
|
+
missingOptions: opts.requiredStates
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const options = statusField.options.map((option) => option.name);
|
|
100
|
+
const missingOptions = opts.requiredStates.filter((state) => !options.includes(state));
|
|
101
|
+
return {
|
|
102
|
+
ok: missingOptions.length === 0,
|
|
103
|
+
project: {
|
|
104
|
+
id: project.id,
|
|
105
|
+
number: opts.number,
|
|
106
|
+
owner: opts.owner
|
|
107
|
+
},
|
|
108
|
+
statusField: {
|
|
109
|
+
id: statusField.id,
|
|
110
|
+
options
|
|
111
|
+
},
|
|
112
|
+
missingProject: false,
|
|
113
|
+
missingStatusField: false,
|
|
114
|
+
missingOptions
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
export async function syncGhProject(opts) {
|
|
118
|
+
const client = resolveGhClient(opts);
|
|
119
|
+
const verified = await verifyGhProject({ ...opts, client });
|
|
120
|
+
const created = [];
|
|
121
|
+
if (verified.ok) {
|
|
122
|
+
return {
|
|
123
|
+
...verified,
|
|
124
|
+
created,
|
|
125
|
+
updated: []
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
let project = verified.project;
|
|
129
|
+
let statusField = verified.statusField;
|
|
130
|
+
let missingOptions = [...verified.missingOptions];
|
|
131
|
+
if (project === null) {
|
|
132
|
+
project = await createProject(client, opts);
|
|
133
|
+
created.push("project");
|
|
134
|
+
statusField = null;
|
|
135
|
+
missingOptions = [...opts.requiredStates];
|
|
136
|
+
}
|
|
137
|
+
if (statusField === null) {
|
|
138
|
+
const createdStatusField = await createStatusField(client, project.id);
|
|
139
|
+
statusField = createdStatusField;
|
|
140
|
+
created.push("field");
|
|
141
|
+
missingOptions = opts.requiredStates.filter((state) => !createdStatusField.options.includes(state));
|
|
142
|
+
}
|
|
143
|
+
if (missingOptions.length > 0) {
|
|
144
|
+
for (const optionName of missingOptions) {
|
|
145
|
+
await createStatusOption(client, statusField.id, optionName);
|
|
146
|
+
created.push(`option:${optionName}`);
|
|
147
|
+
}
|
|
148
|
+
statusField = {
|
|
149
|
+
id: statusField.id,
|
|
150
|
+
options: [...statusField.options, ...missingOptions]
|
|
151
|
+
};
|
|
152
|
+
missingOptions = [];
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
ok: statusField !== null && missingOptions.length === 0,
|
|
156
|
+
project,
|
|
157
|
+
statusField,
|
|
158
|
+
missingProject: false,
|
|
159
|
+
missingStatusField: statusField === null,
|
|
160
|
+
missingOptions,
|
|
161
|
+
created,
|
|
162
|
+
updated: []
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
async function createProject(client, opts) {
|
|
166
|
+
const target = `${opts.owner}/${opts.number}`;
|
|
167
|
+
try {
|
|
168
|
+
const ownerId = await lookupOwnerId(client, opts.owner);
|
|
169
|
+
const result = await client.graphql(CREATE_PROJECT_MUTATION, {
|
|
170
|
+
input: {
|
|
171
|
+
ownerId,
|
|
172
|
+
title: opts.title ?? `${opts.owner}/${opts.number}`
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
const project = result.createProjectV2?.projectV2;
|
|
176
|
+
if (project === undefined || project === null) {
|
|
177
|
+
throw new Error("createProjectV2 returned no project");
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
id: project.id,
|
|
181
|
+
number: project.number,
|
|
182
|
+
owner: opts.owner
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
throw new GhProjectSyncError({
|
|
187
|
+
op: "createProject",
|
|
188
|
+
target,
|
|
189
|
+
cause: error,
|
|
190
|
+
message: errorMessage(error)
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async function lookupOwnerId(client, owner) {
|
|
195
|
+
const organizationResult = await client.graphql(OWNER_ORGANIZATION_QUERY, {
|
|
196
|
+
owner
|
|
197
|
+
});
|
|
198
|
+
const organizationId = organizationResult.organization?.id;
|
|
199
|
+
if (organizationId !== undefined) {
|
|
200
|
+
return organizationId;
|
|
201
|
+
}
|
|
202
|
+
const userResult = await client.graphql(OWNER_USER_QUERY, {
|
|
203
|
+
owner
|
|
204
|
+
});
|
|
205
|
+
const userId = userResult.user?.id;
|
|
206
|
+
if (userId !== undefined) {
|
|
207
|
+
return userId;
|
|
208
|
+
}
|
|
209
|
+
throw new Error(`GitHub owner not found: ${owner}`);
|
|
210
|
+
}
|
|
211
|
+
async function createStatusField(client, projectId) {
|
|
212
|
+
try {
|
|
213
|
+
const result = await client.graphql(CREATE_STATUS_FIELD_MUTATION, {
|
|
214
|
+
input: {
|
|
215
|
+
projectId,
|
|
216
|
+
dataType: "SINGLE_SELECT",
|
|
217
|
+
name: "Status",
|
|
218
|
+
singleSelectOptions: []
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
const field = result.createProjectV2Field?.projectV2Field;
|
|
222
|
+
if (!isStatusField(field)) {
|
|
223
|
+
throw new Error("createProjectV2Field returned no Status field");
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
id: field.id,
|
|
227
|
+
options: field.options.map((option) => option.name)
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
throw new GhProjectSyncError({
|
|
232
|
+
op: "createField",
|
|
233
|
+
target: "Status",
|
|
234
|
+
cause: error,
|
|
235
|
+
message: errorMessage(error)
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
async function createStatusOption(client, fieldId, name) {
|
|
240
|
+
try {
|
|
241
|
+
await client.graphql(CREATE_STATUS_OPTION_MUTATION, {
|
|
242
|
+
input: {
|
|
243
|
+
fieldId,
|
|
244
|
+
name,
|
|
245
|
+
color: "GRAY"
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
throw new GhProjectSyncError({
|
|
251
|
+
op: "createOption",
|
|
252
|
+
target: name,
|
|
253
|
+
cause: error,
|
|
254
|
+
message: errorMessage(error)
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function errorMessage(error) {
|
|
259
|
+
return error instanceof Error ? error.message : String(error);
|
|
260
|
+
}
|
|
261
|
+
function resolveGhClient(opts) {
|
|
262
|
+
if (opts.client !== undefined) {
|
|
263
|
+
return opts.client;
|
|
264
|
+
}
|
|
265
|
+
const token = opts.auth?.token;
|
|
266
|
+
if (token === undefined || token.length === 0) {
|
|
267
|
+
throw new GhProjectSyncError({
|
|
268
|
+
op: "lookup",
|
|
269
|
+
target: "auth",
|
|
270
|
+
message: "missing_auth"
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
return createGhClient({
|
|
274
|
+
token,
|
|
275
|
+
fetch: opts.fetch
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
function selectStatusField(project) {
|
|
279
|
+
const fields = project.fields?.nodes?.filter(isStatusField) ?? [];
|
|
280
|
+
const exactStatusField = fields.find((field) => field.name === "Status");
|
|
281
|
+
if (exactStatusField !== undefined) {
|
|
282
|
+
return exactStatusField;
|
|
283
|
+
}
|
|
284
|
+
if (isStatusField(project.field) && isExactStatusField(project.field)) {
|
|
285
|
+
return project.field;
|
|
286
|
+
}
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
function isExactStatusField(field) {
|
|
290
|
+
return field.name === undefined || field.name === "Status";
|
|
291
|
+
}
|
|
292
|
+
function isStatusOption(value) {
|
|
293
|
+
return (typeof value === "object" &&
|
|
294
|
+
value !== null &&
|
|
295
|
+
"id" in value &&
|
|
296
|
+
typeof value.id === "string" &&
|
|
297
|
+
"name" in value &&
|
|
298
|
+
typeof value.name === "string");
|
|
299
|
+
}
|
|
300
|
+
function isStatusField(value) {
|
|
301
|
+
return (typeof value === "object" &&
|
|
302
|
+
value !== null &&
|
|
303
|
+
"id" in value &&
|
|
304
|
+
typeof value.id === "string" &&
|
|
305
|
+
(!("name" in value) || typeof value.name === "string") &&
|
|
306
|
+
"options" in value &&
|
|
307
|
+
Array.isArray(value.options) &&
|
|
308
|
+
value.options.every(isStatusOption));
|
|
309
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { TaskDefaults, TaskList } from "../types.js";
|
|
2
|
+
export declare const PROJECT_ORGANIZATION_QUERY = "query Project($owner: String!, $number: Int!) {\n organization(login: $owner) {\n projectV2(number: $number) {\n id\n title\n field(name: \"Status\") {\n ... on ProjectV2SingleSelectField {\n id\n name\n options { id name }\n }\n }\n fields(first: 100) {\n nodes {\n ... on ProjectV2SingleSelectField {\n id\n name\n options { id name }\n }\n }\n }\n }\n }\n}";
|
|
3
|
+
export declare const PROJECT_USER_QUERY = "query Project($owner: String!, $number: Int!) {\n user(login: $owner) {\n projectV2(number: $number) {\n id\n title\n field(name: \"Status\") {\n ... on ProjectV2SingleSelectField {\n id\n name\n options { id name }\n }\n }\n fields(first: 100) {\n nodes {\n ... on ProjectV2SingleSelectField {\n id\n name\n options { id name }\n }\n }\n }\n }\n }\n}";
|
|
2
4
|
export interface GhIssuesBackendDeps {
|
|
3
5
|
repo: string;
|
|
4
6
|
project: {
|
|
@@ -2,7 +2,7 @@ import { eventsFromState, findEvent } from "../state-machine.js";
|
|
|
2
2
|
import { AnchorNotFoundError, InvalidTransitionError, OrderMismatchError, TaskNotFoundError } from "../types.js";
|
|
3
3
|
import { createGhClient } from "./gh-issues-client.js";
|
|
4
4
|
import { applyOrder, sortTasks } from "./utils.js";
|
|
5
|
-
const PROJECT_ORGANIZATION_QUERY = `query Project($owner: String!, $number: Int!) {
|
|
5
|
+
export const PROJECT_ORGANIZATION_QUERY = `query Project($owner: String!, $number: Int!) {
|
|
6
6
|
organization(login: $owner) {
|
|
7
7
|
projectV2(number: $number) {
|
|
8
8
|
id
|
|
@@ -10,13 +10,23 @@ const PROJECT_ORGANIZATION_QUERY = `query Project($owner: String!, $number: Int!
|
|
|
10
10
|
field(name: "Status") {
|
|
11
11
|
... on ProjectV2SingleSelectField {
|
|
12
12
|
id
|
|
13
|
+
name
|
|
13
14
|
options { id name }
|
|
14
15
|
}
|
|
15
16
|
}
|
|
17
|
+
fields(first: 100) {
|
|
18
|
+
nodes {
|
|
19
|
+
... on ProjectV2SingleSelectField {
|
|
20
|
+
id
|
|
21
|
+
name
|
|
22
|
+
options { id name }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
16
26
|
}
|
|
17
27
|
}
|
|
18
28
|
}`;
|
|
19
|
-
const PROJECT_USER_QUERY = `query Project($owner: String!, $number: Int!) {
|
|
29
|
+
export const PROJECT_USER_QUERY = `query Project($owner: String!, $number: Int!) {
|
|
20
30
|
user(login: $owner) {
|
|
21
31
|
projectV2(number: $number) {
|
|
22
32
|
id
|
|
@@ -24,9 +34,19 @@ const PROJECT_USER_QUERY = `query Project($owner: String!, $number: Int!) {
|
|
|
24
34
|
field(name: "Status") {
|
|
25
35
|
... on ProjectV2SingleSelectField {
|
|
26
36
|
id
|
|
37
|
+
name
|
|
27
38
|
options { id name }
|
|
28
39
|
}
|
|
29
40
|
}
|
|
41
|
+
fields(first: 100) {
|
|
42
|
+
nodes {
|
|
43
|
+
... on ProjectV2SingleSelectField {
|
|
44
|
+
id
|
|
45
|
+
name
|
|
46
|
+
options { id name }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
30
50
|
}
|
|
31
51
|
}
|
|
32
52
|
}`;
|