@norrix/cli 0.0.41 → 0.0.43

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.
@@ -8,15 +8,15 @@ import { fileURLToPath } from 'url';
8
8
  import archiver from 'archiver';
9
9
  // import FormData from 'form-data';
10
10
  import { configureAmplify, loadCliEnvFiles } from './amplify-config.js';
11
- import { computeFingerprint, embedFingerprintInNativeResources, embedVersionInNativeResources, embedNxConfigurationInNativeResources } from './fingerprinting.js';
12
- import { loadNorrixConfig, hasNorrixConfig, saveNorrixConfig } from './config.js';
11
+ import { computeFingerprint, embedFingerprintInNativeResources, embedVersionInNativeResources, embedNxConfigurationInNativeResources, } from './fingerprinting.js';
12
+ import { loadNorrixConfig, hasNorrixConfig, saveNorrixConfig, } from './config.js';
13
13
  import { detectWorkspaceContext, getNxProjectDependencies, getWorkspaceDependenciesFallback, createWorkspaceManifest, logWorkspaceContext, isAtWorkspaceRoot, discoverNativeScriptApps, getWorkspaceContextForApp, detectNxBuildConfigurations, } from './workspace.js';
14
14
  import { signIn as amplifySignIn, signOut as amplifySignOut, getCurrentUser, fetchAuthSession, } from 'aws-amplify/auth';
15
15
  import crypto from 'crypto';
16
16
  import { Amplify } from 'aws-amplify';
17
17
  import { PROD_DEFAULTS } from './prod-defaults.js';
18
18
  import { DEV_DEFAULTS } from './dev-defaults.js';
19
- import { clearSelectedOrgId, getSelectedOrgId, setSelectedOrgId } from './cli-settings.js';
19
+ import { clearSelectedOrgId, getSelectedOrgId, setSelectedOrgId, } from './cli-settings.js';
20
20
  let CURRENT_ENV = 'prod';
21
21
  let CURRENT_DEFAULTS = PROD_DEFAULTS;
22
22
  let API_URL = PROD_DEFAULTS.apiUrl;
