@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.
- package/README.md +26 -120
- package/dist/app/permission-mode.d.ts +5 -0
- package/dist/app/permission-mode.js +10 -0
- package/dist/app/runtime.d.ts +11 -1
- package/dist/app/runtime.js +52 -4
- package/dist/app/status.js +9 -7
- package/dist/app/system-prompt.js +7 -1
- package/dist/cli/arguments.d.ts +1 -0
- package/dist/cli/arguments.js +7 -1
- package/dist/cli/commands/agent.d.ts +3 -1
- package/dist/cli/commands/agent.js +5 -8
- package/dist/cli/commands/doctor.js +2 -2
- package/dist/cli/commands/init.d.ts +1 -1
- package/dist/cli/commands/init.js +1 -1
- package/dist/cli/main.js +1 -1
- package/dist/config/schema.d.ts +0 -5
- package/dist/config/schema.js +0 -1
- package/dist/environments/adapter.d.ts +1 -0
- package/dist/environments/adapters/warden.d.ts +5 -0
- package/dist/environments/adapters/warden.js +7 -0
- package/dist/environments/backend.d.ts +7 -0
- package/dist/environments/backend.js +31 -0
- package/dist/environments/model.d.ts +2 -2
- package/dist/environments/model.js +1 -3
- package/dist/features/magerun2/index.d.ts +2 -0
- package/dist/features/magerun2/index.js +2 -0
- package/dist/features/magerun2/model.d.ts +10 -0
- package/dist/features/magerun2/model.js +1 -0
- package/dist/features/magerun2/service.d.ts +12 -0
- package/dist/{testing/magerun.js → features/magerun2/service.js} +34 -13
- package/dist/integrations/pi/context.js +13 -7
- package/dist/integrations/pi/environment.d.ts +3 -0
- package/dist/integrations/pi/environment.js +64 -0
- package/dist/integrations/pi/magerun2.d.ts +4 -0
- package/dist/integrations/pi/magerun2.js +35 -0
- package/dist/integrations/pi/optional-services.d.ts +9 -0
- package/dist/integrations/pi/optional-services.js +79 -0
- package/dist/integrations/pi/permissions.d.ts +4 -2
- package/dist/integrations/pi/permissions.js +48 -21
- package/dist/integrations/pi/testing.js +8 -10
- package/dist/integrations/pi/theme.d.ts +2 -0
- package/dist/integrations/pi/theme.js +23 -4
- package/dist/integrations/pi/workflows.js +1 -1
- package/dist/magento/prompt.d.ts +2 -1
- package/dist/magento/prompt.js +2 -2
- package/dist/testing/discovery.d.ts +0 -1
- package/dist/testing/discovery.js +1 -4
- package/dist/testing/host.d.ts +1 -0
- package/dist/testing/host.js +14 -3
- package/dist/testing/index.d.ts +0 -1
- package/dist/testing/index.js +0 -1
- package/dist/testing/model.d.ts +0 -1
- package/package.json +5 -1
- package/skills/magerun2/SKILL.md +22 -0
- 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
|
|
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
|
-
##
|
|
5
|
+
## Overview
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
15
|
+
## Commands and tools
|
|
18
16
|
|
|
19
|
-
|
|
17
|
+
Extension slash commands are namespaced with `duo-` to avoid collisions with other Pi extensions.
|
|
20
18
|
|
|
21
|
-
|
|
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
|
-
##
|
|
22
|
+
## Environments
|
|
24
23
|
|
|
25
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
63
|
+
## Provisioning
|
|
106
64
|
|
|
107
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
87
|
+
## Safety and optional services
|
|
177
88
|
|
|
178
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/app/runtime.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/app/runtime.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { loadConfig, selectEnvironment, setProjectTheme, } from "#config/index";
|
|
2
2
|
import { EnvironmentBackend } from "#environments/backend";
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
}
|
package/dist/app/status.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export function refreshStatus(ctx, state) {
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
?
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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.
|
|
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
|
}
|
package/dist/cli/arguments.d.ts
CHANGED
package/dist/cli/arguments.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
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}`);
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -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"> & {
|
package/dist/config/schema.js
CHANGED
|
@@ -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;
|