@mittwald/cli 1.10.0 → 1.11.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/bin/run.js +4 -1
- package/dist/commands/app/create/node.d.ts +1 -3
- package/dist/commands/app/create/php-worker.d.ts +1 -3
- package/dist/commands/app/create/php.d.ts +1 -3
- package/dist/commands/app/create/python.d.ts +1 -3
- package/dist/commands/app/create/static.d.ts +1 -3
- package/dist/commands/app/dependency/update.js +2 -4
- package/dist/commands/app/install/contao.d.ts +1 -3
- package/dist/commands/app/install/joomla.d.ts +1 -3
- package/dist/commands/app/install/matomo.d.ts +1 -3
- package/dist/commands/app/install/nextcloud.d.ts +1 -3
- package/dist/commands/app/install/shopware5.d.ts +1 -3
- package/dist/commands/app/install/shopware6.d.ts +1 -3
- package/dist/commands/app/install/typo3.d.ts +1 -3
- package/dist/commands/app/install/wordpress.d.ts +1 -3
- package/dist/commands/app/ssh.d.ts +1 -0
- package/dist/commands/app/ssh.js +15 -1
- package/dist/commands/app/update.js +4 -8
- package/dist/commands/app/upgrade.js +9 -9
- package/dist/commands/backup/create.js +1 -2
- package/dist/commands/backup/download.js +4 -5
- package/dist/commands/container/run.d.ts +4 -2
- package/dist/commands/container/run.js +7 -6
- package/dist/commands/database/mysql/create.js +1 -2
- package/dist/commands/database/mysql/dump.js +1 -2
- package/dist/commands/database/mysql/import.js +1 -2
- package/dist/commands/ddev/init.js +2 -6
- package/dist/commands/extension/install.js +1 -3
- package/dist/commands/login/reset.js +1 -2
- package/dist/commands/mail/address/create.js +1 -4
- package/dist/commands/mail/deliverybox/create.js +1 -4
- package/dist/commands/project/create.js +4 -6
- package/dist/commands/registry/create.js +1 -2
- package/dist/commands/registry/update.js +1 -2
- package/dist/commands/user/api-token/create.js +1 -1
- package/dist/commands/user/ssh-key/create.js +3 -8
- package/dist/commands/user/ssh-key/import.js +2 -3
- package/dist/lib/basecommands/DeleteBaseCommand.js +4 -4
- package/dist/lib/ddev/init_assert.js +1 -6
- package/dist/lib/ddev/init_database.js +1 -7
- package/dist/lib/ddev/init_projecttype.js +2 -11
- package/dist/lib/intellij/config.d.ts +5 -0
- package/dist/lib/intellij/config.js +295 -0
- package/dist/lib/intellij/config.test.d.ts +1 -0
- package/dist/lib/intellij/config.test.js +262 -0
- package/dist/lib/intellij/config_xml_types.d.ts +72 -0
- package/dist/lib/intellij/config_xml_types.js +2 -0
- package/dist/lib/resources/app/Installer.d.ts +6 -2
- package/dist/lib/resources/app/Installer.js +13 -6
- package/dist/lib/resources/app/flags.js +9 -12
- package/dist/lib/resources/app/install.d.ts +2 -1
- package/dist/lib/resources/app/install.js +3 -1
- package/dist/lib/resources/app/versions.js +1 -4
- package/dist/lib/resources/app/wait.js +1 -3
- package/dist/lib/resources/mail/commons.js +1 -4
- package/dist/lib/resources/ssh/appinstall.d.ts +3 -1
- package/dist/lib/resources/ssh/appinstall.js +1 -0
- package/dist/lib/resources/stack/enrich.js +9 -2
- package/dist/lib/resources/stack/enrich.test.d.ts +1 -0
- package/dist/lib/resources/stack/enrich.test.js +167 -0
- package/dist/lib/resources/stack/types.d.ts +1 -1
- package/dist/rendering/process/components/ProcessStateIcon.js +1 -1
- package/dist/rendering/process/process.d.ts +16 -16
- package/dist/rendering/process/process_exec.d.ts +1 -2
- package/dist/rendering/process/process_fancy.d.ts +9 -9
- package/dist/rendering/process/process_flags.js +4 -0
- package/dist/rendering/process/process_quiet.d.ts +3 -4
- package/dist/rendering/process/process_simple.d.ts +26 -0
- package/dist/rendering/process/process_simple.js +143 -0
- package/dist/rendering/process/process_simple.test.d.ts +1 -0
- package/dist/rendering/process/process_simple.test.js +149 -0
- package/dist/rendering/react/components/AppInstallation/AppBackendAccessHints.d.ts +15 -0
- package/dist/rendering/react/components/AppInstallation/AppBackendAccessHints.js +13 -0
- package/dist/rendering/react/components/AppInstallation/AppDomainConnectionHints.d.ts +12 -0
- package/dist/rendering/react/components/AppInstallation/AppDomainConnectionHints.js +21 -0
- package/dist/rendering/react/components/AppInstallation/AppManagementCommands.d.ts +12 -0
- package/dist/rendering/react/components/AppInstallation/AppManagementCommands.js +17 -0
- package/dist/rendering/react/components/AppInstallation/AppUsageHints.d.ts +17 -0
- package/dist/rendering/react/components/AppInstallation/AppUsageHints.js +23 -0
- package/dist/rendering/react/components/Container/CommandHint.d.ts +14 -0
- package/dist/rendering/react/components/Container/CommandHint.js +13 -0
- package/dist/rendering/react/components/Container/ContainerManagementCommands.d.ts +22 -0
- package/dist/rendering/react/components/Container/ContainerManagementCommands.js +23 -0
- package/dist/rendering/react/components/Container/ContainerUsageHints.d.ts +12 -0
- package/dist/rendering/react/components/Container/ContainerUsageHints.js +35 -0
- package/dist/rendering/react/components/Container/DomainConnectionHints.d.ts +12 -0
- package/dist/rendering/react/components/Container/DomainConnectionHints.js +21 -0
- package/dist/rendering/react/components/Container/InternalConnectionHints.d.ts +12 -0
- package/dist/rendering/react/components/Container/InternalConnectionHints.js +12 -0
- package/dist/rendering/react/components/Container/NoPortsUsageHints.d.ts +12 -0
- package/dist/rendering/react/components/Container/NoPortsUsageHints.js +12 -0
- package/dist/rendering/react/components/Container/PortConnectionHints.d.ts +13 -0
- package/dist/rendering/react/components/Container/PortConnectionHints.js +11 -0
- package/dist/rendering/react/components/Container/PortForwardingHints.d.ts +12 -0
- package/dist/rendering/react/components/Container/PortForwardingHints.js +18 -0
- package/dist/rendering/react/components/Container/types.d.ts +4 -0
- package/dist/rendering/react/components/Container/types.js +1 -0
- package/dist/rendering/react/components/Error/ErrorBox.d.ts +1 -1
- package/dist/rendering/react/components/Error/ErrorBox.js +3 -4
- package/dist/rendering/react/components/Error/GenericError.js +1 -1
- package/dist/rendering/react/components/ErrorBoundary.d.ts +1 -1
- package/dist/rendering/react/components/Success.d.ts +0 -1
- package/dist/rendering/react/components/Success.js +4 -2
- package/dist/rendering/react/styles/useDefaultBoxStyles.d.ts +11 -0
- package/dist/rendering/react/styles/useDefaultBoxStyles.js +23 -0
- package/package.json +7 -6
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach, afterEach } from "@jest/globals";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import * as os from "os";
|
|
5
|
+
import { XMLParser } from "fast-xml-parser";
|
|
6
|
+
import { generateIntellijConfigs } from "./config.js";
|
|
7
|
+
describe("IntelliJ Config Generator", () => {
|
|
8
|
+
let tempDir;
|
|
9
|
+
let testData;
|
|
10
|
+
const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: "@_" });
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "intellij-test-"));
|
|
13
|
+
testData = {
|
|
14
|
+
host: "ssh.test.example.com",
|
|
15
|
+
user: "testuser@app123",
|
|
16
|
+
directory: "/var/www/html/app123",
|
|
17
|
+
appShortId: "app123",
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
describe("generateIntellijConfigs", () => {
|
|
24
|
+
test("should create .idea directory if it doesn't exist", () => {
|
|
25
|
+
generateIntellijConfigs(testData, tempDir);
|
|
26
|
+
const ideaDir = path.join(tempDir, ".idea");
|
|
27
|
+
expect(fs.existsSync(ideaDir)).toBe(true);
|
|
28
|
+
expect(fs.statSync(ideaDir).isDirectory()).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
test("should generate all three configuration files", () => {
|
|
31
|
+
generateIntellijConfigs(testData, tempDir);
|
|
32
|
+
const ideaDir = path.join(tempDir, ".idea");
|
|
33
|
+
expect(fs.existsSync(path.join(ideaDir, "sshConfigs.xml"))).toBe(true);
|
|
34
|
+
expect(fs.existsSync(path.join(ideaDir, "webServers.xml"))).toBe(true);
|
|
35
|
+
expect(fs.existsSync(path.join(ideaDir, "deployment.xml"))).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
test("should generate valid XML files", () => {
|
|
38
|
+
generateIntellijConfigs(testData, tempDir);
|
|
39
|
+
const ideaDir = path.join(tempDir, ".idea");
|
|
40
|
+
// Test that each file can be parsed without errors
|
|
41
|
+
const sshContent = fs.readFileSync(path.join(ideaDir, "sshConfigs.xml"), "utf8");
|
|
42
|
+
expect(() => parser.parse(sshContent)).not.toThrow();
|
|
43
|
+
const webContent = fs.readFileSync(path.join(ideaDir, "webServers.xml"), "utf8");
|
|
44
|
+
expect(() => parser.parse(webContent)).not.toThrow();
|
|
45
|
+
const deployContent = fs.readFileSync(path.join(ideaDir, "deployment.xml"), "utf8");
|
|
46
|
+
expect(() => parser.parse(deployContent)).not.toThrow();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe("SSH Configs XML", () => {
|
|
50
|
+
test("should create SSH config with correct attributes", () => {
|
|
51
|
+
generateIntellijConfigs(testData, tempDir);
|
|
52
|
+
const configPath = path.join(tempDir, ".idea", "sshConfigs.xml");
|
|
53
|
+
const content = fs.readFileSync(configPath, "utf8");
|
|
54
|
+
const xmlDoc = parser.parse(content);
|
|
55
|
+
expect(xmlDoc.project["@_version"]).toBe("4");
|
|
56
|
+
expect(xmlDoc.project.component["@_name"]).toBe("SshConfigs");
|
|
57
|
+
const sshConfig = xmlDoc.project.component.configs.sshConfig;
|
|
58
|
+
expect(sshConfig["@_authType"]).toBe("OPEN_SSH");
|
|
59
|
+
expect(sshConfig["@_host"]).toBe("ssh.test.example.com");
|
|
60
|
+
expect(sshConfig["@_port"]).toBe("22");
|
|
61
|
+
expect(sshConfig["@_username"]).toBe("testuser@app123");
|
|
62
|
+
expect(sshConfig["@_useOpenSSHConfig"]).toBe("true");
|
|
63
|
+
expect(sshConfig["@_nameFormat"]).toBe("DESCRIPTIVE");
|
|
64
|
+
expect(sshConfig["@_id"]).toBeDefined();
|
|
65
|
+
});
|
|
66
|
+
test("should not add duplicate SSH configs for same host", () => {
|
|
67
|
+
generateIntellijConfigs(testData, tempDir);
|
|
68
|
+
generateIntellijConfigs(testData, tempDir); // Run twice
|
|
69
|
+
const configPath = path.join(tempDir, ".idea", "sshConfigs.xml");
|
|
70
|
+
const content = fs.readFileSync(configPath, "utf8");
|
|
71
|
+
const xmlDoc = parser.parse(content);
|
|
72
|
+
const sshConfig = xmlDoc.project.component.configs.sshConfig;
|
|
73
|
+
expect(Array.isArray(sshConfig)).toBe(false); // Should still be single config
|
|
74
|
+
});
|
|
75
|
+
test("should add multiple SSH configs for different hosts", () => {
|
|
76
|
+
generateIntellijConfigs(testData, tempDir);
|
|
77
|
+
const differentHostData = { ...testData, host: "ssh.other.example.com", appShortId: "app456" };
|
|
78
|
+
generateIntellijConfigs(differentHostData, tempDir);
|
|
79
|
+
const configPath = path.join(tempDir, ".idea", "sshConfigs.xml");
|
|
80
|
+
const content = fs.readFileSync(configPath, "utf8");
|
|
81
|
+
const xmlDoc = parser.parse(content);
|
|
82
|
+
const sshConfigs = xmlDoc.project.component.configs.sshConfig;
|
|
83
|
+
expect(Array.isArray(sshConfigs)).toBe(true);
|
|
84
|
+
expect(sshConfigs).toHaveLength(2);
|
|
85
|
+
expect(sshConfigs[0]["@_host"]).toBe("ssh.test.example.com");
|
|
86
|
+
expect(sshConfigs[1]["@_host"]).toBe("ssh.other.example.com");
|
|
87
|
+
});
|
|
88
|
+
test("should handle existing SSH config file correctly", () => {
|
|
89
|
+
const ideaDir = path.join(tempDir, ".idea");
|
|
90
|
+
fs.mkdirSync(ideaDir, { recursive: true });
|
|
91
|
+
const existingConfig = `<?xml version="1.0" encoding="UTF-8"?>
|
|
92
|
+
<project version="4">
|
|
93
|
+
<component name="SshConfigs">
|
|
94
|
+
<configs>
|
|
95
|
+
<sshConfig authType="OPEN_SSH" host="existing.example.com" id="existing-id" port="22" nameFormat="DESCRIPTIVE" username="existing" useOpenSSHConfig="true" />
|
|
96
|
+
</configs>
|
|
97
|
+
</component>
|
|
98
|
+
</project>`;
|
|
99
|
+
fs.writeFileSync(path.join(ideaDir, "sshConfigs.xml"), existingConfig);
|
|
100
|
+
generateIntellijConfigs(testData, tempDir);
|
|
101
|
+
const configPath = path.join(ideaDir, "sshConfigs.xml");
|
|
102
|
+
const content = fs.readFileSync(configPath, "utf8");
|
|
103
|
+
const xmlDoc = parser.parse(content);
|
|
104
|
+
const sshConfigs = xmlDoc.project.component.configs.sshConfig;
|
|
105
|
+
expect(Array.isArray(sshConfigs)).toBe(true);
|
|
106
|
+
expect(sshConfigs).toHaveLength(2);
|
|
107
|
+
});
|
|
108
|
+
test("should reuse existing SSH config ID in web server configuration", () => {
|
|
109
|
+
const ideaDir = path.join(tempDir, ".idea");
|
|
110
|
+
fs.mkdirSync(ideaDir, { recursive: true });
|
|
111
|
+
// Create existing SSH config with the same host as testData
|
|
112
|
+
const existingConfig = `<?xml version="1.0" encoding="UTF-8"?>
|
|
113
|
+
<project version="4">
|
|
114
|
+
<component name="SshConfigs">
|
|
115
|
+
<configs>
|
|
116
|
+
<sshConfig authType="OPEN_SSH" host="ssh.test.example.com" id="existing-ssh-id" port="22" nameFormat="DESCRIPTIVE" username="testuser@app123" useOpenSSHConfig="true" />
|
|
117
|
+
</configs>
|
|
118
|
+
</component>
|
|
119
|
+
</project>`;
|
|
120
|
+
fs.writeFileSync(path.join(ideaDir, "sshConfigs.xml"), existingConfig);
|
|
121
|
+
generateIntellijConfigs(testData, tempDir);
|
|
122
|
+
// Verify SSH config wasn't duplicated
|
|
123
|
+
const sshConfigPath = path.join(ideaDir, "sshConfigs.xml");
|
|
124
|
+
const sshContent = fs.readFileSync(sshConfigPath, "utf8");
|
|
125
|
+
const sshXmlDoc = parser.parse(sshContent);
|
|
126
|
+
const sshConfig = sshXmlDoc.project.component.configs.sshConfig;
|
|
127
|
+
expect(Array.isArray(sshConfig)).toBe(false); // Should still be single config
|
|
128
|
+
expect(sshConfig["@_id"]).toBe("existing-ssh-id");
|
|
129
|
+
// Verify web server config uses the existing SSH config ID
|
|
130
|
+
const webConfigPath = path.join(ideaDir, "webServers.xml");
|
|
131
|
+
const webContent = fs.readFileSync(webConfigPath, "utf8");
|
|
132
|
+
const webXmlDoc = parser.parse(webContent);
|
|
133
|
+
const webServer = webXmlDoc.project.component.option.webServer;
|
|
134
|
+
expect(webServer.fileTransfer["@_sshConfigId"]).toBe("existing-ssh-id");
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
describe("Web Servers XML", () => {
|
|
138
|
+
test("should create web server config with correct structure", () => {
|
|
139
|
+
generateIntellijConfigs(testData, tempDir);
|
|
140
|
+
const configPath = path.join(tempDir, ".idea", "webServers.xml");
|
|
141
|
+
const content = fs.readFileSync(configPath, "utf8");
|
|
142
|
+
const xmlDoc = parser.parse(content);
|
|
143
|
+
expect(xmlDoc.project["@_version"]).toBe("4");
|
|
144
|
+
expect(xmlDoc.project.component["@_name"]).toBe("WebServers");
|
|
145
|
+
expect(xmlDoc.project.component.option["@_name"]).toBe("servers");
|
|
146
|
+
const webServer = xmlDoc.project.component.option.webServer;
|
|
147
|
+
expect(webServer["@_name"]).toBe("app123");
|
|
148
|
+
expect(webServer["@_id"]).toBeDefined();
|
|
149
|
+
const fileTransfer = webServer.fileTransfer;
|
|
150
|
+
expect(fileTransfer["@_accessType"]).toBe("SFTP");
|
|
151
|
+
expect(fileTransfer["@_host"]).toBe("ssh.test.example.com");
|
|
152
|
+
expect(fileTransfer["@_port"]).toBe("22");
|
|
153
|
+
expect(fileTransfer["@_authAgent"]).toBe("true");
|
|
154
|
+
expect(fileTransfer["@_sshConfig"]).toBe("testuser@app123@ssh.test.example.com:22 agent");
|
|
155
|
+
const advancedOptions = fileTransfer.advancedOptions.advancedOptions;
|
|
156
|
+
expect(advancedOptions["@_dataProtectionLevel"]).toBe("Private");
|
|
157
|
+
expect(advancedOptions["@_keepAliveTimeout"]).toBe("0");
|
|
158
|
+
expect(advancedOptions["@_passiveMode"]).toBe("true");
|
|
159
|
+
expect(advancedOptions["@_shareSSLContext"]).toBe("true");
|
|
160
|
+
});
|
|
161
|
+
test("should not add duplicate web servers for same app", () => {
|
|
162
|
+
generateIntellijConfigs(testData, tempDir);
|
|
163
|
+
generateIntellijConfigs(testData, tempDir); // Run twice
|
|
164
|
+
const configPath = path.join(tempDir, ".idea", "webServers.xml");
|
|
165
|
+
const content = fs.readFileSync(configPath, "utf8");
|
|
166
|
+
const xmlDoc = parser.parse(content);
|
|
167
|
+
const webServer = xmlDoc.project.component.option.webServer;
|
|
168
|
+
expect(Array.isArray(webServer)).toBe(false); // Should still be single server
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
describe("Deployment XML", () => {
|
|
172
|
+
test("should create deployment config with correct mapping", () => {
|
|
173
|
+
generateIntellijConfigs(testData, tempDir);
|
|
174
|
+
const configPath = path.join(tempDir, ".idea", "deployment.xml");
|
|
175
|
+
const content = fs.readFileSync(configPath, "utf8");
|
|
176
|
+
const xmlDoc = parser.parse(content);
|
|
177
|
+
expect(xmlDoc.project["@_version"]).toBe("4");
|
|
178
|
+
expect(xmlDoc.project.component["@_name"]).toBe("PublishConfigData");
|
|
179
|
+
expect(xmlDoc.project.component["@_serverName"]).toBe("app123");
|
|
180
|
+
expect(xmlDoc.project.component["@_remoteFilesAllowedToDisappearOnAutoupload"]).toBe("false");
|
|
181
|
+
const paths = xmlDoc.project.component.serverData.paths;
|
|
182
|
+
expect(paths["@_name"]).toBe("app123");
|
|
183
|
+
const mapping = paths.serverdata.mappings.mapping;
|
|
184
|
+
expect(mapping["@_deploy"]).toBe("/var/www/html/app123");
|
|
185
|
+
expect(mapping["@_local"]).toBe("$PROJECT_DIR$");
|
|
186
|
+
expect(mapping["@_web"]).toBe("/");
|
|
187
|
+
});
|
|
188
|
+
test("should not add duplicate deployment configs for same app", () => {
|
|
189
|
+
generateIntellijConfigs(testData, tempDir);
|
|
190
|
+
generateIntellijConfigs(testData, tempDir); // Run twice
|
|
191
|
+
const configPath = path.join(tempDir, ".idea", "deployment.xml");
|
|
192
|
+
const content = fs.readFileSync(configPath, "utf8");
|
|
193
|
+
const xmlDoc = parser.parse(content);
|
|
194
|
+
const paths = xmlDoc.project.component.serverData.paths;
|
|
195
|
+
expect(Array.isArray(paths)).toBe(false); // Should still be single path
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
describe("SSH username handling", () => {
|
|
199
|
+
test("should use complete user string as SSH username", () => {
|
|
200
|
+
const userData = { ...testData, user: "m.helmich@mittwald.de@a-ce3rzc" };
|
|
201
|
+
generateIntellijConfigs(userData, tempDir);
|
|
202
|
+
const configPath = path.join(tempDir, ".idea", "sshConfigs.xml");
|
|
203
|
+
const content = fs.readFileSync(configPath, "utf8");
|
|
204
|
+
const xmlDoc = parser.parse(content);
|
|
205
|
+
const sshConfig = xmlDoc.project.component.configs.sshConfig;
|
|
206
|
+
expect(sshConfig["@_username"]).toBe("m.helmich@mittwald.de@a-ce3rzc");
|
|
207
|
+
// Also check web server config uses the same username
|
|
208
|
+
const webConfigPath = path.join(tempDir, ".idea", "webServers.xml");
|
|
209
|
+
const webContent = fs.readFileSync(webConfigPath, "utf8");
|
|
210
|
+
const webXmlDoc = parser.parse(webContent);
|
|
211
|
+
const webServer = webXmlDoc.project.component.option.webServer;
|
|
212
|
+
expect(webServer.fileTransfer["@_sshConfig"]).toBe("m.helmich@mittwald.de@a-ce3rzc@ssh.test.example.com:22 agent");
|
|
213
|
+
});
|
|
214
|
+
test("should handle simple usernames as-is", () => {
|
|
215
|
+
const userData = { ...testData, user: "plainuser" };
|
|
216
|
+
generateIntellijConfigs(userData, tempDir);
|
|
217
|
+
const configPath = path.join(tempDir, ".idea", "sshConfigs.xml");
|
|
218
|
+
const content = fs.readFileSync(configPath, "utf8");
|
|
219
|
+
const xmlDoc = parser.parse(content);
|
|
220
|
+
const sshConfig = xmlDoc.project.component.configs.sshConfig;
|
|
221
|
+
expect(sshConfig["@_username"]).toBe("plainuser");
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
describe("Error handling", () => {
|
|
225
|
+
test("should handle invalid directory gracefully", () => {
|
|
226
|
+
const invalidDir = "/root/nonexistent/readonly";
|
|
227
|
+
// This should throw an error for invalid directory
|
|
228
|
+
expect(() => {
|
|
229
|
+
generateIntellijConfigs(testData, invalidDir);
|
|
230
|
+
}).toThrow("Cannot create .idea directory");
|
|
231
|
+
});
|
|
232
|
+
test("should throw error for malformed existing XML files", () => {
|
|
233
|
+
const ideaDir = path.join(tempDir, ".idea");
|
|
234
|
+
fs.mkdirSync(ideaDir, { recursive: true });
|
|
235
|
+
// Create malformed XML
|
|
236
|
+
const malformedXml = "<?xml version='1.0'?><project><unclosed>";
|
|
237
|
+
fs.writeFileSync(path.join(ideaDir, "sshConfigs.xml"), malformedXml);
|
|
238
|
+
// Should throw an error for malformed XML
|
|
239
|
+
expect(() => {
|
|
240
|
+
generateIntellijConfigs(testData, tempDir);
|
|
241
|
+
}).toThrow();
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
describe("XML structure validation", () => {
|
|
245
|
+
test("should generate properly formatted XML with correct declarations", () => {
|
|
246
|
+
generateIntellijConfigs(testData, tempDir);
|
|
247
|
+
const ideaDir = path.join(tempDir, ".idea");
|
|
248
|
+
// Check SSH config
|
|
249
|
+
const sshContent = fs.readFileSync(path.join(ideaDir, "sshConfigs.xml"), "utf8");
|
|
250
|
+
expect(sshContent).toMatch(/^<\?xml version="1\.0" encoding="UTF-8"\?>/);
|
|
251
|
+
expect(sshContent).toContain('<project version="4">');
|
|
252
|
+
// Check web servers
|
|
253
|
+
const webContent = fs.readFileSync(path.join(ideaDir, "webServers.xml"), "utf8");
|
|
254
|
+
expect(webContent).toMatch(/^<\?xml version="1\.0" encoding="UTF-8"\?>/);
|
|
255
|
+
expect(webContent).toContain('<project version="4">');
|
|
256
|
+
// Check deployment
|
|
257
|
+
const deployContent = fs.readFileSync(path.join(ideaDir, "deployment.xml"), "utf8");
|
|
258
|
+
expect(deployContent).toMatch(/^<\?xml version="1\.0" encoding="UTF-8"\?>/);
|
|
259
|
+
expect(deployContent).toContain('<project version="4">');
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export interface XmlDocument {
|
|
2
|
+
"?xml": {
|
|
3
|
+
"@_version": string;
|
|
4
|
+
"@_encoding": string;
|
|
5
|
+
};
|
|
6
|
+
project: XmlProject;
|
|
7
|
+
}
|
|
8
|
+
export interface XmlProject {
|
|
9
|
+
"@_version": string;
|
|
10
|
+
component: XmlComponent;
|
|
11
|
+
}
|
|
12
|
+
export interface XmlComponent {
|
|
13
|
+
"@_name": string;
|
|
14
|
+
configs?: XmlConfigs;
|
|
15
|
+
option?: XmlOption;
|
|
16
|
+
serverData?: XmlServerData;
|
|
17
|
+
"@_serverName"?: string;
|
|
18
|
+
"@_remoteFilesAllowedToDisappearOnAutoupload"?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface XmlConfigs {
|
|
21
|
+
sshConfig?: XmlSshConfig | XmlSshConfig[];
|
|
22
|
+
}
|
|
23
|
+
export interface XmlSshConfig {
|
|
24
|
+
"@_authType": string;
|
|
25
|
+
"@_host": string;
|
|
26
|
+
"@_id": string;
|
|
27
|
+
"@_port": string;
|
|
28
|
+
"@_nameFormat": string;
|
|
29
|
+
"@_username": string;
|
|
30
|
+
"@_useOpenSSHConfig": string;
|
|
31
|
+
}
|
|
32
|
+
export interface XmlOption {
|
|
33
|
+
"@_name": string;
|
|
34
|
+
webServer?: XmlWebServer | XmlWebServer[];
|
|
35
|
+
}
|
|
36
|
+
export interface XmlWebServer {
|
|
37
|
+
"@_id": string;
|
|
38
|
+
"@_name": string;
|
|
39
|
+
fileTransfer: XmlFileTransfer;
|
|
40
|
+
}
|
|
41
|
+
export interface XmlFileTransfer {
|
|
42
|
+
"@_accessType": string;
|
|
43
|
+
"@_host": string;
|
|
44
|
+
"@_port": string;
|
|
45
|
+
"@_sshConfigId": string;
|
|
46
|
+
"@_sshConfig": string;
|
|
47
|
+
"@_authAgent": string;
|
|
48
|
+
advancedOptions: XmlAdvancedOptions;
|
|
49
|
+
}
|
|
50
|
+
export interface XmlAdvancedOptions {
|
|
51
|
+
advancedOptions: {
|
|
52
|
+
"@_dataProtectionLevel": string;
|
|
53
|
+
"@_keepAliveTimeout": string;
|
|
54
|
+
"@_passiveMode": string;
|
|
55
|
+
"@_shareSSLContext": string;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export interface XmlServerData {
|
|
59
|
+
paths?: XmlPath | XmlPath[];
|
|
60
|
+
}
|
|
61
|
+
export interface XmlPath {
|
|
62
|
+
"@_name": string;
|
|
63
|
+
serverdata: {
|
|
64
|
+
mappings: {
|
|
65
|
+
mapping: {
|
|
66
|
+
"@_deploy": string;
|
|
67
|
+
"@_local": string;
|
|
68
|
+
"@_web": string;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { OutputFlags } from "@oclif/core/interfaces";
|
|
2
2
|
import { AvailableFlagName, RelevantFlagInput } from "./flags.js";
|
|
3
3
|
import React from "react";
|
|
4
|
-
import { MittwaldAPIV2Client } from "@mittwald/api-client";
|
|
4
|
+
import { MittwaldAPIV2, MittwaldAPIV2Client } from "@mittwald/api-client";
|
|
5
5
|
import { Config } from "@oclif/core";
|
|
6
|
+
type AppVersion = MittwaldAPIV2.Components.Schemas.AppAppVersion;
|
|
7
|
+
type AppInstallation = MittwaldAPIV2.Components.Schemas.AppAppInstallation;
|
|
6
8
|
type ImplicitDefaultFlag = "wait" | "wait-timeout" | "site-title";
|
|
7
9
|
export interface AppInstallationResult {
|
|
8
|
-
|
|
10
|
+
appInstallation: AppInstallation;
|
|
11
|
+
appVersion: AppVersion;
|
|
12
|
+
host?: string;
|
|
9
13
|
}
|
|
10
14
|
export declare class AppInstaller<TFlagName extends AvailableFlagName> {
|
|
11
15
|
readonly appId: string;
|
|
@@ -6,6 +6,7 @@ import { normalizeToAppVersionUuid } from "./versions.js";
|
|
|
6
6
|
import { triggerAppInstallation } from "./install.js";
|
|
7
7
|
import { waitUntilAppStateHasNormalized } from "./wait.js";
|
|
8
8
|
import { Success } from "../../../rendering/react/components/Success.js";
|
|
9
|
+
import AppUsageHints from "../../../rendering/react/components/AppInstallation/AppUsageHints.js";
|
|
9
10
|
export class AppInstaller {
|
|
10
11
|
appId;
|
|
11
12
|
appName;
|
|
@@ -35,22 +36,28 @@ export class AppInstaller {
|
|
|
35
36
|
const projectId = await withProjectId(apiClient, "flag", flags, args, config);
|
|
36
37
|
await autofillFlags(apiClient, process, this.appSupportedFlags, flags, projectId, this.appName, this.defaultFlagValues);
|
|
37
38
|
const appVersion = await normalizeToAppVersionUuid(apiClient, "version" in flags ? flags.version : "latest", process, this.appId);
|
|
38
|
-
const
|
|
39
|
+
const appInstallation = await triggerAppInstallation(apiClient, process, projectId, flags, appVersion);
|
|
39
40
|
let successText;
|
|
40
41
|
if (flags.wait) {
|
|
41
|
-
await waitUntilAppStateHasNormalized(apiClient, process,
|
|
42
|
+
await waitUntilAppStateHasNormalized(apiClient, process, appInstallation.id, "waiting for app installation to be ready", flags["wait-timeout"]);
|
|
42
43
|
successText = `Your ${this.appName} installation is now complete. Have fun! 🎉`;
|
|
43
44
|
}
|
|
44
45
|
else {
|
|
45
46
|
successText = `Your ${this.appName} installation has started. Have fun when it's ready! 🎉`;
|
|
46
47
|
}
|
|
47
|
-
process.complete(_jsx(Success, { children: successText }));
|
|
48
|
-
return {
|
|
48
|
+
await process.complete(_jsx(Success, { children: successText }));
|
|
49
|
+
return {
|
|
50
|
+
appInstallation,
|
|
51
|
+
appVersion,
|
|
52
|
+
host: "host" in flags && typeof flags.host === "string"
|
|
53
|
+
? flags.host
|
|
54
|
+
: undefined,
|
|
55
|
+
};
|
|
49
56
|
}
|
|
50
57
|
render(result, flags) {
|
|
51
58
|
if (flags.quiet) {
|
|
52
|
-
return result.
|
|
59
|
+
return result.appInstallation.id;
|
|
53
60
|
}
|
|
54
|
-
return
|
|
61
|
+
return (_jsx(AppUsageHints, { appInstallation: result.appInstallation, appVersion: result.appVersion, appName: this.appName, appHost: result.host }));
|
|
55
62
|
}
|
|
56
63
|
}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
1
|
import { getDefaultIngressForProject } from "../project/ingress.js";
|
|
3
|
-
import { Value } from "../../../rendering/react/components/Value.js";
|
|
4
2
|
import { getProjectShortIdFromUuid } from "../project/shortId.js";
|
|
5
|
-
import { Text } from "ink";
|
|
6
3
|
import { assertStatus } from "@mittwald/api-client-commons";
|
|
7
4
|
import { projectFlags } from "../project/flags.js";
|
|
8
5
|
import { processFlags, } from "../../../rendering/process/process_flags.js";
|
|
@@ -169,7 +166,7 @@ export async function autofillFlags(apiClient, process, necessaryFlags, flags, p
|
|
|
169
166
|
if (necessaryFlags.includes("host") && !flags.host) {
|
|
170
167
|
flags.host =
|
|
171
168
|
"https://" + (await getDefaultIngressForProject(apiClient, projectId));
|
|
172
|
-
process.addInfo(
|
|
169
|
+
process.addInfo(`Using default Host ${flags["host"]}`);
|
|
173
170
|
}
|
|
174
171
|
// Title
|
|
175
172
|
if (necessaryFlags.includes("site-title") && !flags["site-title"]) {
|
|
@@ -186,12 +183,12 @@ export async function autofillFlags(apiClient, process, necessaryFlags, flags, p
|
|
|
186
183
|
else {
|
|
187
184
|
flags["admin-user"] = await getProjectShortIdFromUuid(apiClient, projectId);
|
|
188
185
|
}
|
|
189
|
-
process.addInfo(
|
|
186
|
+
process.addInfo(`Using generated Admin User: ${flags["admin-user"]}`);
|
|
190
187
|
}
|
|
191
188
|
// Admin Pass
|
|
192
189
|
if (necessaryFlags.includes("admin-pass") && !flags["admin-pass"]) {
|
|
193
190
|
flags["admin-pass"] = generatePasswordWithSpecialChars();
|
|
194
|
-
process.addInfo(
|
|
191
|
+
process.addInfo(`Using generated random Admin Pass: ${flags["admin-pass"]}`);
|
|
195
192
|
}
|
|
196
193
|
// Admin Firstname
|
|
197
194
|
if (necessaryFlags.includes("admin-firstname") && !flags["admin-firstname"]) {
|
|
@@ -201,7 +198,7 @@ export async function autofillFlags(apiClient, process, necessaryFlags, flags, p
|
|
|
201
198
|
else {
|
|
202
199
|
flags["admin-firstname"] = "Max";
|
|
203
200
|
}
|
|
204
|
-
process.addInfo(
|
|
201
|
+
process.addInfo(`Using mStudio firstname as Admin firstname (${flags["admin-firstname"]})`);
|
|
205
202
|
}
|
|
206
203
|
// Admin Lastname
|
|
207
204
|
if (necessaryFlags.includes("admin-lastname") && !flags["admin-lastname"]) {
|
|
@@ -211,26 +208,26 @@ export async function autofillFlags(apiClient, process, necessaryFlags, flags, p
|
|
|
211
208
|
else {
|
|
212
209
|
flags["admin-lastname"] = "Mustermann";
|
|
213
210
|
}
|
|
214
|
-
process.addInfo(
|
|
211
|
+
process.addInfo(`Using mStudio lastname as Admin lastname (${flags["admin-lastname"]})`);
|
|
215
212
|
}
|
|
216
213
|
// Admin E-Mail
|
|
217
214
|
if (necessaryFlags.includes("admin-email") && !flags["admin-email"]) {
|
|
218
215
|
flags["admin-email"] = ownUser.data.email;
|
|
219
|
-
process.addInfo(
|
|
216
|
+
process.addInfo(`Using mStudio email as Admin email (${flags["admin-email"]})`);
|
|
220
217
|
}
|
|
221
218
|
// Shop E-Mail
|
|
222
219
|
if (necessaryFlags.includes("shop-email") && !flags["shop-email"]) {
|
|
223
220
|
flags["shop-email"] = ownUser.data.email;
|
|
224
|
-
process.addInfo(
|
|
221
|
+
process.addInfo(`Using mStudio email as Shop email (${flags["shop-email"]})`);
|
|
225
222
|
}
|
|
226
223
|
// Shop Language Code
|
|
227
224
|
if (necessaryFlags.includes("shop-lang") && !flags["shop-lang"]) {
|
|
228
225
|
flags["shop-lang"] = defaults["shop-lang"] ?? "de-DE";
|
|
229
|
-
process.addInfo(
|
|
226
|
+
process.addInfo(`Using default shop language '${flags["shop-lang"]}'.`);
|
|
230
227
|
}
|
|
231
228
|
// Shop Currency
|
|
232
229
|
if (necessaryFlags.includes("shop-currency") && !flags["shop-currency"]) {
|
|
233
230
|
flags["shop-currency"] = "EUR";
|
|
234
|
-
process.addInfo(
|
|
231
|
+
process.addInfo("Using default shop currency '€'.");
|
|
235
232
|
}
|
|
236
233
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { MittwaldAPIV2, MittwaldAPIV2Client } from "@mittwald/api-client";
|
|
2
2
|
import { ProcessRenderer } from "../../../rendering/process/process.js";
|
|
3
|
+
type AppAppInstallation = MittwaldAPIV2.Components.Schemas.AppAppInstallation;
|
|
3
4
|
type AppAppVersion = MittwaldAPIV2.Components.Schemas.AppAppVersion;
|
|
4
5
|
export declare function triggerAppInstallation(apiClient: MittwaldAPIV2Client, process: ProcessRenderer, projectId: string, flags: {
|
|
5
6
|
"site-title": string;
|
|
6
7
|
"document-root"?: string;
|
|
7
8
|
} & {
|
|
8
9
|
[k: string]: unknown;
|
|
9
|
-
}, appVersion: AppAppVersion): Promise<
|
|
10
|
+
}, appVersion: AppAppVersion): Promise<AppAppInstallation>;
|
|
10
11
|
export {};
|
|
@@ -38,5 +38,7 @@ export async function triggerAppInstallation(apiClient, process, projectId, flag
|
|
|
38
38
|
assertStatus(result, 204);
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
const result = await apiClient.app.getAppinstallation({ appInstallationId });
|
|
42
|
+
assertStatus(result, 200);
|
|
43
|
+
return result.data;
|
|
42
44
|
}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
1
|
import { assertStatus } from "@mittwald/api-client-commons";
|
|
3
2
|
import { gt } from "semver";
|
|
4
|
-
import { Value } from "../../../rendering/react/components/Value.js";
|
|
5
|
-
import { Text } from "ink";
|
|
6
3
|
import { getAppInstallationFromUuid, getAppNameFromUuid } from "./uuid.js";
|
|
7
4
|
import { compare } from "semver";
|
|
8
5
|
export async function normalizeToAppVersionUuid(apiClient, version, process, appUuid) {
|
|
@@ -16,7 +13,7 @@ export async function normalizeToAppVersionUuid(apiClient, version, process, app
|
|
|
16
13
|
if (!appVersion) {
|
|
17
14
|
throw new Error(`${await getAppNameFromUuid(apiClient, appUuid)} version ${version} does not seem to exist for the mStudio.`);
|
|
18
15
|
}
|
|
19
|
-
process.addInfo(
|
|
16
|
+
process.addInfo(`installing version: ${appVersion.externalVersion}`);
|
|
20
17
|
return appVersion;
|
|
21
18
|
}
|
|
22
19
|
// Get latest available Internal App Version for App UUID
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
1
|
import { waitUntil } from "../../wait.js";
|
|
3
|
-
import { Text } from "ink";
|
|
4
2
|
export async function waitUntilAppStateHasNormalized(apiClient, process, appInstallationId, label, timeout) {
|
|
5
|
-
const stepWaiting = process.addStep(
|
|
3
|
+
const stepWaiting = process.addStep(label);
|
|
6
4
|
await waitUntil(async () => {
|
|
7
5
|
const installationResponse = await apiClient.app.getAppinstallation({
|
|
8
6
|
appInstallationId,
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
1
|
import { generatePassword } from "../../util/password/generatePassword.js";
|
|
3
|
-
import { Value } from "../../../rendering/react/components/Value.js";
|
|
4
|
-
import { Text } from "ink";
|
|
5
2
|
export async function generateRandomPassword(process) {
|
|
6
3
|
const generated = await process.runStep("generating random password", async () => generatePassword(32));
|
|
7
|
-
process.addInfo(
|
|
4
|
+
process.addInfo(` generated password: ${generated} `);
|
|
8
5
|
return generated;
|
|
9
6
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import { MittwaldAPIV2Client } from "@mittwald/api-client";
|
|
2
2
|
import { SSHConnectionData } from "./types.js";
|
|
3
|
-
export declare function getSSHConnectionForAppInstallation(client: MittwaldAPIV2Client, appInstallationId: string, sshUser: string | undefined): Promise<SSHConnectionData
|
|
3
|
+
export declare function getSSHConnectionForAppInstallation(client: MittwaldAPIV2Client, appInstallationId: string, sshUser: string | undefined): Promise<SSHConnectionData & {
|
|
4
|
+
appShortId: string;
|
|
5
|
+
}>;
|
|
@@ -14,8 +14,15 @@ async function setEnvironmentFromEnvFile(service) {
|
|
|
14
14
|
return service;
|
|
15
15
|
}
|
|
16
16
|
const enriched = structuredClone(service);
|
|
17
|
-
const
|
|
18
|
-
|
|
17
|
+
const envFiles = Array.isArray(service.env_file)
|
|
18
|
+
? service.env_file
|
|
19
|
+
: [service.env_file];
|
|
20
|
+
let envVars = {};
|
|
21
|
+
for (const envFile of envFiles) {
|
|
22
|
+
const envFileContent = await readFile(envFile, "utf-8");
|
|
23
|
+
const fileEnvVars = parse(envFileContent);
|
|
24
|
+
envVars = { ...envVars, ...fileEnvVars };
|
|
25
|
+
}
|
|
19
26
|
delete enriched.env_file;
|
|
20
27
|
enriched.envs = {
|
|
21
28
|
...envVars,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|