@microsoft/managed-apps-cli 0.7.0 → 0.9.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.
Files changed (152) hide show
  1. package/README.md +42 -6
  2. package/dist/ArgumentProvider.d.ts +11 -0
  3. package/dist/ArgumentProvider.d.ts.map +1 -1
  4. package/dist/ArgumentProvider.js +61 -19
  5. package/dist/ArgumentProvider.js.map +1 -1
  6. package/dist/Authentication/NodeMsalAuthenticationProvider.d.ts +16 -0
  7. package/dist/Authentication/NodeMsalAuthenticationProvider.d.ts.map +1 -1
  8. package/dist/Authentication/NodeMsalAuthenticationProvider.js +58 -6
  9. package/dist/Authentication/NodeMsalAuthenticationProvider.js.map +1 -1
  10. package/dist/Cli.d.ts.map +1 -1
  11. package/dist/Cli.js +23 -7
  12. package/dist/Cli.js.map +1 -1
  13. package/dist/CliSettings.d.ts +2 -2
  14. package/dist/CliSettings.d.ts.map +1 -1
  15. package/dist/CliSettings.js +3 -4
  16. package/dist/CliSettings.js.map +1 -1
  17. package/dist/CliUtils.d.ts +25 -8
  18. package/dist/CliUtils.d.ts.map +1 -1
  19. package/dist/CliUtils.js +19 -23
  20. package/dist/CliUtils.js.map +1 -1
  21. package/dist/Constants/CommandRegistry.d.ts.map +1 -1
  22. package/dist/Constants/CommandRegistry.js +28 -1
  23. package/dist/Constants/CommandRegistry.js.map +1 -1
  24. package/dist/Constants/HelpStrings.d.ts +55 -5
  25. package/dist/Constants/HelpStrings.d.ts.map +1 -1
  26. package/dist/Constants/HelpStrings.js +113 -42
  27. package/dist/Constants/HelpStrings.js.map +1 -1
  28. package/dist/Constants.d.ts +1 -1
  29. package/dist/Constants.d.ts.map +1 -1
  30. package/dist/Constants.js +5 -2
  31. package/dist/Constants.js.map +1 -1
  32. package/dist/Errors/GitHubErrorMap.d.ts.map +1 -1
  33. package/dist/Errors/GitHubErrorMap.js +18 -11
  34. package/dist/Errors/GitHubErrorMap.js.map +1 -1
  35. package/dist/FS/CliFs.d.ts.map +1 -1
  36. package/dist/FS/CliFs.js.map +1 -1
  37. package/dist/HttpClient/CliHttpClient.d.ts +30 -0
  38. package/dist/HttpClient/CliHttpClient.d.ts.map +1 -1
  39. package/dist/HttpClient/CliHttpClient.js +136 -1
  40. package/dist/HttpClient/CliHttpClient.js.map +1 -1
  41. package/dist/Logger/CliLogger.d.ts +50 -8
  42. package/dist/Logger/CliLogger.d.ts.map +1 -1
  43. package/dist/Logger/CliLogger.js +92 -37
  44. package/dist/Logger/CliLogger.js.map +1 -1
  45. package/dist/Logger/ConsoleWriter.d.ts +5 -0
  46. package/dist/Logger/ConsoleWriter.d.ts.map +1 -1
  47. package/dist/Logger/ConsoleWriter.js +22 -14
  48. package/dist/Logger/ConsoleWriter.js.map +1 -1
  49. package/dist/Middletier/FunctionsBundler.js +5 -1
  50. package/dist/Middletier/FunctionsBundler.js.map +1 -1
  51. package/dist/Middletier/PackFunctions.d.ts +5 -4
  52. package/dist/Middletier/PackFunctions.d.ts.map +1 -1
  53. package/dist/Middletier/PackFunctions.js +8 -7
  54. package/dist/Middletier/PackFunctions.js.map +1 -1
  55. package/dist/Types/Cli.types.d.ts +1 -1
  56. package/dist/Types/Cli.types.d.ts.map +1 -1
  57. package/dist/Types/Command.types.d.ts +14 -3
  58. package/dist/Types/Command.types.d.ts.map +1 -1
  59. package/dist/Types/Command.types.js +4 -0
  60. package/dist/Types/Command.types.js.map +1 -1
  61. package/dist/Utils/BuildPolling.d.ts +18 -0
  62. package/dist/Utils/BuildPolling.d.ts.map +1 -0
  63. package/dist/Utils/BuildPolling.js +63 -0
  64. package/dist/Utils/BuildPolling.js.map +1 -0
  65. package/dist/Utils/BuildVerbDescriptionTable.d.ts +11 -1
  66. package/dist/Utils/BuildVerbDescriptionTable.d.ts.map +1 -1
  67. package/dist/Utils/BuildVerbDescriptionTable.js +73 -17
  68. package/dist/Utils/BuildVerbDescriptionTable.js.map +1 -1
  69. package/dist/Utils/DcfRecovery.d.ts +84 -0
  70. package/dist/Utils/DcfRecovery.d.ts.map +1 -0
  71. package/dist/Utils/DcfRecovery.js +266 -0
  72. package/dist/Utils/DcfRecovery.js.map +1 -0
  73. package/dist/Utils/EnhanceNetworkError.d.ts.map +1 -1
  74. package/dist/Utils/EnhanceNetworkError.js +2 -0
  75. package/dist/Utils/EnhanceNetworkError.js.map +1 -1
  76. package/dist/Utils/EnvVars.d.ts +1 -1
  77. package/dist/Utils/EnvVars.d.ts.map +1 -1
  78. package/dist/Utils/EnvVars.js.map +1 -1
  79. package/dist/Utils/RepoUrl.d.ts +11 -6
  80. package/dist/Utils/RepoUrl.d.ts.map +1 -1
  81. package/dist/Utils/RepoUrl.js +1 -1
  82. package/dist/Utils/RepoUrl.js.map +1 -1
  83. package/dist/Utils/ResolveCommand.d.ts +37 -1
  84. package/dist/Utils/ResolveCommand.d.ts.map +1 -1
  85. package/dist/Utils/ResolveCommand.js +97 -20
  86. package/dist/Utils/ResolveCommand.js.map +1 -1
  87. package/dist/Verbs/AddConnector.d.ts.map +1 -1
  88. package/dist/Verbs/AddConnector.js.map +1 -1
  89. package/dist/Verbs/AddConnectorFlows.d.ts.map +1 -1
  90. package/dist/Verbs/AddConnectorFlows.js +2 -2
  91. package/dist/Verbs/AddConnectorFlows.js.map +1 -1
  92. package/dist/Verbs/BuildMaafApp.d.ts.map +1 -1
  93. package/dist/Verbs/BuildMaafApp.js +98 -22
  94. package/dist/Verbs/BuildMaafApp.js.map +1 -1
  95. package/dist/Verbs/ConnectorHelpers.d.ts.map +1 -1
  96. package/dist/Verbs/ConnectorHelpers.js.map +1 -1
  97. package/dist/Verbs/CreateMaafApp.d.ts +54 -5
  98. package/dist/Verbs/CreateMaafApp.d.ts.map +1 -1
  99. package/dist/Verbs/CreateMaafApp.js +238 -238
  100. package/dist/Verbs/CreateMaafApp.js.map +1 -1
  101. package/dist/Verbs/DeployMaafApp.d.ts.map +1 -1
  102. package/dist/Verbs/DeployMaafApp.js +48 -20
  103. package/dist/Verbs/DeployMaafApp.js.map +1 -1
  104. package/dist/Verbs/GetMaafBuildStatus.d.ts.map +1 -1
  105. package/dist/Verbs/GetMaafBuildStatus.js +66 -17
  106. package/dist/Verbs/GetMaafBuildStatus.js.map +1 -1
  107. package/dist/Verbs/GetSettings.d.ts.map +1 -1
  108. package/dist/Verbs/GetSettings.js +9 -18
  109. package/dist/Verbs/GetSettings.js.map +1 -1
  110. package/dist/Verbs/Init.d.ts.map +1 -1
  111. package/dist/Verbs/Init.js +63 -46
  112. package/dist/Verbs/Init.js.map +1 -1
  113. package/dist/Verbs/ListFlows.d.ts.map +1 -1
  114. package/dist/Verbs/ListFlows.js +1 -2
  115. package/dist/Verbs/ListFlows.js.map +1 -1
  116. package/dist/Verbs/MaafAppPermissions.d.ts +12 -6
  117. package/dist/Verbs/MaafAppPermissions.d.ts.map +1 -1
  118. package/dist/Verbs/MaafAppPermissions.js +208 -66
  119. package/dist/Verbs/MaafAppPermissions.js.map +1 -1
  120. package/dist/Verbs/PlayMaafApp.d.ts +22 -0
  121. package/dist/Verbs/PlayMaafApp.d.ts.map +1 -0
  122. package/dist/Verbs/PlayMaafApp.js +147 -0
  123. package/dist/Verbs/PlayMaafApp.js.map +1 -0
  124. package/dist/Verbs/RefreshDataSource.d.ts.map +1 -1
  125. package/dist/Verbs/RefreshDataSource.js.map +1 -1
  126. package/dist/Verbs/RefreshGitAuth.d.ts.map +1 -1
  127. package/dist/Verbs/RefreshGitAuth.js +31 -16
  128. package/dist/Verbs/RefreshGitAuth.js.map +1 -1
  129. package/dist/Verbs/SetSetting.d.ts.map +1 -1
  130. package/dist/Verbs/SetSetting.js +39 -49
  131. package/dist/Verbs/SetSetting.js.map +1 -1
  132. package/dist/Verbs/ShareLinkCreate.d.ts +23 -0
  133. package/dist/Verbs/ShareLinkCreate.d.ts.map +1 -0
  134. package/dist/Verbs/ShareLinkCreate.js +73 -0
  135. package/dist/Verbs/ShareLinkCreate.js.map +1 -0
  136. package/dist/Verbs/ShareLinkList.d.ts +16 -0
  137. package/dist/Verbs/ShareLinkList.d.ts.map +1 -0
  138. package/dist/Verbs/ShareLinkList.js +84 -0
  139. package/dist/Verbs/ShareLinkList.js.map +1 -0
  140. package/dist/Verbs/ShareLinkRevoke.d.ts +22 -0
  141. package/dist/Verbs/ShareLinkRevoke.d.ts.map +1 -0
  142. package/dist/Verbs/ShareLinkRevoke.js +137 -0
  143. package/dist/Verbs/ShareLinkRevoke.js.map +1 -0
  144. package/dist/Verbs/VerbConstants.d.ts +113 -16
  145. package/dist/Verbs/VerbConstants.d.ts.map +1 -1
  146. package/dist/Verbs/VerbConstants.js +102 -24
  147. package/dist/Verbs/VerbConstants.js.map +1 -1
  148. package/dist/Verbs/index.d.ts +4 -0
  149. package/dist/Verbs/index.d.ts.map +1 -1
  150. package/dist/Verbs/index.js +4 -0
  151. package/dist/Verbs/index.js.map +1 -1
  152. package/package.json +5 -5
