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

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 (55) hide show
  1. package/README.md +26 -120
  2. package/dist/app/permission-mode.d.ts +5 -0
  3. package/dist/app/permission-mode.js +10 -0
  4. package/dist/app/runtime.d.ts +11 -1
  5. package/dist/app/runtime.js +52 -4
  6. package/dist/app/status.js +9 -7
  7. package/dist/app/system-prompt.js +7 -1
  8. package/dist/cli/arguments.d.ts +1 -0
  9. package/dist/cli/arguments.js +7 -1
  10. package/dist/cli/commands/agent.d.ts +3 -1
  11. package/dist/cli/commands/agent.js +5 -8
  12. package/dist/cli/commands/doctor.js +2 -2
  13. package/dist/cli/commands/init.d.ts +1 -1
  14. package/dist/cli/commands/init.js +1 -1
  15. package/dist/cli/main.js +1 -1
  16. package/dist/config/schema.d.ts +0 -5
  17. package/dist/config/schema.js +0 -1
  18. package/dist/environments/adapter.d.ts +1 -0
  19. package/dist/environments/adapters/warden.d.ts +5 -0
  20. package/dist/environments/adapters/warden.js +7 -0
  21. package/dist/environments/backend.d.ts +7 -0
  22. package/dist/environments/backend.js +31 -0
  23. package/dist/environments/model.d.ts +2 -2
  24. package/dist/environments/model.js +1 -3
  25. package/dist/features/magerun2/index.d.ts +2 -0
  26. package/dist/features/magerun2/index.js +2 -0
  27. package/dist/features/magerun2/model.d.ts +10 -0
  28. package/dist/features/magerun2/model.js +1 -0
  29. package/dist/features/magerun2/service.d.ts +12 -0
  30. package/dist/{testing/magerun.js → features/magerun2/service.js} +34 -13
  31. package/dist/integrations/pi/context.js +13 -7
  32. package/dist/integrations/pi/environment.d.ts +3 -0
  33. package/dist/integrations/pi/environment.js +64 -0
  34. package/dist/integrations/pi/magerun2.d.ts +4 -0
  35. package/dist/integrations/pi/magerun2.js +35 -0
  36. package/dist/integrations/pi/optional-services.d.ts +9 -0
  37. package/dist/integrations/pi/optional-services.js +79 -0
  38. package/dist/integrations/pi/permissions.d.ts +4 -2
  39. package/dist/integrations/pi/permissions.js +48 -21
  40. package/dist/integrations/pi/testing.js +8 -10
  41. package/dist/integrations/pi/theme.d.ts +2 -0
  42. package/dist/integrations/pi/theme.js +23 -4
  43. package/dist/integrations/pi/workflows.js +1 -1
  44. package/dist/magento/prompt.d.ts +2 -1
  45. package/dist/magento/prompt.js +2 -2
  46. package/dist/testing/discovery.d.ts +0 -1
  47. package/dist/testing/discovery.js +1 -4
  48. package/dist/testing/host.d.ts +1 -0
  49. package/dist/testing/host.js +14 -3
  50. package/dist/testing/index.d.ts +0 -1
  51. package/dist/testing/index.js +0 -1
  52. package/dist/testing/model.d.ts +0 -1
  53. package/package.json +5 -1
  54. package/skills/magerun2/SKILL.md +22 -0
  55. package/dist/testing/magerun.d.ts +0 -8
package/README.md CHANGED
@@ -1,66 +1,27 @@
1
1
  # MerchantDuo
2
2
 
3
- MerchantDuo is a Pi coding agent for Magento teams that works from the store's real environment, Magento version, deployment mode, and theme context.
3
+ MerchantDuo is a Pi coding agent for Magento 2.4 teams. It starts from the store's selected environment, Magento version, deployment mode, and theme context instead of treating a Magento project as a generic repository.
4
4
 
5
- ## Main features
5
+ ## Overview
6
6
 
7
- - Magento-aware coding sessions
8
- - Local, Warden, and SSH environments
9
- - Developer-store provisioning
10
- - Magento 2.4, Luma, Hyva, Mage2Gen, and testing skills
11
- - Magento workflows and focused test tools
12
- - Version, edition, deployment-mode, and theme detection
13
- - Remote-write and operational safety controls
7
+ - Works with local checkouts, Warden, and SSH environments.
8
+ - Detects Magento edition, version, PHP version, deployment mode, cache types, and registered theme inheritance.
9
+ - Ships Magento 2.4, Luma, Hyva, Mage2Gen, and testing skills.
10
+ - Provides guided local/Warden store provisioning.
11
+ - Keeps remote writes and Magento operations behind explicit execution boundaries.
14
12
 
15
- ## Details
13
+ At session start, MerchantDuo produces a Magento snapshot from the selected environment. Frontend and admin URLs are discovered but remain unverified until an explicit test. The compact status line shows MerchantDuo and Magento versions, environment lifecycle, session permission mode, an explicit theme scope when one is selected, and the frontend URL when available.
16
14
 
17
- MerchantDuo gives Magento developers a coding-agent workspace that starts with the project context they need. It runs on Pi, inspects the selected Magento environment, keeps theme scope visible, and exposes Magento-specific tools for the work that usually slows an agency team down.
15
+ ## Commands and tools
18
16
 