@@ -159,7 +159,9 @@ async function ensureOrgSelected(params) {
159
159
  const { organizations } = await fetchOrganizations(Boolean(params.verbose));
160
160
  const normalizedOrgs = organizations.filter((o) => o && o.id);
161
161
  // If forcePrompt is NOT set, use stored selection if valid
162
- if (!params.forcePrompt && stored && normalizedOrgs.some((o) => o.id === stored)) {
162
+ if (!params.forcePrompt &&
163
+ stored &&
164
+ normalizedOrgs.some((o) => o.id === stored)) {
163
165
  setCurrentOrgId(stored);
164
166
  return stored;
165
167
  }
@@ -1277,7 +1279,8 @@ async function zipWorkspaceProject(projectName, workspaceCtx, isUpdate = false,
1277
1279
  // 4. Add tools directory if it exists and is referenced
1278
1280
  for (const toolPath of deps.toolPaths) {
1279
1281
  const absoluteToolPath = path.join(workspaceCtx.workspaceRoot, toolPath);
1280
- if (fs.existsSync(absoluteToolPath) && fs.statSync(absoluteToolPath).isDirectory()) {
1282
+ if (fs.existsSync(absoluteToolPath) &&
1283
+ fs.statSync(absoluteToolPath).isDirectory()) {
1281
1284
  console.log(`Adding tools: ${toolPath}`);
1282
1285
  archive.directory(absoluteToolPath, toolPath, (entry) => {
1283
1286
  if (entry.name.includes('node_modules')) {
@@ -1290,7 +1293,8 @@ async function zipWorkspaceProject(projectName, workspaceCtx, isUpdate = false,
1290
1293
  // 5. Add asset paths if they exist
1291
1294
  for (const assetPath of deps.assetPaths) {
1292
1295
  const absoluteAssetPath = path.join(workspaceCtx.workspaceRoot, assetPath);
1293
- if (fs.existsSync(absoluteAssetPath) && fs.statSync(absoluteAssetPath).isDirectory()) {
1296
+ if (fs.existsSync(absoluteAssetPath) &&
1297
+ fs.statSync(absoluteAssetPath).isDirectory()) {
1294
1298
  if (verbose) {
1295
1299
  console.log(`Adding assets: ${assetPath}`);
1296
1300
  }
@@ -1302,7 +1306,7 @@ async function zipWorkspaceProject(projectName, workspaceCtx, isUpdate = false,
1302
1306
  if (deps.localFileDeps && deps.localFileDeps.length > 0) {
1303
1307
  const addedDirs = new Set();
1304
1308
  // Filter out local deps that are subdirectories of already-added libs
1305
- const filteredLocalDeps = deps.localFileDeps.filter(localDep => {
1309
+ const filteredLocalDeps = deps.localFileDeps.filter((localDep) => {
1306
1310
  // Check if this local dep is inside any of the lib paths
1307
1311
  for (const libPath of deps.libPaths) {
1308
1312
  if (localDep.startsWith(libPath + '/') || localDep === libPath) {
@@ -1335,7 +1339,8 @@ async function zipWorkspaceProject(projectName, workspaceCtx, isUpdate = false,
1335
1339
  }
1336
1340
  else if (stat.isDirectory()) {
1337
1341
  // Skip if this directory or a parent is already in libPaths
1338
- const alreadyCovered = deps.libPaths.some(libPath => localDep.startsWith(libPath + '/') || libPath.startsWith(localDep + '/'));
1342
+ const alreadyCovered = deps.libPaths.some((libPath) => localDep.startsWith(libPath + '/') ||
1343
+ libPath.startsWith(localDep + '/'));
1339
1344
  if (alreadyCovered) {
1340
1345
  if (verbose) {
1341
1346
  console.log(` - ${localDep} (skipped, overlaps with libPaths)`);
@@ -1369,7 +1374,7 @@ async function zipWorkspaceProject(projectName, workspaceCtx, isUpdate = false,
1369
1374
  export async function build(cliPlatformArg, cliConfigurationArg, cliDistributionArg, verbose = false, options // string for backwards compatibility with old projectArg
1370
1375
  ) {
1371
1376
  // Normalize options - support both new object and legacy string projectArg
1372
- const opts = typeof options === 'string' ? { project: options } : (options || {});
1377
+ const opts = typeof options === 'string' ? { project: options } : options || {};
1373
1378
  ensureInitialized();
1374
1379
  try {
1375
1380
  await ensureOrgSelected({
@@ -1407,9 +1412,11 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
1407
1412
  logWorkspaceContext(workspaceCtx, verbose);
1408
1413
  }
1409
1414
  // 1. Get project info - use workspaceCtx.projectName for Nx workspaces (from project.json)
1410
- const projectName = workspaceCtx.projectName || await getProjectName();
1415
+ const projectName = workspaceCtx.projectName || (await getProjectName());
1411
1416
  // 2. Determine platform (CLI arg preferred, then config, otherwise prompt)
1412
- let platform = (cliPlatformArg || norrixConfig.defaultPlatform || '').toLowerCase();
1417
+ let platform = (cliPlatformArg ||
1418
+ norrixConfig.defaultPlatform ||
1419
+ '').toLowerCase();
1413
1420
  const validPlatforms = ['android', 'ios', 'visionos'];
1414
1421
  spinner.stop();
1415
1422
  if (!validPlatforms.includes(platform)) {
@@ -1428,7 +1435,9 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
1428
1435
  }
1429
1436
  // 2.1 Determine configuration (CLI arg preferred, then config, otherwise prompt)
1430
1437
  const validConfigurations = ['debug', 'release'];
1431
- let configuration = (cliConfigurationArg || norrixConfig.defaultConfiguration || '').toLowerCase();
1438
+ let configuration = (cliConfigurationArg ||
1439
+ norrixConfig.defaultConfiguration ||
1440
+ '').toLowerCase();
1432
1441
  if (!validConfigurations.includes(configuration)) {
1433
1442
  if (opts.nonInteractive) {
1434
1443
  // Default to 'debug' in non-interactive mode if not specified
@@ -1448,9 +1457,11 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
1448
1457
  configuration = answer.configuration;
1449
1458
  }
1450
1459
  }
1451
- // 2.2 Determine Nx configuration for workspace builds (CLI arg preferred, then prompt if available)
1452
- let nxConfiguration = opts.nxConfiguration;
1453
- if (!nxConfiguration && workspaceCtx.type === 'nx' && !opts.nonInteractive) {
1460
+ // 2.2 Determine configuration for workspace builds (CLI arg preferred, then prompt if available)
1461
+ let configurationTarget = opts.configuration;
1462
+ if (!configurationTarget &&
1463
+ workspaceCtx.type === 'nx' &&
1464
+ !opts.nonInteractive) {
1454
1465
  const nxConfigs = detectNxBuildConfigurations(workspaceCtx.appRoot);
1455
1466
  if (nxConfigs && nxConfigs.configurations.length > 0) {
1456
1467
  const choices = nxConfigs.configurations.map((c) => ({
@@ -1459,20 +1470,20 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
1459
1470
  }));
1460
1471
  // Add option to skip/use default
1461
1472
  choices.unshift({ name: '(none - use defaults)', value: '' });
1462
- const { chosenNxConfig } = await inquirer.prompt([
1473
+ const { chosenConfig } = await inquirer.prompt([
1463
1474
  {
1464
1475
  type: 'list',
1465
- name: 'chosenNxConfig',
1466
- message: 'Nx build configuration (environment):',
1476
+ name: 'chosenConfig',
1477
+ message: 'Configuration (deployment target):',
1467
1478
  choices,
1468
1479
  default: nxConfigs.defaultConfiguration || '',
1469
1480
  },
1470
1481
  ]);
1471
- nxConfiguration = chosenNxConfig || undefined;
1482
+ configurationTarget = chosenConfig || undefined;
1472
1483
  }
1473
1484
  }
1474
- // Store resolved nxConfiguration back to opts for later use
1475
- opts.nxConfiguration = nxConfiguration;
1485
+ // Store resolved configuration back to opts for later use
1486
+ opts.configuration = configurationTarget;
1476
1487
  const normalizeIosDistribution = (input) => {
1477
1488
  const v = String(input ?? '')
1478
1489
  .trim()
@@ -1799,11 +1810,18 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
1799
1810
  // Resolve values from: CLI flags > config file > environment variables
1800
1811
  const configKeystorePath = norrixConfig.android?.keystorePath;
1801
1812
  const configKeyAlias = norrixConfig.android?.keyAlias;
1802
- const resolvedKeystorePath = trimString(opts.keystorePath) || configKeystorePath || process.env.KEYSTORE_PATH;
1813
+ const resolvedKeystorePath = trimString(opts.keystorePath) ||
1814
+ configKeystorePath ||
1815
+ process.env.KEYSTORE_PATH;
1803
1816
  const resolvedKeystorePassword = trimString(opts.keystorePassword) || process.env.KEYSTORE_PASSWORD;
1804
- const resolvedKeyAlias = trimString(opts.keyAlias) || configKeyAlias || process.env.KEYSTORE_ALIAS;
1805
- const resolvedKeyPassword = trimString(opts.keyPassword) || process.env.KEYSTORE_ALIAS_PASSWORD || process.env.KEY_PASSWORD;
1806
- const resolvedPlayJsonPath = trimString(opts.playJsonPath) || process.env.PLAY_SERVICE_ACCOUNT_JSON_PATH;
1817
+ const resolvedKeyAlias = trimString(opts.keyAlias) ||
1818
+ configKeyAlias ||
1819
+ process.env.KEYSTORE_ALIAS;
1820
+ const resolvedKeyPassword = trimString(opts.keyPassword) ||
1821
+ process.env.KEYSTORE_ALIAS_PASSWORD ||
1822
+ process.env.KEY_PASSWORD;
1823
+ const resolvedPlayJsonPath = trimString(opts.playJsonPath) ||
1824
+ process.env.PLAY_SERVICE_ACCOUNT_JSON_PATH;
1807
1825
  if (opts.nonInteractive) {
1808
1826
  // Non-interactive mode: use CLI flags, config, and env vars only
1809
1827
  // All values are optional - the cloud build will use org-level credentials if not provided
@@ -1979,17 +1997,17 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
1979
1997
  }
1980
1998
  }
1981
1999
  }
1982
- // Embed nxConfiguration in native resources (Info.plist for iOS, app.gradle for Android)
2000
+ // Embed configuration in native resources (Info.plist for iOS, app.gradle for Android)
1983
2001
  // This ensures the client SDK can match OTA updates to the correct environment (stg, prod, etc.)
1984
- // Apps with the same bundle ID but different nxConfigurations will receive different OTA updates.
1985
- if (opts.nxConfiguration) {
1986
- const nxConfigEmbedPaths = embedNxConfigurationInNativeResources(projectRoot, opts.nxConfiguration, platform);
2002
+ // Apps with the same bundle ID but different configurations will receive different OTA updates.
2003
+ if (opts.configuration) {
2004
+ const configEmbedPaths = embedNxConfigurationInNativeResources(projectRoot, opts.configuration, platform);
1987
2005
  if (verbose) {
1988
- if (nxConfigEmbedPaths.infoPlistPath) {
1989
- console.log(`[nxConfiguration] Embedded '${opts.nxConfiguration}' in Info.plist: ${nxConfigEmbedPaths.infoPlistPath}`);
2006
+ if (configEmbedPaths.infoPlistPath) {
2007
+ console.log(`[configuration] Embedded '${opts.configuration}' in Info.plist: ${configEmbedPaths.infoPlistPath}`);
1990
2008
  }
1991
- if (nxConfigEmbedPaths.appGradlePath) {
1992
- console.log(`[nxConfiguration] Embedded '${opts.nxConfiguration}' in app.gradle: ${nxConfigEmbedPaths.appGradlePath}`);
2009
+ if (configEmbedPaths.appGradlePath) {
2010
+ console.log(`[configuration] Embedded '${opts.configuration}' in app.gradle: ${configEmbedPaths.appGradlePath}`);
1993
2011
  }
1994
2012
  }
1995
2013
  }
@@ -2011,7 +2029,9 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
2011
2029
  // Returns the actual S3 key used (may differ when using presigned URLs)
2012
2030
  const actualS3Key = await putObjectToStorage(defaultS3Key, fileBuffer, projectName, platform, 'build', verbose);
2013
2031
  // Extract relative key (without public/ prefix if present) for the API
2014
- const s3KeyRel = actualS3Key.startsWith('public/') ? actualS3Key.slice(7) : actualS3Key;
2032
+ const s3KeyRel = actualS3Key.startsWith('public/')
2033
+ ? actualS3Key.slice(7)
2034
+ : actualS3Key;
2015
2035
  spinner.text = 'Upload complete. Starting build...';
2016
2036
  // 5. Call the Next.js API with the S3 key and build parameters
2017
2037
  // 5. Call the Next.js API with the S3 key, build parameters, and signing data (release only)
@@ -2031,15 +2051,17 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
2031
2051
  inferredAppId = undefined;
2032
2052
  }
2033
2053
  // Include workspace info for CI to properly navigate the project structure
2034
- const workspaceInfo = workspaceContext.type === 'nx' ? {
2035
- workspaceType: workspaceContext.type,
2036
- appPath: workspaceContext.relativeAppPath,
2037
- projectName: workspaceContext.projectName,
2038
- } : undefined;
2054
+ const workspaceInfo = workspaceContext.type === 'nx'
2055
+ ? {
2056
+ workspaceType: workspaceContext.type,
2057
+ appPath: workspaceContext.relativeAppPath,
2058
+ projectName: workspaceContext.projectName,
2059
+ }
2060
+ : undefined;
2039
2061
  // For standalone projects, use the project option or infer from app ID/package name
2040
2062
  // This allows env vars to be scoped to specific standalone projects within an org
2041
2063
  const standaloneProjectName = workspaceContext.type === 'standalone'
2042
- ? (opts.project || inferredAppId || projectName)
2064
+ ? opts.project || inferredAppId || projectName
2043
2065
  : undefined;
2044
2066
  const response = await axios.post(`${API_URL}/build`, {
2045
2067
  projectName,
@@ -2049,19 +2071,23 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
2049
2071
  verbose: Boolean(verbose),
2050
2072
  version: version || '',
2051
2073
  buildNumber: buildNumber || '',
2052
- configuration,
2053
- // Nx configuration (e.g., 'prod', 'stg', 'dev') for monorepo builds
2054
- ...(opts.nxConfiguration ? { nxConfiguration: opts.nxConfiguration } : {}),
2074
+ buildType: configuration,
2075
+ // Configuration/deployment target (e.g., 'prod', 'stg', 'dev')
2076
+ ...(opts.configuration ? { configuration: opts.configuration } : {}),
2055
2077
  ...(distributionType ? { distributionType } : {}),
2056
2078
  // Android package type override (apk or aab) - takes precedence over distributionType
2057
- ...(opts.androidPackageType ? { androidPackageType: opts.androidPackageType } : {}),
2079
+ ...(opts.androidPackageType
2080
+ ? { androidPackageType: opts.androidPackageType }
2081
+ : {}),
2058
2082
  fingerprint,
2059
2083
  // Provide the relative key (without public/) – the workflow prepends public/
2060
2084
  s3Key: s3KeyRel,
2061
2085
  // Workspace context for Nx monorepos
2062
2086
  ...(workspaceInfo ? { workspace: workspaceInfo } : {}),
2063
2087
  // For standalone projects, include project name for env var scoping
2064
- ...(standaloneProjectName ? { projectName: standaloneProjectName } : {}),
2088
+ ...(standaloneProjectName
2089
+ ? { projectName: standaloneProjectName }
2090
+ : {}),
2065
2091
  // Only include raw credentials if not encrypted
2066
2092
  ...(encryptedSecrets ? { encryptedSecrets } : {}),
2067
2093
  ...(!encryptedSecrets && iosCredentials ? { iosCredentials } : {}),
@@ -2092,7 +2118,8 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
2092
2118
  headers: await getAuthHeaders(),
2093
2119
  });
2094
2120
  if (buildRes?.data?.version) {
2095
- recordedVersion = String(buildRes.data.version).trim() || recordedVersion;
2121
+ recordedVersion =
2122
+ String(buildRes.data.version).trim() || recordedVersion;
2096
2123
  }
2097
2124
  if (buildRes?.data?.buildNumber) {
2098
2125
  recordedBuildNumber =
@@ -2383,7 +2410,10 @@ export async function submit(cliPlatformArg, cliTrackArg, verbose = false, optio
2383
2410
  ]);
2384
2411
  // Build iOS credentials if provided
2385
2412
  const ascKeyPath = normalizePath(iosConfig.apiKeyPath);
2386
- if (iosConfig.ascKeyId && iosConfig.ascIssuerId && ascKeyPath && fs.existsSync(ascKeyPath)) {
2413
+ if (iosConfig.ascKeyId &&
2414
+ iosConfig.ascIssuerId &&
2415
+ ascKeyPath &&
2416
+ fs.existsSync(ascKeyPath)) {
2387
2417
  const ascPrivateKeyContent = fs.readFileSync(ascKeyPath, 'utf8');
2388
2418
  const ascPrivateKeyBase64 = Buffer.from(ascPrivateKeyContent).toString('base64');
2389
2419
  credentials.iosCredentials = {
@@ -2541,7 +2571,7 @@ export async function submit(cliPlatformArg, cliTrackArg, verbose = false, optio
2541
2571
  export async function update(cliPlatformArg, cliVersionArg, verbose = false, options // string for backwards compatibility with old projectArg
2542
2572
  ) {
2543
2573
  // Normalize options - support both new object and legacy string projectArg
2544
- const opts = typeof options === 'string' ? { project: options } : (options || {});
2574
+ const opts = typeof options === 'string' ? { project: options } : options || {};
2545
2575
  ensureInitialized();
2546
2576
  try {
2547
2577
  await ensureOrgSelected({
@@ -2596,9 +2626,11 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false, opt
2596
2626
  platform = chosenPlatform;
2597
2627
  spinner.start('Preparing over-the-air update...');
2598
2628
  }
2599
- // Determine Nx configuration for workspace builds (CLI arg preferred, then prompt if available)
2600
- let nxConfiguration = opts.nxConfiguration;
2601
- if (!nxConfiguration && workspaceCtx.type === 'nx' && !opts.nonInteractive) {
2629
+ // Determine configuration for workspace builds (CLI arg preferred, then prompt if available)
2630
+ let configurationTarget = opts.configuration;
2631
+ if (!configurationTarget &&
2632
+ workspaceCtx.type === 'nx' &&
2633
+ !opts.nonInteractive) {
2602
2634
  spinner.stop();
2603
2635
  const nxConfigs = detectNxBuildConfigurations(workspaceCtx.appRoot);
2604
2636
  if (nxConfigs && nxConfigs.configurations.length > 0) {
@@ -2608,21 +2640,21 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false, opt
2608
2640
  }));
2609
2641
  // Add option to skip/use default
2610
2642
  choices.unshift({ name: '(none - use defaults)', value: '' });
2611
- const { chosenNxConfig } = await inquirer.prompt([
2643
+ const { chosenConfig } = await inquirer.prompt([
2612
2644
  {
2613
2645
  type: 'list',
2614
- name: 'chosenNxConfig',
2615
- message: 'Nx build configuration (environment):',
2646
+ name: 'chosenConfig',
2647
+ message: 'Configuration (deployment target):',
2616
2648
  choices,
2617
2649
  default: nxConfigs.defaultConfiguration || '',
2618
2650
  },
2619
2651
  ]);
2620
- nxConfiguration = chosenNxConfig || undefined;
2652
+ configurationTarget = chosenConfig || undefined;
2621
2653
  }
2622
2654
  spinner.start('Preparing over-the-air update...');
2623
2655
  }
2624
- // Store resolved nxConfiguration back to opts for later use
2625
- opts.nxConfiguration = nxConfiguration;
2656
+ // Store resolved configuration back to opts for later use
2657
+ opts.configuration = configurationTarget;
2626
2658
  // Infer version from native project files (same as build)
2627
2659
  const appleVersionInfo = platform === 'ios' || platform === 'visionos'
2628
2660
  ? getAppleVersionFromInfoPlist(platform)
@@ -2727,7 +2759,8 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false, opt
2727
2759
  }
2728
2760
  else if (opts.nonInteractive) {
2729
2761
  // In non-interactive mode, use server suggestion or leave undefined for auto-increment
2730
- buildNumber = serverSuggestedBuildNumber || localInferredBuildNumber || undefined;
2762
+ buildNumber =
2763
+ serverSuggestedBuildNumber || localInferredBuildNumber || undefined;
2731
2764
  }
2732
2765
  else {
2733
2766
  const buildPromptAnswers = await inquirer.prompt([
@@ -2761,7 +2794,8 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false, opt
2761
2794
  default: '',
2762
2795
  },
2763
2796
  ]);
2764
- buildNumber = String(buildPromptAnswers.buildNumber || '').trim() || undefined;
2797
+ buildNumber =
2798
+ String(buildPromptAnswers.buildNumber || '').trim() || undefined;
2765
2799
  notes = buildPromptAnswers.notes || '';
2766
2800
  }
2767
2801
  // Check the app directory structure before packaging
@@ -2803,7 +2837,7 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false, opt
2803
2837
  };
2804
2838
  const hashesMatch = printFingerprintComparisonDetails(`build ${latestBuild.id}`, baseFp, 'local project', localFp);
2805
2839
  if (!hashesMatch) {
2806
- console.log("These changes cannot be delivered via OTA and will require users to update through the store.");
2840
+ console.log('These changes cannot be delivered via OTA and will require users to update through the store.');
2807
2841
  // In non-interactive mode, proceed automatically (CI pipelines should control this via version matching)
2808
2842
  if (opts.nonInteractive) {
2809
2843
  console.log('Non-interactive mode: Proceeding with OTA update despite fingerprint mismatch.');
@@ -2848,9 +2882,9 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false, opt
2848
2882
  spinner.start('Packaging for over-the-air update...');
2849
2883
  // Create the update bundle (workspace-aware) - pass true to include node_modules for updates
2850
2884
  // Use workspaceCtx.projectName for Nx workspaces (from project.json)
2851
- const projectName = workspaceCtx?.projectName || await getProjectName();
2885
+ const projectName = workspaceCtx?.projectName || (await getProjectName());
2852
2886
  const { zipPath, workspaceContext } = await zipProject(projectName, workspaceCtx, true, verbose);
2853
- spinner.text = 'Uploading update to Norrix cloud storage...';
2887
+ spinner.text = 'Preparing...';
2854
2888
  const fileBuffer = fs.readFileSync(zipPath);
2855
2889
  const updateFolder = `update-${Date.now()}`;
2856
2890
  const safeVersion = String(version || '').replace(/\s+/g, '-');
@@ -2863,18 +2897,22 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false, opt
2863
2897
  // Upload to S3 - uses presigned URL for API key auth, or direct SDK for Cognito auth
2864
2898
  const actualS3Key = await putObjectToStorage(defaultS3Key, fileBuffer, projectName, platform, 'update', verbose);
2865
2899
  // Extract relative key (without public/ prefix if present) for the API
2866
- const s3KeyRel = actualS3Key.startsWith('public/') ? actualS3Key.slice(7) : actualS3Key;
2900
+ const s3KeyRel = actualS3Key.startsWith('public/')
2901
+ ? actualS3Key.slice(7)
2902
+ : actualS3Key;
2867
2903
  spinner.text = 'Upload complete. Starting update...';
2868
2904
  // Include workspace info for CI to properly navigate the project structure
2869
- const workspaceInfo = workspaceContext.type === 'nx' ? {
2870
- workspaceType: workspaceContext.type,
2871
- appPath: workspaceContext.relativeAppPath,
2872
- projectName: workspaceContext.projectName,
2873
- } : undefined;
2905
+ const workspaceInfo = workspaceContext.type === 'nx'
2906
+ ? {
2907
+ workspaceType: workspaceContext.type,
2908
+ appPath: workspaceContext.relativeAppPath,
2909
+ projectName: workspaceContext.projectName,
2910
+ }
2911
+ : undefined;
2874
2912
  // For standalone projects, use the project option or infer from app ID
2875
2913
  // This allows env vars to be scoped to specific standalone projects within an org
2876
2914
  const standaloneProjectName = workspaceContext.type === 'standalone'
2877
- ? (opts.project || appId)
2915
+ ? opts.project || appId
2878
2916
  : undefined;
2879
2917
  const response = await axios.post(`${API_URL}/update`, {
2880
2918
  appId,
@@ -2883,14 +2921,18 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false, opt
2883
2921
  buildNumber: buildNumber || '',
2884
2922
  releaseNotes: notes,
2885
2923
  fingerprint,
2886
- // Nx configuration (e.g., 'prod', 'stg', 'dev') for monorepo builds
2887
- ...(opts.nxConfiguration ? { nxConfiguration: opts.nxConfiguration } : {}),
2924
+ // Build type: defaults to 'release' for OTA updates (production apps)
2925
+ buildType: opts.buildType || 'release',
2926
+ // Configuration/deployment target (e.g., 'prod', 'stg', 'dev')
2927
+ ...(opts.configuration ? { configuration: opts.configuration } : {}),
2888
2928
  // Provide the relative key (without public/). Consumers will prepend public/
2889
2929
  s3Key: s3KeyRel,
2890
2930
  // Workspace context for Nx monorepos
2891
2931
  ...(workspaceInfo ? { workspace: workspaceInfo } : {}),
2892
2932
  // For standalone projects, include project name for env var scoping
2893
- ...(standaloneProjectName ? { projectName: standaloneProjectName } : {}),
2933
+ ...(standaloneProjectName
2934
+ ? { projectName: standaloneProjectName }
2935
+ : {}),
2894
2936
  }, {
2895
2937
  headers: {
2896
2938
  'Content-Type': 'application/json',
@@ -2907,8 +2949,7 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false, opt
2907
2949
  let recordedUpdateVersion = String(version || '').trim() || undefined;
2908
2950
  let recordedUpdateBuildNumber = (response?.data?.buildNumber
2909
2951
  ? String(response.data.buildNumber).trim()
2910
- : undefined) ||
2911
- (buildNumber ? String(buildNumber).trim() : undefined);
2952
+ : undefined) || (buildNumber ? String(buildNumber).trim() : undefined);
2912
2953
  if (!recordedUpdateBuildNumber) {
2913
2954
  try {
2914
2955
  const updateRes = await axios.get(`${API_URL}/update/${updateId}`, {