@microsoft/managed-apps-cli 0.9.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -342
- package/dist/ArgumentProvider.d.ts.map +1 -1
- package/dist/ArgumentProvider.js +8 -1
- package/dist/ArgumentProvider.js.map +1 -1
- package/dist/Authentication/NodeMsalAuthenticationProvider.js +20 -1
- package/dist/Authentication/NodeMsalAuthenticationProvider.js.map +1 -1
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +15 -3
- package/dist/Cli.js.map +1 -1
- package/dist/Constants/CommandRegistry.d.ts +1 -2
- package/dist/Constants/CommandRegistry.d.ts.map +1 -1
- package/dist/Constants/CommandRegistry.js +26 -45
- package/dist/Constants/CommandRegistry.js.map +1 -1
- package/dist/Constants/HelpStrings.d.ts +12 -8
- package/dist/Constants/HelpStrings.d.ts.map +1 -1
- package/dist/Constants/HelpStrings.js +98 -80
- package/dist/Constants/HelpStrings.js.map +1 -1
- package/dist/Errors/GitHubErrorMap.d.ts +43 -14
- package/dist/Errors/GitHubErrorMap.d.ts.map +1 -1
- package/dist/Errors/GitHubErrorMap.js +189 -147
- package/dist/Errors/GitHubErrorMap.js.map +1 -1
- package/dist/Logger/CliLogger.d.ts +2 -0
- package/dist/Logger/CliLogger.d.ts.map +1 -1
- package/dist/Logger/CliLogger.js +12 -3
- package/dist/Logger/CliLogger.js.map +1 -1
- package/dist/Logger/ConsoleWriter.d.ts +2 -0
- package/dist/Logger/ConsoleWriter.d.ts.map +1 -1
- package/dist/Logger/ConsoleWriter.js +17 -7
- package/dist/Logger/ConsoleWriter.js.map +1 -1
- package/dist/Types/Argument.types.d.ts +1 -0
- package/dist/Types/Argument.types.d.ts.map +1 -1
- package/dist/Types/Command.types.d.ts +6 -0
- package/dist/Types/Command.types.d.ts.map +1 -1
- package/dist/Types/Command.types.js +8 -0
- package/dist/Types/Command.types.js.map +1 -1
- package/dist/Utils/BuildVerbDescriptionTable.d.ts.map +1 -1
- package/dist/Utils/BuildVerbDescriptionTable.js +6 -2
- package/dist/Utils/BuildVerbDescriptionTable.js.map +1 -1
- package/dist/Utils/ConnectorIds.d.ts +12 -0
- package/dist/Utils/ConnectorIds.d.ts.map +1 -0
- package/dist/Utils/ConnectorIds.js +20 -0
- package/dist/Utils/ConnectorIds.js.map +1 -0
- package/dist/Utils/DcfRecovery.d.ts +7 -7
- package/dist/Utils/DcfRecovery.d.ts.map +1 -1
- package/dist/Utils/DcfRecovery.js +17 -37
- package/dist/Utils/DcfRecovery.js.map +1 -1
- package/dist/Utils/DownloadTemplate.d.ts +45 -0
- package/dist/Utils/DownloadTemplate.d.ts.map +1 -0
- package/dist/Utils/DownloadTemplate.js +161 -0
- package/dist/Utils/DownloadTemplate.js.map +1 -0
- package/dist/Utils/EnvVars.d.ts +1 -1
- package/dist/Utils/EnvVars.d.ts.map +1 -1
- package/dist/Utils/EnvVars.js.map +1 -1
- package/dist/Utils/EnvironmentRoutingUtils.d.ts +20 -1
- package/dist/Utils/EnvironmentRoutingUtils.d.ts.map +1 -1
- package/dist/Utils/EnvironmentRoutingUtils.js +78 -3
- package/dist/Utils/EnvironmentRoutingUtils.js.map +1 -1
- package/dist/Utils/GitErrorClassifier.d.ts +40 -0
- package/dist/Utils/GitErrorClassifier.d.ts.map +1 -0
- package/dist/Utils/GitErrorClassifier.js +120 -0
- package/dist/Utils/GitErrorClassifier.js.map +1 -0
- package/dist/Utils/GitRunner.d.ts +107 -0
- package/dist/Utils/GitRunner.d.ts.map +1 -0
- package/dist/Utils/GitRunner.js +89 -0
- package/dist/Utils/GitRunner.js.map +1 -0
- package/dist/Utils/GitUtils.d.ts +12 -0
- package/dist/Utils/GitUtils.d.ts.map +1 -1
- package/dist/Utils/GitUtils.js +83 -20
- package/dist/Utils/GitUtils.js.map +1 -1
- package/dist/Utils/GitVersionChecker.d.ts +38 -0
- package/dist/Utils/GitVersionChecker.d.ts.map +1 -0
- package/dist/Utils/GitVersionChecker.js +64 -0
- package/dist/Utils/GitVersionChecker.js.map +1 -0
- package/dist/Utils/MaafAppConfig.d.ts +22 -0
- package/dist/Utils/MaafAppConfig.d.ts.map +1 -0
- package/dist/Utils/MaafAppConfig.js +32 -0
- package/dist/Utils/MaafAppConfig.js.map +1 -0
- package/dist/Utils/NodeVersionChecker.d.ts +3 -6
- package/dist/Utils/NodeVersionChecker.d.ts.map +1 -1
- package/dist/Utils/NodeVersionChecker.js +9 -26
- package/dist/Utils/NodeVersionChecker.js.map +1 -1
- package/dist/Utils/Paging.d.ts +6 -0
- package/dist/Utils/Paging.d.ts.map +1 -1
- package/dist/Utils/Paging.js +36 -1
- package/dist/Utils/Paging.js.map +1 -1
- package/dist/Utils/PinCli.d.ts +28 -0
- package/dist/Utils/PinCli.d.ts.map +1 -0
- package/dist/Utils/PinCli.js +58 -0
- package/dist/Utils/PinCli.js.map +1 -0
- package/dist/Utils/RepoDetails.d.ts +29 -0
- package/dist/Utils/RepoDetails.d.ts.map +1 -0
- package/dist/Utils/RepoDetails.js +37 -0
- package/dist/Utils/RepoDetails.js.map +1 -0
- package/dist/Utils/RepoOption.d.ts +35 -0
- package/dist/Utils/RepoOption.d.ts.map +1 -0
- package/dist/Utils/RepoOption.js +36 -0
- package/dist/Utils/RepoOption.js.map +1 -0
- package/dist/Utils/ResolveCommand.d.ts.map +1 -1
- package/dist/Utils/ResolveCommand.js +6 -4
- package/dist/Utils/ResolveCommand.js.map +1 -1
- package/dist/Utils/Spinner.d.ts.map +1 -1
- package/dist/Utils/Spinner.js +3 -0
- package/dist/Utils/Spinner.js.map +1 -1
- package/dist/Verbs/AddAction.js +6 -6
- package/dist/Verbs/AddAction.js.map +1 -1
- package/dist/Verbs/AddConnector.d.ts +6 -4
- package/dist/Verbs/AddConnector.d.ts.map +1 -1
- package/dist/Verbs/AddConnector.js +54 -14
- package/dist/Verbs/AddConnector.js.map +1 -1
- package/dist/Verbs/AddConnectorFlows.d.ts +29 -16
- package/dist/Verbs/AddConnectorFlows.d.ts.map +1 -1
- package/dist/Verbs/AddConnectorFlows.js +73 -25
- package/dist/Verbs/AddConnectorFlows.js.map +1 -1
- package/dist/Verbs/AddDataSource.d.ts +3 -4
- package/dist/Verbs/AddDataSource.d.ts.map +1 -1
- package/dist/Verbs/AddDataSource.js +9 -20
- package/dist/Verbs/AddDataSource.js.map +1 -1
- package/dist/Verbs/AddTable.js +7 -7
- package/dist/Verbs/AddTable.js.map +1 -1
- package/dist/Verbs/AuthStatus.d.ts.map +1 -1
- package/dist/Verbs/AuthStatus.js +7 -4
- package/dist/Verbs/AuthStatus.js.map +1 -1
- package/dist/Verbs/BuildMaafApp.js +1 -1
- package/dist/Verbs/BuildMaafApp.js.map +1 -1
- package/dist/Verbs/ConnectorHelpers.d.ts +5 -4
- package/dist/Verbs/ConnectorHelpers.d.ts.map +1 -1
- package/dist/Verbs/ConnectorHelpers.js +14 -9
- package/dist/Verbs/ConnectorHelpers.js.map +1 -1
- package/dist/Verbs/CreateMaafApp.d.ts +25 -42
- package/dist/Verbs/CreateMaafApp.d.ts.map +1 -1
- package/dist/Verbs/CreateMaafApp.js +346 -268
- package/dist/Verbs/CreateMaafApp.js.map +1 -1
- package/dist/Verbs/DeployMaafApp.d.ts.map +1 -1
- package/dist/Verbs/DeployMaafApp.js +4 -4
- package/dist/Verbs/DeployMaafApp.js.map +1 -1
- package/dist/Verbs/Dev.js +1 -1
- package/dist/Verbs/Dev.js.map +1 -1
- package/dist/Verbs/GetMaafApp.d.ts.map +1 -1
- package/dist/Verbs/GetMaafApp.js +10 -0
- package/dist/Verbs/GetMaafApp.js.map +1 -1
- package/dist/Verbs/GetMaafBuildStatus.d.ts.map +1 -1
- package/dist/Verbs/GetMaafBuildStatus.js +52 -3
- package/dist/Verbs/GetMaafBuildStatus.js.map +1 -1
- package/dist/Verbs/GetSettings.js +1 -1
- package/dist/Verbs/GetSettings.js.map +1 -1
- package/dist/Verbs/Init.d.ts.map +1 -1
- package/dist/Verbs/Init.js +183 -60
- package/dist/Verbs/Init.js.map +1 -1
- package/dist/Verbs/ListConnectorActions.js +12 -9
- package/dist/Verbs/ListConnectorActions.js.map +1 -1
- package/dist/Verbs/ListConnectors.d.ts.map +1 -1
- package/dist/Verbs/ListConnectors.js +3 -2
- package/dist/Verbs/ListConnectors.js.map +1 -1
- package/dist/Verbs/ListMaafApps.js +3 -3
- package/dist/Verbs/ListMaafApps.js.map +1 -1
- package/dist/Verbs/MaafAppPermissions.d.ts.map +1 -1
- package/dist/Verbs/MaafAppPermissions.js +89 -17
- package/dist/Verbs/MaafAppPermissions.js.map +1 -1
- package/dist/Verbs/Pack.d.ts +1 -1
- package/dist/Verbs/RefreshDataSource.js +1 -1
- package/dist/Verbs/RefreshDataSource.js.map +1 -1
- package/dist/Verbs/RefreshGitAuth.d.ts.map +1 -1
- package/dist/Verbs/RefreshGitAuth.js +3 -15
- package/dist/Verbs/RefreshGitAuth.js.map +1 -1
- package/dist/Verbs/{DeleteDataSource.d.ts → RemoveDataSource.d.ts} +2 -2
- package/dist/Verbs/RemoveDataSource.d.ts.map +1 -0
- package/dist/Verbs/RemoveDataSource.js +158 -0
- package/dist/Verbs/RemoveDataSource.js.map +1 -0
- package/dist/Verbs/SetSetting.js +2 -2
- package/dist/Verbs/SetSetting.js.map +1 -1
- package/dist/Verbs/ShareLinkList.js +1 -1
- package/dist/Verbs/ShareLinkList.js.map +1 -1
- package/dist/Verbs/ShowMaafApp.d.ts +6 -0
- package/dist/Verbs/ShowMaafApp.d.ts.map +1 -1
- package/dist/Verbs/ShowMaafApp.js +10 -2
- package/dist/Verbs/ShowMaafApp.js.map +1 -1
- package/dist/Verbs/VerbConstants.d.ts +20 -23
- package/dist/Verbs/VerbConstants.d.ts.map +1 -1
- package/dist/Verbs/VerbConstants.js +20 -23
- package/dist/Verbs/VerbConstants.js.map +1 -1
- package/dist/Verbs/index.d.ts +1 -1
- package/dist/Verbs/index.d.ts.map +1 -1
- package/dist/Verbs/index.js +1 -1
- package/dist/Verbs/index.js.map +1 -1
- package/package.json +11 -9
- package/dist/Verbs/DeleteDataSource.d.ts.map +0 -1
- package/dist/Verbs/DeleteDataSource.js +0 -105
- package/dist/Verbs/DeleteDataSource.js.map +0 -1
|
@@ -4,9 +4,7 @@
|
|
|
4
4
|
import { createAsync, getPlayerServiceConfig, updateEnvironmentName, writeRepoConfig, } from '@microsoft/managed-apps-actions';
|
|
5
5
|
import { AppConfigFileName } from '@microsoft/managed-apps-common/config';
|
|
6
6
|
import { regionToCloudSafe } from '@microsoft/managed-apps-common/services';
|
|
7
|
-
import { execFileSync } from 'child_process';
|
|
8
7
|
import * as fs from 'fs';
|
|
9
|
-
import { downloadTemplate } from 'giget';
|
|
10
8
|
import * as os from 'os';
|
|
11
9
|
import * as path from 'path';
|
|
12
10
|
import { getCliLogger, getSettings } from '../CliSettings.js';
|
|
@@ -16,38 +14,17 @@ import { HelpStrings } from '../Constants/HelpStrings.js';
|
|
|
16
14
|
import { AppError, ConfigurationError, UsageError } from '../Errors/CliError.js';
|
|
17
15
|
import { printInfo, printJsonLine, printJsonSuccess, printSuccess, printWarning, } from '../Utils/ConsoleOutput.js';
|
|
18
16
|
import { withGitHubDcfRecoveryAsync } from '../Utils/DcfRecovery.js';
|
|
19
|
-
import {
|
|
17
|
+
import { downloadTemplate, parseGithubTemplate } from '../Utils/DownloadTemplate.js';
|
|
18
|
+
import { awaitRoutingEnvironmentAsync, startPdeRoutingAsync, } from '../Utils/EnvironmentRoutingUtils.js';
|
|
20
19
|
import { buildGcmConfig } from '../Utils/GitCredentialConfig.js';
|
|
20
|
+
import { runGit } from '../Utils/GitRunner.js';
|
|
21
|
+
import { ensureGitInstalled } from '../Utils/GitVersionChecker.js';
|
|
22
|
+
import { buildMaafAppConfig } from '../Utils/MaafAppConfig.js';
|
|
23
|
+
import { pinCliIntoAppPackageJson } from '../Utils/PinCli.js';
|
|
24
|
+
import { parseRepoOption } from '../Utils/RepoOption.js';
|
|
25
|
+
import { CliSpinner } from '../Utils/Spinner.js';
|
|
21
26
|
import { CreateMaafAppArguments } from './VerbConstants.js';
|
|
22
27
|
const DEFAULT_TEMPLATE = 'github:microsoft/Managed-Apps/templates/vite8';
|
|
23
|
-
/**
|
|
24
|
-
* Sentinel value for `--repo` that selects the no-source-control flow: just create the
|
|
25
|
-
* server-side app record, write `ms.config.json` with `repoType: 'none'`, and skip every
|
|
26
|
-
* local-setup step (git init/clone, template overlay, GCM config) plus the server-side
|
|
27
|
-
* GRS auto-provisioning. Comparison is case-insensitive — `none`, `None`, `NONE` all
|
|
28
|
-
* select this flow. Any other non-empty `--repo` value is treated as a remote URL.
|
|
29
|
-
*/
|
|
30
|
-
const REPO_NONE_SENTINEL = 'none';
|
|
31
|
-
/**
|
|
32
|
-
* Maps the raw `--repo` flag value to a `(repoType, remoteUrl)` pair.
|
|
33
|
-
* - omitted / empty → `native` (default GRS-backed flow)
|
|
34
|
-
* - `none` (case-insensitive) → `none` (no GRS, no local setup)
|
|
35
|
-
* - any other non-empty value → `github` (external GitHub binding — `*.ghe.com` or `github.com`;
|
|
36
|
-
* value flows through as `remoteUrl` to the action and the eventual git clone)
|
|
37
|
-
*
|
|
38
|
-
* Runtime invariant: `remoteUrl` is set iff `repoType === 'github'`. Consumers
|
|
39
|
-
* branch on `repoType` and assert `remoteUrl!` at github use sites — see
|
|
40
|
-
* `RepoSelection` for why narrowing is not automatic.
|
|
41
|
-
*/
|
|
42
|
-
export function parseRepoOption(rawRepoValue) {
|
|
43
|
-
if (rawRepoValue === undefined || rawRepoValue === '') {
|
|
44
|
-
return { repoType: 'native', remoteUrl: undefined };
|
|
45
|
-
}
|
|
46
|
-
if (rawRepoValue.trim().toLowerCase() === REPO_NONE_SENTINEL) {
|
|
47
|
-
return { repoType: 'none', remoteUrl: undefined };
|
|
48
|
-
}
|
|
49
|
-
return { repoType: 'github', remoteUrl: rawRepoValue };
|
|
50
|
-
}
|
|
51
28
|
/**
|
|
52
29
|
* Creates a new MAAF app and (where applicable) sets up local source control.
|
|
53
30
|
*
|
|
@@ -124,43 +101,60 @@ export async function createMaafAppAsync(argumentProvider) {
|
|
|
124
101
|
// wipe local files). To bind a non-empty directory to a MAAF app, use `ms app
|
|
125
102
|
// init` instead.
|
|
126
103
|
//
|
|
127
|
-
// --repo none path: no git
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
// other flows).
|
|
104
|
+
// --repo none path: no source-control git (no `git init`, no `git clone` of a
|
|
105
|
+
// user-supplied remote). Git itself is still required, because the template
|
|
106
|
+
// download is a sparse `git clone` of the github-hosted template repo —
|
|
107
|
+
// hence `ensureGitInstalled` runs unconditionally below.
|
|
132
108
|
// ──────────────────────────────────────────────────────────────────────────
|
|
133
|
-
|
|
109
|
+
const usePreflightSpinner = !jsonMode && process.stdout.isTTY;
|
|
110
|
+
if (!jsonMode && !usePreflightSpinner) {
|
|
134
111
|
printInfo('Running preflight checks...');
|
|
135
112
|
}
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
113
|
+
// Spinner runs only in TTY interactive mode; --json keeps stdout clean and
|
|
114
|
+
// non-TTY (CI, piped output) uses plain prose so log files stay readable.
|
|
115
|
+
// Most preflight checks are sync, so the spinner frame can pause while a
|
|
116
|
+
// check runs; we still update the status label before each check so users
|
|
117
|
+
// always see the current step. The try/finally guarantees we restore the
|
|
118
|
+
// cursor even if a preflight check throws.
|
|
119
|
+
const preflightSpinner = usePreflightSpinner ? new CliSpinner() : null;
|
|
120
|
+
preflightSpinner?.start('Running preflight checks...');
|
|
121
|
+
try {
|
|
122
|
+
// --repo none is incompatible with --repository-id: the former opts out of any
|
|
123
|
+
// platform-managed git binding, the latter forces one. Fail fast before any
|
|
124
|
+
// server call so we don't half-bind the app.
|
|
125
|
+
if (repoType === 'none' && repositoryId) {
|
|
126
|
+
throw new UsageError("'--repo none' is incompatible with '--repository-id'. " +
|
|
127
|
+
"Use '--repo none' for an app with no git binding, or omit '--repo' (or pass a repo URL) to use a backing repository.");
|
|
128
|
+
}
|
|
129
|
+
preflightSpinner?.update('Validating template source...');
|
|
130
|
+
ensureTemplateSourceValid(template);
|
|
131
|
+
preflightSpinner?.update('Checking git installation...');
|
|
145
132
|
ensureGitInstalled();
|
|
133
|
+
preflightSpinner?.update('Verifying target directory is writable...');
|
|
134
|
+
ensureTargetDirectoryWritable(appDir);
|
|
135
|
+
if (repoType === 'github') {
|
|
136
|
+
// RepoSelection is a flat interface (not a discriminated union), so `remoteUrl`
|
|
137
|
+
// doesn't auto-narrow to `string` here even though the github branch guarantees it.
|
|
138
|
+
preflightSpinner?.update('Verifying target directory is empty...');
|
|
139
|
+
ensureRepoTargetDirectoryEmpty(appDir);
|
|
140
|
+
// Pre-network: cheap partial clone into a temp dir + ls-tree HEAD to detect
|
|
141
|
+
// a non-empty remote before createAsync runs. Catches the rejection case
|
|
142
|
+
// without leaving an orphaned server-side app or scaffolded files in
|
|
143
|
+
// appDir. Cheapest probe shape: --depth=1 --filter=blob:none --no-checkout
|
|
144
|
+
// pulls refs + the tip commit + its tree(s) only (no blob bytes, no working
|
|
145
|
+
// tree). On any probe error (network, auth, server doesn't support partial
|
|
146
|
+
// clone, etc.) we swallow and let the subsequent cloneRepo emit its
|
|
147
|
+
// friendlier error — see ensureExternalRepoIsEmpty for the rationale.
|
|
148
|
+
preflightSpinner?.update(`Verifying ${remoteUrl} is empty...`);
|
|
149
|
+
ensureExternalRepoIsEmpty(remoteUrl);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
preflightSpinner?.update('Verifying target directory is empty...');
|
|
153
|
+
ensureTargetDirectoryEmpty(appDir);
|
|
154
|
+
}
|
|
146
155
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
// RepoSelection is a flat interface (not a discriminated union), so `remoteUrl`
|
|
150
|
-
// doesn't auto-narrow to `string` here even though the github branch guarantees it.
|
|
151
|
-
ensureRepoTargetDirectoryEmpty(appDir);
|
|
152
|
-
// Pre-network: cheap partial clone into a temp dir + ls-tree HEAD to detect
|
|
153
|
-
// a non-empty remote before createAsync runs. Catches the rejection case
|
|
154
|
-
// without leaving an orphaned server-side app or scaffolded files in
|
|
155
|
-
// appDir. Cheapest probe shape: --depth=1 --filter=blob:none --no-checkout
|
|
156
|
-
// pulls refs + the tip commit + its tree(s) only (no blob bytes, no working
|
|
157
|
-
// tree). On any probe error (network, auth, server doesn't support partial
|
|
158
|
-
// clone, etc.) we swallow and let the subsequent cloneRepo emit its
|
|
159
|
-
// friendlier error — see ensureExternalRepoIsEmpty for the rationale.
|
|
160
|
-
ensureExternalRepoIsEmpty(remoteUrl);
|
|
161
|
-
}
|
|
162
|
-
else {
|
|
163
|
-
ensureTargetDirectoryEmpty(appDir);
|
|
156
|
+
finally {
|
|
157
|
+
preflightSpinner?.stop();
|
|
164
158
|
}
|
|
165
159
|
// ─── End preflight ────────────────────────────────────────────────────────
|
|
166
160
|
// The debug env-var override (MS_CLI_MAAF_DEBUG_ENVIRONMENT_ID) is honored by ArgumentProvider
|
|
@@ -184,9 +178,10 @@ export async function createMaafAppAsync(argumentProvider) {
|
|
|
184
178
|
/* ignorePrompt */ true);
|
|
185
179
|
const buildEntryPoint = await argumentProvider.getOption('buildEntryPoint',
|
|
186
180
|
/* ignorePrompt */ true);
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
|
|
181
|
+
// Shared environment-routing logic between CreateMaafApp and Init.
|
|
182
|
+
// `awaitRoutingEnvironmentAsync` prints a `--environment-id` hint on any
|
|
183
|
+
// failure — HTTP error codes, `operationSuccess: false`, and exceptions.
|
|
184
|
+
const environmentId = await awaitRoutingEnvironmentAsync(preResolvedEnvironmentId, routingEnvPromise);
|
|
190
185
|
updateEnvironmentName(environmentId);
|
|
191
186
|
// Re-check the directory invariant right before the server call. The same
|
|
192
187
|
// check ran in preflight, but the routing-env resolution between then and
|
|
@@ -319,33 +314,49 @@ export async function createMaafAppAsync(argumentProvider) {
|
|
|
319
314
|
if (!jsonMode) {
|
|
320
315
|
printInfo(`Fetching template '${template}' into ${appDir}...`);
|
|
321
316
|
}
|
|
322
|
-
|
|
323
|
-
|
|
317
|
+
downloadTemplate(template, { dir: appDir });
|
|
318
|
+
// Pin the scaffolding CLI into the app so a later package install provides a
|
|
319
|
+
// project-local `ms` — which `npx ms` / `pnpm exec` / `bun x` resolve ahead of
|
|
320
|
+
// any globally-installed CLI — at the exact version the app was created with.
|
|
321
|
+
// Runs for every repo flow (the template, and therefore its package.json,
|
|
322
|
+
// lands via downloadTemplate above regardless of `--repo`).
|
|
323
|
+
pinCliIntoAppPackageJson(appDir, jsonMode);
|
|
324
|
+
const config = buildMaafAppConfig({
|
|
324
325
|
appId: app.id,
|
|
325
326
|
appDisplayName: app.displayName,
|
|
326
327
|
description: app.description,
|
|
327
328
|
environmentId: app.environmentId,
|
|
329
|
+
repoType,
|
|
330
|
+
cloud: argumentProvider.getCloud(),
|
|
331
|
+
buildPath,
|
|
332
|
+
buildCommand,
|
|
333
|
+
buildEntryPoint,
|
|
328
334
|
// Only the `native` flow has a platform-managed git binding. `github` tracks
|
|
329
335
|
// external binding server-side via `externalRepo`, and `none` intentionally has
|
|
330
336
|
// no platform-managed binding. Omit `repositoryId` for both so downstream verbs
|
|
331
337
|
// key off `repoType` instead of a placeholder value.
|
|
332
|
-
|
|
333
|
-
repoType,
|
|
338
|
+
repositoryId: repoType === 'native' ? app.repositoryId : undefined,
|
|
334
339
|
// externalRepoUrl is persisted only for github-bound apps so that post-create verbs
|
|
335
340
|
// (build, deploy) know which GitHub host to authenticate against during inline DCF
|
|
336
341
|
// recovery without re-prompting the user for the URL.
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
buildPath: buildPath ?? './dist',
|
|
340
|
-
buildCommand: buildCommand ?? 'npm run build',
|
|
341
|
-
buildEntryPoint: buildEntryPoint ?? 'index.html',
|
|
342
|
-
};
|
|
342
|
+
externalRepoUrl: repoType === 'github' ? remoteUrl : undefined,
|
|
343
|
+
});
|
|
343
344
|
await writeRepoConfig(path.join(appDir, AppConfigFileName), config);
|
|
344
345
|
// Auto commit + push for git-backed flows (native + github). The `none` flow
|
|
345
346
|
// has no remote, so it's skipped. Push failure is non-fatal — see the warning
|
|
346
347
|
// path in commitAndPushScaffoldAsync.
|
|
347
348
|
if (repoType !== 'none') {
|
|
348
|
-
|
|
349
|
+
// Classify the push only for the native (GRS) flow: we own its GCM browser
|
|
350
|
+
// OAuth credentials, so we can label an auth/network failure correctly. The
|
|
351
|
+
// github flow uses the developer's own credentials (gh / SSH / PAT) we don't
|
|
352
|
+
// control, so it's left unclassified. Unlike fetch, push is non-fatal: the
|
|
353
|
+
// classified error is caught and turned into a PushOutcome, so its exit code
|
|
354
|
+
// never propagates. Classification only relabels the cause-specific message —
|
|
355
|
+
// surfaced as the human-mode warning and carried in the --json error payload.
|
|
356
|
+
const pushClassify = repoType === 'native' && cloneUrl
|
|
357
|
+
? { action: `pushing to ${gitHostLabel(cloneUrl)}` }
|
|
358
|
+
: undefined;
|
|
359
|
+
pushOutcome = await commitAndPushScaffoldAsync(appDir, jsonMode, pushClassify);
|
|
349
360
|
}
|
|
350
361
|
}
|
|
351
362
|
catch (setupError) {
|
|
@@ -355,7 +366,21 @@ export async function createMaafAppAsync(argumentProvider) {
|
|
|
355
366
|
if (setupError instanceof UsageError) {
|
|
356
367
|
throw setupError;
|
|
357
368
|
}
|
|
358
|
-
|
|
369
|
+
const detail = setupError instanceof Error ? setupError.message : String(setupError);
|
|
370
|
+
const message = `App '${app.displayName}' was created, but local setup failed: ${detail}\n` +
|
|
371
|
+
buildOrphanedAppRecoveryHint(app.id, app.environmentId);
|
|
372
|
+
// Re-message a typed AppError in place rather than re-wrapping it, so the
|
|
373
|
+
// failure category survives the orphan-hint prefix intact: a classified
|
|
374
|
+
// AuthenticationError keeps its exit code (3), name, errorData, AND its
|
|
375
|
+
// subclass identity (`instanceof AuthenticationError` still holds for any
|
|
376
|
+
// future handler that branches on the class). Constructing a fresh AppError
|
|
377
|
+
// would downgrade everything to exit 1 and erase the subclass. The original
|
|
378
|
+
// message is preserved as `detail` inside the new message, so nothing is lost.
|
|
379
|
+
if (setupError instanceof AppError) {
|
|
380
|
+
setupError.message = message;
|
|
381
|
+
throw setupError;
|
|
382
|
+
}
|
|
383
|
+
throw new AppError(message);
|
|
359
384
|
}
|
|
360
385
|
if (jsonMode) {
|
|
361
386
|
// `appId` is the bare GUID returned by the server (`MaafAppDefinition.id`) and
|
|
@@ -392,17 +417,22 @@ export async function createMaafAppAsync(argumentProvider) {
|
|
|
392
417
|
* Failure is non-fatal: prints a warning with the retry command and returns
|
|
393
418
|
* `{ success: false, ... }`. The server-side app and local scaffold are unchanged.
|
|
394
419
|
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
420
|
+
* All three git commands go through {@link runGit}, so stdin is ignored (the
|
|
421
|
+
* child can never block waiting for input) and stdout is discarded (git's
|
|
422
|
+
* progress prose stays out of our pretty output). Stderr handling follows the
|
|
423
|
+
* runGit policy: the un-classified `add`/`commit` steps re-emit git's stderr on
|
|
424
|
+
* failure, and the `push` step either streams stderr live (un-classified github
|
|
425
|
+
* flow, `network: true`) or pipes it for classification (native flow, when a
|
|
426
|
+
* `pushClassify` context is supplied) — either way git's own error context
|
|
427
|
+
* reaches the user alongside our warning. Note that a classified push error is
|
|
428
|
+
* caught here, so (unlike fetch) its exit code never propagates; classification
|
|
429
|
+
* only shapes the warning text and the `--json` error payload.
|
|
400
430
|
*
|
|
401
431
|
* Commit identity (user.name / user.email) is read by git itself from
|
|
402
432
|
* local-then-global config — we pass nothing. Missing identity surfaces as
|
|
403
|
-
* git's own descriptive commit error
|
|
433
|
+
* git's own descriptive commit error via runGit's stderr handling.
|
|
404
434
|
*/
|
|
405
|
-
export async function commitAndPushScaffoldAsync(appDir, jsonMode) {
|
|
435
|
+
export async function commitAndPushScaffoldAsync(appDir, jsonMode, pushClassify) {
|
|
406
436
|
// Full add+commit+push sequence: any of the three commands may have been the
|
|
407
437
|
// one that failed (e.g. `git commit` errors when user.name/user.email aren't
|
|
408
438
|
// set), so re-running just `git push` wouldn't recover. The sequence is
|
|
@@ -411,17 +441,12 @@ export async function commitAndPushScaffoldAsync(appDir, jsonMode) {
|
|
|
411
441
|
// `git push` still pushes the existing commit.
|
|
412
442
|
const retryCommand = `git add . && git commit -m "Initial scaffold from 'ms app create'" && git push -u origin HEAD`;
|
|
413
443
|
try {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
});
|
|
418
|
-
execFileSync('git', ['commit', '-m', "Initial scaffold from 'ms app create'"], {
|
|
444
|
+
runGit(['add', '.'], { cwd: appDir });
|
|
445
|
+
runGit(['commit', '-m', "Initial scaffold from 'ms app create'"], { cwd: appDir });
|
|
446
|
+
runGit(['push', '-u', 'origin', 'HEAD'], {
|
|
419
447
|
cwd: appDir,
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
execFileSync('git', ['push', '-u', 'origin', 'HEAD'], {
|
|
423
|
-
cwd: appDir,
|
|
424
|
-
stdio: ['ignore', 'ignore', 'inherit'],
|
|
448
|
+
network: true,
|
|
449
|
+
classify: pushClassify,
|
|
425
450
|
});
|
|
426
451
|
return { success: true };
|
|
427
452
|
}
|
|
@@ -435,18 +460,88 @@ export async function commitAndPushScaffoldAsync(appDir, jsonMode) {
|
|
|
435
460
|
? { ...err }
|
|
436
461
|
: { message: String(err) };
|
|
437
462
|
if (!jsonMode) {
|
|
463
|
+
// A classified native push failure (AuthenticationError / NetworkError)
|
|
464
|
+
// carries the actionable, cause-specific guidance — plus git's own output —
|
|
465
|
+
// in its message, but runGit's classify path throws *before* re-emitting
|
|
466
|
+
// stderr, so without this the human-readable terminal would only see the
|
|
467
|
+
// generic retry hint. (Unclassified add/commit failures and the live-stderr
|
|
468
|
+
// github push already surfaced git's output, so only re-print for the
|
|
469
|
+
// classified case to avoid duplicate noise.)
|
|
470
|
+
if (err instanceof AppError) {
|
|
471
|
+
printWarning(err.message);
|
|
472
|
+
}
|
|
438
473
|
printWarning(`Could not commit and push the initial scaffold. Your app and local scaffold are ready. ` +
|
|
439
474
|
`To finish wiring it up, run:\n ${retryCommand}`);
|
|
440
475
|
}
|
|
441
476
|
return { success: false, error: errorPayload, retryCommand };
|
|
442
477
|
}
|
|
443
478
|
}
|
|
479
|
+
const GIT_CLONE_ACCESS_FAILURE_SIGNATURES = [
|
|
480
|
+
'repository not found',
|
|
481
|
+
'fatal: repository',
|
|
482
|
+
'could not read username',
|
|
483
|
+
'terminal prompts disabled',
|
|
484
|
+
'authentication failed',
|
|
485
|
+
'permission denied',
|
|
486
|
+
'403',
|
|
487
|
+
'remote: not found',
|
|
488
|
+
];
|
|
489
|
+
function getCapturedStderr(err) {
|
|
490
|
+
const stderr = err.stderr;
|
|
491
|
+
if (typeof stderr === 'string') {
|
|
492
|
+
return stderr;
|
|
493
|
+
}
|
|
494
|
+
if (Buffer.isBuffer(stderr)) {
|
|
495
|
+
return stderr.toString();
|
|
496
|
+
}
|
|
497
|
+
if (stderr instanceof Uint8Array) {
|
|
498
|
+
return Buffer.from(stderr).toString();
|
|
499
|
+
}
|
|
500
|
+
return '';
|
|
501
|
+
}
|
|
502
|
+
function getUnderlyingCloneError(err, stderr) {
|
|
503
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
504
|
+
const trimmedStderr = stderr.trim();
|
|
505
|
+
if (!trimmedStderr || message.includes(trimmedStderr)) {
|
|
506
|
+
return message;
|
|
507
|
+
}
|
|
508
|
+
return `${message}\n${trimmedStderr}`;
|
|
509
|
+
}
|
|
510
|
+
function isGitCloneAccessFailure(stderr) {
|
|
511
|
+
const normalizedStderr = stderr.toLowerCase();
|
|
512
|
+
return GIT_CLONE_ACCESS_FAILURE_SIGNATURES.some((signature) => normalizedStderr.includes(signature));
|
|
513
|
+
}
|
|
514
|
+
// Shared remediation body for the two access-failure surfaces (the pre-create
|
|
515
|
+
// probe and the post-create clone). Both are LOCAL `git clone` failures. The
|
|
516
|
+
// external-repo clone injects no credentials/config — it relies entirely on the
|
|
517
|
+
// user's own git setup — so the CLI can't meaningfully point at a specific
|
|
518
|
+
// credential mechanism (GCM, gh, SSH, PAT, …); the guidance stays generic.
|
|
519
|
+
// It also deliberately does NOT mention the Microsoft GitHub App or
|
|
520
|
+
// `git auth refresh`, which only affect the server-side Entra↔GitHub mapping.
|
|
521
|
+
// Carries no raw git output either; the raw detail is preserved on the thrown
|
|
522
|
+
// error's `errorData` for telemetry/debug — see the throw sites below.
|
|
523
|
+
function gitHubAccessFailureGuidance() {
|
|
524
|
+
return `Make sure the repository exists and that you have access to it.`;
|
|
525
|
+
}
|
|
526
|
+
function buildGitHubCloneAccessErrorMessage({ directory, repoUrl }) {
|
|
527
|
+
return `Failed to clone '${repoUrl}' into '${directory}'. ` + gitHubAccessFailureGuidance();
|
|
528
|
+
}
|
|
529
|
+
// Pre-create variant: the probe runs BEFORE the server app is created, so the
|
|
530
|
+
// lead makes explicit that nothing was created (no orphan / no recovery hint).
|
|
531
|
+
function buildGitHubAccessPreflightErrorMessage(repoUrl) {
|
|
532
|
+
return (`Cannot access repository '${repoUrl}', so no app was created. ` + gitHubAccessFailureGuidance());
|
|
533
|
+
}
|
|
534
|
+
function buildGenericCloneFailureMessage({ directory, repoUrl }) {
|
|
535
|
+
return `Failed to clone '${repoUrl}' into '${directory}'. See the git output above for details.`;
|
|
536
|
+
}
|
|
444
537
|
/**
|
|
445
538
|
* Clones the developer's external GitHub repo (`*.ghe.com` tenant or `github.com`) into `directory`.
|
|
446
539
|
* Auth flows through the user's existing git credential setup — the CLI does not inject
|
|
447
540
|
* tokens here. On failure, surfaces a CLI-friendly error pointing the user at the
|
|
448
|
-
* GitHub App install prerequisite
|
|
449
|
-
*
|
|
541
|
+
* GitHub App install prerequisite. Git's stderr is captured (via `runGit`, with
|
|
542
|
+
* `emitStderr: false`) so it can be run through the access-failure classification,
|
|
543
|
+
* and is re-emitted on failure so the underlying message still appears inline
|
|
544
|
+
* above the wrapper.
|
|
450
545
|
*
|
|
451
546
|
* Targeting rules (mirrors `git clone` semantics):
|
|
452
547
|
* - directory non-existent → clone creates it
|
|
@@ -456,82 +551,137 @@ export async function commitAndPushScaffoldAsync(appDir, jsonMode) {
|
|
|
456
551
|
*/
|
|
457
552
|
export function cloneRepo({ directory, repoUrl }) {
|
|
458
553
|
try {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
554
|
+
// Pipe + capture stderr (no `network`, so it is not inherited) so the catch
|
|
555
|
+
// can run it through `isGitCloneAccessFailure`; `emitStderr: false` lets the
|
|
556
|
+
// catch own the single re-emission below instead of runGit double-printing.
|
|
557
|
+
runGit(['clone', repoUrl, directory], { emitStderr: false });
|
|
462
558
|
}
|
|
463
559
|
catch (err) {
|
|
464
|
-
|
|
560
|
+
const stderr = getCapturedStderr(err);
|
|
561
|
+
if (stderr) {
|
|
562
|
+
// Re-emit git's own output live so the raw detail is still visible to the
|
|
563
|
+
// user (and in logs) without polluting the thrown error's message.
|
|
564
|
+
process.stderr.write(stderr);
|
|
565
|
+
}
|
|
566
|
+
const message = isGitCloneAccessFailure(stderr)
|
|
567
|
+
? buildGitHubCloneAccessErrorMessage({ directory, repoUrl })
|
|
568
|
+
: buildGenericCloneFailureMessage({ directory, repoUrl });
|
|
569
|
+
// Keep the user-facing message clean; preserve the raw git detail on
|
|
570
|
+
// errorData for telemetry/debug rather than embedding it in the message.
|
|
571
|
+
// Clone failures span remote access and local git credential configuration.
|
|
572
|
+
throw new ConfigurationError(message, {
|
|
573
|
+
underlyingError: getUnderlyingCloneError(err, stderr),
|
|
574
|
+
});
|
|
465
575
|
}
|
|
466
576
|
}
|
|
467
577
|
export function initLocalRepo({ directory, gitUrl, loginAuthority, authResource, jsonMode = false, }) {
|
|
468
|
-
|
|
578
|
+
const gcmConfig = buildGcmConfigOrThrow({ gitUrl, loginAuthority, authResource });
|
|
579
|
+
gitInit(directory);
|
|
580
|
+
writeGcmConfig(directory, gcmConfig);
|
|
581
|
+
addOrUpdateRemote(directory, gitUrl);
|
|
582
|
+
announceGitAuthentication(gitUrl, jsonMode);
|
|
583
|
+
fetchOrigin(directory, gitUrl);
|
|
584
|
+
checkoutMainIfPresent(directory);
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Builds the per-host GCM OAuth config, re-wrapping any URL/protocol validation
|
|
588
|
+
* failure from {@link buildGcmConfig} as a `ConfigurationError` so the verb's
|
|
589
|
+
* error handling treats an invalid clone URL as a user-actionable setup problem.
|
|
590
|
+
*/
|
|
591
|
+
function buildGcmConfigOrThrow({ gitUrl, loginAuthority, authResource, }) {
|
|
469
592
|
try {
|
|
470
|
-
|
|
593
|
+
return buildGcmConfig({ gitUrl, loginAuthority, authResource });
|
|
471
594
|
}
|
|
472
595
|
catch (err) {
|
|
473
596
|
throw new ConfigurationError(`Git credential helper setup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
474
597
|
}
|
|
598
|
+
}
|
|
599
|
+
/** Creates the target directory (idempotent) and initializes an empty git repo. */
|
|
600
|
+
function gitInit(directory) {
|
|
475
601
|
fs.mkdirSync(directory, { recursive: true });
|
|
476
|
-
|
|
477
|
-
|
|
602
|
+
runGit(['init'], { cwd: directory });
|
|
603
|
+
}
|
|
604
|
+
/** Writes GCM config before fetching so the first network call is already authenticated. */
|
|
605
|
+
function writeGcmConfig(directory, gcmConfig) {
|
|
478
606
|
for (const [key, value] of gcmConfig) {
|
|
479
|
-
|
|
480
|
-
cwd: directory,
|
|
481
|
-
stdio: ['ignore', 'ignore', 'inherit'],
|
|
482
|
-
});
|
|
607
|
+
runGit(['config', '--local', key, value], { cwd: directory });
|
|
483
608
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Adds `origin`, tolerating the already-exists case by falling back to
|
|
612
|
+
* `set-url`. `git remote add` fails if origin already exists. Capture stderr
|
|
613
|
+
* (allowFailure) so we can inspect the message and only fall back to `set-url`
|
|
614
|
+
* for that specific case — other failures (permissions, corrupt repo) must
|
|
615
|
+
* propagate rather than produce a misleading set-url error.
|
|
616
|
+
*/
|
|
617
|
+
function addOrUpdateRemote(directory, gitUrl) {
|
|
618
|
+
const addRemote = runGit(['remote', 'add', 'origin', gitUrl], {
|
|
619
|
+
cwd: directory,
|
|
620
|
+
allowFailure: true,
|
|
621
|
+
emitStderr: false,
|
|
622
|
+
});
|
|
623
|
+
if (!addRemote.success) {
|
|
624
|
+
if (!addRemote.stderr.includes('already exists')) {
|
|
625
|
+
process.stderr.write(addRemote.stderr);
|
|
626
|
+
// `runGit` always populates `error` on the failure variant, so no fallback
|
|
627
|
+
// is needed (the GitResult union guarantees it here).
|
|
628
|
+
throw addRemote.error;
|
|
629
|
+
}
|
|
630
|
+
runGit(['remote', 'set-url', 'origin', gitUrl], { cwd: directory });
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Best-effort host label for a git remote URL. Returns the URL's host, or — when
|
|
635
|
+
* the string isn't a parseable URL — the original string. Used for human-facing
|
|
636
|
+
* action labels (auth/fetch/push messages) where a malformed URL must not turn a
|
|
637
|
+
* cosmetic label into a thrown `Invalid URL` that masquerades as the real
|
|
638
|
+
* failure. The platform issues these URLs, so the fallback is effectively
|
|
639
|
+
* unreachable, but the guard keeps the label site from becoming a crash site.
|
|
640
|
+
*/
|
|
641
|
+
function gitHostLabel(gitUrl) {
|
|
487
642
|
try {
|
|
488
|
-
|
|
489
|
-
cwd: directory,
|
|
490
|
-
stdio: ['ignore', 'ignore', 'pipe'],
|
|
491
|
-
});
|
|
643
|
+
return new URL(gitUrl).host;
|
|
492
644
|
}
|
|
493
|
-
catch
|
|
494
|
-
|
|
495
|
-
if (!stderr.includes('already exists')) {
|
|
496
|
-
process.stderr.write(stderr);
|
|
497
|
-
throw err;
|
|
498
|
-
}
|
|
499
|
-
execFileSync('git', ['remote', 'set-url', 'origin', gitUrl], {
|
|
500
|
-
cwd: directory,
|
|
501
|
-
stdio: ['ignore', 'ignore', 'inherit'],
|
|
502
|
-
});
|
|
645
|
+
catch {
|
|
646
|
+
return gitUrl;
|
|
503
647
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Prints the pre-fetch authentication notice. The first `git fetch` triggers
|
|
651
|
+
* GCM's OAuth flow if the user has no cached credentials, which can pop a
|
|
652
|
+
* browser without warning. Tell the user up front so they don't see the browser
|
|
653
|
+
* as a surprise (and know which host / account to authenticate against). Skipped
|
|
654
|
+
* under `--json` so the prose doesn't end up interleaved with the JSON envelope
|
|
655
|
+
* on stdout.
|
|
656
|
+
*/
|
|
657
|
+
function announceGitAuthentication(gitUrl, jsonMode) {
|
|
509
658
|
if (!jsonMode) {
|
|
510
|
-
printInfo(`Authenticating to ${
|
|
659
|
+
printInfo(`Authenticating to ${gitHostLabel(gitUrl)} for the initial git fetch. ` +
|
|
511
660
|
'A browser window may open. Sign in with the account associated with this tenant.');
|
|
512
661
|
}
|
|
513
|
-
|
|
662
|
+
}
|
|
663
|
+
/** Fetches `origin`, classifying any auth/network failure against the remote host. */
|
|
664
|
+
function fetchOrigin(directory, gitUrl) {
|
|
665
|
+
runGit(['fetch', 'origin'], {
|
|
514
666
|
cwd: directory,
|
|
515
|
-
|
|
667
|
+
network: true,
|
|
668
|
+
classify: { action: `fetching from ${gitHostLabel(gitUrl)}` },
|
|
516
669
|
});
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
}
|
|
530
|
-
if (
|
|
531
|
-
|
|
532
|
-
cwd: directory,
|
|
533
|
-
stdio: ['ignore', 'ignore', 'inherit'],
|
|
534
|
-
});
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Checks out `main` tracking `origin/main` when that ref exists. Probe for
|
|
673
|
+
* origin/main locally (no network needed after fetch). A failed probe means the
|
|
674
|
+
* ref doesn't exist (empty GRS repo); allowFailure turns that expected case into
|
|
675
|
+
* a `success: false` signal instead of a throw.
|
|
676
|
+
*/
|
|
677
|
+
function checkoutMainIfPresent(directory) {
|
|
678
|
+
const mainProbe = runGit(['rev-parse', '--verify', 'origin/main'], {
|
|
679
|
+
cwd: directory,
|
|
680
|
+
allowFailure: true,
|
|
681
|
+
emitStderr: false,
|
|
682
|
+
});
|
|
683
|
+
if (mainProbe.success) {
|
|
684
|
+
runGit(['checkout', '-b', 'main', 'origin/main'], { cwd: directory });
|
|
535
685
|
}
|
|
536
686
|
}
|
|
537
687
|
// ─── Recovery hint for post-create failures ─────────────────────────────────
|
|
@@ -543,108 +693,21 @@ export function initLocalRepo({ directory, gitUrl, loginAuthority, authResource,
|
|
|
543
693
|
export function buildOrphanedAppRecoveryHint(serverAppId, environmentId) {
|
|
544
694
|
return (`The server app is still in environment ${environmentId} (id: ${serverAppId}).\n` +
|
|
545
695
|
`If you want to clean it up before retrying, run the following command:\n` +
|
|
546
|
-
`
|
|
696
|
+
` ${CLI_BINARY_NAME} app delete --app ${serverAppId} --environment-id ${environmentId} --force`);
|
|
547
697
|
}
|
|
548
698
|
// ─── Preflight helpers ──────────────────────────────────────────────────────
|
|
549
699
|
// All checks below run before any server call so a local misconfiguration
|
|
550
|
-
// (
|
|
551
|
-
//
|
|
700
|
+
// (bad template source, unwritable directory, …) fails fast and never leaves
|
|
701
|
+
// an orphaned MAAF app + GRS repo on the server. The git presence + version
|
|
702
|
+
// check lives in `Utils/GitVersionChecker.ts`.
|
|
552
703
|
// ────────────────────────────────────────────────────────────────────────────
|
|
553
|
-
export function ensureGitInstalled() {
|
|
554
|
-
try {
|
|
555
|
-
execFileSync('git', ['--version'], { stdio: ['ignore', 'ignore', 'ignore'] });
|
|
556
|
-
}
|
|
557
|
-
catch {
|
|
558
|
-
throw new ConfigurationError("'git' is required but was not found on PATH. Install git from https://git-scm.com/ and retry.");
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
// Known giget provider prefixes. Used only for a *soft* "did you mean?" warning —
|
|
562
|
-
// not a hard reject, so adding a new giget provider doesn't silently fail
|
|
563
|
-
// preflight here. Sourced from giget's docs (github, gitlab, bitbucket, sourcehut,
|
|
564
|
-
// http, https) as of the version pinned in package.json. Update opportunistically
|
|
565
|
-
// when bumping giget; a stale entry only suppresses one warning, never blocks
|
|
566
|
-
// a valid template.
|
|
567
|
-
const KNOWN_GIGET_PROVIDERS = new Set([
|
|
568
|
-
'github',
|
|
569
|
-
'gitlab',
|
|
570
|
-
'bitbucket',
|
|
571
|
-
'sourcehut',
|
|
572
|
-
'http',
|
|
573
|
-
'https',
|
|
574
|
-
]);
|
|
575
|
-
// Single-edit Damerau-Levenshtein distance, capped at 1: returns true iff `a`
|
|
576
|
-
// is one substitution, one insertion, one deletion, or one adjacent
|
|
577
|
-
// transposition away from `b`. Used to pick the closest known provider for a
|
|
578
|
-
// "did you mean?" hint without pulling in a dependency.
|
|
579
|
-
function isOneEditAway(a, b) {
|
|
580
|
-
if (a === b)
|
|
581
|
-
return false;
|
|
582
|
-
if (Math.abs(a.length - b.length) > 1)
|
|
583
|
-
return false;
|
|
584
|
-
if (a.length === b.length) {
|
|
585
|
-
let diff = 0;
|
|
586
|
-
let transposition = null;
|
|
587
|
-
for (let i = 0; i < a.length; i++) {
|
|
588
|
-
if (a[i] !== b[i]) {
|
|
589
|
-
diff++;
|
|
590
|
-
if (transposition === null)
|
|
591
|
-
transposition = i;
|
|
592
|
-
if (diff > 2)
|
|
593
|
-
return false;
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
if (diff <= 1)
|
|
597
|
-
return true;
|
|
598
|
-
if (diff === 2 && transposition !== null) {
|
|
599
|
-
// Adjacent swap: gihub vs github, swap positions transposition and transposition+1.
|
|
600
|
-
return a[transposition] === b[transposition + 1] && a[transposition + 1] === b[transposition];
|
|
601
|
-
}
|
|
602
|
-
return false;
|
|
603
|
-
}
|
|
604
|
-
// One insertion or deletion away. Walk both strings; allow one skip.
|
|
605
|
-
const [shorter, longer] = a.length < b.length ? [a, b] : [b, a];
|
|
606
|
-
let i = 0;
|
|
607
|
-
let j = 0;
|
|
608
|
-
let skipped = false;
|
|
609
|
-
while (i < shorter.length && j < longer.length) {
|
|
610
|
-
if (shorter[i] === longer[j]) {
|
|
611
|
-
i++;
|
|
612
|
-
j++;
|
|
613
|
-
continue;
|
|
614
|
-
}
|
|
615
|
-
if (skipped)
|
|
616
|
-
return false;
|
|
617
|
-
skipped = true;
|
|
618
|
-
j++;
|
|
619
|
-
}
|
|
620
|
-
return true;
|
|
621
|
-
}
|
|
622
704
|
export function ensureTemplateSourceValid(source) {
|
|
623
|
-
//
|
|
624
|
-
//
|
|
625
|
-
//
|
|
626
|
-
//
|
|
627
|
-
//
|
|
628
|
-
|
|
629
|
-
throw new UsageError('Template source is empty. Use --template to specify one.');
|
|
630
|
-
}
|
|
631
|
-
// Soft validation: if the source looks like `<prefix>:<rest>` but the prefix
|
|
632
|
-
// is one edit away from a known giget provider (e.g. `gihub:` → `github:`),
|
|
633
|
-
// warn but proceed. giget will produce the authoritative error if the
|
|
634
|
-
// prefix is genuinely invalid; for the common typo case we surface the fix
|
|
635
|
-
// before the network round-trip. No hard-fail keeps this forward-compatible
|
|
636
|
-
// with new giget providers — a stale `KNOWN_GIGET_PROVIDERS` set only
|
|
637
|
-
// suppresses a warning, it never blocks a valid template.
|
|
638
|
-
const prefixMatch = /^([a-z][a-z0-9+.-]*):/i.exec(source);
|
|
639
|
-
if (!prefixMatch)
|
|
640
|
-
return;
|
|
641
|
-
const prefix = prefixMatch[1].toLowerCase();
|
|
642
|
-
if (KNOWN_GIGET_PROVIDERS.has(prefix))
|
|
643
|
-
return;
|
|
644
|
-
const suggestion = [...KNOWN_GIGET_PROVIDERS].find((known) => isOneEditAway(prefix, known));
|
|
645
|
-
if (suggestion) {
|
|
646
|
-
printWarning(`Template source '${source}' uses an unrecognized provider prefix '${prefix}:'. Did you mean '${suggestion}:'?`);
|
|
647
|
-
}
|
|
705
|
+
// Delegates to the github-only parser in DownloadTemplate. The parser throws
|
|
706
|
+
// `UsageError` for any unsupported shape (non-`github:` prefix, malformed
|
|
707
|
+
// URL, missing owner/repo) — which is what preflight wants: surface the
|
|
708
|
+
// problem before any server call. We discard the parsed result; the real
|
|
709
|
+
// download path re-parses at call time.
|
|
710
|
+
parseGithubTemplate(source);
|
|
648
711
|
}
|
|
649
712
|
export function ensureTargetDirectoryEmpty(appDir) {
|
|
650
713
|
// Scaffolding into a non-empty directory would mix the template with the
|
|
@@ -670,7 +733,7 @@ export function ensureRepoTargetDirectoryEmpty(appDir) {
|
|
|
670
733
|
return;
|
|
671
734
|
const entries = fs.readdirSync(appDir);
|
|
672
735
|
if (entries.length > 0) {
|
|
673
|
-
throw new UsageError(`'${appDir}' is not empty. --repo requires an empty target directory; specify an empty directory as the first argument
|
|
736
|
+
throw new UsageError(`'${appDir}' is not empty. --repo requires an empty target directory; specify an empty directory as the first argument, move to one before running, or run 'ms app init' to bind an existing project to a new app.`);
|
|
674
737
|
}
|
|
675
738
|
}
|
|
676
739
|
/**
|
|
@@ -682,14 +745,18 @@ export function ensureRepoTargetDirectoryEmpty(appDir) {
|
|
|
682
745
|
* bandwidth on the rejection path is tens of KB even for a multi-GB repo. The
|
|
683
746
|
* temp dir is removed in `finally`.
|
|
684
747
|
*
|
|
685
|
-
* Failure semantics
|
|
748
|
+
* Failure semantics:
|
|
686
749
|
* - Non-empty tree at HEAD → throws `UsageError` with the "is not empty" /
|
|
687
750
|
* "ms app init" actionable lead, BEFORE createAsync. No orphan, no leak.
|
|
688
751
|
* - Empty tree (incl. the cleaned-up-repo edge case: commit1 add, commit2
|
|
689
752
|
* delete all) → ls-tree returns no lines → returns without throwing.
|
|
690
|
-
* -
|
|
691
|
-
*
|
|
692
|
-
*
|
|
753
|
+
* - Clone fails with a recognizable access/auth signature (private repo
|
|
754
|
+
* hidden as "not found", missing local credentials, 403, …) → throws
|
|
755
|
+
* `ConfigurationError` BEFORE createAsync so we never create a server-side
|
|
756
|
+
* app the user can't clone (fail FAST, no orphan).
|
|
757
|
+
* - Any other git error (network blip, server doesn't support --filter,
|
|
758
|
+
* temp-fs hiccup, …) → swallow and return (fail OPEN). The subsequent
|
|
759
|
+
* cloneRepo surfaces the friendlier message for the user-facing failure;
|
|
693
760
|
* we don't want this probe to become a parallel error-message surface.
|
|
694
761
|
*/
|
|
695
762
|
export function ensureExternalRepoIsEmpty(repoUrl) {
|
|
@@ -703,27 +770,38 @@ export function ensureExternalRepoIsEmpty(repoUrl) {
|
|
|
703
770
|
return;
|
|
704
771
|
}
|
|
705
772
|
try {
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
//
|
|
711
|
-
//
|
|
712
|
-
//
|
|
773
|
+
const probeClone = runGit(['clone', '--depth=1', '--filter=blob:none', '--no-checkout', repoUrl, probeDir], { allowFailure: true, emitStderr: false });
|
|
774
|
+
if (!probeClone.success) {
|
|
775
|
+
// Clone failed. When git reports a recognizable access/auth failure
|
|
776
|
+
// (a private repo hidden as "not found", missing local credentials, a
|
|
777
|
+
// 403, etc.) fail FAST here — before createAsync — so we never create a
|
|
778
|
+
// server-side app the user can't clone (no orphan). For ambiguous or
|
|
779
|
+
// transient errors (server doesn't support --filter, network blip, a
|
|
780
|
+
// temp-fs hiccup, …) stay fail-OPEN and let the later cloneRepo surface
|
|
781
|
+
// the friendlier, fuller message.
|
|
782
|
+
const stderr = probeClone.stderr;
|
|
783
|
+
if (isGitCloneAccessFailure(stderr)) {
|
|
784
|
+
// Clean user-facing message; raw git detail kept on errorData for
|
|
785
|
+
// telemetry/debug. The probe clone is silent (stderr piped, not
|
|
786
|
+
// inherited), so we don't echo the confusing internal probe command.
|
|
787
|
+
throw new ConfigurationError(buildGitHubAccessPreflightErrorMessage(repoUrl), {
|
|
788
|
+
underlyingError: getUnderlyingCloneError(probeClone.error, stderr),
|
|
789
|
+
});
|
|
790
|
+
}
|
|
713
791
|
return;
|
|
714
792
|
}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
catch {
|
|
793
|
+
const lsTree = runGit(['-C', probeDir, 'ls-tree', '-r', 'HEAD'], {
|
|
794
|
+
captureStdout: true,
|
|
795
|
+
allowFailure: true,
|
|
796
|
+
emitStderr: false,
|
|
797
|
+
});
|
|
798
|
+
if (!lsTree.success) {
|
|
722
799
|
// Truly empty repo (no commits, no HEAD): ls-tree errors. Treat as empty
|
|
723
800
|
// so create proceeds; cloneRepo of a no-commits remote produces an empty
|
|
724
801
|
// working tree, which is the happy path.
|
|
725
802
|
return;
|
|
726
803
|
}
|
|
804
|
+
const lsTreeOutput = lsTree.stdout;
|
|
727
805
|
if (lsTreeOutput.trim().length > 0) {
|
|
728
806
|
// `ls-tree -r HEAD` enumerates every tracked path — including hidden
|
|
729
807
|
// ones (.gitignore, .gitattributes, etc.). Any tracked file at all
|