@norrix/cli 0.0.21 → 0.0.25

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.
@@ -7,17 +7,51 @@ import * as os from 'os';
7
7
  import { fileURLToPath } from 'url';
8
8
  import archiver from 'archiver';
9
9
  // import FormData from 'form-data';
10
- import { configureAmplify } from './amplify-config.js';
10
+ import { configureAmplify, loadCliEnvFiles } from './amplify-config.js';
11
11
  import { computeFingerprint, writeRuntimeFingerprintFile } from './fingerprinting.js';
12
+ import { loadNorrixConfig, hasNorrixConfig, saveNorrixConfig } from './config.js';
13
+ import { detectWorkspaceContext, getNxProjectDependencies, getWorkspaceDependenciesFallback, createWorkspaceManifest, logWorkspaceContext, isAtWorkspaceRoot, discoverNativeScriptApps, getWorkspaceContextForApp, } from './workspace.js';
12
14
  import { signIn as amplifySignIn, signOut as amplifySignOut, getCurrentUser, fetchAuthSession, } from 'aws-amplify/auth';
13
15
  import crypto from 'crypto';
14
16
  import { Amplify } from 'aws-amplify';
15
- // Initialize Amplify once at startup
16
- configureAmplify();
17
+ import { PROD_DEFAULTS } from './prod-defaults.js';
18
+ import { DEV_DEFAULTS } from './dev-defaults.js';
19
+ let CURRENT_ENV = 'prod';
20
+ let CURRENT_DEFAULTS = PROD_DEFAULTS;
21
+ let API_URL = PROD_DEFAULTS.apiUrl;
22
+ let IS_INITIALIZED = false;
23
+ function defaultsForEnv(env) {
24
+ return env === 'dev' ? DEV_DEFAULTS : PROD_DEFAULTS;
25
+ }
26
+ /**
27
+ * Initialize runtime config for this CLI process.
28
+ *
29
+ * - Loads env files (user config always; package-root only in dev)
30
+ * - Selects dev/prod in-code defaults
31
+ * - Configures Amplify Auth/Storage
32
+ * - Computes the API base URL used by commands
33
+ */
34
+ export function initNorrixCli(env = 'prod') {
35
+ // Allow re-init when running tests or when the caller changes env.
36
+ if (IS_INITIALIZED && env === CURRENT_ENV)
37
+ return;
38
+ CURRENT_ENV = env;
39
+ loadCliEnvFiles(env);
40
+ CURRENT_DEFAULTS = defaultsForEnv(env);
41
+ configureAmplify(env);
42
+ API_URL = process.env.NORRIX_API_URL || CURRENT_DEFAULTS.apiUrl;
43
+ IS_INITIALIZED = true;
44
+ }
45
+ function ensureInitialized() {
46
+ if (!IS_INITIALIZED) {
47
+ initNorrixCli('prod');
48
+ }
49
+ }
17
50
  /**
18
51
  * Return Authorization header containing the current Cognito ID token (if signed in).
19
52
  */
