@shzlwio/windrunner-cli 1.0.0

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/client.js ADDED
@@ -0,0 +1,104 @@
1
+ export class CliError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = "CliError";
5
+ }
6
+ }
7
+ export class WindrunnerClient {
8
+ options;
9
+ apiBaseUrl;
10
+ constructor(options) {
11
+ this.options = options;
12
+ const baseUrl = options.url.replace(/\/+$/, "");
13
+ this.apiBaseUrl = baseUrl.endsWith("/api/v1") ? baseUrl : `${baseUrl}/api/v1`;
14
+ }
15
+ async get(path) {
16
+ return this.request("GET", path);
17
+ }
18
+ async post(path, body) {
19
+ return this.mutate("POST", path, body);
20
+ }
21
+ async put(path, body) {
22
+ return this.mutate("PUT", path, body);
23
+ }
24
+ async delete(path) {
25
+ return this.mutate("DELETE", path);
26
+ }
27
+ async mutate(method, path, body) {
28
+ if (this.options.dryRun) {
29
+ return {
30
+ dryRun: true,
31
+ method,
32
+ path: `${this.apiBaseUrl}${path}`,
33
+ ...(body === undefined ? {} : { body }),
34
+ };
35
+ }
36
+ return this.request(method, path, body);
37
+ }
38
+ async request(method, path, body) {
39
+ const apiKey = process.env.WINDRUNNER_API_KEY;
40
+ if (!apiKey) {
41
+ throw new CliError("WINDRUNNER_API_KEY is required for API requests.");
42
+ }
43
+ const headers = {
44
+ Accept: "application/json",
45
+ Authorization: `Bearer ${apiKey}`,
46
+ };
47
+ if (body !== undefined) {
48
+ headers["Content-Type"] = "application/json";
49
+ }
50
+ let response;
51
+ try {
52
+ response = await fetch(`${this.apiBaseUrl}${path}`, {
53
+ method,
54
+ headers,
55
+ body: body === undefined ? undefined : JSON.stringify(body),
56
+ });
57
+ }
58
+ catch (error) {
59
+ const message = error instanceof Error ? error.message : String(error);
60
+ throw new CliError(`Could not connect to Windrunner: ${message}`);
61
+ }
62
+ const text = await response.text();
63
+ let payload = null;
64
+ if (text.trim()) {
65
+ try {
66
+ payload = JSON.parse(text);
67
+ }
68
+ catch {
69
+ throw new CliError(`Windrunner returned invalid JSON (${response.status}).`);
70
+ }
71
+ }
72
+ if (!response.ok) {
73
+ throw new CliError(formatApiFailure(response.status, payload));
74
+ }
75
+ if (isApiResponse(payload) && payload.errors && payload.errors.length > 0) {
76
+ throw new CliError(formatApiErrors(payload.errors));
77
+ }
78
+ if (!isApiResponse(payload)) {
79
+ return { data: payload };
80
+ }
81
+ return payload;
82
+ }
83
+ }
84
+ function isApiResponse(value) {
85
+ return Boolean(value && typeof value === "object" && ("data" in value || "errors" in value || "meta" in value));
86
+ }
87
+ function formatApiFailure(status, payload) {
88
+ if (isApiResponse(payload) && payload.errors?.length) {
89
+ return `Request failed (${status}): ${formatApiErrors(payload.errors)}`;
90
+ }
91
+ if (payload && typeof payload === "object" && "message" in payload && typeof payload.message === "string") {
92
+ return `Request failed (${status}): ${payload.message}`;
93
+ }
94
+ return `Request failed (${status}).`;
95
+ }
96
+ function formatApiErrors(errors) {
97
+ return errors
98
+ .map((error) => {
99
+ const prefix = error.code ? `${error.code}: ` : "";
100
+ const field = error.field ? ` (${error.field})` : "";
101
+ return `${prefix}${error.message ?? "Unknown API error"}${field}`;
102
+ })
103
+ .join("; ");
104
+ }
package/dist/index.js ADDED
@@ -0,0 +1,372 @@
1
+ #!/usr/bin/env node
2
+ import { createInterface } from "node:readline/promises";
3
+ import { stdin as input, stderr as output } from "node:process";
4
+ import { Command } from "commander";
5
+ import { CliError, WindrunnerClient } from "./client.js";
6
+ function getGlobalOptions(command) {
7
+ const options = command.optsWithGlobals();
8
+ return {
9
+ url: options.url || process.env.WINDRUNNER_URL || "http://localhost:8080",
10
+ json: Boolean(options.json),
11
+ dryRun: Boolean(options.dryRun),
12
+ yes: Boolean(options.yes),
13
+ };
14
+ }
15
+ function printResult(value, options) {
16
+ if (options.json) {
17
+ console.log(JSON.stringify(value));
18
+ return;
19
+ }
20
+ console.log(JSON.stringify(value, null, 2));
21
+ }
22
+ function printResponse(response, options) {
23
+ if (isDryRunResult(response)) {
24
+ printResult(response, options);
25
+ return;
26
+ }
27
+ printResult(response.data, options);
28
+ }
29
+ function isDryRunResult(value) {
30
+ return "dryRun" in value && value.dryRun === true;
31
+ }
32
+ function encode(value) {
33
+ return encodeURIComponent(value);
34
+ }
35
+ function numberValue(value, name) {
36
+ if (value === undefined)
37
+ return undefined;
38
+ const parsed = Number(value);
39
+ if (!Number.isInteger(parsed) || parsed < 0) {
40
+ throw new CliError(`--${name} must be a non-negative integer.`);
41
+ }
42
+ return parsed;
43
+ }
44
+ function queryString(parameters) {
45
+ const query = new URLSearchParams();
46
+ for (const [key, value] of Object.entries(parameters)) {
47
+ if (value !== undefined && value !== "") {
48
+ query.set(key, String(value));
49
+ }
50
+ }
51
+ const encoded = query.toString();
52
+ return encoded ? `?${encoded}` : "";
53
+ }
54
+ function collectOption(value, previous = []) {
55
+ return [...previous, value];
56
+ }
57
+ function parseAssignees(values) {
58
+ if (values === undefined)
59
+ return undefined;
60
+ return values.map((value) => {
61
+ const separator = value.indexOf(":");
62
+ if (separator <= 0 || separator === value.length - 1) {
63
+ throw new CliError(`Invalid assignee '${value}'. Use USER:<id> or TEAM:<id>.`);
64
+ }
65
+ const assigneeType = value.slice(0, separator).toUpperCase();
66
+ const assigneeId = value.slice(separator + 1).trim();
67
+ if (assigneeType !== "USER" && assigneeType !== "TEAM") {
68
+ throw new CliError(`Invalid assignee type '${assigneeType}'. Use USER or TEAM.`);
69
+ }
70
+ if (!assigneeId) {
71
+ throw new CliError("Assignee id is required.");
72
+ }
73
+ return { assigneeType, assigneeId };
74
+ });
75
+ }
76
+ function requireText(value, optionName) {
77
+ if (!value || !value.trim()) {
78
+ throw new CliError(`${optionName} is required.`);
79
+ }
80
+ return value.trim();
81
+ }
82
+ function validateDate(value) {
83
+ if (value === undefined)
84
+ return undefined;
85
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
86
+ throw new CliError("--due-date must use YYYY-MM-DD format.");
87
+ }
88
+ return value;
89
+ }
90
+ function workItemFields(options, current) {
91
+ const title = options.title === undefined ? current?.title : String(options.title);
92
+ const result = {
93
+ title: requireText(title, "--title"),
94
+ };
95
+ const values = [
96
+ ["type", options.type === undefined ? current?.type : options.type],
97
+ ["status", options.status === undefined ? current?.status : options.status],
98
+ ["dueDate", options.dueDate === undefined ? current?.dueDate : validateDate(String(options.dueDate))],
99
+ ["priority", options.priority === undefined ? current?.priority : options.priority],
100
+ [
101
+ "parentWorkItemId",
102
+ options.parentId === undefined ? current?.parentWorkItemId : String(options.parentId),
103
+ ],
104
+ ];
105
+ for (const [key, value] of values) {
106
+ if (value !== undefined) {
107
+ result[key] = value;
108
+ }
109
+ }
110
+ return result;
111
+ }
112
+ async function confirmDelete(message, options) {
113
+ if (options.yes)
114
+ return;
115
+ if (!input.isTTY) {
116
+ throw new CliError(`${message} Re-run with --yes when using a non-interactive terminal.`);
117
+ }
118
+ const readline = createInterface({ input, output });
119
+ try {
120
+ const answer = await readline.question(`${message} [y/N] `);
121
+ if (!/^(y|yes)$/i.test(answer.trim())) {
122
+ throw new CliError("Cancelled.");
123
+ }
124
+ }
125
+ finally {
126
+ readline.close();
127
+ }
128
+ }
129
+ function addPagination(command) {
130
+ return command
131
+ .option("--page <number>", "Page number (zero-based)", "0")
132
+ .option("--size <number>", "Items per page; the server caps this at 100", "50")
133
+ .option("--updated-after <timestamp>", "Only return records updated after an ISO-8601 timestamp (UTC recommended)");
134
+ }
135
+ const sharedHelp = `
136
+ Environment:
137
+ WINDRUNNER_URL Server URL (default: http://localhost:8080)
138
+ WINDRUNNER_API_KEY Bearer API key; keep it in the environment, not in arguments
139
+
140
+ Global options:
141
+ --url <url> Override WINDRUNNER_URL
142
+ --json Print compact machine-readable JSON
143
+ --dry-run Preview a mutation without sending the mutation request
144
+ -y, --yes Skip destructive-operation confirmation prompts
145
+
146
+ Output and safety:
147
+ Normal output is pretty JSON. Use --json when another agent or script will parse it.
148
+ Destructive commands require confirmation unless --yes is explicitly provided.
149
+ A dry run never sends a mutation request and does not require an API key.
150
+ `;
151
+ function addAgentHelp(command, details) {
152
+ return command.addHelpText("after", `${sharedHelp}\n${details.trim()}\n`);
153
+ }
154
+ const program = new Command();
155
+ program
156
+ .name("windrunner")
157
+ .description("Command-line interface for Windrunner")
158
+ .version("0.1.0")
159
+ .option("--url <url>", "Windrunner server URL", process.env.WINDRUNNER_URL || "http://localhost:8080")
160
+ .option("--json", "Print compact JSON output")
161
+ .option("--dry-run", "Preview mutations without sending them")
162
+ .option("-y, --yes", "Skip confirmation prompts");
163
+ addAgentHelp(program, `Quick start:
164
+ export WINDRUNNER_URL=http://localhost:8080
165
+ export WINDRUNNER_API_KEY=your-api-key
166
+ windrunner projects list --json
167
+
168
+ Use '<command> --help' for command-specific arguments and examples.`);
169
+ const projects = program.command("projects").description("Manage projects");
170
+ addAgentHelp(projects, "Permissions: projects:read for both project commands.");
171
+ addAgentHelp(projects
172
+ .command("list")
173
+ .description("List projects visible to the API key")
174
+ .option("--page <number>", "Page number (zero-based)", "0")
175
+ .option("--size <number>", "Items per page; the server caps this at 100", "50"), `Permissions: projects:read
176
+
177
+ Examples:
178
+ windrunner projects list
179
+ windrunner projects list --page 1 --size 25 --json`)
180
+ .action(async (options, command) => {
181
+ const globalOptions = getGlobalOptions(command);
182
+ const client = new WindrunnerClient(globalOptions);
183
+ const response = await client.get(`/projects${queryString({ page: numberValue(options.page, "page"), size: numberValue(options.size, "size") })}`);
184
+ printResponse(response, globalOptions);
185
+ });
186
+ addAgentHelp(projects
187
+ .command("get")
188
+ .description("Get a project")
189
+ .argument("<projectId>", "Project id"), `Permissions: projects:read
190
+
191
+ Example:
192
+ windrunner projects get PROJECT_ID --json`)
193
+ .action(async (projectId, _options, command) => {
194
+ const globalOptions = getGlobalOptions(command);
195
+ const client = new WindrunnerClient(globalOptions);
196
+ printResponse(await client.get(`/projects/${encode(projectId)}`), globalOptions);
197
+ });
198
+ const workItems = program.command("work-items").description("Manage work items");
199
+ addAgentHelp(workItems, "Use --json for machine-readable results. Work item type and status values are validated by the server.");
200
+ addAgentHelp(addPagination(workItems
201
+ .command("list")
202
+ .description("List work items in a project")
203
+ .argument("<projectId>", "Project id")
204
+ .option("--status <status>", "Filter by status")
205
+ .option("--type <type>", "Filter by type")
206
+ .option("--priority <priority>", "Filter by priority")), `Permissions: work_items:read
207
+
208
+ Examples:
209
+ windrunner work-items list PROJECT_ID
210
+ windrunner work-items list PROJECT_ID --status OPEN --size 25 --json`).action(async (projectId, options, command) => {
211
+ const globalOptions = getGlobalOptions(command);
212
+ const client = new WindrunnerClient(globalOptions);
213
+ const response = await client.get(`/projects/${encode(projectId)}/work-items${queryString({
214
+ page: numberValue(options.page, "page"),
215
+ size: numberValue(options.size, "size"),
216
+ status: options.status,
217
+ type: options.type,
218
+ priority: options.priority,
219
+ updated_after: options.updatedAfter,
220
+ })}`);
221
+ printResponse(response, globalOptions);
222
+ });
223
+ addAgentHelp(workItems
224
+ .command("get")
225
+ .description("Get a work item")
226
+ .argument("<workItemId>", "Work item id"), `Permissions: work_items:read
227
+
228
+ Example:
229
+ windrunner work-items get WORK_ITEM_ID --json`)
230
+ .action(async (workItemId, _options, command) => {
231
+ const globalOptions = getGlobalOptions(command);
232
+ const client = new WindrunnerClient(globalOptions);
233
+ printResponse(await client.get(`/work-items/${encode(workItemId)}`), globalOptions);
234
+ });
235
+ function addWorkItemWriteOptions(command) {
236
+ return command
237
+ .requiredOption("--title <title>", "Work item title")
238
+ .option("--type <type>", "Work item type")
239
+ .option("--status <status>", "Work item status")
240
+ .option("--due-date <date>", "Due date in YYYY-MM-DD format")
241
+ .option("--priority <priority>", "Work item priority")
242
+ .option("--parent-id <workItemId>", "Parent work item id")
243
+ .option("--assignee <type:id>", "Assignee in USER:<id> or TEAM:<id> format", collectOption);
244
+ }
245
+ addAgentHelp(addWorkItemWriteOptions(workItems
246
+ .command("create")
247
+ .description("Create a work item")
248
+ .argument("<projectId>", "Project id")), `Permissions: work_items:write
249
+ Required: --title.
250
+ Repeat --assignee for multiple assignments using USER:<id> or TEAM:<id>.
251
+
252
+ Examples:
253
+ windrunner work-items create PROJECT_ID --title "Fix login"
254
+ windrunner work-items create PROJECT_ID --title "Release" --status OPEN --assignee USER:user-1 --dry-run`).action(async (projectId, options, command) => {
255
+ const globalOptions = getGlobalOptions(command);
256
+ const client = new WindrunnerClient(globalOptions);
257
+ const body = {
258
+ workItem: workItemFields(options),
259
+ };
260
+ const assignees = parseAssignees(options.assignee);
261
+ if (assignees !== undefined)
262
+ body.assignees = assignees;
263
+ printResponse(await client.post(`/projects/${encode(projectId)}/work-items`, body), globalOptions);
264
+ });
265
+ addAgentHelp(addWorkItemWriteOptions(workItems
266
+ .command("update")
267
+ .description("Update a work item")
268
+ .argument("<workItemId>", "Work item id")), `Permissions: work_items:read and work_items:write.
269
+ Required: --title. The CLI reads the current item first so omitted fields are preserved.
270
+
271
+ Examples:
272
+ windrunner work-items update WORK_ITEM_ID --title "Updated title"
273
+ windrunner work-items update WORK_ITEM_ID --title "Done" --status DONE --json`).action(async (workItemId, options, command) => {
274
+ const globalOptions = getGlobalOptions(command);
275
+ const client = new WindrunnerClient(globalOptions);
276
+ let current;
277
+ // The API accepts a full WorkItem representation for PUT. Fetching first
278
+ // lets the CLI offer a safe field-oriented update without clearing fields
279
+ // the caller did not mention.
280
+ if (!globalOptions.dryRun) {
281
+ const existing = await client.get(`/work-items/${encode(workItemId)}`);
282
+ current = existing.data?.workItem;
283
+ if (!current)
284
+ throw new CliError("Work item response did not include a work item.");
285
+ }
286
+ const body = {
287
+ workItem: workItemFields(options, current),
288
+ };
289
+ const assignees = parseAssignees(options.assignee);
290
+ if (assignees !== undefined)
291
+ body.assignees = assignees;
292
+ printResponse(await client.put(`/work-items/${encode(workItemId)}`, body), globalOptions);
293
+ });
294
+ addAgentHelp(workItems
295
+ .command("delete")
296
+ .description("Delete a work item and its descendants")
297
+ .argument("<workItemId>", "Work item id"), `Permissions: work_items:write
298
+ This permanently deletes the work item, descendants, entries, and relationships.
299
+ Use --dry-run to preview the request. Use --yes only when deletion is explicitly intended.
300
+
301
+ Examples:
302
+ windrunner work-items delete WORK_ITEM_ID --dry-run
303
+ windrunner work-items delete WORK_ITEM_ID --yes`)
304
+ .action(async (workItemId, _options, command) => {
305
+ const globalOptions = getGlobalOptions(command);
306
+ const client = new WindrunnerClient(globalOptions);
307
+ if (!globalOptions.dryRun) {
308
+ await confirmDelete(`Delete work item ${workItemId}? This cannot be undone.`, globalOptions);
309
+ }
310
+ printResponse(await client.delete(`/work-items/${encode(workItemId)}`), globalOptions);
311
+ });
312
+ const entries = program.command("entries").description("Manage entries");
313
+ addAgentHelp(entries, "Entries are attached to work items. Use --json for machine-readable results.");
314
+ addAgentHelp(addPagination(entries
315
+ .command("list")
316
+ .description("List entries attached to a work item")
317
+ .argument("<workItemId>", "Work item id")), `Permissions: entries:read
318
+
319
+ Example:
320
+ windrunner entries list WORK_ITEM_ID --updated-after 2026-01-01T00:00:00Z --json`).action(async (workItemId, options, command) => {
321
+ const globalOptions = getGlobalOptions(command);
322
+ const client = new WindrunnerClient(globalOptions);
323
+ const response = await client.get(`/work-items/${encode(workItemId)}/entries${queryString({
324
+ page: numberValue(options.page, "page"),
325
+ size: numberValue(options.size, "size"),
326
+ updated_after: options.updatedAfter,
327
+ })}`);
328
+ printResponse(response, globalOptions);
329
+ });
330
+ addAgentHelp(entries
331
+ .command("create")
332
+ .description("Create an entry on a work item")
333
+ .argument("<workItemId>", "Work item id")
334
+ .requiredOption("--body <body>", "Entry body")
335
+ .option("--type <type>", "Entry type"), `Permissions: entries:write
336
+ Required: --body.
337
+
338
+ Examples:
339
+ windrunner entries create WORK_ITEM_ID --body "Deployment completed"
340
+ windrunner entries create WORK_ITEM_ID --body "Comment" --type COMMENT --dry-run`)
341
+ .action(async (workItemId, options, command) => {
342
+ const globalOptions = getGlobalOptions(command);
343
+ const client = new WindrunnerClient(globalOptions);
344
+ const body = {
345
+ body: requireText(options.body, "--body"),
346
+ ...(options.type === undefined ? {} : { type: options.type }),
347
+ };
348
+ printResponse(await client.post(`/work-items/${encode(workItemId)}/entries`, body), globalOptions);
349
+ });
350
+ addAgentHelp(program
351
+ .command("search")
352
+ .description("Search project work items, entries, and relationships")
353
+ .argument("<projectId>", "Project id")
354
+ .argument("<query>", "Search query")
355
+ .option("--limit <number>", "Maximum number of matches"), `Permissions: work_items:read
356
+
357
+ Example:
358
+ windrunner search PROJECT_ID "login failure" --limit 20 --json`)
359
+ .action(async (projectId, query, options, command) => {
360
+ const globalOptions = getGlobalOptions(command);
361
+ const client = new WindrunnerClient(globalOptions);
362
+ const limit = numberValue(options.limit, "limit");
363
+ printResponse(await client.get(`/projects/${encode(projectId)}/search${queryString({ q: query, limit })}`), globalOptions);
364
+ });
365
+ try {
366
+ await program.parseAsync(process.argv);
367
+ }
368
+ catch (error) {
369
+ const message = error instanceof Error ? error.message : String(error);
370
+ console.error(`Error: ${message}`);
371
+ process.exitCode = 1;
372
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@shzlwio/windrunner-cli",
3
+ "version": "1.0.0",
4
+ "description": "Command-line interface for Windrunner.",
5
+ "type": "module",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "bin": {
10
+ "windrunner": "./dist/index.js"
11
+ },
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "dev": "tsx src/index.ts",
15
+ "start": "node dist/index.js",
16
+ "typecheck": "tsc --noEmit"
17
+ },
18
+ "engines": {
19
+ "node": ">=20"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^26.3.0",
23
+ "tsx": "^4.23.12",
24
+ "typescript": "^7.0.2"
25
+ },
26
+ "dependencies": {
27
+ "commander": "^15.0.0"
28
+ }
29
+ }
package/src/client.ts ADDED
@@ -0,0 +1,119 @@
1
+ import type { ApiError, ApiResponse, DryRunResult, GlobalOptions, JsonObject } from "./types.js";
2
+
3
+ export class CliError extends Error {
4
+ constructor(message: string) {
5
+ super(message);
6
+ this.name = "CliError";
7
+ }
8
+ }
9
+
10
+ export class WindrunnerClient {
11
+ private readonly apiBaseUrl: string;
12
+
13
+ constructor(private readonly options: GlobalOptions) {
14
+ const baseUrl = options.url.replace(/\/+$/, "");
15
+ this.apiBaseUrl = baseUrl.endsWith("/api/v1") ? baseUrl : `${baseUrl}/api/v1`;
16
+ }
17
+
18
+ async get<T>(path: string): Promise<ApiResponse<T>> {
19
+ return this.request<T>("GET", path);
20
+ }
21
+
22
+ async post<T>(path: string, body: unknown): Promise<ApiResponse<T> | DryRunResult> {
23
+ return this.mutate<T>("POST", path, body);
24
+ }
25
+
26
+ async put<T>(path: string, body: unknown): Promise<ApiResponse<T> | DryRunResult> {
27
+ return this.mutate<T>("PUT", path, body);
28
+ }
29
+
30
+ async delete(path: string): Promise<ApiResponse<null> | DryRunResult> {
31
+ return this.mutate<null>("DELETE", path);
32
+ }
33
+
34
+ private async mutate<T>(method: string, path: string, body?: unknown): Promise<ApiResponse<T> | DryRunResult> {
35
+ if (this.options.dryRun) {
36
+ return {
37
+ dryRun: true,
38
+ method,
39
+ path: `${this.apiBaseUrl}${path}`,
40
+ ...(body === undefined ? {} : { body }),
41
+ };
42
+ }
43
+ return this.request<T>(method, path, body);
44
+ }
45
+
46
+ private async request<T>(method: string, path: string, body?: unknown): Promise<ApiResponse<T>> {
47
+ const apiKey = process.env.WINDRUNNER_API_KEY;
48
+ if (!apiKey) {
49
+ throw new CliError("WINDRUNNER_API_KEY is required for API requests.");
50
+ }
51
+
52
+ const headers: Record<string, string> = {
53
+ Accept: "application/json",
54
+ Authorization: `Bearer ${apiKey}`,
55
+ };
56
+ if (body !== undefined) {
57
+ headers["Content-Type"] = "application/json";
58
+ }
59
+
60
+ let response: Response;
61
+ try {
62
+ response = await fetch(`${this.apiBaseUrl}${path}`, {
63
+ method,
64
+ headers,
65
+ body: body === undefined ? undefined : JSON.stringify(body),
66
+ });
67
+ } catch (error) {
68
+ const message = error instanceof Error ? error.message : String(error);
69
+ throw new CliError(`Could not connect to Windrunner: ${message}`);
70
+ }
71
+
72
+ const text = await response.text();
73
+ let payload: ApiResponse<T> | JsonObject | null = null;
74
+ if (text.trim()) {
75
+ try {
76
+ payload = JSON.parse(text) as ApiResponse<T> | JsonObject;
77
+ } catch {
78
+ throw new CliError(`Windrunner returned invalid JSON (${response.status}).`);
79
+ }
80
+ }
81
+
82
+ if (!response.ok) {
83
+ throw new CliError(formatApiFailure(response.status, payload));
84
+ }
85
+
86
+ if (isApiResponse(payload) && payload.errors && payload.errors.length > 0) {
87
+ throw new CliError(formatApiErrors(payload.errors));
88
+ }
89
+
90
+ if (!isApiResponse(payload)) {
91
+ return { data: payload as T | null };
92
+ }
93
+ return payload;
94
+ }
95
+ }
96
+
97
+ function isApiResponse(value: unknown): value is ApiResponse<unknown> {
98
+ return Boolean(value && typeof value === "object" && ("data" in value || "errors" in value || "meta" in value));
99
+ }
100
+
101
+ function formatApiFailure(status: number, payload: unknown): string {
102
+ if (isApiResponse(payload) && payload.errors?.length) {
103
+ return `Request failed (${status}): ${formatApiErrors(payload.errors)}`;
104
+ }
105
+ if (payload && typeof payload === "object" && "message" in payload && typeof payload.message === "string") {
106
+ return `Request failed (${status}): ${payload.message}`;
107
+ }
108
+ return `Request failed (${status}).`;
109
+ }
110
+
111
+ function formatApiErrors(errors: ApiError[]): string {
112
+ return errors
113
+ .map((error) => {
114
+ const prefix = error.code ? `${error.code}: ` : "";
115
+ const field = error.field ? ` (${error.field})` : "";
116
+ return `${prefix}${error.message ?? "Unknown API error"}${field}`;
117
+ })
118
+ .join("; ");
119
+ }
package/src/index.ts ADDED
@@ -0,0 +1,514 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createInterface } from "node:readline/promises";
4
+ import { stdin as input, stderr as output } from "node:process";
5
+ import { Command } from "commander";
6
+
7
+ import { CliError, WindrunnerClient } from "./client.js";
8
+ import type {
9
+ ApiResponse,
10
+ Assignee,
11
+ DryRunResult,
12
+ GlobalOptions,
13
+ JsonObject,
14
+ Project,
15
+ WorkItem,
16
+ WorkItemResponse,
17
+ Entry,
18
+ } from "./types.js";
19
+
20
+ function getGlobalOptions(command: Command): GlobalOptions {
21
+ const options = command.optsWithGlobals() as GlobalOptions;
22
+ return {
23
+ url: options.url || process.env.WINDRUNNER_URL || "http://localhost:8080",
24
+ json: Boolean(options.json),
25
+ dryRun: Boolean(options.dryRun),
26
+ yes: Boolean(options.yes),
27
+ };
28
+ }
29
+
30
+ function printResult(value: unknown, options: GlobalOptions): void {
31
+ if (options.json) {
32
+ console.log(JSON.stringify(value));
33
+ return;
34
+ }
35
+ console.log(JSON.stringify(value, null, 2));
36
+ }
37
+
38
+ function printResponse<T>(response: ApiResponse<T> | DryRunResult, options: GlobalOptions): void {
39
+ if (isDryRunResult(response)) {
40
+ printResult(response, options);
41
+ return;
42
+ }
43
+ printResult(response.data, options);
44
+ }
45
+
46
+ function isDryRunResult(value: ApiResponse<unknown> | DryRunResult): value is DryRunResult {
47
+ return "dryRun" in value && value.dryRun === true;
48
+ }
49
+
50
+ function encode(value: string): string {
51
+ return encodeURIComponent(value);
52
+ }
53
+
54
+ function numberValue(value: string | undefined, name: string): number | undefined {
55
+ if (value === undefined) return undefined;
56
+ const parsed = Number(value);
57
+ if (!Number.isInteger(parsed) || parsed < 0) {
58
+ throw new CliError(`--${name} must be a non-negative integer.`);
59
+ }
60
+ return parsed;
61
+ }
62
+
63
+ function queryString(parameters: Record<string, string | number | undefined>): string {
64
+ const query = new URLSearchParams();
65
+ for (const [key, value] of Object.entries(parameters)) {
66
+ if (value !== undefined && value !== "") {
67
+ query.set(key, String(value));
68
+ }
69
+ }
70
+ const encoded = query.toString();
71
+ return encoded ? `?${encoded}` : "";
72
+ }
73
+
74
+ function collectOption(value: string, previous: string[] = []): string[] {
75
+ return [...previous, value];
76
+ }
77
+
78
+ function parseAssignees(values: string[] | undefined): Assignee[] | undefined {
79
+ if (values === undefined) return undefined;
80
+ return values.map((value) => {
81
+ const separator = value.indexOf(":");
82
+ if (separator <= 0 || separator === value.length - 1) {
83
+ throw new CliError(`Invalid assignee '${value}'. Use USER:<id> or TEAM:<id>.`);
84
+ }
85
+ const assigneeType = value.slice(0, separator).toUpperCase();
86
+ const assigneeId = value.slice(separator + 1).trim();
87
+ if (assigneeType !== "USER" && assigneeType !== "TEAM") {
88
+ throw new CliError(`Invalid assignee type '${assigneeType}'. Use USER or TEAM.`);
89
+ }
90
+ if (!assigneeId) {
91
+ throw new CliError("Assignee id is required.");
92
+ }
93
+ return { assigneeType, assigneeId };
94
+ });
95
+ }
96
+
97
+ function requireText(value: string | undefined, optionName: string): string {
98
+ if (!value || !value.trim()) {
99
+ throw new CliError(`${optionName} is required.`);
100
+ }
101
+ return value.trim();
102
+ }
103
+
104
+ function validateDate(value: string | undefined): string | undefined {
105
+ if (value === undefined) return undefined;
106
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
107
+ throw new CliError("--due-date must use YYYY-MM-DD format.");
108
+ }
109
+ return value;
110
+ }
111
+
112
+ function workItemFields(options: Record<string, unknown>, current?: WorkItem): WorkItem {
113
+ const title = options.title === undefined ? current?.title : String(options.title);
114
+ const result: WorkItem = {
115
+ title: requireText(title, "--title"),
116
+ };
117
+
118
+ const values: Array<[keyof WorkItem, unknown]> = [
119
+ ["type", options.type === undefined ? current?.type : options.type],
120
+ ["status", options.status === undefined ? current?.status : options.status],
121
+ ["dueDate", options.dueDate === undefined ? current?.dueDate : validateDate(String(options.dueDate))],
122
+ ["priority", options.priority === undefined ? current?.priority : options.priority],
123
+ [
124
+ "parentWorkItemId",
125
+ options.parentId === undefined ? current?.parentWorkItemId : String(options.parentId),
126
+ ],
127
+ ];
128
+
129
+ for (const [key, value] of values) {
130
+ if (value !== undefined) {
131
+ result[key] = value as never;
132
+ }
133
+ }
134
+ return result;
135
+ }
136
+
137
+ async function confirmDelete(message: string, options: GlobalOptions): Promise<void> {
138
+ if (options.yes) return;
139
+ if (!input.isTTY) {
140
+ throw new CliError(`${message} Re-run with --yes when using a non-interactive terminal.`);
141
+ }
142
+
143
+ const readline = createInterface({ input, output });
144
+ try {
145
+ const answer = await readline.question(`${message} [y/N] `);
146
+ if (!/^(y|yes)$/i.test(answer.trim())) {
147
+ throw new CliError("Cancelled.");
148
+ }
149
+ } finally {
150
+ readline.close();
151
+ }
152
+ }
153
+
154
+ function addPagination(command: Command): Command {
155
+ return command
156
+ .option("--page <number>", "Page number (zero-based)", "0")
157
+ .option("--size <number>", "Items per page; the server caps this at 100", "50")
158
+ .option("--updated-after <timestamp>", "Only return records updated after an ISO-8601 timestamp (UTC recommended)");
159
+ }
160
+
161
+ const sharedHelp = `
162
+ Environment:
163
+ WINDRUNNER_URL Server URL (default: http://localhost:8080)
164
+ WINDRUNNER_API_KEY Bearer API key; keep it in the environment, not in arguments
165
+
166
+ Global options:
167
+ --url <url> Override WINDRUNNER_URL
168
+ --json Print compact machine-readable JSON
169
+ --dry-run Preview a mutation without sending the mutation request
170
+ -y, --yes Skip destructive-operation confirmation prompts
171
+
172
+ Output and safety:
173
+ Normal output is pretty JSON. Use --json when another agent or script will parse it.
174
+ Destructive commands require confirmation unless --yes is explicitly provided.
175
+ A dry run never sends a mutation request and does not require an API key.
176
+ `;
177
+
178
+ function addAgentHelp(command: Command, details: string): Command {
179
+ return command.addHelpText("after", `${sharedHelp}\n${details.trim()}\n`);
180
+ }
181
+
182
+ const program = new Command();
183
+
184
+ program
185
+ .name("windrunner")
186
+ .description("Command-line interface for Windrunner")
187
+ .version("0.1.0")
188
+ .option("--url <url>", "Windrunner server URL", process.env.WINDRUNNER_URL || "http://localhost:8080")
189
+ .option("--json", "Print compact JSON output")
190
+ .option("--dry-run", "Preview mutations without sending them")
191
+ .option("-y, --yes", "Skip confirmation prompts");
192
+
193
+ addAgentHelp(
194
+ program,
195
+ `Quick start:
196
+ export WINDRUNNER_URL=http://localhost:8080
197
+ export WINDRUNNER_API_KEY=your-api-key
198
+ windrunner projects list --json
199
+
200
+ Use '<command> --help' for command-specific arguments and examples.`,
201
+ );
202
+
203
+ const projects = program.command("projects").description("Manage projects");
204
+ addAgentHelp(projects, "Permissions: projects:read for both project commands.");
205
+
206
+ addAgentHelp(
207
+ projects
208
+ .command("list")
209
+ .description("List projects visible to the API key")
210
+ .option("--page <number>", "Page number (zero-based)", "0")
211
+ .option("--size <number>", "Items per page; the server caps this at 100", "50"),
212
+ `Permissions: projects:read
213
+
214
+ Examples:
215
+ windrunner projects list
216
+ windrunner projects list --page 1 --size 25 --json`,
217
+ )
218
+ .action(async (options: { page: string; size: string }, command: Command) => {
219
+ const globalOptions = getGlobalOptions(command);
220
+ const client = new WindrunnerClient(globalOptions);
221
+ const response = await client.get<Project[]>(
222
+ `/projects${queryString({ page: numberValue(options.page, "page"), size: numberValue(options.size, "size") })}`,
223
+ );
224
+ printResponse(response, globalOptions);
225
+ });
226
+
227
+ addAgentHelp(
228
+ projects
229
+ .command("get")
230
+ .description("Get a project")
231
+ .argument("<projectId>", "Project id"),
232
+ `Permissions: projects:read
233
+
234
+ Example:
235
+ windrunner projects get PROJECT_ID --json`,
236
+ )
237
+ .action(async (projectId: string, _options: unknown, command: Command) => {
238
+ const globalOptions = getGlobalOptions(command);
239
+ const client = new WindrunnerClient(globalOptions);
240
+ printResponse(await client.get<Project>(`/projects/${encode(projectId)}`), globalOptions);
241
+ });
242
+
243
+ const workItems = program.command("work-items").description("Manage work items");
244
+ addAgentHelp(workItems, "Use --json for machine-readable results. Work item type and status values are validated by the server.");
245
+
246
+ addAgentHelp(
247
+ addPagination(
248
+ workItems
249
+ .command("list")
250
+ .description("List work items in a project")
251
+ .argument("<projectId>", "Project id")
252
+ .option("--status <status>", "Filter by status")
253
+ .option("--type <type>", "Filter by type")
254
+ .option("--priority <priority>", "Filter by priority"),
255
+ ),
256
+ `Permissions: work_items:read
257
+
258
+ Examples:
259
+ windrunner work-items list PROJECT_ID
260
+ windrunner work-items list PROJECT_ID --status OPEN --size 25 --json`,
261
+ ).action(
262
+ async (
263
+ projectId: string,
264
+ options: {
265
+ page: string;
266
+ size: string;
267
+ status?: string;
268
+ type?: string;
269
+ priority?: string;
270
+ updatedAfter?: string;
271
+ },
272
+ command: Command,
273
+ ) => {
274
+ const globalOptions = getGlobalOptions(command);
275
+ const client = new WindrunnerClient(globalOptions);
276
+ const response = await client.get<WorkItemResponse[]>(
277
+ `/projects/${encode(projectId)}/work-items${queryString({
278
+ page: numberValue(options.page, "page"),
279
+ size: numberValue(options.size, "size"),
280
+ status: options.status,
281
+ type: options.type,
282
+ priority: options.priority,
283
+ updated_after: options.updatedAfter,
284
+ })}`,
285
+ );
286
+ printResponse(response, globalOptions);
287
+ },
288
+ );
289
+
290
+ addAgentHelp(
291
+ workItems
292
+ .command("get")
293
+ .description("Get a work item")
294
+ .argument("<workItemId>", "Work item id"),
295
+ `Permissions: work_items:read
296
+
297
+ Example:
298
+ windrunner work-items get WORK_ITEM_ID --json`,
299
+ )
300
+ .action(async (workItemId: string, _options: unknown, command: Command) => {
301
+ const globalOptions = getGlobalOptions(command);
302
+ const client = new WindrunnerClient(globalOptions);
303
+ printResponse(await client.get<WorkItemResponse>(`/work-items/${encode(workItemId)}`), globalOptions);
304
+ });
305
+
306
+ function addWorkItemWriteOptions(command: Command): Command {
307
+ return command
308
+ .requiredOption("--title <title>", "Work item title")
309
+ .option("--type <type>", "Work item type")
310
+ .option("--status <status>", "Work item status")
311
+ .option("--due-date <date>", "Due date in YYYY-MM-DD format")
312
+ .option("--priority <priority>", "Work item priority")
313
+ .option("--parent-id <workItemId>", "Parent work item id")
314
+ .option("--assignee <type:id>", "Assignee in USER:<id> or TEAM:<id> format", collectOption);
315
+ }
316
+
317
+ addAgentHelp(
318
+ addWorkItemWriteOptions(
319
+ workItems
320
+ .command("create")
321
+ .description("Create a work item")
322
+ .argument("<projectId>", "Project id"),
323
+ ),
324
+ `Permissions: work_items:write
325
+ Required: --title.
326
+ Repeat --assignee for multiple assignments using USER:<id> or TEAM:<id>.
327
+
328
+ Examples:
329
+ windrunner work-items create PROJECT_ID --title "Fix login"
330
+ windrunner work-items create PROJECT_ID --title "Release" --status OPEN --assignee USER:user-1 --dry-run`,
331
+ ).action(
332
+ async (
333
+ projectId: string,
334
+ options: {
335
+ title: string;
336
+ type?: string;
337
+ status?: string;
338
+ dueDate?: string;
339
+ priority?: string;
340
+ parentId?: string;
341
+ assignee?: string[];
342
+ },
343
+ command: Command,
344
+ ) => {
345
+ const globalOptions = getGlobalOptions(command);
346
+ const client = new WindrunnerClient(globalOptions);
347
+ const body: JsonObject = {
348
+ workItem: workItemFields(options),
349
+ };
350
+ const assignees = parseAssignees(options.assignee);
351
+ if (assignees !== undefined) body.assignees = assignees;
352
+ printResponse(await client.post<WorkItemResponse>(`/projects/${encode(projectId)}/work-items`, body), globalOptions);
353
+ },
354
+ );
355
+
356
+ addAgentHelp(
357
+ addWorkItemWriteOptions(
358
+ workItems
359
+ .command("update")
360
+ .description("Update a work item")
361
+ .argument("<workItemId>", "Work item id"),
362
+ ),
363
+ `Permissions: work_items:read and work_items:write.
364
+ Required: --title. The CLI reads the current item first so omitted fields are preserved.
365
+
366
+ Examples:
367
+ windrunner work-items update WORK_ITEM_ID --title "Updated title"
368
+ windrunner work-items update WORK_ITEM_ID --title "Done" --status DONE --json`,
369
+ ).action(
370
+ async (
371
+ workItemId: string,
372
+ options: {
373
+ title: string;
374
+ type?: string;
375
+ status?: string;
376
+ dueDate?: string;
377
+ priority?: string;
378
+ parentId?: string;
379
+ assignee?: string[];
380
+ },
381
+ command: Command,
382
+ ) => {
383
+ const globalOptions = getGlobalOptions(command);
384
+ const client = new WindrunnerClient(globalOptions);
385
+ let current: WorkItem | undefined;
386
+
387
+ // The API accepts a full WorkItem representation for PUT. Fetching first
388
+ // lets the CLI offer a safe field-oriented update without clearing fields
389
+ // the caller did not mention.
390
+ if (!globalOptions.dryRun) {
391
+ const existing = await client.get<WorkItemResponse>(`/work-items/${encode(workItemId)}`);
392
+ current = existing.data?.workItem;
393
+ if (!current) throw new CliError("Work item response did not include a work item.");
394
+ }
395
+
396
+ const body: JsonObject = {
397
+ workItem: workItemFields(options, current),
398
+ };
399
+ const assignees = parseAssignees(options.assignee);
400
+ if (assignees !== undefined) body.assignees = assignees;
401
+ printResponse(await client.put<WorkItemResponse>(`/work-items/${encode(workItemId)}`, body), globalOptions);
402
+ },
403
+ );
404
+
405
+ addAgentHelp(
406
+ workItems
407
+ .command("delete")
408
+ .description("Delete a work item and its descendants")
409
+ .argument("<workItemId>", "Work item id"),
410
+ `Permissions: work_items:write
411
+ This permanently deletes the work item, descendants, entries, and relationships.
412
+ Use --dry-run to preview the request. Use --yes only when deletion is explicitly intended.
413
+
414
+ Examples:
415
+ windrunner work-items delete WORK_ITEM_ID --dry-run
416
+ windrunner work-items delete WORK_ITEM_ID --yes`,
417
+ )
418
+ .action(async (workItemId: string, _options: unknown, command: Command) => {
419
+ const globalOptions = getGlobalOptions(command);
420
+ const client = new WindrunnerClient(globalOptions);
421
+ if (!globalOptions.dryRun) {
422
+ await confirmDelete(`Delete work item ${workItemId}? This cannot be undone.`, globalOptions);
423
+ }
424
+ printResponse(await client.delete(`/work-items/${encode(workItemId)}`), globalOptions);
425
+ });
426
+
427
+ const entries = program.command("entries").description("Manage entries");
428
+ addAgentHelp(entries, "Entries are attached to work items. Use --json for machine-readable results.");
429
+
430
+ addAgentHelp(
431
+ addPagination(
432
+ entries
433
+ .command("list")
434
+ .description("List entries attached to a work item")
435
+ .argument("<workItemId>", "Work item id"),
436
+ ),
437
+ `Permissions: entries:read
438
+
439
+ Example:
440
+ windrunner entries list WORK_ITEM_ID --updated-after 2026-01-01T00:00:00Z --json`,
441
+ ).action(
442
+ async (
443
+ workItemId: string,
444
+ options: { page: string; size: string; updatedAfter?: string },
445
+ command: Command,
446
+ ) => {
447
+ const globalOptions = getGlobalOptions(command);
448
+ const client = new WindrunnerClient(globalOptions);
449
+ const response = await client.get<Entry[]>(
450
+ `/work-items/${encode(workItemId)}/entries${queryString({
451
+ page: numberValue(options.page, "page"),
452
+ size: numberValue(options.size, "size"),
453
+ updated_after: options.updatedAfter,
454
+ })}`,
455
+ );
456
+ printResponse(response, globalOptions);
457
+ },
458
+ );
459
+
460
+ addAgentHelp(
461
+ entries
462
+ .command("create")
463
+ .description("Create an entry on a work item")
464
+ .argument("<workItemId>", "Work item id")
465
+ .requiredOption("--body <body>", "Entry body")
466
+ .option("--type <type>", "Entry type"),
467
+ `Permissions: entries:write
468
+ Required: --body.
469
+
470
+ Examples:
471
+ windrunner entries create WORK_ITEM_ID --body "Deployment completed"
472
+ windrunner entries create WORK_ITEM_ID --body "Comment" --type COMMENT --dry-run`,
473
+ )
474
+ .action(async (workItemId: string, options: { body: string; type?: string }, command: Command) => {
475
+ const globalOptions = getGlobalOptions(command);
476
+ const client = new WindrunnerClient(globalOptions);
477
+ const body: Entry = {
478
+ body: requireText(options.body, "--body"),
479
+ ...(options.type === undefined ? {} : { type: options.type }),
480
+ };
481
+ printResponse(await client.post<Entry>(`/work-items/${encode(workItemId)}/entries`, body), globalOptions);
482
+ });
483
+
484
+ addAgentHelp(
485
+ program
486
+ .command("search")
487
+ .description("Search project work items, entries, and relationships")
488
+ .argument("<projectId>", "Project id")
489
+ .argument("<query>", "Search query")
490
+ .option("--limit <number>", "Maximum number of matches"),
491
+ `Permissions: work_items:read
492
+
493
+ Example:
494
+ windrunner search PROJECT_ID "login failure" --limit 20 --json`,
495
+ )
496
+ .action(async (projectId: string, query: string, options: { limit?: string }, command: Command) => {
497
+ const globalOptions = getGlobalOptions(command);
498
+ const client = new WindrunnerClient(globalOptions);
499
+ const limit = numberValue(options.limit, "limit");
500
+ printResponse(
501
+ await client.get<JsonObject>(
502
+ `/projects/${encode(projectId)}/search${queryString({ q: query, limit })}`,
503
+ ),
504
+ globalOptions,
505
+ );
506
+ });
507
+
508
+ try {
509
+ await program.parseAsync(process.argv);
510
+ } catch (error) {
511
+ const message = error instanceof Error ? error.message : String(error);
512
+ console.error(`Error: ${message}`);
513
+ process.exitCode = 1;
514
+ }
package/src/types.ts ADDED
@@ -0,0 +1,74 @@
1
+ export type JsonObject = Record<string, unknown>;
2
+
3
+ export interface ApiError {
4
+ code?: string;
5
+ message?: string;
6
+ field?: string;
7
+ details?: unknown;
8
+ }
9
+
10
+ export interface ApiResponse<T> {
11
+ data: T | null;
12
+ errors?: ApiError[];
13
+ meta?: JsonObject | null;
14
+ }
15
+
16
+ export interface GlobalOptions {
17
+ url: string;
18
+ json?: boolean;
19
+ dryRun?: boolean;
20
+ yes?: boolean;
21
+ }
22
+
23
+ export interface Project {
24
+ id: string;
25
+ name: string;
26
+ createdByUserId?: string;
27
+ createdAt?: string;
28
+ updatedAt?: string;
29
+ archivedAt?: string | null;
30
+ }
31
+
32
+ export interface WorkItem {
33
+ id?: string;
34
+ projectId?: string;
35
+ parentWorkItemId?: string | null;
36
+ sortIndex?: number;
37
+ type?: string;
38
+ title: string;
39
+ status?: string;
40
+ dueDate?: string | null;
41
+ priority?: string | null;
42
+ createdByUserId?: string;
43
+ createdAt?: string;
44
+ updatedAt?: string;
45
+ }
46
+
47
+ export interface Assignee {
48
+ assigneeType: string;
49
+ assigneeId: string;
50
+ }
51
+
52
+ export interface WorkItemResponse {
53
+ workItem: WorkItem;
54
+ assignees: Assignee[];
55
+ }
56
+
57
+ export interface Entry {
58
+ id?: string;
59
+ projectId?: string;
60
+ workItemId?: string;
61
+ sortIndex?: number;
62
+ authorUserId?: string;
63
+ type?: string;
64
+ body: string;
65
+ createdAt?: string;
66
+ updatedAt?: string;
67
+ }
68
+
69
+ export interface DryRunResult {
70
+ dryRun: true;
71
+ method: string;
72
+ path: string;
73
+ body?: unknown;
74
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "lib": ["ES2023"],
5
+ "module": "NodeNext",
6
+ "moduleResolution": "NodeNext",
7
+ "rootDir": "src",
8
+ "outDir": "dist",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "skipLibCheck": true,
13
+ "types": ["node"]
14
+ },
15
+ "include": ["src/**/*.ts"]
16
+ }