partnercore-proxy 0.1.5 → 0.4.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/CHANGELOG.md +114 -6
- package/README.md +168 -130
- package/dist/al/extension-manager.js +10 -50
- package/dist/al/extension-manager.js.map +1 -1
- package/dist/al/index.js +2 -18
- package/dist/al/index.js.map +1 -1
- package/dist/al/language-server.d.ts +315 -2
- package/dist/al/language-server.d.ts.map +1 -1
- package/dist/al/language-server.js +685 -47
- package/dist/al/language-server.js.map +1 -1
- package/dist/cli.js +36 -68
- package/dist/cli.js.map +1 -1
- package/dist/cloud/index.js +1 -17
- package/dist/cloud/index.js.map +1 -1
- package/dist/cloud/relay-client.js +5 -12
- package/dist/cloud/relay-client.js.map +1 -1
- package/dist/config/index.js +2 -18
- package/dist/config/index.js.map +1 -1
- package/dist/config/loader.js +8 -47
- package/dist/config/loader.js.map +1 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +65 -4
- package/dist/config/types.js.map +1 -1
- package/dist/container/bc-container.d.ts +212 -0
- package/dist/container/bc-container.d.ts.map +1 -0
- package/dist/container/bc-container.js +703 -0
- package/dist/container/bc-container.js.map +1 -0
- package/dist/git/git-operations.d.ts +182 -0
- package/dist/git/git-operations.d.ts.map +1 -0
- package/dist/git/git-operations.js +442 -0
- package/dist/git/git-operations.js.map +1 -0
- package/dist/index.js +6 -22
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.js +1 -17
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/server.js +10 -14
- package/dist/mcp/server.js.map +1 -1
- package/dist/memory/project-memory.d.ts +83 -0
- package/dist/memory/project-memory.d.ts.map +1 -0
- package/dist/memory/project-memory.js +273 -0
- package/dist/memory/project-memory.js.map +1 -0
- package/dist/router/index.js +1 -17
- package/dist/router/index.js.map +1 -1
- package/dist/router/tool-router.d.ts +62 -0
- package/dist/router/tool-router.d.ts.map +1 -1
- package/dist/router/tool-router.js +2577 -328
- package/dist/router/tool-router.js.map +1 -1
- package/dist/utils/index.js +2 -18
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/logger.js +8 -16
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/security.js +10 -54
- package/dist/utils/security.js.map +1 -1
- package/package.json +4 -3
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Business Central Container Manager
|
|
3
|
+
*
|
|
4
|
+
* Provides tools for interacting with BC Docker containers
|
|
5
|
+
* using BcContainerHelper PowerShell module.
|
|
6
|
+
*/
|
|
7
|
+
import { exec } from 'child_process';
|
|
8
|
+
import { promisify } from 'util';
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import { getLogger } from '../utils/logger.js';
|
|
12
|
+
const execAsync = promisify(exec);
|
|
13
|
+
/**
|
|
14
|
+
* BC Container Manager
|
|
15
|
+
*/
|
|
16
|
+
export class BCContainerManager {
|
|
17
|
+
logger = getLogger();
|
|
18
|
+
workspaceRoot;
|
|
19
|
+
constructor(workspaceRoot) {
|
|
20
|
+
this.workspaceRoot = workspaceRoot;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* List all BC containers
|
|
24
|
+
*/
|
|
25
|
+
async listContainers() {
|
|
26
|
+
try {
|
|
27
|
+
const { stdout } = await execAsync('docker ps -a --filter "ancestor=mcr.microsoft.com/businesscentral" --format "{{json .}}"');
|
|
28
|
+
const containers = [];
|
|
29
|
+
const lines = stdout.trim().split('\n').filter(l => l);
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
try {
|
|
32
|
+
const data = JSON.parse(line);
|
|
33
|
+
containers.push({
|
|
34
|
+
id: data.ID,
|
|
35
|
+
name: data.Names,
|
|
36
|
+
image: data.Image,
|
|
37
|
+
status: data.Status,
|
|
38
|
+
ports: data.Ports ? data.Ports.split(',').map((p) => p.trim()) : [],
|
|
39
|
+
created: data.CreatedAt,
|
|
40
|
+
running: data.State === 'running',
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Skip malformed lines
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Also try BcContainerHelper format
|
|
48
|
+
try {
|
|
49
|
+
const { stdout: psStdout } = await execAsync('powershell -Command "Get-BcContainers | ConvertTo-Json"');
|
|
50
|
+
const bcContainers = JSON.parse(psStdout);
|
|
51
|
+
if (Array.isArray(bcContainers)) {
|
|
52
|
+
for (const bc of bcContainers) {
|
|
53
|
+
if (!containers.find(c => c.name === bc)) {
|
|
54
|
+
containers.push({
|
|
55
|
+
id: bc,
|
|
56
|
+
name: bc,
|
|
57
|
+
image: 'unknown',
|
|
58
|
+
status: 'unknown',
|
|
59
|
+
ports: [],
|
|
60
|
+
created: 'unknown',
|
|
61
|
+
running: true,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// BcContainerHelper not available, use docker only
|
|
69
|
+
}
|
|
70
|
+
return containers;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
this.logger.error('Failed to list containers:', error);
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Get a specific container by name
|
|
79
|
+
*/
|
|
80
|
+
async getContainer(containerName) {
|
|
81
|
+
const containers = await this.listContainers();
|
|
82
|
+
return containers.find(c => c.name === containerName || c.id.startsWith(containerName)) || null;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Compile AL project using BcContainerHelper
|
|
86
|
+
*/
|
|
87
|
+
async compile(containerName, options) {
|
|
88
|
+
const startTime = Date.now();
|
|
89
|
+
const appFolder = options?.appProjectFolder || this.workspaceRoot;
|
|
90
|
+
const outputFolder = options?.outputFolder || path.join(appFolder, '.output');
|
|
91
|
+
// Ensure output folder exists
|
|
92
|
+
if (!fs.existsSync(outputFolder)) {
|
|
93
|
+
fs.mkdirSync(outputFolder, { recursive: true });
|
|
94
|
+
}
|
|
95
|
+
// Read app.json to get app info
|
|
96
|
+
const appJsonPath = path.join(appFolder, 'app.json');
|
|
97
|
+
if (!fs.existsSync(appJsonPath)) {
|
|
98
|
+
return {
|
|
99
|
+
success: false,
|
|
100
|
+
errors: ['app.json not found in project folder'],
|
|
101
|
+
warnings: [],
|
|
102
|
+
duration: Date.now() - startTime,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const appJson = JSON.parse(fs.readFileSync(appJsonPath, 'utf-8'));
|
|
106
|
+
const expectedAppFile = path.join(outputFolder, `${appJson.publisher}_${appJson.name}_${appJson.version}.app`);
|
|
107
|
+
try {
|
|
108
|
+
// Use BcContainerHelper to compile
|
|
109
|
+
const escapedAppFolder = appFolder.replace(/\\/g, '\\\\');
|
|
110
|
+
const escapedOutputFolder = outputFolder.replace(/\\/g, '\\\\');
|
|
111
|
+
const psCommand = [
|
|
112
|
+
'$ErrorActionPreference = "Stop"',
|
|
113
|
+
'$result = Compile-AppInBcContainer \\',
|
|
114
|
+
` -containerName '${containerName}' \\`,
|
|
115
|
+
` -appProjectFolder '${escapedAppFolder}' \\`,
|
|
116
|
+
` -appOutputFolder '${escapedOutputFolder}' \\`,
|
|
117
|
+
' -EnableCodeCop \\',
|
|
118
|
+
' -EnableAppSourceCop \\',
|
|
119
|
+
' -EnableUICop \\',
|
|
120
|
+
' -EnablePerTenantExtensionCop \\',
|
|
121
|
+
' 2>&1',
|
|
122
|
+
'$result | ConvertTo-Json -Depth 10',
|
|
123
|
+
].join('\n');
|
|
124
|
+
const { stdout, stderr } = await execAsync(`powershell -Command "${psCommand.replace(/"/g, '\\"')}"`, { maxBuffer: 10 * 1024 * 1024 });
|
|
125
|
+
const errors = [];
|
|
126
|
+
const warnings = [];
|
|
127
|
+
// Parse output for errors and warnings
|
|
128
|
+
const outputLines = (stdout + stderr).split('\n');
|
|
129
|
+
for (const line of outputLines) {
|
|
130
|
+
if (line.includes('error ') || line.includes('Error:')) {
|
|
131
|
+
errors.push(line.trim());
|
|
132
|
+
}
|
|
133
|
+
else if (line.includes('warning ') || line.includes('Warning:')) {
|
|
134
|
+
warnings.push(line.trim());
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Check if app file was created
|
|
138
|
+
const appFileExists = fs.existsSync(expectedAppFile);
|
|
139
|
+
return {
|
|
140
|
+
success: errors.length === 0 && appFileExists,
|
|
141
|
+
appFile: appFileExists ? expectedAppFile : undefined,
|
|
142
|
+
errors,
|
|
143
|
+
warnings,
|
|
144
|
+
duration: Date.now() - startTime,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
149
|
+
return {
|
|
150
|
+
success: false,
|
|
151
|
+
errors: [errorMessage],
|
|
152
|
+
warnings: [],
|
|
153
|
+
duration: Date.now() - startTime,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Publish app to BC container
|
|
159
|
+
*/
|
|
160
|
+
async publish(containerName, options) {
|
|
161
|
+
const appFolder = this.workspaceRoot;
|
|
162
|
+
const outputFolder = path.join(appFolder, '.output');
|
|
163
|
+
const syncMode = options?.syncMode || 'Development';
|
|
164
|
+
// Find the app file
|
|
165
|
+
let appFile = options?.appFile;
|
|
166
|
+
if (!appFile) {
|
|
167
|
+
if (fs.existsSync(outputFolder)) {
|
|
168
|
+
const files = fs.readdirSync(outputFolder).filter(f => f.endsWith('.app'));
|
|
169
|
+
if (files.length > 0) {
|
|
170
|
+
// Get the most recent app file
|
|
171
|
+
const appFiles = files
|
|
172
|
+
.map(f => ({ name: f, time: fs.statSync(path.join(outputFolder, f)).mtime.getTime() }))
|
|
173
|
+
.sort((a, b) => b.time - a.time);
|
|
174
|
+
appFile = path.join(outputFolder, appFiles[0].name);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (!appFile || !fs.existsSync(appFile)) {
|
|
179
|
+
return {
|
|
180
|
+
success: false,
|
|
181
|
+
message: 'App file not found. Run compile first.',
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const escapedAppFile = appFile.replace(/\\/g, '\\\\');
|
|
186
|
+
const psCommandParts = [
|
|
187
|
+
'$ErrorActionPreference = "Stop"',
|
|
188
|
+
'Publish-BcContainerApp \\',
|
|
189
|
+
` -containerName '${containerName}' \\`,
|
|
190
|
+
` -appFile '${escapedAppFile}' \\`,
|
|
191
|
+
` -syncMode ${syncMode} \\`,
|
|
192
|
+
options?.skipVerification ? ' -skipVerification \\' : '',
|
|
193
|
+
options?.install ? ' -install \\' : '',
|
|
194
|
+
' -useDevEndpoint',
|
|
195
|
+
"Write-Output 'SUCCESS'",
|
|
196
|
+
].filter(Boolean);
|
|
197
|
+
const psCommand = psCommandParts.join('\n');
|
|
198
|
+
const { stdout, stderr } = await execAsync(`powershell -Command "${psCommand.replace(/"/g, '\\"')}"`, { maxBuffer: 10 * 1024 * 1024 });
|
|
199
|
+
const success = stdout.includes('SUCCESS');
|
|
200
|
+
return {
|
|
201
|
+
success,
|
|
202
|
+
message: success ? `Published ${path.basename(appFile)}` : stderr || 'Publish failed',
|
|
203
|
+
syncMode,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
208
|
+
return {
|
|
209
|
+
success: false,
|
|
210
|
+
message: errorMessage,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Run tests in BC container
|
|
216
|
+
*/
|
|
217
|
+
async runTests(containerName, options) {
|
|
218
|
+
const startTime = Date.now();
|
|
219
|
+
try {
|
|
220
|
+
// Build test parameters
|
|
221
|
+
let testParams = '';
|
|
222
|
+
if (options?.testCodeunit) {
|
|
223
|
+
testParams += ` -testCodeunit ${options.testCodeunit}`;
|
|
224
|
+
}
|
|
225
|
+
if (options?.testFunction) {
|
|
226
|
+
testParams += ` -testFunction '${options.testFunction}'`;
|
|
227
|
+
}
|
|
228
|
+
if (options?.extensionId) {
|
|
229
|
+
testParams += ` -extensionId '${options.extensionId}'`;
|
|
230
|
+
}
|
|
231
|
+
const psCommandParts = [
|
|
232
|
+
'$ErrorActionPreference = "Stop"',
|
|
233
|
+
'$results = Run-TestsInBcContainer \\',
|
|
234
|
+
` -containerName '${containerName}' \\`,
|
|
235
|
+
testParams ? ` ${testParams.trim()} \\` : '',
|
|
236
|
+
` -detailed:${options?.detailed ? '$true' : '$false'} \\`,
|
|
237
|
+
' -returnTrueIfAllPassed',
|
|
238
|
+
'$results | ConvertTo-Json -Depth 10',
|
|
239
|
+
].filter(Boolean);
|
|
240
|
+
const psCommand = psCommandParts.join('\n');
|
|
241
|
+
const { stdout } = await execAsync(`powershell -Command "${psCommand.replace(/"/g, '\\"')}"`, { maxBuffer: 10 * 1024 * 1024, timeout: 600000 });
|
|
242
|
+
// Parse results
|
|
243
|
+
let testsPassed = 0;
|
|
244
|
+
let testsFailed = 0;
|
|
245
|
+
let testsSkipped = 0;
|
|
246
|
+
const results = [];
|
|
247
|
+
try {
|
|
248
|
+
const parsed = JSON.parse(stdout);
|
|
249
|
+
if (typeof parsed === 'boolean') {
|
|
250
|
+
// Simple result
|
|
251
|
+
return {
|
|
252
|
+
success: parsed,
|
|
253
|
+
testsRun: 1,
|
|
254
|
+
testsPassed: parsed ? 1 : 0,
|
|
255
|
+
testsFailed: parsed ? 0 : 1,
|
|
256
|
+
testsSkipped: 0,
|
|
257
|
+
duration: Date.now() - startTime,
|
|
258
|
+
results: [],
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
// Detailed results
|
|
262
|
+
if (Array.isArray(parsed)) {
|
|
263
|
+
for (const test of parsed) {
|
|
264
|
+
const result = {
|
|
265
|
+
name: test.name || test.testFunction || 'Unknown',
|
|
266
|
+
codeunitId: test.codeunitId || 0,
|
|
267
|
+
codeunitName: test.codeunitName || 'Unknown',
|
|
268
|
+
result: test.result === '0' || test.result === 'Passed' ? 'Passed' : test.result === '1' || test.result === 'Failed' ? 'Failed' : 'Skipped',
|
|
269
|
+
message: test.message,
|
|
270
|
+
duration: test.duration || 0,
|
|
271
|
+
};
|
|
272
|
+
results.push(result);
|
|
273
|
+
if (result.result === 'Passed')
|
|
274
|
+
testsPassed++;
|
|
275
|
+
else if (result.result === 'Failed')
|
|
276
|
+
testsFailed++;
|
|
277
|
+
else
|
|
278
|
+
testsSkipped++;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
// Parse error, try to extract from output
|
|
284
|
+
if (stdout.includes('True')) {
|
|
285
|
+
testsPassed = 1;
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
testsFailed = 1;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
success: testsFailed === 0,
|
|
293
|
+
testsRun: testsPassed + testsFailed + testsSkipped,
|
|
294
|
+
testsPassed,
|
|
295
|
+
testsFailed,
|
|
296
|
+
testsSkipped,
|
|
297
|
+
duration: Date.now() - startTime,
|
|
298
|
+
results,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
303
|
+
return {
|
|
304
|
+
success: false,
|
|
305
|
+
testsRun: 0,
|
|
306
|
+
testsPassed: 0,
|
|
307
|
+
testsFailed: 1,
|
|
308
|
+
testsSkipped: 0,
|
|
309
|
+
duration: Date.now() - startTime,
|
|
310
|
+
results: [{
|
|
311
|
+
name: 'Test Execution',
|
|
312
|
+
codeunitId: 0,
|
|
313
|
+
codeunitName: 'N/A',
|
|
314
|
+
result: 'Failed',
|
|
315
|
+
message: errorMessage,
|
|
316
|
+
duration: 0,
|
|
317
|
+
}],
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Get container logs
|
|
323
|
+
*/
|
|
324
|
+
async getLogs(containerName, options) {
|
|
325
|
+
try {
|
|
326
|
+
let dockerCmd = `docker logs ${containerName}`;
|
|
327
|
+
if (options?.tail) {
|
|
328
|
+
dockerCmd += ` --tail ${options.tail}`;
|
|
329
|
+
}
|
|
330
|
+
if (options?.since) {
|
|
331
|
+
dockerCmd += ` --since ${options.since}`;
|
|
332
|
+
}
|
|
333
|
+
const { stdout, stderr } = await execAsync(dockerCmd, { maxBuffer: 10 * 1024 * 1024 });
|
|
334
|
+
return stdout + stderr;
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
338
|
+
return `Error getting logs: ${errorMessage}`;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Start a container
|
|
343
|
+
*/
|
|
344
|
+
async startContainer(containerName) {
|
|
345
|
+
try {
|
|
346
|
+
await execAsync(`docker start ${containerName}`);
|
|
347
|
+
return { success: true, message: `Container ${containerName} started` };
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
351
|
+
return { success: false, message: errorMessage };
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Stop a container
|
|
356
|
+
*/
|
|
357
|
+
async stopContainer(containerName) {
|
|
358
|
+
try {
|
|
359
|
+
await execAsync(`docker stop ${containerName}`);
|
|
360
|
+
return { success: true, message: `Container ${containerName} stopped` };
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
364
|
+
return { success: false, message: errorMessage };
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Restart a container
|
|
369
|
+
*/
|
|
370
|
+
async restartContainer(containerName) {
|
|
371
|
+
try {
|
|
372
|
+
await execAsync(`docker restart ${containerName}`);
|
|
373
|
+
return { success: true, message: `Container ${containerName} restarted` };
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
377
|
+
return { success: false, message: errorMessage };
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Create a new BC container
|
|
382
|
+
*/
|
|
383
|
+
async createContainer(containerName, options) {
|
|
384
|
+
const startTime = Date.now();
|
|
385
|
+
try {
|
|
386
|
+
// Build the New-BcContainer command
|
|
387
|
+
const params = [
|
|
388
|
+
'$ErrorActionPreference = "Stop"',
|
|
389
|
+
];
|
|
390
|
+
// Determine artifact URL
|
|
391
|
+
let artifactUrl = options.artifactUrl;
|
|
392
|
+
if (!artifactUrl) {
|
|
393
|
+
const version = options.version || '';
|
|
394
|
+
const country = options.country || 'us';
|
|
395
|
+
const type = options.type || 'Sandbox';
|
|
396
|
+
params.push(`$artifactUrl = Get-BCArtifactUrl -type ${type} -country ${country}${version ? ` -version '${version}'` : ''} -select Latest`);
|
|
397
|
+
artifactUrl = '$artifactUrl';
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
params.push(`$artifactUrl = '${artifactUrl}'`);
|
|
401
|
+
artifactUrl = '$artifactUrl';
|
|
402
|
+
}
|
|
403
|
+
// Build New-BcContainer parameters
|
|
404
|
+
const containerParams = [
|
|
405
|
+
`New-BcContainer`,
|
|
406
|
+
`-containerName '${containerName}'`,
|
|
407
|
+
`-artifactUrl ${artifactUrl}`,
|
|
408
|
+
`-accept_eula${options.accept_eula !== false ? '' : ':$false'}`,
|
|
409
|
+
];
|
|
410
|
+
// Auth
|
|
411
|
+
if (options.auth) {
|
|
412
|
+
containerParams.push(`-auth ${options.auth}`);
|
|
413
|
+
}
|
|
414
|
+
// Credential
|
|
415
|
+
if (options.credential) {
|
|
416
|
+
params.push(`$credential = New-Object System.Management.Automation.PSCredential('${options.credential.username}', (ConvertTo-SecureString '${options.credential.password}' -AsPlainText -Force))`);
|
|
417
|
+
containerParams.push('-credential $credential');
|
|
418
|
+
}
|
|
419
|
+
// License file
|
|
420
|
+
if (options.licenseFile) {
|
|
421
|
+
containerParams.push(`-licenseFile '${options.licenseFile.replace(/\\/g, '\\\\')}'`);
|
|
422
|
+
}
|
|
423
|
+
// Test toolkit options
|
|
424
|
+
if (options.includeTestToolkit) {
|
|
425
|
+
containerParams.push('-includeTestToolkit');
|
|
426
|
+
}
|
|
427
|
+
if (options.includeTestLibrariesOnly) {
|
|
428
|
+
containerParams.push('-includeTestLibrariesOnly');
|
|
429
|
+
}
|
|
430
|
+
if (options.includeTestFrameworkOnly) {
|
|
431
|
+
containerParams.push('-includeTestFrameworkOnly');
|
|
432
|
+
}
|
|
433
|
+
// Other options
|
|
434
|
+
if (options.accept_outdated) {
|
|
435
|
+
containerParams.push('-accept_outdated');
|
|
436
|
+
}
|
|
437
|
+
if (options.enableTaskScheduler) {
|
|
438
|
+
containerParams.push('-enableTaskScheduler');
|
|
439
|
+
}
|
|
440
|
+
if (options.assignPremiumPlan) {
|
|
441
|
+
containerParams.push('-assignPremiumPlan');
|
|
442
|
+
}
|
|
443
|
+
if (options.multitenant) {
|
|
444
|
+
containerParams.push('-multitenant');
|
|
445
|
+
}
|
|
446
|
+
if (options.memoryLimit) {
|
|
447
|
+
containerParams.push(`-memoryLimit '${options.memoryLimit}'`);
|
|
448
|
+
}
|
|
449
|
+
if (options.isolation) {
|
|
450
|
+
containerParams.push(`-isolation ${options.isolation}`);
|
|
451
|
+
}
|
|
452
|
+
if (options.updateHosts) {
|
|
453
|
+
containerParams.push('-updateHosts');
|
|
454
|
+
}
|
|
455
|
+
params.push(containerParams.join(' `\n '));
|
|
456
|
+
params.push("Write-Output 'CONTAINER_CREATED_SUCCESS'");
|
|
457
|
+
params.push(`Write-Output "http://${containerName}/BC/"`);
|
|
458
|
+
const psCommand = params.join('\n');
|
|
459
|
+
this.logger.info(`Creating container ${containerName}...`);
|
|
460
|
+
const { stdout, stderr } = await execAsync(`powershell -Command "${psCommand.replace(/"/g, '\\"')}"`, {
|
|
461
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
462
|
+
timeout: 1800000 // 30 minutes for container creation
|
|
463
|
+
});
|
|
464
|
+
const success = stdout.includes('CONTAINER_CREATED_SUCCESS');
|
|
465
|
+
const duration = Math.round((Date.now() - startTime) / 1000);
|
|
466
|
+
if (success) {
|
|
467
|
+
// Extract web client URL
|
|
468
|
+
const urlMatch = stdout.match(/http:\/\/[^\s]+/);
|
|
469
|
+
return {
|
|
470
|
+
success: true,
|
|
471
|
+
message: `Container ${containerName} created successfully in ${duration}s`,
|
|
472
|
+
containerName,
|
|
473
|
+
webClientUrl: urlMatch ? urlMatch[0] : undefined,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
return {
|
|
478
|
+
success: false,
|
|
479
|
+
message: stderr || stdout || 'Container creation failed',
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
485
|
+
return { success: false, message: errorMessage };
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Remove a container
|
|
490
|
+
*/
|
|
491
|
+
async removeContainer(containerName, force) {
|
|
492
|
+
try {
|
|
493
|
+
// Try BcContainerHelper first
|
|
494
|
+
const psCommand = `Remove-BcContainer -containerName '${containerName}'`;
|
|
495
|
+
await execAsync(`powershell -Command "${psCommand}"`, { timeout: 300000 });
|
|
496
|
+
return { success: true, message: `Container ${containerName} removed` };
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
// Fall back to docker
|
|
500
|
+
try {
|
|
501
|
+
await execAsync(`docker rm ${force ? '-f' : ''} ${containerName}`);
|
|
502
|
+
return { success: true, message: `Container ${containerName} removed` };
|
|
503
|
+
}
|
|
504
|
+
catch (error) {
|
|
505
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
506
|
+
return { success: false, message: errorMessage };
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Get container info including web client URL
|
|
512
|
+
*/
|
|
513
|
+
async getContainerUrl(containerName) {
|
|
514
|
+
try {
|
|
515
|
+
const psCommand = `
|
|
516
|
+
$config = Get-BcContainerServerConfiguration -containerName '${containerName}'
|
|
517
|
+
@{
|
|
518
|
+
webClientUrl = "http://${containerName}/$($config.ServerInstance)/WebClient"
|
|
519
|
+
soapUrl = "http://${containerName}:7047/$($config.ServerInstance)/WS"
|
|
520
|
+
oDataUrl = "http://${containerName}:7048/$($config.ServerInstance)/OData"
|
|
521
|
+
} | ConvertTo-Json
|
|
522
|
+
`;
|
|
523
|
+
const { stdout } = await execAsync(`powershell -Command "${psCommand.replace(/"/g, '\\"')}"`);
|
|
524
|
+
return JSON.parse(stdout);
|
|
525
|
+
}
|
|
526
|
+
catch {
|
|
527
|
+
return {};
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Get installed extensions/apps from container
|
|
532
|
+
*/
|
|
533
|
+
async getExtensions(containerName) {
|
|
534
|
+
try {
|
|
535
|
+
const psCommand = `
|
|
536
|
+
$apps = Get-BcContainerAppInfo -containerName '${containerName}' -tenantSpecificProperties
|
|
537
|
+
$apps | ForEach-Object {
|
|
538
|
+
@{
|
|
539
|
+
name = $_.Name
|
|
540
|
+
publisher = $_.Publisher
|
|
541
|
+
version = $_.Version
|
|
542
|
+
appId = $_.AppId
|
|
543
|
+
scope = $_.Scope
|
|
544
|
+
isPublished = $_.IsPublished
|
|
545
|
+
isInstalled = $_.IsInstalled
|
|
546
|
+
}
|
|
547
|
+
} | ConvertTo-Json -Depth 5
|
|
548
|
+
`;
|
|
549
|
+
const { stdout } = await execAsync(`powershell -Command "${psCommand.replace(/"/g, '\\"')}"`, { maxBuffer: 10 * 1024 * 1024 });
|
|
550
|
+
let extensions = [];
|
|
551
|
+
try {
|
|
552
|
+
const parsed = JSON.parse(stdout);
|
|
553
|
+
extensions = Array.isArray(parsed) ? parsed : [parsed];
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
// Empty result
|
|
557
|
+
}
|
|
558
|
+
return {
|
|
559
|
+
success: true,
|
|
560
|
+
extensions,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
catch (error) {
|
|
564
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
565
|
+
return { success: false, extensions: [], message: errorMessage };
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Uninstall an app from container
|
|
570
|
+
*/
|
|
571
|
+
async uninstallApp(containerName, options) {
|
|
572
|
+
try {
|
|
573
|
+
const params = [
|
|
574
|
+
'$ErrorActionPreference = "Stop"',
|
|
575
|
+
];
|
|
576
|
+
// Add credential if provided
|
|
577
|
+
if (options.credential) {
|
|
578
|
+
params.push(`$credential = New-Object System.Management.Automation.PSCredential('${options.credential.username}', (ConvertTo-SecureString '${options.credential.password}' -AsPlainText -Force))`);
|
|
579
|
+
}
|
|
580
|
+
const uninstallParams = [
|
|
581
|
+
'Uninstall-BcContainerApp',
|
|
582
|
+
`-containerName '${containerName}'`,
|
|
583
|
+
`-name '${options.name}'`,
|
|
584
|
+
];
|
|
585
|
+
if (options.publisher) {
|
|
586
|
+
uninstallParams.push(`-publisher '${options.publisher}'`);
|
|
587
|
+
}
|
|
588
|
+
if (options.version) {
|
|
589
|
+
uninstallParams.push(`-version '${options.version}'`);
|
|
590
|
+
}
|
|
591
|
+
if (options.force) {
|
|
592
|
+
uninstallParams.push('-Force');
|
|
593
|
+
}
|
|
594
|
+
if (options.credential) {
|
|
595
|
+
uninstallParams.push('-credential $credential');
|
|
596
|
+
}
|
|
597
|
+
params.push(uninstallParams.join(' '));
|
|
598
|
+
params.push("Write-Output 'UNINSTALL_SUCCESS'");
|
|
599
|
+
const psCommand = params.join('\n');
|
|
600
|
+
const { stdout, stderr } = await execAsync(`powershell -Command "${psCommand.replace(/"/g, '\\"')}"`, { maxBuffer: 10 * 1024 * 1024 });
|
|
601
|
+
const success = stdout.includes('UNINSTALL_SUCCESS');
|
|
602
|
+
return {
|
|
603
|
+
success,
|
|
604
|
+
message: success
|
|
605
|
+
? `Successfully uninstalled ${options.name}`
|
|
606
|
+
: stderr || stdout || 'Uninstall failed',
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
catch (error) {
|
|
610
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
611
|
+
return { success: false, message: errorMessage };
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Compile and return only warnings (quick check)
|
|
616
|
+
*/
|
|
617
|
+
async compileWarningsOnly(containerName, options) {
|
|
618
|
+
const appFolder = options?.appProjectFolder || this.workspaceRoot;
|
|
619
|
+
const outputFolder = path.join(appFolder, '.output');
|
|
620
|
+
if (!fs.existsSync(outputFolder)) {
|
|
621
|
+
fs.mkdirSync(outputFolder, { recursive: true });
|
|
622
|
+
}
|
|
623
|
+
try {
|
|
624
|
+
const escapedAppFolder = appFolder.replace(/\\/g, '\\\\');
|
|
625
|
+
const escapedOutputFolder = outputFolder.replace(/\\/g, '\\\\');
|
|
626
|
+
const psCommand = [
|
|
627
|
+
'$ErrorActionPreference = "Continue"',
|
|
628
|
+
'$warnings = @()',
|
|
629
|
+
'Compile-AppInBcContainer \\',
|
|
630
|
+
` -containerName '${containerName}' \\`,
|
|
631
|
+
` -appProjectFolder '${escapedAppFolder}' \\`,
|
|
632
|
+
` -appOutputFolder '${escapedOutputFolder}' \\`,
|
|
633
|
+
' -EnableCodeCop \\',
|
|
634
|
+
' -EnableAppSourceCop \\',
|
|
635
|
+
' -EnableUICop \\',
|
|
636
|
+
' -EnablePerTenantExtensionCop \\',
|
|
637
|
+
' 2>&1 | ForEach-Object {',
|
|
638
|
+
' if ($_ -match "warning") { $warnings += $_.ToString() }',
|
|
639
|
+
' }',
|
|
640
|
+
'$warnings | ConvertTo-Json',
|
|
641
|
+
].join('\n');
|
|
642
|
+
const { stdout, stderr } = await execAsync(`powershell -Command "${psCommand.replace(/"/g, '\\"')}"`, { maxBuffer: 10 * 1024 * 1024 });
|
|
643
|
+
const warnings = [];
|
|
644
|
+
// Parse warnings from output
|
|
645
|
+
const outputLines = (stdout + stderr).split('\n');
|
|
646
|
+
for (const line of outputLines) {
|
|
647
|
+
if (line.toLowerCase().includes('warning')) {
|
|
648
|
+
warnings.push(line.trim());
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return {
|
|
652
|
+
success: true,
|
|
653
|
+
warnings,
|
|
654
|
+
warningCount: warnings.length,
|
|
655
|
+
message: warnings.length === 0
|
|
656
|
+
? 'No warnings found'
|
|
657
|
+
: `Found ${warnings.length} warning(s)`,
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
catch (error) {
|
|
661
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
662
|
+
return {
|
|
663
|
+
success: false,
|
|
664
|
+
warnings: [],
|
|
665
|
+
warningCount: 0,
|
|
666
|
+
message: errorMessage
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Download symbols from container
|
|
672
|
+
*/
|
|
673
|
+
async downloadSymbols(containerName, targetFolder) {
|
|
674
|
+
const folder = targetFolder || path.join(this.workspaceRoot, '.alpackages');
|
|
675
|
+
try {
|
|
676
|
+
if (!fs.existsSync(folder)) {
|
|
677
|
+
fs.mkdirSync(folder, { recursive: true });
|
|
678
|
+
}
|
|
679
|
+
const psCommand = `
|
|
680
|
+
$ErrorActionPreference = 'Stop'
|
|
681
|
+
Get-BcContainerAppInfo -containerName '${containerName}' -tenantSpecificProperties -sort DependenciesFirst | ForEach-Object {
|
|
682
|
+
if ($_.IsPublished) {
|
|
683
|
+
$appFile = Join-Path '${folder.replace(/\\/g, '\\\\')}' "$($_.Publisher)_$($_.Name)_$($_.Version).app"
|
|
684
|
+
Get-BcContainerApp -containerName '${containerName}' -appName $_.Name -appVersion $_.Version -appPublisher $_.Publisher > $appFile
|
|
685
|
+
Write-Output $appFile
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
`;
|
|
689
|
+
const { stdout } = await execAsync(`powershell -Command "${psCommand.replace(/"/g, '\\"')}"`, { maxBuffer: 50 * 1024 * 1024, timeout: 300000 });
|
|
690
|
+
const files = stdout.trim().split('\n').filter(f => f && f.endsWith('.app'));
|
|
691
|
+
return {
|
|
692
|
+
success: true,
|
|
693
|
+
message: `Downloaded ${files.length} symbol files`,
|
|
694
|
+
files,
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
catch (error) {
|
|
698
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
699
|
+
return { success: false, message: errorMessage };
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
//# sourceMappingURL=bc-container.js.map
|