bcdocker 1.0.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/LICENSE +21 -0
- package/README.md +165 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +224 -0
- package/dist/executor.d.ts +7 -0
- package/dist/executor.js +72 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +203 -0
- package/package.json +59 -0
- package/ps/Apps.ps1 +360 -0
- package/ps/BCDocker.psm1 +30 -0
- package/ps/Container.ps1 +397 -0
- package/ps/Helpers.ps1 +505 -0
- package/ps/Tests.ps1 +144 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { runPowerShell, runRawPowerShell } from "./executor.js";
|
|
5
|
+
const server = new McpServer({
|
|
6
|
+
name: "bcd",
|
|
7
|
+
version: "1.0.0",
|
|
8
|
+
});
|
|
9
|
+
// ── Container Tools ──────────────────────────────────────
|
|
10
|
+
server.tool("list-containers", "List all Business Central Docker containers with their running status", {}, async () => {
|
|
11
|
+
const result = await runRawPowerShell(`
|
|
12
|
+
Import-Module BcContainerHelper -DisableNameChecking -ErrorAction Stop
|
|
13
|
+
$containers = Get-BcContainers
|
|
14
|
+
if ($containers.Count -eq 0) {
|
|
15
|
+
Write-Output "No BC containers found."
|
|
16
|
+
} else {
|
|
17
|
+
foreach ($name in $containers) {
|
|
18
|
+
$status = docker inspect $name --format '{{.State.Status}}' 2>$null
|
|
19
|
+
Write-Output "$name [$status]"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
`);
|
|
23
|
+
return { content: [{ type: "text", text: result.stdout || "No containers found." }] };
|
|
24
|
+
});
|
|
25
|
+
server.tool("container-info", "Get detailed info about a BC container: version, status, and service endpoints", { containerName: z.string().describe("Name of the BC container") }, async ({ containerName }) => {
|
|
26
|
+
const result = await runRawPowerShell(`
|
|
27
|
+
Import-Module BcContainerHelper -DisableNameChecking -ErrorAction Stop
|
|
28
|
+
$bcVersion = Get-BcContainerNavVersion -containerNameOrId '${containerName}'
|
|
29
|
+
$webUrl = Get-BcContainerUrl -containerName '${containerName}' -useHttps:$false
|
|
30
|
+
$status = docker inspect '${containerName}' --format '{{.State.Status}}' 2>$null
|
|
31
|
+
Write-Output "Container : ${containerName}"
|
|
32
|
+
Write-Output "Status : $status"
|
|
33
|
+
Write-Output "BC Version : $bcVersion"
|
|
34
|
+
Write-Output "Web Client : $webUrl"
|
|
35
|
+
Write-Output "OData/API : http://${containerName}:7048/BC/api"
|
|
36
|
+
Write-Output "Dev Service: http://${containerName}:7049/BC"
|
|
37
|
+
`);
|
|
38
|
+
return { content: [{ type: "text", text: result.stdout }] };
|
|
39
|
+
});
|
|
40
|
+
server.tool("create-container", "Create a new Business Central Docker container with test toolkit. Long-running (5-30 min).", {
|
|
41
|
+
containerName: z.string().default("bcsandbox").describe("Docker container name"),
|
|
42
|
+
version: z.string().default("sandbox").describe("BC version: 'sandbox', 'onprem', or specific like '26.0'"),
|
|
43
|
+
country: z.string().default("us").describe("Localization: us, w1, gb, nl, dk, de, etc."),
|
|
44
|
+
userName: z.string().default("admin").describe("Admin username"),
|
|
45
|
+
password: z.string().default("P@ssw0rd!").describe("Admin password"),
|
|
46
|
+
memoryLimit: z.string().default("8G").describe("Memory limit: 8G, 12G, 16G"),
|
|
47
|
+
isolation: z.enum(["hyperv", "process"]).default("hyperv").describe("Isolation mode"),
|
|
48
|
+
testToolkit: z.enum(["none", "libraries", "full"]).default("libraries").describe("Test toolkit: none, libraries (faster), or full (all MS tests)"),
|
|
49
|
+
bypassCDN: z.boolean().default(false).describe("Skip Azure CDN, download from blob storage directly"),
|
|
50
|
+
licenseFile: z.string().optional().describe("Path or URL to .bclicense / .flf license file"),
|
|
51
|
+
}, async ({ containerName, version, country, userName, password, memoryLimit, isolation, testToolkit, bypassCDN, licenseFile }) => {
|
|
52
|
+
const includeToolkit = testToolkit !== "none" ? "$true" : "$false";
|
|
53
|
+
const libOnly = testToolkit === "libraries" ? "$true" : "$false";
|
|
54
|
+
const cdn = bypassCDN ? "-BypassCDN" : "";
|
|
55
|
+
const lic = licenseFile ? `-LicenseFile '${licenseFile}'` : "";
|
|
56
|
+
const result = await runPowerShell(`
|
|
57
|
+
New-BCDContainer \`
|
|
58
|
+
-ContainerName '${containerName}' \`
|
|
59
|
+
-Version '${version}' \`
|
|
60
|
+
-Country '${country}' \`
|
|
61
|
+
-UserName '${userName}' \`
|
|
62
|
+
-Password '${password}' \`
|
|
63
|
+
-MemoryLimit '${memoryLimit}' \`
|
|
64
|
+
-Isolation '${isolation}' \`
|
|
65
|
+
-IncludeTestToolkit ${includeToolkit} \`
|
|
66
|
+
-TestLibrariesOnly ${libOnly} \`
|
|
67
|
+
${cdn} ${lic}
|
|
68
|
+
`, 1_800_000);
|
|
69
|
+
return { content: [{ type: "text", text: result.stdout || "Container creation complete." }] };
|
|
70
|
+
});
|
|
71
|
+
server.tool("remove-container", "Remove a Business Central Docker container", { containerName: z.string().describe("Name of the container to remove") }, async ({ containerName }) => {
|
|
72
|
+
const result = await runPowerShell(`Remove-BCDContainer -ContainerName '${containerName}'`);
|
|
73
|
+
return { content: [{ type: "text", text: result.stdout || `Container '${containerName}' removed.` }] };
|
|
74
|
+
});
|
|
75
|
+
server.tool("start-container", "Start a stopped BC Docker container", { containerName: z.string().describe("Name of the container to start") }, async ({ containerName }) => {
|
|
76
|
+
const result = await runPowerShell(`Start-BCDContainer -ContainerName '${containerName}'`);
|
|
77
|
+
return { content: [{ type: "text", text: result.stdout || `Container '${containerName}' started.` }] };
|
|
78
|
+
});
|
|
79
|
+
server.tool("stop-container", "Stop a running BC Docker container", { containerName: z.string().describe("Name of the container to stop") }, async ({ containerName }) => {
|
|
80
|
+
const result = await runPowerShell(`Stop-BCDContainer -ContainerName '${containerName}'`);
|
|
81
|
+
return { content: [{ type: "text", text: result.stdout || `Container '${containerName}' stopped.` }] };
|
|
82
|
+
});
|
|
83
|
+
server.tool("restart-container", "Restart a BC Docker container", { containerName: z.string().describe("Name of the container to restart") }, async ({ containerName }) => {
|
|
84
|
+
const result = await runPowerShell(`Restart-BCDContainer -ContainerName '${containerName}'`);
|
|
85
|
+
return { content: [{ type: "text", text: result.stdout || `Container '${containerName}' restarted.` }] };
|
|
86
|
+
});
|
|
87
|
+
server.tool("open-webclient", "Open the BC Web Client URL for a container. Returns the URL.", { containerName: z.string().describe("Name of the BC container") }, async ({ containerName }) => {
|
|
88
|
+
const result = await runRawPowerShell(`
|
|
89
|
+
Import-Module BcContainerHelper -DisableNameChecking -ErrorAction Stop
|
|
90
|
+
$url = Get-BcContainerUrl -containerName '${containerName}' -useHttps:$false
|
|
91
|
+
Write-Output $url
|
|
92
|
+
`);
|
|
93
|
+
return { content: [{ type: "text", text: result.stdout || "Could not resolve URL." }] };
|
|
94
|
+
});
|
|
95
|
+
// ── App Tools ────────────────────────────────────────────
|
|
96
|
+
server.tool("list-apps", "List all apps installed in a BC container, optionally filtered by publisher", {
|
|
97
|
+
containerName: z.string().describe("Target container name"),
|
|
98
|
+
publisher: z.string().optional().describe("Filter by publisher (e.g. 'Microsoft')"),
|
|
99
|
+
}, async ({ containerName, publisher }) => {
|
|
100
|
+
const filter = publisher
|
|
101
|
+
? `| Where-Object { $_.Publisher -eq '${publisher}' }`
|
|
102
|
+
: "";
|
|
103
|
+
const result = await runRawPowerShell(`
|
|
104
|
+
Import-Module BcContainerHelper -DisableNameChecking -ErrorAction Stop
|
|
105
|
+
$apps = Get-BcContainerAppInfo -containerName '${containerName}' -tenant default -tenantSpecificProperties ${filter}
|
|
106
|
+
$apps | Select-Object Name, Publisher, Version, IsInstalled, Scope |
|
|
107
|
+
Sort-Object Publisher, Name |
|
|
108
|
+
Format-Table -AutoSize | Out-String -Width 200
|
|
109
|
+
`);
|
|
110
|
+
return { content: [{ type: "text", text: result.stdout || "No apps found." }] };
|
|
111
|
+
});
|
|
112
|
+
server.tool("install-app", "Publish and install a .app file into a BC container", {
|
|
113
|
+
containerName: z.string().describe("Target container name"),
|
|
114
|
+
appFile: z.string().describe("Full path to the .app file"),
|
|
115
|
+
userName: z.string().default("admin").describe("Admin username"),
|
|
116
|
+
password: z.string().default("P@ssw0rd!").describe("Admin password"),
|
|
117
|
+
}, async ({ containerName, appFile, userName, password }) => {
|
|
118
|
+
const result = await runPowerShell(`
|
|
119
|
+
$cred = Get-BCCredential -UserName '${userName}' -Password '${password}'
|
|
120
|
+
Install-BCDApp -ContainerName '${containerName}' -AppFile '${appFile}' -Credential $cred
|
|
121
|
+
`);
|
|
122
|
+
return { content: [{ type: "text", text: result.stdout || "App installed." }] };
|
|
123
|
+
});
|
|
124
|
+
server.tool("uninstall-app", "Uninstall an app from a BC container by name. Automatically handles dependency order.", {
|
|
125
|
+
containerName: z.string().describe("Target container name"),
|
|
126
|
+
appName: z.string().describe("Name of the app to uninstall"),
|
|
127
|
+
appPublisher: z.string().describe("Publisher of the app"),
|
|
128
|
+
}, async ({ containerName, appName, appPublisher }) => {
|
|
129
|
+
const result = await runRawPowerShell(`
|
|
130
|
+
Import-Module BcContainerHelper -DisableNameChecking -ErrorAction Stop
|
|
131
|
+
$allApps = Get-BcContainerAppInfo -containerName '${containerName}' -tenant default -tenantSpecificProperties
|
|
132
|
+
$sorted = Get-BcContainerAppInfo -containerName '${containerName}' -tenant default -tenantSpecificProperties -sort DependenciesLast
|
|
133
|
+
$target = $allApps | Where-Object { $_.Name -eq '${appName}' -and $_.Publisher -eq '${appPublisher}' }
|
|
134
|
+
if (-not $target) { Write-Output "App '${appName}' by '${appPublisher}' not found."; exit }
|
|
135
|
+
foreach ($app in $sorted) {
|
|
136
|
+
if ($app.Name -eq '${appName}' -and $app.Publisher -eq '${appPublisher}' -and $app.IsInstalled) {
|
|
137
|
+
UnInstall-BcContainerApp -name $app.Name -containerName '${containerName}' -publisher $app.Publisher -version $app.Version -force
|
|
138
|
+
Write-Output "Uninstalled: $($app.Name) v$($app.Version)"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
`);
|
|
142
|
+
return { content: [{ type: "text", text: result.stdout || "Uninstall complete." }] };
|
|
143
|
+
});
|
|
144
|
+
server.tool("publish-project", "Compile and publish an AL project folder into a BC container", {
|
|
145
|
+
containerName: z.string().describe("Target container name"),
|
|
146
|
+
projectFolder: z.string().describe("Full path to the AL project folder"),
|
|
147
|
+
userName: z.string().default("admin").describe("Admin username"),
|
|
148
|
+
password: z.string().default("P@ssw0rd!").describe("Admin password"),
|
|
149
|
+
}, async ({ containerName, projectFolder, userName, password }) => {
|
|
150
|
+
const result = await runPowerShell(`
|
|
151
|
+
$cred = Get-BCCredential -UserName '${userName}' -Password '${password}'
|
|
152
|
+
Publish-BCDProject -ContainerName '${containerName}' -ProjectFolder '${projectFolder}' -Credential $cred
|
|
153
|
+
`, 300_000);
|
|
154
|
+
return { content: [{ type: "text", text: result.stdout || "Project compiled and published." }] };
|
|
155
|
+
});
|
|
156
|
+
// ── Test & License Tools ─────────────────────────────────
|
|
157
|
+
server.tool("import-test-toolkit", "Import the BC Test Toolkit (libraries only or full framework) into a container", {
|
|
158
|
+
containerName: z.string().describe("Target container name"),
|
|
159
|
+
librariesOnly: z.boolean().default(true).describe("true = just helper libs (faster), false = full MS test codeunits"),
|
|
160
|
+
userName: z.string().default("admin").describe("Admin username"),
|
|
161
|
+
password: z.string().default("P@ssw0rd!").describe("Admin password"),
|
|
162
|
+
}, async ({ containerName, librariesOnly, userName, password }) => {
|
|
163
|
+
const result = await runPowerShell(`
|
|
164
|
+
$cred = Get-BCCredential -UserName '${userName}' -Password '${password}'
|
|
165
|
+
Import-BCDTestToolkit -ContainerName '${containerName}' -Credential $cred -LibrariesOnly $${librariesOnly}
|
|
166
|
+
`, 300_000);
|
|
167
|
+
return { content: [{ type: "text", text: result.stdout || "Test toolkit imported." }] };
|
|
168
|
+
});
|
|
169
|
+
server.tool("import-license", "Import a license file into a BC container", {
|
|
170
|
+
containerName: z.string().describe("Target container name"),
|
|
171
|
+
licenseFile: z.string().describe("Full path or URL to .bclicense / .flf file"),
|
|
172
|
+
}, async ({ containerName, licenseFile }) => {
|
|
173
|
+
const result = await runPowerShell(`Import-BCDLicense -ContainerName '${containerName}' -LicenseFile '${licenseFile}'`);
|
|
174
|
+
return { content: [{ type: "text", text: result.stdout || "License imported." }] };
|
|
175
|
+
});
|
|
176
|
+
server.tool("run-tests", "Run AL tests in a BC container. Can run all tests, a specific codeunit, or compile-and-run from a project folder.", {
|
|
177
|
+
containerName: z.string().default("bcsandbox").describe("Target container name"),
|
|
178
|
+
testCodeunitId: z.number().optional().describe("Specific test codeunit ID (omit for all)"),
|
|
179
|
+
testFunctionName: z.string().optional().describe("Specific test function name"),
|
|
180
|
+
appProjectFolder: z.string().optional().describe("AL test project folder to compile/publish/run"),
|
|
181
|
+
userName: z.string().default("admin").describe("Admin username"),
|
|
182
|
+
password: z.string().default("P@ssw0rd!").describe("Admin password"),
|
|
183
|
+
}, async ({ containerName, testCodeunitId, testFunctionName, appProjectFolder, userName, password }) => {
|
|
184
|
+
const params = [`-ContainerName '${containerName}'`];
|
|
185
|
+
params.push(`-Credential (Get-BCCredential -UserName '${userName}' -Password '${password}')`);
|
|
186
|
+
if (testCodeunitId)
|
|
187
|
+
params.push(`-TestCodeunitId ${testCodeunitId}`);
|
|
188
|
+
if (testFunctionName)
|
|
189
|
+
params.push(`-TestFunctionName '${testFunctionName}'`);
|
|
190
|
+
if (appProjectFolder)
|
|
191
|
+
params.push(`-AppProjectFolder '${appProjectFolder}'`);
|
|
192
|
+
const result = await runPowerShell(`Invoke-BCDTests ${params.join(" ")}`, 600_000);
|
|
193
|
+
return { content: [{ type: "text", text: result.stdout || "Test run complete." }] };
|
|
194
|
+
});
|
|
195
|
+
// ── Start ────────────────────────────────────────────────
|
|
196
|
+
async function main() {
|
|
197
|
+
const transport = new StdioServerTransport();
|
|
198
|
+
await server.connect(transport);
|
|
199
|
+
}
|
|
200
|
+
main().catch((err) => {
|
|
201
|
+
console.error("MCP server failed:", err);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bcdocker",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server and CLI for Business Central Docker container management. Create, manage, and test BC containers from your terminal or AI assistant.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"bcd": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/server.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"ps",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"start": "node dist/server.js",
|
|
19
|
+
"cli": "node dist/cli.js",
|
|
20
|
+
"prepublishOnly": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"business-central",
|
|
24
|
+
"dynamics-365",
|
|
25
|
+
"docker",
|
|
26
|
+
"bc",
|
|
27
|
+
"al",
|
|
28
|
+
"mcp",
|
|
29
|
+
"model-context-protocol",
|
|
30
|
+
"bccontainerhelper",
|
|
31
|
+
"devops",
|
|
32
|
+
"cli"
|
|
33
|
+
],
|
|
34
|
+
"author": "Oleksandr (Ciellos)",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/olederkach/bcdocker.git"
|
|
39
|
+
},
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/olederkach/bcdocker/issues"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/olederkach/bcdocker#readme",
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=18.0.0"
|
|
46
|
+
},
|
|
47
|
+
"os": [
|
|
48
|
+
"win32"
|
|
49
|
+
],
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
52
|
+
"commander": "^13.0.0",
|
|
53
|
+
"zod": "^3.25.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/node": "^22.0.0",
|
|
57
|
+
"typescript": "^5.7.0"
|
|
58
|
+
}
|
|
59
|
+
}
|
package/ps/Apps.ps1
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
function Get-BCDApps {
|
|
2
|
+
<#
|
|
3
|
+
.SYNOPSIS
|
|
4
|
+
Lists all apps installed in a BC container. Optionally filters by publisher.
|
|
5
|
+
|
|
6
|
+
.PARAMETER ContainerName
|
|
7
|
+
Target container. Prompts with picker if omitted.
|
|
8
|
+
|
|
9
|
+
.PARAMETER Publisher
|
|
10
|
+
Filter by publisher name (e.g. "Microsoft", "Contoso").
|
|
11
|
+
|
|
12
|
+
.PARAMETER GridView
|
|
13
|
+
Show results in Out-GridView instead of console.
|
|
14
|
+
#>
|
|
15
|
+
[CmdletBinding()]
|
|
16
|
+
param(
|
|
17
|
+
[string]$ContainerName,
|
|
18
|
+
[string]$Publisher,
|
|
19
|
+
[switch]$GridView
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if (-not (Assert-BcContainerHelper)) { return }
|
|
23
|
+
|
|
24
|
+
if (-not $ContainerName) {
|
|
25
|
+
$ContainerName = Select-BCContainer -Title "Select container to list apps"
|
|
26
|
+
if (-not $ContainerName) { return }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
$apps = Get-BcContainerAppInfo -containerName $ContainerName -tenant default -tenantSpecificProperties
|
|
30
|
+
|
|
31
|
+
if ($Publisher) {
|
|
32
|
+
$apps = $apps | Where-Object { $_.Publisher -eq $Publisher }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
$result = foreach ($app in $apps) {
|
|
36
|
+
[PSCustomObject]@{
|
|
37
|
+
Name = $app.Name
|
|
38
|
+
Publisher = $app.Publisher
|
|
39
|
+
Version = $app.Version
|
|
40
|
+
Scope = $app.Scope
|
|
41
|
+
IsInstalled = $app.IsInstalled
|
|
42
|
+
SyncState = $app.SyncState
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if ($GridView) {
|
|
47
|
+
$result | Sort-Object Publisher, Name | Out-GridView -Title "Apps in $ContainerName"
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
$result | Sort-Object Publisher, Name | Format-Table -AutoSize
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function Install-BCDApp {
|
|
55
|
+
<#
|
|
56
|
+
.SYNOPSIS
|
|
57
|
+
Publishes and installs .app file(s) into a BC container.
|
|
58
|
+
|
|
59
|
+
.PARAMETER ContainerName
|
|
60
|
+
Target container.
|
|
61
|
+
|
|
62
|
+
.PARAMETER AppFile
|
|
63
|
+
Path(s) to .app file(s). Opens file picker if omitted.
|
|
64
|
+
|
|
65
|
+
.PARAMETER Credential
|
|
66
|
+
Container credentials.
|
|
67
|
+
|
|
68
|
+
.PARAMETER UseDevEndpoint
|
|
69
|
+
Publish via dev endpoint. Default: $true
|
|
70
|
+
#>
|
|
71
|
+
[CmdletBinding()]
|
|
72
|
+
param(
|
|
73
|
+
[string]$ContainerName,
|
|
74
|
+
[string[]]$AppFile,
|
|
75
|
+
[PSCredential]$Credential,
|
|
76
|
+
[bool]$UseDevEndpoint = $true
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if (-not (Assert-BcContainerHelper)) { return }
|
|
80
|
+
|
|
81
|
+
if (-not $ContainerName) {
|
|
82
|
+
$ContainerName = Select-BCContainer -Title "Select container to install app"
|
|
83
|
+
if (-not $ContainerName) { return }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (-not $AppFile) {
|
|
87
|
+
$AppFile = Select-AppFiles -Title "Select .app file(s) to install"
|
|
88
|
+
if (-not $AppFile) { return }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (-not $Credential) {
|
|
92
|
+
$Credential = Get-BCCredential
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
Write-BCBanner "Install App(s)"
|
|
96
|
+
|
|
97
|
+
foreach ($file in $AppFile) {
|
|
98
|
+
$fileName = Split-Path $file -Leaf
|
|
99
|
+
Write-BCStep "PUB" "Publishing $fileName..."
|
|
100
|
+
|
|
101
|
+
$params = @{
|
|
102
|
+
containerName = $ContainerName
|
|
103
|
+
appFile = $file
|
|
104
|
+
credential = $Credential
|
|
105
|
+
install = $true
|
|
106
|
+
sync = $true
|
|
107
|
+
syncMode = "ForceSync"
|
|
108
|
+
skipVerification = $true
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if ($UseDevEndpoint) {
|
|
112
|
+
$params.useDevEndpoint = $true
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
Publish-BcContainerApp @params
|
|
116
|
+
Write-BCSuccess "$fileName installed."
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function Uninstall-BCDApp {
|
|
121
|
+
<#
|
|
122
|
+
.SYNOPSIS
|
|
123
|
+
Uninstalls selected apps from a BC container, resolving dependency order.
|
|
124
|
+
|
|
125
|
+
.PARAMETER ContainerName
|
|
126
|
+
Target container.
|
|
127
|
+
|
|
128
|
+
.PARAMETER IncludeMicrosoft
|
|
129
|
+
Show Microsoft apps in the selection list. Default: $false
|
|
130
|
+
#>
|
|
131
|
+
[CmdletBinding()]
|
|
132
|
+
param(
|
|
133
|
+
[string]$ContainerName,
|
|
134
|
+
[switch]$IncludeMicrosoft
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
if (-not (Assert-BcContainerHelper)) { return }
|
|
138
|
+
|
|
139
|
+
if (-not $ContainerName) {
|
|
140
|
+
$ContainerName = Select-BCContainer -Title "Select container to uninstall apps from"
|
|
141
|
+
if (-not $ContainerName) { return }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
$allApps = Get-BcContainerAppInfo -containerName $ContainerName -tenant default -tenantSpecificProperties
|
|
145
|
+
|
|
146
|
+
$installable = $allApps | Where-Object { $_.IsInstalled }
|
|
147
|
+
if (-not $IncludeMicrosoft) {
|
|
148
|
+
$installable = $installable | Where-Object { $_.Publisher -ne "Microsoft" }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if ($installable.Count -eq 0) {
|
|
152
|
+
Write-BCInfo "No uninstallable apps found."
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
$selected = $installable |
|
|
157
|
+
Select-Object Name, Publisher, Version |
|
|
158
|
+
Sort-Object Name |
|
|
159
|
+
Out-GridView -PassThru -Title "Select app(s) to uninstall"
|
|
160
|
+
|
|
161
|
+
if (-not $selected) { return }
|
|
162
|
+
|
|
163
|
+
Write-BCBanner "Uninstall Apps"
|
|
164
|
+
|
|
165
|
+
$sorted = Get-BcContainerAppInfo -containerName $ContainerName -tenant default -tenantSpecificProperties -sort DependenciesLast
|
|
166
|
+
|
|
167
|
+
$toRemove = @{}
|
|
168
|
+
foreach ($app in $selected) {
|
|
169
|
+
$key = "$($app.Name)|$($app.Publisher)"
|
|
170
|
+
$toRemove[$key] = $true
|
|
171
|
+
Add-BCDDependentApps -AppName $app.Name -AppPublisher $app.Publisher -Map $toRemove -AllApps $allApps
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
$ordered = $sorted | Where-Object {
|
|
175
|
+
$toRemove.ContainsKey("$($_.Name)|$($_.Publisher)")
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
foreach ($app in $ordered) {
|
|
179
|
+
Write-BCStep "DEL" "Uninstalling $($app.Name) v$($app.Version)..."
|
|
180
|
+
try {
|
|
181
|
+
UnInstall-BcContainerApp -name $app.Name -containerName $ContainerName `
|
|
182
|
+
-publisher $app.Publisher -version $app.Version -force -ErrorAction Stop
|
|
183
|
+
Write-BCSuccess "$($app.Name) uninstalled."
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
Write-BCError "Failed: $_"
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function Add-BCDDependentApps {
|
|
192
|
+
param(
|
|
193
|
+
[string]$AppName,
|
|
194
|
+
[string]$AppPublisher,
|
|
195
|
+
[hashtable]$Map,
|
|
196
|
+
$AllApps
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
$AllApps | Where-Object { $_.IsInstalled -and $_.Dependencies } | ForEach-Object {
|
|
200
|
+
$dependent = $_
|
|
201
|
+
$_.Dependencies | ForEach-Object {
|
|
202
|
+
$parts = $_ -split ','
|
|
203
|
+
if ($parts.Count -ge 2) {
|
|
204
|
+
$depName = $parts[0].Trim()
|
|
205
|
+
$depPub = $parts[1].Trim()
|
|
206
|
+
if ($depName -eq $AppName -and $depPub -eq $AppPublisher) {
|
|
207
|
+
$key = "$($dependent.Name)|$($dependent.Publisher)"
|
|
208
|
+
if (-not $Map.ContainsKey($key)) {
|
|
209
|
+
$Map[$key] = $true
|
|
210
|
+
Add-BCDDependentApps -AppName $dependent.Name -AppPublisher $dependent.Publisher -Map $Map -AllApps $AllApps
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function Publish-BCDProject {
|
|
219
|
+
<#
|
|
220
|
+
.SYNOPSIS
|
|
221
|
+
Compiles and publishes an AL project into a BC container.
|
|
222
|
+
|
|
223
|
+
.PARAMETER ContainerName
|
|
224
|
+
Target container.
|
|
225
|
+
|
|
226
|
+
.PARAMETER ProjectFolder
|
|
227
|
+
Path to the AL project folder. Opens folder picker if omitted.
|
|
228
|
+
|
|
229
|
+
.PARAMETER Credential
|
|
230
|
+
Container credentials.
|
|
231
|
+
#>
|
|
232
|
+
[CmdletBinding()]
|
|
233
|
+
param(
|
|
234
|
+
[string]$ContainerName,
|
|
235
|
+
[string]$ProjectFolder,
|
|
236
|
+
[PSCredential]$Credential
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
if (-not (Assert-BcContainerHelper)) { return }
|
|
240
|
+
|
|
241
|
+
if (-not $ContainerName) {
|
|
242
|
+
$ContainerName = Select-BCContainer -Title "Select container to publish to"
|
|
243
|
+
if (-not $ContainerName) { return }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (-not $ProjectFolder) {
|
|
247
|
+
$ProjectFolder = Select-Folder -Description "Select AL project folder"
|
|
248
|
+
if (-not $ProjectFolder) { return }
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (-not $Credential) {
|
|
252
|
+
$Credential = Get-BCCredential
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
Write-BCBanner "Compile & Publish"
|
|
256
|
+
Write-BCStep "1/2" "Compiling $ProjectFolder..."
|
|
257
|
+
|
|
258
|
+
$appFile = Compile-AppInBcContainer `
|
|
259
|
+
-containerName $ContainerName `
|
|
260
|
+
-appProjectFolder $ProjectFolder `
|
|
261
|
+
-credential $Credential `
|
|
262
|
+
-UpdateSymbols
|
|
263
|
+
|
|
264
|
+
Write-BCSuccess "Compiled: $appFile"
|
|
265
|
+
|
|
266
|
+
Write-BCStep "2/2" "Publishing..."
|
|
267
|
+
Publish-BcContainerApp `
|
|
268
|
+
-containerName $ContainerName `
|
|
269
|
+
-appFile $appFile `
|
|
270
|
+
-credential $Credential `
|
|
271
|
+
-install -sync `
|
|
272
|
+
-syncMode ForceSync `
|
|
273
|
+
-skipVerification `
|
|
274
|
+
-useDevEndpoint
|
|
275
|
+
|
|
276
|
+
Write-BCSuccess "Published and installed."
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function Import-BCDTestToolkit {
|
|
280
|
+
<#
|
|
281
|
+
.SYNOPSIS
|
|
282
|
+
Imports the BC Test Toolkit into a container.
|
|
283
|
+
|
|
284
|
+
.PARAMETER ContainerName
|
|
285
|
+
Target container.
|
|
286
|
+
|
|
287
|
+
.PARAMETER Credential
|
|
288
|
+
Container credentials.
|
|
289
|
+
|
|
290
|
+
.PARAMETER LibrariesOnly
|
|
291
|
+
Install only test libraries (no Microsoft test codeunits). Default: $true
|
|
292
|
+
#>
|
|
293
|
+
[CmdletBinding()]
|
|
294
|
+
param(
|
|
295
|
+
[string]$ContainerName,
|
|
296
|
+
[PSCredential]$Credential,
|
|
297
|
+
[bool]$LibrariesOnly = $true
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
if (-not (Assert-BcContainerHelper)) { return }
|
|
301
|
+
|
|
302
|
+
if (-not $ContainerName) {
|
|
303
|
+
$ContainerName = Select-BCContainer -Title "Select container for test toolkit"
|
|
304
|
+
if (-not $ContainerName) { return }
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (-not $Credential) {
|
|
308
|
+
$Credential = Get-BCCredential
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
$mode = if ($LibrariesOnly) { "libraries only" } else { "full framework" }
|
|
312
|
+
Write-BCBanner "Import Test Toolkit ($mode)"
|
|
313
|
+
|
|
314
|
+
Import-TestToolkitToBcContainer `
|
|
315
|
+
-containerName $ContainerName `
|
|
316
|
+
-credential $Credential `
|
|
317
|
+
-includeTestLibrariesOnly:$LibrariesOnly
|
|
318
|
+
|
|
319
|
+
Write-BCSuccess "Test Toolkit imported."
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function Import-BCDLicense {
|
|
323
|
+
<#
|
|
324
|
+
.SYNOPSIS
|
|
325
|
+
Imports a license file into a BC container.
|
|
326
|
+
|
|
327
|
+
.PARAMETER ContainerName
|
|
328
|
+
Target container.
|
|
329
|
+
|
|
330
|
+
.PARAMETER LicenseFile
|
|
331
|
+
Path or URL to the license file. Opens file picker if omitted.
|
|
332
|
+
#>
|
|
333
|
+
[CmdletBinding()]
|
|
334
|
+
param(
|
|
335
|
+
[string]$ContainerName,
|
|
336
|
+
[string]$LicenseFile
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
if (-not (Assert-BcContainerHelper)) { return }
|
|
340
|
+
|
|
341
|
+
if (-not $ContainerName) {
|
|
342
|
+
$ContainerName = Select-BCContainer -Title "Select container for license"
|
|
343
|
+
if (-not $ContainerName) { return }
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (-not $LicenseFile) {
|
|
347
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
348
|
+
$dialog = New-Object System.Windows.Forms.OpenFileDialog
|
|
349
|
+
$dialog.Filter = "BC License files (*.bclicense;*.flf)|*.bclicense;*.flf|All files (*.*)|*.*"
|
|
350
|
+
$dialog.Title = "Select license file"
|
|
351
|
+
if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
|
|
352
|
+
$LicenseFile = $dialog.FileName
|
|
353
|
+
}
|
|
354
|
+
else { return }
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
Write-BCBanner "Import License"
|
|
358
|
+
Import-BcContainerLicense -licenseFile $LicenseFile -containerName $ContainerName -restart
|
|
359
|
+
Write-BCSuccess "License imported and service restarted."
|
|
360
|
+
}
|
package/ps/BCDocker.psm1
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
$ModuleRoot = $PSScriptRoot
|
|
2
|
+
|
|
3
|
+
. (Join-Path $ModuleRoot "Helpers.ps1")
|
|
4
|
+
. (Join-Path $ModuleRoot "Container.ps1")
|
|
5
|
+
. (Join-Path $ModuleRoot "Apps.ps1")
|
|
6
|
+
. (Join-Path $ModuleRoot "Tests.ps1")
|
|
7
|
+
|
|
8
|
+
Export-ModuleMember -Function @(
|
|
9
|
+
'New-BCDContainer'
|
|
10
|
+
'Remove-BCDContainer'
|
|
11
|
+
'Start-BCDContainer'
|
|
12
|
+
'Stop-BCDContainer'
|
|
13
|
+
'Restart-BCDContainer'
|
|
14
|
+
'Get-BCDContainers'
|
|
15
|
+
'Get-BCDContainerInfo'
|
|
16
|
+
'Open-BCDWebClient'
|
|
17
|
+
'Get-BCDApps'
|
|
18
|
+
'Install-BCDApp'
|
|
19
|
+
'Uninstall-BCDApp'
|
|
20
|
+
'Publish-BCDProject'
|
|
21
|
+
'Import-BCDTestToolkit'
|
|
22
|
+
'Import-BCDLicense'
|
|
23
|
+
'Invoke-BCDTests'
|
|
24
|
+
'Show-BCDMainMenu'
|
|
25
|
+
'Show-BCDContainerForm'
|
|
26
|
+
'Get-BCCredential'
|
|
27
|
+
'Select-BCContainer'
|
|
28
|
+
'Select-AppFiles'
|
|
29
|
+
'Select-Folder'
|
|
30
|
+
)
|