@microsoft/managed-apps-cli 0.13.0 → 0.16.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 (197) hide show
  1. package/README.md +1 -1
  2. package/dist/ArgumentProvider.d.ts +1 -2
  3. package/dist/ArgumentProvider.d.ts.map +1 -1
  4. package/dist/ArgumentProvider.js.map +1 -1
  5. package/dist/Authentication/NoOpAuthenticationProvider.d.ts +25 -0
  6. package/dist/Authentication/NoOpAuthenticationProvider.d.ts.map +1 -0
  7. package/dist/Authentication/NoOpAuthenticationProvider.js +31 -0
  8. package/dist/Authentication/NoOpAuthenticationProvider.js.map +1 -0
  9. package/dist/Cli.d.ts.map +1 -1
  10. package/dist/Cli.js +14 -0
  11. package/dist/Cli.js.map +1 -1
  12. package/dist/CliSettings.d.ts.map +1 -1
  13. package/dist/CliSettings.js +2 -1
  14. package/dist/CliSettings.js.map +1 -1
  15. package/dist/Constants/CommandRegistry.d.ts.map +1 -1
  16. package/dist/Constants/CommandRegistry.js +22 -4
  17. package/dist/Constants/CommandRegistry.js.map +1 -1
  18. package/dist/Constants/HelpStrings.d.ts +21 -3
  19. package/dist/Constants/HelpStrings.d.ts.map +1 -1
  20. package/dist/Constants/HelpStrings.js +53 -18
  21. package/dist/Constants/HelpStrings.js.map +1 -1
  22. package/dist/Constants.d.ts +2 -0
  23. package/dist/Constants.d.ts.map +1 -1
  24. package/dist/Constants.js +8 -0
  25. package/dist/Constants.js.map +1 -1
  26. package/dist/HttpClient/CliHttpClient.d.ts +1 -1
  27. package/dist/HttpClient/CliHttpClient.d.ts.map +1 -1
  28. package/dist/HttpClient/CliHttpClient.js +49 -2
  29. package/dist/HttpClient/CliHttpClient.js.map +1 -1
  30. package/dist/Logger/CliLogger.d.ts +1 -1
  31. package/dist/Logger/CliLogger.d.ts.map +1 -1
  32. package/dist/Logger/CliLogger.js +23 -4
  33. package/dist/Logger/CliLogger.js.map +1 -1
  34. package/dist/Logger/ConsoleWriter.d.ts +1 -0
  35. package/dist/Logger/ConsoleWriter.d.ts.map +1 -1
  36. package/dist/Logger/ConsoleWriter.js +3 -0
  37. package/dist/Logger/ConsoleWriter.js.map +1 -1
  38. package/dist/Logger/OneDSWriter.d.ts +1 -1
  39. package/dist/Logger/OneDSWriter.d.ts.map +1 -1
  40. package/dist/Logger/OneDSWriter.js +8 -3
  41. package/dist/Logger/OneDSWriter.js.map +1 -1
  42. package/dist/Testing/MockFetch.d.ts +23 -0
  43. package/dist/Testing/MockFetch.d.ts.map +1 -0
  44. package/dist/Testing/MockFetch.js +177 -0
  45. package/dist/Testing/MockFetch.js.map +1 -0
  46. package/dist/Testing/MockRoutes.d.ts +34 -0
  47. package/dist/Testing/MockRoutes.d.ts.map +1 -0
  48. package/dist/Testing/MockRoutes.js +226 -0
  49. package/dist/Testing/MockRoutes.js.map +1 -0
  50. package/dist/Testing/fixtures/connectors/commondataserviceforapps.d.ts +6 -0
  51. package/dist/Testing/fixtures/connectors/commondataserviceforapps.d.ts.map +1 -0
  52. package/dist/Testing/fixtures/connectors/commondataserviceforapps.js +25 -0
  53. package/dist/Testing/fixtures/connectors/commondataserviceforapps.js.map +1 -0
  54. package/dist/Testing/fixtures/connectors/dataverseContactEntity.d.ts +6 -0
  55. package/dist/Testing/fixtures/connectors/dataverseContactEntity.d.ts.map +1 -0
  56. package/dist/Testing/fixtures/connectors/dataverseContactEntity.js +55 -0
  57. package/dist/Testing/fixtures/connectors/dataverseContactEntity.js.map +1 -0
  58. package/dist/Testing/fixtures/connectors/excelonlinebusiness.d.ts +6 -0
  59. package/dist/Testing/fixtures/connectors/excelonlinebusiness.d.ts.map +1 -0
  60. package/dist/Testing/fixtures/connectors/excelonlinebusiness.js +1970 -0
  61. package/dist/Testing/fixtures/connectors/excelonlinebusiness.js.map +1 -0
  62. package/dist/Testing/fixtures/connectors/kusto.d.ts +6 -0
  63. package/dist/Testing/fixtures/connectors/kusto.d.ts.map +1 -0
  64. package/dist/Testing/fixtures/connectors/kusto.js +1333 -0
  65. package/dist/Testing/fixtures/connectors/kusto.js.map +1 -0
  66. package/dist/Testing/fixtures/connectors/msnweather.d.ts +6 -0
  67. package/dist/Testing/fixtures/connectors/msnweather.d.ts.map +1 -0
  68. package/dist/Testing/fixtures/connectors/msnweather.js +917 -0
  69. package/dist/Testing/fixtures/connectors/msnweather.js.map +1 -0
  70. package/dist/Testing/fixtures/connectors/office365groups.d.ts +6 -0
  71. package/dist/Testing/fixtures/connectors/office365groups.d.ts.map +1 -0
  72. package/dist/Testing/fixtures/connectors/office365groups.js +2144 -0
  73. package/dist/Testing/fixtures/connectors/office365groups.js.map +1 -0
  74. package/dist/Testing/fixtures/connectors/office365users.d.ts +6 -0
  75. package/dist/Testing/fixtures/connectors/office365users.d.ts.map +1 -0
  76. package/dist/Testing/fixtures/connectors/office365users.js +1969 -0
  77. package/dist/Testing/fixtures/connectors/office365users.js.map +1 -0
  78. package/dist/Testing/fixtures/connectors/onedriveforbusiness.d.ts +6 -0
  79. package/dist/Testing/fixtures/connectors/onedriveforbusiness.d.ts.map +1 -0
  80. package/dist/Testing/fixtures/connectors/onedriveforbusiness.js +4873 -0
  81. package/dist/Testing/fixtures/connectors/onedriveforbusiness.js.map +1 -0
  82. package/dist/Testing/fixtures/connectors/powerappsforappmakers.d.ts +6 -0
  83. package/dist/Testing/fixtures/connectors/powerappsforappmakers.d.ts.map +1 -0
  84. package/dist/Testing/fixtures/connectors/powerappsforappmakers.js +4682 -0
  85. package/dist/Testing/fixtures/connectors/powerappsforappmakers.js.map +1 -0
  86. package/dist/Testing/fixtures/connectors/sharepointTestListMetadata.d.ts +6 -0
  87. package/dist/Testing/fixtures/connectors/sharepointTestListMetadata.d.ts.map +1 -0
  88. package/dist/Testing/fixtures/connectors/sharepointTestListMetadata.js +1078 -0
  89. package/dist/Testing/fixtures/connectors/sharepointTestListMetadata.js.map +1 -0
  90. package/dist/Testing/fixtures/connectors/sharepointonline.d.ts +6 -0
  91. package/dist/Testing/fixtures/connectors/sharepointonline.d.ts.map +1 -0
  92. package/dist/Testing/fixtures/connectors/sharepointonline.js +28 -0
  93. package/dist/Testing/fixtures/connectors/sharepointonline.js.map +1 -0
  94. package/dist/Testing/fixtures/connectors/teams.d.ts +6 -0
  95. package/dist/Testing/fixtures/connectors/teams.d.ts.map +1 -0
  96. package/dist/Testing/fixtures/connectors/teams.js +9987 -0
  97. package/dist/Testing/fixtures/connectors/teams.js.map +1 -0
  98. package/dist/Testing/fixtures/connectors/translatorv2.d.ts +6 -0
  99. package/dist/Testing/fixtures/connectors/translatorv2.d.ts.map +1 -0
  100. package/dist/Testing/fixtures/connectors/translatorv2.js +307 -0
  101. package/dist/Testing/fixtures/connectors/translatorv2.js.map +1 -0
  102. package/dist/Testing/fixtures/index.d.ts +23 -0
  103. package/dist/Testing/fixtures/index.d.ts.map +1 -0
  104. package/dist/Testing/fixtures/index.js +96 -0
  105. package/dist/Testing/fixtures/index.js.map +1 -0
  106. package/dist/Types/LoggerTypes.types.d.ts +1 -0
  107. package/dist/Types/LoggerTypes.types.d.ts.map +1 -1
  108. package/dist/Utils/CoerceCliVersion.d.ts +27 -0
  109. package/dist/Utils/CoerceCliVersion.d.ts.map +1 -0
  110. package/dist/Utils/CoerceCliVersion.js +67 -0
  111. package/dist/Utils/CoerceCliVersion.js.map +1 -0
  112. package/dist/Utils/EnvVars.d.ts +1 -1
  113. package/dist/Utils/EnvVars.d.ts.map +1 -1
  114. package/dist/Utils/EnvVars.js.map +1 -1
  115. package/dist/Utils/EnvironmentRoutingUtils.d.ts +1 -2
  116. package/dist/Utils/EnvironmentRoutingUtils.d.ts.map +1 -1
  117. package/dist/Utils/EnvironmentRoutingUtils.js.map +1 -1
  118. package/dist/Utils/MaafAppConfig.d.ts +2 -0
  119. package/dist/Utils/MaafAppConfig.d.ts.map +1 -1
  120. package/dist/Utils/MaafAppConfig.js +6 -0
  121. package/dist/Utils/MaafAppConfig.js.map +1 -1
  122. package/dist/Utils/PinCli.d.ts +18 -13
  123. package/dist/Utils/PinCli.d.ts.map +1 -1
  124. package/dist/Utils/PinCli.js +37 -18
  125. package/dist/Utils/PinCli.js.map +1 -1
  126. package/dist/Utils/RepoOption.d.ts +1 -0
  127. package/dist/Utils/RepoOption.d.ts.map +1 -1
  128. package/dist/Utils/RepoOption.js +14 -0
  129. package/dist/Utils/RepoOption.js.map +1 -1
  130. package/dist/Utils/ShareAccessRouting.d.ts +78 -0
  131. package/dist/Utils/ShareAccessRouting.d.ts.map +1 -0
  132. package/dist/Utils/ShareAccessRouting.js +93 -0
  133. package/dist/Utils/ShareAccessRouting.js.map +1 -0
  134. package/dist/Verbs/AddConnector.d.ts +6 -6
  135. package/dist/Verbs/AddConnector.d.ts.map +1 -1
  136. package/dist/Verbs/AddConnector.js +62 -36
  137. package/dist/Verbs/AddConnector.js.map +1 -1
  138. package/dist/Verbs/AddConnectorFlows.d.ts +14 -2
  139. package/dist/Verbs/AddConnectorFlows.d.ts.map +1 -1
  140. package/dist/Verbs/AddConnectorFlows.js +67 -18
  141. package/dist/Verbs/AddConnectorFlows.js.map +1 -1
  142. package/dist/Verbs/ConnectorHelpers.d.ts +79 -6
  143. package/dist/Verbs/ConnectorHelpers.d.ts.map +1 -1
  144. package/dist/Verbs/ConnectorHelpers.js +280 -32
  145. package/dist/Verbs/ConnectorHelpers.js.map +1 -1
  146. package/dist/Verbs/CreateMaafApp.d.ts +8 -16
  147. package/dist/Verbs/CreateMaafApp.d.ts.map +1 -1
  148. package/dist/Verbs/CreateMaafApp.js +224 -96
  149. package/dist/Verbs/CreateMaafApp.js.map +1 -1
  150. package/dist/Verbs/DeleteMaafApp.d.ts +13 -9
  151. package/dist/Verbs/DeleteMaafApp.d.ts.map +1 -1
  152. package/dist/Verbs/DeleteMaafApp.js +41 -21
  153. package/dist/Verbs/DeleteMaafApp.js.map +1 -1
  154. package/dist/Verbs/Dev.d.ts.map +1 -1
  155. package/dist/Verbs/Dev.js +34 -3
  156. package/dist/Verbs/Dev.js.map +1 -1
  157. package/dist/Verbs/GetMaafApp.d.ts.map +1 -1
  158. package/dist/Verbs/GetMaafApp.js +18 -6
  159. package/dist/Verbs/GetMaafApp.js.map +1 -1
  160. package/dist/Verbs/Init.d.ts.map +1 -1
  161. package/dist/Verbs/Init.js +58 -5
  162. package/dist/Verbs/Init.js.map +1 -1
  163. package/dist/Verbs/ListConnectors.d.ts.map +1 -1
  164. package/dist/Verbs/ListConnectors.js +12 -1
  165. package/dist/Verbs/ListConnectors.js.map +1 -1
  166. package/dist/Verbs/ListMaafApps.d.ts +9 -8
  167. package/dist/Verbs/ListMaafApps.d.ts.map +1 -1
  168. package/dist/Verbs/ListMaafApps.js +30 -23
  169. package/dist/Verbs/ListMaafApps.js.map +1 -1
  170. package/dist/Verbs/MaafAppPermissions.d.ts +8 -17
  171. package/dist/Verbs/MaafAppPermissions.d.ts.map +1 -1
  172. package/dist/Verbs/MaafAppPermissions.js +84 -91
  173. package/dist/Verbs/MaafAppPermissions.js.map +1 -1
  174. package/dist/Verbs/Pack.d.ts +30 -1
  175. package/dist/Verbs/Pack.d.ts.map +1 -1
  176. package/dist/Verbs/Pack.js +179 -2
  177. package/dist/Verbs/Pack.js.map +1 -1
  178. package/dist/Verbs/RefreshDataSource.d.ts +15 -0
  179. package/dist/Verbs/RefreshDataSource.d.ts.map +1 -1
  180. package/dist/Verbs/RefreshDataSource.js +41 -27
  181. package/dist/Verbs/RefreshDataSource.js.map +1 -1
  182. package/dist/Verbs/RemoveDataSource.d.ts.map +1 -1
  183. package/dist/Verbs/RemoveDataSource.js +17 -3
  184. package/dist/Verbs/RemoveDataSource.js.map +1 -1
  185. package/dist/Verbs/ShareList.d.ts +10 -0
  186. package/dist/Verbs/ShareList.d.ts.map +1 -0
  187. package/dist/Verbs/ShareList.js +254 -0
  188. package/dist/Verbs/ShareList.js.map +1 -0
  189. package/dist/Verbs/VerbConstants.d.ts +56 -3
  190. package/dist/Verbs/VerbConstants.d.ts.map +1 -1
  191. package/dist/Verbs/VerbConstants.js +40 -3
  192. package/dist/Verbs/VerbConstants.js.map +1 -1
  193. package/dist/Verbs/index.d.ts +1 -0
  194. package/dist/Verbs/index.d.ts.map +1 -1
  195. package/dist/Verbs/index.js +1 -0
  196. package/dist/Verbs/index.js.map +1 -1
  197. package/package.json +4 -4
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { createAsync, getPlayerServiceConfig, updateEnvironmentName, writeRepoConfig, } from '@microsoft/managed-apps-actions';
5
5
  import { AppConfigFileName } from '@microsoft/managed-apps-common/config';