20
53
  async function getAuthHeaders() {
54
+ ensureInitialized();
21
55
  try {
22
56
  const session = await fetchAuthSession();
23
57
  const idToken = session.tokens?.idToken?.toString();
@@ -41,8 +75,8 @@ async function getAuthHeaders() {
41
75
  * The Next.js API then dispatches validated build/submit/update tasks to WarpBuild
42
76
  * (GitHub Actions) for heavyweight processing.
43
77
  */
44
- // Config (will be moved to a config file/env variables later)
45
- const API_URL = process.env.NORRIX_API_URL || 'http://localhost:3000/api';
78
+ // NOTE: API_URL is initialized by initNorrixCli(). We keep a prod fallback for
79
+ // library consumers that import functions directly without running the CLI entrypoint.
46
80
  // Get dirname equivalent in ESM
47
81
  const __filename = fileURLToPath(import.meta.url);
48
82
  const __dirname = path.dirname(__filename);
@@ -95,6 +129,7 @@ function encryptSecretsIfConfigured(secrets) {
95
129
  * falls back to AWS SDK v3 in pure Node (where FileReader is unavailable).
96
130
  */
97
131
  async function putObjectToStorage(key, data) {
132
+ ensureInitialized();
98
133
  // CLI always runs in Node; use AWS SDK v3 directly
99
134
  await uploadToS3Sdk(key, data);
100
135
  }
@@ -102,10 +137,10 @@ function getS3BucketRegionFromAmplify() {
102
137
  const cfg = Amplify.getConfig?.() ?? {};
103
138
  const bucket = cfg?.Storage?.S3?.bucket ||
104
139
  process.env.NORRIX_STORAGE_BUCKET_NAME ||
105
- process.env.NEXT_PUBLIC_STORAGE_BUCKET_NAME;
140
+ process.env.NORRIX_S3_BUCKET;
106
141
  const region = cfg?.Storage?.S3?.region ||
107
142
  process.env.NORRIX_STORAGE_BUCKET_REGION ||
108
- process.env.NEXT_PUBLIC_STORAGE_BUCKET_REGION ||
143
+ process.env.NORRIX_AWS_REGION ||
109
144
  process.env.AWS_REGION ||
110
145
  process.env.AWS_DEFAULT_REGION;
111
146
  if (!bucket || !region) {
@@ -145,6 +180,7 @@ async function uploadToS3Sdk(key, data) {
145
180
  await client.send(new PutObjectCommand({ Bucket: bucket, Key: key, Body: data }));
146
181
  }
147
182
  export async function printFingerprint(cliPlatformArg, appId, verbose = false) {
183
+ ensureInitialized();
148
184
  try {
149
185
  const projectRoot = process.cwd();
150
186
  const platform = (cliPlatformArg || '').toLowerCase() || undefined;
@@ -350,6 +386,7 @@ function printFingerprintComparisonDetails(fromLabel, fromFp, toLabel, toFp) {
350
386
  return hashesMatch;
351
387
  }
352
388
  export async function compareFingerprint(fromId, toArg, verbose = false) {
389
+ ensureInitialized();
353
390
  try {
354
391
  if (!fromId) {
355
392
  throw new Error('Missing required --from <id> argument');
@@ -600,20 +637,106 @@ function getAndroidVersionFromAppGradle() {
600
637
  }
601
638
  }
602
639
  /**
603
- * Creates a zip file of the current directory (NativeScript project)
640
+ * Resolve workspace context for a command.
641
+ *
642
+ * When running from workspace root:
643
+ * - If --project is provided, use that project
644
+ * - Otherwise, discover NativeScript apps and prompt for selection
645
+ *
646
+ * When running from within an app directory:
647
+ * - Use the current directory's context
648
+ *
649
+ * @param projectArg - Optional project name from --project flag
650
+ * @param spinner - Optional spinner to stop before prompting
651
+ */
652
+ async function resolveWorkspaceContext(projectArg, spinner) {
653
+ const originalCwd = process.cwd();
654
+ // If we're at workspace root (has nx.json but no nativescript.config)
655
+ if (isAtWorkspaceRoot()) {
656
+ // If --project was provided, use it
657
+ if (projectArg) {
658
+ const ctx = getWorkspaceContextForApp(projectArg);
659
+ if (!ctx) {
660
+ const apps = discoverNativeScriptApps();
661
+ const appNames = apps.map((a) => a.name).join(', ');
662
+ throw new Error(`Project '${projectArg}' not found in workspace. Available NativeScript apps: ${appNames || 'none'}`);
663
+ }
664
+ // Change to the app directory for subsequent operations
665
+ process.chdir(ctx.appRoot);
666
+ return { workspaceContext: ctx, originalCwd };
667
+ }
668
+ // Discover NativeScript apps and prompt for selection
669
+ const apps = discoverNativeScriptApps();
670
+ if (apps.length === 0) {
671
+ throw new Error('No NativeScript apps found in this workspace. ' +
672
+ 'Run this command from within a NativeScript app directory, or ensure your apps have nativescript.config.ts files.');
673
+ }
674
+ if (apps.length === 1) {
675
+ // Only one app, use it automatically
676
+ if (spinner)
677
+ spinner.stop();
678
+ console.log(`Found NativeScript app: ${apps[0].name} (${apps[0].path})`);
679
+ const ctx = getWorkspaceContextForApp(apps[0].name);
680
+ if (ctx) {
681
+ process.chdir(ctx.appRoot);
682
+ return { workspaceContext: ctx, originalCwd };
683
+ }
684
+ }
685
+ // Multiple apps - stop spinner before prompting
686
+ if (spinner)
687
+ spinner.stop();
688
+ // Multiple apps - prompt for selection
689
+ const { selectedApp } = await inquirer.prompt([
690
+ {
691
+ type: 'list',
692
+ name: 'selectedApp',
693
+ message: 'Select a NativeScript app:',
694
+ choices: apps.map((app) => ({
695
+ name: `${app.name} (${app.path})`,
696
+ value: app.name,
697
+ })),
698
+ pageSize: 15, // Show more items to avoid scroll issues
699
+ loop: false, // Don't loop back to start when at end
700
+ },
701
+ ]);
702
+ const ctx = getWorkspaceContextForApp(selectedApp);
703
+ if (!ctx) {
704
+ throw new Error(`Failed to get context for selected app: ${selectedApp}`);
705
+ }
706
+ process.chdir(ctx.appRoot);
707
+ return { workspaceContext: ctx, originalCwd };
708
+ }
709
+ // Not at workspace root - use current directory
710
+ const ctx = detectWorkspaceContext();
711
+ return { workspaceContext: ctx, originalCwd };
712
+ }
713
+ /**
714
+ * Creates a zip file of the current directory (NativeScript project).
715
+ * For Nx workspaces, this includes the app, dependent libs, and workspace config files.
716
+ * For standalone projects, this zips the current directory.
717
+ */
718
+ async function zipProject(projectName, isUpdate = false, verbose = false) {
719
+ const workspaceCtx = detectWorkspaceContext();
720
+ if (workspaceCtx.type === 'nx') {
721
+ return zipWorkspaceProject(projectName, workspaceCtx, isUpdate, verbose);
722
+ }
723
+ else {
724
+ return zipStandaloneProject(projectName, workspaceCtx, isUpdate);
725
+ }
726
+ }
727
+ /**
728
+ * Zip a standalone NativeScript project (original behavior)
604
729
  */
605
- async function zipProject(projectName, isUpdate = false) {
730
+ async function zipStandaloneProject(projectName, workspaceCtx, isUpdate = false) {
606
731
  return new Promise((resolve, reject) => {
607
732
  const outputPath = path.join(process.cwd(), `${projectName}.zip`);
608
733
  const output = fs.createWriteStream(outputPath);
609
734
  const archive = archiver('zip', {
610
- zlib: { level: 9 }, // Compression level
735
+ zlib: { level: 9 },
611
736
  });
612
- // Listen for all archive data to be written
613
737
  output.on('close', () => {
614
- resolve(outputPath);
738
+ resolve({ zipPath: outputPath, workspaceContext: workspaceCtx });
615
739
  });
616
- // Listen for warnings and errors
617
740
  archive.on('warning', (err) => {
618
741
  if (err.code === 'ENOENT') {
619
742
  console.warn('Archive warning:', err);
@@ -625,9 +748,8 @@ async function zipProject(projectName, isUpdate = false) {
625
748
  archive.on('error', (err) => {
626
749
  reject(err);
627
750
  });
628
- // Pipe archive data to the file
629
751
  archive.pipe(output);
630
- // Determine the primary app directory (prefer appPath from NativeScript config)
752
+ // Determine the primary app directory
631
753
  const nsAppPath = getNativeScriptAppPath();
632
754
  const nsAppDir = nsAppPath
633
755
  ? path.join(process.cwd(), nsAppPath)
@@ -647,18 +769,12 @@ async function zipProject(projectName, isUpdate = false) {
647
769
  }
648
770
  else {
649
771
  console.warn('Warning: app directory not found in the project root');
650
- const checked = [nsAppDir, srcDir, appDir].filter(Boolean).join(', ');
651
- console.warn(`Checked locations: ${checked}`);
652
- console.log('Creating an empty app directory in the zip');
653
772
  }
654
- // For both builds/updates, include App_Resources and exclude node_modules.
655
- // For OTA updates specifically, we also exclude the runtime fingerprint
656
- // file, since it reflects the native store binary, not the OTA payload.
657
773
  const ignorePatterns = [
658
- 'node_modules/**', // Always exclude node_modules
659
- '*.zip', // Exclude existing zip files
660
- 'platforms/**', // Exclude platform-specific directories
661
- 'hooks/**', // Exclude hooks directory
774
+ 'node_modules/**',
775
+ '*.zip',
776
+ 'platforms/**',
777
+ 'hooks/**',
662
778
  ];
663
779
  if (isUpdate) {
664
780
  ignorePatterns.push('**/assets/norrix.fingerprint.json');
@@ -667,7 +783,132 @@ async function zipProject(projectName, isUpdate = false) {
667
783
  cwd: process.cwd(),
668
784
  ignore: ignorePatterns,
669
785
  });
670
- // Finalize the archive
786
+ archive.finalize();
787
+ });
788
+ }
789
+ /**
790
+ * Zip an Nx workspace project including the app and its dependencies
791
+ */
792
+ async function zipWorkspaceProject(projectName, workspaceCtx, isUpdate = false, verbose = false) {
793
+ return new Promise((resolve, reject) => {
794
+ const outputPath = path.join(workspaceCtx.appRoot, `${projectName}.zip`);
795
+ const output = fs.createWriteStream(outputPath);
796
+ const archive = archiver('zip', {
797
+ zlib: { level: 9 },
798
+ });
799
+ output.on('close', () => {
800
+ resolve({ zipPath: outputPath, workspaceContext: workspaceCtx });
801
+ });
802
+ archive.on('warning', (err) => {
803
+ if (err.code === 'ENOENT') {
804
+ if (verbose)
805
+ console.warn('Archive warning:', err);
806
+ }
807
+ else {
808
+ reject(err);
809
+ }
810
+ });
811
+ archive.on('error', (err) => {
812
+ reject(err);
813
+ });
814
+ archive.pipe(output);
815
+ logWorkspaceContext(workspaceCtx, verbose);
816
+ // Get workspace dependencies using Nx CLI (preferred) or fallback
817
+ let deps;
818
+ if (workspaceCtx.projectName) {
819
+ deps = getNxProjectDependencies(workspaceCtx.projectName, workspaceCtx.workspaceRoot, verbose);
820
+ }
821
+ if (!deps) {
822
+ if (verbose) {
823
+ console.log('[workspace] Using fallback dependency detection');
824
+ }
825
+ deps = getWorkspaceDependenciesFallback(workspaceCtx, verbose);
826
+ }
827
+ // Create manifest for CI
828
+ const manifest = createWorkspaceManifest(workspaceCtx, deps);
829
+ archive.append(JSON.stringify(manifest, null, 2), {
830
+ name: '.norrix/manifest.json',
831
+ });
832
+ // Base ignore patterns for the entire workspace
833
+ const ignorePatterns = [
834
+ '**/node_modules/**',
835
+ '**/*.zip',
836
+ '**/platforms/**',
837
+ '**/dist/**',
838
+ '**/.git/**',
839
+ '**/hooks/**',
840
+ // Exclude other apps (not the current one)
841
+ 'apps/**',
842
+ ];
843
+ if (isUpdate) {
844
+ ignorePatterns.push('**/assets/norrix.fingerprint.json');
845
+ }
846
+ // 1. Add the app itself at its relative workspace path
847
+ console.log(`Adding app: ${workspaceCtx.relativeAppPath}`);
848
+ archive.directory(workspaceCtx.appRoot, workspaceCtx.relativeAppPath, (entry) => {
849
+ // Filter out node_modules, platforms, etc.
850
+ if (entry.name.includes('node_modules') ||
851
+ entry.name.includes('platforms') ||
852
+ entry.name.endsWith('.zip')) {
853
+ return false;
854
+ }
855
+ if (isUpdate && entry.name.includes('norrix.fingerprint.json')) {
856
+ return false;
857
+ }
858
+ return entry;
859
+ });
860
+ // 2. Add dependent libs
861
+ if (deps.libPaths.length > 0) {
862
+ console.log(`Adding ${deps.libPaths.length} library dependencies`);
863
+ for (const libPath of deps.libPaths) {
864
+ const absoluteLibPath = path.join(workspaceCtx.workspaceRoot, libPath);
865
+ if (fs.existsSync(absoluteLibPath)) {
866
+ if (verbose) {
867
+ console.log(` - ${libPath}`);
868
+ }
869
+ archive.directory(absoluteLibPath, libPath, (entry) => {
870
+ if (entry.name.includes('node_modules')) {
871
+ return false;
872
+ }
873
+ return entry;
874
+ });
875
+ }
876
+ }
877
+ }
878
+ // 3. Add root config files
879
+ console.log('Adding workspace root configuration files');
880
+ for (const configFile of deps.rootConfigs) {
881
+ const configPath = path.join(workspaceCtx.workspaceRoot, configFile);
882
+ if (fs.existsSync(configPath)) {
883
+ if (verbose) {
884
+ console.log(` - ${configFile}`);
885
+ }
886
+ archive.file(configPath, { name: configFile });
887
+ }
888
+ }
889
+ // 4. Add tools directory if it exists and is referenced
890
+ for (const toolPath of deps.toolPaths) {
891
+ const absoluteToolPath = path.join(workspaceCtx.workspaceRoot, toolPath);
892
+ if (fs.existsSync(absoluteToolPath) && fs.statSync(absoluteToolPath).isDirectory()) {
893
+ console.log(`Adding tools: ${toolPath}`);
894
+ archive.directory(absoluteToolPath, toolPath, (entry) => {
895
+ if (entry.name.includes('node_modules')) {
896
+ return false;
897
+ }
898
+ return entry;
899
+ });
900
+ }
901
+ }
902
+ // 5. Add asset paths if they exist
903
+ for (const assetPath of deps.assetPaths) {
904
+ const absoluteAssetPath = path.join(workspaceCtx.workspaceRoot, assetPath);
905
+ if (fs.existsSync(absoluteAssetPath) && fs.statSync(absoluteAssetPath).isDirectory()) {
906
+ if (verbose) {
907
+ console.log(`Adding assets: ${assetPath}`);
908
+ }
909
+ archive.directory(absoluteAssetPath, assetPath);
910
+ }
911
+ }
671
912
  archive.finalize();
672
913
  });
673
914
  }
@@ -675,15 +916,33 @@ async function zipProject(projectName, isUpdate = false) {
675
916
  * Build command implementation
676
917
  * Uploads project to S3 and triggers build via the Next.js API gateway -> WarpBuild
677
918
  */
678
- export async function build(cliPlatformArg, cliConfigurationArg, verbose = false) {
919
+ export async function build(cliPlatformArg, cliConfigurationArg, cliDistributionArg, verbose = false, options // string for backwards compatibility with old projectArg
920
+ ) {
921
+ // Normalize options - support both new object and legacy string projectArg
922
+ const opts = typeof options === 'string' ? { project: options } : (options || {});
923
+ ensureInitialized();
679
924
  let spinner;
925
+ let originalCwd;
680
926
  try {
681
927
  spinner = ora('Preparing app for building...');
682
928
  spinner.start();
929
+ // 0. Resolve workspace context (handles --project and prompting)
930
+ const resolved = await resolveWorkspaceContext(opts.project, spinner);
931
+ originalCwd = resolved.originalCwd;
932
+ const workspaceCtx = resolved.workspaceContext;
933
+ // Restart spinner after potential prompts
934
+ if (!spinner.isSpinning) {
935
+ spinner.start('Preparing app for building...');
936
+ }
937
+ // Load Norrix config file if present
938
+ const norrixConfig = await loadNorrixConfig(process.cwd());
939
+ if (workspaceCtx.type === 'nx' && verbose) {
940
+ logWorkspaceContext(workspaceCtx, verbose);
941
+ }
683
942
  // 1. Get project info
684
943
  const projectName = await getProjectName();
685
- // 2. Determine platform (CLI arg preferred, otherwise prompt)
686
- let platform = (cliPlatformArg || '').toLowerCase();
944
+ // 2. Determine platform (CLI arg preferred, then config, otherwise prompt)
945
+ let platform = (cliPlatformArg || norrixConfig.defaultPlatform || '').toLowerCase();
687
946
  const validPlatforms = ['android', 'ios', 'visionos'];
688
947
  spinner.stop();
689
948
  if (!validPlatforms.includes(platform)) {
@@ -712,6 +971,64 @@ export async function build(cliPlatformArg, cliConfigurationArg, verbose = false
712
971
  ]);
713
972
  configuration = answer.configuration;
714
973
  }
974
+ const normalizeIosDistribution = (input) => {
975
+ const v = String(input ?? '')
976
+ .trim()
977
+ .toLowerCase();
978
+ if (!v)
979
+ return undefined;
980
+ if (v === 'enterprise' ||
981
+ v === 'inhouse' ||
982
+ v === 'in-house' ||
983
+ v === 'in_house') {
984
+ return 'enterprise';
985
+ }
986
+ if (v === 'adhoc' || v === 'ad-hoc' || v === 'ad_hoc' || v === 'ad hoc') {
987
+ return 'adhoc';
988
+ }
989
+ if (v === 'appstore' ||
990
+ v === 'app-store' ||
991
+ v === 'app store' ||
992
+ v === 'store' ||
993
+ v === 'app_store') {
994
+ return 'appstore';
995
+ }
996
+ return undefined;
997
+ };
998
+ // 2.2 iOS distribution type (release only): CLI arg > config file > prompt
999
+ let distributionType;
1000
+ if (platform === 'ios' && configuration === 'release') {
1001
+ // Try CLI arg first
1002
+ distributionType = normalizeIosDistribution(cliDistributionArg);
1003
+ if (!distributionType && cliDistributionArg) {
1004
+ throw new Error(`Invalid iOS distribution type '${cliDistributionArg}'. Use 'appstore', 'adhoc', or 'enterprise'.`);
1005
+ }
1006
+ // Fall back to config file
1007
+ if (!distributionType && norrixConfig.ios?.distributionType) {
1008
+ distributionType = norrixConfig.ios.distributionType;
1009
+ }
1010
+ // Prompt if still not set (unless non-interactive mode)
1011
+ if (!distributionType && !opts.nonInteractive) {
1012
+ const { distribution } = await inquirer.prompt([
1013
+ {
1014
+ type: 'list',
1015
+ name: 'distribution',
1016
+ message: 'Distribution Type:',
1017
+ choices: [
1018
+ { name: 'App Store', value: 'appstore' },
1019
+ { name: 'Ad Hoc', value: 'adhoc' },
1020
+ { name: 'Enterprise', value: 'enterprise' },
1021
+ ],
1022
+ default: 'appstore',
1023
+ },
1024
+ ]);
1025
+ distributionType = distribution;
1026
+ }
1027
+ // Default to appstore in non-interactive mode if nothing else provided
1028
+ if (!distributionType) {
1029
+ distributionType = 'appstore';
1030
+ }
1031
+ }
715
1032
  const appleVersionInfo = platform === 'ios' || platform === 'visionos'
716
1033
  ? getAppleVersionFromInfoPlist(platform)
717
1034
  : {};
@@ -781,7 +1098,31 @@ export async function build(cliPlatformArg, cliConfigurationArg, verbose = false
781
1098
  if (configuration === 'release') {
782
1099
  spinner.stop();
783
1100
  if (platform === 'ios') {
1101
+ // Determine Team ID from: CLI flag > config file > prompt
1102
+ const configTeamId = norrixConfig.ios?.teamId;
1103
+ const resolvedTeamId = opts.teamId || configTeamId;
1104
+ // If we have teamId from CLI or config, and non-interactive mode, skip prompts
1105
+ const shouldPromptForTeamId = !resolvedTeamId && !opts.nonInteractive;
784
1106
  const iosAnswers = await inquirer.prompt([
1107
+ {
1108
+ type: 'input',
1109
+ name: 'teamId',
1110
+ message: 'Apple Developer Team ID (required for code signing, e.g. "ABC123XYZ"):',
1111
+ default: resolvedTeamId || '',
1112
+ when: () => shouldPromptForTeamId || !resolvedTeamId,
1113
+ validate: (input) => {
1114
+ // Team ID is optional if user provides their own p12/profile
1115
+ // but we'll strongly recommend it
1116
+ if (!input.trim()) {
1117
+ return true; // Allow empty, workflow will try to proceed without it
1118
+ }
1119
+ // Basic validation: Apple Team IDs are typically 10 alphanumeric chars
1120
+ if (/^[A-Z0-9]{10}$/.test(input.trim())) {
1121
+ return true;
1122
+ }
1123
+ return 'Team ID should be 10 alphanumeric characters (e.g. "ABC123XYZ"). Leave empty to skip.';
1124
+ },
1125
+ },
785
1126
  {
786
1127
  type: 'input',
787
1128
  name: 'p12Path',
@@ -804,7 +1145,7 @@ export async function build(cliPlatformArg, cliConfigurationArg, verbose = false
804
1145
  {
805
1146
  type: 'confirm',
806
1147
  name: 'hasAscKey',
807
- message: 'Provide App Store Connect API Key? (optional, for App Store operations)',
1148
+ message: 'Provide App Store Connect API Key? (optional, for auto-provisioning)',
808
1149
  default: false,
809
1150
  },
810
1151
  {
@@ -828,23 +1169,24 @@ export async function build(cliPlatformArg, cliConfigurationArg, verbose = false
828
1169
  default: '',
829
1170
  when: (a) => a.hasAscKey,
830
1171
  },
831
- {
832
- type: 'input',
833
- name: 'ascTeamId',
834
- message: 'Apple Developer Team ID (optional, required for API-key provisioning):',
835
- default: '',
836
- when: (a) => a.hasAscKey,
837
- },
838
1172
  ]);
1173
+ // Use resolved teamId from CLI/config, or from prompt
1174
+ const finalTeamId = resolvedTeamId || iosAnswers.teamId?.trim();
839
1175
  iosCredentials = {
1176
+ teamId: finalTeamId || undefined,
840
1177
  p12Base64: readOptionalFileAsBase64(iosAnswers.p12Path),
841
1178
  p12Password: iosAnswers.p12Password || undefined,
842
1179
  mobileprovisionBase64: readOptionalFileAsBase64(iosAnswers.mobileprovisionPath),
843
1180
  ascApiKeyId: iosAnswers.ascApiKeyId || undefined,
844
1181
  ascIssuerId: iosAnswers.ascIssuerId || undefined,
845
1182
  ascPrivateKey: readOptionalFileAsBase64(iosAnswers.ascPrivateKeyPath),
846
- ascTeamId: iosAnswers.ascTeamId || undefined,
1183
+ // Track paths for config saving (not sent to API)
1184
+ _p12Path: iosAnswers.p12Path || undefined,
1185
+ _mobileprovisionPath: iosAnswers.mobileprovisionPath || undefined,
847
1186
  };
1187
+ if (finalTeamId) {
1188
+ console.log(`Using Apple Team ID: ${finalTeamId}`);
1189
+ }
848
1190
  }
849
1191
  else if (platform === 'android') {
850
1192
  const androidAnswers = await inquirer.prompt([
@@ -895,6 +1237,66 @@ export async function build(cliPlatformArg, cliConfigurationArg, verbose = false
895
1237
  keyPassword: androidAnswers.keyPassword || undefined,
896
1238
  playServiceAccountJson: readOptionalFileAsBase64(androidAnswers.playJsonPath),
897
1239
  };
1240
+ // Track Android paths for config saving
1241
+ androidCredentials._keystorePath = androidAnswers.keystorePath || undefined;
1242
+ androidCredentials._keyAlias = androidAnswers.keyAlias || undefined;
1243
+ }
1244
+ // Offer to save config if no norrix.config.ts exists and we collected useful values
1245
+ const appRoot = process.cwd();
1246
+ if (!hasNorrixConfig(appRoot) && !opts.nonInteractive) {
1247
+ // Collect saveable values
1248
+ const saveableOptions = {
1249
+ platform: platform,
1250
+ };
1251
+ if (platform === 'ios' && iosCredentials) {
1252
+ if (iosCredentials.teamId) {
1253
+ saveableOptions.teamId = iosCredentials.teamId;
1254
+ }
1255
+ if (distributionType) {
1256
+ saveableOptions.distributionType = distributionType;
1257
+ }
1258
+ // Don't save actual credential file paths since they may contain secrets
1259
+ // But save paths that users can re-use
1260
+ if (iosCredentials._p12Path) {
1261
+ saveableOptions.p12Path = iosCredentials._p12Path;
1262
+ }
1263
+ if (iosCredentials._mobileprovisionPath) {
1264
+ saveableOptions.provisioningProfilePath = iosCredentials._mobileprovisionPath;
1265
+ }
1266
+ }
1267
+ if (platform === 'android' && androidCredentials) {
1268
+ if (androidCredentials._keystorePath) {
1269
+ saveableOptions.keystorePath = androidCredentials._keystorePath;
1270
+ }
1271
+ if (androidCredentials._keyAlias) {
1272
+ saveableOptions.keyAlias = androidCredentials._keyAlias;
1273
+ }
1274
+ }
1275
+ // Only offer to save if we have something useful
1276
+ const hasSaveableValues = saveableOptions.teamId ||
1277
+ saveableOptions.distributionType ||
1278
+ saveableOptions.p12Path ||
1279
+ saveableOptions.provisioningProfilePath ||
1280
+ saveableOptions.keystorePath;
1281
+ if (hasSaveableValues) {
1282
+ const { shouldSave } = await inquirer.prompt([
1283
+ {
1284
+ type: 'confirm',
1285
+ name: 'shouldSave',
1286
+ message: 'Save these settings to norrix.config.ts for future builds?',
1287
+ default: true,
1288
+ },
1289
+ ]);
1290
+ if (shouldSave) {
1291
+ try {
1292
+ const savedPath = saveNorrixConfig(appRoot, saveableOptions);
1293
+ console.log(`✓ Configuration saved to ${path.basename(savedPath)}`);
1294
+ }
1295
+ catch (saveError) {
1296
+ console.warn(`Warning: Could not save config file: ${saveError.message}`);
1297
+ }
1298
+ }
1299
+ }
898
1300
  }
899
1301
  spinner.start('Creating project archive...');
900
1302
  }
@@ -906,8 +1308,8 @@ export async function build(cliPlatformArg, cliConfigurationArg, verbose = false
906
1308
  });
907
1309
  writeRuntimeFingerprintFile(projectRoot, fingerprint, platform);
908
1310
  spinner.start('Creating project archive...');
909
- // 3. Zip the project
910
- const zipPath = await zipProject(projectName, false);
1311
+ // 3. Zip the project (workspace-aware)
1312
+ const { zipPath, workspaceContext } = await zipProject(projectName, false, verbose);
911
1313
  spinner.text = 'Project archive created';
912
1314
  // 4. Upload the project zip to S3
913
1315
  spinner.text = 'Working...';
@@ -934,16 +1336,27 @@ export async function build(cliPlatformArg, cliConfigurationArg, verbose = false
934
1336
  catch {
935
1337
  inferredAppId = undefined;
936
1338
  }
1339
+ // Include workspace info for CI to properly navigate the project structure
1340
+ const workspaceInfo = workspaceContext.type === 'nx' ? {
1341
+ workspaceType: workspaceContext.type,
1342
+ appPath: workspaceContext.relativeAppPath,
1343
+ projectName: workspaceContext.projectName,
1344
+ } : undefined;
937
1345
  const response = await axios.post(`${API_URL}/build`, {
938
1346
  projectName,
939
1347
  appId: inferredAppId,
940
1348
  platform,
1349
+ // Forward CLI verbosity so CI can enable expensive logs (e.g. `ns --log trace`).
1350
+ verbose: Boolean(verbose),
941
1351
  version: version || '',
942
1352
  buildNumber: buildNumber || '',
943
1353
  configuration,
1354
+ ...(distributionType ? { distributionType } : {}),
944
1355
  fingerprint,
945
1356
  // Provide the relative key (without public/) – the workflow prepends public/
946
1357
  s3Key: s3KeyRel,
1358
+ // Workspace context for Nx monorepos
1359
+ ...(workspaceInfo ? { workspace: workspaceInfo } : {}),
947
1360
  // Only include raw credentials if not encrypted
948
1361
  ...(encryptedSecrets ? { encryptedSecrets } : {}),
949
1362
  ...(!encryptedSecrets && iosCredentials ? { iosCredentials } : {}),
@@ -959,6 +1372,9 @@ export async function build(cliPlatformArg, cliConfigurationArg, verbose = false
959
1372
  // 6. Clean up the zip file
960
1373
  fs.unlinkSync(zipPath);
961
1374
  const buildId = response.data.id;
1375
+ const dispatchedEnv = response?.data?.norrixEnv
1376
+ ? String(response.data.norrixEnv).trim()
1377
+ : undefined;
962
1378
  // Prefer printing what the backend recorded (in case it normalizes values)
963
1379
  // but fall back to local inputs/inferred values if the record is not yet available.
964
1380
  let recordedVersion = String(version || '').trim() || undefined;
@@ -984,11 +1400,38 @@ export async function build(cliPlatformArg, cliConfigurationArg, verbose = false
984
1400
  spinner.succeed('Build started successfully!');
985
1401
  console.log('✅ Your app is being built on Norrix.');
986
1402
  console.log(` Build ID: ${buildId}`);
1403
+ console.log(` Backend API: ${API_URL}`);
1404
+ if (process.env.NORRIX_API_URL) {
1405
+ console.log(` NORRIX_API_URL: ${process.env.NORRIX_API_URL}`);
1406
+ }
1407
+ if (dispatchedEnv) {
1408
+ console.log(` Backend env: ${dispatchedEnv}`);
1409
+ }
987
1410
  console.log(formatVersionBuildLine(recordedVersion, recordedBuildNumber));
988
1411
  console.log(` Platform: ${platform}`);
1412
+ if (distributionType) {
1413
+ console.log(` Distribution: ${distributionType === 'enterprise'
1414
+ ? 'Enterprise'
1415
+ : distributionType === 'adhoc'
1416
+ ? 'Ad Hoc'
1417
+ : 'App Store'}`);
1418
+ }
989
1419
  console.log(` You can check the status with: norrix build-status ${buildId}`);
1420
+ // Restore original cwd if we changed it
1421
+ if (originalCwd && process.cwd() !== originalCwd) {
1422
+ process.chdir(originalCwd);
1423
+ }
990
1424
  }
991
1425
  catch (error) {
1426
+ // Restore original cwd if we changed it
1427
+ if (originalCwd && process.cwd() !== originalCwd) {
1428
+ try {
1429
+ process.chdir(originalCwd);
1430
+ }
1431
+ catch {
1432
+ // Ignore chdir errors during error handling
1433
+ }
1434
+ }
992
1435
  const apiMessage = (error?.response?.data &&
993
1436
  (error.response.data.error || error.response.data.message)) ||
994
1437
  undefined;
@@ -1020,6 +1463,7 @@ export async function build(cliPlatformArg, cliConfigurationArg, verbose = false
1020
1463
  * Submits the built app to app stores via the Next.js API gateway
1021
1464
  */
1022
1465
  export async function submit(cliPlatformArg, cliTrackArg, verbose = false) {
1466
+ ensureInitialized();
1023
1467
  const spinner = ora('Preparing app for submission...');
1024
1468
  try {
1025
1469
  spinner.start();
@@ -1191,6 +1635,9 @@ export async function submit(cliPlatformArg, cliTrackArg, verbose = false) {
1191
1635
  ...credentials,
1192
1636
  }, { headers: await getAuthHeaders() });
1193
1637
  const submitId = response.data.id;
1638
+ const dispatchedEnv = response?.data?.norrixEnv
1639
+ ? String(response.data.norrixEnv).trim()
1640
+ : undefined;
1194
1641
  spinner.succeed('Submission started successfully!');
1195
1642
  // Try to print the exact version/buildNumber of what was submitted.
1196
1643
  // Prefer the selected build metadata, but fall back to fetching by ID
@@ -1230,6 +1677,13 @@ export async function submit(cliPlatformArg, cliTrackArg, verbose = false) {
1230
1677
  }
1231
1678
  console.log('✅ Your app submission request was received.');
1232
1679
  console.log(` Submission ID: ${submitId}`);
1680
+ console.log(` Backend API: ${API_URL}`);
1681
+ if (process.env.NORRIX_API_URL) {
1682
+ console.log(` NORRIX_API_URL: ${process.env.NORRIX_API_URL}`);
1683
+ }
1684
+ if (dispatchedEnv) {
1685
+ console.log(` Backend env: ${dispatchedEnv}`);
1686
+ }
1233
1687
  console.log(` Platform: ${platform === 'android' ? 'Google Play Store' : 'Apple App Store'}`);
1234
1688
  console.log(formatVersionBuildLine(submittedVersion, submittedBuildNumber));
1235
1689
  console.log(` Track: ${track}`);
@@ -1268,11 +1722,27 @@ export async function submit(cliPlatformArg, cliTrackArg, verbose = false) {
1268
1722
  * Update command implementation
1269
1723
  * Publishes over-the-air updates to deployed apps via the Next.js API gateway
1270
1724
  */
1271
- export async function update(cliPlatformArg, cliVersionArg, verbose = false) {
1725
+ export async function update(cliPlatformArg, cliVersionArg, verbose = false, options // string for backwards compatibility with old projectArg
1726
+ ) {
1727
+ // Normalize options - support both new object and legacy string projectArg
1728
+ const opts = typeof options === 'string' ? { project: options } : (options || {});
1729
+ ensureInitialized();
1272
1730
  let spinner;
1731
+ let originalCwd;
1273
1732
  try {
1274
1733
  spinner = ora('Preparing over-the-air update...');
1275
1734
  spinner.start();
1735
+ // 0. Resolve workspace context (handles --project and prompting)
1736
+ const resolved = await resolveWorkspaceContext(opts.project, spinner);
1737
+ originalCwd = resolved.originalCwd;
1738
+ const workspaceCtx = resolved.workspaceContext;
1739
+ // Restart spinner after potential prompts
1740
+ if (!spinner.isSpinning) {
1741
+ spinner.start('Preparing over-the-air update...');
1742
+ }
1743
+ if (workspaceCtx.type === 'nx' && verbose) {
1744
+ logWorkspaceContext(workspaceCtx, verbose);
1745
+ }
1276
1746
  // Normalize and/or ask for platform first (CLI arg takes precedence if valid)
1277
1747
  let platform = (cliPlatformArg || '').toLowerCase();
1278
1748
  const validPlatforms = ['android', 'ios', 'visionos'];
@@ -1308,27 +1778,45 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false) {
1308
1778
  // Ask for app ID and version first
1309
1779
  spinner.stop();
1310
1780
  const cliVersion = (cliVersionArg || '').trim();
1311
- const baseAnswers = await inquirer.prompt([
1312
- {
1313
- type: 'input',
1314
- name: 'appId',
1315
- message: 'Enter the App ID to update:',
1316
- default: inferredAppId || '',
1317
- validate: (input) => input.length > 0 || 'App ID is required',
1318
- },
1319
- {
1320
- type: 'input',
1321
- name: 'version',
1322
- message: inferredVersion
1323
- ? `Update version (${inferredVersion}, enter to accept):`
1324
- : 'Update version:',
1325
- default: inferredVersion,
1326
- when: () => !cliVersion,
1327
- validate: (input) => input.length > 0 || 'Version is required',
1328
- },
1329
- ]);
1330
- const appId = baseAnswers.appId;
1331
- const version = (cliVersion || baseAnswers.version);
1781
+ // Resolve appId: CLI flag → inferred → prompt
1782
+ let appId;
1783
+ if (opts.appId) {
1784
+ appId = opts.appId;
1785
+ }
1786
+ else {
1787
+ const { appId: promptedAppId } = await inquirer.prompt([
1788
+ {
1789
+ type: 'input',
1790
+ name: 'appId',
1791
+ message: 'Enter the App ID to update:',
1792
+ default: inferredAppId || '',
1793
+ validate: (input) => input.length > 0 || 'App ID is required',
1794
+ },
1795
+ ]);
1796
+ appId = promptedAppId;
1797
+ }
1798
+ // Resolve version: CLI arg → inferred → prompt
1799
+ let version;
1800
+ if (cliVersion) {
1801
+ version = cliVersion;
1802
+ }
1803
+ else if (inferredVersion && opts.nonInteractive) {
1804
+ version = inferredVersion;
1805
+ }
1806
+ else {
1807
+ const { version: promptedVersion } = await inquirer.prompt([
1808
+ {
1809
+ type: 'input',
1810
+ name: 'version',
1811
+ message: inferredVersion
1812
+ ? `Update version (${inferredVersion}, enter to accept):`
1813
+ : 'Update version:',
1814
+ default: inferredVersion,
1815
+ validate: (input) => input.length > 0 || 'Version is required',
1816
+ },
1817
+ ]);
1818
+ version = promptedVersion;
1819
+ }
1332
1820
  // Ask the server what the next buildNumber would be for this app so we
1333
1821
  // can present a sensible default in the prompt, matching what the API
1334
1822
  // will auto-increment to if left blank.
@@ -1352,38 +1840,51 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false) {
1352
1840
  : platform === 'android'
1353
1841
  ? androidVersionInfo.buildNumber
1354
1842
  : undefined;
1355
- const { buildNumber: promptedBuildNumber, notes } = await inquirer.prompt([
1356
- {
1357
- type: 'input',
1358
- name: 'buildNumber',
1359
- message: (() => {
1360
- if (serverSuggestedBuildNumber) {
1361
- return `Update build number (${serverSuggestedBuildNumber}, enter to accept or override; blank to auto increment from server):`;
1362
- }
1363
- if (localInferredBuildNumber) {
1364
- return `Update build number (${localInferredBuildNumber}, enter to auto increment from server):`;
1365
- }
1366
- return 'Update build number (leave blank to auto increment from server):';
1367
- })(),
1368
- default: serverSuggestedBuildNumber || localInferredBuildNumber || '',
1369
- validate: (input) => {
1370
- const val = String(input).trim();
1371
- if (!val)
1372
- return true; // allow blank -> server will auto-increment
1373
- if (!/^\d+$/.test(val)) {
1374
- return 'Build number must be a positive integer or blank to auto-increment';
1375
- }
1376
- return true;
1843
+ // Resolve buildNumber: CLI flag server suggestion → local inferred → prompt
1844
+ let buildNumber;
1845
+ let notes = '';
1846
+ if (opts.buildNumber) {
1847
+ buildNumber = opts.buildNumber;
1848
+ }
1849
+ else if (opts.nonInteractive) {
1850
+ // In non-interactive mode, use server suggestion or leave undefined for auto-increment
1851
+ buildNumber = serverSuggestedBuildNumber || localInferredBuildNumber || undefined;
1852
+ }
1853
+ else {
1854
+ const buildPromptAnswers = await inquirer.prompt([
1855
+ {
1856
+ type: 'input',
1857
+ name: 'buildNumber',
1858
+ message: (() => {
1859
+ if (serverSuggestedBuildNumber) {
1860
+ return `Update build number (${serverSuggestedBuildNumber}, enter to accept or override; blank to auto increment from server):`;
1861
+ }
1862
+ if (localInferredBuildNumber) {
1863
+ return `Update build number (${localInferredBuildNumber}, enter to auto increment from server):`;
1864
+ }
1865
+ return 'Update build number (leave blank to auto increment from server):';
1866
+ })(),
1867
+ default: serverSuggestedBuildNumber || localInferredBuildNumber || '',
1868
+ validate: (input) => {
1869
+ const val = String(input).trim();
1870
+ if (!val)
1871
+ return true; // allow blank -> server will auto-increment
1872
+ if (!/^\d+$/.test(val)) {
1873
+ return 'Build number must be a positive integer or blank to auto-increment';
1874
+ }
1875
+ return true;
1876
+ },
1377
1877
  },
1378
- },
1379
- {
1380
- type: 'input',
1381
- name: 'notes',
1382
- message: 'Release notes (optional):',
1383
- default: '',
1384
- },
1385
- ]);
1386
- const buildNumber = String(promptedBuildNumber || '').trim() || undefined;
1878
+ {
1879
+ type: 'input',
1880
+ name: 'notes',
1881
+ message: 'Release notes (optional):',
1882
+ default: '',
1883
+ },
1884
+ ]);
1885
+ buildNumber = String(buildPromptAnswers.buildNumber || '').trim() || undefined;
1886
+ notes = buildPromptAnswers.notes || '';
1887
+ }
1387
1888
  // Check the app directory structure before packaging
1388
1889
  const srcAppDir = path.join(process.cwd(), 'src', 'app');
1389
1890
  const appDir = path.join(process.cwd(), 'app');
@@ -1460,9 +1961,9 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false) {
1460
1961
  // fingerprint JSON under the app source tree is the
1461
1962
  // single source of truth for OTA compatibility.
1462
1963
  spinner.start('Packaging for over-the-air update...');
1463
- // Create the update bundle - pass true to include node_modules for updates
1964
+ // Create the update bundle (workspace-aware) - pass true to include node_modules for updates
1464
1965
  const projectName = await getProjectName();
1465
- const zipPath = await zipProject(projectName, true);
1966
+ const { zipPath, workspaceContext } = await zipProject(projectName, true, verbose);
1466
1967
  spinner.text = 'Uploading update to Norrix cloud storage...';
1467
1968
  const fileBuffer = fs.readFileSync(zipPath);
1468
1969
  const updateFolder = `update-${Date.now()}`;
@@ -1470,6 +1971,12 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false) {
1470
1971
  const s3KeyRel = `updates/${updateFolder}/${appId}-${safeVersion}.zip`;
1471
1972
  await putObjectToStorage(`public/${s3KeyRel}`, fileBuffer);
1472
1973
  spinner.text = 'Upload complete. Starting update...';
1974
+ // Include workspace info for CI to properly navigate the project structure
1975
+ const workspaceInfo = workspaceContext.type === 'nx' ? {
1976
+ workspaceType: workspaceContext.type,
1977
+ appPath: workspaceContext.relativeAppPath,
1978
+ projectName: workspaceContext.projectName,
1979
+ } : undefined;
1473
1980
  const response = await axios.post(`${API_URL}/update`, {
1474
1981
  appId,
1475
1982
  platform,
@@ -1479,6 +1986,8 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false) {
1479
1986
  fingerprint,
1480
1987
  // Provide the relative key (without public/). Consumers will prepend public/
1481
1988
  s3Key: s3KeyRel,
1989
+ // Workspace context for Nx monorepos
1990
+ ...(workspaceInfo ? { workspace: workspaceInfo } : {}),
1482
1991
  }, {
1483
1992
  headers: {
1484
1993
  'Content-Type': 'application/json',
@@ -1488,6 +1997,9 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false) {
1488
1997
  // Clean up the zip file
1489
1998
  fs.unlinkSync(zipPath);
1490
1999
  const updateId = response.data.id;
2000
+ const dispatchedEnv = response?.data?.norrixEnv
2001
+ ? String(response.data.norrixEnv).trim()
2002
+ : undefined;
1491
2003
  // Prefer the backend-recorded values (especially buildNumber when auto-incremented).
1492
2004
  let recordedUpdateVersion = String(version || '').trim() || undefined;
1493
2005
  let recordedUpdateBuildNumber = (response?.data?.buildNumber
@@ -1516,10 +2028,30 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false) {
1516
2028
  spinner.succeed('Update published successfully!');
1517
2029
  console.log('✅ Your update has been published.');
1518
2030
  console.log(` Update ID: ${updateId}`);
2031
+ console.log(` Backend API: ${API_URL}`);
2032
+ if (process.env.NORRIX_API_URL) {
2033
+ console.log(` NORRIX_API_URL: ${process.env.NORRIX_API_URL}`);
2034
+ }
2035
+ if (dispatchedEnv) {
2036
+ console.log(` Backend env: ${dispatchedEnv}`);
2037
+ }
1519
2038
  console.log(formatVersionBuildLine(recordedUpdateVersion, recordedUpdateBuildNumber));
1520
2039
  console.log(` You can check the status with: norrix update-status ${updateId}`);
2040
+ // Restore original cwd if we changed it
2041
+ if (originalCwd && process.cwd() !== originalCwd) {
2042
+ process.chdir(originalCwd);
2043
+ }
1521
2044
  }
1522
2045
  catch (error) {
2046
+ // Restore original cwd if we changed it
2047
+ if (originalCwd && process.cwd() !== originalCwd) {
2048
+ try {
2049
+ process.chdir(originalCwd);
2050
+ }
2051
+ catch {
2052
+ // Ignore chdir errors during error handling
2053
+ }
2054
+ }
1523
2055
  const apiMessage = (error?.response?.data &&
1524
2056
  (error.response.data.error || error.response.data.message)) ||
1525
2057
  undefined;
@@ -1548,6 +2080,7 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false) {
1548
2080
  * Checks the status of a build via the Next.js API gateway
1549
2081
  */
1550
2082
  export async function buildStatus(buildId, verbose = false) {
2083
+ ensureInitialized();
1551
2084
  try {
1552
2085
  const spinner = ora(`Checking status of build ${buildId}...`).start();
1553
2086
  const response = await axios.get(`${API_URL}/build/${buildId}`, {
@@ -1585,6 +2118,7 @@ export async function buildStatus(buildId, verbose = false) {
1585
2118
  * Checks the status of a submission via the Next.js API gateway
1586
2119
  */
1587
2120
  export async function submitStatus(submitId, verbose = false) {
2121
+ ensureInitialized();
1588
2122
  try {
1589
2123
  const spinner = ora(`Checking status of submission ${submitId}...`).start();
1590
2124
  const response = await axios.get(`${API_URL}/submit/${submitId}`, {
@@ -1618,6 +2152,7 @@ export async function submitStatus(submitId, verbose = false) {
1618
2152
  * Checks the status of an update via the Next.js API gateway
1619
2153
  */
1620
2154
  export async function updateStatus(updateId, verbose = false) {
2155
+ ensureInitialized();
1621
2156
  try {
1622
2157
  const spinner = ora(`Checking status of update ${updateId}...`).start();
1623
2158
  const response = await axios.get(`${API_URL}/update/${updateId}`, {
@@ -1650,6 +2185,7 @@ export async function updateStatus(updateId, verbose = false) {
1650
2185
  * Sign-In command implementation (email / password via Cognito)
1651
2186
  */
1652
2187
  export async function signIn(verbose = false) {
2188
+ ensureInitialized();
1653
2189
  try {
1654
2190
  const answers = await inquirer.prompt([
1655
2191
  {
@@ -1697,6 +2233,7 @@ export async function signIn(verbose = false) {
1697
2233
  * Sign-Out command implementation
1698
2234
  */
1699
2235
  export async function signOut(verbose = false) {
2236
+ ensureInitialized();
1700
2237
  try {
1701
2238
  const spinner = ora('Signing out...').start();
1702
2239
  await amplifySignOut();
@@ -1721,6 +2258,7 @@ export async function signOut(verbose = false) {
1721
2258
  * Upload a file to S3 via Amplify Storage
1722
2259
  */
1723
2260
  export async function uploadFile(filePath, options, verbose = false) {
2261
+ ensureInitialized();
1724
2262
  try {
1725
2263
  const resolvedPath = path.resolve(process.cwd(), filePath);
1726
2264
  if (!fs.existsSync(resolvedPath)) {
@@ -1751,6 +2289,7 @@ export async function uploadFile(filePath, options, verbose = false) {
1751
2289
  * Current User command implementation
1752
2290
  */
1753
2291
  export async function currentUser(verbose = false) {
2292
+ ensureInitialized();
1754
2293
  try {
1755
2294
  const spinner = ora('Fetching current user...').start();
1756
2295
  let user;
@@ -1802,6 +2341,7 @@ export async function currentUser(verbose = false) {
1802
2341
  * Open billing checkout for the current organization
1803
2342
  */
1804
2343
  export async function billingCheckout(priceId, verbose = false) {
2344
+ ensureInitialized();
1805
2345
  try {
1806
2346
  if (!priceId) {
1807
2347
  const a = await inquirer.prompt([
@@ -1834,6 +2374,7 @@ export async function billingCheckout(priceId, verbose = false) {
1834
2374
  * Open Stripe billing portal for the current organization
1835
2375
  */
1836
2376
  export async function billingPortal(verbose = false) {
2377
+ ensureInitialized();
1837
2378
  try {
1838
2379
  const spinner = ora('Creating billing portal session...').start();
1839
2380
  const res = await axios.post(`${API_URL}/billing/portal`, {}, { headers: await getAuthHeaders() });