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.
Files changed (54) hide show
  1. package/CHANGELOG.md +114 -6
  2. package/README.md +168 -130
  3. package/dist/al/extension-manager.js +10 -50
  4. package/dist/al/extension-manager.js.map +1 -1
  5. package/dist/al/index.js +2 -18
  6. package/dist/al/index.js.map +1 -1
  7. package/dist/al/language-server.d.ts +315 -2
  8. package/dist/al/language-server.d.ts.map +1 -1
  9. package/dist/al/language-server.js +685 -47
  10. package/dist/al/language-server.js.map +1 -1
  11. package/dist/cli.js +36 -68
  12. package/dist/cli.js.map +1 -1
  13. package/dist/cloud/index.js +1 -17
  14. package/dist/cloud/index.js.map +1 -1
  15. package/dist/cloud/relay-client.js +5 -12
  16. package/dist/cloud/relay-client.js.map +1 -1
  17. package/dist/config/index.js +2 -18
  18. package/dist/config/index.js.map +1 -1
  19. package/dist/config/loader.js +8 -47
  20. package/dist/config/loader.js.map +1 -1
  21. package/dist/config/types.d.ts.map +1 -1
  22. package/dist/config/types.js +65 -4
  23. package/dist/config/types.js.map +1 -1
  24. package/dist/container/bc-container.d.ts +212 -0
  25. package/dist/container/bc-container.d.ts.map +1 -0
  26. package/dist/container/bc-container.js +703 -0
  27. package/dist/container/bc-container.js.map +1 -0
  28. package/dist/git/git-operations.d.ts +182 -0
  29. package/dist/git/git-operations.d.ts.map +1 -0
  30. package/dist/git/git-operations.js +442 -0
  31. package/dist/git/git-operations.js.map +1 -0
  32. package/dist/index.js +6 -22
  33. package/dist/index.js.map +1 -1
  34. package/dist/mcp/index.js +1 -17
  35. package/dist/mcp/index.js.map +1 -1
  36. package/dist/mcp/server.js +10 -14
  37. package/dist/mcp/server.js.map +1 -1
  38. package/dist/memory/project-memory.d.ts +83 -0
  39. package/dist/memory/project-memory.d.ts.map +1 -0
  40. package/dist/memory/project-memory.js +273 -0
  41. package/dist/memory/project-memory.js.map +1 -0
  42. package/dist/router/index.js +1 -17
  43. package/dist/router/index.js.map +1 -1
  44. package/dist/router/tool-router.d.ts +62 -0
  45. package/dist/router/tool-router.d.ts.map +1 -1
  46. package/dist/router/tool-router.js +2577 -328
  47. package/dist/router/tool-router.js.map +1 -1
  48. package/dist/utils/index.js +2 -18
  49. package/dist/utils/index.js.map +1 -1
  50. package/dist/utils/logger.js +8 -16
  51. package/dist/utils/logger.js.map +1 -1
  52. package/dist/utils/security.js +10 -54
  53. package/dist/utils/security.js.map +1 -1
  54. 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