@shzlwio/windrunner-cli 1.0.0 → 1.1.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/index.js +513 -3
- package/package.json +1 -1
- package/src/index.ts +784 -4
- package/src/types.ts +80 -0
package/src/index.ts
CHANGED
|
@@ -12,9 +12,18 @@ import type {
|
|
|
12
12
|
GlobalOptions,
|
|
13
13
|
JsonObject,
|
|
14
14
|
Project,
|
|
15
|
+
ProjectMember,
|
|
16
|
+
ProjectTeam,
|
|
15
17
|
WorkItem,
|
|
16
18
|
WorkItemResponse,
|
|
17
19
|
Entry,
|
|
20
|
+
Relationship,
|
|
21
|
+
SearchResult,
|
|
22
|
+
Team,
|
|
23
|
+
TeamMember,
|
|
24
|
+
UserIdentity,
|
|
25
|
+
ContentOrderItem,
|
|
26
|
+
AuditLog,
|
|
18
27
|
} from "./types.js";
|
|
19
28
|
|
|
20
29
|
function getGlobalOptions(command: Command): GlobalOptions {
|
|
@@ -75,6 +84,24 @@ function collectOption(value: string, previous: string[] = []): string[] {
|
|
|
75
84
|
return [...previous, value];
|
|
76
85
|
}
|
|
77
86
|
|
|
87
|
+
function collectRequiredOption(values: string[] | undefined, optionName: string): string[] {
|
|
88
|
+
if (!values || values.length === 0) {
|
|
89
|
+
throw new CliError(`At least one ${optionName} is required.`);
|
|
90
|
+
}
|
|
91
|
+
return values.map((value) => requireText(value, `--${optionName}`));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function parseEntityReference(value: string, optionName: string): { entityType: string; entityId: string } {
|
|
95
|
+
const separator = value.indexOf(":");
|
|
96
|
+
if (separator <= 0 || separator === value.length - 1) {
|
|
97
|
+
throw new CliError(`Invalid ${optionName} '${value}'. Use TYPE:<id>.`);
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
entityType: requireText(value.slice(0, separator), `--${optionName}`).toUpperCase(),
|
|
101
|
+
entityId: requireText(value.slice(separator + 1), `--${optionName}`),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
78
105
|
function parseAssignees(values: string[] | undefined): Assignee[] | undefined {
|
|
79
106
|
if (values === undefined) return undefined;
|
|
80
107
|
return values.map((value) => {
|
|
@@ -158,6 +185,12 @@ function addPagination(command: Command): Command {
|
|
|
158
185
|
.option("--updated-after <timestamp>", "Only return records updated after an ISO-8601 timestamp (UTC recommended)");
|
|
159
186
|
}
|
|
160
187
|
|
|
188
|
+
function addPageOptions(command: Command, defaultSize = "50"): Command {
|
|
189
|
+
return command
|
|
190
|
+
.option("--page <number>", "Page number (zero-based)", "0")
|
|
191
|
+
.option("--size <number>", "Items per page; the server caps this at 100", defaultSize);
|
|
192
|
+
}
|
|
193
|
+
|
|
161
194
|
const sharedHelp = `
|
|
162
195
|
Environment:
|
|
163
196
|
WINDRUNNER_URL Server URL (default: http://localhost:8080)
|
|
@@ -184,7 +217,7 @@ const program = new Command();
|
|
|
184
217
|
program
|
|
185
218
|
.name("windrunner")
|
|
186
219
|
.description("Command-line interface for Windrunner")
|
|
187
|
-
.version("
|
|
220
|
+
.version("1.1.0")
|
|
188
221
|
.option("--url <url>", "Windrunner server URL", process.env.WINDRUNNER_URL || "http://localhost:8080")
|
|
189
222
|
.option("--json", "Print compact JSON output")
|
|
190
223
|
.option("--dry-run", "Preview mutations without sending them")
|
|
@@ -201,7 +234,7 @@ Use '<command> --help' for command-specific arguments and examples.`,
|
|
|
201
234
|
);
|
|
202
235
|
|
|
203
236
|
const projects = program.command("projects").description("Manage projects");
|
|
204
|
-
addAgentHelp(projects, "
|
|
237
|
+
addAgentHelp(projects, "Use command-specific help for the required API-key scope.");
|
|
205
238
|
|
|
206
239
|
addAgentHelp(
|
|
207
240
|
projects
|
|
@@ -240,6 +273,261 @@ Example:
|
|
|
240
273
|
printResponse(await client.get<Project>(`/projects/${encode(projectId)}`), globalOptions);
|
|
241
274
|
});
|
|
242
275
|
|
|
276
|
+
addAgentHelp(
|
|
277
|
+
projects
|
|
278
|
+
.command("create")
|
|
279
|
+
.description("Create a project")
|
|
280
|
+
.requiredOption("--name <name>", "Project name")
|
|
281
|
+
.option("--owner-user <userId>", "Project owner user id; repeat for multiple owners", collectOption)
|
|
282
|
+
.option("--owner-team <teamId>", "Project owner team id; repeat for multiple owners", collectOption),
|
|
283
|
+
`Permissions: projects:write
|
|
284
|
+
At least one --owner-user or --owner-team is required.
|
|
285
|
+
|
|
286
|
+
Examples:
|
|
287
|
+
windrunner projects create --name "Platform work" --owner-user user-1
|
|
288
|
+
windrunner projects create --name "Shared work" --owner-team team-1 --dry-run --json`,
|
|
289
|
+
).action(
|
|
290
|
+
async (
|
|
291
|
+
options: { name: string; ownerUser?: string[]; ownerTeam?: string[] },
|
|
292
|
+
command: Command,
|
|
293
|
+
) => {
|
|
294
|
+
const globalOptions = getGlobalOptions(command);
|
|
295
|
+
const ownerUserIds = options.ownerUser?.map((value) => requireText(value, "--owner-user")) ?? [];
|
|
296
|
+
const ownerTeamIds = options.ownerTeam?.map((value) => requireText(value, "--owner-team")) ?? [];
|
|
297
|
+
if (ownerUserIds.length === 0 && ownerTeamIds.length === 0) {
|
|
298
|
+
throw new CliError("At least one --owner-user or --owner-team is required.");
|
|
299
|
+
}
|
|
300
|
+
const client = new WindrunnerClient(globalOptions);
|
|
301
|
+
printResponse(
|
|
302
|
+
await client.post<Project>("/projects", {
|
|
303
|
+
name: requireText(options.name, "--name"),
|
|
304
|
+
ownerUserIds,
|
|
305
|
+
ownerTeamIds,
|
|
306
|
+
}),
|
|
307
|
+
globalOptions,
|
|
308
|
+
);
|
|
309
|
+
},
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
addAgentHelp(
|
|
313
|
+
projects
|
|
314
|
+
.command("update")
|
|
315
|
+
.description("Update a project")
|
|
316
|
+
.argument("<projectId>", "Project id")
|
|
317
|
+
.requiredOption("--name <name>", "Project name"),
|
|
318
|
+
`Permissions: projects:write
|
|
319
|
+
|
|
320
|
+
Example:
|
|
321
|
+
windrunner projects update PROJECT_ID --name "Updated project"`,
|
|
322
|
+
).action(async (projectId: string, options: { name: string }, command: Command) => {
|
|
323
|
+
const globalOptions = getGlobalOptions(command);
|
|
324
|
+
const client = new WindrunnerClient(globalOptions);
|
|
325
|
+
printResponse(
|
|
326
|
+
await client.put<Project>(`/projects/${encode(projectId)}`, {
|
|
327
|
+
name: requireText(options.name, "--name"),
|
|
328
|
+
}),
|
|
329
|
+
globalOptions,
|
|
330
|
+
);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
addAgentHelp(
|
|
334
|
+
projects
|
|
335
|
+
.command("delete")
|
|
336
|
+
.description("Delete a project and all of its content")
|
|
337
|
+
.argument("<projectId>", "Project id"),
|
|
338
|
+
`Permissions: projects:write
|
|
339
|
+
This permanently deletes the project, work items, entries, relationships, and access links.
|
|
340
|
+
Use --dry-run to preview the request. Use --yes only when deletion is explicitly intended.
|
|
341
|
+
|
|
342
|
+
Example:
|
|
343
|
+
windrunner projects delete PROJECT_ID --yes`,
|
|
344
|
+
).action(async (projectId: string, _options: unknown, command: Command) => {
|
|
345
|
+
const globalOptions = getGlobalOptions(command);
|
|
346
|
+
const client = new WindrunnerClient(globalOptions);
|
|
347
|
+
if (!globalOptions.dryRun) {
|
|
348
|
+
await confirmDelete(`Delete project ${projectId}? This cannot be undone.`, globalOptions);
|
|
349
|
+
}
|
|
350
|
+
printResponse(await client.delete(`/projects/${encode(projectId)}`), globalOptions);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
const projectMembers = projects.command("members").description("Manage project user access");
|
|
354
|
+
addAgentHelp(projectMembers, "Project membership changes require project owner access.");
|
|
355
|
+
|
|
356
|
+
addAgentHelp(
|
|
357
|
+
addPageOptions(
|
|
358
|
+
projectMembers
|
|
359
|
+
.command("list")
|
|
360
|
+
.description("List users with access to a project")
|
|
361
|
+
.argument("<projectId>", "Project id"),
|
|
362
|
+
),
|
|
363
|
+
`Permissions: project_access:read
|
|
364
|
+
|
|
365
|
+
Example:
|
|
366
|
+
windrunner projects members list PROJECT_ID --json`,
|
|
367
|
+
).action(async (projectId: string, options: { page: string; size: string }, command: Command) => {
|
|
368
|
+
const globalOptions = getGlobalOptions(command);
|
|
369
|
+
const client = new WindrunnerClient(globalOptions);
|
|
370
|
+
printResponse(
|
|
371
|
+
await client.get<ProjectMember[]>(
|
|
372
|
+
`/projects/${encode(projectId)}/members${queryString({
|
|
373
|
+
page: numberValue(options.page, "page"),
|
|
374
|
+
size: numberValue(options.size, "size"),
|
|
375
|
+
})}`,
|
|
376
|
+
),
|
|
377
|
+
globalOptions,
|
|
378
|
+
);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
addAgentHelp(
|
|
382
|
+
projectMembers
|
|
383
|
+
.command("add")
|
|
384
|
+
.description("Add or update a project user")
|
|
385
|
+
.argument("<projectId>", "Project id")
|
|
386
|
+
.requiredOption("--user-id <userId>", "User id")
|
|
387
|
+
.option("--role <role>", "Project role: OWNER, EDITOR, or VIEWER", "VIEWER"),
|
|
388
|
+
`Permissions: project_access:write
|
|
389
|
+
|
|
390
|
+
Example:
|
|
391
|
+
windrunner projects members add PROJECT_ID --user-id USER_ID --role EDITOR`,
|
|
392
|
+
).action(
|
|
393
|
+
async (projectId: string, options: { userId: string; role: string }, command: Command) => {
|
|
394
|
+
const globalOptions = getGlobalOptions(command);
|
|
395
|
+
const client = new WindrunnerClient(globalOptions);
|
|
396
|
+
printResponse(
|
|
397
|
+
await client.post<ProjectMember>(`/projects/${encode(projectId)}/members`, {
|
|
398
|
+
userId: requireText(options.userId, "--user-id"),
|
|
399
|
+
role: options.role,
|
|
400
|
+
}),
|
|
401
|
+
globalOptions,
|
|
402
|
+
);
|
|
403
|
+
},
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
addAgentHelp(
|
|
407
|
+
projectMembers
|
|
408
|
+
.command("remove")
|
|
409
|
+
.description("Remove a user from a project")
|
|
410
|
+
.argument("<projectId>", "Project id")
|
|
411
|
+
.argument("<userId>", "User id"),
|
|
412
|
+
`Permissions: project_access:write
|
|
413
|
+
|
|
414
|
+
Example:
|
|
415
|
+
windrunner projects members remove PROJECT_ID USER_ID --yes`,
|
|
416
|
+
).action(async (projectId: string, userId: string, _options: unknown, command: Command) => {
|
|
417
|
+
const globalOptions = getGlobalOptions(command);
|
|
418
|
+
const client = new WindrunnerClient(globalOptions);
|
|
419
|
+
if (!globalOptions.dryRun) {
|
|
420
|
+
await confirmDelete(`Remove user ${userId} from project ${projectId}?`, globalOptions);
|
|
421
|
+
}
|
|
422
|
+
printResponse(
|
|
423
|
+
await client.delete(`/projects/${encode(projectId)}/members/${encode(userId)}`),
|
|
424
|
+
globalOptions,
|
|
425
|
+
);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
const projectTeams = projects.command("teams").description("Manage project team access");
|
|
429
|
+
addAgentHelp(projectTeams, "Project team links require project owner access.");
|
|
430
|
+
|
|
431
|
+
addAgentHelp(
|
|
432
|
+
addPageOptions(
|
|
433
|
+
projectTeams
|
|
434
|
+
.command("list")
|
|
435
|
+
.description("List teams linked to a project")
|
|
436
|
+
.argument("<projectId>", "Project id"),
|
|
437
|
+
),
|
|
438
|
+
`Permissions: project_access:read
|
|
439
|
+
|
|
440
|
+
Example:
|
|
441
|
+
windrunner projects teams list PROJECT_ID --json`,
|
|
442
|
+
).action(async (projectId: string, options: { page: string; size: string }, command: Command) => {
|
|
443
|
+
const globalOptions = getGlobalOptions(command);
|
|
444
|
+
const client = new WindrunnerClient(globalOptions);
|
|
445
|
+
printResponse(
|
|
446
|
+
await client.get<ProjectTeam[]>(
|
|
447
|
+
`/projects/${encode(projectId)}/teams${queryString({
|
|
448
|
+
page: numberValue(options.page, "page"),
|
|
449
|
+
size: numberValue(options.size, "size"),
|
|
450
|
+
})}`,
|
|
451
|
+
),
|
|
452
|
+
globalOptions,
|
|
453
|
+
);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
addAgentHelp(
|
|
457
|
+
projectTeams
|
|
458
|
+
.command("add")
|
|
459
|
+
.description("Add or update a project team")
|
|
460
|
+
.argument("<projectId>", "Project id")
|
|
461
|
+
.requiredOption("--team-id <teamId>", "Team id")
|
|
462
|
+
.option("--role <role>", "Project role: OWNER, EDITOR, or VIEWER", "VIEWER"),
|
|
463
|
+
`Permissions: project_access:write
|
|
464
|
+
|
|
465
|
+
Example:
|
|
466
|
+
windrunner projects teams add PROJECT_ID --team-id TEAM_ID --role EDITOR`,
|
|
467
|
+
).action(
|
|
468
|
+
async (projectId: string, options: { teamId: string; role: string }, command: Command) => {
|
|
469
|
+
const globalOptions = getGlobalOptions(command);
|
|
470
|
+
const client = new WindrunnerClient(globalOptions);
|
|
471
|
+
printResponse(
|
|
472
|
+
await client.post<ProjectTeam>(`/projects/${encode(projectId)}/teams`, {
|
|
473
|
+
teamId: requireText(options.teamId, "--team-id"),
|
|
474
|
+
role: options.role,
|
|
475
|
+
}),
|
|
476
|
+
globalOptions,
|
|
477
|
+
);
|
|
478
|
+
},
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
addAgentHelp(
|
|
482
|
+
projectTeams
|
|
483
|
+
.command("remove")
|
|
484
|
+
.description("Unlink a team from a project")
|
|
485
|
+
.argument("<projectId>", "Project id")
|
|
486
|
+
.argument("<teamId>", "Team id"),
|
|
487
|
+
`Permissions: project_access:write
|
|
488
|
+
|
|
489
|
+
Example:
|
|
490
|
+
windrunner projects teams remove PROJECT_ID TEAM_ID --yes`,
|
|
491
|
+
).action(async (projectId: string, teamId: string, _options: unknown, command: Command) => {
|
|
492
|
+
const globalOptions = getGlobalOptions(command);
|
|
493
|
+
const client = new WindrunnerClient(globalOptions);
|
|
494
|
+
if (!globalOptions.dryRun) {
|
|
495
|
+
await confirmDelete(`Unlink team ${teamId} from project ${projectId}?`, globalOptions);
|
|
496
|
+
}
|
|
497
|
+
printResponse(
|
|
498
|
+
await client.delete(`/projects/${encode(projectId)}/teams/${encode(teamId)}`),
|
|
499
|
+
globalOptions,
|
|
500
|
+
);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
addAgentHelp(
|
|
504
|
+
projects
|
|
505
|
+
.command("reorder")
|
|
506
|
+
.description("Reorder work items and entries in a project content stream")
|
|
507
|
+
.argument("<projectId>", "Project id")
|
|
508
|
+
.requiredOption("--item <type:id>", "Ordered item in WORK_ITEM:<id> or ENTRY:<id> format", collectOption)
|
|
509
|
+
.option("--parent-id <workItemId>", "Parent work item id; omit for the project root"),
|
|
510
|
+
`Permissions: work_items:write and entries:write
|
|
511
|
+
Repeat --item in the desired order.
|
|
512
|
+
|
|
513
|
+
Example:
|
|
514
|
+
windrunner projects reorder PROJECT_ID \\
|
|
515
|
+
--item WORK_ITEM:item-1 --item ENTRY:entry-1 --item WORK_ITEM:item-2`,
|
|
516
|
+
).action(
|
|
517
|
+
async (projectId: string, options: { item: string[]; parentId?: string }, command: Command) => {
|
|
518
|
+
const globalOptions = getGlobalOptions(command);
|
|
519
|
+
const client = new WindrunnerClient(globalOptions);
|
|
520
|
+
const items = options.item.map((value) => parseEntityReference(value, "item"));
|
|
521
|
+
printResponse(
|
|
522
|
+
await client.put<ContentOrderItem[]>(`/projects/${encode(projectId)}/content-order`, {
|
|
523
|
+
...(options.parentId === undefined ? {} : { parentWorkItemId: options.parentId }),
|
|
524
|
+
items,
|
|
525
|
+
}),
|
|
526
|
+
globalOptions,
|
|
527
|
+
);
|
|
528
|
+
},
|
|
529
|
+
);
|
|
530
|
+
|
|
243
531
|
const workItems = program.command("work-items").description("Manage work items");
|
|
244
532
|
addAgentHelp(workItems, "Use --json for machine-readable results. Work item type and status values are validated by the server.");
|
|
245
533
|
|
|
@@ -424,6 +712,35 @@ Examples:
|
|
|
424
712
|
printResponse(await client.delete(`/work-items/${encode(workItemId)}`), globalOptions);
|
|
425
713
|
});
|
|
426
714
|
|
|
715
|
+
addAgentHelp(
|
|
716
|
+
workItems
|
|
717
|
+
.command("move")
|
|
718
|
+
.description("Move a work item to a different content position")
|
|
719
|
+
.argument("<workItemId>", "Work item id")
|
|
720
|
+
.option("--parent-id <workItemId>", "Destination parent work item id; omit for the project root")
|
|
721
|
+
.option("--before <type:id>", "Place before WORK_ITEM:<id> or ENTRY:<id> in the destination stream"),
|
|
722
|
+
`Permissions: work_items:write
|
|
723
|
+
|
|
724
|
+
Examples:
|
|
725
|
+
windrunner work-items move WORK_ITEM_ID --parent-id PARENT_ID
|
|
726
|
+
windrunner work-items move WORK_ITEM_ID --before ENTRY:entry-1`,
|
|
727
|
+
).action(
|
|
728
|
+
async (workItemId: string, options: { parentId?: string; before?: string }, command: Command) => {
|
|
729
|
+
const globalOptions = getGlobalOptions(command);
|
|
730
|
+
const client = new WindrunnerClient(globalOptions);
|
|
731
|
+
const before = options.before === undefined ? undefined : parseEntityReference(options.before, "before");
|
|
732
|
+
printResponse(
|
|
733
|
+
await client.put<WorkItemResponse>(`/work-items/${encode(workItemId)}/move`, {
|
|
734
|
+
...(options.parentId === undefined ? {} : { parentWorkItemId: options.parentId }),
|
|
735
|
+
...(before === undefined
|
|
736
|
+
? {}
|
|
737
|
+
: { beforeEntityType: before.entityType, beforeEntityId: before.entityId }),
|
|
738
|
+
}),
|
|
739
|
+
globalOptions,
|
|
740
|
+
);
|
|
741
|
+
},
|
|
742
|
+
);
|
|
743
|
+
|
|
427
744
|
const entries = program.command("entries").description("Manage entries");
|
|
428
745
|
addAgentHelp(entries, "Entries are attached to work items. Use --json for machine-readable results.");
|
|
429
746
|
|
|
@@ -481,6 +798,65 @@ Examples:
|
|
|
481
798
|
printResponse(await client.post<Entry>(`/work-items/${encode(workItemId)}/entries`, body), globalOptions);
|
|
482
799
|
});
|
|
483
800
|
|
|
801
|
+
addAgentHelp(
|
|
802
|
+
entries
|
|
803
|
+
.command("get")
|
|
804
|
+
.description("Get an entry")
|
|
805
|
+
.argument("<entryId>", "Entry id"),
|
|
806
|
+
`Permissions: entries:read
|
|
807
|
+
|
|
808
|
+
Example:
|
|
809
|
+
windrunner entries get ENTRY_ID --json`,
|
|
810
|
+
).action(async (entryId: string, _options: unknown, command: Command) => {
|
|
811
|
+
const globalOptions = getGlobalOptions(command);
|
|
812
|
+
const client = new WindrunnerClient(globalOptions);
|
|
813
|
+
printResponse(await client.get<Entry>(`/entries/${encode(entryId)}`), globalOptions);
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
addAgentHelp(
|
|
817
|
+
entries
|
|
818
|
+
.command("update")
|
|
819
|
+
.description("Update an entry")
|
|
820
|
+
.argument("<entryId>", "Entry id")
|
|
821
|
+
.requiredOption("--body <body>", "Entry body")
|
|
822
|
+
.option("--type <type>", "Entry type; defaults to COMMENT when omitted"),
|
|
823
|
+
`Permissions: entries:write
|
|
824
|
+
Required: --body. The API treats an omitted type as COMMENT.
|
|
825
|
+
|
|
826
|
+
Example:
|
|
827
|
+
windrunner entries update ENTRY_ID --body "Updated context" --type EVIDENCE`,
|
|
828
|
+
).action(async (entryId: string, options: { body: string; type?: string }, command: Command) => {
|
|
829
|
+
const globalOptions = getGlobalOptions(command);
|
|
830
|
+
const client = new WindrunnerClient(globalOptions);
|
|
831
|
+
printResponse(
|
|
832
|
+
await client.put<Entry>(`/entries/${encode(entryId)}`, {
|
|
833
|
+
body: requireText(options.body, "--body"),
|
|
834
|
+
...(options.type === undefined ? {} : { type: options.type }),
|
|
835
|
+
}),
|
|
836
|
+
globalOptions,
|
|
837
|
+
);
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
addAgentHelp(
|
|
841
|
+
entries
|
|
842
|
+
.command("delete")
|
|
843
|
+
.description("Delete an entry")
|
|
844
|
+
.argument("<entryId>", "Entry id"),
|
|
845
|
+
`Permissions: entries:write
|
|
846
|
+
This permanently deletes the entry and its relationships.
|
|
847
|
+
Use --dry-run to preview the request.
|
|
848
|
+
|
|
849
|
+
Example:
|
|
850
|
+
windrunner entries delete ENTRY_ID --yes`,
|
|
851
|
+
).action(async (entryId: string, _options: unknown, command: Command) => {
|
|
852
|
+
const globalOptions = getGlobalOptions(command);
|
|
853
|
+
const client = new WindrunnerClient(globalOptions);
|
|
854
|
+
if (!globalOptions.dryRun) {
|
|
855
|
+
await confirmDelete(`Delete entry ${entryId}? This cannot be undone.`, globalOptions);
|
|
856
|
+
}
|
|
857
|
+
printResponse(await client.delete(`/entries/${encode(entryId)}`), globalOptions);
|
|
858
|
+
});
|
|
859
|
+
|
|
484
860
|
addAgentHelp(
|
|
485
861
|
program
|
|
486
862
|
.command("search")
|
|
@@ -488,7 +864,7 @@ addAgentHelp(
|
|
|
488
864
|
.argument("<projectId>", "Project id")
|
|
489
865
|
.argument("<query>", "Search query")
|
|
490
866
|
.option("--limit <number>", "Maximum number of matches"),
|
|
491
|
-
`Permissions: work_items:read
|
|
867
|
+
`Permissions: work_items:read, entries:read, and relationships:read
|
|
492
868
|
|
|
493
869
|
Example:
|
|
494
870
|
windrunner search PROJECT_ID "login failure" --limit 20 --json`,
|
|
@@ -498,13 +874,417 @@ Example:
|
|
|
498
874
|
const client = new WindrunnerClient(globalOptions);
|
|
499
875
|
const limit = numberValue(options.limit, "limit");
|
|
500
876
|
printResponse(
|
|
501
|
-
await client.get<
|
|
877
|
+
await client.get<SearchResult>(
|
|
502
878
|
`/projects/${encode(projectId)}/search${queryString({ q: query, limit })}`,
|
|
503
879
|
),
|
|
504
880
|
globalOptions,
|
|
505
881
|
);
|
|
506
882
|
});
|
|
507
883
|
|
|
884
|
+
const relationships = program.command("relationships").description("Manage work item relationships");
|
|
885
|
+
addAgentHelp(relationships, "Relationships connect work items and entries with a type and optional reason.");
|
|
886
|
+
|
|
887
|
+
addAgentHelp(
|
|
888
|
+
addPageOptions(
|
|
889
|
+
relationships
|
|
890
|
+
.command("list")
|
|
891
|
+
.description("List relationships in a project")
|
|
892
|
+
.argument("<projectId>", "Project id")
|
|
893
|
+
.option("--type <type>", "Filter by relationship type")
|
|
894
|
+
.option("--created-after <timestamp>", "Only return relationships created after an ISO-8601 timestamp"),
|
|
895
|
+
),
|
|
896
|
+
`Permissions: relationships:read
|
|
897
|
+
|
|
898
|
+
Example:
|
|
899
|
+
windrunner relationships list PROJECT_ID --type BLOCKED_BY --json`,
|
|
900
|
+
).action(
|
|
901
|
+
async (
|
|
902
|
+
projectId: string,
|
|
903
|
+
options: { page: string; size: string; type?: string; createdAfter?: string },
|
|
904
|
+
command: Command,
|
|
905
|
+
) => {
|
|
906
|
+
const globalOptions = getGlobalOptions(command);
|
|
907
|
+
const client = new WindrunnerClient(globalOptions);
|
|
908
|
+
printResponse(
|
|
909
|
+
await client.get<Relationship[]>(
|
|
910
|
+
`/projects/${encode(projectId)}/relationships${queryString({
|
|
911
|
+
page: numberValue(options.page, "page"),
|
|
912
|
+
size: numberValue(options.size, "size"),
|
|
913
|
+
type: options.type,
|
|
914
|
+
created_after: options.createdAfter,
|
|
915
|
+
})}`,
|
|
916
|
+
),
|
|
917
|
+
globalOptions,
|
|
918
|
+
);
|
|
919
|
+
},
|
|
920
|
+
);
|
|
921
|
+
|
|
922
|
+
addAgentHelp(
|
|
923
|
+
relationships
|
|
924
|
+
.command("create")
|
|
925
|
+
.description("Create a relationship")
|
|
926
|
+
.argument("<projectId>", "Project id")
|
|
927
|
+
.requiredOption("--from <type:id>", "Source entity in WORK_ITEM:<id> or ENTRY:<id> format")
|
|
928
|
+
.requiredOption("--to <type:id>", "Target entity in WORK_ITEM:<id> or ENTRY:<id> format")
|
|
929
|
+
.requiredOption("--type <type>", "Relationship type")
|
|
930
|
+
.option("--reason <reason>", "Relationship reason")
|
|
931
|
+
.option("--source-entry-id <entryId>", "Entry supporting the relationship"),
|
|
932
|
+
`Permissions: relationships:write
|
|
933
|
+
|
|
934
|
+
Example:
|
|
935
|
+
windrunner relationships create PROJECT_ID \\
|
|
936
|
+
--from WORK_ITEM:item-1 --to WORK_ITEM:item-2 --type BLOCKED_BY \\
|
|
937
|
+
--reason "Waiting on the database migration"`,
|
|
938
|
+
).action(
|
|
939
|
+
async (
|
|
940
|
+
projectId: string,
|
|
941
|
+
options: {
|
|
942
|
+
from: string;
|
|
943
|
+
to: string;
|
|
944
|
+
type: string;
|
|
945
|
+
reason?: string;
|
|
946
|
+
sourceEntryId?: string;
|
|
947
|
+
},
|
|
948
|
+
command: Command,
|
|
949
|
+
) => {
|
|
950
|
+
const globalOptions = getGlobalOptions(command);
|
|
951
|
+
const client = new WindrunnerClient(globalOptions);
|
|
952
|
+
const from = parseEntityReference(options.from, "from");
|
|
953
|
+
const to = parseEntityReference(options.to, "to");
|
|
954
|
+
printResponse(
|
|
955
|
+
await client.post<Relationship>(`/projects/${encode(projectId)}/relationships`, {
|
|
956
|
+
fromEntityType: from.entityType,
|
|
957
|
+
fromEntityId: from.entityId,
|
|
958
|
+
toEntityType: to.entityType,
|
|
959
|
+
toEntityId: to.entityId,
|
|
960
|
+
type: requireText(options.type, "--type"),
|
|
961
|
+
...(options.reason === undefined ? {} : { reason: options.reason }),
|
|
962
|
+
...(options.sourceEntryId === undefined ? {} : { sourceEntryId: options.sourceEntryId }),
|
|
963
|
+
}),
|
|
964
|
+
globalOptions,
|
|
965
|
+
);
|
|
966
|
+
},
|
|
967
|
+
);
|
|
968
|
+
|
|
969
|
+
addAgentHelp(
|
|
970
|
+
relationships
|
|
971
|
+
.command("update-reason")
|
|
972
|
+
.description("Update or clear a relationship reason")
|
|
973
|
+
.argument("<relationshipId>", "Relationship id")
|
|
974
|
+
.option("--reason <reason>", "New reason; omit to clear the reason"),
|
|
975
|
+
`Permissions: relationships:write
|
|
976
|
+
|
|
977
|
+
Examples:
|
|
978
|
+
windrunner relationships update-reason RELATIONSHIP_ID --reason "New explanation"
|
|
979
|
+
windrunner relationships update-reason RELATIONSHIP_ID --dry-run`,
|
|
980
|
+
).action(async (relationshipId: string, options: { reason?: string }, command: Command) => {
|
|
981
|
+
const globalOptions = getGlobalOptions(command);
|
|
982
|
+
const client = new WindrunnerClient(globalOptions);
|
|
983
|
+
printResponse(
|
|
984
|
+
await client.put<Relationship>(`/relationships/${encode(relationshipId)}/reason`, {
|
|
985
|
+
reason: options.reason ?? null,
|
|
986
|
+
}),
|
|
987
|
+
globalOptions,
|
|
988
|
+
);
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
addAgentHelp(
|
|
992
|
+
relationships
|
|
993
|
+
.command("delete")
|
|
994
|
+
.description("Delete a relationship")
|
|
995
|
+
.argument("<relationshipId>", "Relationship id"),
|
|
996
|
+
`Permissions: relationships:write
|
|
997
|
+
Use --dry-run to preview the request.
|
|
998
|
+
|
|
999
|
+
Example:
|
|
1000
|
+
windrunner relationships delete RELATIONSHIP_ID --yes`,
|
|
1001
|
+
).action(async (relationshipId: string, _options: unknown, command: Command) => {
|
|
1002
|
+
const globalOptions = getGlobalOptions(command);
|
|
1003
|
+
const client = new WindrunnerClient(globalOptions);
|
|
1004
|
+
if (!globalOptions.dryRun) {
|
|
1005
|
+
await confirmDelete(`Delete relationship ${relationshipId}? This cannot be undone.`, globalOptions);
|
|
1006
|
+
}
|
|
1007
|
+
printResponse(await client.delete(`/relationships/${encode(relationshipId)}`), globalOptions);
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
const teams = program.command("teams").description("Manage teams");
|
|
1011
|
+
addAgentHelp(teams, "Team creation, updates, deletion, and membership changes require an admin-like API-key owner.");
|
|
1012
|
+
|
|
1013
|
+
addAgentHelp(
|
|
1014
|
+
addPageOptions(
|
|
1015
|
+
teams
|
|
1016
|
+
.command("list")
|
|
1017
|
+
.description("List teams"),
|
|
1018
|
+
),
|
|
1019
|
+
`Permissions: teams:read
|
|
1020
|
+
|
|
1021
|
+
Example:
|
|
1022
|
+
windrunner teams list --json`,
|
|
1023
|
+
).action(async (options: { page: string; size: string }, command: Command) => {
|
|
1024
|
+
const globalOptions = getGlobalOptions(command);
|
|
1025
|
+
const client = new WindrunnerClient(globalOptions);
|
|
1026
|
+
printResponse(
|
|
1027
|
+
await client.get<Team[]>(`/teams${queryString({
|
|
1028
|
+
page: numberValue(options.page, "page"),
|
|
1029
|
+
size: numberValue(options.size, "size"),
|
|
1030
|
+
})}`),
|
|
1031
|
+
globalOptions,
|
|
1032
|
+
);
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
addAgentHelp(
|
|
1036
|
+
teams
|
|
1037
|
+
.command("get")
|
|
1038
|
+
.description("Get a team")
|
|
1039
|
+
.argument("<teamId>", "Team id"),
|
|
1040
|
+
`Permissions: teams:read
|
|
1041
|
+
|
|
1042
|
+
Example:
|
|
1043
|
+
windrunner teams get TEAM_ID --json`,
|
|
1044
|
+
).action(async (teamId: string, _options: unknown, command: Command) => {
|
|
1045
|
+
const globalOptions = getGlobalOptions(command);
|
|
1046
|
+
const client = new WindrunnerClient(globalOptions);
|
|
1047
|
+
printResponse(await client.get<Team>(`/teams/${encode(teamId)}`), globalOptions);
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
addAgentHelp(
|
|
1051
|
+
teams
|
|
1052
|
+
.command("create")
|
|
1053
|
+
.description("Create a team")
|
|
1054
|
+
.requiredOption("--name <name>", "Team name")
|
|
1055
|
+
.requiredOption("--owner-user <userId>", "Team owner user id; repeat for multiple owners", collectOption)
|
|
1056
|
+
.option("--description <description>", "Team description"),
|
|
1057
|
+
`Permissions: teams:write
|
|
1058
|
+
At least one --owner-user is required.
|
|
1059
|
+
|
|
1060
|
+
Example:
|
|
1061
|
+
windrunner teams create --name "Platform" --owner-user user-1 --description "Platform team"`,
|
|
1062
|
+
).action(
|
|
1063
|
+
async (options: { name: string; ownerUser: string[]; description?: string }, command: Command) => {
|
|
1064
|
+
const globalOptions = getGlobalOptions(command);
|
|
1065
|
+
const client = new WindrunnerClient(globalOptions);
|
|
1066
|
+
printResponse(
|
|
1067
|
+
await client.post<Team>("/teams", {
|
|
1068
|
+
name: requireText(options.name, "--name"),
|
|
1069
|
+
ownerUserIds: collectRequiredOption(options.ownerUser, "--owner-user"),
|
|
1070
|
+
...(options.description === undefined ? {} : { description: options.description }),
|
|
1071
|
+
}),
|
|
1072
|
+
globalOptions,
|
|
1073
|
+
);
|
|
1074
|
+
},
|
|
1075
|
+
);
|
|
1076
|
+
|
|
1077
|
+
addAgentHelp(
|
|
1078
|
+
teams
|
|
1079
|
+
.command("update")
|
|
1080
|
+
.description("Update a team")
|
|
1081
|
+
.argument("<teamId>", "Team id")
|
|
1082
|
+
.requiredOption("--name <name>", "Team name")
|
|
1083
|
+
.requiredOption("--description <description>", "Team description; use an empty value to clear it"),
|
|
1084
|
+
`Permissions: teams:write
|
|
1085
|
+
Both fields are required because the API accepts a full team representation.
|
|
1086
|
+
|
|
1087
|
+
Example:
|
|
1088
|
+
windrunner teams update TEAM_ID --name "Platform engineering" --description "Owns platform services"`,
|
|
1089
|
+
).action(
|
|
1090
|
+
async (teamId: string, options: { name: string; description: string }, command: Command) => {
|
|
1091
|
+
const globalOptions = getGlobalOptions(command);
|
|
1092
|
+
const client = new WindrunnerClient(globalOptions);
|
|
1093
|
+
printResponse(
|
|
1094
|
+
await client.put<Team>(`/teams/${encode(teamId)}`, {
|
|
1095
|
+
name: requireText(options.name, "--name"),
|
|
1096
|
+
description: options.description,
|
|
1097
|
+
}),
|
|
1098
|
+
globalOptions,
|
|
1099
|
+
);
|
|
1100
|
+
},
|
|
1101
|
+
);
|
|
1102
|
+
|
|
1103
|
+
addAgentHelp(
|
|
1104
|
+
teams
|
|
1105
|
+
.command("delete")
|
|
1106
|
+
.description("Delete a team")
|
|
1107
|
+
.argument("<teamId>", "Team id"),
|
|
1108
|
+
`Permissions: teams:write
|
|
1109
|
+
This permanently deletes the team and removes its memberships and project links.
|
|
1110
|
+
Use --dry-run to preview the request.
|
|
1111
|
+
|
|
1112
|
+
Example:
|
|
1113
|
+
windrunner teams delete TEAM_ID --yes`,
|
|
1114
|
+
).action(async (teamId: string, _options: unknown, command: Command) => {
|
|
1115
|
+
const globalOptions = getGlobalOptions(command);
|
|
1116
|
+
const client = new WindrunnerClient(globalOptions);
|
|
1117
|
+
if (!globalOptions.dryRun) {
|
|
1118
|
+
await confirmDelete(`Delete team ${teamId}? This cannot be undone.`, globalOptions);
|
|
1119
|
+
}
|
|
1120
|
+
printResponse(await client.delete(`/teams/${encode(teamId)}`), globalOptions);
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
const teamMembers = teams.command("members").description("Manage team membership");
|
|
1124
|
+
addAgentHelp(teamMembers, "Team membership changes require an admin-like API-key owner.");
|
|
1125
|
+
|
|
1126
|
+
addAgentHelp(
|
|
1127
|
+
addPageOptions(
|
|
1128
|
+
teamMembers
|
|
1129
|
+
.command("list")
|
|
1130
|
+
.description("List team members")
|
|
1131
|
+
.argument("<teamId>", "Team id"),
|
|
1132
|
+
),
|
|
1133
|
+
`Permissions: team_members:read
|
|
1134
|
+
|
|
1135
|
+
Example:
|
|
1136
|
+
windrunner teams members list TEAM_ID --json`,
|
|
1137
|
+
).action(async (teamId: string, options: { page: string; size: string }, command: Command) => {
|
|
1138
|
+
const globalOptions = getGlobalOptions(command);
|
|
1139
|
+
const client = new WindrunnerClient(globalOptions);
|
|
1140
|
+
printResponse(
|
|
1141
|
+
await client.get<TeamMember[]>(
|
|
1142
|
+
`/teams/${encode(teamId)}/members${queryString({
|
|
1143
|
+
page: numberValue(options.page, "page"),
|
|
1144
|
+
size: numberValue(options.size, "size"),
|
|
1145
|
+
})}`,
|
|
1146
|
+
),
|
|
1147
|
+
globalOptions,
|
|
1148
|
+
);
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
addAgentHelp(
|
|
1152
|
+
teamMembers
|
|
1153
|
+
.command("add")
|
|
1154
|
+
.description("Add a user to a team")
|
|
1155
|
+
.argument("<teamId>", "Team id")
|
|
1156
|
+
.requiredOption("--user-id <userId>", "User id")
|
|
1157
|
+
.option("--role <role>", "Team role: TEAM_OWNER or TEAM_MEMBER", "TEAM_MEMBER"),
|
|
1158
|
+
`Permissions: team_members:write
|
|
1159
|
+
|
|
1160
|
+
Example:
|
|
1161
|
+
windrunner teams members add TEAM_ID --user-id USER_ID --role TEAM_MEMBER`,
|
|
1162
|
+
).action(async (teamId: string, options: { userId: string; role: string }, command: Command) => {
|
|
1163
|
+
const globalOptions = getGlobalOptions(command);
|
|
1164
|
+
const client = new WindrunnerClient(globalOptions);
|
|
1165
|
+
printResponse(
|
|
1166
|
+
await client.post<TeamMember>(`/teams/${encode(teamId)}/members`, {
|
|
1167
|
+
userId: requireText(options.userId, "--user-id"),
|
|
1168
|
+
role: options.role,
|
|
1169
|
+
}),
|
|
1170
|
+
globalOptions,
|
|
1171
|
+
);
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
addAgentHelp(
|
|
1175
|
+
teamMembers
|
|
1176
|
+
.command("remove")
|
|
1177
|
+
.description("Remove a user from a team")
|
|
1178
|
+
.argument("<teamId>", "Team id")
|
|
1179
|
+
.argument("<userId>", "User id"),
|
|
1180
|
+
`Permissions: team_members:write
|
|
1181
|
+
|
|
1182
|
+
Example:
|
|
1183
|
+
windrunner teams members remove TEAM_ID USER_ID --yes`,
|
|
1184
|
+
).action(async (teamId: string, userId: string, _options: unknown, command: Command) => {
|
|
1185
|
+
const globalOptions = getGlobalOptions(command);
|
|
1186
|
+
const client = new WindrunnerClient(globalOptions);
|
|
1187
|
+
if (!globalOptions.dryRun) {
|
|
1188
|
+
await confirmDelete(`Remove user ${userId} from team ${teamId}?`, globalOptions);
|
|
1189
|
+
}
|
|
1190
|
+
printResponse(await client.delete(`/teams/${encode(teamId)}/members/${encode(userId)}`), globalOptions);
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
addAgentHelp(
|
|
1194
|
+
addPageOptions(
|
|
1195
|
+
teams
|
|
1196
|
+
.command("projects")
|
|
1197
|
+
.description("List projects linked to a team")
|
|
1198
|
+
.argument("<teamId>", "Team id"),
|
|
1199
|
+
),
|
|
1200
|
+
`Permissions: team_projects:read
|
|
1201
|
+
|
|
1202
|
+
Example:
|
|
1203
|
+
windrunner teams projects TEAM_ID --json`,
|
|
1204
|
+
).action(async (teamId: string, options: { page: string; size: string }, command: Command) => {
|
|
1205
|
+
const globalOptions = getGlobalOptions(command);
|
|
1206
|
+
const client = new WindrunnerClient(globalOptions);
|
|
1207
|
+
printResponse(
|
|
1208
|
+
await client.get<ProjectTeam[]>(
|
|
1209
|
+
`/teams/${encode(teamId)}/projects${queryString({
|
|
1210
|
+
page: numberValue(options.page, "page"),
|
|
1211
|
+
size: numberValue(options.size, "size"),
|
|
1212
|
+
})}`,
|
|
1213
|
+
),
|
|
1214
|
+
globalOptions,
|
|
1215
|
+
);
|
|
1216
|
+
});
|
|
1217
|
+
|
|
1218
|
+
const users = program.command("users").description("Resolve users");
|
|
1219
|
+
addAgentHelp(users, "Only limited identity fields are returned by the external API.");
|
|
1220
|
+
|
|
1221
|
+
addAgentHelp(
|
|
1222
|
+
users
|
|
1223
|
+
.command("get")
|
|
1224
|
+
.description("Get limited user identity information")
|
|
1225
|
+
.argument("<userId>", "User id"),
|
|
1226
|
+
`Permissions: users:read
|
|
1227
|
+
|
|
1228
|
+
Example:
|
|
1229
|
+
windrunner users get USER_ID --json`,
|
|
1230
|
+
).action(async (userId: string, _options: unknown, command: Command) => {
|
|
1231
|
+
const globalOptions = getGlobalOptions(command);
|
|
1232
|
+
const client = new WindrunnerClient(globalOptions);
|
|
1233
|
+
printResponse(await client.get<UserIdentity>(`/users/${encode(userId)}`), globalOptions);
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
const auditLogs = program.command("audit-logs").description("Read audit logs");
|
|
1237
|
+
addAgentHelp(auditLogs, "Audit log access requires an administrator or superadministrator API-key owner.");
|
|
1238
|
+
|
|
1239
|
+
addAgentHelp(
|
|
1240
|
+
addPageOptions(
|
|
1241
|
+
auditLogs
|
|
1242
|
+
.command("list")
|
|
1243
|
+
.description("List platform audit logs"),
|
|
1244
|
+
"20",
|
|
1245
|
+
),
|
|
1246
|
+
`Permissions: audit_logs:read
|
|
1247
|
+
|
|
1248
|
+
Example:
|
|
1249
|
+
windrunner audit-logs list --json`,
|
|
1250
|
+
).action(async (options: { page: string; size: string }, command: Command) => {
|
|
1251
|
+
const globalOptions = getGlobalOptions(command);
|
|
1252
|
+
const client = new WindrunnerClient(globalOptions);
|
|
1253
|
+
printResponse(
|
|
1254
|
+
await client.get<AuditLog[]>(`/audit-logs${queryString({
|
|
1255
|
+
page: numberValue(options.page, "page"),
|
|
1256
|
+
size: numberValue(options.size, "size"),
|
|
1257
|
+
})}`),
|
|
1258
|
+
globalOptions,
|
|
1259
|
+
);
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
addAgentHelp(
|
|
1263
|
+
addPageOptions(
|
|
1264
|
+
auditLogs
|
|
1265
|
+
.command("project")
|
|
1266
|
+
.description("List audit logs for a project")
|
|
1267
|
+
.argument("<projectId>", "Project id"),
|
|
1268
|
+
"20",
|
|
1269
|
+
),
|
|
1270
|
+
`Permissions: audit_logs:read
|
|
1271
|
+
|
|
1272
|
+
Example:
|
|
1273
|
+
windrunner audit-logs project PROJECT_ID --json`,
|
|
1274
|
+
).action(async (projectId: string, options: { page: string; size: string }, command: Command) => {
|
|
1275
|
+
const globalOptions = getGlobalOptions(command);
|
|
1276
|
+
const client = new WindrunnerClient(globalOptions);
|
|
1277
|
+
printResponse(
|
|
1278
|
+
await client.get<AuditLog[]>(
|
|
1279
|
+
`/projects/${encode(projectId)}/audit-logs${queryString({
|
|
1280
|
+
page: numberValue(options.page, "page"),
|
|
1281
|
+
size: numberValue(options.size, "size"),
|
|
1282
|
+
})}`,
|
|
1283
|
+
),
|
|
1284
|
+
globalOptions,
|
|
1285
|
+
);
|
|
1286
|
+
});
|
|
1287
|
+
|
|
508
1288
|
try {
|
|
509
1289
|
await program.parseAsync(process.argv);
|
|
510
1290
|
} catch (error) {
|