@merchantduo/code 0.1.0-beta.0 → 0.2.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -99
- package/dist/app/runtime.d.ts +30 -0
- package/dist/app/runtime.js +56 -0
- package/dist/app/status.d.ts +3 -0
- package/dist/app/status.js +9 -0
- package/dist/app/system-prompt.d.ts +2 -0
- package/dist/{skills.js → app/system-prompt.js} +3 -32
- package/dist/cli/arguments.d.ts +10 -0
- package/dist/cli/arguments.js +34 -0
- package/dist/cli/commands/agent.d.ts +1 -0
- package/dist/cli/commands/agent.js +33 -0
- package/dist/cli/commands/doctor.d.ts +3 -0
- package/dist/cli/commands/doctor.js +14 -0
- package/dist/cli/commands/init.d.ts +9 -0
- package/dist/cli/commands/init.js +68 -0
- package/dist/cli/commands/provision.d.ts +6 -0
- package/dist/cli/commands/provision.js +35 -0
- package/dist/cli/main.js +31 -0
- package/dist/config/index.d.ts +11 -0
- package/dist/config/index.js +15 -0
- package/dist/config/loader.d.ts +10 -0
- package/dist/config/loader.js +61 -0
- package/dist/config/paths.d.ts +1 -0
- package/dist/config/paths.js +7 -0
- package/dist/config/project-store.d.ts +2 -0
- package/dist/config/project-store.js +12 -0
- package/dist/config/schema.d.ts +210 -0
- package/dist/config/schema.js +36 -0
- package/dist/config/trust-store.d.ts +3 -0
- package/dist/config/trust-store.js +29 -0
- package/dist/environments/adapter.d.ts +5 -0
- package/dist/environments/adapters/local.d.ts +12 -0
- package/dist/environments/adapters/local.js +14 -0
- package/dist/environments/adapters/ssh.d.ts +11 -0
- package/dist/environments/adapters/ssh.js +24 -0
- package/dist/environments/adapters/warden.d.ts +13 -0
- package/dist/environments/adapters/warden.js +24 -0
- package/dist/environments/backend.d.ts +13 -0
- package/dist/environments/backend.js +73 -0
- package/dist/environments/executor.d.ts +23 -0
- package/dist/environments/factory.d.ts +3 -0
- package/dist/environments/factory.js +13 -0
- package/dist/environments/model.d.ts +28 -0
- package/dist/environments/model.js +3 -0
- package/dist/environments/path-policy.d.ts +1 -0
- package/dist/environments/path-policy.js +8 -0
- package/dist/{mage2gen.d.ts → features/mage2gen/service.d.ts} +0 -1
- package/dist/{mage2gen.js → features/mage2gen/service.js} +8 -3
- package/dist/integrations/pi/context.d.ts +2 -0
- package/dist/integrations/pi/context.js +46 -0
- package/dist/integrations/pi/mage2gen.d.ts +2 -0
- package/dist/integrations/pi/mage2gen.js +19 -0
- package/dist/integrations/pi/permissions.d.ts +6 -0
- package/dist/integrations/pi/permissions.js +36 -0
- package/dist/integrations/pi/session.d.ts +12 -0
- package/dist/integrations/pi/session.js +9 -0
- package/dist/integrations/pi/testing.d.ts +2 -0
- package/dist/integrations/pi/testing.js +88 -0
- package/dist/integrations/pi/theme.d.ts +2 -0
- package/dist/integrations/pi/theme.js +6 -0
- package/dist/integrations/pi/workflows.d.ts +2 -0
- package/dist/integrations/pi/workflows.js +26 -0
- package/dist/integrations/pi/workspace.d.ts +5 -0
- package/dist/integrations/pi/workspace.js +33 -0
- package/dist/magento/deploy-mode.d.ts +2 -0
- package/dist/magento/deploy-mode.js +10 -0
- package/dist/magento/index.d.ts +5 -0
- package/dist/magento/index.js +4 -0
- package/dist/magento/inspector.d.ts +9 -0
- package/dist/magento/inspector.js +56 -0
- package/dist/magento/model.d.ts +25 -0
- package/dist/magento/prompt.d.ts +2 -0
- package/dist/magento/prompt.js +3 -0
- package/dist/magento/themes.d.ts +3 -0
- package/dist/magento/themes.js +46 -0
- package/dist/provision/advisor.d.ts +12 -0
- package/dist/provision/advisor.js +28 -0
- package/dist/provision/discovery.d.ts +12 -0
- package/dist/provision/discovery.js +61 -0
- package/dist/provision/interview.d.ts +9 -0
- package/dist/provision/interview.js +29 -0
- package/dist/provision/model.d.ts +82 -0
- package/dist/provision/model.js +5 -0
- package/dist/provision/planner.d.ts +4 -0
- package/dist/provision/planner.js +51 -0
- package/dist/provision/runner.d.ts +35 -0
- package/dist/provision/runner.js +98 -0
- package/dist/shared/package-paths.d.ts +3 -0
- package/dist/shared/package-paths.js +10 -0
- package/dist/testing/discovery.d.ts +10 -0
- package/dist/testing/discovery.js +68 -0
- package/dist/testing/host.d.ts +29 -0
- package/dist/testing/host.js +50 -0
- package/dist/testing/index.d.ts +5 -0
- package/dist/testing/index.js +5 -0
- package/dist/testing/magerun.d.ts +8 -0
- package/dist/testing/magerun.js +73 -0
- package/dist/testing/model.d.ts +18 -0
- package/dist/testing/url.d.ts +4 -0
- package/dist/testing/url.js +26 -0
- package/dist/workflows/builtins.d.ts +2 -0
- package/dist/workflows/builtins.js +38 -0
- package/dist/workflows/change-tracker.d.ts +9 -0
- package/dist/workflows/change-tracker.js +38 -0
- package/dist/workflows/definition.d.ts +7 -0
- package/dist/workflows/index.d.ts +4 -0
- package/dist/workflows/index.js +3 -0
- package/dist/workflows/runner.d.ts +7 -0
- package/dist/workflows/runner.js +28 -0
- package/package.json +32 -9
- package/skills/magento-testing/SKILL.md +10 -0
- package/dist/backend.d.ts +0 -24
- package/dist/backend.js +0 -103
- package/dist/cli.js +0 -89
- package/dist/config.d.ts +0 -127
- package/dist/config.js +0 -170
- package/dist/config.test.js +0 -51
- package/dist/constants.d.ts +0 -20
- package/dist/constants.js +0 -15
- package/dist/mage2gen.test.js +0 -6
- package/dist/magento.d.ts +0 -40
- package/dist/magento.js +0 -119
- package/dist/magento.test.js +0 -31
- package/dist/skills.d.ts +0 -11
- package/dist/skills.test.js +0 -34
- package/dist/workflow.d.ts +0 -20
- package/dist/workflow.js +0 -67
- package/dist/workflow.test.js +0 -17
- package/extensions/merchantduo.js +0 -311
- /package/dist/{cli.d.ts → cli/main.d.ts} +0 -0
- /package/dist/{config.test.d.ts → environments/adapter.js} +0 -0
- /package/dist/{mage2gen.test.d.ts → environments/executor.js} +0 -0
- /package/dist/{magento.test.d.ts → magento/model.js} +0 -0
- /package/dist/{skills.test.d.ts → testing/model.js} +0 -0
- /package/dist/{workflow.test.d.ts → workflows/definition.js} +0 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { parseDeployMode } from "#magento/deploy-mode";
|
|
2
|
+
import { parseRegisteredThemes, resolveThemes } from "#magento/themes";
|
|
3
|
+
/** Ordered, fail-soft Magento inspection. New probes can be extracted without changing consumers. */
|
|
4
|
+
export class DefaultMagentoInspector {
|
|
5
|
+
async inspect(executor) {
|
|
6
|
+
const warnings = [];
|
|
7
|
+
const evidence = [];
|
|
8
|
+
const composer = await executor.run([
|
|
9
|
+
"sh",
|
|
10
|
+
"-lc",
|
|
11
|
+
"test -f composer.lock && node -e \"const x=require('./composer.lock'); console.log(JSON.stringify(x.packages||[]))\" || true",
|
|
12
|
+
]);
|
|
13
|
+
const match = composer.stdout.match(/magento\/(product-community-edition|product-enterprise-edition).*?"version":"([^"]+)/s);
|
|
14
|
+
const edition = match?.[1] === "product-enterprise-edition"
|
|
15
|
+
? "Adobe Commerce"
|
|
16
|
+
: match
|
|
17
|
+
? "Open Source"
|
|
18
|
+
: undefined;
|
|
19
|
+
const composerVersion = match?.[2];
|
|
20
|
+
if (composerVersion)
|
|
21
|
+
evidence.push("composer.lock");
|
|
22
|
+
const cli = await executor.run(["bin/magento", "--version"]);
|
|
23
|
+
const cliVersion = cli.stdout.match(/(?:Magento|Adobe Commerce)[^0-9]*(\d+\.\d+\.\d+(?:-[^\s]+)?)/)?.[1];
|
|
24
|
+
if (cliVersion)
|
|
25
|
+
evidence.push("bin/magento --version");
|
|
26
|
+
if (composerVersion &&
|
|
27
|
+
cliVersion &&
|
|
28
|
+
!cliVersion.startsWith(composerVersion))
|
|
29
|
+
warnings.push(`Composer (${composerVersion}) and CLI (${cliVersion}) disagree`);
|
|
30
|
+
const [php, mode, cache, modules, registeredThemes] = await Promise.all([
|
|
31
|
+
executor.run(["php", "-r", "echo PHP_VERSION;"]),
|
|
32
|
+
executor.run(["bin/magento", "deploy:mode:show"]),
|
|
33
|
+
executor.run(["bin/magento", "cache:status", "--no-ansi"]),
|
|
34
|
+
executor.run(["bin/magento", "module:status", "--enabled", "--no-ansi"]),
|
|
35
|
+
executor.run(["sh", "-lc", themeDiscoveryScript]),
|
|
36
|
+
]);
|
|
37
|
+
return {
|
|
38
|
+
version: composerVersion ?? cliVersion,
|
|
39
|
+
edition,
|
|
40
|
+
phpVersion: php.stdout.trim() || undefined,
|
|
41
|
+
mode: parseDeployMode(mode.stdout, mode.exitCode),
|
|
42
|
+
cacheTypes: cache.stdout
|
|
43
|
+
.split("\n")
|
|
44
|
+
.map((line) => line.match(/^([^:]+):/)?.[1])
|
|
45
|
+
.filter((value) => Boolean(value)),
|
|
46
|
+
modulesEnabled: modules.stdout.split("\n").filter(Boolean).length || undefined,
|
|
47
|
+
warnings,
|
|
48
|
+
evidence,
|
|
49
|
+
themes: resolveThemes(parseRegisteredThemes(registeredThemes.stdout)),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const themeDiscoveryScript = `{
|
|
54
|
+
find vendor -mindepth 3 -maxdepth 3 -type f -name registration.php 2>/dev/null
|
|
55
|
+
find app/design -mindepth 4 -maxdepth 4 -type f -name registration.php 2>/dev/null
|
|
56
|
+
} | while read f; do d=$(dirname "$f"); x="$d/theme.xml"; [ -f "$x" ] || continue; code=$(sed -n "s/.*ComponentRegistrar::THEME[[:space:]]*,[[:space:]]*['\\\"]\\([^'\\\"]*\\).*/\\1/p" "$f" | head -1); [ -n "$code" ] || continue; title=$(sed -n 's:.*<title>\\(.*\\)</title>.*:\\1:p' "$x" | head -1); parent=$(sed -n 's:.*<parent>\\(.*\\)</parent>.*:\\1:p' "$x" | head -1); node -e 'console.log(JSON.stringify({code:process.argv[1],path:process.argv[2],title:process.argv[3]||undefined,parent:process.argv[4]||undefined})+"\\u001e")' "$code" "$d" "$title" "$parent"; done`;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type MagentoDeployMode = "default" | "developer" | "production";
|
|
2
|
+
export type MagentoTheme = {
|
|
3
|
+
code: string;
|
|
4
|
+
title?: string;
|
|
5
|
+
path: string;
|
|
6
|
+
parentChain: string[];
|
|
7
|
+
kind: "luma" | "hyva" | "other";
|
|
8
|
+
};
|
|
9
|
+
export type RawTheme = {
|
|
10
|
+
code: string;
|
|
11
|
+
path: string;
|
|
12
|
+
title?: string;
|
|
13
|
+
parent?: string;
|
|
14
|
+
};
|
|
15
|
+
export type MagentoSnapshot = {
|
|
16
|
+
version?: string;
|
|
17
|
+
edition?: "Open Source" | "Adobe Commerce";
|
|
18
|
+
phpVersion?: string;
|
|
19
|
+
mode?: MagentoDeployMode;
|
|
20
|
+
cacheTypes: string[];
|
|
21
|
+
modulesEnabled?: number;
|
|
22
|
+
warnings: string[];
|
|
23
|
+
evidence: string[];
|
|
24
|
+
themes: MagentoTheme[];
|
|
25
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export function snapshotPrompt(envName, writable, status, snapshot) {
|
|
2
|
+
return `[MerchantDuo: environment=${envName}; write policy=${writable ? "writable" : "read-only"}; 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
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export function resolveThemes(themes) {
|
|
2
|
+
const byCode = new Map(themes.map((theme) => [theme.code, theme]));
|
|
3
|
+
const lineage = (theme) => {
|
|
4
|
+
const chain = [];
|
|
5
|
+
const seen = new Set([theme.code]);
|
|
6
|
+
let parent = theme.parent;
|
|
7
|
+
while (parent && !seen.has(parent)) {
|
|
8
|
+
chain.push(parent);
|
|
9
|
+
seen.add(parent);
|
|
10
|
+
parent = byCode.get(parent)?.parent;
|
|
11
|
+
}
|
|
12
|
+
return chain;
|
|
13
|
+
};
|
|
14
|
+
return themes.map((theme) => {
|
|
15
|
+
const parentChain = lineage(theme);
|
|
16
|
+
const ancestry = [theme.code, ...parentChain];
|
|
17
|
+
return {
|
|
18
|
+
code: theme.code,
|
|
19
|
+
title: theme.title,
|
|
20
|
+
path: theme.path,
|
|
21
|
+
parentChain,
|
|
22
|
+
kind: ancestry.includes("Magento/luma")
|
|
23
|
+
? "luma"
|
|
24
|
+
: ancestry.includes("Hyva/default") || ancestry.includes("Hyva/reset")
|
|
25
|
+
? "hyva"
|
|
26
|
+
: "other",
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export function parseRegisteredThemes(output) {
|
|
31
|
+
return output
|
|
32
|
+
.split("\u001e")
|
|
33
|
+
.map((entry) => entry.trim())
|
|
34
|
+
.filter(Boolean)
|
|
35
|
+
.flatMap((entry) => {
|
|
36
|
+
try {
|
|
37
|
+
const value = JSON.parse(entry);
|
|
38
|
+
return typeof value.code === "string" && typeof value.path === "string"
|
|
39
|
+
? [value]
|
|
40
|
+
: [];
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ProvisionPlan, type ProvisionSnapshot } from "#provision/model";
|
|
2
|
+
export declare const PROVISION_ADVISOR_BASE_URL = "https://merchantduo.invalid/v1";
|
|
3
|
+
export declare const PROVISION_ADVISOR_MODEL = "merchantduo-mock";
|
|
4
|
+
export declare class ProvisionAdvisor {
|
|
5
|
+
private readonly token;
|
|
6
|
+
private readonly fetcher;
|
|
7
|
+
constructor(token?: string | undefined, fetcher?: typeof fetch);
|
|
8
|
+
advise(snapshot: ProvisionSnapshot, plan: ProvisionPlan): Promise<{
|
|
9
|
+
plan: ProvisionPlan;
|
|
10
|
+
diagnostic?: string;
|
|
11
|
+
}>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { redactProvisionSnapshot } from "#provision/model";
|
|
2
|
+
export const PROVISION_ADVISOR_BASE_URL = "https://merchantduo.invalid/v1";
|
|
3
|
+
export const PROVISION_ADVISOR_MODEL = "merchantduo-mock";
|
|
4
|
+
export class ProvisionAdvisor {
|
|
5
|
+
token;
|
|
6
|
+
fetcher;
|
|
7
|
+
constructor(token = process.env.MERCHANTDUO_LLM_TOKEN, fetcher = fetch) {
|
|
8
|
+
this.token = token;
|
|
9
|
+
this.fetcher = fetcher;
|
|
10
|
+
}
|
|
11
|
+
async advise(snapshot, plan) {
|
|
12
|
+
if (!this.token)
|
|
13
|
+
return { plan };
|
|
14
|
+
try {
|
|
15
|
+
const response = await this.fetcher(`${PROVISION_ADVISOR_BASE_URL}/chat/completions`, { method: "POST", headers: { authorization: `Bearer ${this.token}`, "content-type": "application/json" }, body: JSON.stringify({ model: PROVISION_ADVISOR_MODEL, response_format: { type: "json_object" }, messages: [{ role: "user", content: JSON.stringify({ snapshot: redactProvisionSnapshot(snapshot), allowedActions: plan.actions }) }] }) });
|
|
16
|
+
if (!response.ok)
|
|
17
|
+
throw new Error(`advisor returned HTTP ${response.status}`);
|
|
18
|
+
const body = await response.json();
|
|
19
|
+
const selected = JSON.parse(body.choices?.[0]?.message?.content ?? "{}");
|
|
20
|
+
if (!Array.isArray(selected.actions) || !selected.actions.every((action) => plan.actions.some((allowed) => JSON.stringify(allowed) === JSON.stringify(action))))
|
|
21
|
+
throw new Error("advisor selected an action outside the approved plan");
|
|
22
|
+
return { plan: { ...plan, actions: selected.actions } };
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
return { plan, diagnostic: `Provision advisor unavailable: ${error.message}; using deterministic plan.` };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ProvisionSnapshot } from "#provision/model";
|
|
2
|
+
export type ProvisionProbe = {
|
|
3
|
+
run(argv: string[], cwd?: string): Promise<{
|
|
4
|
+
exitCode: number;
|
|
5
|
+
stdout: string;
|
|
6
|
+
stderr: string;
|
|
7
|
+
}>;
|
|
8
|
+
};
|
|
9
|
+
export declare class SystemProvisionProbe implements ProvisionProbe {
|
|
10
|
+
run(argv: string[], cwd?: string): Promise<import("#environments/executor").CommandResult>;
|
|
11
|
+
}
|
|
12
|
+
export declare function discoverProvision(root: string, probe?: ProvisionProbe): Promise<ProvisionSnapshot>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { access, readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { basename, join, resolve } from "node:path";
|
|
4
|
+
const exists = async (path) => access(path, constants.F_OK).then(() => true).catch(() => false);
|
|
5
|
+
export class SystemProvisionProbe {
|
|
6
|
+
async run(argv, cwd) {
|
|
7
|
+
const { EnvironmentBackend } = await import("#environments/backend");
|
|
8
|
+
const backend = new EnvironmentBackend({ type: "local", root: cwd ?? process.cwd() }, cwd ?? process.cwd());
|
|
9
|
+
return backend.run(argv);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
async function command(probe, name, cwd) {
|
|
13
|
+
return (await probe.run(["sh", "-lc", `command -v ${name} >/dev/null`], cwd)).exitCode === 0;
|
|
14
|
+
}
|
|
15
|
+
async function serviceActive(probe, name, cwd) {
|
|
16
|
+
return (await probe.run(["sh", "-lc", `systemctl is-active --quiet ${name} 2>/dev/null || pgrep -x ${name} >/dev/null 2>&1`], cwd)).exitCode === 0;
|
|
17
|
+
}
|
|
18
|
+
function parseVhosts(text, root) {
|
|
19
|
+
const normalized = root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
20
|
+
const rootMatch = text.match(new RegExp(`(?:root|DocumentRoot)\\s+["']?(${normalized}(?:/pub)?)["']?`, "i"));
|
|
21
|
+
if (!rootMatch)
|
|
22
|
+
return undefined;
|
|
23
|
+
const domain = text.match(/(?:server_name|ServerName)\s+([^\s;]+)/i)?.[1];
|
|
24
|
+
return domain ? { server: /server_name/i.test(text) ? "nginx" : "apache", domain, documentRoot: rootMatch[1] } : undefined;
|
|
25
|
+
}
|
|
26
|
+
export async function discoverProvision(root, probe = new SystemProvisionProbe()) {
|
|
27
|
+
const resolved = resolve(root);
|
|
28
|
+
const entries = await readdir(resolved).catch(() => []);
|
|
29
|
+
const composer = await exists(join(resolved, "composer.json"));
|
|
30
|
+
const binMagento = await exists(join(resolved, "bin/magento"));
|
|
31
|
+
const composerText = composer ? await readFile(join(resolved, "composer.json"), "utf8").catch(() => "") : "";
|
|
32
|
+
const detected = binMagento || /magento\/(product|project|composer)/.test(composerText);
|
|
33
|
+
const dumps = entries.filter((name) => /\.sql(?:\.gz)?$/i.test(name)).map((name) => join(resolved, name));
|
|
34
|
+
const [php, composerCommand, mysql, warden, nginx, apache, nginxActive, apacheActive, wardenRunning] = await Promise.all([
|
|
35
|
+
command(probe, "php", resolved), command(probe, "composer", resolved), command(probe, "mysql", resolved), command(probe, "warden", resolved),
|
|
36
|
+
command(probe, "nginx", resolved), command(probe, "apache2", resolved).then((v) => v || command(probe, "httpd", resolved)),
|
|
37
|
+
serviceActive(probe, "nginx", resolved), serviceActive(probe, "apache2", resolved), serviceActive(probe, "warden", resolved),
|
|
38
|
+
]);
|
|
39
|
+
const wardenConfigured = await exists(join(resolved, ".warden", "warden-env.yml")) || await exists(join(resolved, ".env"));
|
|
40
|
+
const env = await readFile(join(resolved, ".env"), "utf8").catch(() => "");
|
|
41
|
+
const domain = env.match(/^TRAEFIK_(?:SUB)?DOMAIN=(.+)$/m)?.[1]?.trim().replace(/^['"]|['"]$/g, "");
|
|
42
|
+
const servers = [nginx && "nginx", apache && "apache"].filter(Boolean);
|
|
43
|
+
const activeServers = [nginxActive && "nginx", apacheActive && "apache"].filter(Boolean);
|
|
44
|
+
const serverConfig = await Promise.all(servers.map(async (server) => {
|
|
45
|
+
const paths = server === "nginx" ? ["/etc/nginx/sites-enabled", "/etc/nginx/conf.d"] : ["/etc/apache2/sites-enabled", "/etc/httpd/conf.d"];
|
|
46
|
+
for (const path of paths) {
|
|
47
|
+
const result = await probe.run(["sh", "-lc", `test -d ${path} && grep -R -h -E 'server_name|ServerName|root|DocumentRoot' ${path} 2>/dev/null || true`], resolved);
|
|
48
|
+
const vhost = parseVhosts(result.stdout, resolved);
|
|
49
|
+
if (vhost)
|
|
50
|
+
return { ...vhost, server };
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}));
|
|
54
|
+
const magentoMode = detected ? await probe.run(["sh", "-lc", "test -x bin/magento && bin/magento deploy:mode:show --no-ansi || true"], resolved) : undefined;
|
|
55
|
+
return {
|
|
56
|
+
workspace: { root: resolved, empty: entries.length === 0, magentoRoot: detected }, source: { composer, binMagento },
|
|
57
|
+
database: { envConfigured: await exists(join(resolved, "app/etc/env.php")), dumps },
|
|
58
|
+
environments: { local: { available: php && composerCommand && mysql && activeServers.length > 0, php, composer: composerCommand, mysql, servers, activeServers, vhost: serverConfig.find(Boolean) }, warden: { available: warden, configured: wardenConfigured, running: wardenRunning, domain } },
|
|
59
|
+
magento: { detected, mode: magentoMode?.stdout.match(/(?:Current|Mode):\s*(\w+)/i)?.[1] }, diagnostics: detected ? [] : [`${basename(resolved)} is not a Magento root`]
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ProvisionInput, ProvisionPlan, ProvisionSnapshot } from "#provision/model";
|
|
2
|
+
export interface ProvisionInterview {
|
|
3
|
+
resolve(snapshot: ProvisionSnapshot, initial: ProvisionInput): Promise<ProvisionInput>;
|
|
4
|
+
approve(plan: ProvisionPlan): Promise<boolean>;
|
|
5
|
+
}
|
|
6
|
+
export declare class TerminalProvisionInterview implements ProvisionInterview {
|
|
7
|
+
resolve(snapshot: ProvisionSnapshot, initial: ProvisionInput): Promise<ProvisionInput>;
|
|
8
|
+
approve(plan: ProvisionPlan): Promise<boolean>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { stdin, stdout } from "node:process";
|
|
3
|
+
export class TerminalProvisionInterview {
|
|
4
|
+
async resolve(snapshot, initial) {
|
|
5
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
6
|
+
try {
|
|
7
|
+
const environment = initial.environment ?? await rl.question(`Environment (${snapshot.environments.warden.available ? "warden" : "local"}/local): `);
|
|
8
|
+
const source = initial.source ?? (snapshot.workspace.magentoRoot ? "existing" : await rl.question("Magento source (git URL, archive path, or HTTPS URL): ").then(parseSource));
|
|
9
|
+
const database = initial.database ?? (snapshot.database.dumps.length === 1 ? `dump:${snapshot.database.dumps[0]}` : await rl.question("Database (current, dump path, or HTTPS URL): ").then(parseDatabase));
|
|
10
|
+
const localDomain = environment === "local" && !snapshot.environments.local.vhost ? await rl.question("Local domain (blank for directory.test): ") : initial.localDomain;
|
|
11
|
+
return { environment, source, database, localDomain: localDomain || undefined };
|
|
12
|
+
}
|
|
13
|
+
finally {
|
|
14
|
+
rl.close();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async approve(plan) {
|
|
18
|
+
stdout.write("\nProvision plan:\n" + plan.actions.map((action, index) => `${index + 1}. ${action.type}${"command" in action ? ` (${action.command})` : ""}`).join("\n") + `\nSudo required: ${plan.requiresSudo ? "yes" : "no"}\n`);
|
|
19
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
20
|
+
try {
|
|
21
|
+
return (await rl.question("Execute this plan? [y/N] ")).trim().toLowerCase() === "y";
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
rl.close();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function parseSource(value) { return /^https?:\/\//.test(value) ? (value.endsWith(".git") ? `git:${value}` : `https:${value}`) : `archive:${value}`; }
|
|
29
|
+
function parseDatabase(value) { return value === "current" ? "current" : /^https?:\/\//.test(value) ? `https:${value}` : `dump:${value}`; }
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** Public, redacted state used to make a provision decision. */
|
|
2
|
+
export type ProvisionEnvironment = "local" | "warden";
|
|
3
|
+
export type SourceInput = "existing" | `git:${string}` | `archive:${string}` | `https:${string}`;
|
|
4
|
+
export type DatabaseInput = "current" | `dump:${string}` | `https:${string}`;
|
|
5
|
+
export type ProvisionInput = {
|
|
6
|
+
environment?: ProvisionEnvironment;
|
|
7
|
+
source?: SourceInput;
|
|
8
|
+
database?: DatabaseInput;
|
|
9
|
+
localDomain?: string;
|
|
10
|
+
};
|
|
11
|
+
export type WebServer = "nginx" | "apache";
|
|
12
|
+
export type ProvisionSnapshot = {
|
|
13
|
+
workspace: {
|
|
14
|
+
root: string;
|
|
15
|
+
empty: boolean;
|
|
16
|
+
magentoRoot: boolean;
|
|
17
|
+
};
|
|
18
|
+
source: {
|
|
19
|
+
composer: boolean;
|
|
20
|
+
binMagento: boolean;
|
|
21
|
+
};
|
|
22
|
+
database: {
|
|
23
|
+
envConfigured: boolean;
|
|
24
|
+
dumps: string[];
|
|
25
|
+
};
|
|
26
|
+
environments: {
|
|
27
|
+
local: {
|
|
28
|
+
available: boolean;
|
|
29
|
+
php: boolean;
|
|
30
|
+
composer: boolean;
|
|
31
|
+
mysql: boolean;
|
|
32
|
+
servers: WebServer[];
|
|
33
|
+
activeServers: WebServer[];
|
|
34
|
+
vhost?: {
|
|
35
|
+
server: WebServer;
|
|
36
|
+
domain: string;
|
|
37
|
+
documentRoot: string;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
warden: {
|
|
41
|
+
available: boolean;
|
|
42
|
+
configured: boolean;
|
|
43
|
+
running: boolean;
|
|
44
|
+
domain?: string;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
magento: {
|
|
48
|
+
detected: boolean;
|
|
49
|
+
mode?: string;
|
|
50
|
+
};
|
|
51
|
+
diagnostics: string[];
|
|
52
|
+
};
|
|
53
|
+
export type ProvisionAction = {
|
|
54
|
+
type: "acquire-source";
|
|
55
|
+
source: Exclude<SourceInput, "existing">;
|
|
56
|
+
} | {
|
|
57
|
+
type: "install-dependencies";
|
|
58
|
+
} | {
|
|
59
|
+
type: "start-warden";
|
|
60
|
+
} | {
|
|
61
|
+
type: "import-database";
|
|
62
|
+
database: Exclude<DatabaseInput, "current">;
|
|
63
|
+
} | {
|
|
64
|
+
type: "configure-database";
|
|
65
|
+
} | {
|
|
66
|
+
type: "magento-setup";
|
|
67
|
+
command: "app:config:import" | "setup:upgrade" | "cache:clean" | "indexer:reindex" | "maintenance:disable";
|
|
68
|
+
} | {
|
|
69
|
+
type: "configure-web-server";
|
|
70
|
+
server: WebServer;
|
|
71
|
+
domain: string;
|
|
72
|
+
} | {
|
|
73
|
+
type: "verify-storefront";
|
|
74
|
+
url?: string;
|
|
75
|
+
};
|
|
76
|
+
export type ProvisionPlan = {
|
|
77
|
+
actions: ProvisionAction[];
|
|
78
|
+
rationale: string[];
|
|
79
|
+
blocked: string[];
|
|
80
|
+
requiresSudo: boolean;
|
|
81
|
+
};
|
|
82
|
+
export declare function redactProvisionSnapshot(snapshot: ProvisionSnapshot): ProvisionSnapshot;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ProvisionInput, ProvisionPlan, ProvisionSnapshot } from "#provision/model";
|
|
2
|
+
/** Deterministic planner. It has no process or filesystem side effects. */
|
|
3
|
+
export declare function createProvisionPlan(snapshot: ProvisionSnapshot, input: ProvisionInput): ProvisionPlan;
|
|
4
|
+
export declare function unresolvedInputs(snapshot: ProvisionSnapshot, input: ProvisionInput): string[];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const sourceMissing = (snapshot, input) => !snapshot.workspace.magentoRoot && (!input.source || input.source === "existing");
|
|
2
|
+
const databaseMissing = (snapshot, input) => !input.database && snapshot.database.dumps.length !== 1;
|
|
3
|
+
/** Deterministic planner. It has no process or filesystem side effects. */
|
|
4
|
+
export function createProvisionPlan(snapshot, input) {
|
|
5
|
+
const blocked = [];
|
|
6
|
+
const rationale = [];
|
|
7
|
+
const actions = [];
|
|
8
|
+
const environment = input.environment;
|
|
9
|
+
if (!environment)
|
|
10
|
+
blocked.push("Select --env local or --env warden.");
|
|
11
|
+
if (sourceMissing(snapshot, input))
|
|
12
|
+
blocked.push("Provide --source existing, a Git URL, archive path, or HTTPS archive URL.");
|
|
13
|
+
if (databaseMissing(snapshot, input))
|
|
14
|
+
blocked.push("Provide --database current, a dump path, or an HTTPS dump URL.");
|
|
15
|
+
if (input.database === "current" && !snapshot.database.envConfigured)
|
|
16
|
+
blocked.push("--database current requires an existing usable Magento database configuration.");
|
|
17
|
+
if (environment === "warden" && !snapshot.environments.warden.available)
|
|
18
|
+
blocked.push("Warden is not installed or available on PATH.");
|
|
19
|
+
if (environment === "local" && (!snapshot.environments.local.php || !snapshot.environments.local.composer || !snapshot.environments.local.mysql))
|
|
20
|
+
blocked.push("Local provisioning requires PHP, Composer, and the MySQL client.");
|
|
21
|
+
if (environment === "local" && snapshot.environments.local.activeServers.length === 0)
|
|
22
|
+
blocked.push("Start nginx or Apache before local provisioning.");
|
|
23
|
+
if (environment === "local" && snapshot.environments.local.activeServers.length > 1 && !input.localDomain)
|
|
24
|
+
blocked.push("Both nginx and Apache are active; choose a local domain/server interactively.");
|
|
25
|
+
if (blocked.length)
|
|
26
|
+
return { actions, rationale, blocked, requiresSudo: false };
|
|
27
|
+
if (!snapshot.workspace.magentoRoot && input.source && input.source !== "existing")
|
|
28
|
+
actions.push({ type: "acquire-source", source: input.source });
|
|
29
|
+
actions.push({ type: "install-dependencies" });
|
|
30
|
+
if (environment === "warden" && !snapshot.environments.warden.running)
|
|
31
|
+
actions.push({ type: "start-warden" });
|
|
32
|
+
const database = input.database ?? (snapshot.database.dumps.length === 1 ? `dump:${snapshot.database.dumps[0]}` : undefined);
|
|
33
|
+
if (environment === "local" && !snapshot.database.envConfigured)
|
|
34
|
+
actions.push({ type: "configure-database" });
|
|
35
|
+
if (database && database !== "current")
|
|
36
|
+
actions.push({ type: "import-database", database });
|
|
37
|
+
actions.push({ type: "magento-setup", command: "app:config:import" }, { type: "magento-setup", command: "setup:upgrade" }, { type: "magento-setup", command: "cache:clean" }, { type: "magento-setup", command: "indexer:reindex" }, { type: "magento-setup", command: "maintenance:disable" });
|
|
38
|
+
const vhost = snapshot.environments.local.vhost;
|
|
39
|
+
if (environment === "local" && !vhost) {
|
|
40
|
+
const server = snapshot.environments.local.activeServers[0];
|
|
41
|
+
const domain = input.localDomain ?? `${snapshot.workspace.root.split("/").filter(Boolean).pop().replace(/[^a-z0-9-]/gi, "-").toLowerCase()}.test`;
|
|
42
|
+
actions.push({ type: "configure-web-server", server, domain });
|
|
43
|
+
rationale.push(`Create a minimal ${server} virtual host for ${domain}.`);
|
|
44
|
+
}
|
|
45
|
+
actions.push({ type: "verify-storefront", url: environment === "warden" ? snapshot.environments.warden.domain : vhost?.domain ?? input.localDomain });
|
|
46
|
+
rationale.push("Composer uses the lock file; Magento deployment mode is preserved.");
|
|
47
|
+
return { actions, rationale, blocked, requiresSudo: actions.some((action) => action.type === "configure-web-server") };
|
|
48
|
+
}
|
|
49
|
+
export function unresolvedInputs(snapshot, input) {
|
|
50
|
+
return createProvisionPlan(snapshot, input).blocked;
|
|
51
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { CommandExecutor } from "#environments/executor";
|
|
2
|
+
import type { ProvisionAction, ProvisionEnvironment, ProvisionInput } from "#provision/model";
|
|
3
|
+
export type ProvisionRunner = {
|
|
4
|
+
run(argv: string[], options?: {
|
|
5
|
+
cwd?: string;
|
|
6
|
+
input?: string;
|
|
7
|
+
}): Promise<{
|
|
8
|
+
exitCode: number;
|
|
9
|
+
stdout: string;
|
|
10
|
+
stderr: string;
|
|
11
|
+
}>;
|
|
12
|
+
};
|
|
13
|
+
export declare class BackendProvisionRunner implements ProvisionRunner {
|
|
14
|
+
private readonly backend;
|
|
15
|
+
constructor(backend: CommandExecutor);
|
|
16
|
+
run(argv: string[], options?: {
|
|
17
|
+
cwd?: string;
|
|
18
|
+
input?: string;
|
|
19
|
+
}): Promise<import("#environments/executor").CommandResult>;
|
|
20
|
+
}
|
|
21
|
+
export declare function validateArchiveEntry(entry: string): boolean;
|
|
22
|
+
export declare function vhostConfig(server: "nginx" | "apache", projectRoot: string, domain: string): string;
|
|
23
|
+
export declare class DefaultProvisionExecutor {
|
|
24
|
+
private readonly runner;
|
|
25
|
+
private readonly root;
|
|
26
|
+
private readonly environment;
|
|
27
|
+
private readonly magentoRunner;
|
|
28
|
+
constructor(runner: ProvisionRunner, root: string, environment: ProvisionEnvironment, magentoRunner?: ProvisionRunner);
|
|
29
|
+
execute(action: ProvisionAction, input: ProvisionInput): Promise<void>;
|
|
30
|
+
private acquire;
|
|
31
|
+
private importDatabase;
|
|
32
|
+
private configureVhost;
|
|
33
|
+
private verify;
|
|
34
|
+
private must;
|
|
35
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { mkdir, rm, stat } from "node:fs/promises";
|
|
2
|
+
import { basename, join, resolve } from "node:path";
|
|
3
|
+
export class BackendProvisionRunner {
|
|
4
|
+
backend;
|
|
5
|
+
constructor(backend) {
|
|
6
|
+
this.backend = backend;
|
|
7
|
+
}
|
|
8
|
+
run(argv, options = {}) { return this.backend.run(argv, { input: options.input }); }
|
|
9
|
+
}
|
|
10
|
+
export function validateArchiveEntry(entry) {
|
|
11
|
+
return entry.length > 0 && !entry.startsWith("/") && !entry.split(/[\\/]+/).includes("..");
|
|
12
|
+
}
|
|
13
|
+
export function vhostConfig(server, projectRoot, domain) {
|
|
14
|
+
if (!/^[a-z0-9-]+\.test$/i.test(domain))
|
|
15
|
+
throw new Error("Local domain must be a sanitized .test hostname");
|
|
16
|
+
const root = join(resolve(projectRoot), "pub");
|
|
17
|
+
return server === "nginx" ? `server { listen 80; server_name ${domain}; root ${root}; index index.php; location / { try_files $uri $uri/ /index.php?$args; } location ~ \\.php$ { try_files $uri =404; fastcgi_pass unix:/run/php/php-fpm.sock; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } }\n` : `<VirtualHost *:80>\n ServerName ${domain}\n DocumentRoot ${root}\n <Directory ${root}>\n AllowOverride All\n Require all granted\n </Directory>\n</VirtualHost>\n`;
|
|
18
|
+
}
|
|
19
|
+
export class DefaultProvisionExecutor {
|
|
20
|
+
runner;
|
|
21
|
+
root;
|
|
22
|
+
environment;
|
|
23
|
+
magentoRunner;
|
|
24
|
+
constructor(runner, root, environment, magentoRunner = runner) {
|
|
25
|
+
this.runner = runner;
|
|
26
|
+
this.root = root;
|
|
27
|
+
this.environment = environment;
|
|
28
|
+
this.magentoRunner = magentoRunner;
|
|
29
|
+
}
|
|
30
|
+
async execute(action, input) {
|
|
31
|
+
switch (action.type) {
|
|
32
|
+
case "acquire-source": return this.acquire(action.source);
|
|
33
|
+
case "install-dependencies": return this.must(["composer", "install", "--no-interaction", "--prefer-dist", "--no-progress"], this.magentoRunner);
|
|
34
|
+
case "start-warden": return this.must(["warden", "env", "config"]).then(() => this.must(["warden", "env", "start"]));
|
|
35
|
+
case "import-database": return this.importDatabase(action.database);
|
|
36
|
+
case "configure-database": throw new Error("Database configuration is required but no safe credential source was supplied. Add app/etc/env.php or use a current configured database.");
|
|
37
|
+
case "magento-setup":
|
|
38
|
+
if (action.command === "app:config:import") {
|
|
39
|
+
const result = await this.magentoRunner.run(["bin/magento", action.command, "--no-interaction"], { cwd: this.root });
|
|
40
|
+
if (result.exitCode !== 0 && !/does not exist|Nothing to import/i.test(result.stderr + result.stdout))
|
|
41
|
+
throw new Error(result.stderr || result.stdout);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
return this.must(["bin/magento", action.command, "--no-interaction"], this.magentoRunner);
|
|
45
|
+
case "configure-web-server": return this.configureVhost(action.server, action.domain);
|
|
46
|
+
case "verify-storefront": return this.verify(action.url);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async acquire(source) {
|
|
50
|
+
const entries = await stat(this.root).then(() => import("node:fs/promises").then(({ readdir }) => readdir(this.root))).catch(() => []);
|
|
51
|
+
if (entries.length)
|
|
52
|
+
throw new Error("Refusing to acquire source into a non-empty non-Magento directory.");
|
|
53
|
+
if (source.startsWith("git:"))
|
|
54
|
+
return this.must(["git", "clone", source.slice(4), "."]);
|
|
55
|
+
const location = source.replace(/^(archive|https):/, "");
|
|
56
|
+
const home = process.env.MERCHANTDUO_HOME ?? join(process.env.XDG_STATE_HOME ?? "/tmp", "merchantduo");
|
|
57
|
+
const stage = join(home, "provisions", `${Date.now()}-${basename(location).replace(/[^a-z0-9._-]/gi, "-")}`);
|
|
58
|
+
await mkdir(stage, { recursive: true });
|
|
59
|
+
try {
|
|
60
|
+
const archive = source.startsWith("https:") ? join(stage, "source.archive") : resolve(location);
|
|
61
|
+
if (source.startsWith("https:"))
|
|
62
|
+
await this.must(["curl", "--fail", "--location", "--output", archive, location]);
|
|
63
|
+
const listed = await this.runner.run(["tar", "-tzf", archive]);
|
|
64
|
+
if (listed.exitCode !== 0 || listed.stdout.split("\n").filter(Boolean).some((entry) => !validateArchiveEntry(entry)))
|
|
65
|
+
throw new Error("Archive is invalid or contains unsafe paths.");
|
|
66
|
+
await this.must(["tar", "-xzf", archive, "-C", this.root, "--strip-components=1"]);
|
|
67
|
+
const magento = await this.runner.run(["sh", "-lc", "test -f composer.json && (test -x bin/magento || grep -q 'magento/' composer.json)"], { cwd: this.root });
|
|
68
|
+
if (magento.exitCode !== 0)
|
|
69
|
+
throw new Error("Extracted archive does not contain a Magento root.");
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
await rm(stage, { recursive: true, force: true });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async importDatabase(database) {
|
|
76
|
+
const source = database.replace(/^(dump|https):/, "");
|
|
77
|
+
if (this.environment === "warden")
|
|
78
|
+
return this.must(["warden", "db", "import", source]);
|
|
79
|
+
if (database.startsWith("https:"))
|
|
80
|
+
throw new Error("Download the local database dump before local import; database credentials are never accepted as CLI arguments.");
|
|
81
|
+
return this.must(["sh", "-lc", `${source.endsWith(".gz") ? "gzip -dc --" : "cat --"} "$1" | mysql`, "merchantduo-db-import", source]);
|
|
82
|
+
}
|
|
83
|
+
async configureVhost(server, domain) {
|
|
84
|
+
const config = vhostConfig(server, this.root, domain);
|
|
85
|
+
const file = server === "nginx" ? `/etc/nginx/sites-available/${domain}` : `/etc/apache2/sites-available/${domain}.conf`;
|
|
86
|
+
const encoded = Buffer.from(config).toString("base64");
|
|
87
|
+
await this.must(["sudo", "sh", "-lc", `echo ${encoded} | base64 -d > ${file}`]);
|
|
88
|
+
if (server === "nginx")
|
|
89
|
+
await this.must(["sudo", "sh", "-lc", `ln -sfn ${file} /etc/nginx/sites-enabled/${domain} && nginx -t && systemctl reload nginx`]);
|
|
90
|
+
else
|
|
91
|
+
await this.must(["sudo", "sh", "-lc", `a2ensite ${domain}.conf && apachectl configtest && systemctl reload apache2`]);
|
|
92
|
+
await this.must(["sudo", "sh", "-lc", `grep -qE '^[[:space:]]*127\\.0\\.0\\.1[[:space:]].*\\b${domain}\\b' /etc/hosts || echo '127.0.0.1 ${domain}' >> /etc/hosts`]);
|
|
93
|
+
}
|
|
94
|
+
async verify(url) { if (url)
|
|
95
|
+
await this.must(["curl", "--fail", "--location", "--max-time", "20", `http://${url}`]); }
|
|
96
|
+
async must(argv, runner = this.runner) { const result = await runner.run(argv, { cwd: this.root }); if (result.exitCode !== 0)
|
|
97
|
+
throw new Error(`${argv.join(" ")} failed: ${result.stderr || result.stdout}`); }
|
|
98
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { dirname, resolve } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
const compiledModuleDirectory = dirname(fileURLToPath(import.meta.url));
|
|
4
|
+
/** Resolve an installed package asset from either src/shared or dist/shared. */
|
|
5
|
+
export function packageRoot() {
|
|
6
|
+
return resolve(compiledModuleDirectory, "../..");
|
|
7
|
+
}
|
|
8
|
+
export function packagePath(...segments) {
|
|
9
|
+
return resolve(packageRoot(), ...segments);
|
|
10
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { EnvironmentBackend } from "#environments/backend";
|
|
2
|
+
import type { Environment } from "#environments/model";
|
|
3
|
+
import type { TestingOptions, TestingSnapshot } from "#testing/model";
|
|
4
|
+
export declare function discoverTesting(environment: Environment, backend: EnvironmentBackend, cwd: string, options: TestingOptions, tools?: {
|
|
5
|
+
http?: string;
|
|
6
|
+
browser?: string;
|
|
7
|
+
magerun?: string;
|
|
8
|
+
}): Promise<TestingSnapshot>;
|
|
9
|
+
export declare function wardenRouting(projectRoot: string): Promise<string | undefined>;
|
|
10
|
+
export declare function findExecutable(candidates: string[]): Promise<string | undefined>;
|