@blogic-cz/agent-tools 0.3.0 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blogic-cz/agent-tools",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "CLI tools for AI coding agent workflows — GitHub, database, Kubernetes, Azure DevOps, logs, and sessions",
5
5
  "keywords": [
6
6
  "agent",
@@ -34,6 +34,14 @@ import {
34
34
  prReplyAndResolveCommand,
35
35
  prReviewTriageCommand,
36
36
  } from "./pr/index";
37
+ import {
38
+ releaseCreateCommand,
39
+ releaseDeleteCommand,
40
+ releaseEditCommand,
41
+ releaseListCommand,
42
+ releaseStatusCommand,
43
+ releaseViewCommand,
44
+ } from "./release";
37
45
  import { repoInfoCommand, repoListCommand, repoSearchCodeCommand } from "./repo";
38
46
  import { GitHubService } from "./service";
39
47
  import {
@@ -108,6 +116,18 @@ const workflowCommand = Command.make("workflow", {}).pipe(
108
116
  ]),
109
117
  );
110
118
 
119
+ const releaseCommand = Command.make("release", {}).pipe(
120
+ Command.withDescription("Release operations (create, list, view, edit, delete, status)"),
121
+ Command.withSubcommands([
122
+ releaseCreateCommand,
123
+ releaseListCommand,
124
+ releaseViewCommand,
125
+ releaseEditCommand,
126
+ releaseDeleteCommand,
127
+ releaseStatusCommand,
128
+ ]),
129
+ );
130
+
111
131
  const mainCommand = Command.make("gh-tool", {}).pipe(
112
132
  Command.withDescription(
113
133
  `GitHub CLI Tool for Coding Agents
@@ -130,9 +150,12 @@ WORKFLOW FOR AI AGENTS:
130
150
  12. Use 'workflow view --run N' to inspect a specific run with jobs/steps
131
151
  13. Use 'workflow logs --run N' to get logs (failed jobs by default)
132
152
  14. Use 'workflow job-logs --run N --job "build-web-app"' to get clean parsed logs for a specific job
133
- 15. Use 'workflow watch --run N' to watch until completion`,
153
+ 15. Use 'workflow watch --run N' to watch until completion
154
+ 16. Use 'release status' to inspect latest release + repository context
155
+ 17. Use 'release create --tag vX.Y.Z --generate-notes' to publish a release
156
+ 18. Use 'release edit/view/list/delete' to maintain existing releases`,
134
157
  ),
135
- Command.withSubcommands([prCommand, issueCommand, repoCommand, workflowCommand]),
158
+ Command.withSubcommands([prCommand, issueCommand, repoCommand, workflowCommand, releaseCommand]),
136
159
  );
137
160
 
138
161
  const cli = Command.run(mainCommand, {
@@ -0,0 +1,561 @@
1
+ import { Command, Flag } from "effect/unstable/cli";
2
+ import { Effect, Option } from "effect";
3
+
4
+ import { formatOption, logFormatted } from "#shared";
5
+ import { GitHubService } from "./service";
6
+
7
+ type ReleaseListItem = {
8
+ tagName: string;
9
+ name: string;
10
+ isDraft: boolean;
11
+ isPrerelease: boolean;
12
+ createdAt: string;
13
+ publishedAt: string | null;
14
+ };
15
+
16
+ type ReleaseAuthor = {
17
+ login: string;
18
+ };
19
+
20
+ type ReleaseAsset = {
21
+ name: string;
22
+ size: number;
23
+ downloadCount: number;
24
+ contentType: string;
25
+ createdAt: string;
26
+ updatedAt: string;
27
+ url: string;
28
+ };
29
+
30
+ type ReleaseDetail = {
31
+ tagName: string;
32
+ name: string;
33
+ isDraft: boolean;
34
+ isPrerelease: boolean;
35
+ createdAt: string;
36
+ publishedAt: string | null;
37
+ url: string;
38
+ body: string;
39
+ targetCommitish: string;
40
+ author: ReleaseAuthor | null;
41
+ assets: ReleaseAsset[];
42
+ };
43
+
44
+ type ReleaseCreateResult = {
45
+ created: true;
46
+ tagName: string;
47
+ name: string;
48
+ url: string;
49
+ isDraft: boolean;
50
+ isPrerelease: boolean;
51
+ };
52
+
53
+ type ReleaseEditResult = {
54
+ edited: true;
55
+ tagName: string;
56
+ name: string;
57
+ url: string;
58
+ isDraft: boolean;
59
+ isPrerelease: boolean;
60
+ };
61
+
62
+ type ReleaseDeleteResult = {
63
+ deleted: boolean;
64
+ tagName: string;
65
+ tagCleaned: boolean;
66
+ dryRun?: true;
67
+ message?: string;
68
+ };
69
+
70
+ type LatestRelease = {
71
+ tagName: string;
72
+ name: string;
73
+ createdAt: string;
74
+ url: string;
75
+ };
76
+
77
+ type ReleaseStatusResult = {
78
+ latestRelease: LatestRelease | null;
79
+ repo: {
80
+ owner: string;
81
+ name: string;
82
+ defaultBranch: string;
83
+ url: string;
84
+ };
85
+ };
86
+
87
+ const listReleases = Effect.fn("release.listReleases")(function* (opts: {
88
+ limit: number;
89
+ repo: string | null;
90
+ }) {
91
+ const gh = yield* GitHubService;
92
+
93
+ const args = [
94
+ "release",
95
+ "list",
96
+ "--json",
97
+ "tagName,name,isDraft,isPrerelease,createdAt,publishedAt",
98
+ "--limit",
99
+ String(opts.limit),
100
+ ];
101
+
102
+ if (opts.repo !== null) {
103
+ args.push("--repo", opts.repo);
104
+ }
105
+
106
+ return yield* gh.runGhJson<ReleaseListItem[]>(args);
107
+ });
108
+
109
+ const viewRelease = Effect.fn("release.viewRelease")(function* (opts: {
110
+ tag: string;
111
+ repo: string | null;
112
+ }) {
113
+ const gh = yield* GitHubService;
114
+
115
+ const args = [
116
+ "release",
117
+ "view",
118
+ opts.tag,
119
+ "--json",
120
+ "tagName,name,isDraft,isPrerelease,createdAt,publishedAt,url,body,targetCommitish,author,assets",
121
+ ];
122
+
123
+ if (opts.repo !== null) {
124
+ args.push("--repo", opts.repo);
125
+ }
126
+
127
+ return yield* gh.runGhJson<ReleaseDetail>(args);
128
+ });
129
+
130
+ const createRelease = Effect.fn("release.createRelease")(function* (opts: {
131
+ tag: string;
132
+ title: string | null;
133
+ body: string | null;
134
+ notesFile: string | null;
135
+ draft: boolean;
136
+ prerelease: boolean;
137
+ generateNotes: boolean;
138
+ notesStartTag: string | null;
139
+ target: string | null;
140
+ verifyTag: boolean;
141
+ latest: boolean | null;
142
+ repo: string | null;
143
+ }) {
144
+ const gh = yield* GitHubService;
145
+
146
+ const args = ["release", "create", opts.tag];
147
+
148
+ if (opts.title !== null) {
149
+ args.push("--title", opts.title);
150
+ }
151
+
152
+ if (opts.body !== null) {
153
+ args.push("--notes", opts.body);
154
+ }
155
+
156
+ if (opts.notesFile !== null) {
157
+ args.push("--notes-file", opts.notesFile);
158
+ }
159
+
160
+ if (opts.draft) {
161
+ args.push("--draft");
162
+ }
163
+
164
+ if (opts.prerelease) {
165
+ args.push("--prerelease");
166
+ }
167
+
168
+ if (opts.generateNotes) {
169
+ args.push("--generate-notes");
170
+ }
171
+
172
+ if (opts.notesStartTag !== null) {
173
+ args.push("--notes-start-tag", opts.notesStartTag);
174
+ }
175
+
176
+ if (opts.target !== null) {
177
+ args.push("--target", opts.target);
178
+ }
179
+
180
+ if (opts.verifyTag) {
181
+ args.push("--verify-tag");
182
+ }
183
+
184
+ if (opts.latest !== null) {
185
+ args.push(`--latest=${opts.latest ? "true" : "false"}`);
186
+ }
187
+
188
+ if (opts.repo !== null) {
189
+ args.push("--repo", opts.repo);
190
+ }
191
+
192
+ const result = yield* gh.runGh(args);
193
+ const lines = result.stdout
194
+ .trim()
195
+ .split("\n")
196
+ .map((line) => line.trim())
197
+ .filter((line) => line.length > 0);
198
+
199
+ const url = lines.length > 0 ? lines[lines.length - 1] : "";
200
+
201
+ const created: ReleaseCreateResult = {
202
+ created: true,
203
+ tagName: opts.tag,
204
+ name: opts.title ?? opts.tag,
205
+ url,
206
+ isDraft: opts.draft,
207
+ isPrerelease: opts.prerelease,
208
+ };
209
+
210
+ return created;
211
+ });
212
+
213
+ const editRelease = Effect.fn("release.editRelease")(function* (opts: {
214
+ tag: string;
215
+ title: string | null;
216
+ body: string | null;
217
+ draft: boolean | null;
218
+ prerelease: boolean | null;
219
+ latest: boolean | null;
220
+ repo: string | null;
221
+ }) {
222
+ const gh = yield* GitHubService;
223
+
224
+ const args = ["release", "edit", opts.tag];
225
+
226
+ if (opts.title !== null) {
227
+ args.push("--title", opts.title);
228
+ }
229
+
230
+ if (opts.body !== null) {
231
+ args.push("--notes", opts.body);
232
+ }
233
+
234
+ if (opts.draft !== null) {
235
+ args.push(`--draft=${opts.draft ? "true" : "false"}`);
236
+ }
237
+
238
+ if (opts.prerelease !== null) {
239
+ args.push(`--prerelease=${opts.prerelease ? "true" : "false"}`);
240
+ }
241
+
242
+ if (opts.latest !== null) {
243
+ args.push(`--latest=${opts.latest ? "true" : "false"}`);
244
+ }
245
+
246
+ if (opts.repo !== null) {
247
+ args.push("--repo", opts.repo);
248
+ }
249
+
250
+ yield* gh.runGh(args);
251
+
252
+ const updated = yield* viewRelease({
253
+ tag: opts.tag,
254
+ repo: opts.repo,
255
+ });
256
+
257
+ const edited: ReleaseEditResult = {
258
+ edited: true,
259
+ tagName: updated.tagName,
260
+ name: updated.name,
261
+ url: updated.url,
262
+ isDraft: updated.isDraft,
263
+ isPrerelease: updated.isPrerelease,
264
+ };
265
+
266
+ return edited;
267
+ });
268
+
269
+ const deleteRelease = Effect.fn("release.deleteRelease")(function* (opts: {
270
+ tag: string;
271
+ cleanupTag: boolean;
272
+ confirm: boolean;
273
+ repo: string | null;
274
+ }) {
275
+ const gh = yield* GitHubService;
276
+
277
+ if (!opts.confirm) {
278
+ const scope = opts.repo !== null ? ` in ${opts.repo}` : "";
279
+ const cleanup = opts.cleanupTag ? " and its git tag" : "";
280
+
281
+ const dryRun: ReleaseDeleteResult = {
282
+ deleted: false,
283
+ tagName: opts.tag,
284
+ tagCleaned: opts.cleanupTag,
285
+ dryRun: true,
286
+ message: `Dry run: would delete release ${opts.tag}${cleanup}${scope}. Re-run with --confirm to execute.`,
287
+ };
288
+
289
+ return dryRun;
290
+ }
291
+
292
+ const args = ["release", "delete", opts.tag, "--yes"];
293
+
294
+ if (opts.cleanupTag) {
295
+ args.push("--cleanup-tag");
296
+ }
297
+
298
+ if (opts.repo !== null) {
299
+ args.push("--repo", opts.repo);
300
+ }
301
+
302
+ yield* gh.runGh(args);
303
+
304
+ const deleted: ReleaseDeleteResult = {
305
+ deleted: true,
306
+ tagName: opts.tag,
307
+ tagCleaned: opts.cleanupTag,
308
+ };
309
+
310
+ return deleted;
311
+ });
312
+
313
+ const releaseStatus = Effect.fn("release.releaseStatus")(function* (repo: string | null) {
314
+ const gh = yield* GitHubService;
315
+
316
+ const args = ["release", "list", "--json", "tagName,name,createdAt,url", "--limit", "1"];
317
+ if (repo !== null) {
318
+ args.push("--repo", repo);
319
+ }
320
+
321
+ const [repoInfo, releases] = yield* Effect.all([
322
+ gh.getRepoInfo(),
323
+ gh.runGhJson<LatestRelease[]>(args),
324
+ ]);
325
+
326
+ const result: ReleaseStatusResult = {
327
+ latestRelease: releases.length > 0 ? releases[0] : null,
328
+ repo: {
329
+ owner: repoInfo.owner,
330
+ name: repoInfo.name,
331
+ defaultBranch: repoInfo.defaultBranch,
332
+ url: repoInfo.url,
333
+ },
334
+ };
335
+
336
+ return result;
337
+ });
338
+
339
+ export const releaseCreateCommand = Command.make(
340
+ "create",
341
+ {
342
+ body: Flag.string("body").pipe(
343
+ Flag.withDescription("Release notes body (markdown)"),
344
+ Flag.optional,
345
+ ),
346
+ draft: Flag.boolean("draft").pipe(
347
+ Flag.withDescription("Create as draft release"),
348
+ Flag.withDefault(false),
349
+ ),
350
+ format: formatOption,
351
+ generateNotes: Flag.boolean("generate-notes").pipe(
352
+ Flag.withDescription("Automatically generate release notes"),
353
+ Flag.withDefault(false),
354
+ ),
355
+ latest: Flag.boolean("latest").pipe(
356
+ Flag.withDescription("Mark this release as latest (true/false). Omit to leave unchanged"),
357
+ Flag.optional,
358
+ ),
359
+ notesFile: Flag.string("notes-file").pipe(
360
+ Flag.withDescription("Path to release notes file (passed to gh --notes-file)"),
361
+ Flag.optional,
362
+ ),
363
+ notesStartTag: Flag.string("notes-start-tag").pipe(
364
+ Flag.withDescription("Tag to start generating notes from"),
365
+ Flag.optional,
366
+ ),
367
+ prerelease: Flag.boolean("prerelease").pipe(
368
+ Flag.withDescription("Mark as pre-release"),
369
+ Flag.withDefault(false),
370
+ ),
371
+ repo: Flag.string("repo").pipe(
372
+ Flag.withDescription("Target repository (owner/name). Defaults to current repo"),
373
+ Flag.optional,
374
+ ),
375
+ tag: Flag.string("tag").pipe(Flag.withDescription("Tag name for release (e.g., v1.2.3)")),
376
+ target: Flag.string("target").pipe(
377
+ Flag.withDescription("Target branch or commit SHA for tag"),
378
+ Flag.optional,
379
+ ),
380
+ title: Flag.string("title").pipe(
381
+ Flag.withDescription("Release title (defaults to tag)"),
382
+ Flag.optional,
383
+ ),
384
+ verifyTag: Flag.boolean("verify-tag").pipe(
385
+ Flag.withDescription("Abort if tag does not already exist in remote"),
386
+ Flag.withDefault(false),
387
+ ),
388
+ },
389
+ ({
390
+ body,
391
+ draft,
392
+ format,
393
+ generateNotes,
394
+ latest,
395
+ notesFile,
396
+ notesStartTag,
397
+ prerelease,
398
+ repo,
399
+ tag,
400
+ target,
401
+ title,
402
+ verifyTag,
403
+ }) =>
404
+ Effect.gen(function* () {
405
+ const result = yield* createRelease({
406
+ tag,
407
+ title: Option.getOrNull(title),
408
+ body: Option.getOrNull(body),
409
+ notesFile: Option.getOrNull(notesFile),
410
+ draft,
411
+ prerelease,
412
+ generateNotes,
413
+ notesStartTag: Option.getOrNull(notesStartTag),
414
+ target: Option.getOrNull(target),
415
+ verifyTag,
416
+ latest: Option.getOrNull(latest),
417
+ repo: Option.getOrNull(repo),
418
+ });
419
+
420
+ yield* logFormatted(result, format);
421
+ }),
422
+ ).pipe(
423
+ Command.withDescription(
424
+ "Create a release (supports --notes-file, --generate-notes, --target, --verify-tag, --latest)",
425
+ ),
426
+ );
427
+
428
+ export const releaseListCommand = Command.make(
429
+ "list",
430
+ {
431
+ format: formatOption,
432
+ limit: Flag.integer("limit").pipe(
433
+ Flag.withDescription("Maximum number of releases to return"),
434
+ Flag.withDefault(10),
435
+ ),
436
+ repo: Flag.string("repo").pipe(
437
+ Flag.withDescription("Target repository (owner/name). Defaults to current repo"),
438
+ Flag.optional,
439
+ ),
440
+ },
441
+ ({ format, limit, repo }) =>
442
+ Effect.gen(function* () {
443
+ const releases = yield* listReleases({
444
+ limit,
445
+ repo: Option.getOrNull(repo),
446
+ });
447
+
448
+ yield* logFormatted(releases, format);
449
+ }),
450
+ ).pipe(Command.withDescription("List releases"));
451
+
452
+ export const releaseViewCommand = Command.make(
453
+ "view",
454
+ {
455
+ format: formatOption,
456
+ repo: Flag.string("repo").pipe(
457
+ Flag.withDescription("Target repository (owner/name). Defaults to current repo"),
458
+ Flag.optional,
459
+ ),
460
+ tag: Flag.string("tag").pipe(Flag.withDescription("Release tag to view (e.g., v1.2.3)")),
461
+ },
462
+ ({ format, repo, tag }) =>
463
+ Effect.gen(function* () {
464
+ const release = yield* viewRelease({
465
+ tag,
466
+ repo: Option.getOrNull(repo),
467
+ });
468
+
469
+ yield* logFormatted(release, format);
470
+ }),
471
+ ).pipe(Command.withDescription("View release details by tag"));
472
+
473
+ export const releaseEditCommand = Command.make(
474
+ "edit",
475
+ {
476
+ body: Flag.string("body").pipe(
477
+ Flag.withDescription("New release notes body (markdown)"),
478
+ Flag.optional,
479
+ ),
480
+ draft: Flag.boolean("draft").pipe(
481
+ Flag.withDescription("Set draft status (true/false). Omit to keep current value"),
482
+ Flag.optional,
483
+ ),
484
+ format: formatOption,
485
+ latest: Flag.boolean("latest").pipe(
486
+ Flag.withDescription("Set latest status (true/false). Omit to keep current value"),
487
+ Flag.optional,
488
+ ),
489
+ prerelease: Flag.boolean("prerelease").pipe(
490
+ Flag.withDescription("Set prerelease status (true/false). Omit to keep current value"),
491
+ Flag.optional,
492
+ ),
493
+ repo: Flag.string("repo").pipe(
494
+ Flag.withDescription("Target repository (owner/name). Defaults to current repo"),
495
+ Flag.optional,
496
+ ),
497
+ tag: Flag.string("tag").pipe(Flag.withDescription("Release tag to edit (e.g., v1.2.3)")),
498
+ title: Flag.string("title").pipe(Flag.withDescription("New release title"), Flag.optional),
499
+ },
500
+ ({ body, draft, format, latest, prerelease, repo, tag, title }) =>
501
+ Effect.gen(function* () {
502
+ const edited = yield* editRelease({
503
+ tag,
504
+ title: Option.getOrNull(title),
505
+ body: Option.getOrNull(body),
506
+ draft: Option.getOrNull(draft),
507
+ prerelease: Option.getOrNull(prerelease),
508
+ latest: Option.getOrNull(latest),
509
+ repo: Option.getOrNull(repo),
510
+ });
511
+
512
+ yield* logFormatted(edited, format);
513
+ }),
514
+ ).pipe(Command.withDescription("Edit an existing release"));
515
+
516
+ export const releaseDeleteCommand = Command.make(
517
+ "delete",
518
+ {
519
+ cleanupTag: Flag.boolean("cleanup-tag").pipe(
520
+ Flag.withDescription("Also delete the git tag from remote"),
521
+ Flag.withDefault(false),
522
+ ),
523
+ confirm: Flag.boolean("confirm").pipe(
524
+ Flag.withDescription("Actually delete release (without this flag, only shows dry-run)"),
525
+ Flag.withDefault(false),
526
+ ),
527
+ format: formatOption,
528
+ repo: Flag.string("repo").pipe(
529
+ Flag.withDescription("Target repository (owner/name). Defaults to current repo"),
530
+ Flag.optional,
531
+ ),
532
+ tag: Flag.string("tag").pipe(Flag.withDescription("Release tag to delete (e.g., v1.2.3)")),
533
+ },
534
+ ({ cleanupTag, confirm, format, repo, tag }) =>
535
+ Effect.gen(function* () {
536
+ const result = yield* deleteRelease({
537
+ tag,
538
+ cleanupTag,
539
+ confirm,
540
+ repo: Option.getOrNull(repo),
541
+ });
542
+
543
+ yield* logFormatted(result, format);
544
+ }),
545
+ ).pipe(Command.withDescription("Delete a release (dry-run by default, use --confirm to execute)"));
546
+
547
+ export const releaseStatusCommand = Command.make(
548
+ "status",
549
+ {
550
+ format: formatOption,
551
+ repo: Flag.string("repo").pipe(
552
+ Flag.withDescription("Target repository (owner/name). Defaults to current repo"),
553
+ Flag.optional,
554
+ ),
555
+ },
556
+ ({ format, repo }) =>
557
+ Effect.gen(function* () {
558
+ const status = yield* releaseStatus(Option.getOrNull(repo));
559
+ yield* logFormatted(status, format);
560
+ }),
561
+ ).pipe(Command.withDescription("Show release readiness status (latest release + repository info)"));