6
- import { regionToCloudSafe } from '@microsoft/managed-apps-common/services';
6
+ import { HttpClientError, regionToCloudSafe } from '@microsoft/managed-apps-common/services';
7
7
  import * as fs from 'fs';
8
8
  import * as os from 'os';
9
9
  import * as path from 'path';
@@ -11,7 +11,7 @@ import { getCliLogger, getSettings } from '../CliSettings.js';
11
11
  import { createDefaultStringOption, resolveEnvironmentId } from '../CliUtils.js';
12
12
  import { CLI_BINARY_NAME } from '../Constants/CliConstants.js';
13
13
  import { HelpStrings } from '../Constants/HelpStrings.js';
14
- import { AppError, ConfigurationError, UsageError } from '../Errors/CliError.js';
14
+ import { AppError, AuthenticationError, ConfigurationError, NetworkError, UsageError, } from '../Errors/CliError.js';
15
15
  import { printInfo, printJsonLine, printJsonSuccess, printSuccess, printWarning, } from '../Utils/ConsoleOutput.js';
16
16
  import { withGitHubDcfRecoveryAsync } from '../Utils/DcfRecovery.js';
17
17
  import { downloadTemplate, parseGithubTemplate } from '../Utils/DownloadTemplate.js';
@@ -25,6 +25,9 @@ import { parseRepoOption } from '../Utils/RepoOption.js';
25
25
  import { CliSpinner } from '../Utils/Spinner.js';