19
- It is built for Magento 2.4 development, not as a generic chat wrapper with a few shell commands attached. A session knows whether it is working locally, in Warden, or through SSH. It can identify the Magento edition, version, PHP version, deployment mode, enabled module count, cache types, and real registered themes before it suggests framework-specific work.
17
+ Extension slash commands are namespaced with `duo-` to avoid collisions with other Pi extensions.
20
18
 
21
- MerchantDuo is currently beta software for development teams.
19
+ - [Command reference](docs/commands.md) covers session theme and permission switching, lifecycle, and navigation commands.
20
+ - [Tool reference](docs/tools.md) covers workspace, environment, Magento workflow, magerun2, testing, PHP-console, and optional knowledge tools, including parameters and confirmation boundaries.
22
21
 
23
- ## What agencies get
22
+ ## Environments
24
23
 
25
- - A Pi coding-agent package with Magento context from the start of each session.
26
- - Local, Warden, and SSH environments selected per project or command.
27
- - A guided `provision` command for preparing a local or Warden developer store.
28
- - Native Magento 2.4, Luma, Hyva, Mage2Gen, and testing skills.
29
- - Magento-aware file, search, edit, module-generation, workflow, HTTP, browser, and PHP-console tools.
30
- - Theme discovery based on registered themes in `vendor` and `app/design`, including parent chains and Luma or Hyva classification.
31
- - Deployment-mode guidance that changes for developer, default, and production installations.
32
- - Guardrails around remote writes, Magento operations, credentials, and browser access.
33
-
34
- The result is less time re-explaining a store to an agent and fewer generic recommendations that do not fit Magento.
35
-
36
- ## Magento context before the first change
37
-
38
- At session startup, MerchantDuo creates a Magento snapshot for the selected environment. It reads `composer.lock` and `bin/magento --version` when available, then reports disagreements instead of silently choosing one. The snapshot includes:
39
-
40
- - Magento Open Source or Adobe Commerce edition and version
41
- - PHP version and deployment mode
42
- - Enabled module count and cache types
43
- - Registered themes, inheritance chains, and Luma or Hyva classification
44
- - Available frontend and admin URLs, marked unverified until explicitly tested
45
- - Whether the selected environment is writable
46
-
47
- Theme discovery only scans real Magento theme roots: package roots under `vendor` and themes under `app/design`. It does not search arbitrary vendor fixtures. Use `/theme` to select `all`, `any`, or a detected theme code. `all` keeps every detected theme in scope, while `any` tells the agent to identify the relevant theme before it edits theme-specific code.
48
-
49
- ## Curated Magento knowledge
50
-
51
- MerchantDuo ships focused Pi skills instead of relying on broad, generic instructions:
52
-
53
- - `magento-24x` guides Magento 2.4 work toward service contracts, dependency injection, declarative schema and data patches, layout XML, and the right deployment-mode workflow.
54
- - `luma` covers Magento fallback, layout XML, RequireJS, UI components, Knockout, LESS, and static assets.
55
- - `hyva` directs work toward Hyva templates, Alpine, Tailwind, and Hyva UI components. The package also vendors the relevant Hyva skills.
56
- - `mage2gen` makes vendored Mage2Gen the standard starting point for new modules.
57
- - `magento-testing` keeps testing explicit, targeted, and unauthenticated.
58
-
59
- The current package provides this curated knowledge locally through Pi skills. `merchantduo doctor` reports knowledge-service status and currently shows that no external knowledge connection is configured. That prevents the agent from presenting an unavailable MCP source as project knowledge.
60
-
61
- ## Environments that match agency work
62
-
63
- Define the environments your team actually uses in `.merchantduo.yaml`, then select one at launch with `--env`.
24
+ Define the environments your team uses in `.merchantduo.yaml`, then select one at launch with `--env`.
64
25
 
65
26
  ```yaml
66
27
  defaultEnvironment: local
@@ -85,12 +46,9 @@ environments:
85
46
  type: ssh
86
47
  host: shop-stage
87
48
  root: /var/www/html
88
- writable: false
89
49
  ```
90
50
 
91
- Local work uses the Magento checkout directly. Warden work routes file and Magento commands through the selected service. SSH starts read-only unless you set `writable: true`, and writable remote file or shell work requires confirmation.
92
-
93
- For SSH user names, ports, and identities, use an OpenSSH alias rather than extra YAML keys:
51
+ Local uses the checkout directly. Warden routes project and Magento execution through its configured service. Local and Warden sessions start in `normal`; SSH starts in `read-only`. Use `/duo-switch-permissions <read-only|normal|yolo>` to change only the current session, or `merchantduo --yolo` to start in `yolo`. Configure SSH user, port, and key through an OpenSSH host alias:
94
52
 
95
53
  ```sshconfig
96
54
  Host shop-stage
@@ -100,89 +58,37 @@ Host shop-stage
100
58
  IdentityFile ~/.ssh/id_ed25519
101
59
  ```
102
60
 
103
- Use the alias as `host: shop-stage` in `.merchantduo.yaml`.
61
+ Use that alias as `host: shop-stage`. `merchantduo init` offers local first and Warden when its project `.env` is detected; it prints an SSH example instead of writing an unused remote environment.
104
62
 
105
- `merchantduo init` offers the local checkout first. When the project `.env` contains Warden routing markers, it also offers a Warden environment. It writes only the environments you accept and then prints the remote SSH example instead of adding a fake remote entry. Local development certificates are accepted by default; set `testing.allowInsecureTls: false` when the selected URLs have publicly trusted certificates.
63
+ ## Provisioning
106
64
 
