@merchantduo/code 0.1.0-beta.0 → 0.2.0-beta.1
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,68 @@
|
|
|
1
|
+
import { access, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { createInterface } from "node:readline/promises";
|
|
4
|
+
import { stdin, stdout } from "node:process";
|
|
5
|
+
import { stringify } from "yaml";
|
|
6
|
+
export class TerminalInitInterview {
|
|
7
|
+
async confirm(question) {
|
|
8
|
+
const terminal = createInterface({ input: stdin, output: stdout });
|
|
9
|
+
try {
|
|
10
|
+
const answer = (await terminal.question(`${question} [Y/n] `)).trim().toLowerCase();
|
|
11
|
+
return answer === "" || answer === "y" || answer === "yes";
|
|
12
|
+
}
|
|
13
|
+
finally {
|
|
14
|
+
terminal.close();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export async function hasWardenEnvironment(cwd) {
|
|
19
|
+
try {
|
|
20
|
+
const source = await readFile(resolve(cwd, ".env"), "utf8");
|
|
21
|
+
return /^\s*(?:WARDEN_ENV_NAME|TRAEFIK_DOMAIN|TRAEFIK_SUBDOMAIN)\s*=/m.test(source);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error.code === "ENOENT")
|
|
25
|
+
return false;
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function initProject(cwd, interview = new TerminalInitInterview(), output = console.log) {
|
|
30
|
+
const path = resolve(cwd, ".merchantduo.yaml");
|
|
31
|
+
try {
|
|
32
|
+
await access(path);
|
|
33
|
+
throw new Error(`${path} already exists`);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error.code !== "ENOENT")
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
const environments = {};
|
|
40
|
+
if (await interview.confirm("Add the local Magento checkout environment?")) {
|
|
41
|
+
environments.local = { type: "local", root: "." };
|
|
42
|
+
}
|
|
43
|
+
const wardenDetected = await hasWardenEnvironment(cwd);
|
|
44
|
+
if (wardenDetected &&
|
|
45
|
+
(await interview.confirm("Warden project .env detected. Add the Warden environment?"))) {
|
|
46
|
+
environments.warden = {
|
|
47
|
+
type: "warden",
|
|
48
|
+
projectRoot: ".",
|
|
49
|
+
root: "/var/www/html",
|
|
50
|
+
filesService: "php-fpm",
|
|
51
|
+
targets: { default: "php-fpm", debug: "php-debug" },
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const selected = Object.keys(environments);
|
|
55
|
+
if (!selected.length) {
|
|
56
|
+
throw new Error("No environment selected; .merchantduo.yaml was not created");
|
|
57
|
+
}
|
|
58
|
+
const config = {
|
|
59
|
+
defaultEnvironment: selected[0],
|
|
60
|
+
activeTheme: "all",
|
|
61
|
+
testing: { allowInsecureTls: true },
|
|
62
|
+
environments,
|
|
63
|
+
};
|
|
64
|
+
await writeFile(path, stringify(config), { mode: 0o600 });
|
|
65
|
+
output(`Created ${path}`);
|
|
66
|
+
output(remoteEnvironmentExample);
|
|
67
|
+
}
|
|
68
|
+
export const remoteEnvironmentExample = `\nTo add a remote environment later, add this under environments:\n\n stage:\n type: ssh\n host: shop-stage\n root: /var/www/html\n writable: false\n\nConfigure its user, port, and identity through ~/.ssh/config:\n\n Host shop-stage\n HostName stage.example.com\n User deploy\n Port 22\n IdentityFile ~/.ssh/id_ed25519`;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { EnvironmentBackend } from "#environments/backend";
|
|
2
|
+
import { ProvisionAdvisor } from "#provision/advisor";
|
|
3
|
+
import { discoverProvision } from "#provision/discovery";
|
|
4
|
+
import { TerminalProvisionInterview } from "#provision/interview";
|
|
5
|
+
import { createProvisionPlan } from "#provision/planner";
|
|
6
|
+
import { BackendProvisionRunner, DefaultProvisionExecutor } from "#provision/runner";
|
|
7
|
+
function source(value) { if (!value)
|
|
8
|
+
return undefined; if (value === "existing")
|
|
9
|
+
return value; return /^https?:\/\//.test(value) ? (value.endsWith(".git") ? `git:${value}` : `https:${value}`) : `archive:${value}`; }
|
|
10
|
+
function database(value) { if (!value)
|
|
11
|
+
return undefined; if (value === "current")
|
|
12
|
+
return value; return /^https?:\/\//.test(value) ? `https:${value}` : `dump:${value}`; }
|
|
13
|
+
export async function provision(cwd, options) {
|
|
14
|
+
if (options.environment && options.environment !== "local" && options.environment !== "warden")
|
|
15
|
+
throw new Error("--env must be local or warden");
|
|
16
|
+
const snapshot = await discoverProvision(cwd);
|
|
17
|
+
const initial = { environment: options.environment, source: source(options.source), database: database(options.database) };
|
|
18
|
+
const interview = new TerminalProvisionInterview();
|
|
19
|
+
const input = options.yes ? initial : await interview.resolve(snapshot, initial);
|
|
20
|
+
const advised = await new ProvisionAdvisor().advise(snapshot, createProvisionPlan(snapshot, input));
|
|
21
|
+
if (advised.diagnostic)
|
|
22
|
+
console.error(`merchantduo: ${advised.diagnostic}`);
|
|
23
|
+
if (advised.plan.blocked.length)
|
|
24
|
+
throw new Error(`Provision requires input:\n- ${advised.plan.blocked.join("\n- ")}`);
|
|
25
|
+
if (!options.yes && !(await interview.approve(advised.plan))) {
|
|
26
|
+
console.log("Provision cancelled.");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const hostBackend = new EnvironmentBackend({ type: "local", root: "." }, cwd);
|
|
30
|
+
const magentoBackend = new EnvironmentBackend(input.environment === "warden" ? { type: "warden", projectRoot: ".", root: "/var/www/html", filesService: "php-fpm", targets: { default: "php-fpm" } } : { type: "local", root: "." }, cwd);
|
|
31
|
+
const executor = new DefaultProvisionExecutor(new BackendProvisionRunner(hostBackend), cwd, input.environment, new BackendProvisionRunner(magentoBackend));
|
|
32
|
+
for (const action of advised.plan.actions)
|
|
33
|
+
await executor.execute(action, input);
|
|
34
|
+
console.log("Provision completed.");
|
|
35
|
+
}
|
package/dist/cli/main.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { loadConfig, selectEnvironment } from "#config/index";
|
|
3
|
+
import { isTrusted, trustProject } from "#config/trust-store";
|
|
4
|
+
import { parseArguments } from "#cli/arguments";
|
|
5
|
+
import { launchAgent } from "#cli/commands/agent";
|
|
6
|
+
import { doctor } from "#cli/commands/doctor";
|
|
7
|
+
import { initProject } from "#cli/commands/init";
|
|
8
|
+
import { provision } from "#cli/commands/provision";
|
|
9
|
+
async function main() {
|
|
10
|
+
const cwd = process.cwd();
|
|
11
|
+
const args = parseArguments(process.argv.slice(2));
|
|
12
|
+
if (args.command === "init")
|
|
13
|
+
return initProject(cwd);
|
|
14
|
+
if (args.command === "provision")
|
|
15
|
+
return provision(cwd, args);
|
|
16
|
+
const loaded = await loadConfig(cwd, { config: args.configPath });
|
|
17
|
+
const selected = selectEnvironment(loaded.config, cwd, args.environment);
|
|
18
|
+
if (loaded.configPath && !(await isTrusted(loaded.configPath))) {
|
|
19
|
+
if (process.env.MERCHANTDUO_TRUST_CONFIG === "1")
|
|
20
|
+
await trustProject(loaded.configPath);
|
|
21
|
+
else
|
|
22
|
+
throw new Error(`Untrusted MerchantDuo configuration: ${loaded.configPath}. Review it and run with MERCHANTDUO_TRUST_CONFIG=1 once.`);
|
|
23
|
+
}
|
|
24
|
+
if (args.command === "doctor")
|
|
25
|
+
return doctor(cwd, selected.name, selected.environment, loaded.config);
|
|
26
|
+
process.exitCode = await launchAgent(args.piArguments, selected.name, loaded.configPath);
|
|
27
|
+
}
|
|
28
|
+
main().catch((error) => {
|
|
29
|
+
console.error(`merchantduo: ${error.message}`);
|
|
30
|
+
process.exitCode = 1;
|
|
31
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Environment } from "#environments/model";
|
|
2
|
+
import type { MerchantConfig } from "#config/schema";
|
|
3
|
+
export { loadConfig, interpolate, type LoadedConfig } from "#config/loader";
|
|
4
|
+
export { merchantHome } from "#config/paths";
|
|
5
|
+
export { setProjectTheme } from "#config/project-store";
|
|
6
|
+
export { ConfigSchema, type MerchantConfig, } from "#config/schema";
|
|
7
|
+
export { configDigest, isTrusted, trustProject } from "#config/trust-store";
|
|
8
|
+
export declare function selectEnvironment(config: MerchantConfig, cwd: string, requested?: string, env?: NodeJS.ProcessEnv): {
|
|
9
|
+
name: string;
|
|
10
|
+
environment: Environment;
|
|
11
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { loadConfig, interpolate } from "#config/loader";
|
|
2
|
+
export { merchantHome } from "#config/paths";
|
|
3
|
+
export { setProjectTheme } from "#config/project-store";
|
|
4
|
+
export { ConfigSchema, } from "#config/schema";
|
|
5
|
+
export { configDigest, isTrusted, trustProject } from "#config/trust-store";
|
|
6
|
+
export function selectEnvironment(config, cwd, requested, env = process.env) {
|
|
7
|
+
const name = requested ?? env.MERCHANTDUO_ENV ?? config.defaultEnvironment;
|
|
8
|
+
if (name) {
|
|
9
|
+
const environment = config.environments[name];
|
|
10
|
+
if (!environment)
|
|
11
|
+
throw new Error(`Unknown MerchantDuo environment: ${name}`);
|
|
12
|
+
return { name, environment };
|
|
13
|
+
}
|
|
14
|
+
return { name: "local", environment: { type: "local", root: cwd } };
|
|
15
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type MerchantConfig } from "#config/schema";
|
|
2
|
+
export type LoadedConfig = {
|
|
3
|
+
config: MerchantConfig;
|
|
4
|
+
configPath?: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function interpolate(value: string, env?: NodeJS.ProcessEnv): string;
|
|
7
|
+
export declare function loadConfig(cwd: string, options?: {
|
|
8
|
+
config?: string;
|
|
9
|
+
env?: NodeJS.ProcessEnv;
|
|
10
|
+
}): Promise<LoadedConfig>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { parse } from "yaml";
|
|
4
|
+
import { ConfigSchema } from "#config/schema";
|
|
5
|
+
import { merchantHome } from "#config/paths";
|
|
6
|
+
export function interpolate(value, env = process.env) {
|
|
7
|
+
return value.replace(/\$\{([A-Z][A-Z0-9_]*)\}/g, (_, name) => {
|
|
8
|
+
if (!env[name])
|
|
9
|
+
throw new Error(`Missing required environment variable: ${name}`);
|
|
10
|
+
return env[name];
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
function expand(value, env) {
|
|
14
|
+
if (typeof value === "string")
|
|
15
|
+
return interpolate(value, env);
|
|
16
|
+
if (Array.isArray(value))
|
|
17
|
+
return value.map((item) => expand(item, env));
|
|
18
|
+
if (value && typeof value === "object")
|
|
19
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, expand(item, env)]));
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
async function readYaml(path) {
|
|
23
|
+
try {
|
|
24
|
+
const value = parse(await readFile(path, "utf8"));
|
|
25
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
26
|
+
throw new Error("expected a YAML object");
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error.code === "ENOENT")
|
|
31
|
+
return undefined;
|
|
32
|
+
throw new Error(`Cannot load ${path}: ${error.message}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export async function loadConfig(cwd, options = {}) {
|
|
36
|
+
const env = options.env ?? process.env;
|
|
37
|
+
const globalPath = resolve(merchantHome(env), "config.yaml");
|
|
38
|
+
const projectPath = options.config ??
|
|
39
|
+
env.MERCHANTDUO_CONFIG ??
|
|
40
|
+
resolve(cwd, ".merchantduo.yaml");
|
|
41
|
+
const globalConfig = await readYaml(globalPath);
|
|
42
|
+
const projectConfig = await readYaml(projectPath);
|
|
43
|
+
const raw = {
|
|
44
|
+
...(globalConfig ?? {}),
|
|
45
|
+
...(projectConfig ?? {}),
|
|
46
|
+
environments: {
|
|
47
|
+
...(globalConfig?.environments ?? {}),
|
|
48
|
+
...(projectConfig?.environments ?? {}),
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
if (!globalConfig && !projectConfig)
|
|
52
|
+
return {
|
|
53
|
+
config: ConfigSchema.parse({
|
|
54
|
+
environments: { local: { type: "local", root: "." } },
|
|
55
|
+
}),
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
config: ConfigSchema.parse(expand(raw, env)),
|
|
59
|
+
configPath: projectConfig ? resolve(projectPath) : undefined,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function merchantHome(env?: NodeJS.ProcessEnv): string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { parse, stringify } from "yaml";
|
|
3
|
+
/** Patch project-owned values without rewriting merged global configuration. */
|
|
4
|
+
export async function setProjectTheme(configPath, activeTheme) {
|
|
5
|
+
const source = await readFile(configPath, "utf8");
|
|
6
|
+
const value = parse(source);
|
|
7
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
8
|
+
throw new Error(`Cannot update ${configPath}: expected a YAML object`);
|
|
9
|
+
await writeFile(configPath, stringify({ ...value, activeTheme }), {
|
|
10
|
+
mode: 0o600,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { Environment } from "#environments/model";
|
|
3
|
+
export declare const ConfigSchema: z.ZodObject<{
|
|
4
|
+
defaultEnvironment: z.ZodOptional<z.ZodString>;
|
|
5
|
+
activeTheme: z.ZodDefault<z.ZodString>;
|
|
6
|
+
testing: z.ZodDefault<z.ZodObject<{
|
|
7
|
+
frontendUrl: z.ZodOptional<z.ZodString>;
|
|
8
|
+
adminUrl: z.ZodOptional<z.ZodString>;
|
|
9
|
+
allowInsecureTls: z.ZodDefault<z.ZodBoolean>;
|
|
10
|
+
}, "strict", z.ZodTypeAny, {
|
|
11
|
+
allowInsecureTls: boolean;
|
|
12
|
+
frontendUrl?: string | undefined;
|
|
13
|
+
adminUrl?: string | undefined;
|
|
14
|
+
}, {
|
|
15
|
+
frontendUrl?: string | undefined;
|
|
16
|
+
adminUrl?: string | undefined;
|
|
17
|
+
allowInsecureTls?: boolean | undefined;
|
|
18
|
+
}>>;
|
|
19
|
+
environments: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodObject<{
|
|
20
|
+
type: z.ZodLiteral<"local">;
|
|
21
|
+
root: z.ZodString;
|
|
22
|
+
testing: z.ZodOptional<z.ZodObject<{
|
|
23
|
+
frontendUrl: z.ZodOptional<z.ZodString>;
|
|
24
|
+
adminUrl: z.ZodOptional<z.ZodString>;
|
|
25
|
+
allowInsecureTls: z.ZodDefault<z.ZodBoolean>;
|
|
26
|
+
}, "strict", z.ZodTypeAny, {
|
|
27
|
+
allowInsecureTls: boolean;
|
|
28
|
+
frontendUrl?: string | undefined;
|
|
29
|
+
adminUrl?: string | undefined;
|
|
30
|
+
}, {
|
|
31
|
+
frontendUrl?: string | undefined;
|
|
32
|
+
adminUrl?: string | undefined;
|
|
33
|
+
allowInsecureTls?: boolean | undefined;
|
|
34
|
+
}>>;
|
|
35
|
+
}, "strict", z.ZodTypeAny, {
|
|
36
|
+
type: "local";
|
|
37
|
+
root: string;
|
|
38
|
+
testing?: {
|
|
39
|
+
allowInsecureTls: boolean;
|
|
40
|
+
frontendUrl?: string | undefined;
|
|
41
|
+
adminUrl?: string | undefined;
|
|
42
|
+
} | undefined;
|
|
43
|
+
}, {
|
|
44
|
+
type: "local";
|
|
45
|
+
root: string;
|
|
46
|
+
testing?: {
|
|
47
|
+
frontendUrl?: string | undefined;
|
|
48
|
+
adminUrl?: string | undefined;
|
|
49
|
+
allowInsecureTls?: boolean | undefined;
|
|
50
|
+
} | undefined;
|
|
51
|
+
}>, z.ZodObject<{
|
|
52
|
+
type: z.ZodLiteral<"warden">;
|
|
53
|
+
projectRoot: z.ZodString;
|
|
54
|
+
root: z.ZodString;
|
|
55
|
+
filesService: z.ZodDefault<z.ZodString>;
|
|
56
|
+
targets: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
57
|
+
testing: z.ZodOptional<z.ZodObject<{
|
|
58
|
+
frontendUrl: z.ZodOptional<z.ZodString>;
|
|
59
|
+
adminUrl: z.ZodOptional<z.ZodString>;
|
|
60
|
+
allowInsecureTls: z.ZodDefault<z.ZodBoolean>;
|
|
61
|
+
}, "strict", z.ZodTypeAny, {
|
|
62
|
+
allowInsecureTls: boolean;
|
|
63
|
+
frontendUrl?: string | undefined;
|
|
64
|
+
adminUrl?: string | undefined;
|
|
65
|
+
}, {
|
|
66
|
+
frontendUrl?: string | undefined;
|
|
67
|
+
adminUrl?: string | undefined;
|
|
68
|
+
allowInsecureTls?: boolean | undefined;
|
|
69
|
+
}>>;
|
|
70
|
+
}, "strict", z.ZodTypeAny, {
|
|
71
|
+
type: "warden";
|
|
72
|
+
root: string;
|
|
73
|
+
projectRoot: string;
|
|
74
|
+
filesService: string;
|
|
75
|
+
targets: Record<string, string>;
|
|
76
|
+
testing?: {
|
|
77
|
+
allowInsecureTls: boolean;
|
|
78
|
+
frontendUrl?: string | undefined;
|
|
79
|
+
adminUrl?: string | undefined;
|
|
80
|
+
} | undefined;
|
|
81
|
+
}, {
|
|
82
|
+
type: "warden";
|
|
83
|
+
root: string;
|
|
84
|
+
projectRoot: string;
|
|
85
|
+
testing?: {
|
|
86
|
+
frontendUrl?: string | undefined;
|
|
87
|
+
adminUrl?: string | undefined;
|
|
88
|
+
allowInsecureTls?: boolean | undefined;
|
|
89
|
+
} | undefined;
|
|
90
|
+
filesService?: string | undefined;
|
|
91
|
+
targets?: Record<string, string> | undefined;
|
|
92
|
+
}>, z.ZodObject<{
|
|
93
|
+
type: z.ZodLiteral<"ssh">;
|
|
94
|
+
host: z.ZodString;
|
|
95
|
+
root: z.ZodString;
|
|
96
|
+
writable: z.ZodDefault<z.ZodBoolean>;
|
|
97
|
+
testing: z.ZodOptional<z.ZodObject<{
|
|
98
|
+
frontendUrl: z.ZodOptional<z.ZodString>;
|
|
99
|
+
adminUrl: z.ZodOptional<z.ZodString>;
|
|
100
|
+
allowInsecureTls: z.ZodDefault<z.ZodBoolean>;
|
|
101
|
+
}, "strict", z.ZodTypeAny, {
|
|
102
|
+
allowInsecureTls: boolean;
|
|
103
|
+
frontendUrl?: string | undefined;
|
|
104
|
+
adminUrl?: string | undefined;
|
|
105
|
+
}, {
|
|
106
|
+
frontendUrl?: string | undefined;
|
|
107
|
+
adminUrl?: string | undefined;
|
|
108
|
+
allowInsecureTls?: boolean | undefined;
|
|
109
|
+
}>>;
|
|
110
|
+
}, "strict", z.ZodTypeAny, {
|
|
111
|
+
type: "ssh";
|
|
112
|
+
root: string;
|
|
113
|
+
host: string;
|
|
114
|
+
writable: boolean;
|
|
115
|
+
testing?: {
|
|
116
|
+
allowInsecureTls: boolean;
|
|
117
|
+
frontendUrl?: string | undefined;
|
|
118
|
+
adminUrl?: string | undefined;
|
|
119
|
+
} | undefined;
|
|
120
|
+
}, {
|
|
121
|
+
type: "ssh";
|
|
122
|
+
root: string;
|
|
123
|
+
host: string;
|
|
124
|
+
testing?: {
|
|
125
|
+
frontendUrl?: string | undefined;
|
|
126
|
+
adminUrl?: string | undefined;
|
|
127
|
+
allowInsecureTls?: boolean | undefined;
|
|
128
|
+
} | undefined;
|
|
129
|
+
writable?: boolean | undefined;
|
|
130
|
+
}>]>>>;
|
|
131
|
+
}, "strict", z.ZodTypeAny, {
|
|
132
|
+
testing: {
|
|
133
|
+
allowInsecureTls: boolean;
|
|
134
|
+
frontendUrl?: string | undefined;
|
|
135
|
+
adminUrl?: string | undefined;
|
|
136
|
+
};
|
|
137
|
+
activeTheme: string;
|
|
138
|
+
environments: Record<string, {
|
|
139
|
+
type: "local";
|
|
140
|
+
root: string;
|
|
141
|
+
testing?: {
|
|
142
|
+
allowInsecureTls: boolean;
|
|
143
|
+
frontendUrl?: string | undefined;
|
|
144
|
+
adminUrl?: string | undefined;
|
|
145
|
+
} | undefined;
|
|
146
|
+
} | {
|
|
147
|
+
type: "warden";
|
|
148
|
+
root: string;
|
|
149
|
+
projectRoot: string;
|
|
150
|
+
filesService: string;
|
|
151
|
+
targets: Record<string, string>;
|
|
152
|
+
testing?: {
|
|
153
|
+
allowInsecureTls: boolean;
|
|
154
|
+
frontendUrl?: string | undefined;
|
|
155
|
+
adminUrl?: string | undefined;
|
|
156
|
+
} | undefined;
|
|
157
|
+
} | {
|
|
158
|
+
type: "ssh";
|
|
159
|
+
root: string;
|
|
160
|
+
host: string;
|
|
161
|
+
writable: boolean;
|
|
162
|
+
testing?: {
|
|
163
|
+
allowInsecureTls: boolean;
|
|
164
|
+
frontendUrl?: string | undefined;
|
|
165
|
+
adminUrl?: string | undefined;
|
|
166
|
+
} | undefined;
|
|
167
|
+
}>;
|
|
168
|
+
defaultEnvironment?: string | undefined;
|
|
169
|
+
}, {
|
|
170
|
+
testing?: {
|
|
171
|
+
frontendUrl?: string | undefined;
|
|
172
|
+
adminUrl?: string | undefined;
|
|
173
|
+
allowInsecureTls?: boolean | undefined;
|
|
174
|
+
} | undefined;
|
|
175
|
+
defaultEnvironment?: string | undefined;
|
|
176
|
+
activeTheme?: string | undefined;
|
|
177
|
+
environments?: Record<string, {
|
|
178
|
+
type: "local";
|
|
179
|
+
root: string;
|
|
180
|
+
testing?: {
|
|
181
|
+
frontendUrl?: string | undefined;
|
|
182
|
+
adminUrl?: string | undefined;
|
|
183
|
+
allowInsecureTls?: boolean | undefined;
|
|
184
|
+
} | undefined;
|
|
185
|
+
} | {
|
|
186
|
+
type: "warden";
|
|
187
|
+
root: string;
|
|
188
|
+
projectRoot: string;
|
|
189
|
+
testing?: {
|
|
190
|
+
frontendUrl?: string | undefined;
|
|
191
|
+
adminUrl?: string | undefined;
|
|
192
|
+
allowInsecureTls?: boolean | undefined;
|
|
193
|
+
} | undefined;
|
|
194
|
+
filesService?: string | undefined;
|
|
195
|
+
targets?: Record<string, string> | undefined;
|
|
196
|
+
} | {
|
|
197
|
+
type: "ssh";
|
|
198
|
+
root: string;
|
|
199
|
+
host: string;
|
|
200
|
+
testing?: {
|
|
201
|
+
frontendUrl?: string | undefined;
|
|
202
|
+
adminUrl?: string | undefined;
|
|
203
|
+
allowInsecureTls?: boolean | undefined;
|
|
204
|
+
} | undefined;
|
|
205
|
+
writable?: boolean | undefined;
|
|
206
|
+
}> | undefined;
|
|
207
|
+
}>;
|
|
208
|
+
export type MerchantConfig = Omit<z.infer<typeof ConfigSchema>, "environments"> & {
|
|
209
|
+
environments: Record<string, Environment>;
|
|
210
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const testing = z
|
|
3
|
+
.object({
|
|
4
|
+
frontendUrl: z.string().url().optional(),
|
|
5
|
+
adminUrl: z.string().url().optional(),
|
|
6
|
+
allowInsecureTls: z.boolean().default(true),
|
|
7
|
+
})
|
|
8
|
+
.strict();
|
|
9
|
+
const local = z.object({ type: z.literal("local"), root: z.string(), testing: testing.optional() }).strict();
|
|
10
|
+
const warden = z
|
|
11
|
+
.object({
|
|
12
|
+
type: z.literal("warden"),
|
|
13
|
+
projectRoot: z.string(),
|
|
14
|
+
root: z.string(),
|
|
15
|
+
filesService: z.string().default("php-fpm"),
|
|
16
|
+
targets: z.record(z.string()).default({ default: "php-fpm" }),
|
|
17
|
+
testing: testing.optional(),
|
|
18
|
+
})
|
|
19
|
+
.strict();
|
|
20
|
+
const ssh = z
|
|
21
|
+
.object({
|
|
22
|
+
type: z.literal("ssh"),
|
|
23
|
+
host: z.string().min(1),
|
|
24
|
+
root: z.string(),
|
|
25
|
+
writable: z.boolean().default(false),
|
|
26
|
+
testing: testing.optional(),
|
|
27
|
+
})
|
|
28
|
+
.strict();
|
|
29
|
+
export const ConfigSchema = z
|
|
30
|
+
.object({
|
|
31
|
+
defaultEnvironment: z.string().optional(),
|
|
32
|
+
activeTheme: z.string().default("all"),
|
|
33
|
+
testing: testing.default({ allowInsecureTls: true }),
|
|
34
|
+
environments: z.record(z.union([local, warden, ssh])).default({}),
|
|
35
|
+
})
|
|
36
|
+
.strict();
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { merchantHome } from "#config/paths";
|
|
5
|
+
export function configDigest(content) {
|
|
6
|
+
return createHash("sha256").update(content).digest("hex");
|
|
7
|
+
}
|
|
8
|
+
export async function trustProject(configPath, env = process.env) {
|
|
9
|
+
const digest = configDigest(await readFile(configPath, "utf8"));
|
|
10
|
+
const path = resolve(merchantHome(env), "trust.json");
|
|
11
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
12
|
+
let trust = {};
|
|
13
|
+
try {
|
|
14
|
+
trust = JSON.parse(await readFile(path, "utf8"));
|
|
15
|
+
}
|
|
16
|
+
catch { }
|
|
17
|
+
trust[resolve(configPath)] = digest;
|
|
18
|
+
await writeFile(path, JSON.stringify(trust, null, 2), { mode: 0o600 });
|
|
19
|
+
}
|
|
20
|
+
export async function isTrusted(configPath, env = process.env) {
|
|
21
|
+
try {
|
|
22
|
+
const trust = JSON.parse(await readFile(resolve(merchantHome(env), "trust.json"), "utf8"));
|
|
23
|
+
return (trust[resolve(configPath)] ===
|
|
24
|
+
configDigest(await readFile(configPath, "utf8")));
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { EnvironmentAdapter } from "#environments/adapter";
|
|
2
|
+
import type { LocalEnvironment } from "#environments/model";
|
|
3
|
+
export declare class LocalAdapter implements EnvironmentAdapter {
|
|
4
|
+
#private;
|
|
5
|
+
constructor(environment: LocalEnvironment, cwd: string);
|
|
6
|
+
path(path: string): string;
|
|
7
|
+
command(argv: string[]): {
|
|
8
|
+
file: string;
|
|
9
|
+
args: string[];
|
|
10
|
+
cwd: string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { contain } from "#environments/path-policy";
|
|
3
|
+
export class LocalAdapter {
|
|
4
|
+
#root;
|
|
5
|
+
constructor(environment, cwd) {
|
|
6
|
+
this.#root = resolve(cwd, environment.root);
|
|
7
|
+
}
|
|
8
|
+
path(path) {
|
|
9
|
+
return contain(this.#root, path);
|
|
10
|
+
}
|
|
11
|
+
command(argv) {
|
|
12
|
+
return { file: argv[0], args: argv.slice(1), cwd: this.#root };
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { EnvironmentAdapter } from "#environments/adapter";
|
|
2
|
+
import type { SshEnvironment } from "#environments/model";
|
|
3
|
+
export declare class SshAdapter implements EnvironmentAdapter {
|
|
4
|
+
readonly environment: SshEnvironment;
|
|
5
|
+
constructor(environment: SshEnvironment);
|
|
6
|
+
path(path: string): string;
|
|
7
|
+
command(argv: string[]): {
|
|
8
|
+
file: string;
|
|
9
|
+
args: string[];
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { contain } from "#environments/path-policy";
|
|
2
|
+
export class SshAdapter {
|
|
3
|
+
environment;
|
|
4
|
+
constructor(environment) {
|
|
5
|
+
this.environment = environment;
|
|
6
|
+
}
|
|
7
|
+
path(path) {
|
|
8
|
+
return path.startsWith("/")
|
|
9
|
+
? contain(this.environment.root, path)
|
|
10
|
+
: `${this.environment.root}/${path}`;
|
|
11
|
+
}
|
|
12
|
+
command(argv) {
|
|
13
|
+
return {
|
|
14
|
+
file: "ssh",
|
|
15
|
+
args: [
|
|
16
|
+
this.environment.host,
|
|
17
|
+
`cd ${shell(this.environment.root)} && ${argv.map(shell).join(" ")}`,
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function shell(value) {
|
|
23
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
24
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { EnvironmentAdapter } from "#environments/adapter";
|
|
2
|
+
import type { WardenEnvironment } from "#environments/model";
|
|
3
|
+
export declare class WardenAdapter implements EnvironmentAdapter {
|
|
4
|
+
readonly environment: WardenEnvironment;
|
|
5
|
+
readonly cwd: string;
|
|
6
|
+
constructor(environment: WardenEnvironment, cwd: string);
|
|
7
|
+
path(path: string): string;
|
|
8
|
+
command(argv: string[], target?: string): {
|
|
9
|
+
file: string;
|
|
10
|
+
args: string[];
|
|
11
|
+
cwd: string;
|
|
12
|
+
};
|
|
13
|
+
}
|