@merchantduo/code 0.3.0-beta.0 → 0.3.0-beta.2

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 (42) hide show
  1. package/README.md +5 -5
  2. package/dist/app/system-prompt.d.ts +2 -1
  3. package/dist/app/system-prompt.js +10 -4
  4. package/dist/cli/arguments.d.ts +0 -3
  5. package/dist/cli/arguments.js +5 -13
  6. package/dist/cli/commands/agent.d.ts +7 -0
  7. package/dist/cli/commands/agent.js +46 -1
  8. package/dist/cli/commands/init.d.ts +14 -1
  9. package/dist/cli/commands/init.js +36 -2
  10. package/dist/cli/commands/provision.d.ts +10 -5
  11. package/dist/cli/commands/provision.js +22 -33
  12. package/dist/cli/main.js +1 -1
  13. package/dist/config/index.d.ts +2 -1
  14. package/dist/config/index.js +2 -1
  15. package/dist/config/loader.js +1 -5
  16. package/dist/config/operator-profile.d.ts +5 -0
  17. package/dist/config/operator-profile.js +28 -0
  18. package/dist/config/schema.d.ts +2 -0
  19. package/dist/config/schema.js +6 -0
  20. package/dist/environments/adapters/warden.d.ts +7 -1
  21. package/dist/environments/adapters/warden.js +24 -3
  22. package/dist/integrations/pi/context.js +3 -1
  23. package/dist/integrations/pi/permissions.d.ts +1 -0
  24. package/dist/integrations/pi/permissions.js +10 -3
  25. package/dist/integrations/pi/workspace.d.ts +1 -0
  26. package/dist/integrations/pi/workspace.js +36 -6
  27. package/dist/workflows/post-edit.d.ts +7 -0
  28. package/dist/workflows/post-edit.js +51 -0
  29. package/package.json +1 -2
  30. package/skills/magento-2.4/SKILL.md +241 -2
  31. package/dist/provision/advisor.d.ts +0 -12
  32. package/dist/provision/advisor.js +0 -28
  33. package/dist/provision/discovery.d.ts +0 -12
  34. package/dist/provision/discovery.js +0 -61
  35. package/dist/provision/interview.d.ts +0 -9
  36. package/dist/provision/interview.js +0 -29
  37. package/dist/provision/model.d.ts +0 -82
  38. package/dist/provision/model.js +0 -5
  39. package/dist/provision/planner.d.ts +0 -4
  40. package/dist/provision/planner.js +0 -51
  41. package/dist/provision/runner.d.ts +0 -35
  42. package/dist/provision/runner.js +0 -98