107
- ## Provision a developer store
108
-
109
- Run provisioning from an empty directory or an existing Magento project:
65
+ Provision a local or Warden developer store from an empty directory or an existing Magento checkout:
110
66
 
111
67
  ```sh
112
68
  merchantduo provision
113
69
  merchantduo provision --env warden --source https://git.example.test/store.git --database ./backup.sql.gz --yes
114
70
  ```
115
71
 
116
- Provisioning supports local and Warden environments. It inspects available services, Magento source, database dumps, and existing web-server routing. The interactive flow asks only for choices it cannot resolve, prints an ordered plan, and asks once before it changes anything.
117
-
118
- With `--yes`, every decision must already be explicit or unambiguous. The command fails rather than guessing a source, database, web server, or environment.
119
-
120
- Sources can be an existing checkout, Git URL, local archive, or HTTPS archive. Databases can be the current database, a local SQL or compressed SQL dump, or an HTTPS URL. Downloaded archives are staged below `MERCHANTDUO_HOME/provisions`, checked for path traversal, and removed after extraction.
121
-
122
- Input precedence is command flags, then a discovered Magento checkout or the only SQL dump in the project root, then the interactive interview.
123
-
124
- For Warden, MerchantDuo runs Warden lifecycle and database commands on the host, then runs Composer and Magento commands in the configured PHP service. Local provisioning requires PHP, Composer, the MySQL client, and an active nginx or Apache service. If no matching virtual host exists, the approved plan can add a minimal `pub` vhost and an `/etc/hosts` entry for the selected local domain. It does not replace an existing matching vhost.
125
-
126
- If `MERCHANTDUO_LLM_TOKEN` is set, provisioning can ask a narrow OpenAI-compatible advisor to choose from its already generated action plan. Its built-in endpoint and model are mock wiring, not a hosted AI service. Invalid advisor output falls back to the deterministic plan. MerchantDuo never sends the token, database credentials, or `app/etc/env.php` contents to the advisor.
127
-
128
- ## Tools that respect Magento boundaries
129
-
130
- MerchantDuo uses direct Pi extensions for context, permissions, workspace work, themes, Mage2Gen, workflows, and testing. The package manifest is the source of truth for both extensions and skills.
131
-
132
- | Capability | What it does |
133
- | --- | --- |
134
- | Workspace tools | Read, search, write, and edit inside the selected environment. |
135
- | `mage2gen_generate_module` | Previews or generates a module under the selected local or Warden project's `app/code`. It refuses existing destinations and does not run over SSH. |
136
- | `magento_workflow` | Previews Magento operations such as cache cleaning, setup upgrade, DI compilation, static-content deployment, reindexing, and tests. Use `execute: true` to request the single in-tool confirmation. |
137
- | `magento_http_test` | Runs an explicit unauthenticated GET or HEAD request against a discovered Magento URL. |
138
- | `magento_browser_test` | Captures a screenshot or DOM dump with an isolated Chromium profile. Artifacts are stored under `MERCHANTDUO_HOME/artifacts/<project-id>/`. |
139
- | `magento_php_repl` | Runs one PHP snippet through a one-shot `n98-magerun2` console. |
140
- | `/navigate-frontend` and `/navigate-adminhtml` | Open a discovered same-origin frontend or admin URL without handling login credentials. |
141
-
142
- No lifecycle hook runs tests, cache actions, deployments, or browser commands because a session started or a file changed. Those operations remain explicit. Testing and navigation are direct user-request boundaries, so they do not ask for the MerchantDuo confirmation.
143
-
144
- ## Magento workflows without blind deployment commands
145
-
146
- MerchantDuo tracks changed files and recommends the smallest relevant operation. It does not assume every edit needs a full Magento deployment.
147
-
148
- | Detected mode | Typical module-change sequence |
149
- | --- | --- |
150
- | Developer | `setup:upgrade`, then clean relevant cache types. |
151
- | Default | `setup:upgrade`, then clean relevant cache types. Compile DI or deploy static content only when the change requires it. |
152
- | Production | Enable maintenance mode, run `setup:upgrade`, compile DI, deploy static content, clean relevant cache types, then disable maintenance mode. |
153
-
154
- If the deployment mode cannot be determined, MerchantDuo reports that fact instead of inventing a sequence.
72
+ The interactive flow discovers available source, database, and service evidence, shows an ordered plan, and asks once before mutations. `--yes` requires all decisions to be explicit or unambiguous.
155
73
 
156
74
  ## Install and start
157
75
 
158
- MerchantDuo requires Node.js 22.19 or later, Bash, Python 3, curl, and Chromium or Chrome. Warden environments require Warden. SSH environments require OpenSSH. A remote PHP console needs `n98-magerun2` on that environment.
159
-
160
- Linux navigation also needs `xdg-open`. Browser discovery accepts `chromium-browser`, `chromium`, `google-chrome`, and `google-chrome-stable`.
161
-
162
- Install the package through your team's npm registry, then create the project configuration:
76
+ MerchantDuo requires Node.js 22.19 or later, Bash, and Python 3. HTTP/browser testing additionally needs curl and Chrome or Chromium; Warden and SSH environments need Warden and OpenSSH respectively.
163
77
 
164
78
  ```sh
79
+ pnpm install --frozen-lockfile
165
80
  merchantduo init
166
81
  merchantduo doctor --env local
167
82
  merchantduo --env local
168
83
  ```
169
84
 
