@blogic-cz/agent-tools 0.17.0 → 0.17.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.
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/az-tool/service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAkB,MAAM,QAAQ,CAAC;AAEhE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAI5C,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAGzF,OAAO,EAAE,aAAa,EAAiB,MAAM,SAAS,CAAC;;yBAK9B,CACnB,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,MAAM,KACb,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,eAAe,GAAG,cAAc,GAAG,cAAc,GAAG,YAAY,CAAC;wBACxE,CAClB,MAAM,EAAE,YAAY,KACjB,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,eAAe,GAAG,cAAc,GAAG,cAAc,GAAG,YAAY,CAAC;;AATjG,qBAAa,SAAU,SAAQ,cAWF;IAC3B,MAAM,CAAC,QAAQ,CAAC,KAAK,yFA6MnB;CACH;AAED,eAAO,MAAM,cAAc,wFAAkB,CAAC"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/az-tool/service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAkB,MAAM,QAAQ,CAAC;AAEhE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAI5C,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAGzF,OAAO,EAAE,aAAa,EAAiB,MAAM,SAAS,CAAC;;yBAM9B,CACnB,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,MAAM,KACb,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,eAAe,GAAG,cAAc,GAAG,cAAc,GAAG,YAAY,CAAC;wBACxE,CAClB,MAAM,EAAE,YAAY,KACjB,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,eAAe,GAAG,cAAc,GAAG,cAAc,GAAG,YAAY,CAAC;;AATjG,qBAAa,SAAU,SAAQ,cAWF;IAC3B,MAAM,CAAC,QAAQ,CAAC,KAAK,yFAmNnB;CACH;AAED,eAAO,MAAM,cAAc,wFAAkB,CAAC"}
@@ -0,0 +1,26 @@
1
+ /** Kept free of I/O so the caller spawns kubectl itself instead of piping through sh, jq and head. */
2
+ type KubeContextEntry = {
3
+ readonly name?: unknown;
4
+ readonly context?: {
5
+ readonly cluster?: unknown;
6
+ };
7
+ };
8
+ type KubeClusterEntry = {
9
+ readonly name?: unknown;
10
+ readonly cluster?: {
11
+ readonly server?: unknown;
12
+ };
13
+ };
14
+ type KubeConfigView = {
15
+ readonly contexts?: readonly KubeContextEntry[];
16
+ readonly clusters?: readonly KubeClusterEntry[];
17
+ };
18
+ export declare const parseKubeConfigView: (stdout: string) => KubeConfigView | undefined;
19
+ /**
20
+ * Two strategies, in the same order the previous jq pipelines used:
21
+ * an exact context.cluster match first, then a cluster whose server URL contains the id.
22
+ * Returns the first match so a multi-context kubeconfig resolves the same way it did before.
23
+ */
24
+ export declare const selectKubeContext: (view: KubeConfigView | undefined, clusterId: string) => string | undefined;
25
+ export {};
26
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/k8s-tool/context.ts"],"names":[],"mappings":"AAAA,sGAAsG;AAEtG,KAAK,gBAAgB,GAAG;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CACnD,CAAC;AAEF,KAAK,gBAAgB,GAAG;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CAClD,CAAC;AAEF,KAAK,cAAc,GAAG;IACpB,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAChD,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;CACjD,CAAC;AAKF,eAAO,MAAM,mBAAmB,GAAI,QAAQ,MAAM,KAAG,cAAc,GAAG,SAOrE,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAC5B,MAAM,cAAc,GAAG,SAAS,EAChC,WAAW,MAAM,KAChB,MAAM,GAAG,SAmCX,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/k8s-tool/service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAuB,MAAM,QAAQ,CAAC;AAErE,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE1D,OAAO,EACL,eAAe,EACf,eAAe,EACf,wBAAwB,EACxB,eAAe,EAChB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,aAAa,EAAiB,MAAM,SAAS,CAAC;;yBAc9B,CACnB,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,WAAW,EAChB,OAAO,CAAC,EAAE,MAAM,KACb,MAAM,CAAC,MAAM,CAChB,MAAM,EACN,eAAe,GAAG,eAAe,GAAG,eAAe,GAAG,wBAAwB,CAC/E;yBACoB,CACnB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,OAAO,EACf,OAAO,CAAC,EAAE,MAAM,KACb,MAAM,CAAC,MAAM,CAChB,aAAa,EACb,eAAe,GAAG,eAAe,GAAG,eAAe,GAAG,wBAAwB,CAC/E;yBACoB,CACnB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,MAAM,KACb,MAAM,CAAC,MAAM,CAChB,aAAa,EACb,eAAe,GAAG,eAAe,GAAG,eAAe,GAAG,wBAAwB,CAC/E;;AA5BL,qBAAa,UAAW,SAAQ,eA8BF;IAC5B,MAAM,CAAC,QAAQ,CAAC,KAAK,0FAwdnB;CACH;AAED,eAAO,MAAM,eAAe,yFAAmB,CAAC;AAEhD,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/k8s-tool/service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAe,MAAM,QAAQ,CAAC;AAE7D,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE1D,OAAO,EACL,eAAe,EACf,eAAe,EACf,wBAAwB,EACxB,eAAe,EAChB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,aAAa,EAAiB,MAAM,SAAS,CAAC;;yBAe9B,CACnB,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,WAAW,EAChB,OAAO,CAAC,EAAE,MAAM,KACb,MAAM,CAAC,MAAM,CAChB,MAAM,EACN,eAAe,GAAG,eAAe,GAAG,eAAe,GAAG,wBAAwB,CAC/E;yBACoB,CACnB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,OAAO,EACf,OAAO,CAAC,EAAE,MAAM,KACb,MAAM,CAAC,MAAM,CAChB,aAAa,EACb,eAAe,GAAG,eAAe,GAAG,eAAe,GAAG,wBAAwB,CAC/E;yBACoB,CACnB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,MAAM,KACb,MAAM,CAAC,MAAM,CAChB,aAAa,EACb,eAAe,GAAG,eAAe,GAAG,eAAe,GAAG,wBAAwB,CAC/E;;AA5BL,qBAAa,UAAW,SAAQ,eA8BF;IAC5B,MAAM,CAAC,QAAQ,CAAC,KAAK,0FA2bnB;CACH;AAED,eAAO,MAAM,eAAe,yFAAmB,CAAC;AAEhD,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC"}
@@ -1,4 +1,3 @@
1
- import { ChildProcessSpawner } from "effect/unstable/process";
2
1
  import { Context, Effect, Layer } from "effect";