@@ -1,25 +1,24 @@
1
1
  /*!
2
2
  * Copyright (C) Microsoft Corporation. All rights reserved.
3
3
  */
4
- import { authenticateForRepoAsync, createAsync, getPlayerServiceConfig, GrsServerError, updateEnvironmentName, writeRepoConfig, } from '@microsoft/managed-apps-actions';
4
+ import { createAsync, getPlayerServiceConfig, updateEnvironmentName, writeRepoConfig, } from '@microsoft/managed-apps-actions';
5
5
  import { AppConfigFileName } from '@microsoft/managed-apps-common/config';
6
- import { getUserRoutingEnvironmentAsync } from '@microsoft/managed-apps-common/services';
6
+ import { getUserRoutingEnvironmentAsync, regionToCloudSafe, } from '@microsoft/managed-apps-common/services';
7
7
  import { execFileSync } from 'child_process';
8
8
  import * as fs from 'fs';
9
9
  import { downloadTemplate } from 'giget';
10
+ import * as os from 'os';
10
11
  import * as path from 'path';
11
- import { getCliLogger } from '../CliSettings.js';
12
- import { createDefaultStringOption } from '../CliUtils.js';
12
+ import { getCliLogger, getSettings } from '../CliSettings.js';
13
+ import { createDefaultStringOption, resolveEnvironmentId } from '../CliUtils.js';
13
14
  import { CLI_CLIENT_ID } from '../Constants.js';
