@merchantduo/code 0.1.0-beta.0 → 0.2.0-beta.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.
- package/README.md +132 -99
- package/dist/app/runtime.d.ts +30 -0
- package/dist/app/runtime.js +56 -0
- package/dist/app/status.d.ts +3 -0
- package/dist/app/status.js +9 -0
- package/dist/app/system-prompt.d.ts +2 -0
- package/dist/{skills.js → app/system-prompt.js} +3 -32
- package/dist/cli/arguments.d.ts +10 -0
- package/dist/cli/arguments.js +34 -0
- package/dist/cli/commands/agent.d.ts +1 -0
- package/dist/cli/commands/agent.js +33 -0
- package/dist/cli/commands/doctor.d.ts +3 -0
- package/dist/cli/commands/doctor.js +14 -0
- package/dist/cli/commands/init.d.ts +9 -0
- package/dist/cli/commands/init.js +68 -0
- package/dist/cli/commands/provision.d.ts +6 -0
- package/dist/cli/commands/provision.js +35 -0
- package/dist/cli/main.js +31 -0
- package/dist/config/index.d.ts +11 -0
- package/dist/config/index.js +15 -0
- package/dist/config/loader.d.ts +10 -0
- package/dist/config/loader.js +61 -0
- package/dist/config/paths.d.ts +1 -0
- package/dist/config/paths.js +7 -0
- package/dist/config/project-store.d.ts +2 -0
- package/dist/config/project-store.js +12 -0
- package/dist/config/schema.d.ts +210 -0
- package/dist/config/schema.js +36 -0
- package/dist/config/trust-store.d.ts +3 -0
- package/dist/config/trust-store.js +29 -0
- package/dist/environments/adapter.d.ts +5 -0
- package/dist/environments/adapters/local.d.ts +12 -0
- package/dist/environments/adapters/local.js +14 -0
- package/dist/environments/adapters/ssh.d.ts +11 -0
- package/dist/environments/adapters/ssh.js +24 -0
- package/dist/environments/adapters/warden.d.ts +13 -0
- package/dist/environments/adapters/warden.js +24 -0
- package/dist/environments/backend.d.ts +13 -0
- package/dist/environments/backend.js +73 -0
- package/dist/environments/executor.d.ts +23 -0
- package/dist/environments/factory.d.ts +3 -0
- package/dist/environments/factory.js +13 -0
- package/dist/environments/model.d.ts +28 -0
- package/dist/environments/model.js +3 -0
- package/dist/environments/path-policy.d.ts +1 -0
- package/dist/environments/path-policy.js +8 -0
- package/dist/{mage2gen.d.ts → features/mage2gen/service.d.ts} +0 -1
- package/dist/{mage2gen.js → features/mage2gen/service.js} +8 -3
- package/dist/integrations/pi/context.d.ts +2 -0
- package/dist/integrations/pi/context.js +46 -0
- package/dist/integrations/pi/mage2gen.d.ts +2 -0
- package/dist/integrations/pi/mage2gen.js +19 -0
- package/dist/integrations/pi/permissions.d.ts +6 -0
- package/dist/integrations/pi/permissions.js +36 -0
- package/dist/integrations/pi/session.d.ts +12 -0
- package/dist/integrations/pi/session.js +9 -0
- package/dist/integrations/pi/testing.d.ts +2 -0
- package/dist/integrations/pi/testing.js +88 -0
- package/dist/integrations/pi/theme.d.ts +2 -0
- package/dist/integrations/pi/theme.js +6 -0
- package/dist/integrations/pi/workflows.d.ts +2 -0
- package/dist/integrations/pi/workflows.js +26 -0
- package/dist/integrations/pi/workspace.d.ts +5 -0
- package/dist/integrations/pi/workspace.js +33 -0
- package/dist/magento/deploy-mode.d.ts +2 -0
- package/dist/magento/deploy-mode.js +10 -0
- package/dist/magento/index.d.ts +5 -0
- package/dist/magento/index.js +4 -0
- package/dist/magento/inspector.d.ts +9 -0
- package/dist/magento/inspector.js +56 -0
- package/dist/magento/model.d.ts +25 -0
- package/dist/magento/prompt.d.ts +2 -0
- package/dist/magento/prompt.js +3 -0
- package/dist/magento/themes.d.ts +3 -0
- package/dist/magento/themes.js +46 -0
- package/dist/provision/advisor.d.ts +12 -0
- package/dist/provision/advisor.js +28 -0
- package/dist/provision/discovery.d.ts +12 -0
- package/dist/provision/discovery.js +61 -0
- package/dist/provision/interview.d.ts +9 -0
- package/dist/provision/interview.js +29 -0
- package/dist/provision/model.d.ts +82 -0
- package/dist/provision/model.js +5 -0
- package/dist/provision/planner.d.ts +4 -0
- package/dist/provision/planner.js +51 -0
- package/dist/provision/runner.d.ts +35 -0
- package/dist/provision/runner.js +98 -0
- package/dist/shared/package-paths.d.ts +3 -0
- package/dist/shared/package-paths.js +10 -0
- package/dist/testing/discovery.d.ts +10 -0
- package/dist/testing/discovery.js +68 -0
- package/dist/testing/host.d.ts +29 -0
- package/dist/testing/host.js +50 -0
- package/dist/testing/index.d.ts +5 -0
- package/dist/testing/index.js +5 -0
- package/dist/testing/magerun.d.ts +8 -0
- package/dist/testing/magerun.js +73 -0
- package/dist/testing/model.d.ts +18 -0
- package/dist/testing/url.d.ts +4 -0
- package/dist/testing/url.js +26 -0
- package/dist/workflows/builtins.d.ts +2 -0
- package/dist/workflows/builtins.js +38 -0
- package/dist/workflows/change-tracker.d.ts +9 -0
- package/dist/workflows/change-tracker.js +38 -0
- package/dist/workflows/definition.d.ts +7 -0
- package/dist/workflows/index.d.ts +4 -0
- package/dist/workflows/index.js +3 -0
- package/dist/workflows/runner.d.ts +7 -0
- package/dist/workflows/runner.js +28 -0
- package/package.json +32 -9
- package/skills/magento-testing/SKILL.md +10 -0
- package/dist/backend.d.ts +0 -24
- package/dist/backend.js +0 -103
- package/dist/cli.js +0 -89
- package/dist/config.d.ts +0 -127
- package/dist/config.js +0 -170
- package/dist/config.test.js +0 -51
- package/dist/constants.d.ts +0 -20
- package/dist/constants.js +0 -15
- package/dist/mage2gen.test.js +0 -6
- package/dist/magento.d.ts +0 -40
- package/dist/magento.js +0 -119
- package/dist/magento.test.js +0 -31
- package/dist/skills.d.ts +0 -11
- package/dist/skills.test.js +0 -34
- package/dist/workflow.d.ts +0 -20
- package/dist/workflow.js +0 -67
- package/dist/workflow.test.js +0 -17
- package/extensions/merchantduo.js +0 -311
- /package/dist/{cli.d.ts → cli/main.d.ts} +0 -0
- /package/dist/{config.test.d.ts → environments/adapter.js} +0 -0
- /package/dist/{mage2gen.test.d.ts → environments/executor.js} +0 -0
- /package/dist/{magento.test.d.ts → magento/model.js} +0 -0
- /package/dist/{skills.test.d.ts → testing/model.js} +0 -0
- /package/dist/{workflow.test.d.ts → workflows/definition.js} +0 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
export class WardenAdapter {
|
|
3
|
+
environment;
|
|
4
|
+
cwd;
|
|
5
|
+
constructor(environment, cwd) {
|
|
6
|
+
this.environment = environment;
|
|
7
|
+
this.cwd = cwd;
|
|
8
|
+
}
|
|
9
|
+
path(path) {
|
|
10
|
+
return path.startsWith("/") ? path : `${this.environment.root}/${path}`;
|
|
11
|
+
}
|
|
12
|
+
command(argv, target) {
|
|
13
|
+
const service = target
|
|
14
|
+
? this.environment.targets[target]
|
|
15
|
+
: this.environment.filesService;
|
|
16
|
+
if (!service)
|
|
17
|
+
throw new Error(`Unknown Warden target: ${target}`);
|
|
18
|
+
return {
|
|
19
|
+
file: "warden",
|
|
20
|
+
args: ["env", "exec", "-T", service, ...argv],
|
|
21
|
+
cwd: resolve(this.cwd, this.environment.projectRoot),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Environment } from "#environments/model";
|
|
2
|
+
import type { CommandExecutor, CommandResult, CommandSpec, RunOptions } from "#environments/executor";
|
|
3
|
+
export { contain } from "#environments/path-policy";
|
|
4
|
+
/** Concrete local/Warden/SSH command adapter selected once for a session. */
|
|
5
|
+
export declare class EnvironmentBackend implements CommandExecutor {
|
|
6
|
+
#private;
|
|
7
|
+
readonly environment: Environment;
|
|
8
|
+
readonly localRoot: string;
|
|
9
|
+
constructor(environment: Environment, localRoot: string);
|
|
10
|
+
path(path: string): string;
|
|
11
|
+
command(argv: string[], target?: string): CommandSpec;
|
|
12
|
+
run(argv: string[], options?: RunOptions): Promise<CommandResult>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createEnvironmentAdapter } from "#environments/factory";
|
|
3
|
+
export { contain } from "#environments/path-policy";
|
|
4
|
+
const MAX_OUTPUT = 64 * 1024;
|
|
5
|
+
/** Concrete local/Warden/SSH command adapter selected once for a session. */
|
|
6
|
+
export class EnvironmentBackend {
|
|
7
|
+
environment;
|
|
8
|
+
localRoot;
|
|
9
|
+
#adapter;
|
|
10
|
+
constructor(environment, localRoot) {
|
|
11
|
+
this.environment = environment;
|
|
12
|
+
this.localRoot = localRoot;
|
|
13
|
+
this.#adapter = createEnvironmentAdapter(environment, localRoot);
|
|
14
|
+
}
|
|
15
|
+
path(path) {
|
|
16
|
+
return this.#adapter.path(path);
|
|
17
|
+
}
|
|
18
|
+
command(argv, target) {
|
|
19
|
+
if (!argv[0])
|
|
20
|
+
throw new Error("Cannot execute an empty command");
|
|
21
|
+
return this.#adapter.command(argv, target);
|
|
22
|
+
}
|
|
23
|
+
async run(argv, options = {}) {
|
|
24
|
+
const spec = this.command(argv, options.target);
|
|
25
|
+
return new Promise((resolveResult) => {
|
|
26
|
+
const child = spawn(spec.file, spec.args, {
|
|
27
|
+
cwd: spec.cwd,
|
|
28
|
+
signal: options.signal,
|
|
29
|
+
detached: process.platform !== "win32",
|
|
30
|
+
});
|
|
31
|
+
let out = "";
|
|
32
|
+
let err = "";
|
|
33
|
+
let truncated = false;
|
|
34
|
+
let settled = false;
|
|
35
|
+
const finish = (result) => {
|
|
36
|
+
if (!settled) {
|
|
37
|
+
settled = true;
|
|
38
|
+
resolveResult(result);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const add = (old, buffer) => {
|
|
42
|
+
const next = old + buffer.toString();
|
|
43
|
+
if (next.length > MAX_OUTPUT) {
|
|
44
|
+
truncated = true;
|
|
45
|
+
return next.slice(0, MAX_OUTPUT);
|
|
46
|
+
}
|
|
47
|
+
return next;
|
|
48
|
+
};
|
|
49
|
+
child.stdout.on("data", (buffer) => (out = add(out, buffer)));
|
|
50
|
+
child.stderr.on("data", (buffer) => (err = add(err, buffer)));
|
|
51
|
+
if (options.input !== undefined)
|
|
52
|
+
child.stdin.end(options.input);
|
|
53
|
+
const timer = options.timeoutMs
|
|
54
|
+
? setTimeout(() => child.kill("SIGTERM"), options.timeoutMs)
|
|
55
|
+
: undefined;
|
|
56
|
+
child.on("error", (error) => {
|
|
57
|
+
if (timer)
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
finish({
|
|
60
|
+
stdout: out,
|
|
61
|
+
stderr: error.message,
|
|
62
|
+
exitCode: 127,
|
|
63
|
+
truncated,
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
child.on("close", (code) => {
|
|
67
|
+
if (timer)
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
finish({ stdout: out, stderr: err, exitCode: code ?? 1, truncated });
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type CommandResult = {
|
|
2
|
+
stdout: string;
|
|
3
|
+
stderr: string;
|
|
4
|
+
exitCode: number;
|
|
5
|
+
truncated: boolean;
|
|
6
|
+
};
|
|
7
|
+
export type CommandSpec = {
|
|
8
|
+
file: string;
|
|
9
|
+
args: string[];
|
|
10
|
+
cwd?: string;
|
|
11
|
+
};
|
|
12
|
+
export type RunOptions = {
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
target?: string;
|
|
16
|
+
input?: string | Buffer;
|
|
17
|
+
};
|
|
18
|
+
/** Port used by Magento inspection and workflows, independent of environment type. */
|
|
19
|
+
export interface CommandExecutor {
|
|
20
|
+
path(path: string): string;
|
|
21
|
+
command(argv: string[], target?: string): CommandSpec;
|
|
22
|
+
run(argv: string[], options?: RunOptions): Promise<CommandResult>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { LocalAdapter } from "#environments/adapters/local";
|
|
2
|
+
import { SshAdapter } from "#environments/adapters/ssh";
|
|
3
|
+
import { WardenAdapter } from "#environments/adapters/warden";
|
|
4
|
+
export function createEnvironmentAdapter(environment, cwd) {
|
|
5
|
+
switch (environment.type) {
|
|
6
|
+
case "local":
|
|
7
|
+
return new LocalAdapter(environment, cwd);
|
|
8
|
+
case "warden":
|
|
9
|
+
return new WardenAdapter(environment, cwd);
|
|
10
|
+
case "ssh":
|
|
11
|
+
return new SshAdapter(environment);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
type EnvironmentTesting = {
|
|
2
|
+
frontendUrl?: string;
|
|
3
|
+
adminUrl?: string;
|
|
4
|
+
allowInsecureTls?: boolean;
|
|
5
|
+
};
|
|
6
|
+
export type LocalEnvironment = {
|
|
7
|
+
type: "local";
|
|
8
|
+
root: string;
|
|
9
|
+
testing?: EnvironmentTesting;
|
|
10
|
+
};
|
|
11
|
+
export type WardenEnvironment = {
|
|
12
|
+
type: "warden";
|
|
13
|
+
projectRoot: string;
|
|
14
|
+
root: string;
|
|
15
|
+
filesService: string;
|
|
16
|
+
targets: Record<string, string>;
|
|
17
|
+
testing?: EnvironmentTesting;
|
|
18
|
+
};
|
|
19
|
+
export type SshEnvironment = {
|
|
20
|
+
type: "ssh";
|
|
21
|
+
host: string;
|
|
22
|
+
root: string;
|
|
23
|
+
writable: boolean;
|
|
24
|
+
testing?: EnvironmentTesting;
|
|
25
|
+
};
|
|
26
|
+
export type Environment = LocalEnvironment | WardenEnvironment | SshEnvironment;
|
|
27
|
+
export declare function environmentWritable(environment: Environment): boolean;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function contain(root: string, candidate: string): string;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { relative, resolve, sep } from "node:path";
|
|
2
|
+
export function contain(root, candidate) {
|
|
3
|
+
const full = resolve(root, candidate);
|
|
4
|
+
const relation = relative(resolve(root), full);
|
|
5
|
+
if (relation === ".." || relation.startsWith(`..${sep}`))
|
|
6
|
+
throw new Error(`Path escapes configured root: ${candidate}`);
|
|
7
|
+
return full;
|
|
8
|
+
}
|
|
@@ -12,5 +12,4 @@ export type Mage2GenRequest = {
|
|
|
12
12
|
};
|
|
13
13
|
export declare function mage2genDestination(projectRoot: string, vendor: string, module: string): string;
|
|
14
14
|
export declare function mage2genModuleExists(request: Mage2GenRequest): Promise<boolean>;
|
|
15
|
-
/** Execute the vendored Mage2Gen library on the host workspace mounted by local/Warden Magento. */
|
|
16
15
|
export declare function generateMage2GenModule(request: Mage2GenRequest): Promise<string>;
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { access } from "node:fs/promises";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
|
+
import { contain } from "#environments/path-policy";
|
|
4
5
|
export function mage2genDestination(projectRoot, vendor, module) {
|
|
5
|
-
|
|
6
|
+
if (!/^[A-Z][A-Za-z0-9]*$/.test(vendor) ||
|
|
7
|
+
!/^[A-Z][A-Za-z0-9]*$/.test(module))
|
|
8
|
+
throw new Error("Mage2Gen requires PascalCase vendor and module names");
|
|
9
|
+
return contain(resolve(projectRoot, "app/code"), `${vendor}/${module}`);
|
|
6
10
|
}
|
|
7
11
|
export async function mage2genModuleExists(request) {
|
|
8
12
|
try {
|
|
@@ -13,7 +17,6 @@ export async function mage2genModuleExists(request) {
|
|
|
13
17
|
return false;
|
|
14
18
|
}
|
|
15
19
|
}
|
|
16
|
-
/** Execute the vendored Mage2Gen library on the host workspace mounted by local/Warden Magento. */
|
|
17
20
|
export async function generateMage2GenModule(request) {
|
|
18
21
|
const program = `
|
|
19
22
|
import json, sys
|
|
@@ -41,6 +44,8 @@ print(data['destination'])
|
|
|
41
44
|
child.stdout.on("data", (chunk) => (output += chunk));
|
|
42
45
|
child.stderr.on("data", (chunk) => (error += chunk));
|
|
43
46
|
child.on("error", reject);
|
|
44
|
-
child.on("close", (code) => code === 0
|
|
47
|
+
child.on("close", (code) => code === 0
|
|
48
|
+
? resolveResult(output.trim())
|
|
49
|
+
: reject(new Error(error || `Mage2Gen exited with ${code}`)));
|
|
45
50
|
});
|
|
46
51
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { refreshStatus } from "#app/status";
|
|
2
|
+
import { merchantDuoSystemPrompt } from "#app/system-prompt";
|
|
3
|
+
import { discoverTesting, discoverMagerun } from "#testing/index";
|
|
4
|
+
import { discoverHostExecutable } from "#testing/host";
|
|
5
|
+
import { runtime, sessionEvents } from "#integrations/pi/session";
|
|
6
|
+
export default function context(pi) {
|
|
7
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
8
|
+
runtime.resetInjection();
|
|
9
|
+
const state = await runtime.boot(ctx.cwd);
|
|
10
|
+
const magerun = await discoverMagerun(state.selected.environment, state.backend, ctx.cwd);
|
|
11
|
+
// HTTP and Chromium deliberately run on the host against Warden's routed URL.
|
|
12
|
+
// Do not use EnvironmentBackend here: its Warden adapter would probe php-fpm.
|
|
13
|
+
// Prefer non-Snap Chrome: Snap Chromium cannot write MerchantDuo artifacts
|
|
14
|
+
// and its private /tmp makes generated output unavailable to the host.
|
|
15
|
+
const [http, browser] = await Promise.all([
|
|
16
|
+
discoverHostExecutable(["curl"]),
|
|
17
|
+
discoverHostExecutable(["google-chrome", "google-chrome-stable", "chromium", "chromium-browser"]),
|
|
18
|
+
]);
|
|
19
|
+
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, magerun: magerun.executable }));
|
|
20
|
+
if (magerun.diagnostic)
|
|
21
|
+
state.testing?.diagnostics.push(magerun.diagnostic);
|
|
22
|
+
refreshStatus(ctx, state);
|
|
23
|
+
sessionEvents.emit("merchantduo.context", state);
|
|
24
|
+
pi.appendEntry("merchantduo.context", { environment: state.selected.name, testing: state.testing });
|
|
25
|
+
});
|
|
26
|
+
pi.on("session_compact", () => runtime.resetInjection());
|
|
27
|
+
pi.on("session_shutdown", () => sessionEvents.clear());
|
|
28
|
+
pi.on("tool_call", (event) => {
|
|
29
|
+
if (event.toolName !== "write" && event.toolName !== "edit")
|
|
30
|
+
return;
|
|
31
|
+
const path = event.input.path;
|
|
32
|
+
if (typeof path === "string")
|
|
33
|
+
runtime.changes.begin(event.toolCallId, path);
|
|
34
|
+
});
|
|
35
|
+
pi.on("tool_result", (event, ctx) => {
|
|
36
|
+
runtime.changes.finish(event.toolCallId, !event.isError);
|
|
37
|
+
const files = runtime.changes.files();
|
|
38
|
+
if (files.length)
|
|
39
|
+
ctx.ui.setStatus("merchantduo-actions", `Pending validation and cache recommendations for ${files.length} edit(s)`);
|
|
40
|
+
});
|
|
41
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
42
|
+
const state = await runtime.boot(ctx.cwd);
|
|
43
|
+
const testing = state.testing;
|
|
44
|
+
return { systemPrompt: `${event.systemPrompt}\n${merchantDuoSystemPrompt()}\nTesting 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.` };
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { mage2genRoot } from "#app/system-prompt";
|
|
4
|
+
import { generateMage2GenModule, mage2genDestination, mage2genModuleExists } from "#features/mage2gen/service";
|
|
5
|
+
import { runtime, text } from "#integrations/pi/session";
|
|
6
|
+
export default function mage2gen(pi) { pi.registerTool({ name: "mage2gen_generate_module", label: "Generate Magento module", description: "Preview or explicitly generate a Magento module through vendored Mage2Gen.", parameters: Type.Object({ vendor: Type.String(), module: Type.String(), description: Type.Optional(Type.String()), execute: Type.Optional(Type.Boolean()) }), async execute(_id, params, _signal, _update, ctx) { const state = await runtime.boot(ctx.cwd); if (state.selected.environment.type === "ssh")
|
|
7
|
+
return text("Mage2Gen is unavailable for SSH."); const root = state.selected.environment.type === "warden" ? resolve(ctx.cwd, state.selected.environment.projectRoot) : resolve(ctx.cwd, state.selected.environment.root); let destination; try {
|
|
8
|
+
destination = mage2genDestination(root, params.vendor, params.module);
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
return text(error.message);
|
|
12
|
+
} const request = { mage2genRoot: mage2genRoot(), projectRoot: root, vendor: params.vendor, module: params.module, description: params.description }; if (await mage2genModuleExists(request))
|
|
13
|
+
return text(`Refused: module destination already exists: ${destination}`); if (!params.execute)
|
|
14
|
+
return text(`Preview: Mage2Gen will create ${destination}. Set execute=true after the user's direct request.`); try {
|
|
15
|
+
return text(`Mage2Gen created ${params.vendor}_${params.module} at ${await generateMage2GenModule(request)}.`);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
return text(`Mage2Gen failed: ${error.message}`);
|
|
19
|
+
} } }); }
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { Environment } from "#environments/model";
|
|
3
|
+
export declare function requiresExecuteConfirmation(toolName: string, input: Record<string, unknown>): boolean;
|
|
4
|
+
export declare function requiresSshConfirmation(environment: Environment, toolName: string): boolean;
|
|
5
|
+
/** The sole MerchantDuo confirmation gate. Explicit test/navigation tools are intentionally excluded. */
|
|
6
|
+
export default function permissions(pi: ExtensionAPI): void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { runtime } from "#integrations/pi/session";
|
|
2
|
+
export function requiresExecuteConfirmation(toolName, input) {
|
|
3
|
+
return (toolName === "mage2gen_generate_module" || toolName === "magento_workflow") && input.execute === true;
|
|
4
|
+
}
|
|
5
|
+
export function requiresSshConfirmation(environment, toolName) {
|
|
6
|
+
return environment.type === "ssh" && environment.writable && ["write", "edit", "bash"].includes(toolName);
|
|
7
|
+
}
|
|
8
|
+
/** The sole MerchantDuo confirmation gate. Explicit test/navigation tools are intentionally excluded. */
|
|
9
|
+
export default function permissions(pi) {
|
|
10
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
11
|
+
const state = await runtime.boot(ctx.cwd);
|
|
12
|
+
const input = event.input;
|
|
13
|
+
if (requiresExecuteConfirmation(event.toolName, input)) {
|
|
14
|
+
const ok = ctx.hasUI && await ctx.ui.confirm("Confirm Magento action", `Execute ${event.toolName}?`);
|
|
15
|
+
return ok ? undefined : { block: true, reason: "Cancelled: confirmation required." };
|
|
16
|
+
}
|
|
17
|
+
if (state.selected.environment.type === "ssh" && ["write", "edit", "bash"].includes(event.toolName)) {
|
|
18
|
+
if (!state.selected.environment.writable)
|
|
19
|
+
return { block: true, reason: "MerchantDuo: this SSH environment is read-only." };
|
|
20
|
+
const ok = !requiresSshConfirmation(state.selected.environment, event.toolName) || (ctx.hasUI && await ctx.ui.confirm("SSH workspace action", `Run ${event.toolName} on ${state.selected.environment.host}?`));
|
|
21
|
+
return ok ? undefined : { block: true, reason: "Cancelled: SSH confirmation required." };
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
pi.on("user_bash", async (event, ctx) => {
|
|
25
|
+
const state = await runtime.boot(ctx.cwd);
|
|
26
|
+
if (state.selected.environment.type !== "ssh")
|
|
27
|
+
return;
|
|
28
|
+
if (!state.selected.environment.writable)
|
|
29
|
+
return { result: { output: "MerchantDuo: this SSH environment is read-only.", exitCode: 1, cancelled: true, truncated: false } };
|
|
30
|
+
const ok = ctx.hasUI && await ctx.ui.confirm("SSH command", `Run command on ${state.selected.environment.host}?`);
|
|
31
|
+
if (!ok)
|
|
32
|
+
return { result: { output: "Cancelled: confirmation required.", exitCode: 1, cancelled: true, truncated: false } };
|
|
33
|
+
const result = await state.backend.run(["sh", "-lc", event.command]);
|
|
34
|
+
return { result: { output: `${result.stdout}${result.stderr}`, exitCode: result.exitCode, cancelled: false, truncated: result.truncated } };
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { MerchantDuoRuntime } from "#app/runtime";
|
|
2
|
+
/** Pi evaluates all package extensions in one module graph; this is session-scoped shared state. */
|
|
3
|
+
export declare const runtime: MerchantDuoRuntime;
|
|
4
|
+
/** Native Pi event bus for resolved session context consumers. */
|
|
5
|
+
export declare const sessionEvents: import("@earendil-works/pi-coding-agent").EventBusController;
|
|
6
|
+
export declare function text(value: string): {
|
|
7
|
+
content: {
|
|
8
|
+
type: "text";
|
|
9
|
+
text: string;
|
|
10
|
+
}[];
|
|
11
|
+
details: {};
|
|
12
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { MerchantDuoRuntime } from "#app/runtime";
|
|
2
|
+
import { createEventBus } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
/** Pi evaluates all package extensions in one module graph; this is session-scoped shared state. */
|
|
4
|
+
export const runtime = new MerchantDuoRuntime();
|
|
5
|
+
/** Native Pi event bus for resolved session context consumers. */
|
|
6
|
+
export const sessionEvents = createEventBus();
|
|
7
|
+
export function text(value) {
|
|
8
|
+
return { content: [{ type: "text", text: value }], details: {} };
|
|
9
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { mkdtemp, mkdir, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { Type } from "typebox";
|
|
5
|
+
import { merchantHome } from "#config/paths";
|
|
6
|
+
import { joinedUrl, relativePath, sameOrigin } from "#testing/url";
|
|
7
|
+
import { artifactExists, bound, browserArguments, NodeHostProcessRunner } from "#testing/host";
|
|
8
|
+
import { runtime, text } from "#integrations/pi/session";
|
|
9
|
+
const host = new NodeHostProcessRunner();
|
|
10
|
+
export default function testing(pi) {
|
|
11
|
+
pi.registerTool({ name: "magento_http_test", label: "Magento HTTP test", description: "Explicit unauthenticated GET or HEAD test against a discovered Magento URL.", parameters: Type.Object({ target: Type.Union([Type.Literal("frontend"), Type.Literal("adminhtml")]), path: Type.Optional(Type.String()), method: Type.Optional(Type.Union([Type.Literal("GET"), Type.Literal("HEAD")])) }), async execute(_id, params, signal, _update, ctx) {
|
|
12
|
+
const state = await runtime.boot(ctx.cwd);
|
|
13
|
+
const base = params.target === "frontend" ? state.testing?.frontendUrl : state.testing?.adminUrl;
|
|
14
|
+
const url = targetUrl(base, params.path ?? "/");
|
|
15
|
+
if (!url)
|
|
16
|
+
return text("Refused: target URL is unavailable or path is not same-origin.");
|
|
17
|
+
const args = ["--silent", "--show-error", "--location", "--max-time", "20", "--connect-timeout", "8", "--dump-header", "-", "--request", params.method ?? "GET", url];
|
|
18
|
+
if (!state.testing?.http.executable)
|
|
19
|
+
return text(state.testing?.http.reason ?? "HTTP testing unavailable.");
|
|
20
|
+
if (state.selected.environment.testing?.allowInsecureTls ?? state.config.testing.allowInsecureTls)
|
|
21
|
+
args.unshift("--insecure");
|
|
22
|
+
const result = await host.run(state.testing.http.executable, args, { cwd: ctx.cwd, signal });
|
|
23
|
+
return text(bound(`URL: ${url}\n${result.output}`));
|
|
24
|
+
} });
|
|
25
|
+
pi.registerTool({ name: "magento_browser_test", label: "Magento browser test", description: "Explicit stateless Chromium screenshot or DOM capture for a discovered Magento URL.", parameters: Type.Object({ target: Type.Union([Type.Literal("frontend"), Type.Literal("adminhtml")]), action: Type.Union([Type.Literal("screenshot"), Type.Literal("dump-dom")]), path: Type.Optional(Type.String()) }), async execute(_id, params, signal, _update, ctx) {
|
|
26
|
+
const state = await runtime.boot(ctx.cwd);
|
|
27
|
+
const executable = state.testing?.browser.executable;
|
|
28
|
+
const base = params.target === "frontend" ? state.testing?.frontendUrl : state.testing?.adminUrl;
|
|
29
|
+
const url = targetUrl(base, params.path ?? "/");
|
|
30
|
+
if (!executable || !url)
|
|
31
|
+
return text("Browser test unavailable: Chromium or target URL is unavailable.");
|
|
32
|
+
const artifactDir = resolve(merchantHome(), "artifacts", projectId(ctx.cwd));
|
|
33
|
+
await mkdir(artifactDir, { recursive: true, mode: 0o700 });
|
|
34
|
+
const profile = await mkdtemp(join(tmpdir(), "merchantduo-chromium-"));
|
|
35
|
+
const artifact = join(artifactDir, `${Date.now()}-${params.action}${params.action === "screenshot" ? ".png" : ".html"}`);
|
|
36
|
+
try {
|
|
37
|
+
const allowInsecureTls = state.selected.environment.testing?.allowInsecureTls ?? state.config.testing.allowInsecureTls;
|
|
38
|
+
const args = browserArguments({ profile, artifact, url, action: params.action, allowInsecureTls });
|
|
39
|
+
const result = await host.run(executable, args, { cwd: ctx.cwd, signal });
|
|
40
|
+
if (params.action === "dump-dom") {
|
|
41
|
+
if (result.code)
|
|
42
|
+
return text(`Browser DOM capture failed (exit ${result.code}):\n${bound(result.output)}`);
|
|
43
|
+
await BunOrNode.write(artifact, bound(result.output));
|
|
44
|
+
return text(`DOM saved to ${artifact}\n${bound(result.output)}`);
|
|
45
|
+
}
|
|
46
|
+
if (result.code)
|
|
47
|
+
return text(`Browser screenshot failed (exit ${result.code}):\n${bound(result.output)}`);
|
|
48
|
+
if (!(await artifactExists(artifact)))
|
|
49
|
+
return text(`Browser screenshot failed: ${executable} did not create the host artifact. A sandboxed Snap Chromium cannot access MerchantDuo artifacts.\n${bound(result.output)}`);
|
|
50
|
+
const image = await readFile(artifact);
|
|
51
|
+
return { content: [{ type: "text", text: `Screenshot saved to ${artifact}\n${bound(result.output)}` }, { type: "image", data: image.toString("base64"), mimeType: "image/png" }], details: {} };
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
await rm(profile, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
} });
|
|
57
|
+
pi.registerTool({ name: "magento_php_repl", label: "Magento PHP console", description: "Explicit one-shot magerun2 PHP snippet execution.", parameters: Type.Object({ snippet: Type.String({ minLength: 1 }), area: Type.Optional(Type.Union([Type.Literal("adminhtml"), Type.Literal("frontend"), Type.Literal("crontab"), Type.Literal("webapi_rest"), Type.Literal("graphql")])) }), async execute(_id, params, signal, _update, ctx) {
|
|
58
|
+
const state = await runtime.boot(ctx.cwd);
|
|
59
|
+
if (state.selected.environment.type === "ssh" && !state.selected.environment.writable)
|
|
60
|
+
return text("PHP REPL is unavailable for read-only SSH.");
|
|
61
|
+
const executable = state.testing?.phpRepl.executable;
|
|
62
|
+
if (!executable)
|
|
63
|
+
return text(state.testing?.phpRepl.reason ?? "PHP REPL unavailable.");
|
|
64
|
+
const args = [executable, "dev:console"];
|
|
65
|
+
if (params.area)
|
|
66
|
+
args.push(`--area=${params.area}`);
|
|
67
|
+
if (state.selected.environment.type === "local") {
|
|
68
|
+
const result = await host.run("sh", ["-lc", `${quote(executable)} dev:console${params.area ? ` --area=${params.area}` : ""}`], { cwd: ctx.cwd, signal, input: params.snippet });
|
|
69
|
+
return text(bound(result.output));
|
|
70
|
+
}
|
|
71
|
+
const result = await state.backend.run(args, { input: params.snippet, signal });
|
|
72
|
+
return text(bound(`${result.stdout}${result.stderr}`));
|
|
73
|
+
} });
|
|
74
|
+
registerNavigate(pi, "navigate-frontend", "frontend");
|
|
75
|
+
registerNavigate(pi, "navigate-adminhtml", "adminhtml");
|
|
76
|
+
}
|
|
77
|
+
function registerNavigate(pi, name, target) { pi.registerCommand(name, { description: `Open the discovered Magento ${target} URL in the default browser.`, handler: async (args, ctx) => { const state = await runtime.boot(ctx.cwd); const base = target === "frontend" ? state.testing?.frontendUrl : state.testing?.adminUrl; const url = targetUrl(base, args.trim() || "/"); if (!url) {
|
|
78
|
+
ctx.ui.notify("URL unavailable or path is not same-origin.", "error");
|
|
79
|
+
return;
|
|
80
|
+
} const [file, openerArgs] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]]; const result = await host.run(file, openerArgs, { cwd: ctx.cwd }); if (result.code)
|
|
81
|
+
ctx.ui.notify(`Could not open browser: ${result.output}`, "error");
|
|
82
|
+
else
|
|
83
|
+
ctx.ui.notify(`Opened ${url}`, "info"); } }); }
|
|
84
|
+
function targetUrl(base, path) { if (!relativePath(path))
|
|
85
|
+
return undefined; const url = joinedUrl(base, path); return url && sameOrigin(base, url) ? url : undefined; }
|
|
86
|
+
function projectId(cwd) { return cwd.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "").slice(-80) || "project"; }
|
|
87
|
+
function quote(value) { return `'${value.replace(/'/g, "'\\''")}'`; }
|
|
88
|
+
const BunOrNode = { async write(path, value) { const { writeFile } = await import("node:fs/promises"); await writeFile(path, value, "utf8"); } };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { refreshStatus } from "#app/status";
|
|
2
|
+
import { runtime } from "#integrations/pi/session";
|
|
3
|
+
export default function theme(pi) { pi.registerCommand("theme", { description: "Select all, any, or a detected Magento theme.", handler: async (args, ctx) => { const state = await runtime.boot(ctx.cwd); const value = args.trim(); const choices = ["all", "any", ...state.magento.themes.map((theme) => theme.code)]; if (!choices.includes(value)) {
|
|
4
|
+
ctx.ui.notify(`Choose one of: ${choices.join(", ")}`, "error");
|
|
5
|
+
return;
|
|
6
|
+
} const changed = await runtime.changeTheme(ctx.cwd, value); refreshStatus(ctx, changed); ctx.ui.notify(`Theme scope set to ${value}.`, "info"); } }); }
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { builtInWorkflows } from "#workflows/builtins";
|
|
3
|
+
import { runWorkflow, syntaxCheck } from "#workflows/runner";
|
|
4
|
+
import { runtime, text } from "#integrations/pi/session";
|
|
5
|
+
export default function workflows(pi) {
|
|
6
|
+
pi.registerTool({ name: "magento_workflow", label: "Magento workflow", description: "Preview or explicitly execute a supported Magento operational action.", parameters: Type.Object({ action: Type.String(), execute: Type.Optional(Type.Boolean()) }), async execute(_id, params, _signal, _update, ctx) {
|
|
7
|
+
const state = await runtime.boot(ctx.cwd);
|
|
8
|
+
if (params.action === "status")
|
|
9
|
+
return text(JSON.stringify({ snapshot: state.magento, testing: state.testing, pendingFiles: runtime.changes.files(), workflows: builtInWorkflows.map(({ id, description }) => ({ id, description })) }, null, 2));
|
|
10
|
+
if (params.action === "syntax-check")
|
|
11
|
+
return text((await syntaxCheck(state.backend, runtime.changes.files())).join("\n") || "No supported changed files require syntax checks.");
|
|
12
|
+
const workflow = builtInWorkflows.find(({ id }) => id === params.action);
|
|
13
|
+
if (!workflow)
|
|
14
|
+
return text(`Unknown workflow: ${params.action}`);
|
|
15
|
+
const cacheTypes = workflow.id === "cache-clean" ? runtime.changes.safeCacheTypes(state.magento) : [];
|
|
16
|
+
const command = workflow.command(cacheTypes);
|
|
17
|
+
if (!params.execute)
|
|
18
|
+
return text(`Preview: ${command.join(" ")}. Set execute=true after the user's direct request.`);
|
|
19
|
+
if (state.selected.environment.type === "ssh" && !workflow.allowRemote)
|
|
20
|
+
return text(`Refused: ${workflow.id} is not enabled for SSH environments.`);
|
|
21
|
+
const result = await runWorkflow(state.backend, workflow, cacheTypes);
|
|
22
|
+
if (result.result.exitCode === 0 && workflow.id === "cache-clean")
|
|
23
|
+
runtime.changes.clear();
|
|
24
|
+
return text(JSON.stringify(result, null, 2));
|
|
25
|
+
} });
|
|
26
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { EnvironmentBackend } from "#environments/backend";
|
|
3
|
+
/** Replaces Pi's native workspace tools with selected-environment operations. */
|
|
4
|
+
export default function workspace(pi: ExtensionAPI): void;
|
|
5
|
+
export declare function workspacePath(backend: EnvironmentBackend, cwd: string, candidate: string): string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { relative, resolve } from "node:path";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { createEditToolDefinition, createReadToolDefinition, createWriteToolDefinition, } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { runtime } from "#integrations/pi/session";
|
|
5
|
+
const shell = (value) => `'${value.replace(/'/g, "'\\''")}'`;
|
|
6
|
+
const result = (value) => ({ content: [{ type: "text", text: value }], details: {} });
|
|
7
|
+
/** Replaces Pi's native workspace tools with selected-environment operations. */
|
|
8
|
+
export default function workspace(pi) {
|
|
9
|
+
registerFileTools(pi);
|
|
10
|
+
pi.registerTool({ name: "bash", label: "bash", description: "Run a shell command in the selected MerchantDuo environment.", parameters: Type.Object({ command: Type.String(), timeout: Type.Optional(Type.Number()) }), async execute(_id, params, signal, _update, ctx) { const state = await runtime.boot(ctx.cwd); const output = await state.backend.run(["sh", "-lc", params.command], { signal, timeoutMs: params.timeout }); return result(`${output.stdout}${output.stderr}`); } });
|
|
11
|
+
pi.registerTool({ name: "ls", label: "ls", description: "List a selected-environment directory.", parameters: Type.Object({ path: Type.Optional(Type.String()), limit: Type.Optional(Type.Number()) }), async execute(_id, params, signal, _update, ctx) { const state = await runtime.boot(ctx.cwd); const path = workspacePath(state.backend, ctx.cwd, params.path ?? "."); const output = await state.backend.run(["sh", "-lc", `find ${shell(path)} -mindepth 1 -maxdepth 1 -printf '%f\\n' | head -n ${Math.max(1, Math.min(params.limit ?? 200, 1000))}`], { signal }); return result(output.stdout || output.stderr); } });
|
|
12
|
+
pi.registerTool({ name: "find", label: "find", description: "Find files in the selected MerchantDuo environment.", parameters: Type.Object({ pattern: Type.String(), path: Type.Optional(Type.String()), limit: Type.Optional(Type.Number()) }), async execute(_id, params, signal, _update, ctx) { const state = await runtime.boot(ctx.cwd); const path = workspacePath(state.backend, ctx.cwd, params.path ?? "."); const limit = Math.max(1, Math.min(params.limit ?? 1000, 5000)); const output = await state.backend.run(["sh", "-lc", `find ${shell(path)} -path '*/node_modules/*' -prune -o -path '*/.git/*' -prune -o -name ${shell(params.pattern)} -print | head -n ${limit}`], { signal }); return result(output.stdout || output.stderr); } });
|
|
13
|
+
pi.registerTool({ name: "grep", label: "grep", description: "Search text using ripgrep in the selected MerchantDuo environment.", parameters: Type.Object({ pattern: Type.String(), path: Type.Optional(Type.String()), glob: Type.Optional(Type.String()), ignoreCase: Type.Optional(Type.Boolean()), literal: Type.Optional(Type.Boolean()), context: Type.Optional(Type.Number()), limit: Type.Optional(Type.Number()) }), async execute(_id, params, signal, _update, ctx) { const state = await runtime.boot(ctx.cwd); const path = workspacePath(state.backend, ctx.cwd, params.path ?? "."); const limit = Math.max(1, Math.min(params.limit ?? 100, 1000)); const flags = ["--line-number", "--color=never", `--max-count=${limit}`, params.ignoreCase ? "--ignore-case" : "", params.literal ? "--fixed-strings" : "", params.context ? `--context=${Math.min(params.context, 20)}` : "", params.glob ? `--glob ${shell(params.glob)}` : ""].filter(Boolean).join(" "); const output = await state.backend.run(["sh", "-lc", `rg ${flags} -- ${shell(params.pattern)} ${shell(path)}`], { signal }); return result(output.stdout || output.stderr); } });
|
|
14
|
+
}
|
|
15
|
+
function registerFileTools(pi) {
|
|
16
|
+
const localRead = createReadToolDefinition(process.cwd());
|
|
17
|
+
const localWrite = createWriteToolDefinition(process.cwd());
|
|
18
|
+
const localEdit = createEditToolDefinition(process.cwd());
|
|
19
|
+
pi.registerTool({ ...localRead, async execute(id, params, signal, update, ctx) { const state = await runtime.boot(ctx.cwd); return createReadToolDefinition(ctx.cwd, { operations: fileOps(state.backend, ctx.cwd) }).execute(id, params, signal, update, ctx); } });
|
|
20
|
+
pi.registerTool({ ...localWrite, async execute(id, params, signal, update, ctx) { const state = await runtime.boot(ctx.cwd); return createWriteToolDefinition(ctx.cwd, { operations: fileOps(state.backend, ctx.cwd) }).execute(id, params, signal, update, ctx); } });
|
|
21
|
+
pi.registerTool({ ...localEdit, async execute(id, params, signal, update, ctx) { const state = await runtime.boot(ctx.cwd); return createEditToolDefinition(ctx.cwd, { operations: fileOps(state.backend, ctx.cwd) }).execute(id, params, signal, update, ctx); } });
|
|
22
|
+
}
|
|
23
|
+
function fileOps(backend, cwd) {
|
|
24
|
+
const readFile = async (path) => { const target = workspacePath(backend, cwd, path); const response = await backend.run(["sh", "-lc", `base64 < ${shell(target)}`]); if (response.exitCode)
|
|
25
|
+
throw new Error(response.stderr || "Cannot read file"); return Buffer.from(response.stdout.replace(/\s/g, ""), "base64"); };
|
|
26
|
+
const writeFile = async (path, content) => { const target = workspacePath(backend, cwd, path); const response = await backend.run(["sh", "-lc", `base64 -d > ${shell(target)}`], { input: Buffer.from(content).toString("base64") }); if (response.exitCode)
|
|
27
|
+
throw new Error(response.stderr || "Cannot write file"); };
|
|
28
|
+
return { readFile, writeFile, access: async (path) => { const response = await backend.run(["sh", "-lc", `test -r ${shell(workspacePath(backend, cwd, path))}`]); if (response.exitCode)
|
|
29
|
+
throw new Error("Path is inaccessible"); }, mkdir: async (path) => { const response = await backend.run(["mkdir", "-p", workspacePath(backend, cwd, path)]); if (response.exitCode)
|
|
30
|
+
throw new Error(response.stderr || "Cannot create directory"); } };
|
|
31
|
+
}
|
|
32
|
+
export function workspacePath(backend, cwd, candidate) { const full = resolve(cwd, candidate); const value = relative(resolve(cwd), full); if (value === ".." || value.startsWith("../"))
|
|
33
|
+
throw new Error(`Path escapes workspace: ${candidate}`); return backend.path(value); }
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function parseDeployMode(output, exitCode) {
|
|
2
|
+
if (exitCode !== 0)
|
|
3
|
+
return undefined;
|
|
4
|
+
const mode = output
|
|
5
|
+
.match(/\b(developer|production|default)\b/i)?.[1]
|
|
6
|
+
?.toLowerCase();
|
|
7
|
+
return mode === "developer" || mode === "production" || mode === "default"
|
|
8
|
+
? mode
|
|
9
|
+
: undefined;
|
|
10
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { parseDeployMode } from "#magento/deploy-mode";
|
|
2
|
+
export { DefaultMagentoInspector, type MagentoInspector, } from "#magento/inspector";
|
|
3
|
+
export type { MagentoDeployMode, MagentoSnapshot, MagentoTheme, RawTheme, } from "#magento/model";
|
|
4
|
+
export { snapshotPrompt } from "#magento/prompt";
|
|
5
|
+
export { parseRegisteredThemes, resolveThemes } from "#magento/themes";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CommandExecutor } from "#environments/executor";
|
|
2
|
+
import type { MagentoSnapshot } from "#magento/model";
|
|
3
|
+
export interface MagentoInspector {
|
|
4
|
+
inspect(executor: CommandExecutor): Promise<MagentoSnapshot>;
|
|
5
|
+
}
|
|
6
|
+
/** Ordered, fail-soft Magento inspection. New probes can be extracted without changing consumers. */
|
|
7
|
+
export declare class DefaultMagentoInspector implements MagentoInspector {
|
|
8
|
+
inspect(executor: CommandExecutor): Promise<MagentoSnapshot>;
|
|
9
|
+
}
|