@blogic-cz/agent-tools 0.3.0 → 0.4.1

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.1",
4
4
  "description": "CLI tools for AI coding agent workflows — GitHub, database, Kubernetes, Azure DevOps, logs, and sessions",
5
5
  "keywords": [
6
6
  "agent",
@@ -79,10 +79,11 @@ export class DbService extends ServiceMap.Service<
79
79
 
80
80
  const envVars: Record<string, string> = {};
81
81
  const regex = /^export\s+([A-Z_][A-Z0-9_]*)=["']?([^"'\n]+)["']?/gm;
82
- let match: RegExpExecArray | null;
82
+ let match = regex.exec(content);
83
83
 
84
- while ((match = regex.exec(content)) !== null) {
84
+ while (match !== null) {
85
85
  envVars[match[1]] = match[2];
86
+ match = regex.exec(content);
86
87
  }
87
88
 
88
89
  yield* Ref.set(zshrcEnvCache, envVars);
@@ -214,7 +215,7 @@ export class DbService extends ServiceMap.Service<
214
215
  ) => {
215
216
  const args = [
216
217
  "-h",
217
- "localhost",
218
+ config.host,
218
219
  "-p",
219
220
  String(config.port),
220
221
  "-U",
@@ -506,14 +507,18 @@ export class DbService extends ServiceMap.Service<
506
507
  }
507
508
 
508
509
  const isLocal = LOCALHOST_HOSTS.has(envConfig.host);
510
+ const isLocalEnvironment = env === "local";
511
+ const needsTunnel = dbConfig.kubectl !== undefined && !isLocalEnvironment && isLocal;
509
512
 
510
513
  return {
514
+ host: envConfig.host,
511
515
  user: envConfig.user,
512
516
  database: envConfig.database,
517
+ password: envConfig.password,
513
518
  passwordEnvVar: envConfig.passwordEnvVar,
514
519
  port: envConfig.port,
515
- needsTunnel: !isLocal && dbConfig.kubectl !== undefined,
516
- allowMutations: isLocal,
520
+ needsTunnel,
521
+ allowMutations: isLocalEnvironment,
517
522
  };
518
523
  };
519
524
 
@@ -4,6 +4,7 @@ export type { Environment, OutputFormat };
4
4
  export type SchemaMode = "tables" | "columns" | "full" | "relationships";
5
5
 
6
6
  export type DbConfig = {
7
+ host: string;
7
8
  user: string;
8
9
  database: string;
9
10
  password?: string;
@@ -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)"));