26
26
  import { CreateMaafAppArguments } from './VerbConstants.js';
27
27
  const DEFAULT_TEMPLATE = 'github:microsoft/Managed-Apps/templates/vite8';
28
+ /** Commit message for the initial scaffold, shared by the auto-push and every
29
+ * manual-recovery hint */
30
+ const SCAFFOLD_COMMIT_MESSAGE = "Initial scaffold from 'ms app create'";
28
31
  /**
29
32
  * Creates a new MAAF app and (where applicable) sets up local source control.
30
33
  *
@@ -93,7 +96,7 @@ export async function createMaafAppAsync(argumentProvider) {
93
96
  // ─── Preflight ────────────────────────────────────────────────────────────
94
97
  // Run all local-only checks before any server call. Once createAsync
95
98
  // succeeds the user has a MAAF app + GRS repo on the server; a later
96
- // local-setup failure leaves them orphaned with no clean recovery path.
99
+ // local-setup failure leaves them with created resources that need recovery.
97
100
  // Anything we can validate without hitting the server belongs here.
98
101
  //
99
102
  // --repo path: requires git installed, writable target, and an empty target
@@ -139,7 +142,7 @@ export async function createMaafAppAsync(argumentProvider) {
139
142
  ensureRepoTargetDirectoryEmpty(appDir);
140
143
  // Pre-network: cheap partial clone into a temp dir + ls-tree HEAD to detect
141
144
  // a non-empty remote before createAsync runs. Catches the rejection case
142
- // without leaving an orphaned server-side app or scaffolded files in
145
+ // without leaving a server-side app or scaffolded files in
143
146
  // appDir. Cheapest probe shape: --depth=1 --filter=blob:none --no-checkout
144
147
  // pulls refs + the tip commit + its tree(s) only (no blob bytes, no working
145
148
  // tree). On any probe error (network, auth, server doesn't support partial
@@ -188,8 +191,8 @@ export async function createMaafAppAsync(argumentProvider) {
188
191
  // now is async and may take seconds — long enough for the user (or another
189
192
  // tool) to drop a file (or ms.config.json) into appDir. Repeating the check
190
193
  // here keeps the TOCTOU window between "directory looks fine" and
191
- // "createAsync succeeded" as small as possible without ever orphaning a
192
- // server-side app.
194
+ // "createAsync succeeded" as small as possible without creating a server-side
195
+ // app before local invariants are verified.
193
196
  if (repoType === 'github') {
194
197
  ensureRepoTargetDirectoryEmpty(appDir);
195
198
  }
@@ -240,21 +243,36 @@ export async function createMaafAppAsync(argumentProvider) {
240
243
  // DCF and retrying once; any other recognized server error is mapped to a
241
244
  // friendly CLI error. Non-github flows pass through untouched. See
242
245
  // `withGitHubDcfRecoveryAsync` for the full contract.
243
- const createResult = await withGitHubDcfRecoveryAsync(runCreate, {
244
- repoType,
245
- externalRepoUrl: remoteUrl,
246
- tenantId,
247
- jsonMode,
248
- openBrowser,
249
- verb: 'app create',
250
- });
246
+ //
247
+ // Any server error that escapes the DCF wrapper (all repo types) is routed
248
+ // through `translateCreateError`, which keys off the HTTP status code to
249
+ // surface an actionable message (e.g. a 403 becomes a permission error naming
250
+ // the environment) instead of the raw `HTTP error status: ...` blob from the RP.
251
+ let createResult;
252
+ try {
253
+ createResult = await withGitHubDcfRecoveryAsync(runCreate, {
254
+ repoType,
255
+ externalRepoUrl: remoteUrl,
256
+ tenantId,
257
+ jsonMode,
258
+ openBrowser,
259
+ verb: 'app create',
260
+ });
261
+ }
262
+ catch (createError) {
263
+ const translated = translateCreateError(createError, { repoType, environmentId });
264
+ if (translated)
265
+ throw translated;
266
+ throw createError;
267
+ }
251
268
  const { app, cloneUrl } = createResult;
252
269
  // Only the `native` flow expects a GRS clone URL — it's the origin for the first
253
- // local push to the auto-provisioned platform-managed repo. `github` binds an
254
- // external GitHub repo (no GRS) and `none` is by-design source-control-free, so neither
255
- // produces a `cloneUrl`. Missing on `native` is a server bug the hoisted check
256
- // below guards both the pretty-output `(clone URL: undefined)` and JSON-envelope
257
- // `gitRemoteUrl: undefined` failure modes that would otherwise leak through.
270
+ // local push to the auto-provisioned platform-managed repo, which GRS seeds with
271
+ // an initial commit on `main`. `github` binds an external GitHub repo (no GRS)
272
+ // and `none` is by-design source-control-free, so neither produces a `cloneUrl`.
273
+ // Missing on `native` is a server bug — the hoisted check below guards both the
274
+ // pretty-output `(clone URL: undefined)` and JSON-envelope `gitRemoteUrl: undefined`
275
+ // failure modes that would otherwise leak through.
258
276
  if (repoType === 'native' && !cloneUrl) {
259
277
  const recoveryHint = buildOrphanedAppRecoveryHint(app.id, app.environmentId);
260
278
  throw new AppError(`App '${app.displayName}' was created on the server, but creating a git repository failed. ` +
@@ -263,10 +281,11 @@ export async function createMaafAppAsync(argumentProvider) {
263
281
  recoveryHint);
264
282
  }
265
283
  // Set up local files. Runs after the API calls succeed; if any step here
266
- // fails the server app is already created and the recovery hint below tells
267
- // the user how to clean it up. The branch per `repoType`:
268
- // - native: init local git repo against the auto-provisioned GRS clone URL,
269
- // then overlay the template, then write ms.config.json.
284
+ // fails the server app is already created. The branch per `repoType`:
285
+ // - native: init local git repo + GCM config for the auto-provisioned GRS
286
+ // repo (seeded with an initial commit on `main`), overlay the
287
+ // template, then write ms.config.json. The push phase reconciles
288
+ // the scaffold onto that seeded history before pushing.
270
289
  // - github: clone the user's external repo, sanity-check it's empty,
271
290
  // overlay the template, then write ms.config.json.
272
291
  // - none: no git, no clone — mkdir -p, overlay the template, write ms.config.json.
@@ -302,7 +321,6 @@ export async function createMaafAppAsync(argumentProvider) {
302
321
  gitUrl: cloneUrl,
303
322
  loginAuthority: argumentProvider.getLoginAuthority(tenantId),
304
323
  authResource: argumentProvider.getAuthResourceUrl(),
305
- jsonMode,
306
324
  });
307
325
  }
308
326
  else {
@@ -315,12 +333,13 @@ export async function createMaafAppAsync(argumentProvider) {
315
333
  printInfo(`Fetching template '${template}' into ${appDir}...`);
316
334
  }
317
335
  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);
336
+ // Pin the CLI into the app so a later package install provides a project-local
337
+ // `ms` — resolved by `npx ms` / `pnpm exec` / `bun x` ahead of any globally-
338
+ // installed CLI — at the version it was created with (or the latest published
339
+ // version for a prerelease build). Runs for every repo flow (the template, and
340
+ // therefore its package.json, lands via downloadTemplate above regardless of
341
+ // `--repo`).
342
+ await pinCliIntoAppPackageJson(appDir, jsonMode, argumentProvider.getHttpClient());
324
343
  const config = buildMaafAppConfig({
325
344
  appId: app.id,
326
345
  appDisplayName: app.displayName,
@@ -346,31 +365,24 @@ export async function createMaafAppAsync(argumentProvider) {
346
365
  // has no remote, so it's skipped. Push failure is non-fatal — see the warning
347
366
  // path in commitAndPushScaffoldAsync.
348
367
  if (repoType !== 'none') {
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);
368
+ pushOutcome = await commitAndPushScaffoldAsync(appDir, jsonMode, repoType === 'native' ? cloneUrl : undefined);
360
369
  }
361
370
  }
362
371
  catch (setupError) {
363
372
  // UsageError carries already-actionable messages (e.g. "repo already contains an
364
- // app — try ms app init"). Let those through unwrapped so the user sees the
365
- // intended phrasing instead of the generic "local setup failed" prefix.
366
- if (setupError instanceof UsageError) {
373
+ // app — try ms app init") and, for the non-native flows that raise it, its own
374
+ // recovery hint. Let those through unwrapped. The native flow has already created
375
+ // the server app + repo by the time any setup step throws, so it always routes
376
+ // through the generic re-messaging below to surface the cleanup hint.
377
+ if (setupError instanceof UsageError && repoType !== 'native') {
367
378
  throw setupError;
368
379
  }
369
380
  const detail = setupError instanceof Error ? setupError.message : String(setupError);
370
381
  const message = `App '${app.displayName}' was created, but local setup failed: ${detail}\n` +
382
+ `The local directory ${appDir} may contain partial files; remove them or choose a new directory before retrying.\n` +
371
383
  buildOrphanedAppRecoveryHint(app.id, app.environmentId);
372
384
  // 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
385
+ // failure category survives the setup-recovery prefix intact: a classified
374
386
  // AuthenticationError keeps its exit code (3), name, errorData, AND its
375
387
  // subclass identity (`instanceof AuthenticationError` still holds for any
376
388
  // future handler that branches on the class). Constructing a fresh AppError
@@ -422,31 +434,41 @@ export async function createMaafAppAsync(argumentProvider) {
422
434
  * progress prose stays out of our pretty output). Stderr handling follows the
423
435
  * runGit policy: the un-classified `add`/`commit` steps re-emit git's stderr on
424
436
  * 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.
437
+ * flow, `network: true`) or pipes it for classification (native flow) either
438
+ * way git's own error context reaches the user alongside our warning. A
439
+ * classified push error is caught here, so its exit code never propagates;
440
+ * classification only shapes the warning text and the `--json` error payload.
430
441
  *
431
442
  * Commit identity (user.name / user.email) is read by git itself from
432
443
  * local-then-global config — we pass nothing. Missing identity surfaces as
433
444
  * git's own descriptive commit error via runGit's stderr handling.
434
445
  */
