@microsoft/managed-apps-cli 0.5.0 → 0.7.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 (192) hide show
  1. package/README.md +65 -24
  2. package/dist/ArgumentProvider.d.ts +10 -2
  3. package/dist/ArgumentProvider.d.ts.map +1 -1
  4. package/dist/ArgumentProvider.js +11 -2
  5. package/dist/ArgumentProvider.js.map +1 -1
  6. package/dist/Authentication/ActiveAccountStore.d.ts +24 -0
  7. package/dist/Authentication/ActiveAccountStore.d.ts.map +1 -0
  8. package/dist/Authentication/ActiveAccountStore.js +54 -0
  9. package/dist/Authentication/ActiveAccountStore.js.map +1 -0
  10. package/dist/Authentication/EnvironmentVariableAuthenticationProvider.d.ts +26 -0
  11. package/dist/Authentication/EnvironmentVariableAuthenticationProvider.d.ts.map +1 -0
  12. package/dist/Authentication/EnvironmentVariableAuthenticationProvider.js +31 -0
  13. package/dist/Authentication/EnvironmentVariableAuthenticationProvider.js.map +1 -0
  14. package/dist/Authentication/NodeMsalAuthenticationProvider.d.ts +57 -3
  15. package/dist/Authentication/NodeMsalAuthenticationProvider.d.ts.map +1 -1
  16. package/dist/Authentication/NodeMsalAuthenticationProvider.js +187 -27
  17. package/dist/Authentication/NodeMsalAuthenticationProvider.js.map +1 -1
  18. package/dist/Cli.d.ts.map +1 -1
  19. package/dist/Cli.js +74 -23
  20. package/dist/Cli.js.map +1 -1
  21. package/dist/CliUtils.d.ts +2 -8
  22. package/dist/CliUtils.d.ts.map +1 -1
  23. package/dist/CliUtils.js.map +1 -1
  24. package/dist/Constants/CommandRegistry.d.ts.map +1 -1
  25. package/dist/Constants/CommandRegistry.js +32 -6
  26. package/dist/Constants/CommandRegistry.js.map +1 -1
  27. package/dist/Constants/HelpStrings.d.ts +39 -3
  28. package/dist/Constants/HelpStrings.d.ts.map +1 -1
  29. package/dist/Constants/HelpStrings.js +63 -10
  30. package/dist/Constants/HelpStrings.js.map +1 -1
  31. package/dist/Constants.d.ts +2 -0
  32. package/dist/Constants.d.ts.map +1 -1
  33. package/dist/Constants.js +2 -0
  34. package/dist/Constants.js.map +1 -1
  35. package/dist/Errors/GitHubErrorMap.d.ts +39 -0
  36. package/dist/Errors/GitHubErrorMap.d.ts.map +1 -0
  37. package/dist/Errors/GitHubErrorMap.js +187 -0
  38. package/dist/Errors/GitHubErrorMap.js.map +1 -0
  39. package/dist/HttpClient/CliHttpClient.d.ts +8 -3
  40. package/dist/HttpClient/CliHttpClient.d.ts.map +1 -1
  41. package/dist/HttpClient/CliHttpClient.js +33 -13
  42. package/dist/HttpClient/CliHttpClient.js.map +1 -1
  43. package/dist/Logger/ConsoleWriter.d.ts.map +1 -1
  44. package/dist/Logger/ConsoleWriter.js +7 -7
  45. package/dist/Logger/ConsoleWriter.js.map +1 -1
  46. package/dist/Middletier/FunctionsBundler.d.ts +17 -0
  47. package/dist/Middletier/FunctionsBundler.d.ts.map +1 -0
  48. package/dist/Middletier/FunctionsBundler.js +165 -0
  49. package/dist/Middletier/FunctionsBundler.js.map +1 -0
  50. package/dist/Middletier/PackFunctions.d.ts +52 -0
  51. package/dist/Middletier/PackFunctions.d.ts.map +1 -0
  52. package/dist/Middletier/PackFunctions.js +122 -0
  53. package/dist/Middletier/PackFunctions.js.map +1 -0
  54. package/dist/Types/Cli.types.d.ts +42 -1
  55. package/dist/Types/Cli.types.d.ts.map +1 -1
  56. package/dist/Types/Command.types.d.ts +1 -1
  57. package/dist/Types/Command.types.d.ts.map +1 -1
  58. package/dist/Utils/ConsoleOutput.d.ts +6 -0
  59. package/dist/Utils/ConsoleOutput.d.ts.map +1 -1
  60. package/dist/Utils/ConsoleOutput.js +23 -12
  61. package/dist/Utils/ConsoleOutput.js.map +1 -1
  62. package/dist/Utils/EnvVars.d.ts +1 -1
  63. package/dist/Utils/EnvVars.d.ts.map +1 -1
  64. package/dist/Utils/EnvVars.js.map +1 -1
  65. package/dist/Utils/EnvironmentRoutingUtils.d.ts +8 -0
  66. package/dist/Utils/EnvironmentRoutingUtils.d.ts.map +1 -0
  67. package/dist/Utils/EnvironmentRoutingUtils.js +45 -0
  68. package/dist/Utils/EnvironmentRoutingUtils.js.map +1 -0
  69. package/dist/Utils/GitHubDcfFlow.d.ts +59 -0
  70. package/dist/Utils/GitHubDcfFlow.d.ts.map +1 -0
  71. package/dist/Utils/GitHubDcfFlow.js +165 -0
  72. package/dist/Utils/GitHubDcfFlow.js.map +1 -0
  73. package/dist/Utils/GitUtils.d.ts +5 -0
  74. package/dist/Utils/GitUtils.d.ts.map +1 -1
  75. package/dist/Utils/GitUtils.js +15 -0
  76. package/dist/Utils/GitUtils.js.map +1 -1
  77. package/dist/Utils/Paging.d.ts.map +1 -1
  78. package/dist/Utils/Paging.js +4 -4
  79. package/dist/Utils/Paging.js.map +1 -1
  80. package/dist/Utils/RepoUrl.d.ts +13 -0
  81. package/dist/Utils/RepoUrl.d.ts.map +1 -0
  82. package/dist/Utils/RepoUrl.js +6 -0
  83. package/dist/Utils/RepoUrl.js.map +1 -0
  84. package/dist/Utils/Spinner.d.ts +9 -1
  85. package/dist/Utils/Spinner.d.ts.map +1 -1
  86. package/dist/Utils/Spinner.js +25 -5
  87. package/dist/Utils/Spinner.js.map +1 -1
  88. package/dist/Verbs/AddAction.d.ts.map +1 -1
  89. package/dist/Verbs/AddAction.js +12 -41
  90. package/dist/Verbs/AddAction.js.map +1 -1
  91. package/dist/Verbs/AddConnector.d.ts +24 -0
  92. package/dist/Verbs/AddConnector.d.ts.map +1 -0
  93. package/dist/Verbs/AddConnector.js +156 -0
  94. package/dist/Verbs/AddConnector.js.map +1 -0
  95. package/dist/Verbs/AddConnectorFlows.d.ts +72 -0
  96. package/dist/Verbs/AddConnectorFlows.d.ts.map +1 -0
  97. package/dist/Verbs/AddConnectorFlows.js +198 -0
  98. package/dist/Verbs/AddConnectorFlows.js.map +1 -0
  99. package/dist/Verbs/AddDataSource.js +1 -1
  100. package/dist/Verbs/AddDataSource.js.map +1 -1
  101. package/dist/Verbs/AddFlow.d.ts.map +1 -1
  102. package/dist/Verbs/AddFlow.js +3 -2
  103. package/dist/Verbs/AddFlow.js.map +1 -1
  104. package/dist/Verbs/AddTable.d.ts.map +1 -1
  105. package/dist/Verbs/AddTable.js +15 -100
  106. package/dist/Verbs/AddTable.js.map +1 -1
  107. package/dist/Verbs/AuthStatus.d.ts.map +1 -1
  108. package/dist/Verbs/AuthStatus.js +34 -6
  109. package/dist/Verbs/AuthStatus.js.map +1 -1
  110. package/dist/Verbs/AuthSwitch.d.ts +6 -0
  111. package/dist/Verbs/AuthSwitch.d.ts.map +1 -0
  112. package/dist/Verbs/AuthSwitch.js +67 -0
  113. package/dist/Verbs/AuthSwitch.js.map +1 -0
  114. package/dist/Verbs/ConnectorHelpers.d.ts +54 -1
  115. package/dist/Verbs/ConnectorHelpers.d.ts.map +1 -1
  116. package/dist/Verbs/ConnectorHelpers.js +217 -16
  117. package/dist/Verbs/ConnectorHelpers.js.map +1 -1
  118. package/dist/Verbs/CreateMaafApp.d.ts +26 -1
  119. package/dist/Verbs/CreateMaafApp.d.ts.map +1 -1
  120. package/dist/Verbs/CreateMaafApp.js +310 -135
  121. package/dist/Verbs/CreateMaafApp.js.map +1 -1
  122. package/dist/Verbs/DeployMaafApp.d.ts.map +1 -1
  123. package/dist/Verbs/DeployMaafApp.js +4 -0
  124. package/dist/Verbs/DeployMaafApp.js.map +1 -1
  125. package/dist/Verbs/Dev.js +5 -5
  126. package/dist/Verbs/Dev.js.map +1 -1
  127. package/dist/Verbs/FindDataverseApi.js +15 -15
  128. package/dist/Verbs/FindDataverseApi.js.map +1 -1
  129. package/dist/Verbs/GetSettings.d.ts +1 -1
  130. package/dist/Verbs/GetSettings.js +1 -1
  131. package/dist/Verbs/Init.d.ts.map +1 -1
  132. package/dist/Verbs/Init.js +129 -61
  133. package/dist/Verbs/Init.js.map +1 -1
  134. package/dist/Verbs/ListConnectors.d.ts.map +1 -1
  135. package/dist/Verbs/ListConnectors.js +4 -2
  136. package/dist/Verbs/ListConnectors.js.map +1 -1
  137. package/dist/Verbs/ListDatasets.d.ts.map +1 -1
  138. package/dist/Verbs/ListDatasets.js +5 -0
  139. package/dist/Verbs/ListDatasets.js.map +1 -1
  140. package/dist/Verbs/ListSqlStoredProcedures.d.ts.map +1 -1
  141. package/dist/Verbs/ListSqlStoredProcedures.js +5 -0
  142. package/dist/Verbs/ListSqlStoredProcedures.js.map +1 -1
  143. package/dist/Verbs/ListTables.d.ts.map +1 -1
  144. package/dist/Verbs/ListTables.js +5 -0
  145. package/dist/Verbs/ListTables.js.map +1 -1
  146. package/dist/Verbs/Login.d.ts.map +1 -1
  147. package/dist/Verbs/Login.js +16 -13
  148. package/dist/Verbs/Login.js.map +1 -1
  149. package/dist/Verbs/Logout.d.ts.map +1 -1
  150. package/dist/Verbs/Logout.js +19 -13
  151. package/dist/Verbs/Logout.js.map +1 -1
  152. package/dist/Verbs/MaafAppPermissions.d.ts +26 -6
  153. package/dist/Verbs/MaafAppPermissions.d.ts.map +1 -1
  154. package/dist/Verbs/MaafAppPermissions.js +193 -28
  155. package/dist/Verbs/MaafAppPermissions.js.map +1 -1
  156. package/dist/Verbs/Pack.d.ts +6 -6
  157. package/dist/Verbs/Pack.d.ts.map +1 -1
  158. package/dist/Verbs/Pack.js +80 -12
  159. package/dist/Verbs/Pack.js.map +1 -1
  160. package/dist/Verbs/RefreshDataSource.d.ts.map +1 -1
  161. package/dist/Verbs/RefreshDataSource.js +4 -3
  162. package/dist/Verbs/RefreshDataSource.js.map +1 -1
  163. package/dist/Verbs/RefreshGitAuth.d.ts +6 -0
  164. package/dist/Verbs/RefreshGitAuth.d.ts.map +1 -0
  165. package/dist/Verbs/RefreshGitAuth.js +153 -0
  166. package/dist/Verbs/RefreshGitAuth.js.map +1 -0
  167. package/dist/Verbs/RemoveFlow.d.ts.map +1 -1
  168. package/dist/Verbs/RemoveFlow.js +4 -3
  169. package/dist/Verbs/RemoveFlow.js.map +1 -1
  170. package/dist/Verbs/SetSetting.d.ts +1 -1
  171. package/dist/Verbs/SetSetting.js +1 -1
  172. package/dist/Verbs/VerbConstants.d.ts +72 -16
  173. package/dist/Verbs/VerbConstants.d.ts.map +1 -1
  174. package/dist/Verbs/VerbConstants.js +64 -16
  175. package/dist/Verbs/VerbConstants.js.map +1 -1
  176. package/dist/Verbs/index.d.ts +3 -0
  177. package/dist/Verbs/index.d.ts.map +1 -1
  178. package/dist/Verbs/index.js +3 -0
  179. package/dist/Verbs/index.js.map +1 -1
  180. package/package.json +8 -8
  181. package/dist/Authentication/OrchardAuthenticationProvider.d.ts +0 -13
  182. package/dist/Authentication/OrchardAuthenticationProvider.d.ts.map +0 -1
  183. package/dist/Authentication/OrchardAuthenticationProvider.js +0 -30
  184. package/dist/Authentication/OrchardAuthenticationProvider.js.map +0 -1
  185. package/dist/HttpClient/OrchardProxy.d.ts +0 -11
  186. package/dist/HttpClient/OrchardProxy.d.ts.map +0 -1
  187. package/dist/HttpClient/OrchardProxy.js +0 -25
  188. package/dist/HttpClient/OrchardProxy.js.map +0 -1
  189. package/dist/HttpClient/Proxy.d.ts +0 -7
  190. package/dist/HttpClient/Proxy.d.ts.map +0 -1
  191. package/dist/HttpClient/Proxy.js +0 -5
  192. package/dist/HttpClient/Proxy.js.map +0 -1
