@merchantduo/code 0.1.0-beta.0 → 0.2.0-beta.0
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,68 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { canonicalUrl, joinedUrl } from "#testing/url";
|
|
4
|
+
export async function discoverTesting(environment, backend, cwd, options, tools = {}) {
|
|
5
|
+
const diagnostics = [];
|
|
6
|
+
let frontendUrl = canonicalUrl(options.frontendUrl ?? "");
|
|
7
|
+
let adminUrl = canonicalUrl(options.adminUrl ?? "");
|
|
8
|
+
if (options.frontendUrl && !frontendUrl)
|
|
9
|
+
diagnostics.push("Configured frontendUrl is invalid.");
|
|
10
|
+
if (options.adminUrl && !adminUrl)
|
|
11
|
+
diagnostics.push("Configured adminUrl is invalid.");
|
|
12
|
+
if (!frontendUrl && environment.type === "warden") {
|
|
13
|
+
const routing = await wardenRouting(join(cwd, environment.projectRoot));
|
|
14
|
+
if (routing)
|
|
15
|
+
frontendUrl = `https://${routing}/`;
|
|
16
|
+
else
|
|
17
|
+
diagnostics.push("Warden routing was not found in .env.");
|
|
18
|
+
}
|
|
19
|
+
if (!frontendUrl && environment.type !== "warden") {
|
|
20
|
+
const result = await backend.run(["bin/magento", "config:show", "web/secure/base_url"]);
|
|
21
|
+
frontendUrl = canonicalUrl(result.stdout.trim());
|
|
22
|
+
if (!frontendUrl) {
|
|
23
|
+
const fallback = await backend.run(["bin/magento", "config:show", "web/unsecure/base_url"]);
|
|
24
|
+
frontendUrl = canonicalUrl(fallback.stdout.trim());
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (!adminUrl) {
|
|
28
|
+
const result = await backend.run(["bin/magento", "info:adminuri"]);
|
|
29
|
+
const path = result.stdout.trim().split(/\s+/).pop();
|
|
30
|
+
adminUrl = joinedUrl(frontendUrl, path?.startsWith("/") ? path : `/${path ?? ""}`);
|
|
31
|
+
}
|
|
32
|
+
const browser = tools.browser
|
|
33
|
+
? { available: true, executable: tools.browser }
|
|
34
|
+
: { available: false, reason: "Install Chromium or Chrome." };
|
|
35
|
+
const phpRepl = tools.magerun
|
|
36
|
+
? { available: true, executable: tools.magerun }
|
|
37
|
+
: { available: false, reason: "n98-magerun2 is unavailable." };
|
|
38
|
+
if (!frontendUrl)
|
|
39
|
+
diagnostics.push("Frontend URL unavailable.");
|
|
40
|
+
if (!adminUrl)
|
|
41
|
+
diagnostics.push("Admin URL unavailable.");
|
|
42
|
+
diagnostics.push("URLs are discovered but unverified; no startup HTTP request was made.");
|
|
43
|
+
const http = tools.http ? { available: true, executable: tools.http } : { available: false, reason: "Install curl." };
|
|
44
|
+
return { frontendUrl, adminUrl, http, browser, phpRepl, diagnostics };
|
|
45
|
+
}
|
|
46
|
+
export async function wardenRouting(projectRoot) {
|
|
47
|
+
try {
|
|
48
|
+
const content = await readFile(join(projectRoot, ".env"), "utf8");
|
|
49
|
+
const picked = Object.fromEntries(content.split(/\r?\n/).flatMap((line) => {
|
|
50
|
+
const match = line.match(/^\s*(TRAEFIK_(?:DOMAIN|SUBDOMAIN))\s*=\s*([^#\r\n]+)\s*$/);
|
|
51
|
+
return match ? [[match[1], match[2].trim().replace(/^['"]|['"]$/g, "")]] : [];
|
|
52
|
+
}));
|
|
53
|
+
return picked.TRAEFIK_DOMAIN && picked.TRAEFIK_SUBDOMAIN ? `${picked.TRAEFIK_SUBDOMAIN}.${picked.TRAEFIK_DOMAIN}` : undefined;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export async function findExecutable(candidates) {
|
|
60
|
+
for (const candidate of candidates) {
|
|
61
|
+
try {
|
|
62
|
+
await access(candidate);
|
|
63
|
+
return candidate;
|
|
64
|
+
}
|
|
65
|
+
catch { /* PATH candidates are resolved by command execution. */ }
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type HostProcessResult = {
|
|
2
|
+
output: string;
|
|
3
|
+
code: number;
|
|
4
|
+
};
|
|
5
|
+
export interface HostProcessRunner {
|
|
6
|
+
run(file: string, args: string[], options: {
|
|
7
|
+
cwd: string;
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
input?: string;
|
|
10
|
+
}): Promise<HostProcessResult>;
|
|
11
|
+
}
|
|
12
|
+
/** Process runner for host-owned test and navigation tools. Never wraps EnvironmentBackend. */
|
|
13
|
+
export declare class NodeHostProcessRunner implements HostProcessRunner {
|
|
14
|
+
run(file: string, args: string[], options: {
|
|
15
|
+
cwd: string;
|
|
16
|
+
signal?: AbortSignal;
|
|
17
|
+
input?: string;
|
|
18
|
+
}): Promise<HostProcessResult>;
|
|
19
|
+
}
|
|
20
|
+
export declare function discoverHostExecutable(names: string[], runner?: HostProcessRunner): Promise<string | undefined>;
|
|
21
|
+
export declare function browserArguments(input: {
|
|
22
|
+
profile: string;
|
|
23
|
+
artifact: string;
|
|
24
|
+
url: string;
|
|
25
|
+
action: "screenshot" | "dump-dom";
|
|
26
|
+
allowInsecureTls: boolean;
|
|
27
|
+
}): string[];
|
|
28
|
+
export declare function artifactExists(path: string): Promise<boolean>;
|
|
29
|
+
export declare function bound(value: string): string;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
const MAX_OUTPUT = 32 * 1024;
|
|
4
|
+
/** Process runner for host-owned test and navigation tools. Never wraps EnvironmentBackend. */
|
|
5
|
+
export class NodeHostProcessRunner {
|
|
6
|
+
run(file, args, options) {
|
|
7
|
+
return new Promise((done) => {
|
|
8
|
+
const child = spawn(file, args, { cwd: options.cwd, signal: options.signal });
|
|
9
|
+
let output = "";
|
|
10
|
+
let settled = false;
|
|
11
|
+
const finish = (result) => { if (!settled) {
|
|
12
|
+
settled = true;
|
|
13
|
+
done(result);
|
|
14
|
+
} };
|
|
15
|
+
const add = (data) => { output = bound(output + data.toString()); };
|
|
16
|
+
child.stdout.on("data", add);
|
|
17
|
+
child.stderr.on("data", add);
|
|
18
|
+
child.on("error", (error) => finish({ output: error.message, code: 127 }));
|
|
19
|
+
child.on("close", (code) => finish({ output, code: code ?? 1 }));
|
|
20
|
+
if (options.input !== undefined)
|
|
21
|
+
child.stdin.end(options.input);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export async function discoverHostExecutable(names, runner = new NodeHostProcessRunner()) {
|
|
26
|
+
for (const name of names) {
|
|
27
|
+
const result = await runner.run("sh", ["-lc", `command -v ${name} 2>/dev/null`], { cwd: process.cwd() });
|
|
28
|
+
if (result.code === 0 && result.output.trim())
|
|
29
|
+
return result.output.trim();
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
export function browserArguments(input) {
|
|
34
|
+
return [
|
|
35
|
+
"--headless",
|
|
36
|
+
"--no-first-run",
|
|
37
|
+
`--user-data-dir=${input.profile}`,
|
|
38
|
+
"--disable-extensions",
|
|
39
|
+
"--virtual-time-budget=10000",
|
|
40
|
+
...(input.allowInsecureTls ? ["--ignore-certificate-errors"] : []),
|
|
41
|
+
input.action === "screenshot" ? `--screenshot=${input.artifact}` : "--dump-dom",
|
|
42
|
+
input.url,
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
export async function artifactExists(path) {
|
|
46
|
+
return access(path).then(() => true).catch(() => false);
|
|
47
|
+
}
|
|
48
|
+
export function bound(value) {
|
|
49
|
+
return value.length > MAX_OUTPUT ? `${value.slice(0, MAX_OUTPUT)}\n[output truncated]` : value;
|
|
50
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { EnvironmentBackend } from "#environments/backend";
|
|
2
|
+
import type { Environment } from "#environments/model";
|
|
3
|
+
export declare function discoverMagerun(environment: Environment, backend: EnvironmentBackend, cwd: string): Promise<{
|
|
4
|
+
executable?: string;
|
|
5
|
+
diagnostic?: string;
|
|
6
|
+
}>;
|
|
7
|
+
/** Install the official PHAR outside the Magento project; verify the release API SHA-256 digest before publishing it. */
|
|
8
|
+
export declare function installMagerun(home: string, _cwd: string, request?: typeof fetch): Promise<string>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import { merchantHome } from "#config/paths";
|
|
5
|
+
const RELEASE_URL = "https://api.github.com/repos/netz98/n98-magerun2/releases/latest";
|
|
6
|
+
export async function discoverMagerun(environment, backend, cwd) {
|
|
7
|
+
if (environment.type === "warden")
|
|
8
|
+
return { executable: "/usr/local/bin/mr" };
|
|
9
|
+
const candidates = environment.type === "local"
|
|
10
|
+
? ["vendor/bin/n98-magerun2", "bin/n98-magerun2", "n98-magerun2", "magerun2"]
|
|
11
|
+
: ["n98-magerun2", "magerun2"];
|
|
12
|
+
for (const candidate of candidates) {
|
|
13
|
+
const result = await backend.run(["sh", "-lc", `command -v ${candidate} 2>/dev/null || test -x ${candidate} && printf '%s' ${candidate}`]);
|
|
14
|
+
const executable = result.stdout.trim();
|
|
15
|
+
if (executable)
|
|
16
|
+
return { executable };
|
|
17
|
+
}
|
|
18
|
+
if (environment.type === "ssh")
|
|
19
|
+
return { diagnostic: environment.writable ? "Install n98-magerun2 on the remote host to enable PHP REPL." : "PHP REPL is disabled for read-only SSH." };
|
|
20
|
+
try {
|
|
21
|
+
return { executable: await installMagerun(merchantHome(), cwd) };
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
return { diagnostic: `Could not install n98-magerun2: ${error.message}` };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Install the official PHAR outside the Magento project; verify the release API SHA-256 digest before publishing it. */
|
|
28
|
+
export async function installMagerun(home, _cwd, request = fetch) {
|
|
29
|
+
const directory = join(home, "tools", "n98-magerun2");
|
|
30
|
+
const destination = join(directory, "n98-magerun2.phar");
|
|
31
|
+
const digestPath = `${destination}.sha256`;
|
|
32
|
+
try {
|
|
33
|
+
await stat(destination);
|
|
34
|
+
const expected = (await readFile(digestPath, "utf8")).trim();
|
|
35
|
+
if (/^[a-f0-9]{64}$/.test(expected) && sha256(await readFile(destination)) === expected)
|
|
36
|
+
return destination;
|
|
37
|
+
}
|
|
38
|
+
catch { /* no verified cache */ }
|
|
39
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
40
|
+
const release = await request(RELEASE_URL, { headers: { Accept: "application/vnd.github+json" } });
|
|
41
|
+
if (!release.ok)
|
|
42
|
+
throw new Error("official release metadata download failed");
|
|
43
|
+
const metadata = await release.json();
|
|
44
|
+
const asset = metadata.assets?.find(({ name }) => name === "n98-magerun2.phar");
|
|
45
|
+
const expected = asset?.digest?.match(/^sha256:([a-fA-F0-9]{64})$/)?.[1]?.toLowerCase();
|
|
46
|
+
if (!asset?.browser_download_url || !expected)
|
|
47
|
+
throw new Error("official release has no PHAR SHA-256 digest");
|
|
48
|
+
const phar = await request(asset.browser_download_url);
|
|
49
|
+
if (!phar.ok)
|
|
50
|
+
throw new Error("official PHAR download failed");
|
|
51
|
+
const bytes = new Uint8Array(await phar.arrayBuffer());
|
|
52
|
+
const actual = sha256(bytes);
|
|
53
|
+
if (!expected || actual !== expected)
|
|
54
|
+
throw new Error("PHAR SHA-256 verification failed");
|
|
55
|
+
const temporary = `${destination}.${process.pid}.${randomUUID()}.tmp`;
|
|
56
|
+
await BunOrNode.write(temporary, bytes);
|
|
57
|
+
await chmod(temporary, 0o700);
|
|
58
|
+
try {
|
|
59
|
+
await rename(temporary, destination);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
try {
|
|
63
|
+
await stat(destination);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
throw new Error("atomic PHAR installation failed");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
await writeFile(digestPath, expected, { mode: 0o600 });
|
|
70
|
+
return destination;
|
|
71
|
+
}
|
|
72
|
+
function sha256(bytes) { return createHash("sha256").update(bytes).digest("hex"); }
|
|
73
|
+
const BunOrNode = { async write(path, bytes) { const { writeFile } = await import("node:fs/promises"); await writeFile(path, bytes, { mode: 0o700, flag: "wx" }); } };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type Capability = {
|
|
2
|
+
available: boolean;
|
|
3
|
+
executable?: string;
|
|
4
|
+
reason?: string;
|
|
5
|
+
};
|
|
6
|
+
export type TestingSnapshot = {
|
|
7
|
+
frontendUrl?: string;
|
|
8
|
+
adminUrl?: string;
|
|
9
|
+
http: Capability;
|
|
10
|
+
browser: Capability;
|
|
11
|
+
phpRepl: Capability;
|
|
12
|
+
diagnostics: string[];
|
|
13
|
+
};
|
|
14
|
+
export type TestingOptions = {
|
|
15
|
+
frontendUrl?: string;
|
|
16
|
+
adminUrl?: string;
|
|
17
|
+
allowInsecureTls: boolean;
|
|
18
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function canonicalUrl(value: string): string | undefined;
|
|
2
|
+
export declare function joinedUrl(base: string | undefined, path?: string): string | undefined;
|
|
3
|
+
export declare function relativePath(value: string): boolean;
|
|
4
|
+
export declare function sameOrigin(base: string | undefined, target: string): boolean;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function canonicalUrl(value) {
|
|
2
|
+
try {
|
|
3
|
+
const url = new URL(value);
|
|
4
|
+
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
5
|
+
return undefined;
|
|
6
|
+
return url.toString();
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function joinedUrl(base, path = "/") {
|
|
13
|
+
if (!base || !relativePath(path))
|
|
14
|
+
return undefined;
|
|
15
|
+
const url = new URL(base);
|
|
16
|
+
url.pathname = `${url.pathname.replace(/\/$/, "")}/${path.replace(/^\//, "")}`.replace(/\/\/{2,}/g, "/");
|
|
17
|
+
return url.toString();
|
|
18
|
+
}
|
|
19
|
+
export function relativePath(value) {
|
|
20
|
+
return !value.includes(":") && !value.startsWith("//") && value.startsWith("/");
|
|
21
|
+
}
|
|
22
|
+
export function sameOrigin(base, target) {
|
|
23
|
+
if (!base)
|
|
24
|
+
return false;
|
|
25
|
+
return new URL(base).origin === new URL(target).origin;
|
|
26
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const builtInWorkflows = [
|
|
2
|
+
operation("cache-clean", "Clean only relevant Magento cache types", false, (types) => ["bin/magento", "cache:clean", ...types]),
|
|
3
|
+
operation("cache-flush", "Flush Magento caches", true, () => [
|
|
4
|
+
"bin/magento",
|
|
5
|
+
"cache:flush",
|
|
6
|
+
]),
|
|
7
|
+
operation("compile", "Compile Magento dependency injection", true, () => [
|
|
8
|
+
"bin/magento",
|
|
9
|
+
"setup:di:compile",
|
|
10
|
+
]),
|
|
11
|
+
operation("setup-upgrade", "Run Magento setup upgrade", true, () => [
|
|
12
|
+
"bin/magento",
|
|
13
|
+
"setup:upgrade",
|
|
14
|
+
]),
|
|
15
|
+
operation("maintenance-enable", "Enable maintenance mode", true, () => [
|
|
16
|
+
"bin/magento",
|
|
17
|
+
"maintenance:enable",
|
|
18
|
+
]),
|
|
19
|
+
operation("maintenance-disable", "Disable maintenance mode", true, () => [
|
|
20
|
+
"bin/magento",
|
|
21
|
+
"maintenance:disable",
|
|
22
|
+
]),
|
|
23
|
+
operation("static-content-deploy", "Deploy static content", true, () => [
|
|
24
|
+
"bin/magento",
|
|
25
|
+
"setup:static-content:deploy",
|
|
26
|
+
]),
|
|
27
|
+
operation("reindex", "Reindex Magento", true, () => [
|
|
28
|
+
"bin/magento",
|
|
29
|
+
"indexer:reindex",
|
|
30
|
+
]),
|
|
31
|
+
operation("tests", "Run Magento tests", true, () => [
|
|
32
|
+
"bin/magento",
|
|
33
|
+
"dev:tests:run",
|
|
34
|
+
]),
|
|
35
|
+
];
|
|
36
|
+
function operation(id, description, risky, command) {
|
|
37
|
+
return { id, description, risky, allowRemote: false, command };
|
|
38
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { MagentoSnapshot } from "#magento/model";
|
|
2
|
+
export declare class ChangeTracker {
|
|
3
|
+
#private;
|
|
4
|
+
begin(toolCallId: string, file: string): void;
|
|
5
|
+
finish(toolCallId: string, succeeded: boolean): void;
|
|
6
|
+
files(): string[];
|
|
7
|
+
safeCacheTypes(snapshot: MagentoSnapshot): string[];
|
|
8
|
+
clear(): void;
|
|
9
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const cacheByEdit = {
|
|
2
|
+
layout: ["layout", "block_html", "full_page"],
|
|
3
|
+
template: ["block_html", "full_page"],
|
|
4
|
+
xml: ["config", "layout", "block_html"],
|
|
5
|
+
di: ["config"],
|
|
6
|
+
graphql: ["config", "full_page"],
|
|
7
|
+
};
|
|
8
|
+
export class ChangeTracker {
|
|
9
|
+
#intent = new Map();
|
|
10
|
+
#files = new Set();
|
|
11
|
+
#cacheTypes = new Set();
|
|
12
|
+
begin(toolCallId, file) {
|
|
13
|
+
this.#intent.set(toolCallId, file);
|
|
14
|
+
}
|
|
15
|
+
finish(toolCallId, succeeded) {
|
|
16
|
+
const file = this.#intent.get(toolCallId);
|
|
17
|
+
this.#intent.delete(toolCallId);
|
|
18
|
+
if (!file || !succeeded)
|
|
19
|
+
return;
|
|
20
|
+
this.#files.add(file);
|
|
21
|
+
const lower = file.toLowerCase();
|
|
22
|
+
for (const [needle, types] of Object.entries(cacheByEdit))
|
|
23
|
+
if (lower.includes(needle) || lower.endsWith(`.${needle}`))
|
|
24
|
+
types.forEach((type) => this.#cacheTypes.add(type));
|
|
25
|
+
if (lower.endsWith(".xml"))
|
|
26
|
+
cacheByEdit.xml.forEach((type) => this.#cacheTypes.add(type));
|
|
27
|
+
}
|
|
28
|
+
files() {
|
|
29
|
+
return [...this.#files];
|
|
30
|
+
}
|
|
31
|
+
safeCacheTypes(snapshot) {
|
|
32
|
+
return [...this.#cacheTypes].filter((type) => snapshot.cacheTypes.includes(type));
|
|
33
|
+
}
|
|
34
|
+
clear() {
|
|
35
|
+
this.#files.clear();
|
|
36
|
+
this.#cacheTypes.clear();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CommandExecutor } from "#environments/executor";
|
|
2
|
+
import type { WorkflowDefinition } from "#workflows/definition";
|
|
3
|
+
export declare function runWorkflow(executor: CommandExecutor, definition: WorkflowDefinition, cacheTypes?: string[]): Promise<{
|
|
4
|
+
command: import("#environments/executor").CommandSpec;
|
|
5
|
+
result: import("#environments/executor").CommandResult;
|
|
6
|
+
}>;
|
|
7
|
+
export declare function syntaxCheck(executor: CommandExecutor, files: string[]): Promise<string[]>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export async function runWorkflow(executor, definition, cacheTypes = []) {
|
|
2
|
+
const command = definition.command(cacheTypes);
|
|
3
|
+
return {
|
|
4
|
+
command: executor.command(command),
|
|
5
|
+
result: await executor.run(command),
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export async function syntaxCheck(executor, files) {
|
|
9
|
+
const outcomes = [];
|
|
10
|
+
for (const file of files) {
|
|
11
|
+
const command = file.endsWith(".php")
|
|
12
|
+
? ["php", "-l", executor.path(file)]
|
|
13
|
+
: file.endsWith(".xml")
|
|
14
|
+
? ["xmllint", "--noout", executor.path(file)]
|
|
15
|
+
: file.endsWith(".json")
|
|
16
|
+
? [
|
|
17
|
+
"node",
|
|
18
|
+
"-e",
|
|
19
|
+
`JSON.parse(require('fs').readFileSync(${JSON.stringify(executor.path(file))},'utf8'))`,
|
|
20
|
+
]
|
|
21
|
+
: undefined;
|
|
22
|
+
if (!command)
|
|
23
|
+
continue;
|
|
24
|
+
const result = await executor.run(command);
|
|
25
|
+
outcomes.push(`${file}: ${result.exitCode === 0 ? "ok" : result.stderr || result.stdout}`);
|
|
26
|
+
}
|
|
27
|
+
return outcomes;
|
|
28
|
+
}
|
package/package.json
CHANGED
|
@@ -1,21 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@merchantduo/code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-beta.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Magento-native Pi-based Coding Agent",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"imports": {
|
|
8
|
+
"#app/*": "./dist/app/*.js",
|
|
9
|
+
"#cli/*": "./dist/cli/*.js",
|
|
10
|
+
"#config/*": "./dist/config/*.js",
|
|
11
|
+
"#environments/*": "./dist/environments/*.js",
|
|
12
|
+
"#features/*": "./dist/features/*.js",
|
|
13
|
+
"#integrations/*": "./dist/integrations/*.js",
|
|
14
|
+
"#magento/*": "./dist/magento/*.js",
|
|
15
|
+
"#provision/*": "./dist/provision/*.js",
|
|
16
|
+
"#shared/*": "./dist/shared/*.js",
|
|
17
|
+
"#skills/*": "./dist/skills/*.js",
|
|
18
|
+
"#testing/*": "./dist/testing/*.js",
|
|
19
|
+
"#workflows/*": "./dist/workflows/*.js"
|
|
20
|
+
},
|
|
7
21
|
"bin": {
|
|
8
|
-
"merchantduo": "./dist/cli.js"
|
|
22
|
+
"merchantduo": "./dist/cli/main.js"
|
|
9
23
|
},
|
|
10
24
|
"files": [
|
|
11
25
|
"dist",
|
|
12
|
-
"extensions",
|
|
13
26
|
"skills",
|
|
14
27
|
"vendor/mage2gen",
|
|
15
28
|
"vendor/hyva-ai-tools",
|
|
16
29
|
"THIRD_PARTY_LICENSES.md",
|
|
17
|
-
"README.md"
|
|
18
|
-
"LICENSE"
|
|
30
|
+
"README.md"
|
|
19
31
|
],
|
|
20
32
|
"keywords": [
|
|
21
33
|
"magento",
|
|
@@ -27,12 +39,22 @@
|
|
|
27
39
|
},
|
|
28
40
|
"pi": {
|
|
29
41
|
"extensions": [
|
|
30
|
-
"./
|
|
42
|
+
"./dist/integrations/pi/context.js",
|
|
43
|
+
"./dist/integrations/pi/permissions.js",
|
|
44
|
+
"./dist/integrations/pi/workspace.js",
|
|
45
|
+
"./dist/integrations/pi/theme.js",
|
|
46
|
+
"./dist/integrations/pi/mage2gen.js",
|
|
47
|
+
"./dist/integrations/pi/workflows.js",
|
|
48
|
+
"./dist/integrations/pi/testing.js"
|
|
49
|
+
],
|
|
50
|
+
"skills": [
|
|
51
|
+
"./skills",
|
|
52
|
+
"./vendor/hyva-ai-tools/skills"
|
|
31
53
|
]
|
|
32
54
|
},
|
|
33
55
|
"dependencies": {
|
|
34
56
|
"@earendil-works/pi-coding-agent": "0.84.1",
|
|
35
|
-
"
|
|
57
|
+
"typebox": "1.3.7",
|
|
36
58
|
"yaml": "^2.8.1",
|
|
37
59
|
"zod": "^3.24.4"
|
|
38
60
|
},
|
|
@@ -42,8 +64,9 @@
|
|
|
42
64
|
"typescript": "^5.8.3"
|
|
43
65
|
},
|
|
44
66
|
"scripts": {
|
|
45
|
-
"
|
|
46
|
-
"
|
|
67
|
+
"clean": "node -e \"const fs=require('node:fs'); for(const path of ['dist','.test-dist']) fs.rmSync(path,{recursive:true,force:true})\"",
|
|
68
|
+
"build": "pnpm clean && tsc -p tsconfig.build.json && node -e \"require('node:fs').chmodSync('dist/cli/main.js',0o755)\"",
|
|
69
|
+
"test": "pnpm build && tsc -p tsconfig.test.json && node --test \".test-dist/**/*.test.js\"",
|
|
47
70
|
"check": "tsc --noEmit"
|
|
48
71
|
}
|
|
49
72
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: magento-testing
|
|
3
|
+
description: Use explicit MerchantDuo Magento testing and navigation tools only when they directly support the current request.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Magento testing
|
|
7
|
+
|
|
8
|
+
Use `magento_http_test` for a focused unauthenticated GET or HEAD request. Use `magento_browser_test` only for a requested screenshot or DOM capture. Use `magento_php_repl` only when the task needs a one-shot Magento PHP inspection. Use navigation commands only when the user asks to open the resolved page.
|
|
9
|
+
|
|
10
|
+
Do not run background checks, broad crawls, cache actions, deployments, PHP snippets, browser actions, or HTTP requests merely because a session started, an edit succeeded, or the agent settled. URLs discovered at startup are unverified until an explicit test is requested. Never assume or manage an admin login.
|
package/dist/backend.d.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type { Environment } from "./config.js";
|
|
2
|
-
export type CommandResult = {
|
|
3
|
-
stdout: string;
|
|
4
|
-
stderr: string;
|
|
5
|
-
exitCode: number;
|
|
6
|
-
truncated: boolean;
|
|
7
|
-
};
|
|
8
|
-
export declare function contain(root: string, candidate: string): string;
|
|
9
|
-
export declare class Backend {
|
|
10
|
-
readonly environment: Environment;
|
|
11
|
-
readonly localRoot: string;
|
|
12
|
-
constructor(environment: Environment, localRoot: string);
|
|
13
|
-
path(path: string): string;
|
|
14
|
-
command(argv: string[], target?: string): {
|
|
15
|
-
file: string;
|
|
16
|
-
args: string[];
|
|
17
|
-
cwd?: string;
|
|
18
|
-
};
|
|
19
|
-
run(argv: string[], options?: {
|
|
20
|
-
signal?: AbortSignal;
|
|
21
|
-
timeoutMs?: number;
|
|
22
|
-
target?: string;
|
|
23
|
-
}): Promise<CommandResult>;
|
|
24
|
-
}
|