@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/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("0.1.0")
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, "Permissions: projects:read for both project commands.");
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<JsonObject>(
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) {