@@ -1,9 +1,9 @@
1
1
  /*!
2
2
  * Copyright (C) Microsoft Corporation. All rights reserved.
3
3
  */
4
- import { createAsync, getPlayerServiceConfig, updateEnvironmentName, writeRepoConfig, } from '@microsoft/managed-apps-actions';
4
+ import { authenticateForRepoAsync, createAsync, getPlayerServiceConfig, GrsServerError, updateEnvironmentName, writeRepoConfig, } from '@microsoft/managed-apps-actions';
5
5
  import { AppConfigFileName } from '@microsoft/managed-apps-common/config';
6
- import { getUserRoutingEnvironmentAsync, ManagedGovernanceError, ManagedGovernanceTimeoutError, pollRoutingEnvironmentAsync, } from '@microsoft/managed-apps-common/services';
6
+ import { getUserRoutingEnvironmentAsync } 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';
@@ -13,9 +13,13 @@ import { createDefaultStringOption } from '../CliUtils.js';
13
13
  import { CLI_CLIENT_ID } from '../Constants.js';
14
14
  import { CLI_BINARY_NAME } from '../Constants/CliConstants.js';
15
15
  import { HelpStrings } from '../Constants/HelpStrings.js';
16
- import { AppError, ConfigurationError, NotFoundError, UsageError } from '../Errors/CliError.js';
17
- import { printInfo, printJsonSuccess, printSuccess, printWarning } from '../Utils/ConsoleOutput.js';
16
+ import { AppError, ConfigurationError, UsageError } from '../Errors/CliError.js';
17
+ import { mapGitHubServerErrorToCliError, matchGitHubAuthError } from '../Errors/GitHubErrorMap.js';
18
+ import { printInfo, printJsonLine, printJsonSuccess, printSuccess, printWarning, } from '../Utils/ConsoleOutput.js';
19
+ import { resolveRoutingEnvironmentIdAsync } from '../Utils/EnvironmentRoutingUtils.js';
18
20
  import { buildGcmConfig } from '../Utils/GitCredentialConfig.js';