3
2
  import type { Environment, LogFile, ReadOptions } from "./types";
4
3
  import { K8sService } from "#k8s/service";
@@ -6,18 +5,13 @@ import { ConfigService } from "#config/loader";
6
5
  import { type LogsError } from "./errors";
7
6
  export declare const parseLogFiles: (output: string) => LogFile[];
8
7
  export declare const formatPrettyOutput: (output: string) => string;
9
- /**
10
- * Sanitize a string for safe use in shell commands by escaping single quotes
11
- * and wrapping in single quotes. This prevents shell injection.
12
- */
13
- export declare const sanitizeShellArg: (input: string) => string;
14
8
  declare const LogsService_base: Context.ServiceClass<LogsService, "@agent-tools/LogsService", {
15
9
  readonly listLogs: (env: Environment, profile?: string) => Effect.Effect<LogFile[], LogsError>;
16
10
  readonly readLogs: (env: Environment, options: ReadOptions, profile?: string) => Effect.Effect<string, LogsError>;
17
11
  }>;
18
12
  export declare class LogsService extends LogsService_base {
19
- static readonly layer: Layer.Layer<LogsService, never, ConfigService | ChildProcessSpawner.ChildProcessSpawner | K8sService>;
13
+ static readonly layer: Layer.Layer<LogsService, never, ConfigService | K8sService>;
20
14
  }
21
- export declare const LogsServiceLayer: Layer.Layer<LogsService, import("#config/loader").ConfigLoadError, ChildProcessSpawner.ChildProcessSpawner>;
15
+ export declare const LogsServiceLayer: Layer.Layer<LogsService, import("#config/loader").ConfigLoadError, import("effect/unstable/process/ChildProcessSpawner").ChildProcessSpawner>;
22
16
  export {};
23
17
  //# sourceMappingURL=service.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/logs-tool/service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAU,MAAM,QAAQ,CAAC;AAExD,OAAO,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAEjE,OAAO,EAAE,UAAU,EAAmB,MAAM,cAAc,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAqC,MAAM,gBAAgB,CAAC;AAElF,OAAO,EAAoC,KAAK,SAAS,EAAE,MAAM,UAAU,CAAC;AAI5E,eAAO,MAAM,aAAa,GAAI,QAAQ,MAAM,KAAG,OAAO,EAarD,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,QAAQ,MAAM,KAAG,MAYnD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,gBAAgB,GAAI,OAAO,MAAM,KAAG,MAA6C,CAAC;;uBAkCxE,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,SAAS,CAAC;uBAC3E,CACjB,GAAG,EAAE,WAAW,EAChB,OAAO,EAAE,WAAW,EACpB,OAAO,CAAC,EAAE,MAAM,KACb,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC;;AARzC,qBAAa,WAAY,SAAQ,gBAUF;IAC7B,MAAM,CAAC,QAAQ,CAAC,KAAK,wGA8RnB;CACH;AAED,eAAO,MAAM,gBAAgB,6GAG5B,CAAC"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/logs-tool/service.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAU,MAAM,QAAQ,CAAC;AAExD,OAAO,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAEjE,OAAO,EAAE,UAAU,EAAmB,MAAM,cAAc,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAqC,MAAM,gBAAgB,CAAC;AAElF,OAAO,EAAoC,KAAK,SAAS,EAAE,MAAM,UAAU,CAAC;AAG5E,eAAO,MAAM,aAAa,GAAI,QAAQ,MAAM,KAAG,OAAO,EAarD,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,QAAQ,MAAM,KAAG,MAYnD,CAAC;;uBAiEqB,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,SAAS,CAAC;uBAC3E,CACjB,GAAG,EAAE,WAAW,EAChB,OAAO,EAAE,WAAW,EACpB,OAAO,CAAC,EAAE,MAAM,KACb,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC;;AARzC,qBAAa,WAAY,SAAQ,gBAUF;IAC7B,MAAM,CAAC,QAAQ,CAAC,KAAK,8DA6PnB;CACH;AAED,eAAO,MAAM,gBAAgB,+IAG5B,CAAC"}
@@ -1,5 +1,4 @@
1
1
  import type { CommandOptions, CommandResult } from "./types";
2
2
  export declare function expandPath(path: string): string;
3
3
  export declare function runCommand(cmd: string[], options?: CommandOptions): Promise<CommandResult>;
4
- export declare function runShellCommand(command: string, options?: CommandOptions): Promise<CommandResult>;
5
4
  //# sourceMappingURL=bun.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bun.d.ts","sourceRoot":"","sources":["../../src/shared/bun.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7D,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAS/C;AAED,wBAAsB,UAAU,CAC9B,GAAG,EAAE,MAAM,EAAE,EACb,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,aAAa,CAAC,CAmCxB;AAED,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,aAAa,CAAC,CAExB"}
