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.
Files changed (91) hide show
  1. package/dist/cli.d.ts +2 -0
  2. package/dist/cli.js +833 -124
  3. package/dist/error-report.d.ts +39 -0
  4. package/dist/error-report.js +330 -0
  5. package/dist/human-in-loop/approval-tasks.js +11 -8
  6. package/dist/human-in-loop/approvals-commands.js +21 -20
  7. package/dist/human-in-loop/default-provider.js +5 -3
  8. package/dist/human-in-loop/runner.js +45 -4
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.js +55 -35
  11. package/dist/json-schema-converter.d.ts +1 -0
  12. package/dist/json-schema-converter.js +102 -52
  13. package/dist/mcp-proxy.d.ts +1 -0
  14. package/dist/mcp-proxy.js +13 -6
  15. package/dist/mcp.d.ts +2 -0
  16. package/dist/mcp.js +131 -55
  17. package/dist/sdk.d.ts +4 -2
  18. package/dist/sdk.js +132 -48
  19. package/dist/source-snippet.d.ts +8 -0
  20. package/dist/source-snippet.js +42 -0
  21. package/dist/stack-trim.d.ts +4 -0
  22. package/dist/stack-trim.js +70 -0
  23. package/dist/suggest.d.ts +4 -0
  24. package/dist/suggest.js +46 -0
  25. package/dist/user-error.d.ts +3 -0
  26. package/dist/user-error.js +7 -1
  27. package/dist/validation-errors.d.ts +5 -0
  28. package/dist/validation-errors.js +18 -0
  29. package/node_modules/@poe-code/design-system/dist/components/help-formatter-plain.d.ts +1 -0
  30. package/node_modules/@poe-code/design-system/dist/components/help-formatter-plain.js +1 -1
  31. package/node_modules/@poe-code/design-system/dist/components/text.d.ts +1 -0
  32. package/node_modules/@poe-code/design-system/dist/components/text.js +8 -0
  33. package/node_modules/@poe-code/design-system/dist/dashboard/buffer.js +8 -1
  34. package/node_modules/@poe-code/design-system/dist/dashboard/keymap.d.ts +5 -0
  35. package/node_modules/@poe-code/design-system/dist/dashboard/keymap.js +146 -12
  36. package/node_modules/@poe-code/design-system/dist/dashboard/terminal.js +31 -0
  37. package/node_modules/@poe-code/design-system/dist/dashboard/types.d.ts +1 -0
  38. package/node_modules/@poe-code/design-system/dist/explorer/actions.d.ts +16 -0
  39. package/node_modules/@poe-code/design-system/dist/explorer/actions.js +39 -0
  40. package/node_modules/@poe-code/design-system/dist/explorer/demo.d.ts +13 -0
  41. package/node_modules/@poe-code/design-system/dist/explorer/demo.js +297 -0
  42. package/node_modules/@poe-code/design-system/dist/explorer/events.d.ts +61 -0
  43. package/node_modules/@poe-code/design-system/dist/explorer/events.js +1 -0
  44. package/node_modules/@poe-code/design-system/dist/explorer/filter.d.ts +10 -0
  45. package/node_modules/@poe-code/design-system/dist/explorer/filter.js +95 -0
  46. package/node_modules/@poe-code/design-system/dist/explorer/index.d.ts +8 -0
  47. package/node_modules/@poe-code/design-system/dist/explorer/index.js +8 -0
  48. package/node_modules/@poe-code/design-system/dist/explorer/jobs.d.ts +7 -0
  49. package/node_modules/@poe-code/design-system/dist/explorer/jobs.js +59 -0
  50. package/node_modules/@poe-code/design-system/dist/explorer/keymap.d.ts +21 -0
  51. package/node_modules/@poe-code/design-system/dist/explorer/keymap.js +363 -0
  52. package/node_modules/@poe-code/design-system/dist/explorer/layout.d.ts +20 -0
  53. package/node_modules/@poe-code/design-system/dist/explorer/layout.js +73 -0
  54. package/node_modules/@poe-code/design-system/dist/explorer/reducer.d.ts +9 -0
  55. package/node_modules/@poe-code/design-system/dist/explorer/reducer.js +704 -0
  56. package/node_modules/@poe-code/design-system/dist/explorer/render/detail.d.ts +4 -0
  57. package/node_modules/@poe-code/design-system/dist/explorer/render/detail.js +96 -0
  58. package/node_modules/@poe-code/design-system/dist/explorer/render/footer.d.ts +4 -0
  59. package/node_modules/@poe-code/design-system/dist/explorer/render/footer.js +49 -0
  60. package/node_modules/@poe-code/design-system/dist/explorer/render/header.d.ts +4 -0
  61. package/node_modules/@poe-code/design-system/dist/explorer/render/header.js +56 -0
  62. package/node_modules/@poe-code/design-system/dist/explorer/render/index.d.ts +8 -0
  63. package/node_modules/@poe-code/design-system/dist/explorer/render/index.js +61 -0
  64. package/node_modules/@poe-code/design-system/dist/explorer/render/list.d.ts +4 -0
  65. package/node_modules/@poe-code/design-system/dist/explorer/render/list.js +106 -0
  66. package/node_modules/@poe-code/design-system/dist/explorer/render/modal.d.ts +3 -0
  67. package/node_modules/@poe-code/design-system/dist/explorer/render/modal.js +91 -0
  68. package/node_modules/@poe-code/design-system/dist/explorer/render/test-fixtures.d.ts +8 -0
  69. package/node_modules/@poe-code/design-system/dist/explorer/render/test-fixtures.js +156 -0
  70. package/node_modules/@poe-code/design-system/dist/explorer/runtime.d.ts +2 -0
  71. package/node_modules/@poe-code/design-system/dist/explorer/runtime.js +282 -0
  72. package/node_modules/@poe-code/design-system/dist/explorer/runtime.test-helpers.d.ts +50 -0
  73. package/node_modules/@poe-code/design-system/dist/explorer/runtime.test-helpers.js +101 -0
  74. package/node_modules/@poe-code/design-system/dist/explorer/state.d.ts +130 -0
  75. package/node_modules/@poe-code/design-system/dist/explorer/state.js +87 -0
  76. package/node_modules/@poe-code/design-system/dist/explorer/theme.d.ts +27 -0
  77. package/node_modules/@poe-code/design-system/dist/explorer/theme.js +97 -0
  78. package/node_modules/@poe-code/design-system/dist/index.d.ts +3 -0
  79. package/node_modules/@poe-code/design-system/dist/index.js +3 -0
  80. package/node_modules/@poe-code/design-system/package.json +1 -0
  81. package/node_modules/@poe-code/task-list/README.md +98 -0
  82. package/node_modules/@poe-code/task-list/dist/backends/gh-issues-sync.d.ts +46 -0
  83. package/node_modules/@poe-code/task-list/dist/backends/gh-issues-sync.js +309 -0
  84. package/node_modules/@poe-code/task-list/dist/backends/gh-issues.d.ts +2 -0
  85. package/node_modules/@poe-code/task-list/dist/backends/gh-issues.js +22 -2
  86. package/node_modules/@poe-code/task-list/dist/backends/markdown-dir.js +266 -99
  87. package/node_modules/@poe-code/task-list/dist/index.d.ts +1 -0
  88. package/node_modules/@poe-code/task-list/dist/index.js +1 -0
  89. package/node_modules/@poe-code/task-list/dist/open.js +3 -0
  90. package/node_modules/@poe-code/task-list/dist/types.d.ts +4 -0
  91. 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
  }`;