@@ -1,51 +0,0 @@
1
- const sourceMissing = (snapshot, input) => !snapshot.workspace.magentoRoot && (!input.source || input.source === "existing");
2
- const databaseMissing = (snapshot, input) => !input.database && snapshot.database.dumps.length !== 1;
3
- /** Deterministic planner. It has no process or filesystem side effects. */
4
- export function createProvisionPlan(snapshot, input) {
5
- const blocked = [];
6
- const rationale = [];
7
- const actions = [];
8
- const environment = input.environment;
9
- if (!environment)
10
- blocked.push("Select --env local or --env warden.");
11
- if (sourceMissing(snapshot, input))
12
- blocked.push("Provide --source existing, a Git URL, archive path, or HTTPS archive URL.");
13
- if (databaseMissing(snapshot, input))
14
- blocked.push("Provide --database current, a dump path, or an HTTPS dump URL.");
15
- if (input.database === "current" && !snapshot.database.envConfigured)
16
- blocked.push("--database current requires an existing usable Magento database configuration.");
17
- if (environment === "warden" && !snapshot.environments.warden.available)
18
- blocked.push("Warden is not installed or available on PATH.");
19
- if (environment === "local" && (!snapshot.environments.local.php || !snapshot.environments.local.composer || !snapshot.environments.local.mysql))
20
- blocked.push("Local provisioning requires PHP, Composer, and the MySQL client.");
21
- if (environment === "local" && snapshot.environments.local.activeServers.length === 0)
22
- blocked.push("Start nginx or Apache before local provisioning.");
23
- if (environment === "local" && snapshot.environments.local.activeServers.length > 1 && !input.localDomain)
24
- blocked.push("Both nginx and Apache are active; choose a local domain/server interactively.");
25
- if (blocked.length)
26
- return { actions, rationale, blocked, requiresSudo: false };
27
- if (!snapshot.workspace.magentoRoot && input.source && input.source !== "existing")
28
- actions.push({ type: "acquire-source", source: input.source });
29
- actions.push({ type: "install-dependencies" });
30
- if (environment === "warden" && !snapshot.environments.warden.running)
31
- actions.push({ type: "start-warden" });
32
- const database = input.database ?? (snapshot.database.dumps.length === 1 ? `dump:${snapshot.database.dumps[0]}` : undefined);
33
- if (environment === "local" && !snapshot.database.envConfigured)
34
- actions.push({ type: "configure-database" });
35
- if (database && database !== "current")
36
- actions.push({ type: "import-database", database });
37
- actions.push({ type: "magento-setup", command: "app:config:import" }, { type: "magento-setup", command: "setup:upgrade" }, { type: "magento-setup", command: "cache:clean" }, { type: "magento-setup", command: "indexer:reindex" }, { type: "magento-setup", command: "maintenance:disable" });
38
- const vhost = snapshot.environments.local.vhost;
39
- if (environment === "local" && !vhost) {
40
- const server = snapshot.environments.local.activeServers[0];
41
- const domain = input.localDomain ?? `${snapshot.workspace.root.split("/").filter(Boolean).pop().replace(/[^a-z0-9-]/gi, "-").toLowerCase()}.test`;
42
- actions.push({ type: "configure-web-server", server, domain });
43
- rationale.push(`Create a minimal ${server} virtual host for ${domain}.`);
44
- }
45
- actions.push({ type: "verify-storefront", url: environment === "warden" ? snapshot.environments.warden.domain : vhost?.domain ?? input.localDomain });
46
- rationale.push("Composer uses the lock file; Magento deployment mode is preserved.");
47
- return { actions, rationale, blocked, requiresSudo: actions.some((action) => action.type === "configure-web-server") };
48
- }
49
- export function unresolvedInputs(snapshot, input) {
50
- return createProvisionPlan(snapshot, input).blocked;
51
- }
@@ -1,35 +0,0 @@
1
- import type { CommandExecutor } from "#environments/executor";
2
- import type { ProvisionAction, ProvisionEnvironment, ProvisionInput } from "#provision/model";
3
- export type ProvisionRunner = {
4
- run(argv: string[], options?: {
5
- cwd?: string;
6
- input?: string;
7
- }): Promise<{
8
- exitCode: number;
9
- stdout: string;
10
- stderr: string;
11
- }>;
12
- };
13
- export declare class BackendProvisionRunner implements ProvisionRunner {
14
- private readonly backend;
15
- constructor(backend: CommandExecutor);
16
- run(argv: string[], options?: {
17
- cwd?: string;
18
- input?: string;
19
- }): Promise<import("#environments/executor").CommandResult>;
20
- }
21
- export declare function validateArchiveEntry(entry: string): boolean;
22
- export declare function vhostConfig(server: "nginx" | "apache", projectRoot: string, domain: string): string;
23
- export declare class DefaultProvisionExecutor {
24
- private readonly runner;
25
- private readonly root;
26
- private readonly environment;
27
- private readonly magentoRunner;
28
- constructor(runner: ProvisionRunner, root: string, environment: ProvisionEnvironment, magentoRunner?: ProvisionRunner);
29
- execute(action: ProvisionAction, input: ProvisionInput): Promise<void>;
30
- private acquire;
31
- private importDatabase;
32
- private configureVhost;
33
- private verify;
34
- private must;
35
- }
@@ -1,98 +0,0 @@
1
- import { mkdir, rm, stat } from "node:fs/promises";
2
- import { basename, join, resolve } from "node:path";
3
- export class BackendProvisionRunner {
4
- backend;
5
- constructor(backend) {
6
- this.backend = backend;
7
- }
8
- run(argv, options = {}) { return this.backend.run(argv, { input: options.input }); }
9
- }
10
- export function validateArchiveEntry(entry) {
11
- return entry.length > 0 && !entry.startsWith("/") && !entry.split(/[\\/]+/).includes("..");
12
- }
13
- export function vhostConfig(server, projectRoot, domain) {
14
- if (!/^[a-z0-9-]+\.test$/i.test(domain))
15
- throw new Error("Local domain must be a sanitized .test hostname");
16
- const root = join(resolve(projectRoot), "pub");
17
- return server === "nginx" ? `server { listen 80; server_name ${domain}; root ${root}; index index.php; location / { try_files $uri $uri/ /index.php?$args; } location ~ \\.php$ { try_files $uri =404; fastcgi_pass unix:/run/php/php-fpm.sock; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } }\n` : `<VirtualHost *:80>\n ServerName ${domain}\n DocumentRoot ${root}\n <Directory ${root}>\n AllowOverride All\n Require all granted\n </Directory>\n</VirtualHost>\n`;
18
- }
19
- export class DefaultProvisionExecutor {
20
- runner;
21
- root;
22
- environment;
23
- magentoRunner;
24
- constructor(runner, root, environment, magentoRunner = runner) {
25
- this.runner = runner;
26
- this.root = root;
27
- this.environment = environment;
28
- this.magentoRunner = magentoRunner;
29
- }
30
- async execute(action, input) {
31
- switch (action.type) {
32
- case "acquire-source": return this.acquire(action.source);
33
- case "install-dependencies": return this.must(["composer", "install", "--no-interaction", "--prefer-dist", "--no-progress"], this.magentoRunner);
34
- case "start-warden": return this.must(["warden", "env", "config"]).then(() => this.must(["warden", "env", "start"]));
35
- case "import-database": return this.importDatabase(action.database);
36
- case "configure-database": throw new Error("Database configuration is required but no safe credential source was supplied. Add app/etc/env.php or use a current configured database.");
37
- case "magento-setup":
38
- if (action.command === "app:config:import") {
39
- const result = await this.magentoRunner.run(["bin/magento", action.command, "--no-interaction"], { cwd: this.root });
40
- if (result.exitCode !== 0 && !/does not exist|Nothing to import/i.test(result.stderr + result.stdout))
41
- throw new Error(result.stderr || result.stdout);
42
- return;
43
- }
44
- return this.must(["bin/magento", action.command, "--no-interaction"], this.magentoRunner);
45
- case "configure-web-server": return this.configureVhost(action.server, action.domain);
46
- case "verify-storefront": return this.verify(action.url);
47
- }
48
- }
49
- async acquire(source) {
50
- const entries = await stat(this.root).then(() => import("node:fs/promises").then(({ readdir }) => readdir(this.root))).catch(() => []);
51
- if (entries.length)
52
- throw new Error("Refusing to acquire source into a non-empty non-Magento directory.");
53
- if (source.startsWith("git:"))
54
- return this.must(["git", "clone", source.slice(4), "."]);
55
- const location = source.replace(/^(archive|https):/, "");
56
- const home = process.env.MERCHANTDUO_HOME ?? join(process.env.XDG_STATE_HOME ?? "/tmp", "merchantduo");
57
- const stage = join(home, "provisions", `${Date.now()}-${basename(location).replace(/[^a-z0-9._-]/gi, "-")}`);
58
- await mkdir(stage, { recursive: true });
59
- try {
60
- const archive = source.startsWith("https:") ? join(stage, "source.archive") : resolve(location);
61
- if (source.startsWith("https:"))
62
- await this.must(["curl", "--fail", "--location", "--output", archive, location]);
63
- const listed = await this.runner.run(["tar", "-tzf", archive]);
64
- if (listed.exitCode !== 0 || listed.stdout.split("\n").filter(Boolean).some((entry) => !validateArchiveEntry(entry)))
65
- throw new Error("Archive is invalid or contains unsafe paths.");
66
- await this.must(["tar", "-xzf", archive, "-C", this.root, "--strip-components=1"]);
67
- const magento = await this.runner.run(["sh", "-lc", "test -f composer.json && (test -x bin/magento || grep -q 'magento/' composer.json)"], { cwd: this.root });
68
- if (magento.exitCode !== 0)
69
- throw new Error("Extracted archive does not contain a Magento root.");
70
- }
71
- finally {
72
- await rm(stage, { recursive: true, force: true });
73
- }
74
- }
75
- async importDatabase(database) {
76
- const source = database.replace(/^(dump|https):/, "");
77
- if (this.environment === "warden")
78
- return this.must(["warden", "db", "import", source]);
79
- if (database.startsWith("https:"))
80
- throw new Error("Download the local database dump before local import; database credentials are never accepted as CLI arguments.");
81
- return this.must(["sh", "-lc", `${source.endsWith(".gz") ? "gzip -dc --" : "cat --"} "$1" | mysql`, "merchantduo-db-import", source]);
82
- }
83
- async configureVhost(server, domain) {
84
- const config = vhostConfig(server, this.root, domain);
85
- const file = server === "nginx" ? `/etc/nginx/sites-available/${domain}` : `/etc/apache2/sites-available/${domain}.conf`;
86
- const encoded = Buffer.from(config).toString("base64");
87
- await this.must(["sudo", "sh", "-lc", `echo ${encoded} | base64 -d > ${file}`]);
88
- if (server === "nginx")
89
- await this.must(["sudo", "sh", "-lc", `ln -sfn ${file} /etc/nginx/sites-enabled/${domain} && nginx -t && systemctl reload nginx`]);
90
- else
91
- await this.must(["sudo", "sh", "-lc", `a2ensite ${domain}.conf && apachectl configtest && systemctl reload apache2`]);
92
- await this.must(["sudo", "sh", "-lc", `grep -qE '^[[:space:]]*127\\.0\\.0\\.1[[:space:]].*\\b${domain}\\b' /etc/hosts || echo '127.0.0.1 ${domain}' >> /etc/hosts`]);
93
- }
94
- async verify(url) { if (url)
95
- await this.must(["curl", "--fail", "--location", "--max-time", "20", `http://${url}`]); }
96
- async must(argv, runner = this.runner) { const result = await runner.run(argv, { cwd: this.root }); if (result.exitCode !== 0)
97
- throw new Error(`${argv.join(" ")} failed: ${result.stderr || result.stdout}`); }
98
- }