435
- export async function commitAndPushScaffoldAsync(appDir, jsonMode, pushClassify) {
446
+ export async function commitAndPushScaffoldAsync(appDir, jsonMode, nativeGitUrl) {
436
447
  // Full add+commit+push sequence: any of the three commands may have been the
437
448
  // one that failed (e.g. `git commit` errors when user.name/user.email aren't
438
449
  // set), so re-running just `git push` wouldn't recover. The sequence is
439
450
  // idempotent: rerunning `git add .` and `git commit` on an already-committed
440
451
  // state either no-ops or errors with "nothing to commit", in which case
441
452
  // `git push` still pushes the existing commit.
442
- const retryCommand = `git add . && git commit -m "Initial scaffold from 'ms app create'" && git push -u origin HEAD`;
453
+ //
454
+ // Native (GRS) has no safe one-line recovery: the failure could be at the
455
+ // fetch/reconcile step (scaffold not yet re-parented onto the seeded main) or
456
+ // at the final push (already re-parented), and a single copy-paste command
457
+ // can't be correct for both. So we surface no command for native and instead
458
+ // point the user at the local scaffold below.
459
+ const retryCommand = nativeGitUrl
460
+ ? undefined
461
+ : `git add . && git commit -m "${SCAFFOLD_COMMIT_MESSAGE}" && git push -u origin HEAD`;
443
462
  try {
444
463
  runGit(['add', '.'], { cwd: appDir });
445
- runGit(['commit', '-m', "Initial scaffold from 'ms app create'"], { cwd: appDir });
464
+ runGit(['commit', '-m', SCAFFOLD_COMMIT_MESSAGE], { cwd: appDir });
465
+ if (nativeGitUrl) {
466
+ reconcileNativeScaffoldWithSeededMain(appDir, nativeGitUrl, jsonMode);
467
+ }
446
468
  runGit(['push', '-u', 'origin', 'HEAD'], {
447
469
  cwd: appDir,
448
470
  network: true,
449
- classify: pushClassify,
471
+ classify: nativeGitUrl ? { action: `pushing to ${gitHostLabel(nativeGitUrl)}` } : undefined,
450
472
  });
451
473
  return { success: true };
452
474
  }
@@ -464,18 +486,68 @@ export async function commitAndPushScaffoldAsync(appDir, jsonMode, pushClassify)
464
486
  // carries the actionable, cause-specific guidance — plus git's own output —
465
487
  // in its message, but runGit's classify path throws *before* re-emitting
466
488
  // stderr, so without this the human-readable terminal would only see the
467
- // generic retry hint. (Unclassified add/commit failures and the live-stderr
489
+ // generic hint. (Unclassified add/commit failures and the live-stderr
468
490
  // github push already surfaced git's output, so only re-print for the
469
491
  // classified case to avoid duplicate noise.)
470
492
  if (err instanceof AppError) {
471
493
  printWarning(err.message);
472
494
  }
495
+ const finishHint = retryCommand
496
+ ? `To finish wiring it up, run:\n ${retryCommand}`
497
+ : `Resolve the git issue above, then finish the initial push from ${appDir}.`;
473
498
  printWarning(`Could not commit and push the initial scaffold. Your app and local scaffold are ready. ` +
474
- `To finish wiring it up, run:\n ${retryCommand}`);
499
+ finishHint);
475
500
  }