14
15
  import { CLI_BINARY_NAME } from '../Constants/CliConstants.js';
15
16
  import { HelpStrings } from '../Constants/HelpStrings.js';
16
17
  import { AppError, ConfigurationError, UsageError } from '../Errors/CliError.js';
17
- import { mapGitHubServerErrorToCliError, matchGitHubAuthError } from '../Errors/GitHubErrorMap.js';
18
18
  import { printInfo, printJsonLine, printJsonSuccess, printSuccess, printWarning, } from '../Utils/ConsoleOutput.js';
19
+ import { withGitHubDcfRecoveryAsync } from '../Utils/DcfRecovery.js';
19
20
  import { resolveRoutingEnvironmentIdAsync } from '../Utils/EnvironmentRoutingUtils.js';
20
21
  import { buildGcmConfig } from '../Utils/GitCredentialConfig.js';
21
- import { ConsoleGitHubDcfDisplay, JsonGitHubDcfDisplay } from '../Utils/GitHubDcfFlow.js';
22
- import { computeRepoHash, parseGitHubRepoUrl } from '../Utils/RepoUrl.js';
23
22
  import { CreateMaafAppArguments } from './VerbConstants.js';
24
23
  const DEFAULT_TEMPLATE = 'github:microsoft/Managed-Apps/templates/vite8';
25
24
  /**
@@ -34,8 +33,8 @@ const REPO_NONE_SENTINEL = 'none';
34
33
  * Maps the raw `--repo` flag value to a `(repoType, remoteUrl)` pair.
35
34
  * - omitted / empty → `native` (default GRS-backed flow)
36
35
  * - `none` (case-insensitive) → `none` (no GRS, no local setup)
37
- * - any other non-empty value → `github` (external GHE binding, value flows through as
38
- * `remoteUrl` to the action and the eventual git clone)
36
+ * - any other non-empty value → `github` (external GitHub binding `*.ghe.com` or `github.com`;
37
+ * value flows through as `remoteUrl` to the action and the eventual git clone)
39
38
  *
40
39
  * Runtime invariant: `remoteUrl` is set iff `repoType === 'github'`. Consumers
41
40
  * branch on `repoType` and assert `remoteUrl!` at github use sites — see
@@ -55,7 +54,7 @@ export function parseRepoOption(rawRepoValue) {
55
54
  *
56
55
  * Three repo flows, selected by `--repo`:
57
56
  * - `native` (default): server auto-provisions a GRS repo; local clone happens via GCM.
58
- * - `github`: external GHE Cloud binding. Clones the user's repo locally. On the first
57
+ * - `github`: external GitHub binding (`*.ghe.com` or `github.com`). Clones the user's repo locally. On the first
59
58
  * `createAsync` call, a missing/expired server-side GitHub identity mapping is
60
59
  * recoverable in-band: a single inline DCF run + exactly one `createAsync` retry.
61
60
  * Any other error — or a second mapping failure after DCF — is a hard fail.
@@ -96,15 +95,25 @@ export async function createMaafAppAsync(argumentProvider) {
96
95
  // stdout free of prose ahead of the envelope. `printError` writes to
97
96
  // stderr and is unaffected.
98
97
  const jsonMode = argumentProvider.isJsonMode();
99
- // Identity confirmation: print the tenant + region we're targeting before
98
+ // Identity confirmation: print the tenant + cloud we're targeting before
100
99
  // anything else — preflight, routing, createAsync — so a user with multiple
101
100
  // accounts signed in has the longest possible window to CTRL-C before any
102
101
  // local check runs or server call fires. Environment id isn't known yet
103
102
  // (routing is what resolves it); included in the success line at the end.
104
103
  // Wording is intentionally tentative ("About to create…") so a preflight
105
104
  // failure on the next line doesn't read as contradicting a "Creating…" line.
105
+ //
106
+ // Surface `cloud` (user-facing, e.g. `public` / `usgov`) instead of `region`
107
+ // (internal, e.g. `prod` / `gccmoderate`) so the value matches what the user
108
+ // typed on `--cloud`. Regions without a Cloud equivalent (`preview`, `ex`,
109
+ // `rx`, or developer modes `local` / `dev`) fall back to the raw region —
110
+ // these don't reach this print via the current `cloudToRegion(this._cloud)`
111
+ // chain in ArgumentProvider, so the fallback is defensive against future
112
+ // code paths (e.g. routing-resolved regions).
106
113
  if (!jsonMode) {
107
- printInfo(`About to create app '${displayName}' in tenant ${tenantId} (region ${region}).`);
114
+ const cloud = regionToCloudSafe(region);
115
+ const target = cloud ? `cloud ${cloud}` : `region ${region}`;
116
+ printInfo(`About to create app '${displayName}' in tenant ${tenantId} (${target}).`);
108
117
  }
109
118
  // ─── Preflight ────────────────────────────────────────────────────────────
110
119
  // Run all local-only checks before any server call. Once createAsync
@@ -142,23 +151,31 @@ export async function createMaafAppAsync(argumentProvider) {
142
151
  // RepoSelection is a flat interface (not a discriminated union), so `remoteUrl`
143
152
  // doesn't auto-narrow to `string` here even though the github branch guarantees it.
144
153
  ensureRepoTargetDirectoryEmpty(appDir);
145
- // Validate --repo as a *.ghe.com URL before any network call. Throws
146
- // UsageError ("not a valid GitHub Enterprise Cloud URL") for
147
- // `github.com/...` and other non-GHE hosts, malformed URIs, etc. — mirrors
148
- // `git auth refresh`'s up-front check.
149
- parseGitHubRepoUrl(remoteUrl);
154
+ // Pre-network: cheap partial clone into a temp dir + ls-tree HEAD to detect
155
+ // a non-empty remote before createAsync runs. Catches the rejection case
156
+ // without leaving an orphaned server-side app or scaffolded files in
157
+ // appDir. Cheapest probe shape: --depth=1 --filter=blob:none --no-checkout
158
+ // pulls refs + the tip commit + its tree(s) only (no blob bytes, no working
159
+ // tree). On any probe error (network, auth, server doesn't support partial
160
+ // clone, etc.) we swallow and let the subsequent cloneRepo emit its
161
+ // friendlier error — see ensureExternalRepoIsEmpty for the rationale.
162
+ ensureExternalRepoIsEmpty(remoteUrl);
150
163
  }
151
164
  else {
152
165
  ensureTargetDirectoryEmpty(appDir);
153
166
  }
154
167
  // ─── End preflight ────────────────────────────────────────────────────────
155
168
  // The debug env-var override (MS_CLI_MAAF_DEBUG_ENVIRONMENT_ID) is honored by ArgumentProvider
156
- // and surfaced via getEnvironmentId(). When set, skip the routing API call entirely.
157
- // For the supported user flow, getEnvironmentId() returns undefined here and routing fires.
158
- const preResolvedEnvironmentId = argumentProvider.getEnvironmentId();
169
+ // and surfaced via getEnvironmentId(). The shared `resolveEnvironmentId` helper checks the
170
+ // `--environment-id` flag first, then falls back to that. When set, skip the automatic
171
+ // environment-selection API call entirely. Without an explicit id, getEnvironmentId() returns
172
+ // undefined and the automatic environment lookup runs.
173
+ const preResolvedEnvironmentId = await resolveEnvironmentId(argumentProvider, {
174
+ failIfMissing: false,
175
+ });
159
176
  // Fire off PDE routing in parallel with the remaining option prompts. The result is awaited
160
177
  // immediately before createAsync, so prompt latency is hidden behind the network call.
161
- // Skipped entirely when the debug override pre-resolved the env id.
178
+ // Skipped entirely when the env id is pre-resolved (--environment-id flag or debug override).
162
179
  const routingEnvPromise = preResolvedEnvironmentId
163
180
  ? undefined
164
181
  : getUserRoutingEnvironmentAsync(httpClient, tenantId, CLI_CLIENT_ID, { provisionDataverse: false }, region, logger);
@@ -195,11 +212,14 @@ export async function createMaafAppAsync(argumentProvider) {
195
212
  else {
196
213
  ensureTargetDirectoryEmpty(appDir);
197
214
  }
198
- // Read --no-browser alongside --json so the recovery DCF helper has both
215
+ // Read --open-browser alongside --json so the recovery DCF helper has both
199
216
  // inputs ready without a second async option read inside the catch block.
200
217
  // Commander converts kebab-case flag names to camelCase property keys;
201
- // the camelName constant keeps this lookup in sync with NoBrowser.name.
202
- const noBrowser = (await argumentProvider.getOption(CreateMaafAppArguments.NoBrowser.camelName,
218
+ // the camelName constant keeps this lookup in sync with OpenBrowser.name.
219
+ const openBrowser = (await argumentProvider.getOption(CreateMaafAppArguments.OpenBrowser.camelName,
220
+ /* ignorePrompt */ true)) ?? false;
221
+ // Forces DCF re-authentication even when a valid mapping exists.
222
+ const forceReauth = (await argumentProvider.getOption(CreateMaafAppArguments.ForceReauth.camelName,
203
223
  /* ignorePrompt */ true)) ?? false;
