@mittwald/cli 1.9.1 → 1.11.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/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/download.js +2 -2
- package/dist/commands/app/exec.d.ts +17 -0
- package/dist/commands/app/exec.js +71 -0
- 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/exec.d.ts +1 -7
- package/dist/commands/container/exec.js +11 -22
- 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/hooks/prerun/update-brew-check.js +1 -1
- 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/ssh/environment.d.ts +7 -0
- package/dist/lib/resources/ssh/environment.js +21 -0
- 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 +6 -5
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { randomUUID } from "crypto";
|
|
4
|
+
import { XMLBuilder, XMLParser } from "fast-xml-parser";
|
|
5
|
+
// Common XML configuration
|
|
6
|
+
const XML_PARSER_CONFIG = {
|
|
7
|
+
ignoreAttributes: false,
|
|
8
|
+
attributeNamePrefix: "@_",
|
|
9
|
+
parseAttributeValue: false,
|
|
10
|
+
};
|
|
11
|
+
const XML_BUILDER_CONFIG = {
|
|
12
|
+
ignoreAttributes: false,
|
|
13
|
+
attributeNamePrefix: "@_",
|
|
14
|
+
format: true,
|
|
15
|
+
suppressBooleanAttributes: false,
|
|
16
|
+
};
|
|
17
|
+
// Common XML document structure
|
|
18
|
+
const createXmlDocumentBase = () => ({
|
|
19
|
+
"?xml": { "@_version": "1.0", "@_encoding": "UTF-8" },
|
|
20
|
+
project: { "@_version": "4" },
|
|
21
|
+
});
|
|
22
|
+
export function generateIntellijConfigs(data, projectDir = ".") {
|
|
23
|
+
const ideaDir = path.join(projectDir, ".idea");
|
|
24
|
+
try {
|
|
25
|
+
if (!fs.existsSync(ideaDir)) {
|
|
26
|
+
fs.mkdirSync(ideaDir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
throw new Error(`Cannot create .idea directory: ${error instanceof Error ? error.message : error}`);
|
|
31
|
+
}
|
|
32
|
+
const webServerId = randomUUID();
|
|
33
|
+
// First, ensure SSH config exists and get its ID (existing or newly created)
|
|
34
|
+
const sshConfigId = ensureSshConfigExists(data, ideaDir);
|
|
35
|
+
// Then create web server and deployment configs using the correct SSH config ID
|
|
36
|
+
generateWebServersXml(data, webServerId, sshConfigId, ideaDir);
|
|
37
|
+
generateDeploymentXml(data, ideaDir);
|
|
38
|
+
}
|
|
39
|
+
// Common XML manipulation utility
|
|
40
|
+
function manipulateXmlFile(ideaDir, config, newItem) {
|
|
41
|
+
const configPath = path.join(ideaDir, config.filename);
|
|
42
|
+
const parser = new XMLParser(XML_PARSER_CONFIG);
|
|
43
|
+
const builder = new XMLBuilder(XML_BUILDER_CONFIG);
|
|
44
|
+
let xmlDoc = loadExistingXmlDocument(configPath, parser);
|
|
45
|
+
if (xmlDoc) {
|
|
46
|
+
const wasAdded = tryAddToExistingDocument(xmlDoc, newItem, config);
|
|
47
|
+
if (!wasAdded) {
|
|
48
|
+
return; // Item already exists
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
xmlDoc = config.createNewDocumentFn(newItem);
|
|
53
|
+
}
|
|
54
|
+
const xmlContent = builder.build(xmlDoc);
|
|
55
|
+
fs.writeFileSync(configPath, xmlContent);
|
|
56
|
+
}
|
|
57
|
+
function loadExistingXmlDocument(configPath, parser) {
|
|
58
|
+
if (!fs.existsSync(configPath)) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const content = fs.readFileSync(configPath, "utf8");
|
|
62
|
+
const xmlDoc = parser.parse(content);
|
|
63
|
+
if (!xmlDoc.project?.component) {
|
|
64
|
+
throw new Error("Invalid XML structure");
|
|
65
|
+
}
|
|
66
|
+
return xmlDoc;
|
|
67
|
+
}
|
|
68
|
+
function tryAddToExistingDocument(xmlDoc, newItem, config) {
|
|
69
|
+
// This function delegates to the specific addItemFn
|
|
70
|
+
// Return false if item already exists, true if added
|
|
71
|
+
return config.addItemFn(xmlDoc, newItem);
|
|
72
|
+
}
|
|
73
|
+
// Helper function to handle array/single element patterns
|
|
74
|
+
function ensureArray(item) {
|
|
75
|
+
return Array.isArray(item) ? item : [item];
|
|
76
|
+
}
|
|
77
|
+
// Specific type-safe helper functions for each XML structure
|
|
78
|
+
function addSshConfig(configs, newConfig) {
|
|
79
|
+
if (Array.isArray(configs.sshConfig)) {
|
|
80
|
+
configs.sshConfig.push(newConfig);
|
|
81
|
+
}
|
|
82
|
+
else if (configs.sshConfig) {
|
|
83
|
+
configs.sshConfig = [configs.sshConfig, newConfig];
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
configs.sshConfig = newConfig;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function addWebServer(option, newServer) {
|
|
90
|
+
if (Array.isArray(option.webServer)) {
|
|
91
|
+
option.webServer.push(newServer);
|
|
92
|
+
}
|
|
93
|
+
else if (option.webServer) {
|
|
94
|
+
option.webServer = [option.webServer, newServer];
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
option.webServer = newServer;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function addDeploymentPath(serverData, newPath) {
|
|
101
|
+
if (Array.isArray(serverData.paths)) {
|
|
102
|
+
serverData.paths.push(newPath);
|
|
103
|
+
}
|
|
104
|
+
else if (serverData.paths) {
|
|
105
|
+
serverData.paths = [serverData.paths, newPath];
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
serverData.paths = newPath;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Function to ensure SSH config exists and return its ID
|
|
112
|
+
function ensureSshConfigExists(data, ideaDir) {
|
|
113
|
+
const configPath = path.join(ideaDir, "sshConfigs.xml");
|
|
114
|
+
const parser = new XMLParser(XML_PARSER_CONFIG);
|
|
115
|
+
const existingXmlDoc = loadExistingXmlDocument(configPath, parser);
|
|
116
|
+
if (existingXmlDoc) {
|
|
117
|
+
// Check if SSH config for this host already exists
|
|
118
|
+
const configs = existingXmlDoc.project.component.configs;
|
|
119
|
+
if (configs?.sshConfig) {
|
|
120
|
+
const existingConfigs = ensureArray(configs.sshConfig);
|
|
121
|
+
const existingConfig = existingConfigs.find((config) => config["@_host"] === data.host);
|
|
122
|
+
if (existingConfig) {
|
|
123
|
+
// Return existing SSH config ID
|
|
124
|
+
return existingConfig["@_id"];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// SSH config doesn't exist, create it with a new ID
|
|
129
|
+
const newSshConfigId = randomUUID();
|
|
130
|
+
generateSshConfigsXml(data, newSshConfigId, ideaDir);
|
|
131
|
+
return newSshConfigId;
|
|
132
|
+
}
|
|
133
|
+
// SSH Config specific functions
|
|
134
|
+
function createSshConfigItem(host, username, configId) {
|
|
135
|
+
return {
|
|
136
|
+
"@_authType": "OPEN_SSH",
|
|
137
|
+
"@_host": host,
|
|
138
|
+
"@_id": configId,
|
|
139
|
+
"@_port": "22",
|
|
140
|
+
"@_nameFormat": "DESCRIPTIVE",
|
|
141
|
+
"@_username": username,
|
|
142
|
+
"@_useOpenSSHConfig": "true",
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function generateSshConfigsXml(data, configId, ideaDir) {
|
|
146
|
+
const sshConfigItem = createSshConfigItem(data.host, data.user, configId);
|
|
147
|
+
const config = {
|
|
148
|
+
filename: "sshConfigs.xml",
|
|
149
|
+
componentName: "SshConfigs",
|
|
150
|
+
checkDuplicateFn: (existing, newItem) => existing.some((config) => config["@_host"] === newItem["@_host"]),
|
|
151
|
+
addItemFn: (xmlDoc, newItem) => {
|
|
152
|
+
const configs = xmlDoc.project.component.configs;
|
|
153
|
+
if (configs?.sshConfig) {
|
|
154
|
+
const existingConfigs = ensureArray(configs.sshConfig);
|
|
155
|
+
if (existingConfigs.some((config) => config["@_host"] === newItem["@_host"])) {
|
|
156
|
+
return false; // Already exists
|
|
157
|
+
}
|
|
158
|
+
addSshConfig(configs, newItem);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
if (!xmlDoc.project.component.configs) {
|
|
162
|
+
xmlDoc.project.component.configs = {};
|
|
163
|
+
}
|
|
164
|
+
xmlDoc.project.component.configs.sshConfig = newItem;
|
|
165
|
+
}
|
|
166
|
+
return true;
|
|
167
|
+
},
|
|
168
|
+
createNewDocumentFn: (newItem) => ({
|
|
169
|
+
...createXmlDocumentBase(),
|
|
170
|
+
project: {
|
|
171
|
+
"@_version": "4",
|
|
172
|
+
component: {
|
|
173
|
+
"@_name": "SshConfigs",
|
|
174
|
+
configs: { sshConfig: newItem },
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
}),
|
|
178
|
+
};
|
|
179
|
+
manipulateXmlFile(ideaDir, config, sshConfigItem);
|
|
180
|
+
}
|
|
181
|
+
// Web Server specific functions
|
|
182
|
+
function createWebServerItem(serverId, appShortId, host, username, sshConfigId) {
|
|
183
|
+
return {
|
|
184
|
+
"@_id": serverId,
|
|
185
|
+
"@_name": appShortId,
|
|
186
|
+
fileTransfer: {
|
|
187
|
+
"@_accessType": "SFTP",
|
|
188
|
+
"@_host": host,
|
|
189
|
+
"@_port": "22",
|
|
190
|
+
"@_sshConfigId": sshConfigId,
|
|
191
|
+
"@_sshConfig": `${username}@${host}:22 agent`,
|
|
192
|
+
"@_authAgent": "true",
|
|
193
|
+
advancedOptions: {
|
|
194
|
+
advancedOptions: {
|
|
195
|
+
"@_dataProtectionLevel": "Private",
|
|
196
|
+
"@_keepAliveTimeout": "0",
|
|
197
|
+
"@_passiveMode": "true",
|
|
198
|
+
"@_shareSSLContext": "true",
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function generateWebServersXml(data, serverId, sshConfigId, ideaDir) {
|
|
205
|
+
const webServerItem = createWebServerItem(serverId, data.appShortId, data.host, data.user, sshConfigId);
|
|
206
|
+
const config = {
|
|
207
|
+
filename: "webServers.xml",
|
|
208
|
+
componentName: "WebServers",
|
|
209
|
+
checkDuplicateFn: (existing, newItem) => existing.some((server) => server["@_name"] === newItem["@_name"]),
|
|
210
|
+
addItemFn: (xmlDoc, newItem) => {
|
|
211
|
+
const servers = xmlDoc.project.component.option;
|
|
212
|
+
if (servers?.webServer) {
|
|
213
|
+
const existingServers = ensureArray(servers.webServer);
|
|
214
|
+
if (existingServers.some((server) => server["@_name"] === newItem["@_name"])) {
|
|
215
|
+
return false; // Already exists
|
|
216
|
+
}
|
|
217
|
+
addWebServer(servers, newItem);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
if (!xmlDoc.project.component.option) {
|
|
221
|
+
xmlDoc.project.component.option = { "@_name": "servers" };
|
|
222
|
+
}
|
|
223
|
+
xmlDoc.project.component.option.webServer = newItem;
|
|
224
|
+
}
|
|
225
|
+
return true;
|
|
226
|
+
},
|
|
227
|
+
createNewDocumentFn: (newItem) => ({
|
|
228
|
+
...createXmlDocumentBase(),
|
|
229
|
+
project: {
|
|
230
|
+
"@_version": "4",
|
|
231
|
+
component: {
|
|
232
|
+
"@_name": "WebServers",
|
|
233
|
+
option: {
|
|
234
|
+
"@_name": "servers",
|
|
235
|
+
webServer: newItem,
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
}),
|
|
240
|
+
};
|
|
241
|
+
manipulateXmlFile(ideaDir, config, webServerItem);
|
|
242
|
+
}
|
|
243
|
+
// Deployment specific functions
|
|
244
|
+
function createDeploymentPathItem(appShortId, directory) {
|
|
245
|
+
return {
|
|
246
|
+
"@_name": appShortId,
|
|
247
|
+
serverdata: {
|
|
248
|
+
mappings: {
|
|
249
|
+
mapping: {
|
|
250
|
+
"@_deploy": directory,
|
|
251
|
+
"@_local": "$PROJECT_DIR$",
|
|
252
|
+
"@_web": "/",
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function generateDeploymentXml(data, ideaDir) {
|
|
259
|
+
const deploymentPathItem = createDeploymentPathItem(data.appShortId, data.directory);
|
|
260
|
+
const config = {
|
|
261
|
+
filename: "deployment.xml",
|
|
262
|
+
componentName: "PublishConfigData",
|
|
263
|
+
checkDuplicateFn: (existing, newItem) => existing.some((path) => path["@_name"] === newItem["@_name"]),
|
|
264
|
+
addItemFn: (xmlDoc, newItem) => {
|
|
265
|
+
const serverData = xmlDoc.project.component.serverData;
|
|
266
|
+
if (serverData?.paths) {
|
|
267
|
+
const existingPaths = ensureArray(serverData.paths);
|
|
268
|
+
if (existingPaths.some((path) => path["@_name"] === newItem["@_name"])) {
|
|
269
|
+
return false; // Already exists
|
|
270
|
+
}
|
|
271
|
+
addDeploymentPath(serverData, newItem);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
if (!xmlDoc.project.component.serverData) {
|
|
275
|
+
xmlDoc.project.component.serverData = {};
|
|
276
|
+
}
|
|
277
|
+
xmlDoc.project.component.serverData.paths = newItem;
|
|
278
|
+
}
|
|
279
|
+
return true;
|
|
280
|
+
},
|
|
281
|
+
createNewDocumentFn: (newItem) => ({
|
|
282
|
+
...createXmlDocumentBase(),
|
|
283
|
+
project: {
|
|
284
|
+
"@_version": "4",
|
|
285
|
+
component: {
|
|
286
|
+
"@_name": "PublishConfigData",
|
|
287
|
+
"@_serverName": data.appShortId,
|
|
288
|
+
"@_remoteFilesAllowedToDisappearOnAutoupload": "false",
|
|
289
|
+
serverData: { paths: newItem },
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
}),
|
|
293
|
+
};
|
|
294
|
+
manipulateXmlFile(ideaDir, config, deploymentPathItem);
|
|
295
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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;
|