@norrix/cli 0.0.24 → 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 +19 -0
- package/dist/cli.js +22 -2
- 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/commands.d.ts +30 -2
- package/dist/lib/commands.js +496 -90
- package/dist/lib/commands.js.map +1 -1
- 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/fingerprinting.d.ts +8 -0
- package/dist/lib/fingerprinting.js +4 -1
- package/dist/lib/fingerprinting.js.map +1 -1
- 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 +1 -1
package/dist/lib/commands.js
CHANGED
|
@@ -9,6 +9,8 @@ import archiver from 'archiver';
|
|
|
9
9
|
// import FormData from 'form-data';
|
|
10
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';
|
|
@@ -635,20 +637,106 @@ function getAndroidVersionFromAppGradle() {
|
|
|
635
637
|
}
|
|
636
638
|
}
|
|
637
639
|
/**
|
|
638
|
-
*
|
|
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.
|
|
639
717
|
*/
|
|
640
|
-
async function zipProject(projectName, isUpdate = false) {
|
|
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)
|
|
729
|
+
*/
|
|
730
|
+
async function zipStandaloneProject(projectName, workspaceCtx, isUpdate = false) {
|
|
641
731
|
return new Promise((resolve, reject) => {
|
|
642
732
|
const outputPath = path.join(process.cwd(), `${projectName}.zip`);
|
|
643
733
|
const output = fs.createWriteStream(outputPath);
|
|
644
734
|
const archive = archiver('zip', {
|
|
645
|
-
zlib: { level: 9 },
|
|
735
|
+
zlib: { level: 9 },
|
|
646
736
|
});
|
|
647
|
-
// Listen for all archive data to be written
|
|
648
737
|
output.on('close', () => {
|
|
649
|
-
resolve(outputPath);
|
|
738
|
+
resolve({ zipPath: outputPath, workspaceContext: workspaceCtx });
|
|
650
739
|
});
|
|
651
|
-
// Listen for warnings and errors
|
|
652
740
|
archive.on('warning', (err) => {
|
|
653
741
|
if (err.code === 'ENOENT') {
|
|
654
742
|
console.warn('Archive warning:', err);
|
|
@@ -660,9 +748,8 @@ async function zipProject(projectName, isUpdate = false) {
|
|
|
660
748
|
archive.on('error', (err) => {
|
|
661
749
|
reject(err);
|
|
662
750
|
});
|
|
663
|
-
// Pipe archive data to the file
|
|
664
751
|
archive.pipe(output);
|
|
665
|
-
// Determine the primary app directory
|
|
752
|
+
// Determine the primary app directory
|
|
666
753
|
const nsAppPath = getNativeScriptAppPath();
|
|
667
754
|
const nsAppDir = nsAppPath
|
|
668
755
|
? path.join(process.cwd(), nsAppPath)
|
|
@@ -682,18 +769,12 @@ async function zipProject(projectName, isUpdate = false) {
|
|
|
682
769
|
}
|
|
683
770
|
else {
|
|
684
771
|
console.warn('Warning: app directory not found in the project root');
|
|
685
|
-
const checked = [nsAppDir, srcDir, appDir].filter(Boolean).join(', ');
|
|
686
|
-
console.warn(`Checked locations: ${checked}`);
|
|
687
|
-
console.log('Creating an empty app directory in the zip');
|
|
688
772
|
}
|
|
689
|
-
// For both builds/updates, include App_Resources and exclude node_modules.
|
|
690
|
-
// For OTA updates specifically, we also exclude the runtime fingerprint
|
|
691
|
-
// file, since it reflects the native store binary, not the OTA payload.
|
|
692
773
|
const ignorePatterns = [
|
|
693
|
-
'node_modules/**',
|
|
694
|
-
'*.zip',
|
|
695
|
-
'platforms/**',
|
|
696
|
-
'hooks/**',
|
|
774
|
+
'node_modules/**',
|
|
775
|
+
'*.zip',
|
|
776
|
+
'platforms/**',
|
|
777
|
+
'hooks/**',
|
|
697
778
|
];
|
|
698
779
|
if (isUpdate) {
|
|
699
780
|
ignorePatterns.push('**/assets/norrix.fingerprint.json');
|
|
@@ -702,7 +783,132 @@ async function zipProject(projectName, isUpdate = false) {
|
|
|
702
783
|
cwd: process.cwd(),
|
|
703
784
|
ignore: ignorePatterns,
|
|
704
785
|
});
|
|
705
|
-
|
|
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
|
+
}
|
|
706
912
|
archive.finalize();
|
|
707
913
|
});
|
|
708
914
|
}
|
|
@@ -710,16 +916,33 @@ async function zipProject(projectName, isUpdate = false) {
|
|
|
710
916
|
* Build command implementation
|
|
711
917
|
* Uploads project to S3 and triggers build via the Next.js API gateway -> WarpBuild
|
|
712
918
|
*/
|
|
713
|
-
export async function build(cliPlatformArg, cliConfigurationArg, cliDistributionArg, 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 || {});
|
|
714
923
|
ensureInitialized();
|
|
715
924
|
let spinner;
|
|
925
|
+
let originalCwd;
|
|
716
926
|
try {
|
|
717
927
|
spinner = ora('Preparing app for building...');
|
|
718
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
|
+
}
|
|
719
942
|
// 1. Get project info
|
|
720
943
|
const projectName = await getProjectName();
|
|
721
|
-
// 2. Determine platform (CLI arg preferred, otherwise prompt)
|
|
722
|
-
let platform = (cliPlatformArg || '').toLowerCase();
|
|
944
|
+
// 2. Determine platform (CLI arg preferred, then config, otherwise prompt)
|
|
945
|
+
let platform = (cliPlatformArg || norrixConfig.defaultPlatform || '').toLowerCase();
|
|
723
946
|
const validPlatforms = ['android', 'ios', 'visionos'];
|
|
724
947
|
spinner.stop();
|
|
725
948
|
if (!validPlatforms.includes(platform)) {
|
|
@@ -772,14 +995,20 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
772
995
|
}
|
|
773
996
|
return undefined;
|
|
774
997
|
};
|
|
775
|
-
// 2.2 iOS distribution type (release only)
|
|
998
|
+
// 2.2 iOS distribution type (release only): CLI arg > config file > prompt
|
|
776
999
|
let distributionType;
|
|
777
1000
|
if (platform === 'ios' && configuration === 'release') {
|
|
1001
|
+
// Try CLI arg first
|
|
778
1002
|
distributionType = normalizeIosDistribution(cliDistributionArg);
|
|
779
1003
|
if (!distributionType && cliDistributionArg) {
|
|
780
1004
|
throw new Error(`Invalid iOS distribution type '${cliDistributionArg}'. Use 'appstore', 'adhoc', or 'enterprise'.`);
|
|
781
1005
|
}
|
|
782
|
-
|
|
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) {
|
|
783
1012
|
const { distribution } = await inquirer.prompt([
|
|
784
1013
|
{
|
|
785
1014
|
type: 'list',
|
|
@@ -795,6 +1024,10 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
795
1024
|
]);
|
|
796
1025
|
distributionType = distribution;
|
|
797
1026
|
}
|
|
1027
|
+
// Default to appstore in non-interactive mode if nothing else provided
|
|
1028
|
+
if (!distributionType) {
|
|
1029
|
+
distributionType = 'appstore';
|
|
1030
|
+
}
|
|
798
1031
|
}
|
|
799
1032
|
const appleVersionInfo = platform === 'ios' || platform === 'visionos'
|
|
800
1033
|
? getAppleVersionFromInfoPlist(platform)
|
|
@@ -865,7 +1098,31 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
865
1098
|
if (configuration === 'release') {
|
|
866
1099
|
spinner.stop();
|
|
867
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;
|
|
868
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
|
+
},
|
|
869
1126
|
{
|
|
870
1127
|
type: 'input',
|
|
871
1128
|
name: 'p12Path',
|
|
@@ -888,7 +1145,7 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
888
1145
|
{
|
|
889
1146
|
type: 'confirm',
|
|
890
1147
|
name: 'hasAscKey',
|
|
891
|
-
message: 'Provide App Store Connect API Key? (optional, for
|
|
1148
|
+
message: 'Provide App Store Connect API Key? (optional, for auto-provisioning)',
|
|
892
1149
|
default: false,
|
|
893
1150
|
},
|
|
894
1151
|
{
|
|
@@ -912,23 +1169,24 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
912
1169
|
default: '',
|
|
913
1170
|
when: (a) => a.hasAscKey,
|
|
914
1171
|
},
|
|
915
|
-
{
|
|
916
|
-
type: 'input',
|
|
917
|
-
name: 'ascTeamId',
|
|
918
|
-
message: 'Apple Developer Team ID (optional, required for API-key provisioning):',
|
|
919
|
-
default: '',
|
|
920
|
-
when: (a) => a.hasAscKey,
|
|
921
|
-
},
|
|
922
1172
|
]);
|
|
1173
|
+
// Use resolved teamId from CLI/config, or from prompt
|
|
1174
|
+
const finalTeamId = resolvedTeamId || iosAnswers.teamId?.trim();
|
|
923
1175
|
iosCredentials = {
|
|
1176
|
+
teamId: finalTeamId || undefined,
|
|
924
1177
|
p12Base64: readOptionalFileAsBase64(iosAnswers.p12Path),
|
|
925
1178
|
p12Password: iosAnswers.p12Password || undefined,
|
|
926
1179
|
mobileprovisionBase64: readOptionalFileAsBase64(iosAnswers.mobileprovisionPath),
|
|
927
1180
|
ascApiKeyId: iosAnswers.ascApiKeyId || undefined,
|
|
928
1181
|
ascIssuerId: iosAnswers.ascIssuerId || undefined,
|
|
929
1182
|
ascPrivateKey: readOptionalFileAsBase64(iosAnswers.ascPrivateKeyPath),
|
|
930
|
-
|
|
1183
|
+
// Track paths for config saving (not sent to API)
|
|
1184
|
+
_p12Path: iosAnswers.p12Path || undefined,
|
|
1185
|
+
_mobileprovisionPath: iosAnswers.mobileprovisionPath || undefined,
|
|
931
1186
|
};
|
|
1187
|
+
if (finalTeamId) {
|
|
1188
|
+
console.log(`Using Apple Team ID: ${finalTeamId}`);
|
|
1189
|
+
}
|
|
932
1190
|
}
|
|
933
1191
|
else if (platform === 'android') {
|
|
934
1192
|
const androidAnswers = await inquirer.prompt([
|
|
@@ -979,6 +1237,66 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
979
1237
|
keyPassword: androidAnswers.keyPassword || undefined,
|
|
980
1238
|
playServiceAccountJson: readOptionalFileAsBase64(androidAnswers.playJsonPath),
|
|
981
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
|
+
}
|
|
982
1300
|
}
|
|
983
1301
|
spinner.start('Creating project archive...');
|
|
984
1302
|
}
|
|
@@ -990,8 +1308,8 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
990
1308
|
});
|
|
991
1309
|
writeRuntimeFingerprintFile(projectRoot, fingerprint, platform);
|
|
992
1310
|
spinner.start('Creating project archive...');
|
|
993
|
-
// 3. Zip the project
|
|
994
|
-
const zipPath = await zipProject(projectName, false);
|
|
1311
|
+
// 3. Zip the project (workspace-aware)
|
|
1312
|
+
const { zipPath, workspaceContext } = await zipProject(projectName, false, verbose);
|
|
995
1313
|
spinner.text = 'Project archive created';
|
|
996
1314
|
// 4. Upload the project zip to S3
|
|
997
1315
|
spinner.text = 'Working...';
|
|
@@ -1018,6 +1336,12 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
1018
1336
|
catch {
|
|
1019
1337
|
inferredAppId = undefined;
|
|
1020
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;
|
|
1021
1345
|
const response = await axios.post(`${API_URL}/build`, {
|
|
1022
1346
|
projectName,
|
|
1023
1347
|
appId: inferredAppId,
|
|
@@ -1031,6 +1355,8 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
1031
1355
|
fingerprint,
|
|
1032
1356
|
// Provide the relative key (without public/) – the workflow prepends public/
|
|
1033
1357
|
s3Key: s3KeyRel,
|
|
1358
|
+
// Workspace context for Nx monorepos
|
|
1359
|
+
...(workspaceInfo ? { workspace: workspaceInfo } : {}),
|
|
1034
1360
|
// Only include raw credentials if not encrypted
|
|
1035
1361
|
...(encryptedSecrets ? { encryptedSecrets } : {}),
|
|
1036
1362
|
...(!encryptedSecrets && iosCredentials ? { iosCredentials } : {}),
|
|
@@ -1091,8 +1417,21 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
1091
1417
|
: 'App Store'}`);
|
|
1092
1418
|
}
|
|
1093
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
|
+
}
|
|
1094
1424
|
}
|
|
1095
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
|
+
}
|
|
1096
1435
|
const apiMessage = (error?.response?.data &&
|
|
1097
1436
|
(error.response.data.error || error.response.data.message)) ||
|
|
1098
1437
|
undefined;
|
|
@@ -1383,12 +1722,27 @@ export async function submit(cliPlatformArg, cliTrackArg, verbose = false) {
|
|
|
1383
1722
|
* Update command implementation
|
|
1384
1723
|
* Publishes over-the-air updates to deployed apps via the Next.js API gateway
|
|
1385
1724
|
*/
|
|
1386
|
-
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 || {});
|
|
1387
1729
|
ensureInitialized();
|
|
1388
1730
|
let spinner;
|
|
1731
|
+
let originalCwd;
|
|
1389
1732
|
try {
|
|
1390
1733
|
spinner = ora('Preparing over-the-air update...');
|
|
1391
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
|
+
}
|
|
1392
1746
|
// Normalize and/or ask for platform first (CLI arg takes precedence if valid)
|
|
1393
1747
|
let platform = (cliPlatformArg || '').toLowerCase();
|
|
1394
1748
|
const validPlatforms = ['android', 'ios', 'visionos'];
|
|
@@ -1424,27 +1778,45 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false) {
|
|
|
1424
1778
|
// Ask for app ID and version first
|
|
1425
1779
|
spinner.stop();
|
|
1426
1780
|
const cliVersion = (cliVersionArg || '').trim();
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
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
|
+
}
|
|
1448
1820
|
// Ask the server what the next buildNumber would be for this app so we
|
|
1449
1821
|
// can present a sensible default in the prompt, matching what the API
|
|
1450
1822
|
// will auto-increment to if left blank.
|
|
@@ -1468,38 +1840,51 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false) {
|
|
|
1468
1840
|
: platform === 'android'
|
|
1469
1841
|
? androidVersionInfo.buildNumber
|
|
1470
1842
|
: undefined;
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
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
|
+
},
|
|
1493
1877
|
},
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
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
|
+
}
|
|
1503
1888
|
// Check the app directory structure before packaging
|
|
1504
1889
|
const srcAppDir = path.join(process.cwd(), 'src', 'app');
|
|
1505
1890
|
const appDir = path.join(process.cwd(), 'app');
|
|
@@ -1576,9 +1961,9 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false) {
|
|
|
1576
1961
|
// fingerprint JSON under the app source tree is the
|
|
1577
1962
|
// single source of truth for OTA compatibility.
|
|
1578
1963
|
spinner.start('Packaging for over-the-air update...');
|
|
1579
|
-
// 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
|
|
1580
1965
|
const projectName = await getProjectName();
|
|
1581
|
-
const zipPath = await zipProject(projectName, true);
|
|
1966
|
+
const { zipPath, workspaceContext } = await zipProject(projectName, true, verbose);
|
|
1582
1967
|
spinner.text = 'Uploading update to Norrix cloud storage...';
|
|
1583
1968
|
const fileBuffer = fs.readFileSync(zipPath);
|
|
1584
1969
|
const updateFolder = `update-${Date.now()}`;
|
|
@@ -1586,6 +1971,12 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false) {
|
|
|
1586
1971
|
const s3KeyRel = `updates/${updateFolder}/${appId}-${safeVersion}.zip`;
|
|
1587
1972
|
await putObjectToStorage(`public/${s3KeyRel}`, fileBuffer);
|
|
1588
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;
|
|
1589
1980
|
const response = await axios.post(`${API_URL}/update`, {
|
|
1590
1981
|
appId,
|
|
1591
1982
|
platform,
|
|
@@ -1595,6 +1986,8 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false) {
|
|
|
1595
1986
|
fingerprint,
|
|
1596
1987
|
// Provide the relative key (without public/). Consumers will prepend public/
|
|
1597
1988
|
s3Key: s3KeyRel,
|
|
1989
|
+
// Workspace context for Nx monorepos
|
|
1990
|
+
...(workspaceInfo ? { workspace: workspaceInfo } : {}),
|
|
1598
1991
|
}, {
|
|
1599
1992
|
headers: {
|
|
1600
1993
|
'Content-Type': 'application/json',
|
|
@@ -1644,8 +2037,21 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false) {
|
|
|
1644
2037
|
}
|
|
1645
2038
|
console.log(formatVersionBuildLine(recordedUpdateVersion, recordedUpdateBuildNumber));
|
|
1646
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
|
+
}
|
|
1647
2044
|
}
|
|
1648
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
|
+
}
|
|
1649
2055
|
const apiMessage = (error?.response?.data &&
|
|
1650
2056
|
(error.response.data.error || error.response.data.message)) ||
|
|
1651
2057
|
undefined;
|