204
224
  // JSON mode (github flow only): emit a structured `create-validation` event
205
225
  // right before the server call so automation parsing NDJSON sees the
@@ -210,95 +230,44 @@ export async function createMaafAppAsync(argumentProvider) {
210
230
  if (jsonMode && repoType === 'github') {
211
231
  printJsonLine({ phase: 'create-validation' });
212
232
  }
213
- // Hoisted so the first attempt and the post-DCF retry share the exact same
214
- // action payload no risk of the two calls drifting apart and no per-call
215
- // object rebuild.
216
- const actionsParams = {
233
+ // Hoisted so the fixed-shape params are defined once and reused across the
234
+ // first attempt and the post-DCF retry. `forceReauth` is injected per-call
235
+ // via `isRetry` so the retry does not re-trigger the server-side mapping
236
+ // bypass against the just-refreshed mapping.
237
+ const baseActionsParams = {
217
238
  displayName,
218
239
  description,
219
240
  repositoryId,
220
241
  remoteUrl,
221
242
  repoType,
243
+ origin: getSettings().origin,
222
244
  };
223
- const runCreate = () => createAsync({
245
+ const runCreate = ({ isRetry, }) => createAsync({
224
246
  tenantId,
225
247
  environmentId,
226
248
  logger,
227
- actionsParams,
249
+ actionsParams: {
250
+ ...baseActionsParams,
251
+ ...(forceReauth && !isRetry && { forceReauth: true }),
252
+ },
253
+ });
254
+ // On `repoType: 'github'`, wrap createAsync with the DCF catch-and-retry:
255
+ // a missing/expired GitHub identity mapping is recovered in-band by running
256
+ // DCF and retrying once; any other recognized server error is mapped to a
257
+ // friendly CLI error. Non-github flows pass through untouched. See
258
+ // `withGitHubDcfRecoveryAsync` for the full contract.
259
+ const createResult = await withGitHubDcfRecoveryAsync(runCreate, {
260
+ repoType,
261
+ externalRepoUrl: remoteUrl,
262
+ tenantId,
263
+ jsonMode,
264
+ openBrowser,
265
+ verb: 'app create',
228
266
  });
229
- // Inline catch-and-retry for recoverable GitHub auth errors. On the github
230
- // flow, a missing/expired GitHub identity mapping is recoverable in-band by
231
- // running the DCF wrapper inline against
232
- // the same GRS environment, then retrying createAsync exactly once. Any
233
- // other recognized GrsServerError — on either the first attempt or the
234
- // post-DCF retry — is mapped via mapGitHubServerErrorToCliError before
235
- // being rethrown so the user sees a friendly CLI-typed error rather than
236
- // the raw server payload. Any second mapping-missing/expired after DCF —
237
- // and anything not recognized at all — escapes as a hard failure (single-retry
238
- // contract; we never loop).
239
- let createResult;
240
- try {
241
- createResult = await runCreate();
242
- }
243
- catch (err) {
244
- if (repoType !== 'github')
245
- throw err;
246
- const matched = matchGitHubAuthError(err);
247
- if (!matched) {
248
- if (err instanceof GrsServerError) {
249
- const mapped = mapGitHubServerErrorToCliError({
250
- code: err.code,
251
- message: err.message,
252
- status: err.status,
253
- details: err.details,
254
- }, { repoUrl: remoteUrl, verb: 'app create' });
255
- if (mapped)
256
- throw mapped;
257
- }
258
- throw err;
259
- }
260
- await runDcfRecoveryAsync({
261
- remoteUrl: remoteUrl,
262
- tenantId,
263
- jsonMode,
264
- noBrowser,
265
- verb: 'app create',
266
- });
267
- try {
268
- createResult = await runCreate();
269
- }
270
- catch (retryErr) {
271
- const remapped = matchGitHubAuthError(retryErr);
272
- if (remapped) {
273
- // Single-retry contract: a second mapping-missing/expired surfaces
274
- // as a hard AuthenticationError with the standard remediation
275
- // pointer. We deliberately do NOT loop DCF — repeated failures
276
- // suggest the user can't complete the device-code flow (denied,
277
- // wrong account, app uninstalled, etc.) and another loop would
278
- // just hang.
279
- const mapped = mapGitHubServerErrorToCliError(remapped.payload, {
280
- repoUrl: remoteUrl,
281
- verb: 'app create',
282
- });
283
- throw mapped ?? retryErr;
284
- }
285
- if (retryErr instanceof GrsServerError) {
286
- const mapped = mapGitHubServerErrorToCliError({
287
- code: retryErr.code,
288
- message: retryErr.message,
289
- status: retryErr.status,
290
- details: retryErr.details,
291
- }, { repoUrl: remoteUrl, verb: 'app create' });
292
- if (mapped)
293
- throw mapped;
294
- }
295
- throw retryErr;
296
- }
297
- }
298
267
  const { app, cloneUrl } = createResult;
299
268
  // Only the `native` flow expects a GRS clone URL — it's the origin for the first
300
269
  // local push to the auto-provisioned platform-managed repo. `github` binds an
301
- // external GHE repo (no GRS) and `none` is by-design source-control-free, so neither
270
+ // external GitHub repo (no GRS) and `none` is by-design source-control-free, so neither
302
271
  // produces a `cloneUrl`. Missing on `native` is a server bug — the hoisted check
303
272
  // below guards both the pretty-output `(clone URL: undefined)` and JSON-envelope
304
273
  // `gitRemoteUrl: undefined` failure modes that would otherwise leak through.
@@ -317,21 +286,30 @@ export async function createMaafAppAsync(argumentProvider) {
317
286
  // - github: clone the user's external repo, sanity-check it's empty,
318
287
  // overlay the template, then write ms.config.json.
319
288
  // - none: no git, no clone — mkdir -p, overlay the template, write ms.config.json.
289
+ let pushOutcome = null;
320
290
  try {
321
291
  if (repoType === 'github') {
322
- // External GitHub Enterprise path: clone the developer's repo. Auth flows through
292
+ // External GitHub path: clone the developer's repo. Auth flows through
323
293
  // the user's existing git credential setup (gh, GCM browser OAuth, SSH key, PAT,
324
294
  // x509 client cert, etc.); the CLI does not inject any tokens at this layer (the
325
295
  // Microsoft GitHub App is server-side only per the MAAF GitHub Integration design
326
- // doc Requirement 1).
296
+ // doc Requirement 1). The post-clone empty-check that used to live here moved
297
+ // to the preflight (ensureExternalRepoIsEmpty), so by the time we get here the
298
+ // remote is known to be empty and the clone produces an empty working tree.
327
299
  cloneRepo({ directory: appDir, repoUrl: remoteUrl });
328
- // Post-clone sanity: --repo at P1 expects an empty GHE repo we can scaffold the
329
- // template into. If the cloned working tree already has content (existing project,
330
- // README, LICENSE, etc.), `ms app init` is the path that handles binding an existing
331
- // project to a new MAAF app bail with that pointer instead of overwriting files.
300
+ // Safety net: the pre-network probe (ensureExternalRepoIsEmpty) is fail-OPEN
301
+ // on errors (e.g. os.tmpdir() inaccessible, server doesn't support partial
302
+ // clone). When that happens, the actual cloneRepo may have pulled content
303
+ // we missed. One last readdirSync of appDir guarantees we don't silently
304
+ // overlay the template onto the user's files.
305
+ // The repo must be _completely_ empty — including hidden / dotfiles like
306
+ // .gitignore, .gitattributes, README, LICENSE, .git-credentials, etc.
307
+ // We only filter `.git` itself (the internal directory created by clone);
308
+ // any other entry, hidden or not, means the user has prior content here.
332
309
  const workingTreeEntries = fs.readdirSync(appDir).filter((entry) => entry !== '.git');
333
310
  if (workingTreeEntries.length > 0) {
334
- 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.`);
311
+ throw new UsageError(`Repository ${remoteUrl} is not empty (must contain no files of any kind, including hidden files like .gitignore, .gitattributes, README, or LICENSE). Choose a different repository to create an app or run 'ms app init' to update the existing app.\n\n` +
312
+ buildOrphanedAppRecoveryHint(app.id, app.environmentId));
335
313
  }
336
314
  }
337
315
  else if (repoType === 'native') {
@@ -364,12 +342,22 @@ export async function createMaafAppAsync(argumentProvider) {
364
342
  // key off `repoType` instead of a placeholder value.
365
343
  ...(repoType === 'native' && { repositoryId: app.repositoryId }),
366
344
  repoType,
345
+ // externalRepoUrl is persisted only for github-bound apps so that post-create verbs
346
+ // (build, deploy) know which GitHub host to authenticate against during inline DCF
347
+ // recovery without re-prompting the user for the URL.
348
+ ...(repoType === 'github' && { externalRepoUrl: remoteUrl }),
367
349
  cloud: argumentProvider.getCloud(),
368
350
  buildPath: buildPath ?? './dist',
369
351
  buildCommand: buildCommand ?? 'npm run build',
370
352
  buildEntryPoint: buildEntryPoint ?? 'index.html',
371
353
  };
372
354
  await writeRepoConfig(path.join(appDir, AppConfigFileName), config);
355
+ // Auto commit + push for git-backed flows (native + github). The `none` flow
356
+ // has no remote, so it's skipped. Push failure is non-fatal — see the warning
357
+ // path in commitAndPushScaffoldAsync.
358
+ if (repoType !== 'none') {
359
+ pushOutcome = await commitAndPushScaffoldAsync(appDir, jsonMode);
360
+ }
373
361
  }
374
362
  catch (setupError) {
375
363
  // UsageError carries already-actionable messages (e.g. "repo already contains an
@@ -385,15 +373,16 @@ export async function createMaafAppAsync(argumentProvider) {
385
373
  // also written to ms.config.json — used by every subsequent CLI command
386
374
  // (build, deploy, info, etc.). `gitRemoteUrl` is the GRS clone URL and is only
387
375
  // present for the `native` flow (the only flow that auto-provisions a
388
- // platform-managed git repo). `externalRepoUrl` is the user-supplied GHE URL and
389
- // only appears in the `github` flow. `repoType` is always emitted so automation
390
- // can detect which source-control mode the app was created in.
376
+ // platform-managed git repo). `externalRepoUrl` is the user-supplied external
377
+ // GitHub URL and only appears in the `github` flow. `repoType` is always emitted so
378
+ // automation can detect which source-control mode the app was created in.
391
379
  printJsonSuccess({
392
380
  appId: app.id,
393
381
  repoType,
394
382
  ...(repoType === 'github' && remoteUrl && { externalRepoUrl: remoteUrl }),
395
383
  ...(repoType === 'native' && cloneUrl && { gitRemoteUrl: cloneUrl }),
396
384
  appDir,
385
+ ...(pushOutcome && { push: pushOutcome }),
397
386
  });
398
387
  }
399
388
  else {
@@ -408,7 +397,63 @@ export async function createMaafAppAsync(argumentProvider) {
408
397
  }
409
398
  }
410
399
  /**
411
- * Clones the developer's external repo (GitHub Enterprise Cloud in v1) into `directory`.
400
+ * Commits the scaffolded files and pushes to the remote. Called by both `native`
401
+ * and `github` flows after writeRepoConfig.
402
+ *
403
+ * Failure is non-fatal: prints a warning with the retry command and returns
404
+ * `{ success: false, ... }`. The server-side app and local scaffold are unchanged.
405
+ *
406
+ * `stdio: ['ignore', 'ignore', 'inherit']`:
407
+ * - stdin ignored: child can't prompt for input (would block forever).
408
+ * - stdout ignored: discards git's progress prose so pretty output stays clean.
409
+ * - stderr inherited: git's actual error messages reach the user's terminal
410
+ * so they see git's own context alongside our warning.
411
+ *
412
+ * Commit identity (user.name / user.email) is read by git itself from
413
+ * local-then-global config — we pass nothing. Missing identity surfaces as
414
+ * git's own descriptive commit error through the inherited stderr.
415
+ */
416
+ export async function commitAndPushScaffoldAsync(appDir, jsonMode) {
417
+ // Full add+commit+push sequence: any of the three commands may have been the
418
+ // one that failed (e.g. `git commit` errors when user.name/user.email aren't
419
+ // set), so re-running just `git push` wouldn't recover. The sequence is
420
+ // idempotent: rerunning `git add .` and `git commit` on an already-committed
421
+ // state either no-ops or errors with "nothing to commit", in which case
422
+ // `git push` still pushes the existing commit.
423
+ const retryCommand = `git add . && git commit -m "Initial scaffold from 'ms app create'" && git push -u origin HEAD`;
424
+ try {
425
+ execFileSync('git', ['add', '.'], {
426
+ cwd: appDir,
427
+ stdio: ['ignore', 'ignore', 'inherit'],
428
+ });
429
+ execFileSync('git', ['commit', '-m', "Initial scaffold from 'ms app create'"], {
430
+ cwd: appDir,
431
+ stdio: ['ignore', 'ignore', 'inherit'],
432
+ });
433
+ execFileSync('git', ['push', '-u', 'origin', 'HEAD'], {
434
+ cwd: appDir,
435
+ stdio: ['ignore', 'ignore', 'inherit'],
436
+ });
437
+ return { success: true };
438
+ }
439
+ catch (err) {
440
+ // Capture the entire error structurally — name + message + stack for
441
+ // Error instances, spread of own properties otherwise. JSON.stringify
442
+ // would otherwise drop Error's non-enumerable fields and emit `{}`.
443
+ const errorPayload = err instanceof Error
444
+ ? { name: err.name, message: err.message, stack: err.stack }
445
+ : typeof err === 'object' && err !== null
446
+ ? { ...err }
447
+ : { message: String(err) };
448
+ if (!jsonMode) {
449
+ printWarning(`Could not commit and push the initial scaffold. Your app and local scaffold are ready. ` +
450
+ `To finish wiring it up, run:\n ${retryCommand}`);
451
+ }
452
+ return { success: false, error: errorPayload, retryCommand };
453
+ }
454
+ }
455
+ /**
456
+ * Clones the developer's external GitHub repo (`*.ghe.com` tenant or `github.com`) into `directory`.
412
457
  * Auth flows through the user's existing git credential setup — the CLI does not inject
413
458
  * tokens here. On failure, surfaces a CLI-friendly error pointing the user at the
414
459
  * GitHub App install prerequisite; git's own stderr is preserved (stdio: inherit) so
@@ -639,6 +684,73 @@ export function ensureRepoTargetDirectoryEmpty(appDir) {
639
684
  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.`);
640
685
  }
641
686
  }
687
+ /**
688
+ * Cheap pre-network probe to detect a non-empty remote BEFORE createAsync runs.
689
+ *
690
+ * Implementation: partial clone (`--depth=1 --filter=blob:none --no-checkout`)
691
+ * into an OS temp dir, then `git ls-tree -r HEAD`. The clone pulls refs + the
692
+ * tip commit + its tree objects only — no blob bytes, no working tree — so the
693
+ * bandwidth on the rejection path is tens of KB even for a multi-GB repo. The
694
+ * temp dir is removed in `finally`.
695
+ *
696
+ * Failure semantics — fail-OPEN:
697
+ * - Non-empty tree at HEAD → throws `UsageError` with the "is not empty" /
698
+ * "ms app init" actionable lead, BEFORE createAsync. No orphan, no leak.
699
+ * - Empty tree (incl. the cleaned-up-repo edge case: commit1 add, commit2
700
+ * delete all) → ls-tree returns no lines → returns without throwing.
701
+ * - Any error from git itself (auth, network blip, server doesn't support
702
+ * --filter, repo doesn't exist, etc.) → swallow and return. The subsequent
703
+ * cloneRepo will surface a friendlier message for the user-facing failure;
704
+ * we don't want this probe to become a parallel error-message surface.
705
+ */
706
+ export function ensureExternalRepoIsEmpty(repoUrl) {
707
+ let probeDir;
708
+ try {
709
+ probeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ms-app-create-probe-'));
710
+ }
711
+ catch {
712
+ // Can't make a temp dir → punt. Real cloneRepo will surface any actionable
713
+ // issue (writable-fs, etc.) on the subsequent attempt.
714
+ return;
715
+ }
716
+ try {
717
+ try {
718
+ execFileSync('git', ['clone', '--depth=1', '--filter=blob:none', '--no-checkout', repoUrl, probeDir], { stdio: ['ignore', 'ignore', 'ignore'] });
719
+ }
720
+ catch {
721
+ // Clone itself failed (auth, network, repo not found, server doesn't
722
+ // support partial clone, etc.). Fail open: the user-facing error will
723
+ // come from the later cloneRepo which has the friendlier messaging.
724
+ return;
725
+ }
726
+ let lsTreeOutput;
727
+ try {
728
+ lsTreeOutput = execFileSync('git', ['-C', probeDir, 'ls-tree', '-r', 'HEAD'], {
729
+ stdio: ['ignore', 'pipe', 'ignore'],
730
+ }).toString();
731
+ }
732
+ catch {
733
+ // Truly empty repo (no commits, no HEAD): ls-tree errors. Treat as empty
734
+ // so create proceeds; cloneRepo of a no-commits remote produces an empty
735
+ // working tree, which is the happy path.
736
+ return;
737
+ }
738
+ if (lsTreeOutput.trim().length > 0) {
739
+ // `ls-tree -r HEAD` enumerates every tracked path — including hidden
740
+ // ones (.gitignore, .gitattributes, etc.). Any tracked file at all
741
+ // means the repo isn't a valid binding target for a fresh app.
742
+ throw new UsageError(`Repository ${repoUrl} is not empty (must contain no tracked files of any kind, including hidden files like .gitignore, .gitattributes, README, or LICENSE). Choose a different repository to create an app or run 'ms app init' to update the existing app.`);
743
+ }
744
+ }
745
+ finally {
746
+ try {
747
+ fs.rmSync(probeDir, { recursive: true, force: true });
748
+ }
749
+ catch {
750
+ // Best-effort cleanup; a leftover temp dir is acceptable.
751
+ }
752
+ }
753
+ }
642
754
  export function ensureTargetDirectoryWritable(appDir) {
643
755
  // Probe write permission on the target dir (or its nearest existing
644
756
  // ancestor, where mkdirSync will need to write). Catches read-only mounts
@@ -670,136 +782,20 @@ export function ensureTargetDirectoryWritable(appDir) {
670
782
  throw new ConfigurationError(`'${appDir}' is not writable. Choose a different directory or fix the permissions and retry.`);
671
783
  }
672
784
  }
673
- /**
674
- * Inline DCF recovery branch for the github catch-and-retry: only the
675
- * `github` flow ever calls this — `native` and `none` have no GitHub identity
676
- * mapping to refresh. SIGINT handling is scoped to this helper (registered
677
- * and removed inside the `try/finally`) so Ctrl+C during the device-code
678
- * polling loop doesn't leak a listener back to the verb's outer body. Server
679
- * errors thrown by `authenticateForRepoAsync` are mapped through
680
- * {@link mapGitHubServerErrorToCliError} before being rethrown so the verb's
681
- * outer catch (and the single-retry contract) sees CLI-typed errors.
682
- */
683
- async function runDcfRecoveryAsync(options) {
684
- const { remoteUrl, tenantId, jsonMode, noBrowser, verb } = options;
685
- // Re-parse here rather than threading the parsed components through every
686
- // catch-and-retry call: preflight already validated the URL shape, so this
687
- // is a cheap string-split on a rare path.
688
- const { host, owner, repo } = parseGitHubRepoUrl(remoteUrl);
689
- const repoHash = computeRepoHash(owner, repo);
690
- const logger = getCliLogger();
691
- // Telemetry: PII-safe — host is the parsed string, repoHash is a truncated
692
- // SHA256. Emit Start at the top so a recovery that never reaches the retry
693
- // (e.g. user denied auth) is still observable.
694
- logger.trackActivityEvent('Maaf.Create.GitHubDcfRecovery', {
695
- repoHost: host,
696
- repoHash,
697
- });
698
- if (!jsonMode) {
699
- printInfo('GitHub authentication is required to bind this repository.');
700
- }
701
- const display = composeRecoveryDisplay({ isJson: jsonMode, noBrowser });
702
- // SIGINT plumbing: a Ctrl+C during the (potentially 15-minute) polling
703
- // loop drops us out cleanly rather than leaving a dangling promise.
704
- // Scoped to the recovery block only — createAsync itself doesn't take a
705
- // signal yet.
706
- const controller = new AbortController();
707
- const onSigint = () => {
708
- controller.abort();
709
- };
710
- process.once('SIGINT', onSigint);
711
- try {
712
- // The resolved auth result (githubLogin, mappingExpiresAt, etc.) is
713
- // intentionally discarded: the retry path below doesn't need it — it
714
- // just needs the server-side identity mapping to now exist so the next
715
- // `createAsync` call clears the 401.
716
- await authenticateForRepoAsync({
717
- remoteUrl: `https://${host}/${owner}/${repo}`,
718
- tenantId,
719
- logger,
720
- display,
721
- // Browser launch is gated by the `display` object selected above:
722
- // `composeRecoveryDisplay` returns a display whose `openVerificationUri`
723
- // is undefined when `noBrowser` is true. `skipBrowser` here is the
724
- // legacy lower-level kill-switch the action exposes; we hardcode it
725
- // to false so the display contract is the single source of truth.
726
- skipBrowser: false,
727
- signal: controller.signal,
728
- });
729
- }
730
- catch (error) {
731
- // Telemetry: pair the Start event with a Failed event on every error path
732
- // so a recovery that throws (DCF declined, server error, abort) still
733
- // shows up as a closed pair in the activity stream rather than an
734
- // orphaned Start. Pattern A (emit Failed in the catch) — chosen over a
735
- // success-flag-in-finally to keep the existing throw flow untouched and
736
- // avoid restructuring the GrsServerError mapping branch below.
737
- logger.trackActivityEvent('Maaf.Create.GitHubDcfRecovery.Failed', {
738
- repoHost: host,
739
- repoHash,
740
- });
741
- // Only GrsServerError carries a structured `code` we can translate to a
742
- // CLI-typed AppError. Anything else (plain Error, network error, etc.)
743
- // propagates as-is — the verb's outer flow treats it as a hard failure
744
- // (no retry, no second "Resuming create...").
745
- if (error instanceof GrsServerError) {
746
- const mapped = mapGitHubServerErrorToCliError({
747
- code: error.code,
748
- message: error.message,
749
- status: error.status,
750
- details: error.details,
751
- }, { repoUrl: remoteUrl, verb });
752
- if (mapped)
753
- throw mapped;
754
- }
755
- throw error;
756
- }
757
- finally {
758
- // Safe in both branches: if SIGINT already fired, `process.once` has
759
- // already auto-removed the listener and `removeListener` is a no-op.
760
- process.removeListener('SIGINT', onSigint);
761
- }
762
- logger.trackActivityEvent('Maaf.Create.GitHubDcfRecovery.Complete', {
763
- repoHost: host,
764
- repoHash,
765
- });
766
- if (!jsonMode) {
767
- // Printed here (before the helper returns) rather than at the retry
768
- // call site so it's adjacent to the DCF-completed telemetry event. If
769
- // the subsequent createAsync retry hard-fails, the user sees
770
- // "Resuming create..." followed by the failure message — still accurate:
771
- // we DID resume; the resumed call is what failed.
772
- printInfo('Resuming create...');
773
- }
774
- }
775
- /**
776
- * Build the right {@link GitHubDcfDisplay} for the create recovery path:
777
- * - JSON mode → NDJSON events, never opens a browser.
778
- * - `--no-browser` → console output, browser launch suppressed.
779
- * - Default → console output + best-effort browser launch (TTY-gated).
780
- *
781
- * Note the inverted default vs `git auth refresh`: in recovery the user has
782
- * already started a destructive operation (create), so we lean toward
783
- * launching a browser by default and require explicit opt-out, instead of
784
- * the refresh verb's explicit opt-in.
785
- */
786
- function composeRecoveryDisplay(opts) {
787
- if (opts.isJson) {
788
- return new JsonGitHubDcfDisplay();
789
- }
790
- if (opts.noBrowser) {
791
- return {
792
- ...ConsoleGitHubDcfDisplay,
793
- openVerificationUri: undefined,
794
- };
795
- }
796
- return ConsoleGitHubDcfDisplay;
797
- }
798
785
  function createOptionSchema(argumentProvider) {
799
786
  argumentProvider.setDescription(HelpStrings.app.create.description);
800
787
  argumentProvider.addExamples(HelpStrings.app.create.examples);
801
788
  argumentProvider.consumePositionalArg('[directory]');
802
789
  argumentProvider.addOptions([
790
+ {
791
+ flags: {
792
+ key: CreateMaafAppArguments.EnvironmentId.name,
793
+ alias: CreateMaafAppArguments.EnvironmentId.alias,
794
+ },
795
+ env: CreateMaafAppArguments.EnvironmentId.envVar,
796
+ description: HelpStrings.app.create.options.environmentId,
797
+ optionalInNonInteractive: true,
798
+ },
803
799
  createDefaultStringOption({
804
800
  flags: {
805
801
  key: CreateMaafAppArguments.DisplayName.name,
@@ -880,8 +876,12 @@ function createOptionSchema(argumentProvider) {
880
876
  }),
881
877
  ]);
882
878
  argumentProvider.addSwitch({
883
- flags: { key: CreateMaafAppArguments.NoBrowser.name },
884
- description: HelpStrings.app.create.options.noBrowser,
879
+ flags: { key: CreateMaafAppArguments.OpenBrowser.name },
880
+ description: HelpStrings.app.create.options.openBrowser,
881
+ });
882
+ argumentProvider.addSwitch({
883
+ flags: { key: CreateMaafAppArguments.ForceReauth.name },
884
+ description: HelpStrings.app.create.options.forceReauth,
885
885
  });
886
886
  }
887
887
  //# sourceMappingURL=CreateMaafApp.js.map