170
- `merchantduo init` creates `.merchantduo.yaml`. Review it before the first agent session. MerchantDuo requires an explicit one-time trust decision for a project configuration. Set `MERCHANTDUO_TRUST_CONFIG=1` only after reviewing that file.
171
-
172
- Testing URLs can be configured at the top level or per environment. Configured URLs take precedence over discovery. Local discovery reads Magento base URLs and the admin URI. Warden reads `TRAEFIK_DOMAIN` and `TRAEFIK_SUBDOMAIN` from the project `.env`. MerchantDuo does not contact discovered URLs until an explicit testing request.
173
-
174
- For SSH, MerchantDuo uses configured URLs or reads Magento remotely when URLs are absent. HTTP tests, browser tests, navigation, temporary browser profiles, and test artifacts always run on and belong to the host, including for Warden. MerchantDuo finds and invokes the host `curl` and Chrome/Chromium, never their container copies. Local TLS exceptions apply consistently to both curl and the browser. The PHP REPL remains Magento-side because it executes application code and therefore needs Warden's PHP service, project files, configuration, and database. When local `n98-magerun2` is unavailable, MerchantDuo looks in `vendor/bin`, `bin`, and `PATH`, then can use a verified official PHAR cache under `MERCHANTDUO_HOME/tools/n98-magerun2` without modifying the Magento project. Failure to obtain that cache does not stop the session.
85
+ Review the generated `.merchantduo.yaml` before the first session. MerchantDuo requires a one-time configuration trust decision; set `MERCHANTDUO_TRUST_CONFIG=1` only after review.
175
86
 
176
- ## Safety model
87
+ ## Safety and optional services
177
88
 
178
- Magento projects carry more operational risk than ordinary application repositories. MerchantDuo keeps the boundaries simple:
89
+ MerchantDuo does not load `app/etc/env.php`, credentials, or tokens into agent context. It does not run background tests, cache commands, deployment actions, browser operations, or PHP code. Explicit operations use the confirmation rules described in the [tool reference](docs/tools.md).
179
90
 
180
- - It does not read `app/etc/env.php`, credentials, or tokens for agent context.
181
- - It treats discovered URLs as unverified until you run an explicit HTTP or browser test.
182
- - It performs no background cache, deployment, browser, HTTP, PHP-console, or workflow action.
183
- - Module generation and Magento workflows preview first. Execution requires `execute: true` and one harness confirmation.
184
- - Read-only SSH blocks write, edit, and shell operations. Writable SSH asks for confirmation before those operations.
185
- - SSH testing uses the publicly reachable URL and never creates a tunnel.
91
+ Set `MERCHANTDUO_MCP_TOKEN` to enable the built-in read-only knowledge tool. Set `MERCHANTDUO_LLM_TOKEN` to enable the built-in MerchantDuo provider. Neither endpoint is project configuration.
186
92
 
187
93
  ## Development
188
94
 
@@ -190,7 +96,7 @@ Magento projects carry more operational risk than ordinary application repositor
190
96
  pnpm install --frozen-lockfile
191
97
  pnpm check
192
98
  pnpm test
