@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.
- package/CHANGELOG.md +102 -0
- package/dist/cli.js +36 -4
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/amplify-config.d.ts +14 -1
- package/dist/lib/amplify-config.js +82 -53
- package/dist/lib/amplify-config.js.map +1 -1
- package/dist/lib/commands.d.ts +40 -2
- package/dist/lib/commands.js +636 -95
- package/dist/lib/commands.js.map +1 -1
- package/dist/lib/config-helpers.spec.d.ts +1 -0
- package/dist/lib/config.d.ts +147 -0
- package/dist/lib/config.js +210 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/defaults.d.ts +14 -0
- package/dist/lib/defaults.js +9 -0
- package/dist/lib/defaults.js.map +1 -0
- package/dist/lib/dev-defaults.d.ts +7 -0
- package/dist/lib/dev-defaults.js +18 -0
- package/dist/lib/dev-defaults.js.map +1 -0
- package/dist/lib/fingerprinting.d.ts +8 -0
- package/dist/lib/fingerprinting.js +4 -1
- package/dist/lib/fingerprinting.js.map +1 -1
- package/dist/lib/prod-defaults.d.ts +8 -0
- package/dist/lib/prod-defaults.js +13 -0
- package/dist/lib/prod-defaults.js.map +1 -0
- package/dist/lib/workspace.d.ts +121 -0
- package/dist/lib/workspace.js +472 -0
- package/dist/lib/workspace.js.map +1 -0
- package/package.json +2 -3
- package/.env.local +0 -9
package/dist/lib/commands.js
CHANGED
|
@@ -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
|
-
|
|
16
|
-
|
|
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
|
-
//
|
|
45
|
-
|
|
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.
|
|
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.
|
|
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
|
-
*
|
|
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
|
|
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 },
|
|
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
|
|
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/**',
|
|
659
|
-
'*.zip',
|
|
660
|
-
'platforms/**',
|
|
661
|
-
'hooks/**',
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
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
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
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
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
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() });
|