@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.
Files changed (135) hide show
  1. package/README.md +132 -99
  2. package/dist/app/runtime.d.ts +30 -0
  3. package/dist/app/runtime.js +56 -0
  4. package/dist/app/status.d.ts +3 -0
  5. package/dist/app/status.js +9 -0
  6. package/dist/app/system-prompt.d.ts +2 -0
  7. package/dist/{skills.js → app/system-prompt.js} +3 -32
  8. package/dist/cli/arguments.d.ts +10 -0
  9. package/dist/cli/arguments.js +34 -0
  10. package/dist/cli/commands/agent.d.ts +1 -0
  11. package/dist/cli/commands/agent.js +33 -0
  12. package/dist/cli/commands/doctor.d.ts +3 -0
  13. package/dist/cli/commands/doctor.js +14 -0
  14. package/dist/cli/commands/init.d.ts +9 -0
  15. package/dist/cli/commands/init.js +68 -0
  16. package/dist/cli/commands/provision.d.ts +6 -0
  17. package/dist/cli/commands/provision.js +35 -0
  18. package/dist/cli/main.js +31 -0
  19. package/dist/config/index.d.ts +11 -0
  20. package/dist/config/index.js +15 -0
  21. package/dist/config/loader.d.ts +10 -0
  22. package/dist/config/loader.js +61 -0
  23. package/dist/config/paths.d.ts +1 -0
  24. package/dist/config/paths.js +7 -0
  25. package/dist/config/project-store.d.ts +2 -0
  26. package/dist/config/project-store.js +12 -0
  27. package/dist/config/schema.d.ts +210 -0
  28. package/dist/config/schema.js +36 -0
  29. package/dist/config/trust-store.d.ts +3 -0
  30. package/dist/config/trust-store.js +29 -0
  31. package/dist/environments/adapter.d.ts +5 -0
  32. package/dist/environments/adapters/local.d.ts +12 -0
  33. package/dist/environments/adapters/local.js +14 -0
  34. package/dist/environments/adapters/ssh.d.ts +11 -0
  35. package/dist/environments/adapters/ssh.js +24 -0
  36. package/dist/environments/adapters/warden.d.ts +13 -0
  37. package/dist/environments/adapters/warden.js +24 -0
  38. package/dist/environments/backend.d.ts +13 -0
  39. package/dist/environments/backend.js +73 -0
  40. package/dist/environments/executor.d.ts +23 -0
  41. package/dist/environments/factory.d.ts +3 -0
  42. package/dist/environments/factory.js +13 -0
  43. package/dist/environments/model.d.ts +28 -0
  44. package/dist/environments/model.js +3 -0
  45. package/dist/environments/path-policy.d.ts +1 -0
  46. package/dist/environments/path-policy.js +8 -0
  47. package/dist/{mage2gen.d.ts → features/mage2gen/service.d.ts} +0 -1
  48. package/dist/{mage2gen.js → features/mage2gen/service.js} +8 -3
  49. package/dist/integrations/pi/context.d.ts +2 -0
  50. package/dist/integrations/pi/context.js +46 -0
  51. package/dist/integrations/pi/mage2gen.d.ts +2 -0
  52. package/dist/integrations/pi/mage2gen.js +19 -0
  53. package/dist/integrations/pi/permissions.d.ts +6 -0
  54. package/dist/integrations/pi/permissions.js +36 -0
  55. package/dist/integrations/pi/session.d.ts +12 -0
  56. package/dist/integrations/pi/session.js +9 -0
  57. package/dist/integrations/pi/testing.d.ts +2 -0
  58. package/dist/integrations/pi/testing.js +88 -0
  59. package/dist/integrations/pi/theme.d.ts +2 -0
  60. package/dist/integrations/pi/theme.js +6 -0
  61. package/dist/integrations/pi/workflows.d.ts +2 -0
  62. package/dist/integrations/pi/workflows.js +26 -0
  63. package/dist/integrations/pi/workspace.d.ts +5 -0
  64. package/dist/integrations/pi/workspace.js +33 -0
  65. package/dist/magento/deploy-mode.d.ts +2 -0
  66. package/dist/magento/deploy-mode.js +10 -0
  67. package/dist/magento/index.d.ts +5 -0
  68. package/dist/magento/index.js +4 -0
  69. package/dist/magento/inspector.d.ts +9 -0
  70. package/dist/magento/inspector.js +56 -0
  71. package/dist/magento/model.d.ts +25 -0
  72. package/dist/magento/prompt.d.ts +2 -0
  73. package/dist/magento/prompt.js +3 -0
  74. package/dist/magento/themes.d.ts +3 -0
  75. package/dist/magento/themes.js +46 -0
  76. package/dist/provision/advisor.d.ts +12 -0
  77. package/dist/provision/advisor.js +28 -0
  78. package/dist/provision/discovery.d.ts +12 -0
  79. package/dist/provision/discovery.js +61 -0
  80. package/dist/provision/interview.d.ts +9 -0
  81. package/dist/provision/interview.js +29 -0
  82. package/dist/provision/model.d.ts +82 -0
  83. package/dist/provision/model.js +5 -0
  84. package/dist/provision/planner.d.ts +4 -0
  85. package/dist/provision/planner.js +51 -0
  86. package/dist/provision/runner.d.ts +35 -0
  87. package/dist/provision/runner.js +98 -0
  88. package/dist/shared/package-paths.d.ts +3 -0
  89. package/dist/shared/package-paths.js +10 -0
  90. package/dist/testing/discovery.d.ts +10 -0
  91. package/dist/testing/discovery.js +68 -0
  92. package/dist/testing/host.d.ts +29 -0
  93. package/dist/testing/host.js +50 -0
  94. package/dist/testing/index.d.ts +5 -0
  95. package/dist/testing/index.js +5 -0
  96. package/dist/testing/magerun.d.ts +8 -0
  97. package/dist/testing/magerun.js +73 -0
  98. package/dist/testing/model.d.ts +18 -0
  99. package/dist/testing/url.d.ts +4 -0
  100. package/dist/testing/url.js +26 -0
  101. package/dist/workflows/builtins.d.ts +2 -0
  102. package/dist/workflows/builtins.js +38 -0
  103. package/dist/workflows/change-tracker.d.ts +9 -0
  104. package/dist/workflows/change-tracker.js +38 -0
  105. package/dist/workflows/definition.d.ts +7 -0
  106. package/dist/workflows/index.d.ts +4 -0
  107. package/dist/workflows/index.js +3 -0
  108. package/dist/workflows/runner.d.ts +7 -0
  109. package/dist/workflows/runner.js +28 -0
  110. package/package.json +32 -9
  111. package/skills/magento-testing/SKILL.md +10 -0
  112. package/dist/backend.d.ts +0 -24
  113. package/dist/backend.js +0 -103
  114. package/dist/cli.js +0 -89
  115. package/dist/config.d.ts +0 -127
  116. package/dist/config.js +0 -170
  117. package/dist/config.test.js +0 -51
  118. package/dist/constants.d.ts +0 -20
  119. package/dist/constants.js +0 -15
  120. package/dist/mage2gen.test.js +0 -6
  121. package/dist/magento.d.ts +0 -40
  122. package/dist/magento.js +0 -119
  123. package/dist/magento.test.js +0 -31
  124. package/dist/skills.d.ts +0 -11
  125. package/dist/skills.test.js +0 -34
  126. package/dist/workflow.d.ts +0 -20
  127. package/dist/workflow.js +0 -67
  128. package/dist/workflow.test.js +0 -17
  129. package/extensions/merchantduo.js +0 -311
  130. /package/dist/{cli.d.ts → cli/main.d.ts} +0 -0
  131. /package/dist/{config.test.d.ts → environments/adapter.js} +0 -0
  132. /package/dist/{mage2gen.test.d.ts → environments/executor.js} +0 -0
  133. /package/dist/{magento.test.d.ts → magento/model.js} +0 -0
  134. /package/dist/{skills.test.d.ts → testing/model.js} +0 -0
  135. /package/dist/{workflow.test.d.ts → workflows/definition.js} +0 -0
