@blogic-cz/agent-tools 0.14.4 → 0.14.6

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.14.4",
3
+ "version": "0.14.6",
4
4
  "description": "CLI tools for AI coding agent workflows — GitHub, database, Kubernetes, Azure DevOps, logs, sessions, and audit",
5
5
  "keywords": [
6
6
  "agent",
@@ -76,6 +76,7 @@ export class DbService extends Context.Service<
76
76
  const remotePort = dbConfig.remotePort ?? 5432;
77
77
 
78
78
  const zshrcEnvCache = yield* Ref.make<Record<string, string> | null>(null);
79
+ const envTemplateRegex = /^\$\{([A-Za-z0-9_]+)\}$/;
79
80
 
80
81
  const loadEnvFromZshrc = Effect.fn("DbService.loadEnvFromZshrc")(function* () {
81
82
  const cached = yield* Ref.get(zshrcEnvCache);
@@ -141,6 +142,42 @@ export class DbService extends Context.Service<
141
142
  return "";
142
143
  });
143
144
 
145
+ const resolveConfigString = Effect.fn("DbService.resolveConfigString")(function* (
146
+ value: string,
147
+ env: string,
148
+ label: string,
149
+ zshrcEnv: Record<string, string>,
150
+ ) {
151
+ const match = value.match(envTemplateRegex);
152
+ if (!match) return value;
153
+
154
+ const envVar = match[1];
155
+ const fromEnv = Bun.env[envVar];
156
+ if (fromEnv !== undefined) return fromEnv;
157
+
158
+ const fromZsh = zshrcEnv[envVar];
159
+ if (fromZsh !== undefined) return fromZsh;
160
+
161
+ return yield* new DbConnectionError({
162
+ message: `Environment variable ${envVar} (required for '${label}' config field) is not set in environment ${env}.`,
163
+ environment: env,
164
+ });
165
+ });
166
+
167
+ const resolveDbConfig = Effect.fn("DbService.resolveDbConfig")(function* (
168
+ config: DbConfig,
169
+ env: string,
170
+ ) {
171
+ const needsEnvResolution =
172
+ envTemplateRegex.test(config.user) || envTemplateRegex.test(config.database);
173
+ const zshrcEnv = needsEnvResolution ? yield* loadEnvFromZshrc() : {};
174
+ return {
175
+ ...config,
176
+ user: yield* resolveConfigString(config.user, env, "user", zshrcEnv),
177
+ database: yield* resolveConfigString(config.database, env, "database", zshrcEnv),
178
+ };
179
+ });
180
+
144
181
  const executeShellCommand = (command: ChildProcess.Command) =>
145
182
  Effect.scoped(
146
183
  Effect.gen(function* () {
@@ -584,10 +621,11 @@ export class DbService extends Context.Service<
584
621
  ) {
585
622
  const config = getConfigForEnv(env);
586
623
  const startTimeMs = yield* Clock.currentTimeMillis;
587
- const password = yield* resolvePassword(config, env);
624
+ const resolvedConfig = yield* resolveDbConfig(config, env);
625
+ const password = yield* resolvePassword(resolvedConfig, env);
588
626
  const mutation = isMutationQuery(sql);
589
627
 
590
- if (mutation && !config.allowMutations) {
628
+ if (mutation && !resolvedConfig.allowMutations) {
591
629
  return yield* new DbMutationBlockedError({
592
630
  message:
593
631
  "Mutation queries (UPDATE, INSERT, DELETE, etc.) are not allowed on this environment. Use a local environment for mutations.",
@@ -596,12 +634,12 @@ export class DbService extends Context.Service<
596
634
  }
597
635
 
598
636
  const queryEffect = mutation
599
- ? executeMutationQuery(config, sql, password, Number(startTimeMs))
600
- : executeSelectQuery(config, sql, password, Number(startTimeMs), true);
637
+ ? executeMutationQuery(resolvedConfig, sql, password, Number(startTimeMs))
638
+ : executeSelectQuery(resolvedConfig, sql, password, Number(startTimeMs), true);
601
639
 
602
640
  return yield* runWithVpnPrerequisites(
603
- config.port,
604
- runQueryWithOptionalTunnel(config, queryEffect),
641
+ resolvedConfig.port,
642
+ runQueryWithOptionalTunnel(resolvedConfig, queryEffect),
605
643
  );
606
644
  });
607
645
 
@@ -612,7 +650,8 @@ export class DbService extends Context.Service<
612
650
  ) {
613
651
  const config = getConfigForEnv(env);
614
652
  const startTimeMs = yield* Clock.currentTimeMillis;
615
- const password = yield* resolvePassword(config, env);
653
+ const resolvedConfig = yield* resolveDbConfig(config, env);
654
+ const password = yield* resolvePassword(resolvedConfig, env);
616
655
 
617
656
  if (mode === "columns" && !table) {
618
657
  const endTime = yield* Clock.currentTimeMillis;
@@ -637,16 +676,26 @@ export class DbService extends Context.Service<
637
676
 
638
677
  const queryEffect =
639
678
  mode === "tables"
640
- ? executeSelectQuery(config, getTableNames(), password, Number(startTimeMs))
679
+ ? executeSelectQuery(resolvedConfig, getTableNames(), password, Number(startTimeMs))
641
680
  : mode === "columns"
642
- ? executeSelectQuery(config, getColumns(table ?? ""), password, Number(startTimeMs))
681
+ ? executeSelectQuery(
682
+ resolvedConfig,
683
+ getColumns(table ?? ""),
684
+ password,
685
+ Number(startTimeMs),
686
+ )
643
687
  : mode === "relationships"
644
- ? executeSelectQuery(config, getRelationships(), password, Number(startTimeMs))
645
- : executeFullSchemaQuery(config, password, Number(startTimeMs));
688
+ ? executeSelectQuery(
689
+ resolvedConfig,
690
+ getRelationships(),
691
+ password,
692
+ Number(startTimeMs),
693
+ )
694
+ : executeFullSchemaQuery(resolvedConfig, password, Number(startTimeMs));
646
695
 
647
696
  const result = yield* runWithVpnPrerequisites(
648
- config.port,
649
- runQueryWithOptionalTunnel(config, queryEffect),
697
+ resolvedConfig.port,
698
+ runQueryWithOptionalTunnel(resolvedConfig, queryEffect),
650
699
  );
651
700
 
652
701
  if (result.success) {
@@ -126,83 +126,105 @@ export const runWithProfilePrerequisites = <A, E, CommandError>(
126
126
  }
127
127
 
128
128
  return Effect.gen(function* () {
129
- if (options?.tryWithoutPrerequisites) {
130
- const directResult = yield* effect.pipe(Effect.result);
129
+ const shouldTryDirect = options?.tryWithoutPrerequisites === true;
130
+
131
+ const tryDirect = () => effect.pipe(Effect.result);
132
+
133
+ if (shouldTryDirect) {
134
+ const directResult = yield* tryDirect();
131
135
  if (Result.isSuccess(directResult)) {
132
136
  return directResult.success;
133
137
  }
134
138
  }
135
139
 
136
- const startedDrivers: Array<{ driver: ResolvedVpnDriver; cooldownMs: number }> = [];
140
+ const prerequisiteResult = yield* Effect.gen(function* () {
141
+ const startedDrivers: Array<{ driver: ResolvedVpnDriver; cooldownMs: number }> = [];
137
142
 
138
- for (const prerequisite of vpnPrerequisites) {
139
- const vpnConfig = config.vpns?.[prerequisite.key];
140
- if (!vpnConfig) {
141
- return yield* new PrerequisiteRunError({
142
- message: `VPN prerequisite "${prerequisite.key}" is not defined.`,
143
- hint: `Add vpns.${prerequisite.key} to agent-tools.json5 or remove the prerequisite.`,
144
- });
145
- }
143
+ for (const prerequisite of vpnPrerequisites) {
144
+ const vpnConfig = config.vpns?.[prerequisite.key];
145
+ if (!vpnConfig) {
146
+ return yield* new PrerequisiteRunError({
147
+ message: `VPN prerequisite "${prerequisite.key}" is not defined.`,
148
+ hint: `Add vpns.${prerequisite.key} to agent-tools.json5 or remove the prerequisite.`,
149
+ });
150
+ }
146
151
 
147
- const driverResolution = resolveVpnDriverConfig(vpnConfig);
148
- if (!driverResolution.success) {
149
- return yield* new PrerequisiteRunError({
150
- message: driverResolution.error,
151
- hint: driverResolution.hint,
152
- });
153
- }
152
+ const driverResolution = resolveVpnDriverConfig(vpnConfig);
153
+ if (!driverResolution.success) {
154
+ return yield* new PrerequisiteRunError({
155
+ message: driverResolution.error,
156
+ hint: driverResolution.hint,
157
+ });
158
+ }
154
159
 
155
- const driver = driverResolution.driver;
156
- const wasConnected = yield* isVpnConnected(driver, runCommand);
157
- if (wasConnected) {
158
- continue;
159
- }
160
+ const driver = driverResolution.driver;
161
+ const wasConnected = yield* isVpnConnected(driver, runCommand);
162
+ if (wasConnected) {
163
+ continue;
164
+ }
160
165
 
161
- const startCommand = makeVpnCommand(driver, "start");
162
- const startResult = yield* runCommand(startCommand.command, startCommand.label).pipe(
163
- Effect.mapError(
164
- () =>
165
- new PrerequisiteRunError({
166
- message: `Failed to start VPN prerequisite "${prerequisite.key}".`,
167
- hint: missingVpnToolHint(driver),
168
- }),
169
- ),
170
- );
171
-
172
- if (startResult.exitCode !== 0) {
173
- const stderr = startResult.stderr.trim();
174
- return yield* new PrerequisiteRunError({
175
- message:
176
- stderr !== "" ? stderr : `Failed to start VPN prerequisite "${prerequisite.key}".`,
177
- hint: missingVpnToolHint(driver),
178
- });
179
- }
166
+ const startCommand = makeVpnCommand(driver, "start");
167
+ const startResult = yield* runCommand(startCommand.command, startCommand.label).pipe(
168
+ Effect.mapError(
169
+ () =>
170
+ new PrerequisiteRunError({
171
+ message: `Failed to start VPN prerequisite "${prerequisite.key}".`,
172
+ hint: missingVpnToolHint(driver),
173
+ }),
174
+ ),
175
+ );
176
+
177
+ if (startResult.exitCode !== 0) {
178
+ const stderr = startResult.stderr.trim();
179
+ return yield* new PrerequisiteRunError({
180
+ message:
181
+ stderr !== "" ? stderr : `Failed to start VPN prerequisite "${prerequisite.key}".`,
182
+ hint: missingVpnToolHint(driver),
183
+ });
184
+ }
180
185
 
181
- const ready = yield* waitForVpn(driver, vpnConfig.connectTimeoutMs ?? 30000, runCommand);
182
- if (!ready) {
183
- return yield* new PrerequisiteRunError({
184
- message: `VPN prerequisite "${prerequisite.key}" did not connect within timeout.`,
185
- hint: missingVpnToolHint(driver),
186
- });
187
- }
186
+ const ready = yield* waitForVpn(driver, vpnConfig.connectTimeoutMs ?? 30000, runCommand);
187
+ if (!ready) {
188
+ return yield* new PrerequisiteRunError({
189
+ message: `VPN prerequisite "${prerequisite.key}" did not connect within timeout.`,
190
+ hint: missingVpnToolHint(driver),
191
+ });
192
+ }
188
193
 
189
- const cleanup = prerequisite.cleanup ?? vpnConfig.defaultCleanup ?? "stop-if-started";
190
- if (cleanup === "stop-if-started") {
191
- startedDrivers.push({ driver, cooldownMs: vpnConfig.cooldownMs ?? 0 });
194
+ const cleanup = prerequisite.cleanup ?? vpnConfig.defaultCleanup ?? "stop-if-started";
195
+ if (cleanup === "stop-if-started") {
196
+ startedDrivers.push({ driver, cooldownMs: vpnConfig.cooldownMs ?? 0 });
197
+ }
192
198
  }
193
- }
194
199
 
195
- const cleanup = Effect.gen(function* () {
196
- for (const started of startedDrivers.toReversed()) {
197
- if (started.cooldownMs > 0) {
198
- yield* Effect.sleep(Duration.millis(started.cooldownMs));
200
+ const cleanup = Effect.gen(function* () {
201
+ for (const started of startedDrivers.toReversed()) {
202
+ if (started.cooldownMs > 0) {
203
+ yield* Effect.sleep(Duration.millis(started.cooldownMs));
204
+ }
205
+
206
+ const stopCommand = makeVpnCommand(started.driver, "stop");
207
+ yield* runCommand(stopCommand.command, stopCommand.label).pipe(Effect.ignore);
199
208
  }
209
+ });
210
+
211
+ return yield* effect.pipe(Effect.ensuring(cleanup));
212
+ }).pipe(Effect.result);
200
213
 
201
- const stopCommand = makeVpnCommand(started.driver, "stop");
202
- yield* runCommand(stopCommand.command, stopCommand.label).pipe(Effect.ignore);
214
+ if (Result.isSuccess(prerequisiteResult)) {
215
+ return prerequisiteResult.success;
216
+ }
217
+
218
+ if (shouldTryDirect && prerequisiteResult.failure instanceof PrerequisiteRunError) {
219
+ const directRetryResult = yield* tryDirect();
220
+ if (Result.isSuccess(directRetryResult)) {
221
+ return directRetryResult.success;
203
222
  }
204
- });
223
+ if (!(directRetryResult.failure instanceof PrerequisiteRunError)) {
224
+ return yield* Effect.fail(directRetryResult.failure);
225
+ }
226
+ }
205
227
 
206
- return yield* effect.pipe(Effect.ensuring(cleanup));
228
+ return yield* Effect.fail(prerequisiteResult.failure);
207
229
  });
208
230
  };