1
+ {"version":3,"file":"bun.d.ts","sourceRoot":"","sources":["../../src/shared/bun.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7D,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAS/C;AAED,wBAAsB,UAAU,CAC9B,GAAG,EAAE,MAAM,EAAE,EACb,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,aAAa,CAAC,CAmCxB"}
@@ -13,11 +13,18 @@ export declare const collectProcessOutput: (process: ChildProcessSpawner.ChildPr
13
13
  stderr: string;
14
14
  exitCode: ChildProcessSpawner.ExitCode;
15
15
  }, import("effect/PlatformError").PlatformError, never>;
16
- export declare const execEffect: (commandStr: string, timeoutMs?: number) => Effect.Effect<{
16
+ export declare const execEffect: (executable: string, args?: readonly string[], timeoutMs?: number) => Effect.Effect<{
17
17
  stdout: string;
18
18
  stderr: string;
19
19
  exitCode: number;
20
20
  }, ExecError, ChildProcessSpawner.ChildProcessSpawner>;
21
21
  export declare const quoteShellArg: (value: string) => string;
22
+ /**
23
+ * Split a command line the way a POSIX shell would word-split it, without running one.
24
+ * Quotes group a value into a single argument and are removed; nothing else is interpreted,
25
+ * so a metacharacter reaches the child as literal text instead of shell syntax.
26
+ */
27
+ export declare const tokenizeCommandLine: (commandLine: string) => string[];
28
+ export declare const renderCommandLine: (argv: readonly string[]) => string;
22
29
  export {};
23
30
  //# sourceMappingURL=exec.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"exec.d.ts","sourceRoot":"","sources":["../../src/shared/exec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,EAAE,MAAM,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;;;;;;;AAIhD,qBAAa,SAAU,SAAQ,cAK7B;CAAG;AAEL,eAAO,MAAM,oBAAoB,GAAI,SAAS,mBAAmB,CAAC,kBAAkB;;;;uDAUhF,CAAC;AAEL,eAAO,MAAM,UAAU,GACrB,YAAY,MAAM,EAClB,YAAW,MAA2B,KACrC,MAAM,CAAC,MAAM,CACd;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,EACpD,SAAS,EACT,mBAAmB,CAAC,mBAAmB,CAsCtC,CAAC;AAEJ,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,WAA0C,CAAC"}
1
+ {"version":3,"file":"exec.d.ts","sourceRoot":"","sources":["../../src/shared/exec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,EAAE,MAAM,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;;;;;;;AAIhD,qBAAa,SAAU,SAAQ,cAK7B;CAAG;AAEL,eAAO,MAAM,oBAAoB,GAAI,SAAS,mBAAmB,CAAC,kBAAkB;;;;uDAUhF,CAAC;AAEL,eAAO,MAAM,UAAU,GACrB,YAAY,MAAM,EAClB,OAAM,SAAS,MAAM,EAAO,EAC5B,YAAW,MAA2B,KACrC,MAAM,CAAC,MAAM,CACd;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,EACpD,SAAS,EACT,mBAAmB,CAAC,mBAAmB,CAyCxC,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,WAA0C,CAAC;AAEtF;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAAI,aAAa,MAAM,KAAG,MAAM,EAwC/D,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,MAAM,SAAS,MAAM,EAAE,KAAG,MACmC,CAAC"}
@@ -1,11 +1,11 @@
1
1
  export type { BaseResult, CommandOptions, CommandResult, Environment, OutputFormat, ToolResult, } from "./types";
2
2
  export { formatAny, formatOption, formatOutput, logFormatted } from "./format";
3
- export { expandPath, runCommand, runShellCommand } from "./bun";
3
+ export { expandPath, runCommand } from "./bun";
4
4
  export { commonArgOptions, parseCommonArgs } from "./cli";
5
5
  export { renderCauseToStderr } from "./error-renderer";
6
6
  export { dumpCommandSchema, makeSchemaCommand, type CommandSchema } from "./schema-dump";
7
7
  export declare const VERSION: string;
8
- export { execEffect, type ExecError } from "./exec";
8
+ export { execEffect, renderCommandLine, tokenizeCommandLine, type ExecError } from "./exec";
9
9
  export { createThrottle, type ThrottleError } from "./throttle";
10
10
  export { aggregateByField, deduplicateLines, formatCountSummary, parseTextTable, stripEmptyColumns, truncateRows, } from "./transform";
11
11
  export { transformLogOutput } from "./log-transform";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/shared/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,UAAU,EACV,cAAc,EACd,aAAa,EACb,WAAW,EACX,YAAY,EACZ,UAAU,GACX,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAE/E,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AAEhE,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AAE1D,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAIzF,eAAO,MAAM,OAAO,QAAc,CAAC;AAEnC,OAAO,EAAE,UAAU,EAAE,KAAK,SAAS,EAAE,MAAM,QAAQ,CAAC;AAEpD,OAAO,EAAE,cAAc,EAAE,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhE,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,EACd,iBAAiB,EACjB,YAAY,GACb,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/shared/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,UAAU,EACV,cAAc,EACd,aAAa,EACb,WAAW,EACX,YAAY,EACZ,UAAU,GACX,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAE/E,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AAE/C,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AAE1D,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAIzF,eAAO,MAAM,OAAO,QAAc,CAAC;AAEnC,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,KAAK,SAAS,EAAE,MAAM,QAAQ,CAAC;AAE5F,OAAO,EAAE,cAAc,EAAE,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhE,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,EACd,iBAAiB,EACjB,YAAY,GACb,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blogic-cz/agent-tools",
3
- "version": "0.17.0",
3
+ "version": "0.17.1",
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",
@@ -9,6 +9,7 @@ import { AzSecurityError, AzCommandError, AzTimeoutError, AzParseError } from ".
9
9
  import { isCommandAllowed, isInvokeAllowed } from "./security";
10
10
  import { transformCmdOutput } from "./transformers";
11
11
  import { ConfigService, getToolConfig } from "#config";
12
+ import { renderCommandLine, tokenizeCommandLine } from "#shared/exec";
12
13
 
13
14
  export class AzService extends Context.Service<
14
15
  AzService,
@@ -43,10 +44,10 @@ export class AzService extends Context.Service<
43
44
 
44
45
  const executor = yield* ChildProcessSpawner.ChildProcessSpawner;
45
46
 
46
- const runShellCommand = (fullCommand: string, timeoutMs: number) =>
47
+ const runAzCommand = (argv: readonly string[], timeoutMs: number) =>
47
48
  Effect.scoped(
48
49
  Effect.gen(function* () {
49
- const command = ChildProcess.make("sh", ["-c", fullCommand], {
50
+ const command = ChildProcess.make("az", argv, {
50
51
  stdout: "pipe",
51
52
  stderr: "pipe",
52
53
  });
@@ -68,7 +69,7 @@ export class AzService extends Context.Service<
68
69
  (platformError) =>
69
70
  new AzCommandError({
70
71
  message: `Command execution failed: ${platformError.message}`,
71
- command: fullCommand,
72
+ command: renderCommandLine(["az", ...argv]),
72
73
  exitCode: -1,
73
74
  hint: "Check that the az CLI is installed and authenticated",
74
75
  nextCommand: "az login",
@@ -112,17 +113,22 @@ export class AzService extends Context.Service<
112
113
  firstWord as (typeof STANDALONE_AZ_COMMANDS)[number],
113
114
  );
114
115
 
115
- let fullCommand: string;
116
+ const cmdArgv = tokenizeCommandLine(cmd);
117
+ const scopeArgv = [
118
+ "--organization",
119
+ azConfig.organization,
120
+ "--project",
121
+ projectName,
122
+ ] as const;
116
123
 
117
- if (isStandaloneCommand) {
118
- fullCommand = `az ${cmd}`;
119
- } else if (isDirectCommand) {
120
- fullCommand = `az ${cmd} --organization "${azConfig.organization}" --project "${projectName}"`;
121
- } else {
122
- fullCommand = `az devops ${cmd} --organization "${azConfig.organization}" --project "${projectName}"`;
123
- }
124
+ const argv = isStandaloneCommand
125
+ ? cmdArgv
126
+ : isDirectCommand
127
+ ? [...cmdArgv, ...scopeArgv]
128
+ : ["devops", ...cmdArgv, ...scopeArgv];
124
129
 
125
- const resultOption = yield* runShellCommand(fullCommand, azConfig.timeoutMs ?? 60000);
130
+ const fullCommand = renderCommandLine(["az", ...argv]);
131
+ const resultOption = yield* runAzCommand(argv, azConfig.timeoutMs ?? 60000);
126
132
 
127
133
  if (Option.isNone(resultOption)) {
128
134
  return yield* new AzTimeoutError({
@@ -165,7 +171,7 @@ export class AzService extends Context.Service<
165
171
  });
166
172
  }
167
173
 
168
- let fullCommand = `az devops invoke --area ${params.area} --resource ${params.resource}`;
174
+ const argv = ["devops", "invoke", "--area", params.area, "--resource", params.resource];
169
175
 
170
176
  const projectName = resolveProject(params.project);
171
177
 
@@ -175,22 +181,23 @@ export class AzService extends Context.Service<
175
181
  };
176
182
 
177
183
  if (Object.keys(routeParameters).length > 0) {
178
- const routeParams = Object.entries(routeParameters)
179
- .map(([k, v]) => `${k}=${v}`)
180
- .join(" ");
181
- fullCommand += ` --route-parameters ${routeParams}`;
184
+ argv.push(
185
+ "--route-parameters",
186
+ ...Object.entries(routeParameters).map(([k, v]) => `${k}=${v}`),
187
+ );
182
188
  }
183
189
 
184
190
  if (params.queryParameters) {
185
- const queryParams = Object.entries(params.queryParameters)
186
- .map(([k, v]) => `${k}=${v}`)
187
- .join(" ");
188
- fullCommand += ` --query-parameters ${queryParams}`;
191
+ argv.push(
192
+ "--query-parameters",
193
+ ...Object.entries(params.queryParameters).map(([k, v]) => `${k}=${v}`),
194
+ );
189
195
  }
190
196
 
191
- fullCommand += ` --organization "${azConfig.organization}" --output json`;
197
+ argv.push("--organization", azConfig.organization, "--output", "json");
192
198
 
193
- const resultOption = yield* runShellCommand(fullCommand, azConfig.timeoutMs ?? 60000);
199
+ const fullCommand = renderCommandLine(["az", ...argv]);
200
+ const resultOption = yield* runAzCommand(argv, azConfig.timeoutMs ?? 60000);
194
201
 
195
202
  if (Option.isNone(resultOption)) {
196
203
  return yield* new AzTimeoutError({
@@ -0,0 +1,73 @@
1
+ /** Kept free of I/O so the caller spawns kubectl itself instead of piping through sh, jq and head. */
2
+
3
+ type KubeContextEntry = {
4
+ readonly name?: unknown;
5
+ readonly context?: { readonly cluster?: unknown };
6
+ };
7
+
8
+ type KubeClusterEntry = {
9
+ readonly name?: unknown;
10
+ readonly cluster?: { readonly server?: unknown };
11
+ };
12
+
13
+ type KubeConfigView = {
14
+ readonly contexts?: readonly KubeContextEntry[];
15
+ readonly clusters?: readonly KubeClusterEntry[];
16
+ };
17
+
18
+ const asString = (value: unknown): string | undefined =>
19
+ typeof value === "string" && value !== "" ? value : undefined;
20
+
21
+ export const parseKubeConfigView = (stdout: string): KubeConfigView | undefined => {
22
+ try {
23
+ const parsed: unknown = JSON.parse(stdout);
24
+ return typeof parsed === "object" && parsed !== null ? (parsed as KubeConfigView) : undefined;
25
+ } catch {
26
+ return undefined;
27
+ }
28
+ };
29
+
30
+ /**
31
+ * Two strategies, in the same order the previous jq pipelines used:
32
+ * an exact context.cluster match first, then a cluster whose server URL contains the id.
33
+ * Returns the first match so a multi-context kubeconfig resolves the same way it did before.
34
+ */
35
+ export const selectKubeContext = (
36
+ view: KubeConfigView | undefined,
37
+ clusterId: string,
38
+ ): string | undefined => {
39
+ if (!view) {
40
+ return undefined;
41
+ }
42
+
43
+ const contexts = view.contexts ?? [];
44
+
45
+ for (const entry of contexts) {
46
+ const name = asString(entry.name);
47
+ if (name !== undefined && asString(entry.context?.cluster) === clusterId) {
48
+ return name;
49
+ }
50
+ }
51
+
52
+ const clusters = view.clusters ?? [];
53
+
54
+ for (const entry of contexts) {
55
+ const name = asString(entry.name);
56
+ const clusterName = asString(entry.context?.cluster);
57
+ if (name === undefined || clusterName === undefined) {
58
+ continue;
59
+ }
60
+
61
+ const matchesServer = clusters.some(
62
+ (cluster) =>
63
+ asString(cluster.name) === clusterName &&
64
+ (asString(cluster.cluster?.server) ?? "").includes(clusterId),
65
+ );
66
+
67
+ if (matchesServer) {
68
+ return name;
69
+ }
70
+ }
71
+
72
+ return undefined;
73
+ };
@@ -1,7 +1,7 @@
1
1
  import { posix } from "node:path";
2
2
 
3
3
  import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
4
- import { Context, Effect, Layer, Option, Ref, Stream } from "effect";
4
+ import { Context, Effect, Layer, Option, Ref } from "effect";
5
5
 
6
6
  import type { CommandResult, Environment } from "./types";
7
7
 
@@ -21,6 +21,7 @@ import { normalizeProfilePrerequisites } from "#shared/prerequisites/config";
21
21
  import { runWithProfilePrerequisites } from "#shared/prerequisites/runtime";
22
22
  import { buildApiProbeArgs } from "#shared/k8s-probe";
23
23
  import { isKubectlCommandAllowed, isSafeLogPath } from "./security";
24
+ import { parseKubeConfigView, selectKubeContext } from "./context";
24
25
 
25
26
  export class K8sService extends Context.Service<
26
27
  K8sService,
@@ -98,41 +99,31 @@ export class K8sService extends Context.Service<
98
99
  );
99
100
  });
100
101
 
101
- const withKubeconfig = (command: string, kubeconfig: string | undefined) =>
102
- kubeconfig ? `KUBECONFIG=${quoteShellArg(kubeconfig)} ${command}` : command;
103
102
  const renderArg = (arg: string) =>
104
103
  /^[A-Za-z0-9_./:=,@+-]+$/.test(arg) ? arg : quoteShellArg(arg);
105
- const renderKubectlCommand = (context: string, argv: readonly string[]) =>
106
- ["kubectl", "--context", context, ...argv].map(renderArg).join(" ");
104
+ const renderKubectlCommand = (context: string | undefined, argv: readonly string[]) =>
105
+ ["kubectl", ...(context ? ["--context", context] : []), ...argv].map(renderArg).join(" ");
107
106
 
108
107
  // Cache context by selected profile/cluster instead of a single default profile.
109
108
  const contextRef = yield* Ref.make<Record<string, string>>({});
110
109
 
111
110
  // Helper that uses executor.spawn() to avoid ChildProcessSpawner requirement in return type
112
- const runShellCommand = (commandStr: string, timeoutMs: number) =>
111
+ const runKubectlCapture = (
112
+ argv: readonly string[],
113
+ kubeconfig: string | undefined,
114
+ timeoutMs: number,
115
+ renderedCommand: string,
116
+ ) =>
113
117
  Effect.scoped(
114
118
  Effect.gen(function* () {
115
- const command = ChildProcess.make("sh", ["-c", commandStr], {
119
+ const command = ChildProcess.make("kubectl", argv, {
116
120
  stdout: "pipe",
117
121
  stderr: "pipe",
122
+ ...(kubeconfig ? { env: { KUBECONFIG: kubeconfig }, extendEnv: true } : {}),
118
123
  });
119
124
  const process = yield* executor.spawn(command);
120
125
 
121
- const stdoutChunk = yield* process.stdout.pipe(
122
- Stream.decodeText(),
123
- Stream.runCollect,
124
- );
125
- const stdout = stdoutChunk.join("");
126
-
127
- const stderrChunk = yield* process.stderr.pipe(
128
- Stream.decodeText(),
129
- Stream.runCollect,
130
- );
131
- const stderr = stderrChunk.join("");
132
-
133
- const exitCode = yield* process.exitCode;
134
-
135
- return { stdout, stderr, exitCode };
126
+ return yield* collectProcessOutput(process);
136
127
  }),
137
128
  ).pipe(
138
129
  Effect.timeoutOption(timeoutMs),
@@ -140,7 +131,7 @@ export class K8sService extends Context.Service<
140
131
  const missing = missingBinaryFromSpawnFailure("kubectl", String(platformError));
141
132
  return new K8sCommandError({
142
133
  message: `Command execution failed: ${String(platformError)}`,
143
- command: commandStr,
134
+ command: renderedCommand,
144
135
  exitCode: -1,
145
136
  stderr: String(platformError),
146
137
  ...(missing ? { hint: missing.hint } : {}),
@@ -214,56 +205,37 @@ export class K8sService extends Context.Service<
214
205
  return { context: cachedContext, kubeconfig };
215
206
  }
216
207
 
217
- const jqCommand = withKubeconfig(
218
- `kubectl config view -o json | jq -r '.contexts[] | select(.context.cluster == "${k8sConfig.clusterId}") | .name' | head -1`,
208
+ const viewCommand = renderKubectlCommand(undefined, ["config", "view", "-o", "json"]);
209
+ const viewResultOption = yield* runKubectlCapture(
210
+ ["config", "view", "-o", "json"],
219
211
  kubeconfig,
212
+ timeoutMs,
213
+ viewCommand,
220
214
  );
221
215
 
222
- const contextResultOption = yield* runShellCommand(jqCommand, timeoutMs);
223
-
224
- if (Option.isNone(contextResultOption)) {
216
+ if (Option.isNone(viewResultOption)) {
225
217
  return yield* new K8sTimeoutError({
226
218
  message: `Context resolution timed out after ${timeoutMs}ms`,
227
- command: jqCommand,
219
+ command: viewCommand,
228
220
  timeoutMs,
229
221
  });
230
222
  }
231
223
 
232
- const contextResult = contextResultOption.value;
233
-
234
- if (contextResult.exitCode === 0 && contextResult.stdout.trim()) {
235
- const resolvedContextValue = contextResult.stdout.trim();
236
- yield* Ref.update(contextRef, (contexts) => ({
237
- ...contexts,
238
- [cacheKey]: resolvedContextValue,
239
- }));
240
- return { context: resolvedContextValue, kubeconfig };
241
- }
242
-
243
- const fallbackCommand = withKubeconfig(
244
- `kubectl config view -o json | jq -r '.contexts[] as $ctx | .clusters[] | select(.name == $ctx.context.cluster and (.cluster.server | contains("${k8sConfig.clusterId}"))) | $ctx.name' | head -1`,
245
- kubeconfig,
246
- );
224
+ const viewResult = viewResultOption.value;
247
225
 
248
- const fallbackResultOption = yield* runShellCommand(fallbackCommand, timeoutMs);
249
-
250
- if (Option.isNone(fallbackResultOption)) {
251
- return yield* new K8sTimeoutError({
252
- message: `Context resolution timed out after ${timeoutMs}ms`,
253
- command: fallbackCommand,
254
- timeoutMs,
255
- });
256
- }
257
-
258
- const fallbackResult = fallbackResultOption.value;
226
+ if (viewResult.exitCode === 0) {
227
+ const resolvedContextValue = selectKubeContext(
228
+ parseKubeConfigView(viewResult.stdout),
229
+ k8sConfig.clusterId,
230
+ );
259
231
 
260
- if (fallbackResult.exitCode === 0 && fallbackResult.stdout.trim()) {
261
- const resolvedContextValue = fallbackResult.stdout.trim();
262
- yield* Ref.update(contextRef, (contexts) => ({
263
- ...contexts,
264
- [cacheKey]: resolvedContextValue,
265
- }));
266
- return { context: resolvedContextValue, kubeconfig };
232
+ if (resolvedContextValue !== undefined) {
233
+ yield* Ref.update(contextRef, (contexts) => ({
234
+ ...contexts,
235
+ [cacheKey]: resolvedContextValue,
236
+ }));
237
+ return { context: resolvedContextValue, kubeconfig };
238
+ }
267
239
  }
268
240
 
269
241
  return yield* new K8sContextError({
@@ -1,6 +1,6 @@
1
- import { isAbsolute, posix, relative, resolve } from "node:path";
1
+ import { readFile, readdir, realpath, stat } from "node:fs/promises";
2
+ import { isAbsolute, join, posix, relative, resolve } from "node:path";
2
3
 
3
- import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
4
4
  import { Context, Effect, Layer, Result } from "effect";
5
5
 
6
6
  import type { Environment, LogFile, ReadOptions } from "./types";
@@ -9,7 +9,6 @@ import { K8sService, K8sServiceLayer } from "#k8s/service";
9
9
  import { ConfigService, ConfigServiceLayer, getToolConfig } from "#config/loader";
10
10
  import type { LogsConfig } from "#config/types";
11
11
  import { LogsNotFoundError, LogsReadError, type LogsError } from "./errors";
12
- import { collectProcessOutput } from "#shared/exec";
13
12
  import { transformLogOutput } from "./transformers";
14
13
 
15
14
  export const parseLogFiles = (output: string): LogFile[] => {
@@ -41,11 +40,36 @@ export const formatPrettyOutput = (output: string): string => {
41
40
  .join("\n---\n");
42
41
  };
43
42
 
44
- /**
45
- * Sanitize a string for safe use in shell commands by escaping single quotes
46
- * and wrapping in single quotes. This prevents shell injection.
47
- */
48
- export const sanitizeShellArg = (input: string): string => `'${input.replace(/'/g, "'\\''")}'`;
43
+ const errorText = (error: unknown): string =>
44
+ error instanceof Error ? error.message : String(error);
45
+
46
+ const takeLastLines = (contents: string, lines: number): string => {
47
+ const all = contents.split("\n");
48
+ const withoutTrailingBlank = all.at(-1) === "" ? all.slice(0, -1) : all;
49
+ return withoutTrailingBlank.slice(-lines).join("\n");
50
+ };
51
+
52
+ /** Newest first, so callers can take the head as the latest log the way `ls -t` did. */
53
+ const readLocalLogDirectory = async (localDir: string): Promise<LogFile[]> => {
54
+ const entries = await readdir(localDir, { withFileTypes: true });
55
+ const logs = entries.filter((entry) => !entry.isDirectory() && entry.name.endsWith(".log"));
56
+
57
+ const described = await Promise.all(
58
+ logs.map(async (entry) => {
59
+ const stats = await stat(join(localDir, entry.name));
60
+ return {
61
+ name: entry.name,
62
+ size: String(stats.size),
63
+ date: stats.mtime.toISOString(),
64
+ modifiedAtMs: stats.mtimeMs,
65
+ };
66
+ }),
67
+ );
68
+
69
+ return described
70
+ .toSorted((left, right) => right.modifiedAtMs - left.modifiedAtMs)
71
+ .map(({ name, size, date }) => ({ name, size, date }));
72
+ };
49
73
 
50
74
  const readCommandOutput = (output: unknown): string => (typeof output === "string" ? output : "");
51
75
  const filterLogLines = (output: string, grep: string | undefined): string => {
@@ -91,45 +115,8 @@ export class LogsService extends Context.Service<
91
115
  LogsService,
92
116
  Effect.gen(function* () {
93
117
  const k8s = yield* K8sService;
94
- const executor = yield* ChildProcessSpawner.ChildProcessSpawner;
95
118
  const config = yield* ConfigService;
96
119
 
97
- const runShellCommand = (commandStr: string) =>
98
- Effect.scoped(
99
- Effect.gen(function* () {
100
- const command = ChildProcess.make("sh", ["-c", commandStr], {
101
- stdout: "pipe",
102
- stderr: "pipe",
103
- });
104
- const process = yield* executor.spawn(command);
105
- return yield* collectProcessOutput(process);
106
- }),
107
- ).pipe(
108
- Effect.catch((platformError) =>
109
- Effect.succeed({
110
- stdout: "",
111
- stderr: String(platformError),
112
- exitCode: -1,
113
- }),
114
- ),
115
- );
116
-
117
- const runDirectCommand = (executable: string, args: readonly string[]) =>
118
- Effect.scoped(
119
- Effect.gen(function* () {
120
- const command = ChildProcess.make(executable, args, {
121
- stdout: "pipe",
122
- stderr: "pipe",
123
- });
124
- const process = yield* executor.spawn(command);
125
- return yield* collectProcessOutput(process);
126
- }),
127
- ).pipe(
128
- Effect.catch((platformError) =>
129
- Effect.succeed({ stdout: "", stderr: String(platformError), exitCode: -1 }),
130
- ),
131
- );
132
-
133
120
  const getLogsConfig = (profile?: string): LogsConfig | undefined =>
134
121
  getToolConfig<LogsConfig>(config, "logs", profile);
135
122
 
@@ -137,16 +124,16 @@ export class LogsService extends Context.Service<
137
124
  logsConfig: LogsConfig,
138
125
  ) {
139
126
  const localDir = logsConfig.localDir;
140
- const result = yield* runShellCommand(`ls -la ${localDir}`);
141
-
142
- if (result.exitCode !== 0) {
143
- return yield* new LogsReadError({
144
- message: result.stderr.trim() || "Failed to list local logs",
145
- source: localDir,
146
- });
147
- }
127
+ const files = yield* Effect.tryPromise(() => readLocalLogDirectory(localDir)).pipe(
128
+ Effect.mapError(
129
+ (error) =>
130
+ new LogsReadError({
131
+ message: errorText(error) || "Failed to list local logs",
132
+ source: localDir,
133
+ }),
134
+ ),
135
+ );
148
136
 
149
- const files = parseLogFiles(result.stdout);
150
137
  if (files.length === 0) {
151
138
  return yield* new LogsNotFoundError({
152
139
  message: "No log files found",
@@ -213,24 +200,25 @@ export class LogsService extends Context.Service<
213
200
  let logFile = options.file;
214
201
 
215
202
  if (!logFile) {
216
- const latest = yield* runShellCommand(`ls -t ${localDir}/*.log 2>/dev/null | head -1`);
217
-
218
- if (latest.exitCode !== 0) {
219
- return yield* new LogsReadError({
220
- message: latest.stderr.trim() || "Failed to find latest log",
221
- source: localDir,
222
- });
223
- }
203
+ const files = yield* Effect.tryPromise(() => readLocalLogDirectory(localDir)).pipe(
204
+ Effect.mapError(
205
+ (error) =>
206
+ new LogsReadError({
207
+ message: errorText(error) || "Failed to find latest log",
208
+ source: localDir,
209
+ }),
210
+ ),
211
+ );
224
212
 
225
- const latestPath = latest.stdout.trim();
226
- if (!latestPath) {
213
+ const latest = files[0];
214
+ if (!latest) {
227
215
  return yield* new LogsNotFoundError({
228
216
  message: "No log files found",
229
217
  path: localDir,
230
218
  });
231
219
  }
232
220
 
233
- logFile = latestPath.split("/").pop() ?? latestPath;
221
+ logFile = latest.name;
234
222
  }
235
223
 
236
224
  const lexicalPath = resolveLocalLogPath(localDir, logFile);
@@ -240,20 +228,22 @@ export class LogsService extends Context.Service<
240
228
  source: localDir,
241
229
  });
242
230
  }
243
- const realpathResult = yield* runDirectCommand("realpath", [localDir, lexicalPath]);
244
- if (realpathResult.exitCode !== 0) {
245
- return yield* new LogsReadError({
246
- message:
247
- realpathResult.stderr.trim() ||
248
- `realpath failed with exit code ${realpathResult.exitCode}`,
249
- source: lexicalPath,
250
- });
251
- }
252
- const [canonicalBase, canonicalPath] = realpathResult.stdout.trim().split("\n");
231
+ const canonical = yield* Effect.tryPromise(async () => ({
232
+ base: await realpath(localDir),
233
+ path: await realpath(lexicalPath),
234
+ })).pipe(
235
+ Effect.mapError(
236
+ (error) =>
237
+ new LogsReadError({
238
+ message: errorText(error) || "Failed to canonicalize the log path",
239
+ source: lexicalPath,
240
+ }),
241
+ ),
242
+ );
243
+
244
+ const canonicalPath = canonical.path;
253
245
  if (
254
- canonicalBase === undefined ||
255
- canonicalPath === undefined ||
256
- resolveLocalLogPath(canonicalBase, canonicalPath) !== canonicalPath ||
246
+ resolveLocalLogPath(canonical.base, canonicalPath) !== canonicalPath ||
257
247
  !canonicalPath.endsWith(".log")
258
248
  ) {
259
249
  return yield* new LogsReadError({
@@ -261,17 +251,18 @@ export class LogsService extends Context.Service<
261
251
  source: localDir,
262
252
  });
263
253
  }
264
- const command = `tail -${options.tail} ${sanitizeShellArg(canonicalPath)}`;
265
- const result = yield* runShellCommand(command);
266
254
 
267
- if (result.exitCode !== 0) {
268
- return yield* new LogsReadError({
269
- message: result.stderr.trim() || `Command failed with exit code ${result.exitCode}`,
270
- source: canonicalPath,
271
- });
272
- }
255
+ const contents = yield* Effect.tryPromise(() => readFile(canonicalPath, "utf8")).pipe(
256
+ Effect.mapError(
257
+ (error) =>
258
+ new LogsReadError({
259
+ message: errorText(error) || "Failed to read the log file",
260
+ source: canonicalPath,
261
+ }),
262
+ ),
263
+ );
273
264
 
274
- const output = filterLogLines(result.stdout, options.grep).trim();
265
+ const output = filterLogLines(takeLastLines(contents, options.tail), options.grep).trim();
275
266
  if (!output) {
276
267
  return "(no matching lines)";
277
268
  }
package/src/shared/bun.ts CHANGED
@@ -50,10 +50,3 @@ export async function runCommand(
50
50
  executionTimeMs: Date.now() - startTime,
51
51
  };
52
52
  }
53
-
54
- export function runShellCommand(
55
- command: string,
56
- options: CommandOptions = {},
57
- ): Promise<CommandResult> {
58
- return runCommand(["sh", "-c", command], options);
59
- }
@@ -23,18 +23,21 @@ export const collectProcessOutput = (process: ChildProcessSpawner.ChildProcessHa
23
23
  });
24
24
 
25
25
  export const execEffect = (
26
- commandStr: string,
26
+ executable: string,
27
+ args: readonly string[] = [],
27
28
  timeoutMs: number = DEFAULT_TIMEOUT_MS,
28
29
  ): Effect.Effect<
29
30
  { stdout: string; stderr: string; exitCode: number },
30
31
  ExecError,
31
32
  ChildProcessSpawner.ChildProcessSpawner
32
- > =>
33
- Effect.scoped(
33
+ > => {
34
+ const commandStr = renderCommandLine([executable, ...args]);
35
+
36
+ return Effect.scoped(
34
37
  Effect.gen(function* () {
35
38
  const executor = yield* ChildProcessSpawner.ChildProcessSpawner;
36
39
 
37
- const command = ChildProcess.make("sh", ["-c", commandStr], {
40
+ const command = ChildProcess.make(executable, args, {
38
41
  stdout: "pipe",
39
42
  stderr: "pipe",
40
43
  });
@@ -67,5 +70,56 @@ export const execEffect = (
67
70
  ),
68
71
  ),
69
72
  );
73
+ };
70
74
 
71
75
  export const quoteShellArg = (value: string) => `'${value.replaceAll("'", "'\\''")}'`;
76
+
77
+ /**
78
+ * Split a command line the way a POSIX shell would word-split it, without running one.
79
+ * Quotes group a value into a single argument and are removed; nothing else is interpreted,
80
+ * so a metacharacter reaches the child as literal text instead of shell syntax.
81
+ */
82
+ export const tokenizeCommandLine = (commandLine: string): string[] => {
83
+ const tokens: string[] = [];
84
+ let current = "";
85
+ let started = false;
86
+ let quote: '"' | "'" | undefined;
87
+
88
+ for (const character of commandLine) {
89
+ if (quote) {
90
+ if (character === quote) {
91
+ quote = undefined;
92
+ } else {
93
+ current += character;
94
+ }
95
+ continue;
96
+ }
97
+
98
+ if (character === '"' || character === "'") {
99
+ quote = character;
100
+ started = true;
101
+ continue;
102
+ }
103
+
104
+ if (/\s/.test(character)) {
105
+ if (started) {
106
+ tokens.push(current);
107
+ current = "";
108
+ started = false;
109
+ }
110
+ continue;
111
+ }
112
+
113
+ current += character;
114
+ started = true;
115
+ }
116
+
117
+ if (started) {
118
+ tokens.push(current);
119
+ }
120
+
121
+ return tokens;
122
+ };
123
+
124
+ export const renderCommandLine = (argv: readonly string[]): string =>
125
+ argv.map((arg) => (/^[A-Za-z0-9_./:=,@+-]+$/.test(arg) ? arg : quoteShellArg(arg))).join(" ");
@@ -9,7 +9,7 @@ export type {
9
9
 
10
10
  export { formatAny, formatOption, formatOutput, logFormatted } from "./format";
11
11
 
12
- export { expandPath, runCommand, runShellCommand } from "./bun";
12
+ export { expandPath, runCommand } from "./bun";
13
13
 
14
14
  export { commonArgOptions, parseCommonArgs } from "./cli";
15
15
 
@@ -21,7 +21,7 @@ export { dumpCommandSchema, makeSchemaCommand, type CommandSchema } from "./sche
21
21
  import pkg from "../../package.json" with { type: "json" };
22
22
  export const VERSION = pkg.version;
23
23
 
24
- export { execEffect, type ExecError } from "./exec";
24
+ export { execEffect, renderCommandLine, tokenizeCommandLine, type ExecError } from "./exec";
25
25
 
26
26
  export { createThrottle, type ThrottleError } from "./throttle";
27
27