@blogic-cz/agent-tools 0.14.45 → 0.14.47
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/README.md +13 -1
- package/package.json +1 -1
- package/src/config/loader.ts +81 -16
- package/src/db-tool/index.ts +7 -2
- package/src/db-tool/service.ts +11 -4
- package/src/db-tool/transformers.ts +7 -2
package/README.md
CHANGED
|
@@ -312,7 +312,19 @@ All settings are optional — audit works out of the box with sensible defaults.
|
|
|
312
312
|
|
|
313
313
|
## Configuration
|
|
314
314
|
|
|
315
|
-
Config is loaded
|
|
315
|
+
Config is loaded by walking up from the current working directory to the nearest regular config file:
|
|
316
|
+
|
|
317
|
+
1. `agent-tools.json`
|
|
318
|
+
2. `agent-tools.json5`
|
|
319
|
+
|
|
320
|
+
That nearest regular config is the base. Local override files are then merged from that directory down to the current working directory:
|
|
321
|
+
|
|
322
|
+
1. `agent-tools.local.json`
|
|
323
|
+
2. `agent-tools.local.json5`
|
|
324
|
+
|
|
325
|
+
Later files override earlier files. Objects are merged deeply; arrays and primitive values are replaced. Missing config = zero-config mode (works for `gh-tool`; others require config).
|
|
326
|
+
|
|
327
|
+
Use `agent-tools.local.json5` for machine-specific ports, paths, and worktree overrides. Keep local files gitignored.
|
|
316
328
|
|
|
317
329
|
### Global Settings
|
|
318
330
|
|
package/package.json
CHANGED
package/src/config/loader.ts
CHANGED
|
@@ -194,8 +194,12 @@ const AgentToolsConfigSchema = Schema.Struct({
|
|
|
194
194
|
github: Schema.optionalKey(Schema.Record(Schema.String, GitHubRepoConfigSchema)),
|
|
195
195
|
});
|
|
196
196
|
|
|
197
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
198
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
199
|
+
}
|
|
200
|
+
|
|
197
201
|
function stripUnknownTopLevelKeys(parsed: unknown): unknown {
|
|
198
|
-
if (
|
|
202
|
+
if (!isRecord(parsed)) {
|
|
199
203
|
return parsed;
|
|
200
204
|
}
|
|
201
205
|
|
|
@@ -223,40 +227,101 @@ export function decodeConfig(
|
|
|
223
227
|
}
|
|
224
228
|
}
|
|
225
229
|
|
|
226
|
-
|
|
230
|
+
const BASE_CONFIG_FILES = ["agent-tools.json", "agent-tools.json5"] as const;
|
|
231
|
+
const LOCAL_CONFIG_FILES = ["agent-tools.local.json", "agent-tools.local.json5"] as const;
|
|
232
|
+
|
|
233
|
+
async function existingFile(filePath: string): Promise<string | undefined> {
|
|
234
|
+
return (await Bun.file(filePath).exists()) ? filePath : undefined;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function findBaseConfigDirectory(
|
|
238
|
+
startDirectory: string = process.cwd(),
|
|
239
|
+
): Promise<string | undefined> {
|
|
227
240
|
let currentDirectory = startDirectory;
|
|
228
241
|
|
|
229
242
|
while (true) {
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
243
|
+
for (const fileName of BASE_CONFIG_FILES) {
|
|
244
|
+
// eslint-disable-next-line eslint/no-await-in-loop -- sequential directory walk, each iteration may short-circuit
|
|
245
|
+
if (await Bun.file(`${currentDirectory}/${fileName}`).exists()) {
|
|
246
|
+
return currentDirectory;
|
|
247
|
+
}
|
|
234
248
|
}
|
|
235
249
|
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
250
|
+
const parentDirectory = dirname(currentDirectory);
|
|
251
|
+
if (parentDirectory === currentDirectory) {
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
currentDirectory = parentDirectory;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function findConfigFiles(startDirectory: string = process.cwd()): Promise<readonly string[]> {
|
|
259
|
+
const baseDirectory = await findBaseConfigDirectory(startDirectory);
|
|
260
|
+
if (!baseDirectory) {
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const directories: string[] = [];
|
|
265
|
+
let currentDirectory = startDirectory;
|
|
266
|
+
while (true) {
|
|
267
|
+
directories.push(currentDirectory);
|
|
268
|
+
if (currentDirectory === baseDirectory) {
|
|
269
|
+
break;
|
|
240
270
|
}
|
|
241
271
|
|
|
242
272
|
const parentDirectory = dirname(currentDirectory);
|
|
243
273
|
if (parentDirectory === currentDirectory) {
|
|
244
|
-
return
|
|
274
|
+
return [];
|
|
245
275
|
}
|
|
246
276
|
currentDirectory = parentDirectory;
|
|
247
277
|
}
|
|
278
|
+
directories.reverse();
|
|
279
|
+
|
|
280
|
+
const configFiles: string[] = [];
|
|
281
|
+
for (const directory of directories) {
|
|
282
|
+
const fileNames =
|
|
283
|
+
directory === baseDirectory
|
|
284
|
+
? [...BASE_CONFIG_FILES, ...LOCAL_CONFIG_FILES]
|
|
285
|
+
: LOCAL_CONFIG_FILES;
|
|
286
|
+
|
|
287
|
+
for (const fileName of fileNames) {
|
|
288
|
+
// eslint-disable-next-line eslint/no-await-in-loop -- config precedence is directory/file order
|
|
289
|
+
const filePath = await existingFile(`${directory}/${fileName}`);
|
|
290
|
+
if (filePath) {
|
|
291
|
+
configFiles.push(filePath);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return configFiles;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function mergeConfigValue(left: unknown, right: unknown): unknown {
|
|
300
|
+
if (!isRecord(left) || !isRecord(right)) {
|
|
301
|
+
return right;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const merged: Record<string, unknown> = { ...left };
|
|
305
|
+
for (const [key, value] of Object.entries(right)) {
|
|
306
|
+
merged[key] = key in merged ? mergeConfigValue(merged[key], value) : value;
|
|
307
|
+
}
|
|
308
|
+
return merged;
|
|
248
309
|
}
|
|
249
310
|
|
|
250
311
|
export async function loadConfig(): Promise<AgentToolsConfig | undefined> {
|
|
251
|
-
const
|
|
252
|
-
if (
|
|
312
|
+
const configPaths = await findConfigFiles();
|
|
313
|
+
if (configPaths.length === 0) {
|
|
253
314
|
return undefined;
|
|
254
315
|
}
|
|
255
316
|
|
|
256
|
-
|
|
257
|
-
const
|
|
317
|
+
let parsed: unknown = {};
|
|
318
|
+
for (const configPath of configPaths) {
|
|
319
|
+
// eslint-disable-next-line eslint/no-await-in-loop -- config precedence is file order
|
|
320
|
+
const fileContent = await Bun.file(configPath).text();
|
|
321
|
+
parsed = mergeConfigValue(parsed, Bun.JSON5.parse(fileContent));
|
|
322
|
+
}
|
|
258
323
|
|
|
259
|
-
return decodeConfig(parsed,
|
|
324
|
+
return decodeConfig(parsed, configPaths.join(", "));
|
|
260
325
|
}
|
|
261
326
|
|
|
262
327
|
export class ConfigService extends Context.Service<ConfigService, AgentToolsConfig | undefined>()(
|
package/src/db-tool/index.ts
CHANGED
|
@@ -64,16 +64,21 @@ const sqlCommand = Command.make(
|
|
|
64
64
|
),
|
|
65
65
|
),
|
|
66
66
|
sql: Flag.string("sql").pipe(Flag.withDescription("SQL query to execute")),
|
|
67
|
+
limit: Flag.optional(Flag.integer("limit")).pipe(
|
|
68
|
+
Flag.withDescription(
|
|
69
|
+
"Max rows to return (default 50). Use 0 for no cap. Prefer a SQL LIMIT for large tables.",
|
|
70
|
+
),
|
|
71
|
+
),
|
|
67
72
|
format: formatOption,
|
|
68
73
|
profile: Flag.optional(Flag.string("profile")).pipe(
|
|
69
74
|
Flag.withDescription("Database profile name from agent-tools.json5 (if multiple configured)"),
|
|
70
75
|
),
|
|
71
76
|
},
|
|
72
|
-
({ env, sql, format }) =>
|
|
77
|
+
({ env, sql, limit, format }) =>
|
|
73
78
|
Effect.gen(function* () {
|
|
74
79
|
const resolvedEnv = yield* resolveEnv(env);
|
|
75
80
|
const db = yield* DbService;
|
|
76
|
-
const result = yield* db.executeQuery(resolvedEnv, sql);
|
|
81
|
+
const result = yield* db.executeQuery(resolvedEnv, sql, Option.getOrUndefined(limit));
|
|
77
82
|
yield* Console.log(formatOutput(result, format));
|
|
78
83
|
}),
|
|
79
84
|
).pipe(Command.withDescription("Execute a SQL query"));
|
package/src/db-tool/service.ts
CHANGED
|
@@ -59,7 +59,11 @@ export { buildApiProbeArgs } from "#shared/k8s-probe";
|
|
|
59
59
|
export class DbService extends Context.Service<
|
|
60
60
|
DbService,
|
|
61
61
|
{
|
|
62
|
-
readonly executeQuery: (
|
|
62
|
+
readonly executeQuery: (
|
|
63
|
+
env: string,
|
|
64
|
+
sql: string,
|
|
65
|
+
limit?: number,
|
|
66
|
+
) => Effect.Effect<QueryResult, DbError>;
|
|
63
67
|
readonly executeSchemaQuery: (
|
|
64
68
|
env: string,
|
|
65
69
|
mode: SchemaMode,
|
|
@@ -83,7 +87,8 @@ export class DbService extends Context.Service<
|
|
|
83
87
|
environment: env,
|
|
84
88
|
});
|
|
85
89
|
return {
|
|
86
|
-
executeQuery: (env: string, _sql: string) =>
|
|
90
|
+
executeQuery: (env: string, _sql: string, _limit?: number) =>
|
|
91
|
+
Effect.fail(noConfigError(env)),
|
|
87
92
|
executeSchemaQuery: (env: string, _mode: SchemaMode, _table?: string) =>
|
|
88
93
|
Effect.fail(noConfigError(env)),
|
|
89
94
|
};
|
|
@@ -468,6 +473,7 @@ export class DbService extends Context.Service<
|
|
|
468
473
|
password: string,
|
|
469
474
|
startTimeMs: number,
|
|
470
475
|
applyTransform = false,
|
|
476
|
+
limit?: number,
|
|
471
477
|
) {
|
|
472
478
|
const selectSql = sql.trim().replace(/;\s*$/, "");
|
|
473
479
|
const wrappedSql = `SELECT json_agg(t) FROM (${selectSql}) t;`;
|
|
@@ -532,7 +538,7 @@ export class DbService extends Context.Service<
|
|
|
532
538
|
});
|
|
533
539
|
|
|
534
540
|
const transformed = applyTransform
|
|
535
|
-
? transformQueryResult(rawData)
|
|
541
|
+
? transformQueryResult(rawData, limit)
|
|
536
542
|
: {
|
|
537
543
|
data: rawData,
|
|
538
544
|
showing: rawData.length,
|
|
@@ -720,6 +726,7 @@ export class DbService extends Context.Service<
|
|
|
720
726
|
const executeQuery = Effect.fn("DbService.executeQuery")(function* (
|
|
721
727
|
env: string,
|
|
722
728
|
sql: string,
|
|
729
|
+
limit?: number,
|
|
723
730
|
) {
|
|
724
731
|
const config = yield* getConfigForEnv(env);
|
|
725
732
|
const startTimeMs = yield* Clock.currentTimeMillis;
|
|
@@ -753,7 +760,7 @@ export class DbService extends Context.Service<
|
|
|
753
760
|
|
|
754
761
|
const queryEffect = mutation
|
|
755
762
|
? executeMutationQuery(resolvedConfig, sql, password, Number(startTimeMs))
|
|
756
|
-
: executeSelectQuery(resolvedConfig, sql, password, Number(startTimeMs), true);
|
|
763
|
+
: executeSelectQuery(resolvedConfig, sql, password, Number(startTimeMs), true, limit);
|
|
757
764
|
|
|
758
765
|
return yield* runWithVpnPrerequisites(
|
|
759
766
|
resolvedConfig.port,
|
|
@@ -22,12 +22,17 @@ function truncateValue(value: unknown): unknown {
|
|
|
22
22
|
return `${value.slice(0, MAX_VALUE_LENGTH)}...`;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export function transformQueryResult(
|
|
25
|
+
export function transformQueryResult(
|
|
26
|
+
data: Record<string, unknown>[],
|
|
27
|
+
// limit 0 (or negative) means "no row cap".
|
|
28
|
+
limit: number = DEFAULT_ROW_LIMIT,
|
|
29
|
+
): TransformResult {
|
|
26
30
|
const withoutEmptyColumns = stripEmptyColumns(data);
|
|
27
31
|
const truncatedValues = withoutEmptyColumns.map((record) =>
|
|
28
32
|
Object.fromEntries(Object.entries(record).map(([key, value]) => [key, truncateValue(value)])),
|
|
29
33
|
);
|
|
30
|
-
const
|
|
34
|
+
const effectiveLimit = limit > 0 ? limit : Number.POSITIVE_INFINITY;
|
|
35
|
+
const { rows, truncated, total, showing } = truncateRows(truncatedValues, effectiveLimit);
|
|
31
36
|
|
|
32
37
|
return {
|
|
33
38
|
data: rows,
|