@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 +1 -1
- package/src/db-tool/service.ts +62 -13
- package/src/shared/prerequisites/runtime.ts +83 -61
package/package.json
CHANGED
package/src/db-tool/service.ts
CHANGED
|
@@ -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
|
|
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 && !
|
|
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(
|
|
600
|
-
: executeSelectQuery(
|
|
637
|
+
? executeMutationQuery(resolvedConfig, sql, password, Number(startTimeMs))
|
|
638
|
+
: executeSelectQuery(resolvedConfig, sql, password, Number(startTimeMs), true);
|
|
601
639
|
|
|
602
640
|
return yield* runWithVpnPrerequisites(
|
|
603
|
-
|
|
604
|
-
runQueryWithOptionalTunnel(
|
|
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
|
|
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(
|
|
679
|
+
? executeSelectQuery(resolvedConfig, getTableNames(), password, Number(startTimeMs))
|
|
641
680
|
: mode === "columns"
|
|
642
|
-
? executeSelectQuery(
|
|
681
|
+
? executeSelectQuery(
|
|
682
|
+
resolvedConfig,
|
|
683
|
+
getColumns(table ?? ""),
|
|
684
|
+
password,
|
|
685
|
+
Number(startTimeMs),
|
|
686
|
+
)
|
|
643
687
|
: mode === "relationships"
|
|
644
|
-
? executeSelectQuery(
|
|
645
|
-
|
|
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
|
-
|
|
649
|
-
runQueryWithOptionalTunnel(
|
|
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
|
-
|
|
130
|
-
|
|
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
|
|
140
|
+
const prerequisiteResult = yield* Effect.gen(function* () {
|
|
141
|
+
const startedDrivers: Array<{ driver: ResolvedVpnDriver; cooldownMs: number }> = [];
|
|
137
142
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
+
const driver = driverResolution.driver;
|
|
161
|
+
const wasConnected = yield* isVpnConnected(driver, runCommand);
|
|
162
|
+
if (wasConnected) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
160
165
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
202
|
-
|
|
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*
|
|
228
|
+
return yield* Effect.fail(prerequisiteResult.failure);
|
|
207
229
|
});
|
|
208
230
|
};
|