21
+ import { ConsoleGitHubDcfDisplay, JsonGitHubDcfDisplay } from '../Utils/GitHubDcfFlow.js';
22
+ import { computeRepoHash, parseGitHubRepoUrl } from '../Utils/RepoUrl.js';
19
23
  import { CreateMaafAppArguments } from './VerbConstants.js';
20
24
  const DEFAULT_TEMPLATE = 'github:microsoft/Managed-Apps/templates/vite8';
21
25
  /**
@@ -32,6 +36,10 @@ const REPO_NONE_SENTINEL = 'none';
32
36
  * - `none` (case-insensitive) → `none` (no GRS, no local setup)
33
37
  * - any other non-empty value → `github` (external GHE binding, value flows through as
34
38
  * `remoteUrl` to the action and the eventual git clone)
39
+ *
40
+ * Runtime invariant: `remoteUrl` is set iff `repoType === 'github'`. Consumers
41
+ * branch on `repoType` and assert `remoteUrl!` at github use sites — see
42
+ * `RepoSelection` for why narrowing is not automatic.
35
43
  */
36
44
  export function parseRepoOption(rawRepoValue) {
37
45
  if (rawRepoValue === undefined || rawRepoValue === '') {
@@ -42,6 +50,17 @@ export function parseRepoOption(rawRepoValue) {
42
50
  }
43
51
  return { repoType: 'github', remoteUrl: rawRepoValue };
44
52
  }
53
+ /**
54
+ * Creates a new MAAF app and (where applicable) sets up local source control.
55
+ *
56
+ * Three repo flows, selected by `--repo`:
57
+ * - `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
59
+ * `createAsync` call, a missing/expired server-side GitHub identity mapping is
60
+ * recoverable in-band: a single inline DCF run + exactly one `createAsync` retry.
61
+ * Any other error — or a second mapping failure after DCF — is a hard fail.
62
+ * - `none`: no git binding at all (just the server record + template + ms.config.json).
63
+ */
45
64
  export async function createMaafAppAsync(argumentProvider) {
46
65
  createOptionSchema(argumentProvider);
47
66
  await argumentProvider.runVerb();
@@ -60,13 +79,9 @@ export async function createMaafAppAsync(argumentProvider) {
60
79
  const appDir = argumentProvider.getPositionalArg(0) ?? process.cwd();
61
80
  const template = await argumentProvider.getOption('template', /* ignorePrompt */ true);
62
81
  // CLI surface is --repo. Three flows, decided up front so the rest of the verb can
63
- // branch on `repoType` instead of inspecting `remoteUrl` everywhere:
64
- // - undefined → native (GRS-backed; current default)
65
- // - 'none' → no-source-control (skip GRS + skip every local setup step)
66
- // - any other value → github (external GHE binding; clone the URL locally)
67
- // The local variable name `remoteUrl` is preserved on purpose: the wire contract to
68
- // RP and the apps-actions param are still `remoteUrl` / `externalRepo.remoteUrl`, so
69
- // renaming end-to-end is deferred until those callers move.
82
+ // branch on `repoType` instead of inspecting `remoteUrl` everywhere. RepoSelection
83
+ // is a flat interface (not a discriminated union), so we branch on `repoType` and
84
+ // assert `remoteUrl!` at github use sites where `string` is required.
70
85
  const rawRepoValue = await argumentProvider.getOption('repo',
71
86
  /* ignorePrompt */ true);
72
87
  const { repoType, remoteUrl } = parseRepoOption(rawRepoValue);
@@ -97,20 +112,16 @@ export async function createMaafAppAsync(argumentProvider) {
97
112
  // local-setup failure leaves them orphaned with no clean recovery path.
98
113
  // Anything we can validate without hitting the server belongs here.
99
114
  //
100
- // --repo path: still requires git installed, writable target, and an empty
101
- // target (since `git clone` refuses non-empty destinations and we don't
102
- // destructively wipe local files). Template-source validation is skipped
103
- // the template is overlaid onto the cloned working tree, but the same
104
- // giget call validates the source. To bind a non-empty directory to a
105
- // MAAF app, use `ms app init` instead.
115
+ // --repo path: requires git installed, writable target, and an empty target
116
+ // (since `git clone` refuses non-empty destinations and we don't destructively
117
+ // wipe local files). To bind a non-empty directory to a MAAF app, use `ms app
118
+ // init` instead.
106
119
  //
107
- // --repo none path: no git, no clone, no template only ms.config.json
108
- // gets written locally. The preflight reduces to (1) target dir must be
109
- // writable so the config write later won't fail post-create, and (2) the
110
- // dir must NOT already contain an ms.config.json (silently rebinding an
111
- // existing config to a new app would be a footgun). Empty-dir is NOT
112
- // required: the user opted out of scaffolding, so dropping an
113
- // ms.config.json next to existing files is the intent.
120
+ // --repo none path: no git, no clone — ms.config.json and the template are
121
+ // written locally. The preflight reduces to (1) target dir must be writable
122
+ // so the config and template writes won't fail post-create, and (2) the dir
123
+ // must be empty (template scaffolding requires a clean target, same as the
124
+ // other flows).
114
125
  // ──────────────────────────────────────────────────────────────────────────
115
126
  if (!jsonMode) {
116
127
  printInfo('Running preflight checks...');
@@ -122,30 +133,23 @@ export async function createMaafAppAsync(argumentProvider) {
122
133
  throw new UsageError("'--repo none' is incompatible with '--repository-id'. " +
123
134
  "Use '--repo none' for an app with no git binding, or omit '--repo' (or pass a repo URL) to use a backing repository.");
124
135
  }
125
- // --repo none does not scaffold a template, so a user-supplied --template would be
126
- // silently dropped. Reject up front rather than appear to honor it.
127
- if (repoType === 'none' && template !== DEFAULT_TEMPLATE) {
128
- throw new UsageError("'--template' is not supported with '--repo none' because no files are scaffolded. " +
129
- "Remove '--template' or drop '--repo none' if you want template scaffolding.");
130
- }
136
+ ensureTemplateSourceValid(template);
131
137
  if (repoType !== 'none') {
132
138
  ensureGitInstalled();
133
139
  }
134
- if (repoType === 'native') {
135
- ensureTemplateSourceValid(template);
136
- }
137
140
  ensureTargetDirectoryWritable(appDir);
138
141
  if (repoType === 'github') {
142
+ // RepoSelection is a flat interface (not a discriminated union), so `remoteUrl`
143
+ // doesn't auto-narrow to `string` here even though the github branch guarantees it.
139
144
  ensureRepoTargetDirectoryEmpty(appDir);
140
- }
141
- else if (repoType === 'native') {
142
- ensureTargetDirectoryEmpty(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);
143
150
  }
144
151
  else {
145
- // --repo none: empty-dir is intentionally not required. We still guard against
146
- // overwriting an existing ms.config.json so the user can't silently rebind a
147
- // pre-existing app folder to a brand-new server app.
148
- ensureNoExistingConfig(appDir);
152
+ ensureTargetDirectoryEmpty(appDir);
149
153
  }
150
154
  // ─── End preflight ────────────────────────────────────────────────────────
151
155
  // The debug env-var override (MS_CLI_MAAF_DEBUG_ENVIRONMENT_ID) is honored by ArgumentProvider
@@ -170,6 +174,8 @@ export async function createMaafAppAsync(argumentProvider) {
170
174
  const buildPath = await argumentProvider.getOption('buildPath', /* ignorePrompt */ true);
171
175
  const buildCommand = await argumentProvider.getOption('buildCommand',
172
176
  /* ignorePrompt */ true);
177
+ const buildEntryPoint = await argumentProvider.getOption('buildEntryPoint',
178
+ /* ignorePrompt */ true);
173
179
  // Explicit branch keeps the non-null read of routingEnvPromise reachable only when
174
180
  // preResolvedEnvironmentId is undefined — i.e. exactly when routingEnvPromise is defined.
175
181
  const environmentId = preResolvedEnvironmentId !== undefined
@@ -186,33 +192,118 @@ export async function createMaafAppAsync(argumentProvider) {
186
192
  if (repoType === 'github') {
187
193
  ensureRepoTargetDirectoryEmpty(appDir);
188
194
  }
189
- else if (repoType === 'native') {
190
- ensureTargetDirectoryEmpty(appDir);
191
- }
192
195
  else {
193
- ensureNoExistingConfig(appDir);
196
+ ensureTargetDirectoryEmpty(appDir);
194
197
  }
195
- const { app, cloneUrl } = await createAsync({
198
+ // Read --no-browser alongside --json so the recovery DCF helper has both
199
+ // inputs ready without a second async option read inside the catch block.
200
+ // 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,
203
+ /* ignorePrompt */ true)) ?? false;
204
+ // JSON mode (github flow only): emit a structured `create-validation` event
205
+ // right before the server call so automation parsing NDJSON sees the
206
+ // validation→createAsync boundary — needed because the github flow has a
207
+ // catch-and-retry branch where validation precedes a recoverable DCF.
208
+ // Gated to `github` so we don't change the NDJSON wire contract for
209
+ // existing `native` / `none` `--json` consumers.
210
+ if (jsonMode && repoType === 'github') {
211
+ printJsonLine({ phase: 'create-validation' });
212
+ }
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 = {
217
+ displayName,
218
+ description,
219
+ repositoryId,
220
+ remoteUrl,
221
+ repoType,
222
+ };
223
+ const runCreate = () => createAsync({
196
224
  tenantId,
197
225
  environmentId,
198
226
  logger,
199
- actionsParams: { displayName, description, repositoryId, remoteUrl, repoType },
227
+ actionsParams,
200
228
  });
201
- // `--repo none` is the only path where the server intentionally returns no GRS clone
202
- // URL (we asked for "no platform-managed repo"). The native and github flows both
203
- // expect one: native uses it as origin for the first push; github uses it as the
204
- // platform-managed build-artifact store backing the external GHE binding. Treat its
205
- // absence on those flows as a server bug — the hoisted check below guards both the
206
- // pretty-output `(clone URL: undefined)` and JSON-envelope `gitRemoteUrl: undefined`
207
- // failure modes that would otherwise leak through.
208
- if (repoType !== 'none' && !cloneUrl) {
209
- const recoveryHint = buildOrphanedAppRecoveryHint(app.id, app.environmentId);
210
- if (repoType === 'github') {
211
- throw new AppError(`App '${app.displayName}' was created and bound to ${remoteUrl}, but the ` +
212
- `platform-managed git repository for build artifact storage was not provisioned. ` +
213
- `This is a platform-side issue; please report it.\n` +
214
- recoveryHint);
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();
215
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
+ const { app, cloneUrl } = createResult;
299
+ // Only the `native` flow expects a GRS clone URL — it's the origin for the first
300
+ // 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
302
+ // produces a `cloneUrl`. Missing on `native` is a server bug — the hoisted check
303
+ // below guards both the pretty-output `(clone URL: undefined)` and JSON-envelope
304
+ // `gitRemoteUrl: undefined` failure modes that would otherwise leak through.
305
+ if (repoType === 'native' && !cloneUrl) {
306
+ const recoveryHint = buildOrphanedAppRecoveryHint(app.id, app.environmentId);
216
307
  throw new AppError(`App '${app.displayName}' was created on the server, but creating a git repository failed. ` +
217
308
  `This is a platform-side issue; please report it. ` +
218
309
  `In the meantime, the partial app needs to be cleaned up.\n` +
@@ -225,7 +316,7 @@ export async function createMaafAppAsync(argumentProvider) {
225
316
  // then overlay the template, then write ms.config.json.
226
317
  // - github: clone the user's external repo, sanity-check it's empty,
227
318
  // overlay the template, then write ms.config.json.
228
- // - none: no git, no templatejust mkdir -p and write ms.config.json.
319
+ // - none: no git, no clone — mkdir -p, overlay the template, write ms.config.json.
229
320
  try {
230
321
  if (repoType === 'github') {
231
322
  // External GitHub Enterprise path: clone the developer's repo. Auth flows through
@@ -253,35 +344,30 @@ export async function createMaafAppAsync(argumentProvider) {
253
344
  });
254
345
  }
255
346
  else {
256
- // --repo none: the user opted out of all scaffolding. Just ensure the
257
- // target dir exists so the writeRepoConfig call below can land
258
- // ms.config.json next to whatever the user already has (or in a fresh
259
- // empty dir). `mkdirSync(..., { recursive: true })` is a no-op if the
260
- // directory already exists.
347
+ // --repo none: no git setup. Ensure the target dir exists so the
348
+ // subsequent downloadTemplate and writeRepoConfig calls can land their
349
+ // files. The preflight already verified the dir is empty.
261
350
  fs.mkdirSync(appDir, { recursive: true });
262
351
  }
263
- // Overlay the template for the flows that scaffold one. `--repo none` opts out of
264
- // scaffolding entirely, so this block is skipped and the user's existing files
265
- // (if any) are left alone.
266
- if (repoType !== 'none') {
267
- if (!jsonMode) {
268
- printInfo(`Fetching template '${template}' into ${appDir}...`);
269
- }
270
- await downloadTemplate(template, { dir: appDir, force: true });
352
+ if (!jsonMode) {
353
+ printInfo(`Fetching template '${template}' into ${appDir}...`);
271
354
  }
355
+ await downloadTemplate(template, { dir: appDir, force: true });
272
356
  const config = {
273
357
  appId: app.id,
274
358
  appDisplayName: app.displayName,
275
359
  description: app.description,
276
360
  environmentId: app.environmentId,
277
- // `--repo none` apps have no platform-managed git binding; omit `repositoryId`
278
- // from the persisted config so downstream verbs that key off it see the
279
- // intentional absence rather than an empty/placeholder value.
280
- ...(repoType !== 'none' && { repositoryId: app.repositoryId }),
361
+ // Only the `native` flow has a platform-managed git binding. `github` tracks
362
+ // external binding server-side via `externalRepo`, and `none` intentionally has
363
+ // no platform-managed binding. Omit `repositoryId` for both so downstream verbs
364
+ // key off `repoType` instead of a placeholder value.
365
+ ...(repoType === 'native' && { repositoryId: app.repositoryId }),
281
366
  repoType,
282
367
  cloud: argumentProvider.getCloud(),
283
368
  buildPath: buildPath ?? './dist',
284
369
  buildCommand: buildCommand ?? 'npm run build',
370
+ buildEntryPoint: buildEntryPoint ?? 'index.html',
285
371
  };
286
372
  await writeRepoConfig(path.join(appDir, AppConfigFileName), config);
287
373
  }
@@ -297,23 +383,23 @@ export async function createMaafAppAsync(argumentProvider) {
297
383
  if (jsonMode) {
298
384
  // `appId` is the bare GUID returned by the server (`MaafAppDefinition.id`) and
299
385
  // also written to ms.config.json — used by every subsequent CLI command
300
- // (build, deploy, info, etc.). `gitRemoteUrl` is the GRS clone URL (auto-provisioned
301
- // alongside the external binding for build artifact storage) and is omitted for
302
- // `--repo none` since no platform-managed git repo exists. `externalRepoUrl` is the
303
- // user-supplied GHE URL and only appears in the `github` flow. `repoType` is always
304
- // emitted so automation can detect the new no-source-control path.
386
+ // (build, deploy, info, etc.). `gitRemoteUrl` is the GRS clone URL and is only
387
+ // 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.
305
391
  printJsonSuccess({
306
392
  appId: app.id,
307
393
  repoType,
308
394
  ...(repoType === 'github' && remoteUrl && { externalRepoUrl: remoteUrl }),
309
- ...(repoType !== 'none' && cloneUrl && { gitRemoteUrl: cloneUrl }),
395
+ ...(repoType === 'native' && cloneUrl && { gitRemoteUrl: cloneUrl }),
310
396
  appDir,
311
397
  });
312
398
  }
313
399
  else {
314
400
  printSuccess(`Created app '${app.displayName}' in ${appDir}.`);
315
401
  if (repoType === 'github') {
316
- printInfo(`A platform-managed git repository was also provisioned for build artifact storage (clone URL: ${cloneUrl}).`);
402
+ printInfo(`Bound to external repository: ${remoteUrl}.`);
317
403
  }
318
404
  else if (repoType === 'none') {
319
405
  printInfo('Your app has been created. Run `git init` (or your VCS of choice) when ready; ' +
@@ -321,43 +407,6 @@ export async function createMaafAppAsync(argumentProvider) {
321
407
  }
322
408
  }
323
409
  }
324
- async function resolveRoutingEnvironmentIdAsync(resultPromise, httpClient, logger) {
325
- // Translate routing-service errors into a UsageError so the user sees an actionable message
326
- // rather than the raw "ManagedGovernance request failed with status 403" implementation detail.
327
- // NotFoundError throws inside the try also flow through `toRoutingUsageError`, which leaves
328
- // non-ManagedGovernance errors unchanged.
329
- try {
330
- const result = await resultPromise;
331
- if (result.status === 200) {
332
- const name = result.data.environment?.name;
333
- if (!name) {
334
- throw new NotFoundError('Routing service returned 200 with no environment name.');
335
- }
336
- return name;
337
- }
338
- // 202: provisioning is in progress — poll until it completes.
339
- // Non-2xx responses are already thrown as ManagedGovernanceError inside
340
- // getUserRoutingEnvironmentAsync, so only 200/202 reach this code path.
341
- const routingResponse = await pollRoutingEnvironmentAsync(httpClient, result.pollingUrl, result.authResource, logger);
342
- if (!routingResponse.environment?.name) {
343
- throw new NotFoundError('Routing service finished provisioning but returned no environment name.');
344
- }
345
- return routingResponse.environment.name;
346
- }
347
- catch (err) {
348
- throw toRoutingUsageError(err);
349
- }
350
- }
351
- function toRoutingUsageError(err) {
352
- if (err instanceof ManagedGovernanceError) {
353
- return new UsageError(`Could not provision a Developer environment for your tenant (status ${err.status}). ` +
354
- 'Ensure your account has the required permissions to create a Developer environment.');
355
- }
356
- if (err instanceof ManagedGovernanceTimeoutError) {
357
- return new UsageError('Timed out waiting for a Developer environment to provision. Try again.');
358
- }
359
- return err instanceof Error ? err : new Error(String(err));
360
- }
361
410
  /**
362
411
  * Clones the developer's external repo (GitHub Enterprise Cloud in v1) into `directory`.
363
412
  * Auth flows through the user's existing git credential setup — the CLI does not inject
@@ -590,19 +639,6 @@ export function ensureRepoTargetDirectoryEmpty(appDir) {
590
639
  throw new UsageError(`'${appDir}' is not empty. --repo requires an empty target directory; specify an empty directory as the first argument or move to one before running.`);
591
640
  }
592
641
  }
593
- export function ensureNoExistingConfig(appDir) {
594
- // --repo none variant: empty-dir is not required, but we MUST refuse to overwrite an
595
- // existing ms.config.json. Silently replacing it would rebind the folder (and any
596
- // subsequent `ms app push/build/deploy` invocations) to a different server app — a
597
- // footgun loud enough to gate fail-fast in preflight. Called twice (preflight +
598
- // just-before-createAsync) for the same TOCTOU reasons as ensureTargetDirectoryEmpty.
599
- const configPath = path.join(appDir, AppConfigFileName);
600
- if (fs.existsSync(configPath)) {
601
- throw new UsageError(`'${configPath}' already exists. --repo none refuses to overwrite an existing app config; ` +
602
- `remove the file (or choose a different directory) before retrying, or run 'ms app init' ` +
603
- `to bind this directory to a new app interactively.`);
604
- }
605
- }
606
642
  export function ensureTargetDirectoryWritable(appDir) {
607
643
  // Probe write permission on the target dir (or its nearest existing
608
644
  // ancestor, where mkdirSync will need to write). Catches read-only mounts
@@ -634,6 +670,131 @@ export function ensureTargetDirectoryWritable(appDir) {
634
670
  throw new ConfigurationError(`'${appDir}' is not writable. Choose a different directory or fix the permissions and retry.`);
635
671
  }
636
672
  }
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
+ }
637
798
  function createOptionSchema(argumentProvider) {
638
799
  argumentProvider.setDescription(HelpStrings.app.create.description);
639
800
  argumentProvider.addExamples(HelpStrings.app.create.examples);
@@ -707,6 +868,20 @@ function createOptionSchema(argumentProvider) {
707
868
  default: 'npm run build',
708
869
  description: HelpStrings.app.create.options.buildCommand,
709
870
  }),
871
+ createDefaultStringOption({
872
+ flags: {
873
+ key: CreateMaafAppArguments.BuildEntryPoint.name,
874
+ },
875
+ env: CreateMaafAppArguments.BuildEntryPoint.envVar,
876
+ message: CreateMaafAppArguments.BuildEntryPoint.promptMessage,
877
+ initialValue: CreateMaafAppArguments.BuildEntryPoint.initialValue,
878
+ default: 'index.html',
879
+ description: HelpStrings.app.create.options.buildEntryPoint,
880
+ }),
710
881
  ]);
882
+ argumentProvider.addSwitch({
883
+ flags: { key: CreateMaafAppArguments.NoBrowser.name },
884
+ description: HelpStrings.app.create.options.noBrowser,
885
+ });
711
886
  }
712
887
  //# sourceMappingURL=CreateMaafApp.js.map