193
- pnpm pack --dry-run
99
+ npm pack --dry-run
194
100
  ```
195
101
 
196
102
  For the extension layout, provisioning boundary, and session lifecycle, see [the architecture notes](docs/architecture.md).
@@ -0,0 +1,5 @@
1
+ import type { Environment } from "#environments/model";
2
+ export declare const permissionModes: readonly ["read-only", "normal", "yolo"];
3
+ export type PermissionMode = (typeof permissionModes)[number];
4
+ /** Resolve one non-persistent session policy. Invalid launch input is rejected, never relaxed. */
5
+ export declare function resolvePermissionMode(environment: Environment, override?: string): PermissionMode;
@@ -0,0 +1,10 @@
1
+ export const permissionModes = ["read-only", "normal", "yolo"];
2
+ /** Resolve one non-persistent session policy. Invalid launch input is rejected, never relaxed. */
3
+ export function resolvePermissionMode(environment, override) {
4
+ if (override !== undefined) {
5
+ if (!permissionModes.includes(override))
6
+ throw new Error(`Invalid MERCHANTDUO_PERMISSION_MODE: ${override}. Expected read-only, normal, or yolo.`);
7
+ return override;
8
+ }
9
+ return environment.type === "ssh" ? "read-only" : "normal";
10
+ }
@@ -1,9 +1,12 @@
1
1
  import { type MerchantConfig } from "#config/index";
2
2
  import { EnvironmentBackend } from "#environments/backend";
3
3
  import type { Environment } from "#environments/model";
4
+ import { type PermissionMode } from "#app/permission-mode";
5
+ import type { EnvironmentStatus } from "#environments/model";
4
6
  import { type MagentoInspector, type MagentoSnapshot } from "#magento/index";
5
7
  import { ChangeTracker } from "#workflows/change-tracker";
6
8
  import type { TestingSnapshot } from "#testing/model";
9
+ import type { Magerun2Capability } from "#features/magerun2/index";
7
10
  export type SessionState = {
8
11
  config: MerchantConfig;
9
12
  configPath?: string;
@@ -12,8 +15,11 @@ export type SessionState = {
12
15
  environment: Environment;
13
16
  };
14
17
  backend: EnvironmentBackend;
18
+ environmentStatus: EnvironmentStatus;
19
+ permissionMode: PermissionMode;
15
20
  magento: MagentoSnapshot;
16
21
  testing?: TestingSnapshot;
22
+ magerun2?: Magerun2Capability;
17
23
  };
18
24
  export declare class MerchantDuoRuntime {
19
25
  #private;
@@ -25,6 +31,10 @@ export declare class MerchantDuoRuntime {
25
31
  resetInjection(): void;
26
32
  injectionKey(skillId: string, theme: string): string;
27
33
  changeTheme(cwd: string, activeTheme: string): Promise<SessionState>;
28
- writable(state: SessionState): boolean;
34
+ setPermissionMode(mode: PermissionMode): SessionState;
29
35
  setTesting(snapshot: TestingSnapshot): void;
36
+ setMagerun2(capability: Magerun2Capability): void;
37
+ setEnvironmentStatus(status: EnvironmentStatus): SessionState;
38
+ startEnvironment(cwd: string): Promise<SessionState>;
39
+ stopEnvironment(cwd: string): Promise<SessionState>;
30
40
  }
@@ -1,6 +1,6 @@
1
1
  import { loadConfig, selectEnvironment, setProjectTheme, } from "#config/index";
2
2
  import { EnvironmentBackend } from "#environments/backend";
3
- import { environmentWritable } from "#environments/model";
3
+ import { resolvePermissionMode } from "#app/permission-mode";
4
4
  import { DefaultMagentoInspector, } from "#magento/index";
5
5
  import { ChangeTracker } from "#workflows/change-tracker";
6
6
  export class MerchantDuoRuntime {
@@ -20,11 +20,14 @@ export class MerchantDuoRuntime {
20
20
  });
21
21
  const selected = selectEnvironment(loaded.config, cwd, process.env.MERCHANTDUO_SELECTED_ENV);
22
22
  const backend = new EnvironmentBackend(selected.environment, cwd);
23
+ const environmentStatus = await backend.status();
23
24
  this.#state = {
24
25
  ...loaded,
25
26
  selected,
26
27
  backend,
27
- magento: await this.inspector.inspect(backend),
28
+ environmentStatus,
29
+ permissionMode: resolvePermissionMode(selected.environment, process.env.MERCHANTDUO_PERMISSION_MODE),
30
+ magento: environmentStatus === "running" ? await this.inspector.inspect(backend) : stoppedMagentoSnapshot(),
28
31
  };
29
32
  return this.#state;
30
33
  }
@@ -45,12 +48,57 @@ export class MerchantDuoRuntime {
45
48
  this.resetInjection();
46
49
  return state;
47
50
  }
48
- writable(state) {
49
- return environmentWritable(state.selected.environment);
51
+ setPermissionMode(mode) {
52
+ if (!this.#state)
53
+ throw new Error("Session has not started");
54
+ this.#state.permissionMode = mode;
55
+ return this.#state;
50
56
  }
51
57
  setTesting(snapshot) {
52
58
  if (!this.#state)
53
59
  throw new Error("Session has not started");
54
60
  this.#state.testing = snapshot;
55
61
  }
62
+ setMagerun2(capability) {
63
+ if (!this.#state)
64
+ throw new Error("Session has not started");
65
+ this.#state.magerun2 = capability;
66
+ }
67
+ setEnvironmentStatus(status) {
68
+ if (!this.#state)
69
+ throw new Error("Session has not started");
70
+ this.#state.environmentStatus = status;
71
+ if (status === "stopped") {
72
+ this.#state.magento = stoppedMagentoSnapshot();
73
+ this.#state.testing = undefined;
74
+ this.#state.magerun2 = undefined;
75
+ }
76
+ return this.#state;
77
+ }
78
+ async startEnvironment(cwd) {
79
+ const state = await this.boot(cwd);
80
+ const result = await state.backend.start();
81
+ if (result && result.exitCode !== 0)
82
+ throw new Error(result.stderr || result.stdout || "Could not start environment");
83
+ state.environmentStatus = await state.backend.status();
84
+ if (state.environmentStatus === "running")
85
+ state.magento = await this.inspector.inspect(state.backend);
86
+ return state;
87
+ }
88
+ async stopEnvironment(cwd) {
89
+ const state = await this.boot(cwd);
90
+ const result = await state.backend.stop();
91
+ if (result && result.exitCode !== 0)
92
+ throw new Error(result.stderr || result.stdout || "Could not stop environment");
93
+ state.environmentStatus = await state.backend.status();
94
+ if (state.environmentStatus === "stopped") {
95
+ state.magento = stoppedMagentoSnapshot();
96
+ state.testing = undefined;
97
+ state.magerun2 = undefined;
98
+ }
99
+ return state;
100
+ }
101
+ }
102
+ function stoppedMagentoSnapshot() {
103
+ return { cacheTypes: [], warnings: ["Environment is stopped"], evidence: [], themes: [] };
56
104
  }
@@ -1,9 +1,11 @@
1
1
  export function refreshStatus(ctx, state) {
2
- const environment = state.selected.environment;
3
- const writePolicy = environment.type === "ssh"
4
- ? environment.writable
5
- ? "write + approve"
6
- : "read-only + approve"
7
- : "write + approve";
8
- ctx.ui.setStatus("merchantduo", `MerchantDuo ${process.env.MERCHANTDUO_PACKAGE_VERSION ?? "?"} | Magento ${state.magento.version ?? "?"} | ${state.selected.name}/${environment.type} | ${writePolicy} | Theme: ${state.config.activeTheme} | URLs: ${state.testing?.frontendUrl ?? "unavailable"} (unverified)`);
2
+ const parts = [
3
+ `MerchantDuo ${process.env.MERCHANTDUO_PACKAGE_VERSION ?? "?"}`,
4
+ `M${state.magento.version ?? "?"}`,
5
+ `${state.selected.name} (${state.environmentStatus})`,
6
+ state.permissionMode === "read-only" ? "r-o" : state.permissionMode,
7
+ ...(state.config.activeTheme === "all" ? [] : [state.config.activeTheme]),
8
+ ...(state.testing?.frontendUrl ? [state.testing.frontendUrl] : []),
9
+ ];
10
+ ctx.ui.setStatus("merchantduo", parts.join(" · "));
9
11
  }
@@ -11,5 +11,11 @@ For every request to create a new Magento module, call the \`mage2gen_generate_m
11
11
 
12
12
  After every change, inspect the changed module and files and choose the smallest applicable post-change workflow from Magento conventions; never apply a full sequence blindly. Ordinary PHP or template edits normally need only the relevant targeted cache clean, if any. Module registration, schema, declarative configuration, or dependency changes trigger mode-aware guidance from the detected snapshot mode: developer recommends \`setup:upgrade\` then targeted cache clean and does not normally compile DI or deploy static content; default recommends \`setup:upgrade\` then targeted cache clean, with compile or static deploy only when the task specifically requires them; production recommends maintenance enable, \`setup:upgrade\`, DI compile, static-content deploy, targeted cache clean, then maintenance disable. If the mode is unknown, report that and do not infer a deployment sequence.
13
13
 
14
- For cache clean/flush, maintenance enable/disable, static content deployment, compilation, setup upgrade, indexing, tests, or deployment: when the user directly requests execution, invoke \`magento_workflow\` with \`execute: true\` immediately. Use only this workflow tool for operational Magento commands. Do not ask the user for a separate conversational confirmation: the harness dialog is the sole authoritative confirmation. When execution was not requested, explain the operation and offer a preview. Never inspect app/etc/env.php or credentials.`;
14
+ For cache clean/flush, maintenance enable/disable, static content deployment, compilation, setup upgrade, indexing, tests, or deployment: when the user directly requests execution, invoke \`magento_workflow\` with \`execute: true\` immediately. Use only this workflow tool for operational Magento commands. Do not ask the user for a separate conversational confirmation: the harness dialog is the sole authoritative confirmation. When execution was not requested, explain the operation and offer a preview.
15
+
16
+ Session permission mode is shown in the startup context and can be changed only with \`/duo-switch-permissions read-only|normal|yolo\`. Read-only blocks direct write/edit but an approved shell or explicit operation may still mutate. Normal retains ordinary local/Warden direct workspace behavior and SSH confirmations. Yolo removes MerchantDuo dialogs only; it never removes the explicit \`execute: true\` boundary, stopped-environment block, SSH lifecycle restriction, Mage2Gen SSH restriction, remote workflow restriction, PHP-console boundary, or credential and URL protections.
17
+
18
+ For a supported cache, deploy, indexing, or test operation, prefer \`magento_workflow\`. For a different n98-magerun2 subcommand, use \`magerun2\` with an exact \`args: string[]\` vector, never a shell command or interpolation. When the user did not directly request execution, return its preview without \`execute: true\`; an executed generic magerun2 call requires \`execute: true\` and uses the same sole harness confirmation. Never send \`dev:console\` through \`magerun2\`: use the typed \`magento_php_repl\` tool instead.
19
+
20
+ Environment lifecycle is explicit. Warden has known direct controls. SSH is always reported running and must never be started, stopped, or restarted. A local environment is agent-directed: when the user requests a start or stop, inspect the actual project lifecycle first, then call \`environment_start\` or \`environment_stop\` with the exact smallest command. Never guess a generic Docker, Compose, npm, or service command, and never affect unrelated host services. Use \`environment_set_status\` after a status inspection that does not itself start or stop the stack. Never inspect app/etc/env.php or credentials.`;
15
21
  }