476
501
  return { success: false, error: errorPayload, retryCommand };
477
502
  }
478
503
  }
504
+ /**
505
+ * Reconciles the freshly committed local scaffold with the platform-seeded
506
+ * `main` branch just before the native (GRS) push. GRS provisions the repo
507
+ * with an initial commit on `main` (a README).
508
+ *
509
+ * 1. `git fetch origin` — bring the seed commit into origin/main
510
+ * (retried once on a transient auth miss).
511
+ * 2. `git reset --soft origin/main` — move HEAD onto the seed while keeping the
512
+ * scaffold tree fully staged.
513
+ * 3. `git commit` — record the scaffold as a child of the seed.
514
+ */
515
+ function reconcileNativeScaffoldWithSeededMain(appDir, gitUrl, jsonMode) {
516
+ announceGitAuthentication(gitUrl, jsonMode, 'the initial git fetch and push');
517
+ fetchOriginWithRetry(appDir, gitUrl);
518
+ const seededMain = runGit(['rev-parse', '--verify', 'origin/main'], {
519
+ cwd: appDir,
520
+ allowFailure: true,
521
+ emitStderr: false,
522
+ });
523
+ if (!seededMain.success) {
524
+ return;
525
+ }
526
+ runGit(['reset', '--soft', 'origin/main'], { cwd: appDir });
527
+ runGit(['commit', '-m', SCAFFOLD_COMMIT_MESSAGE], { cwd: appDir });
528
+ }
529
+ /**
530
+ * Fetches `origin`, retrying exactly once if the first attempt fails.
531
+ *
532
+ * The first attempt is silent (`allowFailure`, stderr piped-not-emitted) so a
533
+ * recoverable miss produces no scary output.
534
+ */
535
+ function fetchOriginWithRetry(appDir, gitUrl) {
536
+ const firstAttempt = runGit(['fetch', 'origin'], {
537
+ cwd: appDir,
538
+ network: true,
539
+ allowFailure: true,
540
+ emitStderr: false,
541
+ });
542
+ if (firstAttempt.success) {
543
+ return;
544
+ }
545
+ runGit(['fetch', 'origin'], {
546
+ cwd: appDir,
547
+ network: true,
548
+ classify: { action: `fetching from ${gitHostLabel(gitUrl)}` },
549
+ });
550
+ }
479
551
  const GIT_CLONE_ACCESS_FAILURE_SIGNATURES = [
480
552
  'repository not found',
481
553
  'fatal: repository',
@@ -527,7 +599,7 @@ function buildGitHubCloneAccessErrorMessage({ directory, repoUrl }) {
527
599
  return `Failed to clone '${repoUrl}' into '${directory}'. ` + gitHubAccessFailureGuidance();
528
600
  }
529
601
  // 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).
602
+ // lead makes explicit that nothing was created (no cleanup / no recovery hint).
531
603
  function buildGitHubAccessPreflightErrorMessage(repoUrl) {
532
604
  return (`Cannot access repository '${repoUrl}', so no app was created. ` + gitHubAccessFailureGuidance());
533
605
  }
@@ -574,14 +646,11 @@ export function cloneRepo({ directory, repoUrl }) {
574
646
  });
