@norrix/cli 0.0.50 → 0.0.51
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/dist/lib/ascii-art.d.ts +8 -0
- package/dist/lib/ascii-art.js +219 -0
- package/dist/lib/ascii-art.js.map +1 -0
- package/dist/lib/commands.js +595 -334
- package/dist/lib/commands.js.map +1 -1
- package/dist/lib/config-tracker.d.ts +41 -0
- package/dist/lib/config-tracker.js +58 -0
- package/dist/lib/config-tracker.js.map +1 -0
- package/dist/lib/config.d.ts +8 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/org-env-helper.d.ts +30 -0
- package/dist/lib/org-env-helper.js +78 -0
- package/dist/lib/org-env-helper.js.map +1 -0
- package/dist/lib/smart-prompt.d.ts +79 -0
- package/dist/lib/smart-prompt.js +131 -0
- package/dist/lib/smart-prompt.js.map +1 -0
- package/package.json +1 -1
package/dist/lib/commands.js
CHANGED
|
@@ -10,6 +10,9 @@ import archiver from 'archiver';
|
|
|
10
10
|
import { configureAmplify, loadCliEnvFiles } from './amplify-config.js';
|
|
11
11
|
import { computeFingerprint, embedFingerprintInNativeResources, embedVersionInNativeResources, embedNxConfigurationInNativeResources, } from './fingerprinting.js';
|
|
12
12
|
import { loadNorrixConfig, hasNorrixConfig, saveNorrixConfig, } from './config.js';
|
|
13
|
+
import { fetchOrgEnvVarNames, getOrgEnvVarNamesForField, hasOrgEnvVar, } from './org-env-helper.js';
|
|
14
|
+
import { smartPrompt } from './smart-prompt.js';
|
|
15
|
+
import { ConfigValueTracker } from './config-tracker.js';
|
|
13
16
|
import { detectWorkspaceContext, getNxProjectDependencies, getWorkspaceDependenciesFallback, createWorkspaceManifest, logWorkspaceContext, isAtWorkspaceRoot, discoverNativeScriptApps, getWorkspaceContextForApp, detectNxBuildConfigurations, } from './workspace.js';
|
|
14
17
|
import { signIn as amplifySignIn, signOut as amplifySignOut, getCurrentUser, fetchAuthSession, } from 'aws-amplify/auth';
|
|
15
18
|
import crypto from 'crypto';
|
|
@@ -1443,6 +1446,10 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
1443
1446
|
}
|
|
1444
1447
|
return;
|
|
1445
1448
|
}
|
|
1449
|
+
// Fetch org environment variable names for smart prompting
|
|
1450
|
+
const availableOrgEnvVars = await fetchOrgEnvVarNames(API_URL, await getAuthHeaders({ includeOrg: true }), verbose);
|
|
1451
|
+
// Track user-provided values for potential config saving
|
|
1452
|
+
const configTracker = new ConfigValueTracker();
|
|
1446
1453
|
let spinner;
|
|
1447
1454
|
let originalCwd;
|
|
1448
1455
|
try {
|
|
@@ -1484,15 +1491,15 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
1484
1491
|
platform = chosenPlatform;
|
|
1485
1492
|
}
|
|
1486
1493
|
// 2.1 Determine configuration (CLI arg preferred, then config, otherwise prompt)
|
|
1487
|
-
const validConfigurations = ['
|
|
1494
|
+
const validConfigurations = ['release', 'debug'];
|
|
1488
1495
|
let configuration = (cliConfigurationArg ||
|
|
1489
1496
|
norrixConfig.defaultConfiguration ||
|
|
1490
1497
|
'').toLowerCase();
|
|
1491
1498
|
if (!validConfigurations.includes(configuration)) {
|
|
1492
1499
|
if (opts.nonInteractive) {
|
|
1493
|
-
// Default to '
|
|
1494
|
-
configuration = '
|
|
1495
|
-
logVerbose('No configuration specified, defaulting to
|
|
1500
|
+
// Default to 'release' in non-interactive mode if not specified
|
|
1501
|
+
configuration = 'release';
|
|
1502
|
+
logVerbose('No configuration specified, defaulting to release', verbose);
|
|
1496
1503
|
}
|
|
1497
1504
|
else {
|
|
1498
1505
|
const answer = await inquirer.prompt([
|
|
@@ -1501,7 +1508,7 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
1501
1508
|
name: 'configuration',
|
|
1502
1509
|
message: 'Build configuration:',
|
|
1503
1510
|
choices: validConfigurations,
|
|
1504
|
-
default: norrixConfig.defaultConfiguration || '
|
|
1511
|
+
default: norrixConfig.defaultConfiguration || 'release',
|
|
1505
1512
|
},
|
|
1506
1513
|
]);
|
|
1507
1514
|
configuration = answer.configuration;
|
|
@@ -1664,6 +1671,46 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
1664
1671
|
buildNumber = cliBuildNumber;
|
|
1665
1672
|
logVerbose(`Using build number from CLI flag: ${buildNumber}`, verbose);
|
|
1666
1673
|
}
|
|
1674
|
+
else if (platform === 'ios' || platform === 'visionos') {
|
|
1675
|
+
const inferredApple = appleVersionInfo;
|
|
1676
|
+
const inferredBuildNumber = inferredApple.buildNumber;
|
|
1677
|
+
if (inferredBuildNumber) {
|
|
1678
|
+
logVerbose(`Auto-detected iOS CFBundleVersion from Info.plist: ${inferredBuildNumber}`, verbose);
|
|
1679
|
+
}
|
|
1680
|
+
if (opts.nonInteractive) {
|
|
1681
|
+
// In non-interactive mode, use inferred or leave undefined for auto-increment
|
|
1682
|
+
buildNumber = inferredBuildNumber || undefined;
|
|
1683
|
+
if (buildNumber) {
|
|
1684
|
+
logVerbose(`Using auto-detected build number: ${buildNumber}`, verbose);
|
|
1685
|
+
}
|
|
1686
|
+
else {
|
|
1687
|
+
logVerbose('Build number will be auto-incremented', verbose);
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
else {
|
|
1691
|
+
const { iosBuildNumber } = await inquirer.prompt([
|
|
1692
|
+
{
|
|
1693
|
+
type: 'input',
|
|
1694
|
+
name: 'iosBuildNumber',
|
|
1695
|
+
message: inferredBuildNumber
|
|
1696
|
+
? `App build number, aka CFBundleVersion (${inferredBuildNumber} - enter to auto increment):`
|
|
1697
|
+
: 'App build number, aka CFBundleVersion (enter to auto increment):',
|
|
1698
|
+
default: inferredBuildNumber || '',
|
|
1699
|
+
validate: (input) => {
|
|
1700
|
+
const val = String(input).trim();
|
|
1701
|
+
if (val === '')
|
|
1702
|
+
return true; // allow blank → auto-increment
|
|
1703
|
+
// iOS CFBundleVersion can be numeric or semver-like (e.g., "1.0.0" or "123")
|
|
1704
|
+
if (/^[\d.]+$/.test(val)) {
|
|
1705
|
+
return true;
|
|
1706
|
+
}
|
|
1707
|
+
return 'Enter a numeric build number (e.g., "123" or "1.0.0"). Leave empty to auto-increment.';
|
|
1708
|
+
},
|
|
1709
|
+
},
|
|
1710
|
+
]);
|
|
1711
|
+
buildNumber = String(iosBuildNumber).trim() || undefined;
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1667
1714
|
else if (platform === 'android') {
|
|
1668
1715
|
const inferredAndroid = androidVersionInfo;
|
|
1669
1716
|
const inferredVersionCode = inferredAndroid.buildNumber;
|
|
@@ -1720,295 +1767,342 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
1720
1767
|
const configAscKeyId = norrixConfig.ios?.ascApiKeyId;
|
|
1721
1768
|
const configAscIssuerId = norrixConfig.ios?.ascIssuerId;
|
|
1722
1769
|
const configAscKeyPath = norrixConfig.ios?.ascPrivateKeyPath;
|
|
1723
|
-
const resolvedTeamId = trimString(opts.teamId) || configTeamId;
|
|
1724
|
-
const resolvedP12Path = trimString(opts.p12Path) || configP12Path;
|
|
1725
|
-
const resolvedP12Password = trimString(opts.p12Password);
|
|
1726
|
-
const resolvedProfilePath = trimString(opts.profilePath) || configProfilePath;
|
|
1727
1770
|
const resolvedAscKeyId = trimString(opts.ascKeyId) || configAscKeyId;
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
}
|
|
1761
|
-
if (/^[A-Z0-9]{10}$/.test(input.trim())) {
|
|
1762
|
-
return true;
|
|
1763
|
-
}
|
|
1764
|
-
return 'Team ID should be 10 alphanumeric characters (e.g. "ABC123XYZ"). Leave empty to skip.';
|
|
1765
|
-
},
|
|
1766
|
-
},
|
|
1767
|
-
// ASC API Key is the recommended approach - ask first
|
|
1771
|
+
// Use smart prompts for credentials
|
|
1772
|
+
// Non-interactive mode will auto-select from CLI > org env > config > process env
|
|
1773
|
+
// Interactive mode will confirm org env and config values before using them
|
|
1774
|
+
// Team ID
|
|
1775
|
+
const teamIdResult = await smartPrompt({
|
|
1776
|
+
name: 'teamId',
|
|
1777
|
+
message: 'Apple Developer Team ID (required for code signing, e.g. "ABC123XYZ"):',
|
|
1778
|
+
cliValue: opts.teamId,
|
|
1779
|
+
configValue: configTeamId,
|
|
1780
|
+
orgEnvVarNames: getOrgEnvVarNamesForField('teamId'),
|
|
1781
|
+
availableOrgEnvVars,
|
|
1782
|
+
nonInteractive: opts.nonInteractive,
|
|
1783
|
+
validate: (input) => {
|
|
1784
|
+
if (!input.trim()) {
|
|
1785
|
+
return true; // Allow empty, workflow will try to proceed without it
|
|
1786
|
+
}
|
|
1787
|
+
if (/^[A-Z0-9]{10}$/.test(input.trim())) {
|
|
1788
|
+
return true;
|
|
1789
|
+
}
|
|
1790
|
+
return 'Team ID should be 10 alphanumeric characters (e.g. "ABC123XYZ"). Leave empty to skip.';
|
|
1791
|
+
},
|
|
1792
|
+
});
|
|
1793
|
+
configTracker.track('teamId', teamIdResult.value, teamIdResult.source);
|
|
1794
|
+
// ASC API Key is the recommended approach
|
|
1795
|
+
// Check if any ASC credentials are available in org env vars
|
|
1796
|
+
const hasAscOrgEnvVars = hasOrgEnvVar(getOrgEnvVarNamesForField('ascApiKeyId'), availableOrgEnvVars) ||
|
|
1797
|
+
hasOrgEnvVar(getOrgEnvVarNamesForField('ascIssuerId'), availableOrgEnvVars) ||
|
|
1798
|
+
hasOrgEnvVar(getOrgEnvVarNamesForField('ascPrivateKeyPath'), availableOrgEnvVars);
|
|
1799
|
+
let useAscKey = Boolean(resolvedAscKeyId) || teamIdResult.source === 'org-env' || hasAscOrgEnvVars;
|
|
1800
|
+
if (!opts.nonInteractive && teamIdResult.source !== 'org-env' && !hasAscOrgEnvVars) {
|
|
1801
|
+
// Ask if user wants to use ASC Key (only in interactive mode, and only if no org env vars)
|
|
1802
|
+
const { useAscKeyAnswer } = await inquirer.prompt([
|
|
1768
1803
|
{
|
|
1769
1804
|
type: 'confirm',
|
|
1770
|
-
name: '
|
|
1805
|
+
name: 'useAscKeyAnswer',
|
|
1771
1806
|
message: 'Use App Store Connect API Key for auto-provisioning? (recommended)',
|
|
1772
1807
|
default: Boolean(resolvedAscKeyId) || true,
|
|
1773
1808
|
},
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
}
|
|
1797
|
-
return true;
|
|
1798
|
-
},
|
|
1809
|
+
]);
|
|
1810
|
+
useAscKey = useAscKeyAnswer;
|
|
1811
|
+
}
|
|
1812
|
+
// Collect ASC credentials if using ASC Key
|
|
1813
|
+
let ascApiKeyIdResult;
|
|
1814
|
+
let ascIssuerIdResult;
|
|
1815
|
+
let ascPrivateKeyPathResult;
|
|
1816
|
+
if (useAscKey) {
|
|
1817
|
+
ascApiKeyIdResult = await smartPrompt({
|
|
1818
|
+
name: 'ascApiKeyId',
|
|
1819
|
+
message: 'App Store Connect (ASC) API Key ID:',
|
|
1820
|
+
cliValue: opts.ascKeyId,
|
|
1821
|
+
configValue: configAscKeyId,
|
|
1822
|
+
orgEnvVarNames: getOrgEnvVarNamesForField('ascApiKeyId'),
|
|
1823
|
+
availableOrgEnvVars,
|
|
1824
|
+
nonInteractive: opts.nonInteractive,
|
|
1825
|
+
required: true,
|
|
1826
|
+
validate: (input) => {
|
|
1827
|
+
if (!input.trim()) {
|
|
1828
|
+
return 'API Key ID is required when using App Store Connect (ASC) Key';
|
|
1829
|
+
}
|
|
1830
|
+
return true;
|
|
1799
1831
|
},
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1832
|
+
});
|
|
1833
|
+
configTracker.track('ascApiKeyId', ascApiKeyIdResult.value, ascApiKeyIdResult.source);
|
|
1834
|
+
ascIssuerIdResult = await smartPrompt({
|
|
1835
|
+
name: 'ascIssuerId',
|
|
1836
|
+
message: 'App Store Connect (ASC) Issuer ID:',
|
|
1837
|
+
cliValue: opts.ascIssuerId,
|
|
1838
|
+
configValue: configAscIssuerId,
|
|
1839
|
+
orgEnvVarNames: getOrgEnvVarNamesForField('ascIssuerId'),
|
|
1840
|
+
availableOrgEnvVars,
|
|
1841
|
+
nonInteractive: opts.nonInteractive,
|
|
1842
|
+
required: true,
|
|
1843
|
+
validate: (input) => {
|
|
1844
|
+
if (!input.trim()) {
|
|
1845
|
+
return 'Issuer ID is required when using App Store Connect (ASC) Key';
|
|
1846
|
+
}
|
|
1847
|
+
return true;
|
|
1812
1848
|
},
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1849
|
+
});
|
|
1850
|
+
configTracker.track('ascIssuerId', ascIssuerIdResult.value, ascIssuerIdResult.source);
|
|
1851
|
+
ascPrivateKeyPathResult = await smartPrompt({
|
|
1852
|
+
name: 'ascPrivateKeyPath',
|
|
1853
|
+
message: 'Path to App Store Connect (ASC) private key .p8:',
|
|
1854
|
+
cliValue: opts.ascKeyPath,
|
|
1855
|
+
configValue: configAscKeyPath,
|
|
1856
|
+
orgEnvVarNames: getOrgEnvVarNamesForField('ascPrivateKeyPath'),
|
|
1857
|
+
availableOrgEnvVars,
|
|
1858
|
+
nonInteractive: opts.nonInteractive,
|
|
1859
|
+
required: true,
|
|
1860
|
+
validate: (input) => {
|
|
1861
|
+
if (!input.trim()) {
|
|
1862
|
+
return 'Path to .p8 key file is required when using App Store Connect (ASC) Key';
|
|
1863
|
+
}
|
|
1864
|
+
return true;
|
|
1820
1865
|
},
|
|
1821
|
-
|
|
1822
|
-
|
|
1866
|
+
});
|
|
1867
|
+
configTracker.track('ascPrivateKeyPath', ascPrivateKeyPathResult.value, ascPrivateKeyPathResult.source);
|
|
1868
|
+
}
|
|
1869
|
+
// Collect p12/mobileprovision credentials if NOT using ASC Key
|
|
1870
|
+
let p12PathResult;
|
|
1871
|
+
let p12PasswordResult;
|
|
1872
|
+
let mobileprovisionPathResult;
|
|
1873
|
+
if (!useAscKey) {
|
|
1874
|
+
p12PathResult = await smartPrompt({
|
|
1875
|
+
name: 'p12Path',
|
|
1876
|
+
message: 'Path to iOS .p12 certificate (optional):',
|
|
1877
|
+
cliValue: opts.p12Path,
|
|
1878
|
+
configValue: configP12Path,
|
|
1879
|
+
orgEnvVarNames: getOrgEnvVarNamesForField('p12Path'),
|
|
1880
|
+
availableOrgEnvVars,
|
|
1881
|
+
nonInteractive: opts.nonInteractive,
|
|
1882
|
+
required: false,
|
|
1883
|
+
});
|
|
1884
|
+
configTracker.track('p12Path', p12PathResult.value, p12PathResult.source);
|
|
1885
|
+
// Only prompt for p12 password if p12 path was provided
|
|
1886
|
+
if (p12PathResult.value) {
|
|
1887
|
+
p12PasswordResult = await smartPrompt({
|
|
1823
1888
|
name: 'p12Password',
|
|
1824
1889
|
message: 'Password for .p12 (if any):',
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
when: (a) => !a.useAscKey,
|
|
1835
|
-
},
|
|
1836
|
-
]);
|
|
1837
|
-
// Use resolved teamId from CLI/config, or from prompt
|
|
1838
|
-
const finalTeamId = trimString(iosAnswers.teamId) || resolvedTeamId;
|
|
1839
|
-
iosCredentials = {
|
|
1840
|
-
teamId: finalTeamId || undefined,
|
|
1841
|
-
p12Base64: readOptionalFileAsBase64(iosAnswers.p12Path),
|
|
1842
|
-
p12Password: trimString(iosAnswers.p12Password),
|
|
1843
|
-
mobileprovisionBase64: readOptionalFileAsBase64(iosAnswers.mobileprovisionPath),
|
|
1844
|
-
ascApiKeyId: trimString(iosAnswers.ascApiKeyId),
|
|
1845
|
-
ascIssuerId: trimString(iosAnswers.ascIssuerId),
|
|
1846
|
-
ascPrivateKey: readOptionalFileAsBase64(iosAnswers.ascPrivateKeyPath),
|
|
1847
|
-
// Track paths for config saving (not sent to API)
|
|
1848
|
-
_p12Path: trimString(iosAnswers.p12Path),
|
|
1849
|
-
_mobileprovisionPath: trimString(iosAnswers.mobileprovisionPath),
|
|
1850
|
-
_ascApiKeyId: trimString(iosAnswers.ascApiKeyId),
|
|
1851
|
-
_ascIssuerId: trimString(iosAnswers.ascIssuerId),
|
|
1852
|
-
_ascPrivateKeyPath: trimString(iosAnswers.ascPrivateKeyPath),
|
|
1853
|
-
};
|
|
1854
|
-
if (finalTeamId) {
|
|
1855
|
-
console.log(`Using Apple Team ID: ${finalTeamId}`);
|
|
1890
|
+
cliValue: opts.p12Password,
|
|
1891
|
+
configValue: undefined, // Don't save passwords in config
|
|
1892
|
+
orgEnvVarNames: getOrgEnvVarNamesForField('p12Password'),
|
|
1893
|
+
availableOrgEnvVars,
|
|
1894
|
+
nonInteractive: opts.nonInteractive,
|
|
1895
|
+
mask: true,
|
|
1896
|
+
required: false,
|
|
1897
|
+
});
|
|
1898
|
+
configTracker.track('p12Password', p12PasswordResult.value, p12PasswordResult.source);
|
|
1856
1899
|
}
|
|
1900
|
+
mobileprovisionPathResult = await smartPrompt({
|
|
1901
|
+
name: 'provisioningProfilePath',
|
|
1902
|
+
message: 'Path to provisioning profile .mobileprovision (optional):',
|
|
1903
|
+
cliValue: opts.profilePath,
|
|
1904
|
+
configValue: configProfilePath,
|
|
1905
|
+
orgEnvVarNames: getOrgEnvVarNamesForField('provisioningProfilePath'),
|
|
1906
|
+
availableOrgEnvVars,
|
|
1907
|
+
nonInteractive: opts.nonInteractive,
|
|
1908
|
+
required: false,
|
|
1909
|
+
});
|
|
1910
|
+
configTracker.track('provisioningProfilePath', mobileprovisionPathResult.value, mobileprovisionPathResult.source);
|
|
1911
|
+
}
|
|
1912
|
+
// Build iosCredentials object
|
|
1913
|
+
// If value is undefined (org env var), don't include in API request
|
|
1914
|
+
iosCredentials = {
|
|
1915
|
+
teamId: teamIdResult.value || undefined,
|
|
1916
|
+
p12Base64: p12PathResult?.value ? readOptionalFileAsBase64(p12PathResult.value) : undefined,
|
|
1917
|
+
p12Password: p12PasswordResult?.value || undefined,
|
|
1918
|
+
mobileprovisionBase64: mobileprovisionPathResult?.value
|
|
1919
|
+
? readOptionalFileAsBase64(mobileprovisionPathResult.value)
|
|
1920
|
+
: undefined,
|
|
1921
|
+
ascApiKeyId: ascApiKeyIdResult?.value || undefined,
|
|
1922
|
+
ascIssuerId: ascIssuerIdResult?.value || undefined,
|
|
1923
|
+
ascPrivateKey: ascPrivateKeyPathResult?.value
|
|
1924
|
+
? readOptionalFileAsBase64(ascPrivateKeyPathResult.value)
|
|
1925
|
+
: undefined,
|
|
1926
|
+
// Track paths for config saving (not sent to API)
|
|
1927
|
+
_p12Path: p12PathResult?.value,
|
|
1928
|
+
_mobileprovisionPath: mobileprovisionPathResult?.value,
|
|
1929
|
+
_ascApiKeyId: ascApiKeyIdResult?.value,
|
|
1930
|
+
_ascIssuerId: ascIssuerIdResult?.value,
|
|
1931
|
+
_ascPrivateKeyPath: ascPrivateKeyPathResult?.value,
|
|
1932
|
+
};
|
|
1933
|
+
if (teamIdResult.value) {
|
|
1934
|
+
console.log(`Using Apple Team ID: ${teamIdResult.value}`);
|
|
1935
|
+
}
|
|
1936
|
+
if (teamIdResult.source === 'org-env') {
|
|
1937
|
+
console.log(`Using organization's iOS credentials`);
|
|
1857
1938
|
}
|
|
1858
1939
|
}
|
|
1859
1940
|
else if (platform === 'android') {
|
|
1860
|
-
//
|
|
1941
|
+
// Use smart prompts for Android credentials
|
|
1861
1942
|
const configKeystorePath = norrixConfig.android?.keystorePath;
|
|
1862
1943
|
const configKeyAlias = norrixConfig.android?.keyAlias;
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1944
|
+
// Keystore path
|
|
1945
|
+
const keystorePathResult = await smartPrompt({
|
|
1946
|
+
name: 'keystorePath',
|
|
1947
|
+
message: 'Path to Android keystore .jks/.keystore (optional):',
|
|
1948
|
+
cliValue: opts.keystorePath,
|
|
1949
|
+
configValue: configKeystorePath,
|
|
1950
|
+
orgEnvVarNames: getOrgEnvVarNamesForField('keystorePath'),
|
|
1951
|
+
availableOrgEnvVars,
|
|
1952
|
+
processEnvNames: ['KEYSTORE_PATH'],
|
|
1953
|
+
nonInteractive: opts.nonInteractive,
|
|
1954
|
+
required: false,
|
|
1955
|
+
});
|
|
1956
|
+
configTracker.track('keystorePath', keystorePathResult.value, keystorePathResult.source);
|
|
1957
|
+
// Keystore password
|
|
1958
|
+
const keystorePasswordResult = await smartPrompt({
|
|
1959
|
+
name: 'keystorePassword',
|
|
1960
|
+
message: 'Keystore password (optional):',
|
|
1961
|
+
cliValue: opts.keystorePassword,
|
|
1962
|
+
configValue: undefined, // Don't save passwords in config
|
|
1963
|
+
orgEnvVarNames: getOrgEnvVarNamesForField('keystorePassword'),
|
|
1964
|
+
availableOrgEnvVars,
|
|
1965
|
+
processEnvNames: ['KEYSTORE_PASSWORD'],
|
|
1966
|
+
nonInteractive: opts.nonInteractive,
|
|
1967
|
+
mask: true,
|
|
1968
|
+
required: false,
|
|
1969
|
+
});
|
|
1970
|
+
configTracker.track('keystorePassword', keystorePasswordResult.value, keystorePasswordResult.source);
|
|
1971
|
+
// Key alias
|
|
1972
|
+
const keyAliasResult = await smartPrompt({
|
|
1973
|
+
name: 'keyAlias',
|
|
1974
|
+
message: 'Key alias (optional):',
|
|
1975
|
+
cliValue: opts.keyAlias,
|
|
1976
|
+
configValue: configKeyAlias,
|
|
1977
|
+
orgEnvVarNames: getOrgEnvVarNamesForField('keyAlias'),
|
|
1978
|
+
availableOrgEnvVars,
|
|
1979
|
+
processEnvNames: ['KEYSTORE_ALIAS'],
|
|
1980
|
+
nonInteractive: opts.nonInteractive,
|
|
1981
|
+
required: false,
|
|
1982
|
+
});
|
|
1983
|
+
configTracker.track('keyAlias', keyAliasResult.value, keyAliasResult.source);
|
|
1984
|
+
// Key password
|
|
1985
|
+
const keyPasswordResult = await smartPrompt({
|
|
1986
|
+
name: 'keyPassword',
|
|
1987
|
+
message: 'Key password (optional):',
|
|
1988
|
+
cliValue: opts.keyPassword,
|
|
1989
|
+
configValue: undefined, // Don't save passwords in config
|
|
1990
|
+
orgEnvVarNames: getOrgEnvVarNamesForField('keyPassword'),
|
|
1991
|
+
availableOrgEnvVars,
|
|
1992
|
+
processEnvNames: ['KEYSTORE_ALIAS_PASSWORD', 'KEY_PASSWORD'],
|
|
1993
|
+
nonInteractive: opts.nonInteractive,
|
|
1994
|
+
mask: true,
|
|
1995
|
+
required: false,
|
|
1996
|
+
});
|
|
1997
|
+
configTracker.track('keyPassword', keyPasswordResult.value, keyPasswordResult.source);
|
|
1998
|
+
// Google Play service account JSON
|
|
1999
|
+
let playJsonPathResult;
|
|
2000
|
+
// In non-interactive mode, check if value exists
|
|
1875
2001
|
if (opts.nonInteractive) {
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
}
|
|
1888
|
-
if (resolvedKeyAlias) {
|
|
1889
|
-
logVerbose(`Using key alias: ${resolvedKeyAlias}`, verbose);
|
|
1890
|
-
}
|
|
2002
|
+
playJsonPathResult = await smartPrompt({
|
|
2003
|
+
name: 'playServiceAccountJsonPath',
|
|
2004
|
+
message: 'Path to Google Play service account JSON (optional):',
|
|
2005
|
+
cliValue: opts.playJsonPath,
|
|
2006
|
+
configValue: undefined,
|
|
2007
|
+
orgEnvVarNames: getOrgEnvVarNamesForField('playServiceAccountJsonPath'),
|
|
2008
|
+
availableOrgEnvVars,
|
|
2009
|
+
processEnvNames: ['PLAY_SERVICE_ACCOUNT_JSON_PATH'],
|
|
2010
|
+
nonInteractive: true,
|
|
2011
|
+
required: false,
|
|
2012
|
+
});
|
|
1891
2013
|
}
|
|
1892
2014
|
else {
|
|
1893
|
-
//
|
|
1894
|
-
const
|
|
1895
|
-
{
|
|
1896
|
-
type: 'input',
|
|
1897
|
-
name: 'keystorePath',
|
|
1898
|
-
message: 'Path to Android keystore .jks/.keystore (optional):',
|
|
1899
|
-
default: resolvedKeystorePath || '',
|
|
1900
|
-
},
|
|
1901
|
-
{
|
|
1902
|
-
type: 'password',
|
|
1903
|
-
name: 'keystorePassword',
|
|
1904
|
-
message: 'Keystore password (optional):',
|
|
1905
|
-
mask: '*',
|
|
1906
|
-
default: '',
|
|
1907
|
-
},
|
|
1908
|
-
{
|
|
1909
|
-
type: 'input',
|
|
1910
|
-
name: 'keyAlias',
|
|
1911
|
-
message: 'Key alias (optional):',
|
|
1912
|
-
default: resolvedKeyAlias || '',
|
|
1913
|
-
},
|
|
1914
|
-
{
|
|
1915
|
-
type: 'password',
|
|
1916
|
-
name: 'keyPassword',
|
|
1917
|
-
message: 'Key password (optional):',
|
|
1918
|
-
mask: '*',
|
|
1919
|
-
default: '',
|
|
1920
|
-
},
|
|
2015
|
+
// In interactive mode, ask if they want to provide it first
|
|
2016
|
+
const { hasPlayJson } = await inquirer.prompt([
|
|
1921
2017
|
{
|
|
1922
2018
|
type: 'confirm',
|
|
1923
2019
|
name: 'hasPlayJson',
|
|
1924
2020
|
message: 'Provide Google Play service account JSON? (optional, for Play operations)',
|
|
1925
2021
|
default: false,
|
|
1926
2022
|
},
|
|
1927
|
-
{
|
|
1928
|
-
type: 'input',
|
|
1929
|
-
name: 'playJsonPath',
|
|
1930
|
-
message: 'Path to Google Play service account JSON (optional):',
|
|
1931
|
-
default: '',
|
|
1932
|
-
when: (a) => a.hasPlayJson,
|
|
1933
|
-
},
|
|
1934
2023
|
]);
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
2024
|
+
if (hasPlayJson) {
|
|
2025
|
+
playJsonPathResult = await smartPrompt({
|
|
2026
|
+
name: 'playServiceAccountJsonPath',
|
|
2027
|
+
message: 'Path to Google Play service account JSON (optional):',
|
|
2028
|
+
cliValue: opts.playJsonPath,
|
|
2029
|
+
configValue: undefined,
|
|
2030
|
+
orgEnvVarNames: getOrgEnvVarNamesForField('playServiceAccountJsonPath'),
|
|
2031
|
+
availableOrgEnvVars,
|
|
2032
|
+
processEnvNames: ['PLAY_SERVICE_ACCOUNT_JSON_PATH'],
|
|
2033
|
+
nonInteractive: false,
|
|
2034
|
+
required: false,
|
|
2035
|
+
});
|
|
2036
|
+
}
|
|
1945
2037
|
}
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
2038
|
+
if (playJsonPathResult) {
|
|
2039
|
+
configTracker.track('playServiceAccountJsonPath', playJsonPathResult.value, playJsonPathResult.source);
|
|
2040
|
+
}
|
|
2041
|
+
// Build androidCredentials object
|
|
2042
|
+
androidCredentials = {
|
|
2043
|
+
keystoreBase64: keystorePathResult.value
|
|
2044
|
+
? readOptionalFileAsBase64(keystorePathResult.value)
|
|
2045
|
+
: undefined,
|
|
2046
|
+
keystorePassword: keystorePasswordResult.value || undefined,
|
|
2047
|
+
keyAlias: keyAliasResult.value || undefined,
|
|
2048
|
+
keyPassword: keyPasswordResult.value || undefined,
|
|
2049
|
+
playServiceAccountJson: playJsonPathResult?.value
|
|
2050
|
+
? readOptionalFileAsBase64(playJsonPathResult.value)
|
|
2051
|
+
: undefined,
|
|
1953
2052
|
};
|
|
1954
|
-
if (
|
|
1955
|
-
|
|
1956
|
-
saveableOptions.teamId = iosCredentials.teamId;
|
|
1957
|
-
}
|
|
1958
|
-
if (distributionType) {
|
|
1959
|
-
saveableOptions.distributionType = distributionType;
|
|
1960
|
-
}
|
|
1961
|
-
// Don't save actual credential file paths since they may contain secrets
|
|
1962
|
-
// But save paths that users can re-use
|
|
1963
|
-
if (iosCredentials._p12Path) {
|
|
1964
|
-
saveableOptions.p12Path = iosCredentials._p12Path;
|
|
1965
|
-
}
|
|
1966
|
-
if (iosCredentials._mobileprovisionPath) {
|
|
1967
|
-
saveableOptions.provisioningProfilePath = iosCredentials._mobileprovisionPath;
|
|
1968
|
-
}
|
|
1969
|
-
// Save ASC API Key details for future builds
|
|
1970
|
-
if (iosCredentials._ascApiKeyId) {
|
|
1971
|
-
saveableOptions.ascApiKeyId = iosCredentials._ascApiKeyId;
|
|
1972
|
-
}
|
|
1973
|
-
if (iosCredentials._ascIssuerId) {
|
|
1974
|
-
saveableOptions.ascIssuerId = iosCredentials._ascIssuerId;
|
|
1975
|
-
}
|
|
1976
|
-
if (iosCredentials._ascPrivateKeyPath) {
|
|
1977
|
-
saveableOptions.ascPrivateKeyPath = iosCredentials._ascPrivateKeyPath;
|
|
1978
|
-
}
|
|
2053
|
+
if (keystorePathResult.value) {
|
|
2054
|
+
logVerbose(`Using keystore from: ${keystorePathResult.value}`, verbose);
|
|
1979
2055
|
}
|
|
1980
|
-
if (
|
|
1981
|
-
|
|
1982
|
-
saveableOptions.keystorePath = androidCredentials._keystorePath;
|
|
1983
|
-
}
|
|
1984
|
-
if (androidCredentials._keyAlias) {
|
|
1985
|
-
saveableOptions.keyAlias = androidCredentials._keyAlias;
|
|
1986
|
-
}
|
|
2056
|
+
if (keystorePathResult.source === 'org-env') {
|
|
2057
|
+
console.log(`Using organization's Android credentials`);
|
|
1987
2058
|
}
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2059
|
+
}
|
|
2060
|
+
// Offer to save config if user provided new values
|
|
2061
|
+
const appRoot = process.cwd();
|
|
2062
|
+
if (!opts.nonInteractive && configTracker.shouldOfferToSave()) {
|
|
2063
|
+
const configExists = hasNorrixConfig(appRoot);
|
|
2064
|
+
const { shouldSave } = await inquirer.prompt([
|
|
2065
|
+
{
|
|
2066
|
+
type: 'confirm',
|
|
2067
|
+
name: 'shouldSave',
|
|
2068
|
+
message: configExists
|
|
2069
|
+
? 'Save new credential paths to norrix.config.ts?'
|
|
2070
|
+
: 'Save credential paths to norrix.config.ts for future builds?',
|
|
2071
|
+
default: true,
|
|
2072
|
+
},
|
|
2073
|
+
]);
|
|
2074
|
+
if (shouldSave) {
|
|
2075
|
+
try {
|
|
2076
|
+
const userProvidedValues = configTracker.getUserProvidedValues();
|
|
2077
|
+
// Start with existing config values (if config exists)
|
|
2078
|
+
const existingConfig = configExists ? norrixConfig : {};
|
|
2079
|
+
const saveableOptions = {
|
|
2080
|
+
platform: platform,
|
|
2081
|
+
};
|
|
2082
|
+
// Merge: Start with existing config, then overlay user-provided values
|
|
2083
|
+
// iOS fields
|
|
2084
|
+
if (platform === 'ios') {
|
|
2085
|
+
saveableOptions.teamId = userProvidedValues.teamId || existingConfig.ios?.teamId;
|
|
2086
|
+
saveableOptions.ascApiKeyId = userProvidedValues.ascApiKeyId || existingConfig.ios?.ascApiKeyId;
|
|
2087
|
+
saveableOptions.ascIssuerId = userProvidedValues.ascIssuerId || existingConfig.ios?.ascIssuerId;
|
|
2088
|
+
saveableOptions.ascPrivateKeyPath = userProvidedValues.ascPrivateKeyPath || existingConfig.ios?.ascPrivateKeyPath;
|
|
2089
|
+
saveableOptions.p12Path = userProvidedValues.p12Path || existingConfig.ios?.p12Path;
|
|
2090
|
+
saveableOptions.provisioningProfilePath = userProvidedValues.provisioningProfilePath || existingConfig.ios?.provisioningProfilePath;
|
|
2091
|
+
saveableOptions.distributionType = (distributionType || existingConfig.ios?.distributionType);
|
|
2008
2092
|
}
|
|
2009
|
-
|
|
2010
|
-
|
|
2093
|
+
// Android fields
|
|
2094
|
+
if (platform === 'android') {
|
|
2095
|
+
saveableOptions.keystorePath = userProvidedValues.keystorePath || existingConfig.android?.keystorePath;
|
|
2096
|
+
saveableOptions.keyAlias = userProvidedValues.keyAlias || existingConfig.android?.keyAlias;
|
|
2097
|
+
saveableOptions.distributionType = (distributionType || existingConfig.android?.distributionType);
|
|
2011
2098
|
}
|
|
2099
|
+
const savedPath = saveNorrixConfig(appRoot, saveableOptions);
|
|
2100
|
+
console.log(configExists
|
|
2101
|
+
? `✓ Configuration updated in ${path.basename(savedPath)}`
|
|
2102
|
+
: `✓ Configuration saved to ${path.basename(savedPath)}`);
|
|
2103
|
+
}
|
|
2104
|
+
catch (saveError) {
|
|
2105
|
+
console.warn(`Warning: Could not save config file: ${saveError.message}`);
|
|
2012
2106
|
}
|
|
2013
2107
|
}
|
|
2014
2108
|
}
|
|
@@ -2181,12 +2275,15 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
2181
2275
|
spinner.succeed('Build started successfully!');
|
|
2182
2276
|
console.log('✅ Your app is being built on Norrix.');
|
|
2183
2277
|
console.log(` Build ID: ${buildId}`);
|
|
2184
|
-
|
|
2185
|
-
if (
|
|
2186
|
-
console.log(`
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2278
|
+
// Only show backend details in dev mode
|
|
2279
|
+
if (CURRENT_ENV === 'dev') {
|
|
2280
|
+
console.log(` Backend API: ${API_URL}`);
|
|
2281
|
+
if (process.env.NORRIX_API_URL) {
|
|
2282
|
+
console.log(` NORRIX_API_URL: ${process.env.NORRIX_API_URL}`);
|
|
2283
|
+
}
|
|
2284
|
+
if (dispatchedEnv) {
|
|
2285
|
+
console.log(` Backend env: ${dispatchedEnv}`);
|
|
2286
|
+
}
|
|
2190
2287
|
}
|
|
2191
2288
|
console.log(formatVersionBuildLine(recordedVersion, recordedBuildNumber));
|
|
2192
2289
|
console.log(` Platform: ${platform}`);
|
|
@@ -2239,6 +2336,41 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
2239
2336
|
}
|
|
2240
2337
|
}
|
|
2241
2338
|
}
|
|
2339
|
+
/**
|
|
2340
|
+
* Prompt for TestFlight group names for external testing distribution.
|
|
2341
|
+
* Shows confirmation to use default group or enter custom group names.
|
|
2342
|
+
*/
|
|
2343
|
+
async function promptForTestFlightGroups(configGroups) {
|
|
2344
|
+
const { useDefaultGroup } = await inquirer.prompt([
|
|
2345
|
+
{
|
|
2346
|
+
type: 'confirm',
|
|
2347
|
+
name: 'useDefaultGroup',
|
|
2348
|
+
message: `Use the default TestFlight group 'External Testers'?\n\n` +
|
|
2349
|
+
`When using External testing, make sure the group exists in App Store Connect.\n` +
|
|
2350
|
+
`Choose 'Y' if you have a group named 'External Testers' already created.\n` +
|
|
2351
|
+
`Choose 'N' to enter custom group names.`,
|
|
2352
|
+
default: true,
|
|
2353
|
+
},
|
|
2354
|
+
]);
|
|
2355
|
+
if (useDefaultGroup) {
|
|
2356
|
+
return configGroups || ['External Testers'];
|
|
2357
|
+
}
|
|
2358
|
+
const { groupNames } = await inquirer.prompt([
|
|
2359
|
+
{
|
|
2360
|
+
type: 'input',
|
|
2361
|
+
name: 'groupNames',
|
|
2362
|
+
message: 'Please enter TestFlight group name(s) (comma-separated):',
|
|
2363
|
+
validate: (input) => {
|
|
2364
|
+
const trimmed = input.trim();
|
|
2365
|
+
return trimmed.length > 0 || 'At least one group name is required';
|
|
2366
|
+
},
|
|
2367
|
+
},
|
|
2368
|
+
]);
|
|
2369
|
+
return groupNames
|
|
2370
|
+
.split(',')
|
|
2371
|
+
.map((g) => g.trim())
|
|
2372
|
+
.filter((g) => g.length > 0);
|
|
2373
|
+
}
|
|
2242
2374
|
/**
|
|
2243
2375
|
* Submit command implementation
|
|
2244
2376
|
* Submits the built app to app stores via the Next.js API gateway
|
|
@@ -2275,6 +2407,10 @@ export async function submit(cliPlatformArg, cliTrackArg, verbose = false, optio
|
|
|
2275
2407
|
}
|
|
2276
2408
|
// Load Norrix config file from the resolved app directory
|
|
2277
2409
|
const norrixConfig = await loadNorrixConfig(process.cwd());
|
|
2410
|
+
// Fetch org env vars for smart prompting
|
|
2411
|
+
const availableOrgEnvVars = await fetchOrgEnvVarNames(API_URL, await getAuthHeaders({ includeOrg: true }), verbose);
|
|
2412
|
+
// Track user-provided values for potential config saving
|
|
2413
|
+
const configTracker = new ConfigValueTracker();
|
|
2278
2414
|
if (verbose) {
|
|
2279
2415
|
console.log(`[submit] Resolved workspace context: ${resolved.workspaceContext.type}`);
|
|
2280
2416
|
console.log(`[submit] App root: ${resolved.workspaceContext.appRoot}`);
|
|
@@ -2282,8 +2418,15 @@ export async function submit(cliPlatformArg, cliTrackArg, verbose = false, optio
|
|
|
2282
2418
|
if (norrixConfig.ios?.ascApiKeyId) {
|
|
2283
2419
|
console.log(`[submit] Found ASC credentials in norrix.config.ts`);
|
|
2284
2420
|
}
|
|
2421
|
+
if (availableOrgEnvVars.length > 0) {
|
|
2422
|
+
console.log(`[submit] Available org env vars: ${availableOrgEnvVars.join(', ')}`);
|
|
2423
|
+
}
|
|
2285
2424
|
}
|
|
2286
|
-
// 1.
|
|
2425
|
+
// 1. Determine platform from CLI arg first (to filter builds)
|
|
2426
|
+
const validPlatforms = ['android', 'ios', 'visionos'];
|
|
2427
|
+
let platform = (cliPlatformArg || '').toLowerCase();
|
|
2428
|
+
const platformSpecified = validPlatforms.includes(platform);
|
|
2429
|
+
// 2. Get available builds to show in prompt
|
|
2287
2430
|
spinner.text = 'Fetching available builds...';
|
|
2288
2431
|
let availableBuilds = [];
|
|
2289
2432
|
try {
|
|
@@ -2293,9 +2436,13 @@ export async function submit(cliPlatformArg, cliTrackArg, verbose = false, optio
|
|
|
2293
2436
|
if (buildsResponse.data.builds &&
|
|
2294
2437
|
Array.isArray(buildsResponse.data.builds)) {
|
|
2295
2438
|
// Filter for successful builds
|
|
2296
|
-
|
|
2297
|
-
.filter((build) => build.status === 'success')
|
|
2298
|
-
|
|
2439
|
+
let builds = buildsResponse.data.builds
|
|
2440
|
+
.filter((build) => build.status === 'success');
|
|
2441
|
+
// If platform was specified in CLI arg, filter to only that platform
|
|
2442
|
+
if (platformSpecified) {
|
|
2443
|
+
builds = builds.filter((build) => build.platform === platform);
|
|
2444
|
+
}
|
|
2445
|
+
availableBuilds = builds.map((build) => {
|
|
2299
2446
|
return {
|
|
2300
2447
|
name: `${build.projectName} (${build.platform} - ${build.version} (${build.buildNumber}) - ${build.id})`,
|
|
2301
2448
|
value: build.id,
|
|
@@ -2309,7 +2456,7 @@ export async function submit(cliPlatformArg, cliTrackArg, verbose = false, optio
|
|
|
2309
2456
|
catch (error) {
|
|
2310
2457
|
spinner.warn("Could not fetch builds. You'll need to enter the Build ID manually.");
|
|
2311
2458
|
}
|
|
2312
|
-
//
|
|
2459
|
+
// 3. Gather submission details from user
|
|
2313
2460
|
spinner.stop();
|
|
2314
2461
|
// Ask for build ID
|
|
2315
2462
|
let buildId;
|
|
@@ -2335,9 +2482,7 @@ export async function submit(cliPlatformArg, cliTrackArg, verbose = false, optio
|
|
|
2335
2482
|
]);
|
|
2336
2483
|
buildId = buildIdAnswer.buildId;
|
|
2337
2484
|
}
|
|
2338
|
-
//
|
|
2339
|
-
let platform = (cliPlatformArg || '').toLowerCase();
|
|
2340
|
-
const validPlatforms = ['android', 'ios', 'visionos'];
|
|
2485
|
+
// If platform wasn't specified, determine from selected build or prompt
|
|
2341
2486
|
// Fallback to platform from selected build if CLI arg missing/invalid
|
|
2342
2487
|
if (!validPlatforms.includes(platform)) {
|
|
2343
2488
|
const selectedBuild = availableBuilds.find((build) => build.value === buildId);
|
|
@@ -2357,26 +2502,53 @@ export async function submit(cliPlatformArg, cliTrackArg, verbose = false, optio
|
|
|
2357
2502
|
]);
|
|
2358
2503
|
platform = platformAnswer.platform;
|
|
2359
2504
|
}
|
|
2360
|
-
//
|
|
2505
|
+
// Platform-specific track configuration
|
|
2361
2506
|
const validTracks = ['production', 'beta', 'alpha', 'internal'];
|
|
2507
|
+
let trackChoices;
|
|
2508
|
+
if (platform === 'ios' || platform === 'visionos') {
|
|
2509
|
+
trackChoices = [
|
|
2510
|
+
{ name: 'Internal testing', value: 'internal' },
|
|
2511
|
+
{ name: 'External testing', value: 'beta' },
|
|
2512
|
+
];
|
|
2513
|
+
}
|
|
2514
|
+
else if (platform === 'android') {
|
|
2515
|
+
trackChoices = [
|
|
2516
|
+
{ name: 'Internal testing', value: 'internal' },
|
|
2517
|
+
{ name: 'Closed testing', value: 'alpha' },
|
|
2518
|
+
{ name: 'Open testing', value: 'beta' },
|
|
2519
|
+
{ name: 'Production', value: 'production' },
|
|
2520
|
+
];
|
|
2521
|
+
}
|
|
2522
|
+
else {
|
|
2523
|
+
// Fallback for unknown platforms
|
|
2524
|
+
trackChoices = [
|
|
2525
|
+
{ name: 'Internal testing', value: 'internal' },
|
|
2526
|
+
{ name: 'External testing', value: 'beta' },
|
|
2527
|
+
];
|
|
2528
|
+
}
|
|
2529
|
+
// Show track prompt if not provided via CLI
|
|
2362
2530
|
let track = (cliTrackArg || '').toLowerCase();
|
|
2363
|
-
if (!validTracks.includes(track)) {
|
|
2531
|
+
if (!validTracks.includes(track) && !options?.nonInteractive) {
|
|
2364
2532
|
const trackAnswer = await inquirer.prompt([
|
|
2365
2533
|
{
|
|
2366
2534
|
type: 'list',
|
|
2367
2535
|
name: 'track',
|
|
2368
2536
|
message: 'Select release track:',
|
|
2369
|
-
choices:
|
|
2370
|
-
|
|
2371
|
-
{ name: 'Beta', value: 'beta' },
|
|
2372
|
-
{ name: 'Alpha', value: 'alpha' },
|
|
2373
|
-
{ name: 'Internal testing', value: 'internal' },
|
|
2374
|
-
],
|
|
2375
|
-
default: 'production',
|
|
2537
|
+
choices: trackChoices,
|
|
2538
|
+
default: 'internal',
|
|
2376
2539
|
},
|
|
2377
2540
|
]);
|
|
2378
2541
|
track = trackAnswer.track;
|
|
2379
2542
|
}
|
|
2543
|
+
// Validate track for platform
|
|
2544
|
+
if ((platform === 'ios' || platform === 'visionos') && !['internal', 'beta'].includes(track)) {
|
|
2545
|
+
if (options?.nonInteractive) {
|
|
2546
|
+
throw new Error(`Invalid track '${track}' for iOS/visionOS. Use 'internal' or 'beta'.`);
|
|
2547
|
+
}
|
|
2548
|
+
// Fall back to internal if invalid
|
|
2549
|
+
console.warn(`⚠️ Track '${track}' not supported for iOS/visionOS. Using 'internal'.`);
|
|
2550
|
+
track = 'internal';
|
|
2551
|
+
}
|
|
2380
2552
|
// Platform-specific configuration
|
|
2381
2553
|
let credentials = {};
|
|
2382
2554
|
// For iOS, use config values or ask for App Store account
|
|
@@ -2415,35 +2587,49 @@ export async function submit(cliPlatformArg, cliTrackArg, verbose = false, optio
|
|
|
2415
2587
|
};
|
|
2416
2588
|
}
|
|
2417
2589
|
else {
|
|
2418
|
-
// Fall back to prompting for credentials
|
|
2590
|
+
// Fall back to prompting for credentials using smart prompts
|
|
2419
2591
|
if (hasConfigAscCredentials && !fs.existsSync(configAscKeyPath)) {
|
|
2420
2592
|
console.warn(`⚠️ ASC private key not found at: ${configAscKeyPath}`);
|
|
2421
2593
|
}
|
|
2422
|
-
|
|
2594
|
+
// Use smart prompts for ASC credentials
|
|
2595
|
+
const ascApiKeyIdResult = await smartPrompt({
|
|
2596
|
+
name: 'ascApiKeyId',
|
|
2597
|
+
message: 'App Store Connect (ASC) API Key ID (optional):',
|
|
2598
|
+
configValue: configAscKeyId,
|
|
2599
|
+
orgEnvVarNames: getOrgEnvVarNamesForField('ascApiKeyId'),
|
|
2600
|
+
availableOrgEnvVars,
|
|
2601
|
+
nonInteractive: options?.nonInteractive,
|
|
2602
|
+
required: false,
|
|
2603
|
+
});
|
|
2604
|
+
configTracker.track('ascApiKeyId', ascApiKeyIdResult.value, ascApiKeyIdResult.source);
|
|
2605
|
+
const ascIssuerIdResult = await smartPrompt({
|
|
2606
|
+
name: 'ascIssuerId',
|
|
2607
|
+
message: 'App Store Connect (ASC) Issuer ID (optional):',
|
|
2608
|
+
configValue: configAscIssuerId,
|
|
2609
|
+
orgEnvVarNames: getOrgEnvVarNamesForField('ascIssuerId'),
|
|
2610
|
+
availableOrgEnvVars,
|
|
2611
|
+
nonInteractive: options?.nonInteractive,
|
|
2612
|
+
required: false,
|
|
2613
|
+
});
|
|
2614
|
+
configTracker.track('ascIssuerId', ascIssuerIdResult.value, ascIssuerIdResult.source);
|
|
2615
|
+
const ascPrivateKeyPathResult = await smartPrompt({
|
|
2616
|
+
name: 'ascPrivateKeyPath',
|
|
2617
|
+
message: 'Path to App Store Connect (ASC) API key (optional):',
|
|
2618
|
+
configValue: configAscKeyPath,
|
|
2619
|
+
orgEnvVarNames: getOrgEnvVarNamesForField('ascPrivateKeyPath'),
|
|
2620
|
+
availableOrgEnvVars,
|
|
2621
|
+
nonInteractive: options?.nonInteractive,
|
|
2622
|
+
required: false,
|
|
2623
|
+
});
|
|
2624
|
+
configTracker.track('ascPrivateKeyPath', ascPrivateKeyPathResult.value, ascPrivateKeyPathResult.source);
|
|
2625
|
+
// Non-credential prompts (username, generate certificate, notes)
|
|
2626
|
+
const { appleId, generateCertificate, notes } = await inquirer.prompt([
|
|
2423
2627
|
{
|
|
2424
2628
|
type: 'input',
|
|
2425
2629
|
name: 'appleId',
|
|
2426
2630
|
message: 'App Store Connect username (optional):',
|
|
2427
2631
|
default: '',
|
|
2428
2632
|
},
|
|
2429
|
-
{
|
|
2430
|
-
type: 'input',
|
|
2431
|
-
name: 'apiKeyPath',
|
|
2432
|
-
message: 'Path to App Store Connect API key (optional):',
|
|
2433
|
-
default: configAscKeyPath || '',
|
|
2434
|
-
},
|
|
2435
|
-
{
|
|
2436
|
-
type: 'input',
|
|
2437
|
-
name: 'ascKeyId',
|
|
2438
|
-
message: 'App Store Connect API Key ID (optional):',
|
|
2439
|
-
default: configAscKeyId || '',
|
|
2440
|
-
},
|
|
2441
|
-
{
|
|
2442
|
-
type: 'input',
|
|
2443
|
-
name: 'ascIssuerId',
|
|
2444
|
-
message: 'App Store Connect Issuer ID (optional):',
|
|
2445
|
-
default: configAscIssuerId || '',
|
|
2446
|
-
},
|
|
2447
2633
|
{
|
|
2448
2634
|
type: 'confirm',
|
|
2449
2635
|
name: 'generateCertificate',
|
|
@@ -2457,38 +2643,55 @@ export async function submit(cliPlatformArg, cliTrackArg, verbose = false, optio
|
|
|
2457
2643
|
default: '',
|
|
2458
2644
|
},
|
|
2459
2645
|
]);
|
|
2646
|
+
// Collect TestFlight groups for external testing (beta track)
|
|
2647
|
+
let testflightGroups;
|
|
2648
|
+
if (track === 'beta' && !options?.nonInteractive) {
|
|
2649
|
+
testflightGroups = await promptForTestFlightGroups(norrixConfig.ios?.testflightGroups);
|
|
2650
|
+
}
|
|
2651
|
+
else if (track === 'beta') {
|
|
2652
|
+
// Non-interactive mode: use config groups or default
|
|
2653
|
+
testflightGroups = norrixConfig.ios?.testflightGroups || ['External Testers'];
|
|
2654
|
+
}
|
|
2460
2655
|
// Build iOS credentials if provided
|
|
2461
|
-
const ascKeyPath = normalizePath(
|
|
2462
|
-
if (
|
|
2463
|
-
|
|
2656
|
+
const ascKeyPath = normalizePath(ascPrivateKeyPathResult.value);
|
|
2657
|
+
if (ascApiKeyIdResult.value &&
|
|
2658
|
+
ascIssuerIdResult.value &&
|
|
2464
2659
|
ascKeyPath &&
|
|
2465
2660
|
fs.existsSync(ascKeyPath)) {
|
|
2466
2661
|
const ascPrivateKeyContent = fs.readFileSync(ascKeyPath, 'utf8');
|
|
2467
2662
|
const ascPrivateKeyBase64 = Buffer.from(ascPrivateKeyContent).toString('base64');
|
|
2468
2663
|
credentials.iosCredentials = {
|
|
2469
|
-
ascApiKeyId:
|
|
2470
|
-
ascIssuerId:
|
|
2664
|
+
ascApiKeyId: ascApiKeyIdResult.value,
|
|
2665
|
+
ascIssuerId: ascIssuerIdResult.value,
|
|
2471
2666
|
ascPrivateKey: ascPrivateKeyBase64,
|
|
2472
2667
|
...(configTeamId ? { teamId: configTeamId } : {}),
|
|
2668
|
+
...(testflightGroups ? { testflightGroups: testflightGroups.join(',') } : {}),
|
|
2473
2669
|
};
|
|
2474
2670
|
}
|
|
2475
2671
|
credentials = {
|
|
2476
2672
|
...credentials,
|
|
2477
|
-
appleId
|
|
2478
|
-
generateCertificate
|
|
2479
|
-
notes
|
|
2673
|
+
appleId,
|
|
2674
|
+
generateCertificate,
|
|
2675
|
+
notes,
|
|
2480
2676
|
};
|
|
2481
2677
|
}
|
|
2482
2678
|
}
|
|
2483
2679
|
// For Android, ask for Play Store account
|
|
2484
2680
|
if (platform === 'android') {
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2681
|
+
// Use smart prompt for service account key
|
|
2682
|
+
// Note: playServiceAccountJsonPath is not yet in NorrixAndroidConfig interface, so using undefined for now
|
|
2683
|
+
const playServiceAccountJsonPathResult = await smartPrompt({
|
|
2684
|
+
name: 'playServiceAccountJsonPath',
|
|
2685
|
+
message: 'Path to Google Play service account key (optional):',
|
|
2686
|
+
configValue: undefined, // Not yet supported in norrix.config.ts
|
|
2687
|
+
orgEnvVarNames: getOrgEnvVarNamesForField('playServiceAccountJsonPath'),
|
|
2688
|
+
availableOrgEnvVars,
|
|
2689
|
+
nonInteractive: options?.nonInteractive,
|
|
2690
|
+
required: false,
|
|
2691
|
+
});
|
|
2692
|
+
configTracker.track('playServiceAccountJsonPath', playServiceAccountJsonPathResult.value, playServiceAccountJsonPathResult.source);
|
|
2693
|
+
// Non-credential prompts
|
|
2694
|
+
const { generateKeystore, notes } = await inquirer.prompt([
|
|
2492
2695
|
{
|
|
2493
2696
|
type: 'confirm',
|
|
2494
2697
|
name: 'generateKeystore',
|
|
@@ -2503,9 +2706,9 @@ export async function submit(cliPlatformArg, cliTrackArg, verbose = false, optio
|
|
|
2503
2706
|
},
|
|
2504
2707
|
]);
|
|
2505
2708
|
credentials = {
|
|
2506
|
-
serviceAccountKeyPath:
|
|
2507
|
-
generateKeystore
|
|
2508
|
-
notes
|
|
2709
|
+
serviceAccountKeyPath: playServiceAccountJsonPathResult.value,
|
|
2710
|
+
generateKeystore,
|
|
2711
|
+
notes,
|
|
2509
2712
|
};
|
|
2510
2713
|
}
|
|
2511
2714
|
spinner.start(`Submitting build to ${platform === 'android' ? 'Google Play' : 'App Store'}...`);
|
|
@@ -2559,17 +2762,72 @@ export async function submit(cliPlatformArg, cliTrackArg, verbose = false, optio
|
|
|
2559
2762
|
}
|
|
2560
2763
|
console.log('✅ Your app submission request was received.');
|
|
2561
2764
|
console.log(` Submission ID: ${submitId}`);
|
|
2562
|
-
|
|
2563
|
-
if (
|
|
2564
|
-
console.log(`
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2765
|
+
// Only show backend details in dev mode
|
|
2766
|
+
if (CURRENT_ENV === 'dev') {
|
|
2767
|
+
console.log(` Backend API: ${API_URL}`);
|
|
2768
|
+
if (process.env.NORRIX_API_URL) {
|
|
2769
|
+
console.log(` NORRIX_API_URL: ${process.env.NORRIX_API_URL}`);
|
|
2770
|
+
}
|
|
2771
|
+
if (dispatchedEnv) {
|
|
2772
|
+
console.log(` Backend env: ${dispatchedEnv}`);
|
|
2773
|
+
}
|
|
2568
2774
|
}
|
|
2569
2775
|
console.log(` Platform: ${platform === 'android' ? 'Google Play Store' : 'Apple App Store'}`);
|
|
2570
2776
|
console.log(formatVersionBuildLine(submittedVersion, submittedBuildNumber));
|
|
2571
2777
|
console.log(` Track: ${track}`);
|
|
2572
2778
|
console.log(` You can check the status with: norrix submit-status ${submitId}`);
|
|
2779
|
+
// Offer to save config if user provided new values
|
|
2780
|
+
const appRoot = process.cwd();
|
|
2781
|
+
if (!options?.nonInteractive && configTracker.shouldOfferToSave()) {
|
|
2782
|
+
const configExists = hasNorrixConfig(appRoot);
|
|
2783
|
+
const { shouldSave } = await inquirer.prompt([
|
|
2784
|
+
{
|
|
2785
|
+
type: 'confirm',
|
|
2786
|
+
name: 'shouldSave',
|
|
2787
|
+
message: configExists
|
|
2788
|
+
? 'Save new credential paths to norrix.config.ts?'
|
|
2789
|
+
: 'Save credential paths to norrix.config.ts for future submissions?',
|
|
2790
|
+
default: true,
|
|
2791
|
+
},
|
|
2792
|
+
]);
|
|
2793
|
+
if (shouldSave) {
|
|
2794
|
+
try {
|
|
2795
|
+
const userProvidedValues = configTracker.getUserProvidedValues();
|
|
2796
|
+
// Start with existing config values (if config exists)
|
|
2797
|
+
const existingConfig = configExists ? norrixConfig : {};
|
|
2798
|
+
const saveableOptions = {
|
|
2799
|
+
platform: platform,
|
|
2800
|
+
};
|
|
2801
|
+
// Merge: Start with existing config, then overlay user-provided values
|
|
2802
|
+
// iOS fields
|
|
2803
|
+
if (platform === 'ios') {
|
|
2804
|
+
saveableOptions.ascApiKeyId = userProvidedValues.ascApiKeyId || existingConfig.ios?.ascApiKeyId;
|
|
2805
|
+
saveableOptions.ascIssuerId = userProvidedValues.ascIssuerId || existingConfig.ios?.ascIssuerId;
|
|
2806
|
+
saveableOptions.ascPrivateKeyPath = userProvidedValues.ascPrivateKeyPath || existingConfig.ios?.ascPrivateKeyPath;
|
|
2807
|
+
// Preserve other iOS config values
|
|
2808
|
+
saveableOptions.teamId = existingConfig.ios?.teamId;
|
|
2809
|
+
saveableOptions.distributionType = existingConfig.ios?.distributionType;
|
|
2810
|
+
saveableOptions.p12Path = existingConfig.ios?.p12Path;
|
|
2811
|
+
saveableOptions.provisioningProfilePath = existingConfig.ios?.provisioningProfilePath;
|
|
2812
|
+
}
|
|
2813
|
+
// Android fields
|
|
2814
|
+
if (platform === 'android') {
|
|
2815
|
+
// Note: playServiceAccountJsonPath not yet in config interface
|
|
2816
|
+
// Preserve existing Android config values
|
|
2817
|
+
saveableOptions.keystorePath = existingConfig.android?.keystorePath;
|
|
2818
|
+
saveableOptions.keyAlias = existingConfig.android?.keyAlias;
|
|
2819
|
+
saveableOptions.distributionType = existingConfig.android?.distributionType;
|
|
2820
|
+
}
|
|
2821
|
+
const savedPath = saveNorrixConfig(appRoot, saveableOptions);
|
|
2822
|
+
console.log(configExists
|
|
2823
|
+
? `✓ Configuration updated in ${path.basename(savedPath)}`
|
|
2824
|
+
: `✓ Configuration saved to ${path.basename(savedPath)}`);
|
|
2825
|
+
}
|
|
2826
|
+
catch (saveError) {
|
|
2827
|
+
console.warn(`Warning: Could not save config file: ${saveError.message}`);
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2573
2831
|
// Restore original cwd if we changed it
|
|
2574
2832
|
if (originalCwd && process.cwd() !== originalCwd) {
|
|
2575
2833
|
process.chdir(originalCwd);
|
|
@@ -3024,12 +3282,15 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false, opt
|
|
|
3024
3282
|
spinner.succeed('Update published successfully!');
|
|
3025
3283
|
console.log('✅ Your update has been published.');
|
|
3026
3284
|
console.log(` Update ID: ${updateId}`);
|
|
3027
|
-
|
|
3028
|
-
if (
|
|
3029
|
-
console.log(`
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3285
|
+
// Only show backend details in dev mode
|
|
3286
|
+
if (CURRENT_ENV === 'dev') {
|
|
3287
|
+
console.log(` Backend API: ${API_URL}`);
|
|
3288
|
+
if (process.env.NORRIX_API_URL) {
|
|
3289
|
+
console.log(` NORRIX_API_URL: ${process.env.NORRIX_API_URL}`);
|
|
3290
|
+
}
|
|
3291
|
+
if (dispatchedEnv) {
|
|
3292
|
+
console.log(` Backend env: ${dispatchedEnv}`);
|
|
3293
|
+
}
|
|
3033
3294
|
}
|
|
3034
3295
|
console.log(formatVersionBuildLine(recordedUpdateVersion, recordedUpdateBuildNumber));
|
|
3035
3296
|
console.log(` You can check the status with: norrix update-status ${updateId}`);
|