@@ -5,6 +5,7 @@ export type CliArguments = {
5
5
  source?: string;
6
6
  database?: string;
7
7
  yes: boolean;
8
+ yolo: boolean;
8
9
  piArguments: string[];
9
10
  };
10
11
  export declare function parseArguments(args: string[]): CliArguments;
@@ -7,6 +7,7 @@ export function parseArguments(args) {
7
7
  let source;
8
8
  let database;
9
9
  let yes = false;
10
+ let yolo = false;
10
11
  for (let index = 0; index < input.length; index += 1) {
11
12
  const value = input[index];
12
13
  if (value === "--env" || value === "--config" || value === "--source" || value === "--database") {
@@ -26,9 +27,14 @@ export function parseArguments(args) {
26
27
  else if (value === "--yes") {
27
28
  yes = true;
28
29
  }
30
+ else if (value === "--yolo") {
31
+ yolo = true;
32
+ }
29
33
  else {
30
34
  piArguments.push(value);
31
35
  }
32
36
  }
33
- return { command, environment, configPath, source, database, yes, piArguments };
37
+ if (command && yolo)
38
+ throw new Error("--yolo is only available when starting an agent session");
39
+ return { command, environment, configPath, source, database, yes, yolo, piArguments };
34
40
  }
@@ -1 +1,3 @@
1
- export declare function launchAgent(piArguments: string[], selectedEnvironment: string, configPath?: string): Promise<number>;
1
+ import type { PermissionMode } from "#app/permission-mode";
2
+ export declare function launchAgent(piArguments: string[], selectedEnvironment: string, configPath?: string, permissionMode?: PermissionMode): Promise<number>;
3
+ export declare function agentEnvironment(base: NodeJS.ProcessEnv, piDirectory: string, selectedEnvironment: string, configPath: string | undefined, version: string, permissionMode?: PermissionMode): NodeJS.ProcessEnv;
@@ -4,7 +4,7 @@ import { dirname, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { merchantHome } from "#config/paths";
6
6
  import { packagePath } from "#shared/package-paths";
7
- export async function launchAgent(piArguments, selectedEnvironment, configPath) {
7
+ export async function launchAgent(piArguments, selectedEnvironment, configPath, permissionMode) {
8
8
  const state = merchantHome();
9
9
  await mkdir(state, { recursive: true, mode: 0o700 });
10
10
  await mkdir(resolve(state, "pi"), { recursive: true, mode: 0o700 });
@@ -19,15 +19,12 @@ export async function launchAgent(piArguments, selectedEnvironment, configPath)
19
19
  ...piArguments,
20
20
  ], {
21
21
  stdio: "inherit",
22
- env: {
23
- ...process.env,
24
- PI_CODING_AGENT_DIR: resolve(state, "pi"),
25
- MERCHANTDUO_SELECTED_ENV: selectedEnvironment,
26
- MERCHANTDUO_CONFIG_PATH: configPath ?? "",
27
- MERCHANTDUO_PACKAGE_VERSION: packageJson.version,
28
- },
22
+ env: agentEnvironment(process.env, resolve(state, "pi"), selectedEnvironment, configPath, packageJson.version, permissionMode),
29
23
  });
30
24
  child.on("error", reject);
31
25
  child.on("exit", (code) => resolveExit(code ?? 1));
32
26
  });
33
27
  }