package/dist/magento.js DELETED
@@ -1,119 +0,0 @@
1
- /**
2
- * Build a registered-theme graph. Metadata is accepted only from registration.php
3
- * locations and theme.xml values; malformed/missing parents remain non-fatal.
4
- */
5
- export function resolveThemes(themes) {
6
- const byCode = new Map(themes.map((theme) => [theme.code, theme]));
7
- const lineage = (theme) => {
8
- const chain = [];
9
- const seen = new Set([theme.code]);
10
- let parent = theme.parent;
11
- while (parent && !seen.has(parent)) {
12
- chain.push(parent);
13
- seen.add(parent);
14
- parent = byCode.get(parent)?.parent;
15
- }
16
- return chain;
17
- };
18
- return themes.map((theme) => {
19
- const parentChain = lineage(theme);
20
- const ancestry = [theme.code, ...parentChain];
21
- return {
22
- code: theme.code,
23
- title: theme.title,
24
- path: theme.path,
25
- parentChain,
26
- kind: ancestry.includes("Magento/luma")
27
- ? "luma"
28
- : ancestry.includes("Hyva/default") || ancestry.includes("Hyva/reset")
29
- ? "hyva"
30
- : "other",
31
- };
32
- });
33
- }
34
- export function parseRegisteredThemes(output) {
35
- return output
36
- .split("\u001e")
37
- .map((entry) => entry.trim())
38
- .filter(Boolean)
39
- .flatMap((entry) => {
40
- try {
41
- const value = JSON.parse(entry);
42
- return typeof value.code === "string" && typeof value.path === "string"
43
- ? [value]
44
- : [];
45
- }
46
- catch {
47
- return [];
48
- }
49
- });
50
- }
51
- /**
52
- * Accept only successful Magento CLI output that names one known deployment mode.
53
- * A failed or unrecognised probe must not influence operational recommendations.
54
- */
55
- export function parseDeployMode(output, exitCode) {
56
- if (exitCode !== 0)
57
- return undefined;
58
- const mode = output.match(/\b(developer|production|default)\b/i)?.[1]?.toLowerCase();
59
- return mode === "developer" || mode === "production" || mode === "default"
60
- ? mode
61
- : undefined;
62
- }
63
- export async function snapshot(backend) {
64
- const warnings = [], evidence = [];
65
- const composer = await backend.run([
66
- "sh",
67
- "-lc",
68
- "test -f composer.lock && node -e \"const x=require('./composer.lock'); console.log(JSON.stringify(x.packages||[]))\" || true",
69
- ]);
70
- const packages = composer.stdout;
71
- const match = packages.match(/magento\/(product-community-edition|product-enterprise-edition).*?\"version\":\"([^\"]+)/s);
72
- const edition = match?.[1] === "product-enterprise-edition"
73
- ? "Adobe Commerce"
74
- : match
75
- ? "Open Source"
76
- : undefined;
77
- const composerVersion = match?.[2];
78
- if (composerVersion)
79
- evidence.push("composer.lock");
80
- const cli = await backend.run(["bin/magento", "--version"]);
81
- const cliVersion = cli.stdout.match(/(?:Magento|Adobe Commerce)[^0-9]*(\d+\.\d+\.\d+(?:-[^\s]+)?)/)?.[1];
82
- if (cliVersion)
83
- evidence.push("bin/magento --version");
84
- if (composerVersion && cliVersion && !cliVersion.startsWith(composerVersion))
85
- warnings.push(`Composer (${composerVersion}) and CLI (${cliVersion}) disagree`);
86
- const [php, mode, cache, modules] = await Promise.all([
87
- backend.run(["php", "-r", "echo PHP_VERSION;"]),
88
- backend.run(["bin/magento", "deploy:mode:show"]),
89
- backend.run(["bin/magento", "cache:status", "--no-ansi"]),
90
- backend.run(["bin/magento", "module:status", "--enabled", "--no-ansi"]),
91
- ]);
92
- // Magento themes live at package roots or app/design area/vendor/theme roots.
93
- // Do not recurse through vendor test fixtures: they also carry registration.php.
94
- const registeredThemes = await backend.run([
95
- "sh",
96
- "-lc",
97
- `{
98
- find vendor -mindepth 3 -maxdepth 3 -type f -name registration.php 2>/dev/null
99
- find app/design -mindepth 4 -maxdepth 4 -type f -name registration.php 2>/dev/null
100
- } | while read f; do d=$(dirname "$f"); x="$d/theme.xml"; [ -f "$x" ] || continue; code=$(sed -n "s/.*ComponentRegistrar::THEME[[:space:]]*,[[:space:]]*['\\\"]\\([^'\\\"]*\\).*/\\1/p" "$f" | head -1); [ -n "$code" ] || continue; title=$(sed -n 's:.*<title>\\(.*\\)</title>.*:\\1:p' "$x" | head -1); parent=$(sed -n 's:.*<parent>\\(.*\\)</parent>.*:\\1:p' "$x" | head -1); node -e 'console.log(JSON.stringify({code:process.argv[1],path:process.argv[2],title:process.argv[3]||undefined,parent:process.argv[4]||undefined})+"\\u001e")' "$code" "$d" "$title" "$parent"; done`,
101
- ]);
102
- return {
103
- version: composerVersion ?? cliVersion,
104
- edition,
105
- phpVersion: php.stdout.trim() || undefined,
106
- mode: parseDeployMode(mode.stdout, mode.exitCode),
107
- cacheTypes: cache.stdout
108
- .split("\n")
109
- .map((l) => l.match(/^([^:]+):/)?.[1])
110
- .filter((x) => Boolean(x)),
111
- modulesEnabled: modules.stdout.split("\n").filter(Boolean).length || undefined,
112
- warnings,
113
- evidence,
114
- themes: resolveThemes(parseRegisteredThemes(registeredThemes.stdout)),
115
- };
116
- }
117
- export function snapshotPrompt(envName, writable, status, s) {
118
- return `[MerchantDuo: environment=${envName}; write policy=${writable ? "writable" : "read-only"}; Magento=${s.edition ?? "unknown"} ${s.version ?? "unknown"}; PHP=${s.phpVersion ?? "unknown"}; mode=${s.mode ?? "unknown"}; MCP=${status}; pending actions are controlled by magento_workflow. Require exact detected edition/version in Magento knowledge MCP arguments. Do not inspect app/etc/env.php or credentials.${s.warnings.length ? ` Warnings: ${s.warnings.join("; ")}` : ""}]`;
119
- }
@@ -1,31 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { parseDeployMode, parseRegisteredThemes, resolveThemes } from "./magento.js";
4
- test("deployment mode parsing accepts each Magento mode and rejects unknown or failed probes", () => {
5
- assert.equal(parseDeployMode("Current application mode: default", 0), "default");
6
- assert.equal(parseDeployMode("Current application mode: developer", 0), "developer");
7
- assert.equal(parseDeployMode("Current application mode: production", 0), "production");
8
- assert.equal(parseDeployMode("Current application mode: staging", 0), undefined);
9
- assert.equal(parseDeployMode("Current application mode: production", 1), undefined);
10
- });
11
- test("registered themes use metadata inheritance and classify descendants", () => {
12
- const themes = resolveThemes([
13
- { code: "Magento/luma", path: "vendor/magento/theme-frontend-luma", title: "Luma" },
14
- { code: "Acme/store", path: "app/design/frontend/Acme/store", parent: "Magento/luma" },
15
- { code: "Hyva/default", path: "vendor/hyva/default" },
16
- { code: "Acme/hyva", path: "app/design/frontend/Acme/hyva", parent: "Hyva/default" },
17
- ]);
18
- assert.deepEqual(themes.find((theme) => theme.code === "Acme/store")?.parentChain, ["Magento/luma"]);
19
- assert.equal(themes.find((theme) => theme.code === "Acme/store")?.kind, "luma");
20
- assert.equal(themes.find((theme) => theme.code === "Acme/hyva")?.kind, "hyva");
21
- });
22
- test("theme metadata ignores malformed records and terminates cycles", () => {
23
- assert.deepEqual(parseRegisteredThemes("bad\u001e{}"), []);
24
- const themes = resolveThemes([
25
- { code: "A/a", path: "a", parent: "B/b" },
26
- { code: "B/b", path: "b", parent: "A/a" },
27
- { code: "C/c", path: "c", parent: "Missing/x" },
28
- ]);
29
- assert.deepEqual(themes[0]?.parentChain, ["B/b"]);
30
- assert.deepEqual(themes[2]?.parentChain, ["Missing/x"]);
31
- });
package/dist/skills.d.ts DELETED
@@ -1,11 +0,0 @@
1
- import type { MagentoTheme } from "./magento.js";
2
- export type SkillName = "magento-24x" | "luma" | "hyva" | "mage2gen";
3
- export declare function skillRoot(): string;
4
- export declare function mage2genRoot(): string;
5
- export declare function selectMagentoSkill(version?: string): SkillName | undefined;
6
- export declare function selectedThemeSkill(activeTheme: string, themes: MagentoTheme[]): SkillName | undefined;
7
- export declare function automaticSkills(version: string | undefined, activeTheme: string, themes: MagentoTheme[]): SkillName[];
8
- export declare function readSkill(name: SkillName): Promise<string>;
9
- export declare function skillPaths(): Record<SkillName, string>;
10
- /** MerchantDuo's stable session rules; environment-specific facts remain in snapshotPrompt. */
11
- export declare function merchantDuoSystemPrompt(mage2genPath?: string): string;
@@ -1,34 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { automaticSkills, merchantDuoSystemPrompt, readSkill, selectMagentoSkill } from "./skills.js";
4
- test("Magento skills use the longest supported version prefix", () => {
5
- assert.equal(selectMagentoSkill("2.4.8-p1"), "magento-24x");
6
- assert.equal(selectMagentoSkill("2.3.7"), undefined);
7
- });
8
- test("baseline session prompt establishes Mage2Gen, theme-scope, and mode-aware workflow guidance", () => {
9
- const prompt = merchantDuoSystemPrompt();
10
- assert.match(prompt, /senior Magento 2\.4 architect/);
11
- assert.match(prompt, /vendor\/mage2gen/);
12
- assert.match(prompt, /mage2gen_generate_module/);
13
- assert.match(prompt, /active theme scope/);
14
- assert.match(prompt, /sole authoritative confirmation/);
15
- assert.match(prompt, /inspect the changed module and files/i);
16
- assert.match(prompt, /developer recommends `setup:upgrade` then targeted cache clean/i);
17
- assert.match(prompt, /default recommends `setup:upgrade` then targeted cache clean/i);
18
- assert.match(prompt, /production recommends maintenance enable, `setup:upgrade`, DI compile, static-content deploy, targeted cache clean, then maintenance disable/i);
19
- assert.match(prompt, /only this workflow tool for operational Magento commands/i);
20
- });
21
- test("Magento 2.4 skill records the mode matrix and workflow-only execution policy", async () => {
22
- const skill = await readSkill("magento-24x");
23
- assert.match(skill, /\| Developer \| Run `setup:upgrade`, then clean only relevant cache types\./);
24
- assert.match(skill, /\| Default \| Run `setup:upgrade`, then clean only relevant cache types\./);
25
- assert.match(skill, /\| Production \| Run maintenance enable → `setup:upgrade` → DI compile → static-content deploy → targeted cache clean → maintenance disable\./);
26
- assert.match(skill, /Use `magento_workflow` rather than raw operational commands\./);
27
- assert.match(skill, /call it with `execute: true` immediately/);
28
- });
29
- test("automatic theme skills require a concrete classified selection", () => {
30
- const themes = [{ code: "Acme/hyva", path: "app/design", parentChain: ["Hyva/default"], kind: "hyva" }];
31
- assert.deepEqual(automaticSkills("2.4.7", "all", themes), ["magento-24x"]);
32
- assert.deepEqual(automaticSkills("2.4.7", "any", themes), ["magento-24x"]);
33
- assert.deepEqual(automaticSkills("2.4.7", "Acme/hyva", themes), ["magento-24x", "hyva"]);
34
- });
@@ -1,20 +0,0 @@
1
- import type { Backend } from "./backend.js";
2
- import type { MagentoSnapshot } from "./magento.js";
3
- export type Pending = {
4
- files: Set<string>;
5
- cacheTypes: Set<string>;
6
- };
7
- export declare function classifyEdit(pending: Pending, file: string): void;
8
- export declare function safeCacheTypes(pending: Pending, snapshot: MagentoSnapshot): string[];
9
- export declare function syntaxCheck(backend: Backend, files: string[]): Promise<string[]>;
10
- export declare function runWorkflow(backend: Backend, action: WorkflowAction, cacheTypes?: string[]): Promise<{
11
- command: {
12
- file: string;
13
- args: string[];
14
- cwd?: string;
15
- };
16
- result: import("./backend.js").CommandResult;
17
- }>;
18
- export type WorkflowAction = "cache-clean" | "cache-flush" | "compile" | "setup-upgrade" | "maintenance-enable" | "maintenance-disable" | "static-content-deploy" | "reindex" | "tests";
19
- /** Build the exact Magento CLI invocation used by the confirmation-gated workflow. */
20
- export declare function workflowCommand(action: WorkflowAction, cacheTypes?: string[]): string[];
package/dist/workflow.js DELETED
@@ -1,67 +0,0 @@
1
- const CACHE = {
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 function classifyEdit(pending, file) {
9
- pending.files.add(file);
10
- const lower = file.toLowerCase();
11
- for (const [needle, types] of Object.entries(CACHE))
12
- if (lower.includes(needle) || lower.endsWith(`.${needle}`))
13
- types.forEach((t) => pending.cacheTypes.add(t));
14
- if (lower.endsWith(".xml"))
15
- CACHE.xml.forEach((t) => pending.cacheTypes.add(t));
16
- }
17
- export function safeCacheTypes(pending, snapshot) {
18
- return [...pending.cacheTypes].filter((x) => snapshot.cacheTypes.includes(x));
19
- }
20
- export async function syntaxCheck(backend, files) {
21
- const outcomes = [];
22
- for (const file of files) {
23
- const cmd = file.endsWith(".php")
24
- ? ["php", "-l", backend.path(file)]
25
- : file.endsWith(".xml")
26
- ? ["xmllint", "--noout", backend.path(file)]
27
- : file.endsWith(".json")
28
- ? [
29
- "node",
30
- "-e",
31
- `JSON.parse(require('fs').readFileSync(${JSON.stringify(backend.path(file))},'utf8'))`,
32
- ]
33
- : undefined;
34
- if (cmd) {
35
- const r = await backend.run(cmd);
36
- outcomes.push(`${file}: ${r.exitCode === 0 ? "ok" : r.stderr || r.stdout}`);
37
- }
38
- }
39
- return outcomes;
40
- }
41
- export async function runWorkflow(backend, action, cacheTypes = []) {
42
- const cmd = workflowCommand(action, cacheTypes);
43
- return { command: backend.command(cmd), result: await backend.run(cmd) };
44
- }
45
- /** Build the exact Magento CLI invocation used by the confirmation-gated workflow. */
46
- export function workflowCommand(action, cacheTypes = []) {
47
- switch (action) {
48
- case "cache-clean":
49
- return ["bin/magento", "cache:clean", ...cacheTypes];
50
- case "cache-flush":
51
- return ["bin/magento", "cache:flush"];
52
- case "compile":
53
- return ["bin/magento", "setup:di:compile"];
54
- case "setup-upgrade":
55
- return ["bin/magento", "setup:upgrade"];
56
- case "maintenance-enable":
57
- return ["bin/magento", "maintenance:enable"];
58
- case "maintenance-disable":
59
- return ["bin/magento", "maintenance:disable"];
60
- case "static-content-deploy":
61
- return ["bin/magento", "setup:static-content:deploy"];
62
- case "reindex":
63
- return ["bin/magento", "indexer:reindex"];
64
- case "tests":
65
- return ["bin/magento", "dev:tests:run"];
66
- }
67
- }
@@ -1,17 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { workflowCommand } from "./workflow.js";
4
- test("workflow commands construct maintenance and default static deployment actions", () => {
5
- assert.deepEqual(workflowCommand("maintenance-enable"), [
6
- "bin/magento",
7
- "maintenance:enable",
8
- ]);
9
- assert.deepEqual(workflowCommand("maintenance-disable"), [
10
- "bin/magento",
11
- "maintenance:disable",
12
- ]);
13
- assert.deepEqual(workflowCommand("static-content-deploy"), [
14
- "bin/magento",
15
- "setup:static-content:deploy",
16
- ]);
17
- });
@@ -1,311 +0,0 @@
1
- // MerchantDuo Pi extension. It deliberately uses Pi lifecycle hooks and native tools;
2
- // no Pi fork or model-facing skills are required.
3
- import { Type } from "typebox";
4
- import { resolve } from "node:path";
5
- import { Backend } from "../dist/backend.js";
6
- import { loadConfig, selectEnvironment, setActiveTheme } from "../dist/config.js";
7
- import { KNOWLEDGE_MCP_URL, PROVIDER } from "../dist/constants.js";
8
- import { snapshot, snapshotPrompt } from "../dist/magento.js";
9
- import { automaticSkills, mage2genRoot, merchantDuoSystemPrompt, readSkill, skillPaths } from "../dist/skills.js";
10
- import { classifyEdit, runWorkflow, safeCacheTypes } from "../dist/workflow.js";
11
- import { generateMage2GenModule, mage2genDestination, mage2genModuleExists } from "../dist/mage2gen.js";
12
-
13
- export default function merchantDuo(pi) {
14
- let state;
15
- let injectionGeneration = 0;
16
- const injected = new Set();
17
- const pending = { files: new Set(), cacheTypes: new Set() };
18
- const boot = async (ctx) => {
19
- if (state) return state;
20
- const { config } = await loadConfig(ctx.cwd, {
21
- config: process.env.MERCHANTDUO_CONFIG_PATH || undefined,
22
- });
23
- const selected = selectEnvironment(
24
- config,
25
- ctx.cwd,
26
- process.env.MERCHANTDUO_SELECTED_ENV,
27
- );
28
- const backend = new Backend(selected.environment, ctx.cwd);
29
- const magento = await snapshot(backend);
30
- state = {
31
- config,
32
- selected,
33
- backend,
34
- magento,
35
- mcp: `${KNOWLEDGE_MCP_URL} (read-only)`,
36
- };
37
- refreshStatus(ctx, state);
38
- return state;
39
- };
40
- const writePolicy = (s) =>
41
- s.selected.environment.type === "ssh"
42
- ? s.selected.environment.writable
43
- ? "write + approve"
44
- : "read-only + approve"
45
- : "write + approve";
46
- const refreshStatus = (ctx, s) => {
47
- ctx.ui.setStatus(
48
- "merchantduo",
49
- `MerchantDuo ${process.env.MERCHANTDUO_PACKAGE_VERSION ?? "?"} | Magento ${s.magento.version ?? "?"} | ${s.selected.name}/${s.selected.environment.type} | ${writePolicy(s)} | Theme: ${s.config.activeTheme} (/theme to switch)`,
50
- );
51
- };
52
- const resetInjection = () => {
53
- injectionGeneration += 1;
54
- injected.clear();
55
- };
56
- // Expose every shipped skill to Pi; automatic injection remains intentionally narrower.
57
- pi.on("resources_discover", () => ({
58
- skillPaths: [
59
- new URL("../skills", import.meta.url).pathname,
60
- new URL("../vendor/hyva-ai-tools/skills", import.meta.url).pathname,
61
- ],
62
- }));
63
- // Register even without a key. Pi only selects it when an explicit API key makes it usable.
64
- pi.registerProvider(PROVIDER.id, {
65
- baseUrl: PROVIDER.baseUrl,
66
- apiKey: "$MERCHANTDUO_API_KEY",
67
- api: "openai-completions",
68
- models: [
69
- {
70
- id: PROVIDER.model,
71
- name: "MerchantDuo Mock (replace before production)",
72
- input: [...PROVIDER.input],
73
- reasoning: PROVIDER.reasoning,
74
- contextWindow: PROVIDER.contextWindow,
75
- maxTokens: PROVIDER.maxTokens,
76
- cost: PROVIDER.cost,
77
- },
78
- ],
79
- });
80
- pi.on("before_agent_start", async (event, ctx) => {
81
- const s = await boot(ctx);
82
- const desired = automaticSkills(s.magento.version, s.config.activeTheme, s.magento.themes);
83
- const pending = desired.filter((name) => !injected.has(`${injectionGeneration}:${name}:${s.config.activeTheme}`));
84
- const instructions = await Promise.all(pending.map(readSkill));
85
- pending.forEach((name) => injected.add(`${injectionGeneration}:${name}:${s.config.activeTheme}`));
86
- return {
87
- systemPrompt: `${event.systemPrompt}\n${merchantDuoSystemPrompt(mage2genRoot())}\n${snapshotPrompt(s.selected.name, s.selected.environment.type !== "ssh" || s.selected.environment.writable, s.mcp, s.magento)}\nMerchantDuo skills are available for explicit use at: ${Object.entries(skillPaths()).map(([name, path]) => `${name}=${path}`).join(", ")}.${instructions.length ? `\n\nMerchantDuo active instructions:\n${instructions.join("\n\n")}` : ""}`,
88
- };
89
- });
90
- pi.on("session_start", async (_event, ctx) => {
91
- await boot(ctx);
92
- resetInjection();
93
- });
94
- pi.on("session_compact", () => resetInjection());
95
- pi.registerCommand("theme", {
96
- description: "Select the project theme scope: all, any, or a detected Magento theme.",
97
- getArgumentCompletions: async () => {
98
- if (!state) return ["all", "any"];
99
- return ["all", "any", ...state.magento.themes.map((theme) => theme.code)].map((value) => ({ value, label: value }));
100
- },
101
- handler: async (args, ctx) => {
102
- const s = await boot(ctx);
103
- const choices = ["all", "any", ...s.magento.themes.map((theme) => theme.code)];
104
- const requested = args.trim() || (await ctx.ui.select("Select Magento theme scope", choices));
105
- if (!requested) return;
106
- if (!choices.includes(requested)) {
107
- ctx.ui.notify(`Unknown theme scope: ${requested}. Choose all, any, or a detected theme.`, "error");
108
- return;
109
- }
110
- if (!process.env.MERCHANTDUO_CONFIG_PATH) {
111
- ctx.ui.notify("No project .merchantduo.yaml is loaded; run merchantduo init before changing the theme.", "error");
112
- return;
113
- }
114
- s.config = await setActiveTheme(process.env.MERCHANTDUO_CONFIG_PATH, requested);
115
- resetInjection();
116
- refreshStatus(ctx, s);
117
- ctx.ui.notify(`Theme scope set to ${requested}; matching skills will apply to the next prompt.`, "info");
118
- },
119
- });
120
- pi.on("tool_call", async (event, ctx) => {
121
- const s = await boot(ctx);
122
- const remote = s.selected.environment.type === "ssh";
123
- if (
124
- remote &&
125
- ["write", "edit"].includes(event.toolName || "") &&
126
- !s.selected.environment.writable
127
- )
128
- return {
129
- block: true,
130
- reason: "MerchantDuo: this SSH environment is read-only.",
131
- };
132
- if (
133
- remote &&
134
- ["bash", "read", "write", "edit", "grep", "find", "ls"].includes(
135
- event.toolName || "",
136
- )
137
- ) {
138
- const approved = ctx.hasUI
139
- ? await ctx.ui.confirm(
140
- "Remote command",
141
- `Run ${event.toolName} on ${s.selected.environment.host}?`,
142
- )
143
- : false;
144
- if (!approved)
145
- return {
146
- block: true,
147
- reason: "MerchantDuo: remote command was not confirmed.",
148
- };
149
- }
150
- if (["write", "edit"].includes(event.toolName || "")) {
151
- const file = event.input.path || event.input.filePath;
152
- if (typeof file === "string") classifyEdit(pending, file);
153
- }
154
- });
155
- pi.on("tool_result", async (_event, ctx) => {
156
- const s = await boot(ctx);
157
- if (pending.files.size && s.config.automation.syntaxChecks === "auto")
158
- ctx.ui.setStatus(
159
- "merchantduo-actions",
160
- `Pending syntax checks and cache recommendations for ${pending.files.size} edit(s)`,
161
- );
162
- });
163
- pi.registerTool({
164
- name: "mage2gen_generate_module",
165
- label: "Generate Magento module",
166
- description:
167
- "Generate initial Magento module scaffolding through the vendored Mage2Gen library. Use this for every new-module request before custom implementation. A direct user request with execute=true receives one authoritative write confirmation.",
168
- parameters: Type.Object({
169
- vendor: Type.String({ description: "Magento module vendor, for example Perspective" }),
170
- module: Type.String({ description: "Magento module name, for example FreshStocks" }),
171
- description: Type.Optional(Type.String()),
172
- frontendRoute: Type.Optional(Type.String({ description: "Optional frontend route id/frontName" })),
173
- section: Type.Optional(Type.String({ description: "Frontend controller section; defaults to index" })),
174
- action: Type.Optional(Type.String({ description: "Frontend controller action; defaults to index" })),
175
- execute: Type.Optional(Type.Boolean()),
176
- }),
177
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
178
- const s = await boot(ctx);
179
- if (!/^[A-Z][A-Za-z0-9]*$/.test(params.vendor) || !/^[A-Z][A-Za-z0-9]*$/.test(params.module))
180
- return { content: [{ type: "text", text: "Mage2Gen requires PascalCase vendor and module names." }], details: {} };
181
- if (s.selected.environment.type === "ssh")
182
- return { content: [{ type: "text", text: "Mage2Gen generation is unavailable for SSH: the vendored Python library runs on the local project workspace." }], details: {} };
183
- const projectRoot = s.selected.environment.type === "warden"
184
- ? resolve(ctx.cwd, s.selected.environment.projectRoot)
185
- : resolve(ctx.cwd, s.selected.environment.root);
186
- const request = {
187
- mage2genRoot: mage2genRoot(),
188
- projectRoot,
189
- vendor: params.vendor,
190
- module: params.module,
191
- description: params.description,
192
- frontend: params.frontendRoute
193
- ? { route: params.frontendRoute, section: params.section ?? "index", action: params.action ?? "index" }
194
- : undefined,
195
- };
196
- const destination = mage2genDestination(projectRoot, params.vendor, params.module);
197
- if (await mage2genModuleExists(request))
198
- return { content: [{ type: "text", text: `Refused: module destination already exists: ${destination}` }], details: {} };
199
- if (!params.execute)
200
- return { content: [{ type: "text", text: `Preview: Mage2Gen will create ${destination}${request.frontend ? ` with frontend route ${request.frontend.route}/${request.frontend.section}/${request.frontend.action}` : ""}. Set execute=true after the user's direct request.` }], details: {} };
201
- if (!(ctx.hasUI && await ctx.ui.confirm("Generate Magento module", `Generate ${params.vendor}_${params.module} at ${destination}?`)))
202
- return { content: [{ type: "text", text: "Cancelled: confirmation required." }], details: {} };
203
- try {
204
- const generated = await generateMage2GenModule(request);
205
- return { content: [{ type: "text", text: `Mage2Gen created ${params.vendor}_${params.module} at ${generated}. Inspect and adapt the generated files before continuing.` }], details: {} };
206
- } catch (error) {
207
- return { content: [{ type: "text", text: `Mage2Gen failed: ${error.message}` }], details: {} };
208
- }
209
- },
210
- });
211
- pi.registerTool({
212
- name: "magento_workflow",
213
- label: "Magento workflow",
214
- description:
215
- "Preview or run an explicit Magento operational workflow. If the user directly requests execution, call with execute=true; MerchantDuo presents the sole authoritative confirmation dialog for risky actions. Do not ask for a separate conversational confirmation.",
216
- parameters: Type.Object({
217
- action: Type.Union([
218
- Type.Literal("status"),
219
- Type.Literal("syntax-check"),
220
- Type.Literal("cache-clean"),
221
- Type.Literal("cache-flush"),
222
- Type.Literal("compile"),
223
- Type.Literal("setup-upgrade"),
224
- Type.Literal("maintenance-enable"),
225
- Type.Literal("maintenance-disable"),
226
- Type.Literal("static-content-deploy"),
227
- Type.Literal("reindex"),
228
- Type.Literal("tests"),
229
- ]),
230
- execute: Type.Optional(Type.Boolean()),
231
- }),
232
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
233
- const s = await boot(ctx);
234
- if (params.action === "status")
235
- return {
236
- content: [
237
- {
238
- type: "text",
239
- text: JSON.stringify(
240
- {
241
- snapshot: s.magento,
242
- pendingFiles: [...pending.files],
243
- safeCacheTypes: safeCacheTypes(pending, s.magento),
244
- },
245
- null,
246
- 2,
247
- ),
248
- },
249
- ],
250
- details: {},
251
- };
252
- if (params.action === "syntax-check")
253
- return {
254
- content: [
255
- {
256
- type: "text",
257
- text: `Syntax checks are pending for: ${[...pending.files].join(", ") || "none"}`,
258
- },
259
- ],
260
- details: {},
261
- };
262
- if (!params.execute)
263
- return {
264
- content: [
265
- {
266
- type: "text",
267
- text: `Preview only. Set execute=true after confirmation. Action: ${params.action}`,
268
- },
269
- ],
270
- details: {},
271
- };
272
- const risky = params.action !== "cache-clean";
273
- if (
274
- risky &&
275
- !(
276
- ctx.hasUI &&
277
- (await ctx.ui.confirm(
278
- "Confirm Magento action",
279
- `Execute ${params.action}?`,
280
- ))
281
- )
282
- )
283
- return {
284
- content: [
285
- { type: "text", text: "Cancelled: confirmation required." },
286
- ],
287
- details: {},
288
- };
289
- if (s.selected.environment.type === "ssh")
290
- return {
291
- content: [
292
- {
293
- type: "text",
294
- text: "Remote mutations require a separately confirmed SSH command; workflow execution is refused here.",
295
- },
296
- ],
297
- details: {},
298
- };
299
- const action = params.action;
300
- const result = await runWorkflow(
301
- s.backend,
302
- action,
303
- action === "cache-clean" ? safeCacheTypes(pending, s.magento) : [],
304
- );
305
- return {
306
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
307
- details: {},
308
- };
309
- },
310
- });
311
- }
File without changes
File without changes
File without changes