@microsoft/managed-apps-cli 0.5.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 +153 -0
- package/README.md +280 -0
- package/dist/ArgumentProvider.d.ts +102 -0
- package/dist/ArgumentProvider.d.ts.map +1 -0
- package/dist/ArgumentProvider.js +376 -0
- package/dist/ArgumentProvider.js.map +1 -0
- package/dist/Authentication/NodeMsalAuthenticationProvider.d.ts +18 -0
- package/dist/Authentication/NodeMsalAuthenticationProvider.d.ts.map +1 -0
- package/dist/Authentication/NodeMsalAuthenticationProvider.js +99 -0
- package/dist/Authentication/NodeMsalAuthenticationProvider.js.map +1 -0
- package/dist/Authentication/OrchardAuthenticationProvider.d.ts +13 -0
- package/dist/Authentication/OrchardAuthenticationProvider.d.ts.map +1 -0
- package/dist/Authentication/OrchardAuthenticationProvider.js +30 -0
- package/dist/Authentication/OrchardAuthenticationProvider.js.map +1 -0
- package/dist/Authentication/ServicePrincipalAuthenticationProvider.d.ts +23 -0
- package/dist/Authentication/ServicePrincipalAuthenticationProvider.d.ts.map +1 -0
- package/dist/Authentication/ServicePrincipalAuthenticationProvider.js +60 -0
- package/dist/Authentication/ServicePrincipalAuthenticationProvider.js.map +1 -0
- package/dist/Bin.d.ts +6 -0
- package/dist/Bin.d.ts.map +1 -0
- package/dist/Bin.js +7 -0
- package/dist/Bin.js.map +1 -0
- package/dist/Cli.d.ts +2 -0
- package/dist/Cli.d.ts.map +1 -0
- package/dist/Cli.js +91 -0
- package/dist/Cli.js.map +1 -0
- package/dist/CliSettings.d.ts +14 -0
- package/dist/CliSettings.d.ts.map +1 -0
- package/dist/CliSettings.js +78 -0
- package/dist/CliSettings.js.map +1 -0
- package/dist/CliUtils.d.ts +92 -0
- package/dist/CliUtils.d.ts.map +1 -0
- package/dist/CliUtils.js +218 -0
- package/dist/CliUtils.js.map +1 -0
- package/dist/Constants/CliConstants.d.ts +6 -0
- package/dist/Constants/CliConstants.d.ts.map +1 -0
- package/dist/Constants/CliConstants.js +6 -0
- package/dist/Constants/CliConstants.js.map +1 -0
- package/dist/Constants/CommandRegistry.d.ts +23 -0
- package/dist/Constants/CommandRegistry.d.ts.map +1 -0
- package/dist/Constants/CommandRegistry.js +193 -0
- package/dist/Constants/CommandRegistry.js.map +1 -0
- package/dist/Constants/HelpStrings.d.ts +321 -0
- package/dist/Constants/HelpStrings.d.ts.map +1 -0
- package/dist/Constants/HelpStrings.js +438 -0
- package/dist/Constants/HelpStrings.js.map +1 -0
- package/dist/Constants.d.ts +10 -0
- package/dist/Constants.d.ts.map +1 -0
- package/dist/Constants.js +18 -0
- package/dist/Constants.js.map +1 -0
- package/dist/Errors/CliError.d.ts +5 -0
- package/dist/Errors/CliError.d.ts.map +1 -0
- package/dist/Errors/CliError.js +7 -0
- package/dist/Errors/CliError.js.map +1 -0
- package/dist/FS/CliFs.d.ts +41 -0
- package/dist/FS/CliFs.d.ts.map +1 -0
- package/dist/FS/CliFs.js +136 -0
- package/dist/FS/CliFs.js.map +1 -0
- package/dist/HttpClient/CliHttpClient.d.ts +24 -0
- package/dist/HttpClient/CliHttpClient.d.ts.map +1 -0
- package/dist/HttpClient/CliHttpClient.js +122 -0
- package/dist/HttpClient/CliHttpClient.js.map +1 -0
- package/dist/HttpClient/OrchardProxy.d.ts +11 -0
- package/dist/HttpClient/OrchardProxy.d.ts.map +1 -0
- package/dist/HttpClient/OrchardProxy.js +25 -0
- package/dist/HttpClient/OrchardProxy.js.map +1 -0
- package/dist/HttpClient/Proxy.d.ts +7 -0
- package/dist/HttpClient/Proxy.d.ts.map +1 -0
- package/dist/HttpClient/Proxy.js +5 -0
- package/dist/HttpClient/Proxy.js.map +1 -0
- package/dist/Logger/CliLogger.d.ts +36 -0
- package/dist/Logger/CliLogger.d.ts.map +1 -0
- package/dist/Logger/CliLogger.js +235 -0
- package/dist/Logger/CliLogger.js.map +1 -0
- package/dist/Logger/ConsoleWriter.d.ts +15 -0
- package/dist/Logger/ConsoleWriter.d.ts.map +1 -0
- package/dist/Logger/ConsoleWriter.js +45 -0
- package/dist/Logger/ConsoleWriter.js.map +1 -0
- package/dist/Logger/Constants.d.ts +6 -0
- package/dist/Logger/Constants.d.ts.map +1 -0
- package/dist/Logger/Constants.js +15 -0
- package/dist/Logger/Constants.js.map +1 -0
- package/dist/Logger/LoggerSettings.d.ts +19 -0
- package/dist/Logger/LoggerSettings.d.ts.map +1 -0
- package/dist/Logger/LoggerSettings.js +114 -0
- package/dist/Logger/LoggerSettings.js.map +1 -0
- package/dist/Logger/OneDSWriter.d.ts +22 -0
- package/dist/Logger/OneDSWriter.d.ts.map +1 -0
- package/dist/Logger/OneDSWriter.js +129 -0
- package/dist/Logger/OneDSWriter.js.map +1 -0
- package/dist/Logger/Scrubber/ScrubCustomDimensions.d.ts +5 -0
- package/dist/Logger/Scrubber/ScrubCustomDimensions.d.ts.map +1 -0
- package/dist/Logger/Scrubber/ScrubCustomDimensions.js +46 -0
- package/dist/Logger/Scrubber/ScrubCustomDimensions.js.map +1 -0
- package/dist/Logger/Scrubber/ScrubCustomDimensionsConfig.d.ts +27 -0
- package/dist/Logger/Scrubber/ScrubCustomDimensionsConfig.d.ts.map +1 -0
- package/dist/Logger/Scrubber/ScrubCustomDimensionsConfig.js +168 -0
- package/dist/Logger/Scrubber/ScrubCustomDimensionsConfig.js.map +1 -0
- package/dist/Logger/Scrubber/ScrubUtils.d.ts +8 -0
- package/dist/Logger/Scrubber/ScrubUtils.d.ts.map +1 -0
- package/dist/Logger/Scrubber/ScrubUtils.js +34 -0
- package/dist/Logger/Scrubber/ScrubUtils.js.map +1 -0
- package/dist/Types/Argument.types.d.ts +27 -0
- package/dist/Types/Argument.types.d.ts.map +1 -0
- package/dist/Types/Argument.types.js +5 -0
- package/dist/Types/Argument.types.js.map +1 -0
- package/dist/Types/Cli.types.d.ts +32 -0
- package/dist/Types/Cli.types.d.ts.map +1 -0
- package/dist/Types/Cli.types.js +5 -0
- package/dist/Types/Cli.types.js.map +1 -0
- package/dist/Types/Command.types.d.ts +52 -0
- package/dist/Types/Command.types.d.ts.map +1 -0
- package/dist/Types/Command.types.js +8 -0
- package/dist/Types/Command.types.js.map +1 -0
- package/dist/Types/Datasource.types.d.ts +43 -0
- package/dist/Types/Datasource.types.d.ts.map +1 -0
- package/dist/Types/Datasource.types.js +5 -0
- package/dist/Types/Datasource.types.js.map +1 -0
- package/dist/Types/LoggerTypes.types.d.ts +17 -0
- package/dist/Types/LoggerTypes.types.d.ts.map +1 -0
- package/dist/Types/LoggerTypes.types.js +5 -0
- package/dist/Types/LoggerTypes.types.js.map +1 -0
- package/dist/Utils/BuildVerbDescriptionTable.d.ts +19 -0
- package/dist/Utils/BuildVerbDescriptionTable.d.ts.map +1 -0
- package/dist/Utils/BuildVerbDescriptionTable.js +87 -0
- package/dist/Utils/BuildVerbDescriptionTable.js.map +1 -0
- package/dist/Utils/ConsoleOutput.d.ts +80 -0
- package/dist/Utils/ConsoleOutput.d.ts.map +1 -0
- package/dist/Utils/ConsoleOutput.js +151 -0
- package/dist/Utils/ConsoleOutput.js.map +1 -0
- package/dist/Utils/DetectPackageManager.d.ts +9 -0
- package/dist/Utils/DetectPackageManager.d.ts.map +1 -0
- package/dist/Utils/DetectPackageManager.js +24 -0
- package/dist/Utils/DetectPackageManager.js.map +1 -0
- package/dist/Utils/EnhanceNetworkError.d.ts +14 -0
- package/dist/Utils/EnhanceNetworkError.d.ts.map +1 -0
- package/dist/Utils/EnhanceNetworkError.js +59 -0
- package/dist/Utils/EnhanceNetworkError.js.map +1 -0
- package/dist/Utils/EnvVars.d.ts +17 -0
- package/dist/Utils/EnvVars.d.ts.map +1 -0
- package/dist/Utils/EnvVars.js +38 -0
- package/dist/Utils/EnvVars.js.map +1 -0
- package/dist/Utils/GitCredentialConfig.d.ts +12 -0
- package/dist/Utils/GitCredentialConfig.d.ts.map +1 -0
- package/dist/Utils/GitCredentialConfig.js +32 -0
- package/dist/Utils/GitCredentialConfig.js.map +1 -0
- package/dist/Utils/GitUtils.d.ts +22 -0
- package/dist/Utils/GitUtils.d.ts.map +1 -0
- package/dist/Utils/GitUtils.js +35 -0
- package/dist/Utils/GitUtils.js.map +1 -0
- package/dist/Utils/Paging.d.ts +23 -0
- package/dist/Utils/Paging.d.ts.map +1 -0
- package/dist/Utils/Paging.js +67 -0
- package/dist/Utils/Paging.js.map +1 -0
- package/dist/Utils/ReadPackageJson.d.ts +9 -0
- package/dist/Utils/ReadPackageJson.d.ts.map +1 -0
- package/dist/Utils/ReadPackageJson.js +13 -0
- package/dist/Utils/ReadPackageJson.js.map +1 -0
- package/dist/Utils/ResolveCommand.d.ts +26 -0
- package/dist/Utils/ResolveCommand.d.ts.map +1 -0
- package/dist/Utils/ResolveCommand.js +62 -0
- package/dist/Utils/ResolveCommand.js.map +1 -0
- package/dist/Utils/RunCommand.d.ts +33 -0
- package/dist/Utils/RunCommand.d.ts.map +1 -0
- package/dist/Utils/RunCommand.js +83 -0
- package/dist/Utils/RunCommand.js.map +1 -0
- package/dist/Utils/Spinner.d.ts +15 -0
- package/dist/Utils/Spinner.d.ts.map +1 -0
- package/dist/Utils/Spinner.js +71 -0
- package/dist/Utils/Spinner.js.map +1 -0
- package/dist/Utils/zipBuildArtifact.d.ts +35 -0
- package/dist/Utils/zipBuildArtifact.d.ts.map +1 -0
- package/dist/Utils/zipBuildArtifact.js +100 -0
- package/dist/Utils/zipBuildArtifact.js.map +1 -0
- package/dist/Verbs/AddAction.d.ts +6 -0
- package/dist/Verbs/AddAction.d.ts.map +1 -0
- package/dist/Verbs/AddAction.js +87 -0
- package/dist/Verbs/AddAction.js.map +1 -0
- package/dist/Verbs/AddDataSource.d.ts +19 -0
- package/dist/Verbs/AddDataSource.d.ts.map +1 -0
- package/dist/Verbs/AddDataSource.js +173 -0
- package/dist/Verbs/AddDataSource.js.map +1 -0
- package/dist/Verbs/AddFlow.d.ts +12 -0
- package/dist/Verbs/AddFlow.d.ts.map +1 -0
- package/dist/Verbs/AddFlow.js +67 -0
- package/dist/Verbs/AddFlow.js.map +1 -0
- package/dist/Verbs/AddTable.d.ts +6 -0
- package/dist/Verbs/AddTable.d.ts.map +1 -0
- package/dist/Verbs/AddTable.js +163 -0
- package/dist/Verbs/AddTable.js.map +1 -0
- package/dist/Verbs/AuthStatus.d.ts +6 -0
- package/dist/Verbs/AuthStatus.d.ts.map +1 -0
- package/dist/Verbs/AuthStatus.js +18 -0
- package/dist/Verbs/AuthStatus.js.map +1 -0
- package/dist/Verbs/BuildMaafApp.d.ts +6 -0
- package/dist/Verbs/BuildMaafApp.d.ts.map +1 -0
- package/dist/Verbs/BuildMaafApp.js +96 -0
- package/dist/Verbs/BuildMaafApp.js.map +1 -0
- package/dist/Verbs/ConnectorHelpers.d.ts +37 -0
- package/dist/Verbs/ConnectorHelpers.d.ts.map +1 -0
- package/dist/Verbs/ConnectorHelpers.js +116 -0
- package/dist/Verbs/ConnectorHelpers.js.map +1 -0
- package/dist/Verbs/CreateMaafApp.d.ts +58 -0
- package/dist/Verbs/CreateMaafApp.d.ts.map +1 -0
- package/dist/Verbs/CreateMaafApp.js +712 -0
- package/dist/Verbs/CreateMaafApp.js.map +1 -0
- package/dist/Verbs/DeleteDataSource.d.ts +6 -0
- package/dist/Verbs/DeleteDataSource.d.ts.map +1 -0
- package/dist/Verbs/DeleteDataSource.js +105 -0
- package/dist/Verbs/DeleteDataSource.js.map +1 -0
- package/dist/Verbs/DeleteMaafApp.d.ts +23 -0
- package/dist/Verbs/DeleteMaafApp.d.ts.map +1 -0
- package/dist/Verbs/DeleteMaafApp.js +98 -0
- package/dist/Verbs/DeleteMaafApp.js.map +1 -0
- package/dist/Verbs/DeployMaafApp.d.ts +6 -0
- package/dist/Verbs/DeployMaafApp.d.ts.map +1 -0
- package/dist/Verbs/DeployMaafApp.js +501 -0
- package/dist/Verbs/DeployMaafApp.js.map +1 -0
- package/dist/Verbs/Dev.d.ts +6 -0
- package/dist/Verbs/Dev.d.ts.map +1 -0
- package/dist/Verbs/Dev.js +212 -0
- package/dist/Verbs/Dev.js.map +1 -0
- package/dist/Verbs/FindDataverseApi.d.ts +12 -0
- package/dist/Verbs/FindDataverseApi.d.ts.map +1 -0
- package/dist/Verbs/FindDataverseApi.js +101 -0
- package/dist/Verbs/FindDataverseApi.js.map +1 -0
- package/dist/Verbs/GetMaafApp.d.ts +6 -0
- package/dist/Verbs/GetMaafApp.d.ts.map +1 -0
- package/dist/Verbs/GetMaafApp.js +59 -0
- package/dist/Verbs/GetMaafApp.js.map +1 -0
- package/dist/Verbs/GetMaafBuildStatus.d.ts +6 -0
- package/dist/Verbs/GetMaafBuildStatus.d.ts.map +1 -0
- package/dist/Verbs/GetMaafBuildStatus.js +67 -0
- package/dist/Verbs/GetMaafBuildStatus.js.map +1 -0
- package/dist/Verbs/GetSettings.d.ts +12 -0
- package/dist/Verbs/GetSettings.d.ts.map +1 -0
- package/dist/Verbs/GetSettings.js +48 -0
- package/dist/Verbs/GetSettings.js.map +1 -0
- package/dist/Verbs/Init.d.ts +6 -0
- package/dist/Verbs/Init.d.ts.map +1 -0
- package/dist/Verbs/Init.js +171 -0
- package/dist/Verbs/Init.js.map +1 -0
- package/dist/Verbs/ListConnectionReferences.d.ts +6 -0
- package/dist/Verbs/ListConnectionReferences.d.ts.map +1 -0
- package/dist/Verbs/ListConnectionReferences.js +70 -0
- package/dist/Verbs/ListConnectionReferences.js.map +1 -0
- package/dist/Verbs/ListConnectorActions.d.ts +6 -0
- package/dist/Verbs/ListConnectorActions.d.ts.map +1 -0
- package/dist/Verbs/ListConnectorActions.js +73 -0
- package/dist/Verbs/ListConnectorActions.js.map +1 -0
- package/dist/Verbs/ListConnectors.d.ts +6 -0
- package/dist/Verbs/ListConnectors.d.ts.map +1 -0
- package/dist/Verbs/ListConnectors.js +66 -0
- package/dist/Verbs/ListConnectors.js.map +1 -0
- package/dist/Verbs/ListDatasets.d.ts +6 -0
- package/dist/Verbs/ListDatasets.d.ts.map +1 -0
- package/dist/Verbs/ListDatasets.js +82 -0
- package/dist/Verbs/ListDatasets.js.map +1 -0
- package/dist/Verbs/ListEnvironmentVariables.d.ts +14 -0
- package/dist/Verbs/ListEnvironmentVariables.d.ts.map +1 -0
- package/dist/Verbs/ListEnvironmentVariables.js +66 -0
- package/dist/Verbs/ListEnvironmentVariables.js.map +1 -0
- package/dist/Verbs/ListFlows.d.ts +13 -0
- package/dist/Verbs/ListFlows.d.ts.map +1 -0
- package/dist/Verbs/ListFlows.js +88 -0
- package/dist/Verbs/ListFlows.js.map +1 -0
- package/dist/Verbs/ListMaafApps.d.ts +71 -0
- package/dist/Verbs/ListMaafApps.d.ts.map +1 -0
- package/dist/Verbs/ListMaafApps.js +168 -0
- package/dist/Verbs/ListMaafApps.js.map +1 -0
- package/dist/Verbs/ListSqlStoredProcedures.d.ts +6 -0
- package/dist/Verbs/ListSqlStoredProcedures.d.ts.map +1 -0
- package/dist/Verbs/ListSqlStoredProcedures.js +84 -0
- package/dist/Verbs/ListSqlStoredProcedures.js.map +1 -0
- package/dist/Verbs/ListTables.d.ts +6 -0
- package/dist/Verbs/ListTables.d.ts.map +1 -0
- package/dist/Verbs/ListTables.js +94 -0
- package/dist/Verbs/ListTables.js.map +1 -0
- package/dist/Verbs/Login.d.ts +6 -0
- package/dist/Verbs/Login.d.ts.map +1 -0
- package/dist/Verbs/Login.js +28 -0
- package/dist/Verbs/Login.js.map +1 -0
- package/dist/Verbs/Logout.d.ts +6 -0
- package/dist/Verbs/Logout.d.ts.map +1 -0
- package/dist/Verbs/Logout.js +28 -0
- package/dist/Verbs/Logout.js.map +1 -0
- package/dist/Verbs/MaafAppPermissions.d.ts +23 -0
- package/dist/Verbs/MaafAppPermissions.d.ts.map +1 -0
- package/dist/Verbs/MaafAppPermissions.js +278 -0
- package/dist/Verbs/MaafAppPermissions.js.map +1 -0
- package/dist/Verbs/Pack.d.ts +42 -0
- package/dist/Verbs/Pack.d.ts.map +1 -0
- package/dist/Verbs/Pack.js +114 -0
- package/dist/Verbs/Pack.js.map +1 -0
- package/dist/Verbs/RefreshDataSource.d.ts +6 -0
- package/dist/Verbs/RefreshDataSource.d.ts.map +1 -0
- package/dist/Verbs/RefreshDataSource.js +72 -0
- package/dist/Verbs/RefreshDataSource.js.map +1 -0
- package/dist/Verbs/RemoveFlow.d.ts +12 -0
- package/dist/Verbs/RemoveFlow.d.ts.map +1 -0
- package/dist/Verbs/RemoveFlow.js +99 -0
- package/dist/Verbs/RemoveFlow.js.map +1 -0
- package/dist/Verbs/SetSetting.d.ts +12 -0
- package/dist/Verbs/SetSetting.d.ts.map +1 -0
- package/dist/Verbs/SetSetting.js +141 -0
- package/dist/Verbs/SetSetting.js.map +1 -0
- package/dist/Verbs/ShareMaafApp.d.ts +12 -0
- package/dist/Verbs/ShareMaafApp.d.ts.map +1 -0
- package/dist/Verbs/ShareMaafApp.js +14 -0
- package/dist/Verbs/ShareMaafApp.js.map +1 -0
- package/dist/Verbs/ShowMaafApp.d.ts +94 -0
- package/dist/Verbs/ShowMaafApp.d.ts.map +1 -0
- package/dist/Verbs/ShowMaafApp.js +216 -0
- package/dist/Verbs/ShowMaafApp.js.map +1 -0
- package/dist/Verbs/TelemetrySettings.d.ts +8 -0
- package/dist/Verbs/TelemetrySettings.d.ts.map +1 -0
- package/dist/Verbs/TelemetrySettings.js +104 -0
- package/dist/Verbs/TelemetrySettings.js.map +1 -0
- package/dist/Verbs/UnshareMaafApp.d.ts +12 -0
- package/dist/Verbs/UnshareMaafApp.d.ts.map +1 -0
- package/dist/Verbs/UnshareMaafApp.js +14 -0
- package/dist/Verbs/UnshareMaafApp.js.map +1 -0
- package/dist/Verbs/VerbConstants.d.ts +415 -0
- package/dist/Verbs/VerbConstants.d.ts.map +1 -0
- package/dist/Verbs/VerbConstants.js +382 -0
- package/dist/Verbs/VerbConstants.js.map +1 -0
- package/dist/Verbs/index.d.ts +39 -0
- package/dist/Verbs/index.d.ts.map +1 -0
- package/dist/Verbs/index.js +39 -0
- package/dist/Verbs/index.js.map +1 -0
- package/package.json +52 -0
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (C) Microsoft Corporation. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import { createAsync, getPlayerServiceConfig, updateEnvironmentName, writeRepoConfig, } from '@microsoft/managed-apps-actions';
|
|
5
|
+
import { AppConfigFileName } from '@microsoft/managed-apps-common/config';
|
|
6
|
+
import { getUserRoutingEnvironmentAsync, ManagedGovernanceError, ManagedGovernanceTimeoutError, pollRoutingEnvironmentAsync, } from '@microsoft/managed-apps-common/services';
|
|
7
|
+
import { execFileSync } from 'child_process';
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import { downloadTemplate } from 'giget';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import { getCliLogger } from '../CliSettings.js';
|
|
12
|
+
import { createDefaultStringOption } from '../CliUtils.js';
|
|
13
|
+
import { CLI_CLIENT_ID } from '../Constants.js';
|
|
14
|
+
import { CLI_BINARY_NAME } from '../Constants/CliConstants.js';
|
|
15
|
+
import { HelpStrings } from '../Constants/HelpStrings.js';
|
|
16
|
+
import { AppError, ConfigurationError, NotFoundError, UsageError } from '../Errors/CliError.js';
|
|
17
|
+
import { printInfo, printJsonSuccess, printSuccess, printWarning } from '../Utils/ConsoleOutput.js';
|
|
18
|
+
import { buildGcmConfig } from '../Utils/GitCredentialConfig.js';
|
|
19
|
+
import { CreateMaafAppArguments } from './VerbConstants.js';
|
|
20
|
+
const DEFAULT_TEMPLATE = 'github:microsoft/Managed-Apps/templates/vite8';
|
|
21
|
+
/**
|
|
22
|
+
* Sentinel value for `--repo` that selects the no-source-control flow: just create the
|
|
23
|
+
* server-side app record, write `ms.config.json` with `repoType: 'none'`, and skip every
|
|
24
|
+
* local-setup step (git init/clone, template overlay, GCM config) plus the server-side
|
|
25
|
+
* GRS auto-provisioning. Comparison is case-insensitive — `none`, `None`, `NONE` all
|
|
26
|
+
* select this flow. Any other non-empty `--repo` value is treated as a remote URL.
|
|
27
|
+
*/
|
|
28
|
+
const REPO_NONE_SENTINEL = 'none';
|
|
29
|
+
/**
|
|
30
|
+
* Maps the raw `--repo` flag value to a `(repoType, remoteUrl)` pair.
|
|
31
|
+
* - omitted / empty → `native` (default GRS-backed flow)
|
|
32
|
+
* - `none` (case-insensitive) → `none` (no GRS, no local setup)
|
|
33
|
+
* - any other non-empty value → `github` (external GHE binding, value flows through as
|
|
34
|
+
* `remoteUrl` to the action and the eventual git clone)
|
|
35
|
+
*/
|
|
36
|
+
export function parseRepoOption(rawRepoValue) {
|
|
37
|
+
if (rawRepoValue === undefined || rawRepoValue === '') {
|
|
38
|
+
return { repoType: 'native', remoteUrl: undefined };
|
|
39
|
+
}
|
|
40
|
+
if (rawRepoValue.trim().toLowerCase() === REPO_NONE_SENTINEL) {
|
|
41
|
+
return { repoType: 'none', remoteUrl: undefined };
|
|
42
|
+
}
|
|
43
|
+
return { repoType: 'github', remoteUrl: rawRepoValue };
|
|
44
|
+
}
|
|
45
|
+
export async function createMaafAppAsync(argumentProvider) {
|
|
46
|
+
createOptionSchema(argumentProvider);
|
|
47
|
+
await argumentProvider.runVerb();
|
|
48
|
+
const tenantId = await argumentProvider.getTenantIdAsync();
|
|
49
|
+
if (!tenantId) {
|
|
50
|
+
throw new UsageError('Unable to determine tenant ID. Ensure you are logged in.');
|
|
51
|
+
}
|
|
52
|
+
const logger = getCliLogger();
|
|
53
|
+
const httpClient = argumentProvider.getHttpClient();
|
|
54
|
+
const { region } = getPlayerServiceConfig();
|
|
55
|
+
const displayName = await argumentProvider.getOption('displayName');
|
|
56
|
+
if (!displayName) {
|
|
57
|
+
throw new UsageError('Argument displayName is required. Use --display-name or -n to specify it.');
|
|
58
|
+
}
|
|
59
|
+
// The positional arg (or cwd) is both the git repo root and the app dir.
|
|
60
|
+
const appDir = argumentProvider.getPositionalArg(0) ?? process.cwd();
|
|
61
|
+
const template = await argumentProvider.getOption('template', /* ignorePrompt */ true);
|
|
62
|
+
// CLI surface is --repo. Three flows, decided up front so the rest of the verb can
|
|
63
|
+
// branch on `repoType` instead of inspecting `remoteUrl` everywhere:
|
|
64
|
+
// - undefined → native (GRS-backed; current default)
|
|
65
|
+
// - 'none' → no-source-control (skip GRS + skip every local setup step)
|
|
66
|
+
// - any other value → github (external GHE binding; clone the URL locally)
|
|
67
|
+
// The local variable name `remoteUrl` is preserved on purpose: the wire contract to
|
|
68
|
+
// RP and the apps-actions param are still `remoteUrl` / `externalRepo.remoteUrl`, so
|
|
69
|
+
// renaming end-to-end is deferred until those callers move.
|
|
70
|
+
const rawRepoValue = await argumentProvider.getOption('repo',
|
|
71
|
+
/* ignorePrompt */ true);
|
|
72
|
+
const { repoType, remoteUrl } = parseRepoOption(rawRepoValue);
|
|
73
|
+
// Read --repository-id up front (instead of after preflight, like the other prompts)
|
|
74
|
+
// so the `--repo none` conflict check below has it. It is `optionalInNonInteractive`
|
|
75
|
+
// and registered with `ignorePrompt: true`, so reading early never triggers an
|
|
76
|
+
// interactive prompt.
|
|
77
|
+
const repositoryId = await argumentProvider.getOption('repositoryId',
|
|
78
|
+
/* ignorePrompt */ true);
|
|
79
|
+
// --json emits a single machine-parseable envelope on stdout, so every
|
|
80
|
+
// informational `printInfo` below is gated behind `!jsonMode` to keep
|
|
81
|
+
// stdout free of prose ahead of the envelope. `printError` writes to
|
|
82
|
+
// stderr and is unaffected.
|
|
83
|
+
const jsonMode = argumentProvider.isJsonMode();
|
|
84
|
+
// Identity confirmation: print the tenant + region we're targeting before
|
|
85
|
+
// anything else — preflight, routing, createAsync — so a user with multiple
|
|
86
|
+
// accounts signed in has the longest possible window to CTRL-C before any
|
|
87
|
+
// local check runs or server call fires. Environment id isn't known yet
|
|
88
|
+
// (routing is what resolves it); included in the success line at the end.
|
|
89
|
+
// Wording is intentionally tentative ("About to create…") so a preflight
|
|
90
|
+
// failure on the next line doesn't read as contradicting a "Creating…" line.
|
|
91
|
+
if (!jsonMode) {
|
|
92
|
+
printInfo(`About to create app '${displayName}' in tenant ${tenantId} (region ${region}).`);
|
|
93
|
+
}
|
|
94
|
+
// ─── Preflight ────────────────────────────────────────────────────────────
|
|
95
|
+
// Run all local-only checks before any server call. Once createAsync
|
|
96
|
+
// succeeds the user has a MAAF app + GRS repo on the server; a later
|
|
97
|
+
// local-setup failure leaves them orphaned with no clean recovery path.
|
|
98
|
+
// Anything we can validate without hitting the server belongs here.
|
|
99
|
+
//
|
|
100
|
+
// --repo path: still requires git installed, writable target, and an empty
|
|
101
|
+
// target (since `git clone` refuses non-empty destinations and we don't
|
|
102
|
+
// destructively wipe local files). Template-source validation is skipped —
|
|
103
|
+
// the template is overlaid onto the cloned working tree, but the same
|
|
104
|
+
// giget call validates the source. To bind a non-empty directory to a
|
|
105
|
+
// MAAF app, use `ms app init` instead.
|
|
106
|
+
//
|
|
107
|
+
// --repo none path: no git, no clone, no template — only ms.config.json
|
|
108
|
+
// gets written locally. The preflight reduces to (1) target dir must be
|
|
109
|
+
// writable so the config write later won't fail post-create, and (2) the
|
|
110
|
+
// dir must NOT already contain an ms.config.json (silently rebinding an
|
|
111
|
+
// existing config to a new app would be a footgun). Empty-dir is NOT
|
|
112
|
+
// required: the user opted out of scaffolding, so dropping an
|
|
113
|
+
// ms.config.json next to existing files is the intent.
|
|
114
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
115
|
+
if (!jsonMode) {
|
|
116
|
+
printInfo('Running preflight checks...');
|
|
117
|
+
}
|
|
118
|
+
// --repo none is incompatible with --repository-id: the former opts out of any
|
|
119
|
+
// platform-managed git binding, the latter forces one. Fail fast before any
|
|
120
|
+
// server call so we don't half-bind the app.
|
|
121
|
+
if (repoType === 'none' && repositoryId) {
|
|
122
|
+
throw new UsageError("'--repo none' is incompatible with '--repository-id'. " +
|
|
123
|
+
"Use '--repo none' for an app with no git binding, or omit '--repo' (or pass a repo URL) to use a backing repository.");
|
|
124
|
+
}
|
|
125
|
+
// --repo none does not scaffold a template, so a user-supplied --template would be
|
|
126
|
+
// silently dropped. Reject up front rather than appear to honor it.
|
|
127
|
+
if (repoType === 'none' && template !== DEFAULT_TEMPLATE) {
|
|
128
|
+
throw new UsageError("'--template' is not supported with '--repo none' because no files are scaffolded. " +
|
|
129
|
+
"Remove '--template' or drop '--repo none' if you want template scaffolding.");
|
|
130
|
+
}
|
|
131
|
+
if (repoType !== 'none') {
|
|
132
|
+
ensureGitInstalled();
|
|
133
|
+
}
|
|
134
|
+
if (repoType === 'native') {
|
|
135
|
+
ensureTemplateSourceValid(template);
|
|
136
|
+
}
|
|
137
|
+
ensureTargetDirectoryWritable(appDir);
|
|
138
|
+
if (repoType === 'github') {
|
|
139
|
+
ensureRepoTargetDirectoryEmpty(appDir);
|
|
140
|
+
}
|
|
141
|
+
else if (repoType === 'native') {
|
|
142
|
+
ensureTargetDirectoryEmpty(appDir);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
// --repo none: empty-dir is intentionally not required. We still guard against
|
|
146
|
+
// overwriting an existing ms.config.json so the user can't silently rebind a
|
|
147
|
+
// pre-existing app folder to a brand-new server app.
|
|
148
|
+
ensureNoExistingConfig(appDir);
|
|
149
|
+
}
|
|
150
|
+
// ─── End preflight ────────────────────────────────────────────────────────
|
|
151
|
+
// The debug env-var override (MS_CLI_MAAF_DEBUG_ENVIRONMENT_ID) is honored by ArgumentProvider
|
|
152
|
+
// and surfaced via getEnvironmentId(). When set, skip the routing API call entirely.
|
|
153
|
+
// For the supported user flow, getEnvironmentId() returns undefined here and routing fires.
|
|
154
|
+
const preResolvedEnvironmentId = argumentProvider.getEnvironmentId();
|
|
155
|
+
// Fire off PDE routing in parallel with the remaining option prompts. The result is awaited
|
|
156
|
+
// immediately before createAsync, so prompt latency is hidden behind the network call.
|
|
157
|
+
// Skipped entirely when the debug override pre-resolved the env id.
|
|
158
|
+
const routingEnvPromise = preResolvedEnvironmentId
|
|
159
|
+
? undefined
|
|
160
|
+
: getUserRoutingEnvironmentAsync(httpClient, tenantId, CLI_CLIENT_ID, { provisionDataverse: false }, region, logger);
|
|
161
|
+
// If a later prompt throws before we await the promise, the rejection would otherwise be
|
|
162
|
+
// unhandled. Log and swallow here; `await routingEnvPromise` below still surfaces the error.
|
|
163
|
+
routingEnvPromise?.catch((error) => {
|
|
164
|
+
logger.trackErrorEvent('CreateMaafApp.RoutingEnvironmentBackgroundFailure', {
|
|
165
|
+
error: error instanceof Error ? error.message : String(error),
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
const description = await argumentProvider.getOption('description',
|
|
169
|
+
/* ignorePrompt */ true);
|
|
170
|
+
const buildPath = await argumentProvider.getOption('buildPath', /* ignorePrompt */ true);
|
|
171
|
+
const buildCommand = await argumentProvider.getOption('buildCommand',
|
|
172
|
+
/* ignorePrompt */ true);
|
|
173
|
+
// Explicit branch keeps the non-null read of routingEnvPromise reachable only when
|
|
174
|
+
// preResolvedEnvironmentId is undefined — i.e. exactly when routingEnvPromise is defined.
|
|
175
|
+
const environmentId = preResolvedEnvironmentId !== undefined
|
|
176
|
+
? preResolvedEnvironmentId
|
|
177
|
+
: await resolveRoutingEnvironmentIdAsync(routingEnvPromise, httpClient, logger);
|
|
178
|
+
updateEnvironmentName(environmentId);
|
|
179
|
+
// Re-check the directory invariant right before the server call. The same
|
|
180
|
+
// check ran in preflight, but the routing-env resolution between then and
|
|
181
|
+
// now is async and may take seconds — long enough for the user (or another
|
|
182
|
+
// tool) to drop a file (or ms.config.json) into appDir. Repeating the check
|
|
183
|
+
// here keeps the TOCTOU window between "directory looks fine" and
|
|
184
|
+
// "createAsync succeeded" as small as possible without ever orphaning a
|
|
185
|
+
// server-side app.
|
|
186
|
+
if (repoType === 'github') {
|
|
187
|
+
ensureRepoTargetDirectoryEmpty(appDir);
|
|
188
|
+
}
|
|
189
|
+
else if (repoType === 'native') {
|
|
190
|
+
ensureTargetDirectoryEmpty(appDir);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
ensureNoExistingConfig(appDir);
|
|
194
|
+
}
|
|
195
|
+
const { app, cloneUrl } = await createAsync({
|
|
196
|
+
tenantId,
|
|
197
|
+
environmentId,
|
|
198
|
+
logger,
|
|
199
|
+
actionsParams: { displayName, description, repositoryId, remoteUrl, repoType },
|
|
200
|
+
});
|
|
201
|
+
// `--repo none` is the only path where the server intentionally returns no GRS clone
|
|
202
|
+
// URL (we asked for "no platform-managed repo"). The native and github flows both
|
|
203
|
+
// expect one: native uses it as origin for the first push; github uses it as the
|
|
204
|
+
// platform-managed build-artifact store backing the external GHE binding. Treat its
|
|
205
|
+
// absence on those flows as a server bug — the hoisted check below guards both the
|
|
206
|
+
// pretty-output `(clone URL: undefined)` and JSON-envelope `gitRemoteUrl: undefined`
|
|
207
|
+
// failure modes that would otherwise leak through.
|
|
208
|
+
if (repoType !== 'none' && !cloneUrl) {
|
|
209
|
+
const recoveryHint = buildOrphanedAppRecoveryHint(app.id, app.environmentId);
|
|
210
|
+
if (repoType === 'github') {
|
|
211
|
+
throw new AppError(`App '${app.displayName}' was created and bound to ${remoteUrl}, but the ` +
|
|
212
|
+
`platform-managed git repository for build artifact storage was not provisioned. ` +
|
|
213
|
+
`This is a platform-side issue; please report it.\n` +
|
|
214
|
+
recoveryHint);
|
|
215
|
+
}
|
|
216
|
+
throw new AppError(`App '${app.displayName}' was created on the server, but creating a git repository failed. ` +
|
|
217
|
+
`This is a platform-side issue; please report it. ` +
|
|
218
|
+
`In the meantime, the partial app needs to be cleaned up.\n` +
|
|
219
|
+
recoveryHint);
|
|
220
|
+
}
|
|
221
|
+
// Set up local files. Runs after the API calls succeed; if any step here
|
|
222
|
+
// fails the server app is already created and the recovery hint below tells
|
|
223
|
+
// the user how to clean it up. The branch per `repoType`:
|
|
224
|
+
// - native: init local git repo against the auto-provisioned GRS clone URL,
|
|
225
|
+
// then overlay the template, then write ms.config.json.
|
|
226
|
+
// - github: clone the user's external repo, sanity-check it's empty,
|
|
227
|
+
// overlay the template, then write ms.config.json.
|
|
228
|
+
// - none: no git, no template — just mkdir -p and write ms.config.json.
|
|
229
|
+
try {
|
|
230
|
+
if (repoType === 'github') {
|
|
231
|
+
// External GitHub Enterprise path: clone the developer's repo. Auth flows through
|
|
232
|
+
// the user's existing git credential setup (gh, GCM browser OAuth, SSH key, PAT,
|
|
233
|
+
// x509 client cert, etc.); the CLI does not inject any tokens at this layer (the
|
|
234
|
+
// Microsoft GitHub App is server-side only per the MAAF GitHub Integration design
|
|
235
|
+
// doc Requirement 1).
|
|
236
|
+
cloneRepo({ directory: appDir, repoUrl: remoteUrl });
|
|
237
|
+
// Post-clone sanity: --repo at P1 expects an empty GHE repo we can scaffold the
|
|
238
|
+
// template into. If the cloned working tree already has content (existing project,
|
|
239
|
+
// README, LICENSE, etc.), `ms app init` is the path that handles binding an existing
|
|
240
|
+
// project to a new MAAF app — bail with that pointer instead of overwriting files.
|
|
241
|
+
const workingTreeEntries = fs.readdirSync(appDir).filter((entry) => entry !== '.git');
|
|
242
|
+
if (workingTreeEntries.length > 0) {
|
|
243
|
+
throw new UsageError(`Repository ${remoteUrl} is not empty. Choose a different repository to create an app or run 'ms app init' to update the existing app.`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
else if (repoType === 'native') {
|
|
247
|
+
initLocalRepo({
|
|
248
|
+
directory: appDir,
|
|
249
|
+
gitUrl: cloneUrl,
|
|
250
|
+
loginAuthority: argumentProvider.getLoginAuthority(tenantId),
|
|
251
|
+
authResource: argumentProvider.getAuthResourceUrl(),
|
|
252
|
+
jsonMode,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
// --repo none: the user opted out of all scaffolding. Just ensure the
|
|
257
|
+
// target dir exists so the writeRepoConfig call below can land
|
|
258
|
+
// ms.config.json next to whatever the user already has (or in a fresh
|
|
259
|
+
// empty dir). `mkdirSync(..., { recursive: true })` is a no-op if the
|
|
260
|
+
// directory already exists.
|
|
261
|
+
fs.mkdirSync(appDir, { recursive: true });
|
|
262
|
+
}
|
|
263
|
+
// Overlay the template for the flows that scaffold one. `--repo none` opts out of
|
|
264
|
+
// scaffolding entirely, so this block is skipped and the user's existing files
|
|
265
|
+
// (if any) are left alone.
|
|
266
|
+
if (repoType !== 'none') {
|
|
267
|
+
if (!jsonMode) {
|
|
268
|
+
printInfo(`Fetching template '${template}' into ${appDir}...`);
|
|
269
|
+
}
|
|
270
|
+
await downloadTemplate(template, { dir: appDir, force: true });
|
|
271
|
+
}
|
|
272
|
+
const config = {
|
|
273
|
+
appId: app.id,
|
|
274
|
+
appDisplayName: app.displayName,
|
|
275
|
+
description: app.description,
|
|
276
|
+
environmentId: app.environmentId,
|
|
277
|
+
// `--repo none` apps have no platform-managed git binding; omit `repositoryId`
|
|
278
|
+
// from the persisted config so downstream verbs that key off it see the
|
|
279
|
+
// intentional absence rather than an empty/placeholder value.
|
|
280
|
+
...(repoType !== 'none' && { repositoryId: app.repositoryId }),
|
|
281
|
+
repoType,
|
|
282
|
+
cloud: argumentProvider.getCloud(),
|
|
283
|
+
buildPath: buildPath ?? './dist',
|
|
284
|
+
buildCommand: buildCommand ?? 'npm run build',
|
|
285
|
+
};
|
|
286
|
+
await writeRepoConfig(path.join(appDir, AppConfigFileName), config);
|
|
287
|
+
}
|
|
288
|
+
catch (setupError) {
|
|
289
|
+
// UsageError carries already-actionable messages (e.g. "repo already contains an
|
|
290
|
+
// app — try ms app init"). Let those through unwrapped so the user sees the
|
|
291
|
+
// intended phrasing instead of the generic "local setup failed" prefix.
|
|
292
|
+
if (setupError instanceof UsageError) {
|
|
293
|
+
throw setupError;
|
|
294
|
+
}
|
|
295
|
+
throw new AppError(`App '${app.displayName}' was created, but local setup failed: ${setupError instanceof Error ? setupError.message : String(setupError)}\n` + buildOrphanedAppRecoveryHint(app.id, app.environmentId));
|
|
296
|
+
}
|
|
297
|
+
if (jsonMode) {
|
|
298
|
+
// `appId` is the bare GUID returned by the server (`MaafAppDefinition.id`) and
|
|
299
|
+
// also written to ms.config.json — used by every subsequent CLI command
|
|
300
|
+
// (build, deploy, info, etc.). `gitRemoteUrl` is the GRS clone URL (auto-provisioned
|
|
301
|
+
// alongside the external binding for build artifact storage) and is omitted for
|
|
302
|
+
// `--repo none` since no platform-managed git repo exists. `externalRepoUrl` is the
|
|
303
|
+
// user-supplied GHE URL and only appears in the `github` flow. `repoType` is always
|
|
304
|
+
// emitted so automation can detect the new no-source-control path.
|
|
305
|
+
printJsonSuccess({
|
|
306
|
+
appId: app.id,
|
|
307
|
+
repoType,
|
|
308
|
+
...(repoType === 'github' && remoteUrl && { externalRepoUrl: remoteUrl }),
|
|
309
|
+
...(repoType !== 'none' && cloneUrl && { gitRemoteUrl: cloneUrl }),
|
|
310
|
+
appDir,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
printSuccess(`Created app '${app.displayName}' in ${appDir}.`);
|
|
315
|
+
if (repoType === 'github') {
|
|
316
|
+
printInfo(`A platform-managed git repository was also provisioned for build artifact storage (clone URL: ${cloneUrl}).`);
|
|
317
|
+
}
|
|
318
|
+
else if (repoType === 'none') {
|
|
319
|
+
printInfo('Your app has been created. Run `git init` (or your VCS of choice) when ready; ' +
|
|
320
|
+
'subsequent `ms app deploy` calls do not require a git remote.');
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async function resolveRoutingEnvironmentIdAsync(resultPromise, httpClient, logger) {
|
|
325
|
+
// Translate routing-service errors into a UsageError so the user sees an actionable message
|
|
326
|
+
// rather than the raw "ManagedGovernance request failed with status 403" implementation detail.
|
|
327
|
+
// NotFoundError throws inside the try also flow through `toRoutingUsageError`, which leaves
|
|
328
|
+
// non-ManagedGovernance errors unchanged.
|
|
329
|
+
try {
|
|
330
|
+
const result = await resultPromise;
|
|
331
|
+
if (result.status === 200) {
|
|
332
|
+
const name = result.data.environment?.name;
|
|
333
|
+
if (!name) {
|
|
334
|
+
throw new NotFoundError('Routing service returned 200 with no environment name.');
|
|
335
|
+
}
|
|
336
|
+
return name;
|
|
337
|
+
}
|
|
338
|
+
// 202: provisioning is in progress — poll until it completes.
|
|
339
|
+
// Non-2xx responses are already thrown as ManagedGovernanceError inside
|
|
340
|
+
// getUserRoutingEnvironmentAsync, so only 200/202 reach this code path.
|
|
341
|
+
const routingResponse = await pollRoutingEnvironmentAsync(httpClient, result.pollingUrl, result.authResource, logger);
|
|
342
|
+
if (!routingResponse.environment?.name) {
|
|
343
|
+
throw new NotFoundError('Routing service finished provisioning but returned no environment name.');
|
|
344
|
+
}
|
|
345
|
+
return routingResponse.environment.name;
|
|
346
|
+
}
|
|
347
|
+
catch (err) {
|
|
348
|
+
throw toRoutingUsageError(err);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function toRoutingUsageError(err) {
|
|
352
|
+
if (err instanceof ManagedGovernanceError) {
|
|
353
|
+
return new UsageError(`Could not provision a Developer environment for your tenant (status ${err.status}). ` +
|
|
354
|
+
'Ensure your account has the required permissions to create a Developer environment.');
|
|
355
|
+
}
|
|
356
|
+
if (err instanceof ManagedGovernanceTimeoutError) {
|
|
357
|
+
return new UsageError('Timed out waiting for a Developer environment to provision. Try again.');
|
|
358
|
+
}
|
|
359
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Clones the developer's external repo (GitHub Enterprise Cloud in v1) into `directory`.
|
|
363
|
+
* Auth flows through the user's existing git credential setup — the CLI does not inject
|
|
364
|
+
* tokens here. On failure, surfaces a CLI-friendly error pointing the user at the
|
|
365
|
+
* GitHub App install prerequisite; git's own stderr is preserved (stdio: inherit) so
|
|
366
|
+
* the underlying message appears inline above the wrapper.
|
|
367
|
+
*
|
|
368
|
+
* Targeting rules (mirrors `git clone` semantics):
|
|
369
|
+
* - directory non-existent → clone creates it
|
|
370
|
+
* - directory exists and is empty → clone fills it
|
|
371
|
+
* - directory exists and is non-empty → clone fails (caller's pre-network guard
|
|
372
|
+
* should reject this case earlier with a clearer message)
|
|
373
|
+
*/
|
|
374
|
+
export function cloneRepo({ directory, repoUrl }) {
|
|
375
|
+
try {
|
|
376
|
+
execFileSync('git', ['clone', repoUrl, directory], {
|
|
377
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
catch (err) {
|
|
381
|
+
throw new ConfigurationError(`Failed to clone '${repoUrl}' into '${directory}'. Ensure the Microsoft GitHub App is installed and authorized for this repository, and that your git credentials can access it (gh auth login, GCM, or your configured SSH key/PAT). Underlying error: ${err instanceof Error ? err.message : String(err)}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
export function initLocalRepo({ directory, gitUrl, loginAuthority, authResource, jsonMode = false, }) {
|
|
385
|
+
let gcmConfig;
|
|
386
|
+
try {
|
|
387
|
+
gcmConfig = buildGcmConfig({ gitUrl, loginAuthority, authResource });
|
|
388
|
+
}
|
|
389
|
+
catch (err) {
|
|
390
|
+
throw new ConfigurationError(`Git credential helper setup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
391
|
+
}
|
|
392
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
393
|
+
execFileSync('git', ['init'], { cwd: directory, stdio: ['ignore', 'ignore', 'inherit'] });
|
|
394
|
+
// Write GCM config before fetching so the first network call is already authenticated.
|
|
395
|
+
for (const [key, value] of gcmConfig) {
|
|
396
|
+
execFileSync('git', ['config', '--local', key, value], {
|
|
397
|
+
cwd: directory,
|
|
398
|
+
stdio: ['ignore', 'ignore', 'inherit'],
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
// `git remote add` fails if origin already exists. Pipe stderr so we can inspect the
|
|
402
|
+
// message and only fall back to `set-url` for that specific case — other failures
|
|
403
|
+
// (permissions, corrupt repo) must propagate rather than produce a misleading set-url error.
|
|
404
|
+
try {
|
|
405
|
+
execFileSync('git', ['remote', 'add', 'origin', gitUrl], {
|
|
406
|
+
cwd: directory,
|
|
407
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
catch (err) {
|
|
411
|
+
const stderr = err.stderr?.toString() ?? '';
|
|
412
|
+
if (!stderr.includes('already exists')) {
|
|
413
|
+
process.stderr.write(stderr);
|
|
414
|
+
throw err;
|
|
415
|
+
}
|
|
416
|
+
execFileSync('git', ['remote', 'set-url', 'origin', gitUrl], {
|
|
417
|
+
cwd: directory,
|
|
418
|
+
stdio: ['ignore', 'ignore', 'inherit'],
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
// The first `git fetch` triggers GCM's OAuth flow if the user has no cached
|
|
422
|
+
// credentials, which can pop a browser without warning. Tell the user up
|
|
423
|
+
// front so they don't see the browser as a surprise (and know which host /
|
|
424
|
+
// account to authenticate against). Skipped under `--json` so the prose
|
|
425
|
+
// doesn't end up interleaved with the JSON envelope on stdout.
|
|
426
|
+
if (!jsonMode) {
|
|
427
|
+
printInfo(`Authenticating to ${new URL(gitUrl).host} for the initial git fetch. ` +
|
|
428
|
+
'A browser window may open. Sign in with the account associated with this tenant.');
|
|
429
|
+
}
|
|
430
|
+
execFileSync('git', ['fetch', 'origin'], {
|
|
431
|
+
cwd: directory,
|
|
432
|
+
stdio: ['ignore', 'ignore', 'inherit'],
|
|
433
|
+
});
|
|
434
|
+
// Probe for origin/main locally (no network needed after fetch).
|
|
435
|
+
// A broad catch here is safe: the only failure mode is "ref doesn't exist".
|
|
436
|
+
let hasMain = false;
|
|
437
|
+
try {
|
|
438
|
+
execFileSync('git', ['rev-parse', '--verify', 'origin/main'], {
|
|
439
|
+
cwd: directory,
|
|
440
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
441
|
+
});
|
|
442
|
+
hasMain = true;
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
// Empty GRS repo — origin/main not yet present.
|
|
446
|
+
}
|
|
447
|
+
if (hasMain) {
|
|
448
|
+
execFileSync('git', ['checkout', '-b', 'main', 'origin/main'], {
|
|
449
|
+
cwd: directory,
|
|
450
|
+
stdio: ['ignore', 'ignore', 'inherit'],
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
// ─── Recovery hint for post-create failures ─────────────────────────────────
|
|
455
|
+
// Once createAsync returns, the MAAF app + GRS repo exist on the server; any
|
|
456
|
+
// later failure (no gitUrl, initLocalRepo, downloadTemplate, writeRepoConfig)
|
|
457
|
+
// strands them. Append this hint to the error message so the user can copy a
|
|
458
|
+
// working `app delete --force` line straight from their terminal and retry.
|
|
459
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
460
|
+
export function buildOrphanedAppRecoveryHint(serverAppId, environmentId) {
|
|
461
|
+
return (`The server app is still in environment ${environmentId} (id: ${serverAppId}).\n` +
|
|
462
|
+
`If you want to clean it up before retrying, run the following command:\n` +
|
|
463
|
+
` npx ${CLI_BINARY_NAME} app delete --app ${serverAppId} --environment-id ${environmentId} --force`);
|
|
464
|
+
}
|
|
465
|
+
// ─── Preflight helpers ──────────────────────────────────────────────────────
|
|
466
|
+
// All checks below run before any server call so a local misconfiguration
|
|
467
|
+
// (missing git, bad template source, unwritable directory) fails fast and
|
|
468
|
+
// never leaves an orphaned MAAF app + GRS repo on the server.
|
|
469
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
470
|
+
export function ensureGitInstalled() {
|
|
471
|
+
try {
|
|
472
|
+
execFileSync('git', ['--version'], { stdio: ['ignore', 'ignore', 'ignore'] });
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
throw new ConfigurationError("'git' is required but was not found on PATH. Install git from https://git-scm.com/ and retry.");
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
// Known giget provider prefixes. Used only for a *soft* "did you mean?" warning —
|
|
479
|
+
// not a hard reject, so adding a new giget provider doesn't silently fail
|
|
480
|
+
// preflight here. Sourced from giget's docs (github, gitlab, bitbucket, sourcehut,
|
|
481
|
+
// http, https) as of the version pinned in package.json. Update opportunistically
|
|
482
|
+
// when bumping giget; a stale entry only suppresses one warning, never blocks
|
|
483
|
+
// a valid template.
|
|
484
|
+
const KNOWN_GIGET_PROVIDERS = new Set([
|
|
485
|
+
'github',
|
|
486
|
+
'gitlab',
|
|
487
|
+
'bitbucket',
|
|
488
|
+
'sourcehut',
|
|
489
|
+
'http',
|
|
490
|
+
'https',
|
|
491
|
+
]);
|
|
492
|
+
// Single-edit Damerau-Levenshtein distance, capped at 1: returns true iff `a`
|
|
493
|
+
// is one substitution, one insertion, one deletion, or one adjacent
|
|
494
|
+
// transposition away from `b`. Used to pick the closest known provider for a
|
|
495
|
+
// "did you mean?" hint without pulling in a dependency.
|
|
496
|
+
function isOneEditAway(a, b) {
|
|
497
|
+
if (a === b)
|
|
498
|
+
return false;
|
|
499
|
+
if (Math.abs(a.length - b.length) > 1)
|
|
500
|
+
return false;
|
|
501
|
+
if (a.length === b.length) {
|
|
502
|
+
let diff = 0;
|
|
503
|
+
let transposition = null;
|
|
504
|
+
for (let i = 0; i < a.length; i++) {
|
|
505
|
+
if (a[i] !== b[i]) {
|
|
506
|
+
diff++;
|
|
507
|
+
if (transposition === null)
|
|
508
|
+
transposition = i;
|
|
509
|
+
if (diff > 2)
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (diff <= 1)
|
|
514
|
+
return true;
|
|
515
|
+
if (diff === 2 && transposition !== null) {
|
|
516
|
+
// Adjacent swap: gihub vs github, swap positions transposition and transposition+1.
|
|
517
|
+
return a[transposition] === b[transposition + 1] && a[transposition + 1] === b[transposition];
|
|
518
|
+
}
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
// One insertion or deletion away. Walk both strings; allow one skip.
|
|
522
|
+
const [shorter, longer] = a.length < b.length ? [a, b] : [b, a];
|
|
523
|
+
let i = 0;
|
|
524
|
+
let j = 0;
|
|
525
|
+
let skipped = false;
|
|
526
|
+
while (i < shorter.length && j < longer.length) {
|
|
527
|
+
if (shorter[i] === longer[j]) {
|
|
528
|
+
i++;
|
|
529
|
+
j++;
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
if (skipped)
|
|
533
|
+
return false;
|
|
534
|
+
skipped = true;
|
|
535
|
+
j++;
|
|
536
|
+
}
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
export function ensureTemplateSourceValid(source) {
|
|
540
|
+
// Hard validation: only enforce non-empty. Any further structural validation
|
|
541
|
+
// (provider name, URL shape, registry lookup) is giget's responsibility —
|
|
542
|
+
// duplicating its provider list locally goes stale silently when giget adds
|
|
543
|
+
// support for a new provider, turning a valid template into a misleading
|
|
544
|
+
// "unknown provider" error at preflight.
|
|
545
|
+
if (!source) {
|
|
546
|
+
throw new UsageError('Template source is empty. Use --template to specify one.');
|
|
547
|
+
}
|
|
548
|
+
// Soft validation: if the source looks like `<prefix>:<rest>` but the prefix
|
|
549
|
+
// is one edit away from a known giget provider (e.g. `gihub:` → `github:`),
|
|
550
|
+
// warn but proceed. giget will produce the authoritative error if the
|
|
551
|
+
// prefix is genuinely invalid; for the common typo case we surface the fix
|
|
552
|
+
// before the network round-trip. No hard-fail keeps this forward-compatible
|
|
553
|
+
// with new giget providers — a stale `KNOWN_GIGET_PROVIDERS` set only
|
|
554
|
+
// suppresses a warning, it never blocks a valid template.
|
|
555
|
+
const prefixMatch = /^([a-z][a-z0-9+.-]*):/i.exec(source);
|
|
556
|
+
if (!prefixMatch)
|
|
557
|
+
return;
|
|
558
|
+
const prefix = prefixMatch[1].toLowerCase();
|
|
559
|
+
if (KNOWN_GIGET_PROVIDERS.has(prefix))
|
|
560
|
+
return;
|
|
561
|
+
const suggestion = [...KNOWN_GIGET_PROVIDERS].find((known) => isOneEditAway(prefix, known));
|
|
562
|
+
if (suggestion) {
|
|
563
|
+
printWarning(`Template source '${source}' uses an unrecognized provider prefix '${prefix}:'. Did you mean '${suggestion}:'?`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
export function ensureTargetDirectoryEmpty(appDir) {
|
|
567
|
+
// Scaffolding into a non-empty directory would mix the template with the
|
|
568
|
+
// user's files, and initLocalRepo's `git remote set-url` fallback would
|
|
569
|
+
// silently replace an existing `origin` remote. Called twice: once in
|
|
570
|
+
// preflight (fail fast before any network) and once right before
|
|
571
|
+
// createAsync (catch a TOCTOU race where the dir gets populated during
|
|
572
|
+
// routing/option resolution).
|
|
573
|
+
if (!fs.existsSync(appDir))
|
|
574
|
+
return;
|
|
575
|
+
const entries = fs.readdirSync(appDir);
|
|
576
|
+
if (entries.length > 0) {
|
|
577
|
+
throw new UsageError(`'${appDir}' is not empty. Specify an empty directory as the first argument, or run 'ms app init' to bind an existing project to a new app.`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
export function ensureRepoTargetDirectoryEmpty(appDir) {
|
|
581
|
+
// --repo variant: empty-dir is mandatory. `git clone` refuses non-empty
|
|
582
|
+
// destinations and we don't destructively wipe local files. To bind an
|
|
583
|
+
// existing local project to a new MAAF app, use `ms app init` instead.
|
|
584
|
+
// Called twice (preflight + just-before-createAsync) for the same TOCTOU
|
|
585
|
+
// reasons as ensureTargetDirectoryEmpty.
|
|
586
|
+
if (!fs.existsSync(appDir))
|
|
587
|
+
return;
|
|
588
|
+
const entries = fs.readdirSync(appDir);
|
|
589
|
+
if (entries.length > 0) {
|
|
590
|
+
throw new UsageError(`'${appDir}' is not empty. --repo requires an empty target directory; specify an empty directory as the first argument or move to one before running.`);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
export function ensureNoExistingConfig(appDir) {
|
|
594
|
+
// --repo none variant: empty-dir is not required, but we MUST refuse to overwrite an
|
|
595
|
+
// existing ms.config.json. Silently replacing it would rebind the folder (and any
|
|
596
|
+
// subsequent `ms app push/build/deploy` invocations) to a different server app — a
|
|
597
|
+
// footgun loud enough to gate fail-fast in preflight. Called twice (preflight +
|
|
598
|
+
// just-before-createAsync) for the same TOCTOU reasons as ensureTargetDirectoryEmpty.
|
|
599
|
+
const configPath = path.join(appDir, AppConfigFileName);
|
|
600
|
+
if (fs.existsSync(configPath)) {
|
|
601
|
+
throw new UsageError(`'${configPath}' already exists. --repo none refuses to overwrite an existing app config; ` +
|
|
602
|
+
`remove the file (or choose a different directory) before retrying, or run 'ms app init' ` +
|
|
603
|
+
`to bind this directory to a new app interactively.`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
export function ensureTargetDirectoryWritable(appDir) {
|
|
607
|
+
// Probe write permission on the target dir (or its nearest existing
|
|
608
|
+
// ancestor, where mkdirSync will need to write). Catches read-only mounts
|
|
609
|
+
// and permission-denied locations before any server-side resource is
|
|
610
|
+
// created. The empty-dir check is intentionally NOT here — it runs inline
|
|
611
|
+
// just before initLocalRepo so the TOCTOU window between check and write
|
|
612
|
+
// stays as small as possible (a directory can become non-empty while the
|
|
613
|
+
// routing/createAsync calls are in flight).
|
|
614
|
+
//
|
|
615
|
+
// Best-effort caveat: `accessSync(W_OK)` is a syscall-level approximation,
|
|
616
|
+
// not a real write. On Windows in particular, NTFS ACL inheritance can grant
|
|
617
|
+
// W_OK on the ancestor while the actual CreateDirectory / CreateFile fails
|
|
618
|
+
// later (UAC virtualization, parent ACE applies but a deny ACE on the leaf
|
|
619
|
+
// does not). We accept that: catching every Windows ACL edge case requires
|
|
620
|
+
// a real mkdir+write+delete probe, which leaves debris on success and has
|
|
621
|
+
// its own TOCTOU. The recovery hint on post-create failures is the
|
|
622
|
+
// backstop when the probe is too optimistic.
|
|
623
|
+
let probe = appDir;
|
|
624
|
+
while (!fs.existsSync(probe)) {
|
|
625
|
+
const parent = path.dirname(probe);
|
|
626
|
+
if (parent === probe)
|
|
627
|
+
break;
|
|
628
|
+
probe = parent;
|
|
629
|
+
}
|
|
630
|
+
try {
|
|
631
|
+
fs.accessSync(probe, fs.constants.W_OK);
|
|
632
|
+
}
|
|
633
|
+
catch {
|
|
634
|
+
throw new ConfigurationError(`'${appDir}' is not writable. Choose a different directory or fix the permissions and retry.`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
function createOptionSchema(argumentProvider) {
|
|
638
|
+
argumentProvider.setDescription(HelpStrings.app.create.description);
|
|
639
|
+
argumentProvider.addExamples(HelpStrings.app.create.examples);
|
|
640
|
+
argumentProvider.consumePositionalArg('[directory]');
|
|
641
|
+
argumentProvider.addOptions([
|
|
642
|
+
createDefaultStringOption({
|
|
643
|
+
flags: {
|
|
644
|
+
key: CreateMaafAppArguments.DisplayName.name,
|
|
645
|
+
alias: CreateMaafAppArguments.DisplayName.alias,
|
|
646
|
+
},
|
|
647
|
+
env: CreateMaafAppArguments.DisplayName.envVar,
|
|
648
|
+
message: CreateMaafAppArguments.DisplayName.promptMessage,
|
|
649
|
+
description: HelpStrings.app.create.options.displayName,
|
|
650
|
+
}),
|
|
651
|
+
{
|
|
652
|
+
flags: {
|
|
653
|
+
key: CreateMaafAppArguments.Description.name,
|
|
654
|
+
alias: CreateMaafAppArguments.Description.alias,
|
|
655
|
+
},
|
|
656
|
+
env: CreateMaafAppArguments.Description.envVar,
|
|
657
|
+
description: HelpStrings.app.create.options.description,
|
|
658
|
+
optionalInNonInteractive: true,
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
flags: {
|
|
662
|
+
key: CreateMaafAppArguments.RepositoryId.name,
|
|
663
|
+
alias: CreateMaafAppArguments.RepositoryId.alias,
|
|
664
|
+
},
|
|
665
|
+
env: CreateMaafAppArguments.RepositoryId.envVar,
|
|
666
|
+
description: HelpStrings.app.create.options.repositoryId,
|
|
667
|
+
optionalInNonInteractive: true,
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
flags: {
|
|
671
|
+
key: CreateMaafAppArguments.Template.name,
|
|
672
|
+
alias: CreateMaafAppArguments.Template.alias,
|
|
673
|
+
},
|
|
674
|
+
env: CreateMaafAppArguments.Template.envVar,
|
|
675
|
+
default: DEFAULT_TEMPLATE,
|
|
676
|
+
description: HelpStrings.app.create.options.template,
|
|
677
|
+
optionalInNonInteractive: true,
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
// No short alias for --repo yet; see VerbConstants.CreateMaafAppArguments.Repo.
|
|
681
|
+
flags: {
|
|
682
|
+
key: CreateMaafAppArguments.Repo.name,
|
|
683
|
+
},
|
|
684
|
+
env: CreateMaafAppArguments.Repo.envVar,
|
|
685
|
+
description: HelpStrings.app.create.options.repo,
|
|
686
|
+
optionalInNonInteractive: true,
|
|
687
|
+
},
|
|
688
|
+
createDefaultStringOption({
|
|
689
|
+
flags: {
|
|
690
|
+
key: CreateMaafAppArguments.BuildPath.name,
|
|
691
|
+
alias: CreateMaafAppArguments.BuildPath.alias,
|
|
692
|
+
},
|
|
693
|
+
env: CreateMaafAppArguments.BuildPath.envVar,
|
|
694
|
+
message: CreateMaafAppArguments.BuildPath.promptMessage,
|
|
695
|
+
initialValue: CreateMaafAppArguments.BuildPath.initialValue,
|
|
696
|
+
default: './dist',
|
|
697
|
+
description: HelpStrings.app.create.options.buildPath,
|
|
698
|
+
}),
|
|
699
|
+
createDefaultStringOption({
|
|
700
|
+
flags: {
|
|
701
|
+
key: CreateMaafAppArguments.BuildCommand.name,
|
|
702
|
+
alias: CreateMaafAppArguments.BuildCommand.alias,
|
|
703
|
+
},
|
|
704
|
+
env: CreateMaafAppArguments.BuildCommand.envVar,
|
|
705
|
+
message: CreateMaafAppArguments.BuildCommand.promptMessage,
|
|
706
|
+
initialValue: CreateMaafAppArguments.BuildCommand.initialValue,
|
|
707
|
+
default: 'npm run build',
|
|
708
|
+
description: HelpStrings.app.create.options.buildCommand,
|
|
709
|
+
}),
|
|
710
|
+
]);
|
|
711
|
+
}
|
|
712
|
+
//# sourceMappingURL=CreateMaafApp.js.map
|