28
+ export function agentEnvironment(base, piDirectory, selectedEnvironment, configPath, version, permissionMode) {
29
+ return { ...base, PI_CODING_AGENT_DIR: piDirectory, MERCHANTDUO_SELECTED_ENV: selectedEnvironment, MERCHANTDUO_CONFIG_PATH: configPath ?? "", MERCHANTDUO_PACKAGE_VERSION: version, ...(permissionMode ? { MERCHANTDUO_PERMISSION_MODE: permissionMode } : {}) };
30
+ }
@@ -1,12 +1,12 @@
1
1
  import { EnvironmentBackend } from "#environments/backend";
2
- import { environmentWritable } from "#environments/model";
2
+ import { resolvePermissionMode } from "#app/permission-mode";
3
3
  import { DefaultMagentoInspector } from "#magento/inspector";
4
4
  export async function doctor(cwd, name, environment, config) {
5
5
  const magento = await new DefaultMagentoInspector().inspect(new EnvironmentBackend(environment, cwd));
6
6
  console.log(JSON.stringify({
7
7
  environment: name,
8
8
  type: environment.type,
9
- writable: environmentWritable(environment),
9
+ defaultPermissionMode: resolvePermissionMode(environment),
10
10
  magento,
11
11
  activeTheme: config.activeTheme,
12
12
  knowledge: { connected: false },
@@ -6,4 +6,4 @@ export declare class TerminalInitInterview implements InitInterview {
6
6
  }
7
7
  export declare function hasWardenEnvironment(cwd: string): Promise<boolean>;
8
8
  export declare function initProject(cwd: string, interview?: InitInterview, output?: (message: string) => void): Promise<void>;
9
- export declare const remoteEnvironmentExample = "\nTo add a remote environment later, add this under environments:\n\n stage:\n type: ssh\n host: shop-stage\n root: /var/www/html\n writable: false\n\nConfigure its user, port, and identity through ~/.ssh/config:\n\n Host shop-stage\n HostName stage.example.com\n User deploy\n Port 22\n IdentityFile ~/.ssh/id_ed25519";
9
+ export declare const remoteEnvironmentExample = "\nTo add a remote environment later, add this under environments:\n\n stage:\n type: ssh\n host: shop-stage\n root: /var/www/html\n\nSSH sessions start read-only. Use /duo-switch-permissions normal or yolo only for the current session. Configure its user, port, and identity through ~/.ssh/config:\n\n Host shop-stage\n HostName stage.example.com\n User deploy\n Port 22\n IdentityFile ~/.ssh/id_ed25519";
@@ -65,4 +65,4 @@ export async function initProject(cwd, interview = new TerminalInitInterview(),
65
65
  output(`Created ${path}`);
66
66
  output(remoteEnvironmentExample);
67
67
  }
68
- export const remoteEnvironmentExample = `\nTo add a remote environment later, add this under environments:\n\n stage:\n type: ssh\n host: shop-stage\n root: /var/www/html\n writable: false\n\nConfigure its user, port, and identity through ~/.ssh/config:\n\n Host shop-stage\n HostName stage.example.com\n User deploy\n Port 22\n IdentityFile ~/.ssh/id_ed25519`;
68
+ export const remoteEnvironmentExample = `\nTo add a remote environment later, add this under environments:\n\n stage:\n type: ssh\n host: shop-stage\n root: /var/www/html\n\nSSH sessions start read-only. Use /duo-switch-permissions normal or yolo only for the current session. Configure its user, port, and identity through ~/.ssh/config:\n\n Host shop-stage\n HostName stage.example.com\n User deploy\n Port 22\n IdentityFile ~/.ssh/id_ed25519`;
package/dist/cli/main.js CHANGED
@@ -23,7 +23,7 @@ async function main() {
23
23
  }
24
24
  if (args.command === "doctor")
25
25
  return doctor(cwd, selected.name, selected.environment, loaded.config);
26
- process.exitCode = await launchAgent(args.piArguments, selected.name, loaded.configPath);
26
+ process.exitCode = await launchAgent(args.piArguments, selected.name, loaded.configPath, args.yolo ? "yolo" : undefined);
27
27
  }
28
28
  main().catch((error) => {
29
29
  console.error(`merchantduo: ${error.message}`);
@@ -93,7 +93,6 @@ export declare const ConfigSchema: z.ZodObject<{
93
93
  type: z.ZodLiteral<"ssh">;
94
94
  host: z.ZodString;
95
95
  root: z.ZodString;
96
- writable: z.ZodDefault<z.ZodBoolean>;
97
96
  testing: z.ZodOptional<z.ZodObject<{
98
97
  frontendUrl: z.ZodOptional<z.ZodString>;
99
98
  adminUrl: z.ZodOptional<z.ZodString>;
@@ -111,7 +110,6 @@ export declare const ConfigSchema: z.ZodObject<{
111
110
  type: "ssh";
112
111
  root: string;
113
112
  host: string;
114
- writable: boolean;
115
113
  testing?: {
116
114
  allowInsecureTls: boolean;
117
115
  frontendUrl?: string | undefined;
@@ -126,7 +124,6 @@ export declare const ConfigSchema: z.ZodObject<{
126
124
  adminUrl?: string | undefined;
127
125
  allowInsecureTls?: boolean | undefined;
128
126
  } | undefined;
129
- writable?: boolean | undefined;
130
127
  }>]>>>;
131
128
  }, "strict", z.ZodTypeAny, {
132
129
  testing: {
@@ -158,7 +155,6 @@ export declare const ConfigSchema: z.ZodObject<{
158
155
  type: "ssh";
159
156
  root: string;
160
157
  host: string;
161
- writable: boolean;
162
158
  testing?: {
163
159
  allowInsecureTls: boolean;
164
160
  frontendUrl?: string | undefined;
@@ -202,7 +198,6 @@ export declare const ConfigSchema: z.ZodObject<{
202
198
  adminUrl?: string | undefined;
203
199
  allowInsecureTls?: boolean | undefined;
204
200
  } | undefined;
205
- writable?: boolean | undefined;
206
201
  }> | undefined;
207
202
  }>;
208
203
  export type MerchantConfig = Omit<z.infer<typeof ConfigSchema>, "environments"> & {
@@ -22,7 +22,6 @@ const ssh = z
22
22
  type: z.literal("ssh"),
23
23
  host: z.string().min(1),
24
24
  root: z.string(),
25
- writable: z.boolean().default(false),
26
25
  testing: testing.optional(),
27
26
  })
28
27
  .strict();
@@ -2,4 +2,5 @@ import type { CommandSpec } from "#environments/executor";
2
2
  export interface EnvironmentAdapter {
3
3
  path(path: string): string;
4
4
  command(argv: string[], target?: string): CommandSpec;
5
+ lifecycleCommand?(action: "status" | "start" | "stop"): CommandSpec;
5
6
  }
@@ -10,4 +10,9 @@ export declare class WardenAdapter implements EnvironmentAdapter {
10
10
  args: string[];
11
11
  cwd: string;
12
12
  };
13
+ lifecycleCommand(action: "status" | "start" | "stop"): {
14
+ file: string;
15
+ args: string[];
16
+ cwd: string;
17
+ };
13
18
  }
@@ -21,4 +21,11 @@ export class WardenAdapter {
21
21
  cwd: resolve(this.cwd, this.environment.projectRoot),
22
22
  };
23
23
  }
24
+ lifecycleCommand(action) {
25
+ // Lifecycle commands run on the host in the Warden project, never in php-fpm.
26
+ const actionArgs = action === "status"
27
+ ? ["env", "ps", "--status", "running", "--services"]
28
+ : ["env", action];
29
+ return { file: "warden", args: actionArgs, cwd: resolve(this.cwd, this.environment.projectRoot) };
30
+ }
24
31
  }
@@ -1,4 +1,5 @@
1
1
  import type { Environment } from "#environments/model";
2
+ import type { EnvironmentStatus } from "#environments/model";
2
3
  import type { CommandExecutor, CommandResult, CommandSpec, RunOptions } from "#environments/executor";
3
4
  export { contain } from "#environments/path-policy";
4
5
  /** Concrete local/Warden/SSH command adapter selected once for a session. */
@@ -10,4 +11,10 @@ export declare class EnvironmentBackend implements CommandExecutor {
10
11
  path(path: string): string;
11
12
  command(argv: string[], target?: string): CommandSpec;
12
13
  run(argv: string[], options?: RunOptions): Promise<CommandResult>;
14
+ lifecycleCommand(action: "status" | "start" | "stop"): CommandSpec | undefined;
15
+ status(): Promise<EnvironmentStatus>;
16
+ start(): Promise<CommandResult | undefined>;
17
+ stop(): Promise<CommandResult | undefined>;
13
18
  }
19
+ /** `warden env ps --status running --services` prints one service per running container. */
20
+ export declare function wardenRunning(stdout: string): boolean;