575
647
  }
576
648
  }
577
- export function initLocalRepo({ directory, gitUrl, loginAuthority, authResource, jsonMode = false, }) {
649
+ export function initLocalRepo({ directory, gitUrl, loginAuthority, authResource, }) {
578
650
  const gcmConfig = buildGcmConfigOrThrow({ gitUrl, loginAuthority, authResource });
579
651
  gitInit(directory);
580
652
  writeGcmConfig(directory, gcmConfig);
581
653
  addOrUpdateRemote(directory, gitUrl);
582
- announceGitAuthentication(gitUrl, jsonMode);
583
- fetchOrigin(directory, gitUrl);
584
- checkoutMainIfPresent(directory);
585
654
  }
586
655
  /**
587
656
  * Builds the per-host GCM OAuth config, re-wrapping any URL/protocol validation
@@ -596,12 +665,19 @@ function buildGcmConfigOrThrow({ gitUrl, loginAuthority, authResource, }) {
596
665
  throw new ConfigurationError(`Git credential helper setup failed: ${err instanceof Error ? err.message : String(err)}`);
597
666
  }
598
667
  }
599
- /** Creates the target directory (idempotent) and initializes an empty git repo. */
668
+ /**
669
+ * Creates the target directory (idempotent) and initializes an empty git repo
670
+ * whose initial branch is `main`.
671
+ */
600
672
  function gitInit(directory) {
601
673
  fs.mkdirSync(directory, { recursive: true });
602
674
  runGit(['init'], { cwd: directory });
675
+ // use `git symbolic-ref HEAD refs/heads/main` to set the initial branch to `main` instead of `master`. This is
676
+ // used instead of `git init -b main` / `-c init.defaultBranch=main` because both
677
+ // of those landed in git 2.28, one minor above this package's enforced 2.27 floor
678
+ runGit(['symbolic-ref', 'HEAD', 'refs/heads/main'], { cwd: directory });
603
679
  }
604
- /** Writes GCM config before fetching so the first network call is already authenticated. */
680
+ /** Writes GCM config before pushing so the first network call is already authenticated. */
605
681
  function writeGcmConfig(directory, gcmConfig) {
606
682
  for (const [key, value] of gcmConfig) {
607
683
  runGit(['config', '--local', key, value], { cwd: directory });
@@ -633,7 +709,7 @@ function addOrUpdateRemote(directory, gitUrl) {
633
709
  /**
634
710
  * Best-effort host label for a git remote URL. Returns the URL's host, or — when
635
711
  * 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
712
+ * action labels (auth/push messages) where a malformed URL must not turn a
637
713
  * cosmetic label into a thrown `Invalid URL` that masquerades as the real
638
714
  * failure. The platform issues these URLs, so the fallback is effectively
639
715
  * unreachable, but the guard keeps the label site from becoming a crash site.
@@ -647,43 +723,19 @@ function gitHostLabel(gitUrl) {
647
723
  }
648
724
  }
649
725
  /**
650
- * Prints the pre-fetch authentication notice. The first `git fetch` triggers
726
+ * Prints the pre-network authentication notice. The first git network call triggers
651
727
  * GCM's OAuth flow if the user has no cached credentials, which can pop a
652
728
  * browser without warning. Tell the user up front so they don't see the browser
653
729
  * as a surprise (and know which host / account to authenticate against). Skipped
654
730
  * under `--json` so the prose doesn't end up interleaved with the JSON envelope
655
731
  * on stdout.
656
732
  */
657
- function announceGitAuthentication(gitUrl, jsonMode) {
733
+ function announceGitAuthentication(gitUrl, jsonMode, action) {
658
734
  if (!jsonMode) {
659
- printInfo(`Authenticating to ${gitHostLabel(gitUrl)} for the initial git fetch. ` +
735
+ printInfo(`Authenticating to ${gitHostLabel(gitUrl)} for ${action}. ` +
660
736
  'A browser window may open. Sign in with the account associated with this tenant.');
661
737
  }
662
738
  }
663
- /** Fetches `origin`, classifying any auth/network failure against the remote host. */
664
- function fetchOrigin(directory, gitUrl) {
665
- runGit(['fetch', 'origin'], {
666
- cwd: directory,
667
- network: true,
668
- classify: { action: `fetching from ${gitHostLabel(gitUrl)}` },
669
- });
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 });
685
- }
686
- }
687
739
  // ─── Recovery hint for post-create failures ─────────────────────────────────
688
740
  // Once createAsync returns, the MAAF app + GRS repo exist on the server; any
689
741
  // later failure (no gitUrl, initLocalRepo, downloadTemplate, writeRepoConfig)
@@ -695,10 +747,86 @@ export function buildOrphanedAppRecoveryHint(serverAppId, environmentId) {
695
747
  `If you want to clean it up before retrying, run the following command:\n` +
696
748
  ` ${CLI_BINARY_NAME} app delete --app ${serverAppId} --environment-id ${environmentId} --force`);
697
749
  }
750
+ /** Extracts `error.message` from a JSON `{ error: { message } }` envelope; `undefined` otherwise. */
751
+ function extractServerMessage(err) {
752
+ if (!err.body)
753
+ return undefined;
754
+ try {
755
+ const parsed = JSON.parse(err.body);
756
+ if (typeof parsed?.error?.message === 'string' && parsed.error.message.trim()) {
757
+ return parsed.error.message;
758
+ }
759
+ }
760
+ catch {
761
+ // not JSON; fall through
762
+ }
763
+ return undefined;
764
+ }
765
+ /**
766
+ * Translates a server error thrown while creating the app (the `createAsync`
767
+ * RP/GRS calls) into a user-facing {@link AppError}, or returns `null` to signal
768
+ * "not handled — propagate the original". For the handled status codes below we
769
+ * key off the HTTP status to pick the error category (which drives the exit code)
770
+ * and surface an actionable message rather than the raw HTTP error blob
771
+ * (`HTTP error status: 403 for POST ...: {...}`). Unrecognized statuses (e.g. 500)
772
+ * return `null` and propagate unchanged, so the raw error can still surface for
773
+ * those — we only suppress the blob for the statuses we explicitly handle.
774
+ *
775
+ * The RP returns a structured `{ error: { code, message } }` envelope whose
776
+ * `message` is the source of truth — it names the offending principal and
777
+ * environment (e.g. "User '<oid>' does not have permission to create
778
+ * repositories in environment '<id>'."). We prefer that server message and fall
779
+ * back to CLI-generated text only when the body carries no usable message.
780
+ *
781
+ * Only translates {@link HttpClientError}. Errors already mapped to a typed
782
+ * {@link AppError} by the GitHub DCF recovery path (see `withGitHubDcfRecoveryAsync`)
783
+ * are passed through untouched.
784
+ */
785
+ function translateCreateError(err, ctx) {
786
+ if (!(err instanceof HttpClientError))
787
+ return null;
788
+ const { environmentId } = ctx;
789
+ // Prefer the RP's own `error.message` — it's the source of truth and carries
790
+ // specifics (principal object id, environment id) the CLI can't reconstruct.
791
+ const serverMessage = extractServerMessage(err);
792
+ // The `native` (default) flow auto-provisions a platform-managed GRS repository,
793
+ // so a permission failure there is specifically about repository creation. The
794
+ // `github` / `none` flows don't provision a repo, so the denial is about creating
795
+ // the app itself. Used only for the fallback wording when the RP sends no message.
796
+ const creatingRepo = ctx.repoType === 'native';
797
+ // Operation-agnostic so it reads correctly whether it's appended to the RP's
798
+ // "...create repositories..." message, the "...create an app..." fallback, or
799
+ // the native/github variants — avoids a "repositories" vs "apps" mismatch.
800
+ const grantHint = 'Ask an admin to grant you the required permission in this environment.';
801
+ if (err.status === 401) {
802
+ return new AuthenticationError(serverMessage ??
803
+ 'Your sign-in session has expired or is invalid. Run `ms auth login` and try again.');
804
+ }
805
+ if (err.status === 403) {
806
+ const base = serverMessage ??
807
+ (creatingRepo
808
+ ? `You do not have permission to create repositories in environment ${environmentId}.`
809
+ : `You do not have permission to create an app in environment ${environmentId}.`);
810
+ return new AuthenticationError(`${base} ${grantHint}`);
811
+ }
812
+ if (err.status === 404) {
813
+ return new UsageError(serverMessage ??
814
+ `Environment ${environmentId} was not found. Verify the environment id and that you have access to it.`);
815
+ }
816
+ if (err.status === 409) {
817
+ return new UsageError(serverMessage ??
818
+ `An app with the same name already exists in environment ${environmentId}. Choose a different name.`);
819
+ }
820
+ if (err.status === 429 || err.status === 503) {
821
+ return new NetworkError(serverMessage ??
822
+ 'The service is temporarily unavailable or rate-limited. Wait a moment and try again.');
823
+ }
824
+ return null;
825
+ }
698
826
  // ─── Preflight helpers ──────────────────────────────────────────────────────
699
827
  // All checks below run before any server call so a local misconfiguration
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
828
+ // (bad template source, unwritable directory, …) fails fast before it creates
829
+ // a MAAF app + GRS repo on the server. The git presence + version
702
830
  // check lives in `Utils/GitVersionChecker.ts`.
703
831
  // ────────────────────────────────────────────────────────────────────────────
704
832
  export function ensureTemplateSourceValid(source) {
@@ -747,13 +875,13 @@ export function ensureRepoTargetDirectoryEmpty(appDir) {
747
875
  *
748
876
  * Failure semantics:
749
877
  * - Non-empty tree at HEAD → throws `UsageError` with the "is not empty" /
750
- * "ms app init" actionable lead, BEFORE createAsync. No orphan, no leak.
878
+ * "ms app init" actionable lead, BEFORE createAsync. Nothing created.
751
879
  * - Empty tree (incl. the cleaned-up-repo edge case: commit1 add, commit2
752
880
  * delete all) → ls-tree returns no lines → returns without throwing.
753
881
  * - Clone fails with a recognizable access/auth signature (private repo
754
882
  * hidden as "not found", missing local credentials, 403, …) → throws
755
883
  * `ConfigurationError` BEFORE createAsync so we never create a server-side
756
- * app the user can't clone (fail FAST, no orphan).
884
+ * app the user can't clone.
757
885
  * - Any other git error (network blip, server doesn't support --filter,
758
886
  * temp-fs hiccup, …) → swallow and return (fail OPEN). The subsequent
759
887
  * cloneRepo surfaces the friendlier message for the user-facing failure;
@@ -775,7 +903,7 @@ export function ensureExternalRepoIsEmpty(repoUrl) {
775
903
  // Clone failed. When git reports a recognizable access/auth failure
776
904
  // (a private repo hidden as "not found", missing local credentials, a
777
905
  // 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
906
+ // server-side app the user can't clone. For ambiguous or
779
907
  // transient errors (server doesn't support --filter, network blip, a
780
908
  // temp-fs hiccup, …) stay fail-OPEN and let the later cloneRepo surface
781
909
  // the friendlier, fuller message.