@merchantduo/code 0.2.0-beta.4 → 0.2.0-beta.5

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.
@@ -0,0 +1,19 @@
1
+ import type { CommandResult, CommandSpec } from "#environments/executor";
2
+ export type NativeLogContext = {
3
+ project: string;
4
+ session: string;
5
+ environment: string;
6
+ };
7
+ /** Append-only, fail-soft evidence for automatic native probes only. */
8
+ export declare class NativeProbeLog {
9
+ #private;
10
+ readonly path: string;
11
+ constructor(path?: string);
12
+ get warning(): string | undefined;
13
+ start(context: NativeLogContext): Promise<void>;
14
+ record(spec: CommandSpec, result: CommandResult): Promise<void>;
15
+ }
16
+ export declare function runNativeProbe(log: NativeProbeLog, executor: {
17
+ command(argv: string[], target?: string): CommandSpec;
18
+ run(argv: string[], options?: Parameters<import("#environments/executor").CommandExecutor["run"]>[1]): Promise<CommandResult>;
19
+ }, argv: string[], options?: Parameters<import("#environments/executor").CommandExecutor["run"]>[1]): Promise<CommandResult>;
@@ -0,0 +1,50 @@
1
+ import { appendFile, chmod, mkdir } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ const MAX_STREAM = 8_000;
5
+ /** Append-only, fail-soft evidence for automatic native probes only. */
6
+ export class NativeProbeLog {
7
+ path;
8
+ #started = false;
9
+ #warning;
10
+ constructor(path = join(homedir(), ".merchantduo", "native-operations.log")) {
11
+ this.path = path;
12
+ }
13
+ get warning() { return this.#warning; }
14
+ async start(context) {
15
+ if (this.#started)
16
+ return;
17
+ this.#started = true;
18
+ await this.#append(`\n=== MerchantDuo native session ${new Date().toISOString()} ===\nsession: ${redact(context.session)}\nproject: ${redact(context.project)}\nenvironment: ${redact(context.environment)}\n`);
19
+ }
20
+ async record(spec, result) {
21
+ const stdout = stream(result.stdout);
22
+ const stderr = stream(result.stderr);
23
+ await this.#append(`\n[${new Date().toISOString()}]\ncommand: ${redact([spec.file, ...spec.args].join(" "))}\nexit_code: ${result.exitCode}\nstdout:\n${stdout.value}${result.truncated || stdout.truncated ? "\n[stdout truncated]" : ""}\nstderr:\n${stderr.value}${result.truncated || stderr.truncated ? "\n[stderr truncated]" : ""}\n`);
24
+ }
25
+ async #append(record) {
26
+ try {
27
+ const directory = dirname(this.path);
28
+ await mkdir(directory, { recursive: true, mode: 0o700 });
29
+ await chmod(directory, 0o700);
30
+ await appendFile(this.path, record, { encoding: "utf8", mode: 0o600 });
31
+ await chmod(this.path, 0o600);
32
+ }
33
+ catch {
34
+ this.#warning = "Native operation logging is unavailable.";
35
+ }
36
+ }
37
+ }
38
+ export async function runNativeProbe(log, executor, argv, options) {
39
+ const spec = executor.command(argv, options?.target);
40
+ const result = await executor.run(argv, options);
41
+ await log.record(spec, result);
42
+ return result;
43
+ }
44
+ function stream(value) {
45
+ const redacted = redact(value);
46
+ return { value: redacted.slice(0, MAX_STREAM), truncated: redacted.length > MAX_STREAM };
47
+ }
48
+ function redact(value) {
49
+ return value.replace(/((?:password|passwd|secret|token|api[_-]?key)\s*(?:=|:|=>)\s*["']?)[^\s"']+/gi, "$1[redacted]");
50
+ }
@@ -7,6 +7,7 @@ import { type MagentoInspector, type MagentoSnapshot } from "#magento/index";
7
7
  import { ChangeTracker } from "#workflows/change-tracker";
8
8
  import type { TestingSnapshot } from "#testing/model";
9
9
  import type { Magerun2Capability } from "#features/magerun2/index";
10
+ import { NativeProbeLog } from "#app/native-operations";
10
11
  export type SessionState = {
11
12
  config: MerchantConfig;
12
13
  configPath?: string;
@@ -15,6 +16,7 @@ export type SessionState = {
15
16
  environment: Environment;
16
17
  };
17
18
  backend: EnvironmentBackend;
19
+ nativeLog: NativeProbeLog;
18
20
  environmentStatus: EnvironmentStatus;
19
21
  permissionMode: PermissionMode;
20
22
  magento: MagentoSnapshot;
@@ -3,6 +3,8 @@ import { EnvironmentBackend } from "#environments/backend";
3
3
  import { resolvePermissionMode } from "#app/permission-mode";
4
4
  import { DefaultMagentoInspector, } from "#magento/index";
5
5
  import { ChangeTracker } from "#workflows/change-tracker";
6
+ import { NativeProbeLog } from "#app/native-operations";
7
+ import { wardenRunning } from "#environments/backend";
6
8
  export class MerchantDuoRuntime {
7
9
  inspector;
8
10
  #state;
@@ -20,14 +22,17 @@ export class MerchantDuoRuntime {
20
22
  });
21
23
  const selected = selectEnvironment(loaded.config, cwd, process.env.MERCHANTDUO_SELECTED_ENV);
22
24
  const backend = new EnvironmentBackend(selected.environment, cwd);
23
- const environmentStatus = await backend.status();
25
+ const nativeLog = new NativeProbeLog();
26
+ await nativeLog.start({ project: cwd, session: `${process.pid}`, environment: selected.name });
27
+ const environmentStatus = await nativeStatus(backend, nativeLog);
24
28
  this.#state = {
25
29
  ...loaded,
26
30
  selected,
27
31
  backend,
32
+ nativeLog,
28
33
  environmentStatus,
29
34
  permissionMode: resolvePermissionMode(selected.environment, process.env.MERCHANTDUO_PERMISSION_MODE),
30
- magento: environmentStatus === "running" ? await this.inspector.inspect(backend) : stoppedMagentoSnapshot(),
35
+ magento: environmentStatus === "running" ? await this.inspector.inspect(backend, (argv, result) => nativeLog.record(backend.command(argv), result)) : stoppedMagentoSnapshot(),
31
36
  };
32
37
  return this.#state;
33
38
  }
@@ -102,3 +107,15 @@ export class MerchantDuoRuntime {
102
107
  function stoppedMagentoSnapshot() {
103
108
  return { cacheTypes: [], warnings: ["Environment is stopped"], evidence: [], themes: [] };
104
109
  }
110
+ async function nativeStatus(backend, log) {
111
+ if (backend.environment.type === "ssh")
112
+ return "running";
113
+ if (backend.environment.type === "local")
114
+ return "unknown";
115
+ const spec = backend.lifecycleCommand("status");
116
+ if (!spec)
117
+ return "stopped";
118
+ const result = await backend.runCommand(spec);
119
+ await log.record(spec, result);
120
+ return result.exitCode === 0 && wardenRunning(result.stdout) ? "running" : "stopped";
121
+ }
@@ -11,8 +11,10 @@ export declare class EnvironmentBackend implements CommandExecutor {
11
11
  path(path: string): string;
12
12
  command(argv: string[], target?: string): CommandSpec;
13
13
  run(argv: string[], options?: RunOptions): Promise<CommandResult>;
14
+ /** Execute a pre-routed adapter command, used by automatic lifecycle probing. */
15
+ runCommand(spec: CommandSpec, options?: RunOptions): Promise<CommandResult>;
14
16
  lifecycleCommand(action: "status" | "start" | "stop"): CommandSpec | undefined;
15
- status(): Promise<EnvironmentStatus>;
17
+ status(onCommand?: (spec: CommandSpec, result: CommandResult) => void): Promise<EnvironmentStatus>;
16
18
  start(): Promise<CommandResult | undefined>;
17
19
  stop(): Promise<CommandResult | undefined>;
18
20
  }
@@ -22,12 +22,16 @@ export class EnvironmentBackend {
22
22
  }
23
23
  async run(argv, options = {}) {
24
24
  const spec = this.command(argv, options.target);
25
+ return this.runCommand(spec, options);
26
+ }
27
+ /** Execute a pre-routed adapter command, used by automatic lifecycle probing. */
28
+ runCommand(spec, options = {}) {
25
29
  return this.#runSpec(spec, options);
26
30
  }
27
31
  lifecycleCommand(action) {
28
32
  return this.#adapter.lifecycleCommand?.(action);
29
33
  }
30
- async status() {
34
+ async status(onCommand) {
31
35
  if (this.environment.type === "ssh")
32
36
  return "running";
33
37
  if (this.environment.type === "local")
@@ -36,6 +40,7 @@ export class EnvironmentBackend {
36
40
  if (!spec)
37
41
  return "stopped";
38
42
  const output = await this.#runSpec(spec);
43
+ onCommand?.(spec, output);
39
44
  return output.exitCode === 0 && wardenRunning(output.stdout) ? "running" : "stopped";
40
45
  }
41
46
  async start() {
@@ -1,9 +1,10 @@
1
1
  import type { EnvironmentBackend } from "#environments/backend";
2
+ import type { CommandExecutor, CommandResult } from "#environments/executor";
2
3
  import type { Environment } from "#environments/model";
3
4
  import { type HostProcessRunner } from "#testing/host";
4
5
  import type { Magerun2Capability, Magerun2Result } from "#features/magerun2/model";
5
6
  /** Resolve magerun once for the selected Magento environment without altering the project. */
6
- export declare function discoverMagerun2(environment: Environment, backend: EnvironmentBackend, cwd: string): Promise<Magerun2Capability>;
7
+ export declare function discoverMagerun2(environment: Environment, backend: CommandExecutor, cwd: string, record?: (argv: string[], result: CommandResult) => Promise<void>): Promise<Magerun2Capability>;
7
8
  /** Run a structured argument vector. Local commands are host processes; Warden and SSH use their selected backend. */
8
9
  export declare function runMagerun2(capability: Magerun2Capability, environment: Environment, backend: EnvironmentBackend, cwd: string, args: string[], stdin?: string, signal?: AbortSignal, host?: HostProcessRunner): Promise<Magerun2Result>;
9
10
  /** Stable display only: this is never passed to a shell. */
@@ -6,14 +6,16 @@ import { NodeHostProcessRunner } from "#testing/host";
6
6
  const RELEASE_URL = "https://api.github.com/repos/netz98/n98-magerun2/releases/latest";
7
7
  const MAX_OUTPUT = 64 * 1024;
8
8
  /** Resolve magerun once for the selected Magento environment without altering the project. */
9
- export async function discoverMagerun2(environment, backend, cwd) {
9
+ export async function discoverMagerun2(environment, backend, cwd, record) {
10
10
  if (environment.type === "warden")
11
11
  return { available: true, executable: "/usr/local/bin/mr" };
12
12
  const candidates = environment.type === "local"
13
13
  ? ["vendor/bin/n98-magerun2", "bin/n98-magerun2", "n98-magerun2", "magerun2"]
14
14
  : ["n98-magerun2", "magerun2"];
15
15
  for (const candidate of candidates) {
16
- const result = await backend.run(["sh", "-lc", `command -v -- ${candidate} 2>/dev/null || { test -x ${candidate} && printf '%s' ${candidate}; }`]);
16
+ const argv = ["sh", "-lc", `command -v -- ${candidate} 2>/dev/null || { test -x ${candidate} && printf '%s' ${candidate}; }`];
17
+ const result = await backend.run(argv);
18
+ await record?.(argv, result);
17
19
  const executable = result.stdout.trim();
18
20
  if (executable)
19
21
  return { available: true, executable };
@@ -1,2 +1,4 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { runtime } from "#integrations/pi/session";
2
3
  export default function context(pi: ExtensionAPI): void;
4
+ export declare function statusMessage(state: Awaited<ReturnType<typeof runtime.boot>>): string;
@@ -2,7 +2,7 @@ import { refreshStatus } from "#app/status";
2
2
  import { merchantDuoSystemPrompt } from "#app/system-prompt";
3
3
  import { discoverTesting } from "#testing/index";
4
4
  import { discoverMagerun2 } from "#features/magerun2/index";
5
- import { discoverHostExecutable } from "#testing/host";
5
+ import { discoverHostExecutable, NodeHostProcessRunner } from "#testing/host";
6
6
  import { runtime, sessionEvents } from "#integrations/pi/session";
7
7
  export default function context(pi) {
8
8
  pi.on("session_start", async (_event, ctx) => {
@@ -14,23 +14,34 @@ export default function context(pi) {
14
14
  pi.appendEntry("merchantduo.context", { environment: state.selected.name, environmentStatus: state.environmentStatus, permissionMode: state.permissionMode });
15
15
  return;
16
16
  }
17
- const magerun2 = await discoverMagerun2(state.selected.environment, state.backend, ctx.cwd);
17
+ const record = (argv, result) => state.nativeLog.record(state.backend.command(argv), result);
18
+ const magerun2 = await discoverMagerun2(state.selected.environment, state.backend, ctx.cwd, record);
18
19
  // HTTP and Chromium deliberately run on the host against Warden's routed URL.
19
20
  // Do not use EnvironmentBackend here: its Warden adapter would probe php-fpm.
20
21
  // Prefer non-Snap Chrome: Snap Chromium cannot write MerchantDuo artifacts
21
22
  // and its private /tmp makes generated output unavailable to the host.
23
+ const hostRunner = new NodeHostProcessRunner();
24
+ const nativeHost = { run: async (file, args, options) => {
25
+ const result = await hostRunner.run(file, args, options);
26
+ await state.nativeLog.record({ file, args, cwd: options.cwd }, { stdout: result.output, stderr: "", exitCode: result.code, truncated: result.truncated ?? false });
27
+ return result;
28
+ } };
22
29
  const [http, browser] = await Promise.all([
23
- discoverHostExecutable(["curl"]),
24
- discoverHostExecutable(["google-chrome", "google-chrome-stable", "chromium", "chromium-browser"]),
30
+ discoverHostExecutable(["curl"], nativeHost),
31
+ discoverHostExecutable(["google-chrome", "google-chrome-stable", "chromium", "chromium-browser"], nativeHost),
25
32
  ]);
26
33
  runtime.setMagerun2(magerun2);
27
- runtime.setTesting(await discoverTesting(state.selected.environment, state.backend, ctx.cwd, { ...state.config.testing, ...state.selected.environment.testing, allowInsecureTls: state.selected.environment.testing?.allowInsecureTls ?? state.config.testing.allowInsecureTls }, { http, browser }));
34
+ runtime.setTesting(await discoverTesting(state.selected.environment, state.backend, ctx.cwd, { ...state.config.testing, ...state.selected.environment.testing, allowInsecureTls: state.selected.environment.testing?.allowInsecureTls ?? state.config.testing.allowInsecureTls }, { http, browser }, record));
28
35
  refreshStatus(ctx, state);
29
36
  sessionEvents.emit("merchantduo.context", state);
30
37
  pi.appendEntry("merchantduo.context", { environment: state.selected.name, environmentStatus: state.environmentStatus, permissionMode: state.permissionMode, testing: state.testing, magerun2: state.magerun2 });
31
38
  });
32
39
  pi.on("session_compact", () => runtime.resetInjection());
33
40
  pi.on("session_shutdown", () => sessionEvents.clear());
41
+ pi.registerCommand("duo-status", { description: "Show the current MerchantDuo session status.", handler: async (_args, ctx) => {
42
+ const state = await runtime.boot(ctx.cwd);
43
+ pi.sendMessage({ customType: "merchantduo.status", content: statusMessage(state), display: true, details: {} });
44
+ } });
34
45
  pi.on("tool_call", (event) => {
35
46
  if (event.toolName !== "write" && event.toolName !== "edit")
36
47
  return;
@@ -50,3 +61,21 @@ export default function context(pi) {
50
61
  return { systemPrompt: `${event.systemPrompt}\n${merchantDuoSystemPrompt()}\nEnvironment: ${state.selected.name} is ${state.environmentStatus}; permission mode=${state.permissionMode}. ${state.environmentStatus === "stopped" ? "Use environment_start only after the user directly asks to start it; do not use project tools until it is running." : "Testing URLs are discovered but unverified. Frontend: " + (testing?.frontendUrl ?? "unavailable") + "; Admin: " + (testing?.adminUrl ?? "unavailable") + ". HTTP, browser, navigation, and test artifacts are host-only; never use environment bash to locate or repair their host paths. Use explicit test tools only when relevant."}` };
51
62
  });
52
63
  }
64
+ export function statusMessage(state) {
65
+ const lines = [
66
+ `Environment: ${state.selected.name} (${state.environmentStatus})`,
67
+ `Permission mode: ${state.permissionMode}`,
68
+ `Magento: ${state.magento.edition ?? "unknown"} ${state.magento.version ?? "version unavailable"}`,
69
+ `PHP: ${state.magento.phpVersion ?? "unavailable"}`,
70
+ `Deploy mode: ${state.magento.mode ?? "unavailable"}`,
71
+ `Active theme: ${state.config.activeTheme}`,
72
+ `Frontend URL: ${state.testing?.frontendUrl ?? "unavailable"}`,
73
+ `Admin URL: ${state.testing?.adminUrl ?? "unavailable"}`,
74
+ `Capabilities: HTTP ${state.testing?.http.available ? "available" : "unavailable"}; browser ${state.testing?.browser.available ? "available" : "unavailable"}; magerun2 ${state.magerun2?.available ? "available" : "unavailable"}`,
75
+ ...state.magento.warnings.map((warning) => `Warning: ${warning}`),
76
+ ...(state.testing?.diagnostics.map((warning) => `Warning: ${warning}`) ?? []),
77
+ ...(state.magerun2?.diagnostic ? [`Warning: ${state.magerun2.diagnostic}`] : []),
78
+ ...(state.nativeLog.warning ? [`Warning: ${state.nativeLog.warning}`] : []),
79
+ ];
80
+ return lines.join("\n");
81
+ }
@@ -1,9 +1,9 @@
1
- import type { CommandExecutor } from "#environments/executor";
1
+ import type { CommandExecutor, CommandResult } from "#environments/executor";
2
2
  import type { MagentoSnapshot } from "#magento/model";
3
3
  export interface MagentoInspector {
4
- inspect(executor: CommandExecutor): Promise<MagentoSnapshot>;
4
+ inspect(executor: CommandExecutor, record?: (argv: string[], result: CommandResult) => Promise<void>): Promise<MagentoSnapshot>;
5
5
  }
6
6
  /** Ordered, fail-soft Magento inspection. New probes can be extracted without changing consumers. */
7
7
  export declare class DefaultMagentoInspector implements MagentoInspector {
8
- inspect(executor: CommandExecutor): Promise<MagentoSnapshot>;
8
+ inspect(executor: CommandExecutor, record?: (argv: string[], result: CommandResult) => Promise<void>): Promise<MagentoSnapshot>;
9
9
  }
@@ -2,13 +2,18 @@ import { parseDeployMode } from "#magento/deploy-mode";
2
2
  import { parseRegisteredThemes, resolveThemes } from "#magento/themes";
3
3
  /** Ordered, fail-soft Magento inspection. New probes can be extracted without changing consumers. */
4
4
  export class DefaultMagentoInspector {
5
- async inspect(executor) {
5
+ async inspect(executor, record) {
6
6
  const warnings = [];
7
7
  const evidence = [];
8
- const composer = await executor.run([
8
+ const run = async (argv) => {
9
+ const result = await executor.run(argv);
10
+ await record?.(argv, result);
11
+ return result;
12
+ };
13
+ const composer = await run([
9
14
  "sh",
10
15
  "-lc",
11
- "test -f composer.lock && node -e \"const x=require('./composer.lock'); console.log(JSON.stringify(x.packages||[]))\" || true",
16
+ `test -f composer.lock && php -r '$x=json_decode(file_get_contents("composer.lock"), true, flags: JSON_THROW_ON_ERROR); echo json_encode($x["packages"] ?? []);'`,
12
17
  ]);
13
18
  const match = composer.stdout.match(/magento\/(product-community-edition|product-enterprise-edition).*?"version":"([^"]+)/s);
14
19
  const edition = match?.[1] === "product-enterprise-edition"
@@ -19,20 +24,21 @@ export class DefaultMagentoInspector {
19
24
  const composerVersion = match?.[2];
20
25
  if (composerVersion)
21
26
  evidence.push("composer.lock");
22
- const cli = await executor.run(["bin/magento", "--version"]);
27
+ const cli = await run(["bin/magento", "--version"]);
23
28
  const cliVersion = cli.stdout.match(/(?:Magento|Adobe Commerce)[^0-9]*(\d+\.\d+\.\d+(?:-[^\s]+)?)/)?.[1];
24
29
  if (cliVersion)
25
30
  evidence.push("bin/magento --version");
31
+ if (!composerVersion && !cliVersion)
32
+ warnings.push("Magento version unavailable");
26
33
  if (composerVersion &&
27
34
  cliVersion &&
28
35
  !cliVersion.startsWith(composerVersion))
29
36
  warnings.push(`Composer (${composerVersion}) and CLI (${cliVersion}) disagree`);
30
- const [php, mode, cache, modules, registeredThemes] = await Promise.all([
31
- executor.run(["php", "-r", "echo PHP_VERSION;"]),
32
- executor.run(["bin/magento", "deploy:mode:show"]),
33
- executor.run(["bin/magento", "cache:status", "--no-ansi"]),
34
- executor.run(["bin/magento", "module:status", "--enabled", "--no-ansi"]),
35
- executor.run(["sh", "-lc", themeDiscoveryScript]),
37
+ const [php, mode, cache, registeredThemes] = await Promise.all([
38
+ run(["php", "-r", "echo PHP_VERSION;"]),
39
+ run(["bin/magento", "deploy:mode:show"]),
40
+ run(["bin/magento", "cache:status", "--no-ansi"]),
41
+ run(["sh", "-lc", themeDiscoveryScript]),
36
42
  ]);
37
43
  return {
38
44
  version: composerVersion ?? cliVersion,
@@ -43,7 +49,6 @@ export class DefaultMagentoInspector {
43
49
  .split("\n")
44
50
  .map((line) => line.match(/^([^:]+):/)?.[1])
45
51
  .filter((value) => Boolean(value)),
46
- modulesEnabled: modules.stdout.split("\n").filter(Boolean).length || undefined,
47
52
  warnings,
48
53
  evidence,
49
54
  themes: resolveThemes(parseRegisteredThemes(registeredThemes.stdout)),
@@ -18,7 +18,6 @@ export type MagentoSnapshot = {
18
18
  phpVersion?: string;
19
19
  mode?: MagentoDeployMode;
20
20
  cacheTypes: string[];
21
- modulesEnabled?: number;
22
21
  warnings: string[];
23
22
  evidence: string[];
24
23
  themes: MagentoTheme[];
@@ -1,9 +1,9 @@
1
- import type { EnvironmentBackend } from "#environments/backend";
1
+ import type { CommandExecutor, CommandResult } from "#environments/executor";
2
2
  import type { Environment } from "#environments/model";
3
3
  import type { TestingOptions, TestingSnapshot } from "#testing/model";
4
- export declare function discoverTesting(environment: Environment, backend: EnvironmentBackend, cwd: string, options: TestingOptions, tools?: {
4
+ export declare function discoverTesting(environment: Environment, backend: CommandExecutor, cwd: string, options: TestingOptions, tools?: {
5
5
  http?: string;
6
6
  browser?: string;
7
- }): Promise<TestingSnapshot>;
7
+ }, record?: (argv: string[], result: CommandResult) => Promise<void>): Promise<TestingSnapshot>;
8
8
  export declare function wardenRouting(projectRoot: string): Promise<string | undefined>;
9
9
  export declare function findExecutable(candidates: string[]): Promise<string | undefined>;
@@ -1,7 +1,7 @@
1
1
  import { access, readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { canonicalUrl, joinedUrl } from "#testing/url";
4
- export async function discoverTesting(environment, backend, cwd, options, tools = {}) {
4
+ export async function discoverTesting(environment, backend, cwd, options, tools = {}, record) {
5
5
  const diagnostics = [];
6
6
  let frontendUrl = canonicalUrl(options.frontendUrl ?? "");
7
7
  let adminUrl = canonicalUrl(options.adminUrl ?? "");
@@ -17,15 +17,21 @@ export async function discoverTesting(environment, backend, cwd, options, tools
17
17
  diagnostics.push("Warden routing was not found in .env.");
18
18
  }
19
19
  if (!frontendUrl && environment.type !== "warden") {
20
- const result = await backend.run(["bin/magento", "config:show", "web/secure/base_url"]);
20
+ const secure = ["bin/magento", "config:show", "web/secure/base_url"];
21
+ const result = await backend.run(secure);
22
+ await record?.(secure, result);
21
23
  frontendUrl = canonicalUrl(result.stdout.trim());
22
24
  if (!frontendUrl) {
23
- const fallback = await backend.run(["bin/magento", "config:show", "web/unsecure/base_url"]);
25
+ const unsecure = ["bin/magento", "config:show", "web/unsecure/base_url"];
26
+ const fallback = await backend.run(unsecure);
27
+ await record?.(unsecure, fallback);
24
28
  frontendUrl = canonicalUrl(fallback.stdout.trim());
25
29
  }
26
30
  }
27
31
  if (!adminUrl) {
28
- const result = await backend.run(["bin/magento", "info:adminuri"]);
32
+ const argv = ["bin/magento", "info:adminuri"];
33
+ const result = await backend.run(argv);
34
+ await record?.(argv, result);
29
35
  const path = result.stdout.trim().split(/\s+/).pop();
30
36
  adminUrl = joinedUrl(frontendUrl, path?.startsWith("/") ? path : `/${path ?? ""}`);
31
37
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@merchantduo/code",
3
- "version": "0.2.0-beta.4",
3
+ "version": "0.2.0-beta.5",
4
4
  "private": false,
5
5
  "description": "Magento-native Pi-based Coding Agent",
6
6
  "type": "module",