@merchantduo/code 0.2.0-beta.3 → 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 +3 -4
- package/dist/app/permission-mode.d.ts +5 -0
- package/dist/app/permission-mode.js +10 -0
- package/dist/app/runtime.d.ts +3 -1
- package/dist/app/runtime.js +7 -3
- package/dist/app/status.js +1 -7
- package/dist/app/system-prompt.js +2 -0
- 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/model.d.ts +0 -2
- package/dist/environments/model.js +1 -3
- package/dist/features/magerun2/service.js +1 -1
- package/dist/integrations/pi/context.js +3 -3
- package/dist/integrations/pi/magerun2.d.ts +1 -1
- package/dist/integrations/pi/magerun2.js +2 -4
- package/dist/integrations/pi/permissions.d.ts +4 -2
- package/dist/integrations/pi/permissions.js +46 -21
- package/dist/integrations/pi/testing.js +0 -2
- 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/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,13 +10,13 @@ MerchantDuo is a Pi coding agent for Magento 2.4 teams. It starts from the store
|
|
|
10
10
|
- Provides guided local/Warden store provisioning.
|
|
11
11
|
- Keeps remote writes and Magento operations behind explicit execution boundaries.
|
|
12
12
|
|
|
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,
|
|
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.
|
|
14
14
|
|
|
15
15
|
## Commands and tools
|
|
16
16
|
|
|
17
17
|
Extension slash commands are namespaced with `duo-` to avoid collisions with other Pi extensions.
|
|
18
18
|
|
|
19
|
-
- [Command reference](docs/commands.md) covers
|
|
19
|
+
- [Command reference](docs/commands.md) covers session theme and permission switching, lifecycle, and navigation commands.
|
|
20
20
|
- [Tool reference](docs/tools.md) covers workspace, environment, Magento workflow, magerun2, testing, PHP-console, and optional knowledge tools, including parameters and confirmation boundaries.
|
|
21
21
|
|
|
22
22
|
## Environments
|
|
@@ -46,10 +46,9 @@ environments:
|
|
|
46
46
|
type: ssh
|
|
47
47
|
host: shop-stage
|
|
48
48
|
root: /var/www/html
|
|
49
|
-
writable: false
|
|
50
49
|
```
|
|
51
50
|
|
|
52
|
-
Local uses the checkout directly. Warden routes project and Magento execution through its configured service. SSH
|
|
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:
|
|
53
52
|
|
|
54
53
|
```sshconfig
|
|
55
54
|
Host shop-stage
|
|
@@ -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,6 +1,7 @@
|
|
|
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";
|
|
4
5
|
import type { EnvironmentStatus } from "#environments/model";
|
|
5
6
|
import { type MagentoInspector, type MagentoSnapshot } from "#magento/index";
|
|
6
7
|
import { ChangeTracker } from "#workflows/change-tracker";
|
|
@@ -15,6 +16,7 @@ export type SessionState = {
|
|
|
15
16
|
};
|
|
16
17
|
backend: EnvironmentBackend;
|
|
17
18
|
environmentStatus: EnvironmentStatus;
|
|
19
|
+
permissionMode: PermissionMode;
|
|
18
20
|
magento: MagentoSnapshot;
|
|
19
21
|
testing?: TestingSnapshot;
|
|
20
22
|
magerun2?: Magerun2Capability;
|
|
@@ -29,7 +31,7 @@ export declare class MerchantDuoRuntime {
|
|
|
29
31
|
resetInjection(): void;
|
|
30
32
|
injectionKey(skillId: string, theme: string): string;
|
|
31
33
|
changeTheme(cwd: string, activeTheme: string): Promise<SessionState>;
|
|
32
|
-
|
|
34
|
+
setPermissionMode(mode: PermissionMode): SessionState;
|
|
33
35
|
setTesting(snapshot: TestingSnapshot): void;
|
|
34
36
|
setMagerun2(capability: Magerun2Capability): void;
|
|
35
37
|
setEnvironmentStatus(status: EnvironmentStatus): SessionState;
|
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 {
|
|
@@ -26,6 +26,7 @@ export class MerchantDuoRuntime {
|
|
|
26
26
|
selected,
|
|
27
27
|
backend,
|
|
28
28
|
environmentStatus,
|
|
29
|
+
permissionMode: resolvePermissionMode(selected.environment, process.env.MERCHANTDUO_PERMISSION_MODE),
|
|
29
30
|
magento: environmentStatus === "running" ? await this.inspector.inspect(backend) : stoppedMagentoSnapshot(),
|
|
30
31
|
};
|
|
31
32
|
return this.#state;
|
|
@@ -47,8 +48,11 @@ export class MerchantDuoRuntime {
|
|
|
47
48
|
this.resetInjection();
|
|
48
49
|
return state;
|
|
49
50
|
}
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
setPermissionMode(mode) {
|
|
52
|
+
if (!this.#state)
|
|
53
|
+
throw new Error("Session has not started");
|
|
54
|
+
this.#state.permissionMode = mode;
|
|
55
|
+
return this.#state;
|
|
52
56
|
}
|
|
53
57
|
setTesting(snapshot) {
|
|
54
58
|
if (!this.#state)
|
package/dist/app/status.js
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
export function refreshStatus(ctx, state) {
|
|
2
|
-
const environment = state.selected.environment;
|
|
3
|
-
const writePolicy = environment.type === "ssh"
|
|
4
|
-
? environment.writable
|
|
5
|
-
? "✎+?"
|
|
6
|
-
: "RO"
|
|
7
|
-
: "✎";
|
|
8
2
|
const parts = [
|
|
9
3
|
`MerchantDuo ${process.env.MERCHANTDUO_PACKAGE_VERSION ?? "?"}`,
|
|
10
4
|
`M${state.magento.version ?? "?"}`,
|
|
11
5
|
`${state.selected.name} (${state.environmentStatus})`,
|
|
12
|
-
|
|
6
|
+
state.permissionMode === "read-only" ? "r-o" : state.permissionMode,
|
|
13
7
|
...(state.config.activeTheme === "all" ? [] : [state.config.activeTheme]),
|
|
14
8
|
...(state.testing?.frontendUrl ? [state.testing.frontendUrl] : []),
|
|
15
9
|
];
|
|
@@ -13,6 +13,8 @@ After every change, inspect the changed module and files and choose the smallest
|
|
|
13
13
|
|
|
14
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
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
|
+
|
|
16
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.
|
|
17
19
|
|
|
18
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.`;
|
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
|
@@ -22,9 +22,7 @@ export type SshEnvironment = {
|
|
|
22
22
|
type: "ssh";
|
|
23
23
|
host: string;
|
|
24
24
|
root: string;
|
|
25
|
-
writable: boolean;
|
|
26
25
|
testing?: EnvironmentTesting;
|
|
27
26
|
};
|
|
28
27
|
export type Environment = LocalEnvironment | WardenEnvironment | SshEnvironment;
|
|
29
|
-
export declare function environmentWritable(environment: Environment): boolean;
|
|
30
28
|
export {};
|
|
@@ -19,7 +19,7 @@ export async function discoverMagerun2(environment, backend, cwd) {
|
|
|
19
19
|
return { available: true, executable };
|
|
20
20
|
}
|
|
21
21
|
if (environment.type === "ssh")
|
|
22
|
-
return { available: false, diagnostic:
|
|
22
|
+
return { available: false, diagnostic: "Install n98-magerun2 on the remote host." };
|
|
23
23
|
try {
|
|
24
24
|
return { available: true, executable: await installMagerun2(merchantHome(), cwd) };
|
|
25
25
|
}
|
|
@@ -11,7 +11,7 @@ export default function context(pi) {
|
|
|
11
11
|
if (state.environmentStatus === "stopped") {
|
|
12
12
|
refreshStatus(ctx, state);
|
|
13
13
|
sessionEvents.emit("merchantduo.context", state);
|
|
14
|
-
pi.appendEntry("merchantduo.context", { environment: state.selected.name, environmentStatus: state.environmentStatus });
|
|
14
|
+
pi.appendEntry("merchantduo.context", { environment: state.selected.name, environmentStatus: state.environmentStatus, permissionMode: state.permissionMode });
|
|
15
15
|
return;
|
|
16
16
|
}
|
|
17
17
|
const magerun2 = await discoverMagerun2(state.selected.environment, state.backend, ctx.cwd);
|
|
@@ -27,7 +27,7 @@ export default function context(pi) {
|
|
|
27
27
|
runtime.setTesting(await discoverTesting(state.selected.environment, state.backend, ctx.cwd, { ...state.config.testing, ...state.selected.environment.testing, allowInsecureTls: state.selected.environment.testing?.allowInsecureTls ?? state.config.testing.allowInsecureTls }, { http, browser }));
|
|
28
28
|
refreshStatus(ctx, state);
|
|
29
29
|
sessionEvents.emit("merchantduo.context", state);
|
|
30
|
-
pi.appendEntry("merchantduo.context", { environment: state.selected.name, environmentStatus: state.environmentStatus, testing: state.testing, magerun2: state.magerun2 });
|
|
30
|
+
pi.appendEntry("merchantduo.context", { environment: state.selected.name, environmentStatus: state.environmentStatus, permissionMode: state.permissionMode, testing: state.testing, magerun2: state.magerun2 });
|
|
31
31
|
});
|
|
32
32
|
pi.on("session_compact", () => runtime.resetInjection());
|
|
33
33
|
pi.on("session_shutdown", () => sessionEvents.clear());
|
|
@@ -47,6 +47,6 @@ export default function context(pi) {
|
|
|
47
47
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
48
48
|
const state = await runtime.boot(ctx.cwd);
|
|
49
49
|
const testing = state.testing;
|
|
50
|
-
return { systemPrompt: `${event.systemPrompt}\n${merchantDuoSystemPrompt()}\nEnvironment: ${state.selected.name} is ${state.environmentStatus}. ${state.environmentStatus === "stopped" ? "Use environment_start only after the user directly asks to start it; do not use project tools until it is running." : "Testing URLs are discovered but unverified. Frontend: " + (testing?.frontendUrl ?? "unavailable") + "; Admin: " + (testing?.adminUrl ?? "unavailable") + ". HTTP, browser, navigation, and test artifacts are host-only; never use environment bash to locate or repair their host paths. Use explicit test tools only when relevant."}` };
|
|
50
|
+
return { systemPrompt: `${event.systemPrompt}\n${merchantDuoSystemPrompt()}\nEnvironment: ${state.selected.name} is ${state.environmentStatus}; permission mode=${state.permissionMode}. ${state.environmentStatus === "stopped" ? "Use environment_start only after the user directly asks to start it; do not use project tools until it is running." : "Testing URLs are discovered but unverified. Frontend: " + (testing?.frontendUrl ?? "unavailable") + "; Admin: " + (testing?.adminUrl ?? "unavailable") + ". HTTP, browser, navigation, and test artifacts are host-only; never use environment bash to locate or repair their host paths. Use explicit test tools only when relevant."}` };
|
|
51
51
|
});
|
|
52
52
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
/** Generic magerun2 is intentionally separate from the typed PHP console. */
|
|
3
3
|
export default function magerun2(pi: ExtensionAPI): void;
|
|
4
|
-
export declare function magerun2Refusal(args: string[]
|
|
4
|
+
export declare function magerun2Refusal(args: string[]): string | undefined;
|
|
@@ -14,7 +14,7 @@ export default function magerun2(pi) {
|
|
|
14
14
|
}),
|
|
15
15
|
async execute(_id, params, signal, _update, ctx) {
|
|
16
16
|
const state = await runtime.boot(ctx.cwd);
|
|
17
|
-
const refusal = magerun2Refusal(params.args
|
|
17
|
+
const refusal = magerun2Refusal(params.args);
|
|
18
18
|
if (refusal)
|
|
19
19
|
return text(refusal);
|
|
20
20
|
const capability = state.magerun2;
|
|
@@ -28,10 +28,8 @@ export default function magerun2(pi) {
|
|
|
28
28
|
},
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
|
-
export function magerun2Refusal(args
|
|
31
|
+
export function magerun2Refusal(args) {
|
|
32
32
|
if (args.includes("dev:console"))
|
|
33
33
|
return "Refused: dev:console belongs to magento_php_repl, which accepts a typed snippet and optional area.";
|
|
34
|
-
if (readOnlySsh)
|
|
35
|
-
return "Refused: generic magerun2 is unavailable for read-only SSH because arbitrary subcommands cannot be classified safely.";
|
|
36
34
|
return undefined;
|
|
37
35
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type PermissionMode } from "#app/permission-mode";
|
|
2
3
|
import type { Environment } from "#environments/model";
|
|
3
4
|
export declare function requiresExecuteConfirmation(toolName: string, input: Record<string, unknown>): boolean;
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
/** Return block/confirmation behavior; callers retain all hard capability constraints. */
|
|
6
|
+
export declare function permissionDecision(mode: PermissionMode, environment: Environment, toolName: string, input: Record<string, unknown>): "allow" | "block" | "confirm";
|
|
7
|
+
/** The sole MerchantDuo confirmation gate. YOLO removes dialogs, never hard constraints. */
|
|
6
8
|
export default function permissions(pi: ExtensionAPI): void;
|
|
@@ -1,37 +1,62 @@
|
|
|
1
|
+
import { permissionModes } from "#app/permission-mode";
|
|
2
|
+
import { refreshStatus } from "#app/status";
|
|
1
3
|
import { runtime } from "#integrations/pi/session";
|
|
4
|
+
const workspaceMutations = new Set(["write", "edit"]);
|
|
5
|
+
const stoppedAllowed = new Set(["environment_status", "environment_start", "environment_stop", "environment_set_status"]);
|
|
2
6
|
export function requiresExecuteConfirmation(toolName, input) {
|
|
3
7
|
return ["environment_start", "environment_stop"].includes(toolName) || ((toolName === "mage2gen_generate_module" || toolName === "magento_workflow" || toolName === "magerun2") && input.execute === true);
|
|
4
8
|
}
|
|
5
|
-
|
|
6
|
-
|
|
9
|
+
/** Return block/confirmation behavior; callers retain all hard capability constraints. */
|
|
10
|
+
export function permissionDecision(mode, environment, toolName, input) {
|
|
11
|
+
if (mode === "yolo")
|
|
12
|
+
return "allow";
|
|
13
|
+
if (mode === "read-only" && workspaceMutations.has(toolName))
|
|
14
|
+
return "block";
|
|
15
|
+
if (mode === "read-only" && (toolName === "bash" || toolName === "magento_php_repl" || requiresExecuteConfirmation(toolName, input)))
|
|
16
|
+
return "confirm";
|
|
17
|
+
if (requiresExecuteConfirmation(toolName, input))
|
|
18
|
+
return "confirm";
|
|
19
|
+
if (mode === "normal" && environment.type === "ssh" && (workspaceMutations.has(toolName) || toolName === "bash"))
|
|
20
|
+
return "confirm";
|
|
21
|
+
return "allow";
|
|
7
22
|
}
|
|
8
|
-
|
|
23
|
+
async function confirm(ctx, title, message) {
|
|
24
|
+
return ctx.hasUI && await ctx.ui.confirm(title, message);
|
|
25
|
+
}
|
|
26
|
+
/** The sole MerchantDuo confirmation gate. YOLO removes dialogs, never hard constraints. */
|
|
9
27
|
export default function permissions(pi) {
|
|
28
|
+
pi.registerCommand("duo-switch-permissions", { description: "Set this session's permission mode: read-only, normal, or yolo.", handler: async (args, ctx) => {
|
|
29
|
+
const mode = args.trim();
|
|
30
|
+
if (!permissionModes.includes(mode)) {
|
|
31
|
+
ctx.ui.notify("Choose one of: read-only, normal, yolo", "error");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
await runtime.boot(ctx.cwd);
|
|
35
|
+
const state = runtime.setPermissionMode(mode);
|
|
36
|
+
refreshStatus(ctx, state);
|
|
37
|
+
ctx.ui.notify(`Session permission mode set to ${mode}. It will reset when this session ends.`, "info");
|
|
38
|
+
} });
|
|
10
39
|
pi.on("tool_call", async (event, ctx) => {
|
|
11
40
|
const state = await runtime.boot(ctx.cwd);
|
|
12
|
-
|
|
13
|
-
if (state.environmentStatus === "stopped" && !["environment_status", "environment_start", "environment_stop", "environment_set_status"].includes(event.toolName))
|
|
41
|
+
if (state.environmentStatus === "stopped" && !stoppedAllowed.has(event.toolName))
|
|
14
42
|
return { block: true, reason: "MerchantDuo: selected environment is stopped. Start it first." };
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const ok = !requiresSshConfirmation(state.selected.environment, event.toolName) || (ctx.hasUI && await ctx.ui.confirm("SSH workspace action", `Run ${event.toolName} on ${state.selected.environment.host}?`));
|
|
23
|
-
return ok ? undefined : { block: true, reason: "Cancelled: SSH confirmation required." };
|
|
24
|
-
}
|
|
43
|
+
const decision = permissionDecision(state.permissionMode, state.selected.environment, event.toolName, event.input);
|
|
44
|
+
if (decision === "allow")
|
|
45
|
+
return;
|
|
46
|
+
if (decision === "block")
|
|
47
|
+
return { block: true, reason: "MerchantDuo: read-only mode blocks direct workspace writes and edits." };
|
|
48
|
+
const ok = await confirm(ctx, "Confirm Magento action", `Run ${event.toolName} in ${state.permissionMode} mode?`);
|
|
49
|
+
return ok ? undefined : { block: true, reason: "Cancelled: confirmation required." };
|
|
25
50
|
});
|
|
26
51
|
pi.on("user_bash", async (event, ctx) => {
|
|
27
52
|
const state = await runtime.boot(ctx.cwd);
|
|
28
|
-
if (state.
|
|
29
|
-
return;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const ok = ctx.hasUI && await ctx.ui.confirm("SSH command", `Run command on ${state.selected.environment.host}?`);
|
|
33
|
-
if (!ok)
|
|
53
|
+
if (state.environmentStatus === "stopped")
|
|
54
|
+
return { result: { output: "MerchantDuo: selected environment is stopped. Start it first.", exitCode: 1, cancelled: true, truncated: false } };
|
|
55
|
+
const decision = state.permissionMode === "yolo" ? "allow" : state.permissionMode === "read-only" || state.selected.environment.type === "ssh" ? "confirm" : "allow";
|
|
56
|
+
if (decision === "confirm" && !(await confirm(ctx, "Confirm shell command", `Run command in ${state.permissionMode} mode?`)))
|
|
34
57
|
return { result: { output: "Cancelled: confirmation required.", exitCode: 1, cancelled: true, truncated: false } };
|
|
58
|
+
if (decision === "allow" && state.selected.environment.type !== "ssh" && state.permissionMode !== "read-only")
|
|
59
|
+
return;
|
|
35
60
|
const result = await state.backend.run(["sh", "-lc", event.command]);
|
|
36
61
|
return { result: { output: `${result.stdout}${result.stderr}`, exitCode: result.exitCode, cancelled: false, truncated: result.truncated } };
|
|
37
62
|
});
|
|
@@ -56,8 +56,6 @@ export default function testing(pi) {
|
|
|
56
56
|
} });
|
|
57
57
|
pi.registerTool({ name: "magento_php_repl", label: "Magento PHP console", description: "Explicit one-shot magerun2 PHP snippet execution.", parameters: Type.Object({ snippet: Type.String({ minLength: 1 }), area: Type.Optional(Type.Union([Type.Literal("adminhtml"), Type.Literal("frontend"), Type.Literal("crontab"), Type.Literal("webapi_rest"), Type.Literal("graphql")])) }), async execute(_id, params, signal, _update, ctx) {
|
|
58
58
|
const state = await runtime.boot(ctx.cwd);
|
|
59
|
-
if (state.selected.environment.type === "ssh" && !state.selected.environment.writable)
|
|
60
|
-
return text("PHP REPL is unavailable for read-only SSH.");
|
|
61
59
|
const executable = state.magerun2?.executable;
|
|
62
60
|
if (!executable)
|
|
63
61
|
return text(state.magerun2?.diagnostic ?? "PHP REPL unavailable.");
|
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
import { refreshStatus } from "#app/status";
|
|
2
2
|
import { runtime } from "#integrations/pi/session";
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
export function themeChoices(state) {
|
|
4
|
+
return ["all", "any", ...state.magento.themes.map((theme) => theme.code)];
|
|
5
|
+
}
|
|
6
|
+
export default function theme(pi) {
|
|
7
|
+
pi.registerCommand("duo-switch-theme", {
|
|
8
|
+
description: "Choose a detected Magento theme, all, or any.",
|
|
9
|
+
handler: async (args, ctx) => {
|
|
10
|
+
const state = await runtime.boot(ctx.cwd);
|
|
11
|
+
const choices = themeChoices(state);
|
|
12
|
+
const typed = args.trim();
|
|
13
|
+
const value = typed || (ctx.hasUI ? await ctx.ui.select("Select Magento theme scope", choices) : undefined);
|
|
14
|
+
if (!value)
|
|
15
|
+
return;
|
|
16
|
+
if (!choices.includes(value)) {
|
|
17
|
+
ctx.ui.notify(`Choose one of: ${choices.join(", ")}`, "error");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const changed = await runtime.changeTheme(ctx.cwd, value);
|
|
21
|
+
refreshStatus(ctx, changed);
|
|
22
|
+
ctx.ui.notify(`Theme scope set to ${value}.`, "info");
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -6,7 +6,7 @@ export default function workflows(pi) {
|
|
|
6
6
|
pi.registerTool({ name: "magento_workflow", label: "Magento workflow", description: "Preview or explicitly execute a supported Magento operational action.", parameters: Type.Object({ action: Type.String(), execute: Type.Optional(Type.Boolean()) }), async execute(_id, params, _signal, _update, ctx) {
|
|
7
7
|
const state = await runtime.boot(ctx.cwd);
|
|
8
8
|
if (params.action === "status")
|
|
9
|
-
return text(JSON.stringify({ snapshot: state.magento, testing: state.testing, magerun2: state.magerun2, pendingFiles: runtime.changes.files(), workflows: builtInWorkflows.map(({ id, description }) => ({ id, description })) }, null, 2));
|
|
9
|
+
return text(JSON.stringify({ snapshot: state.magento, permissionMode: state.permissionMode, testing: state.testing, magerun2: state.magerun2, pendingFiles: runtime.changes.files(), workflows: builtInWorkflows.map(({ id, description }) => ({ id, description })) }, null, 2));
|
|
10
10
|
if (params.action === "syntax-check")
|
|
11
11
|
return text((await syntaxCheck(state.backend, runtime.changes.files())).join("\n") || "No supported changed files require syntax checks.");
|
|
12
12
|
const workflow = builtInWorkflows.find(({ id }) => id === params.action);
|
package/dist/magento/prompt.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import type { MagentoSnapshot } from "#magento/model";
|
|
2
|
-
|
|
2
|
+
import type { PermissionMode } from "#app/permission-mode";
|
|
3
|
+
export declare function snapshotPrompt(envName: string, permissionMode: PermissionMode, status: string, snapshot: MagentoSnapshot): string;
|
package/dist/magento/prompt.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export function snapshotPrompt(envName,
|
|
2
|
-
return `[MerchantDuo: environment=${envName};
|
|
1
|
+
export function snapshotPrompt(envName, permissionMode, status, snapshot) {
|
|
2
|
+
return `[MerchantDuo: environment=${envName}; permission mode=${permissionMode}; Magento=${snapshot.edition ?? "unknown"} ${snapshot.version ?? "unknown"}; PHP=${snapshot.phpVersion ?? "unknown"}; mode=${snapshot.mode ?? "unknown"}; knowledge=${status}; pending actions are controlled by magento_workflow. Do not inspect app/etc/env.php or credentials.${snapshot.warnings.length ? ` Warnings: ${snapshot.warnings.join("; ")}` : ""}]`;
|
|
3
3
|
}
|