@norrix/cli 0.0.25 → 0.0.26
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 +23 -0
- package/dist/cli.js +98 -7
- package/dist/cli.js.map +1 -1
- package/dist/lib/amplify-config.js +0 -1
- package/dist/lib/cli-settings.d.ts +15 -0
- package/dist/lib/cli-settings.js +50 -0
- package/dist/lib/cli-settings.js.map +1 -0
- package/dist/lib/commands.d.ts +74 -4
- package/dist/lib/commands.js +886 -108
- package/dist/lib/commands.js.map +1 -1
- package/dist/lib/config-helpers.spec.js +8 -0
- package/dist/lib/config.d.ts +50 -0
- package/dist/lib/config.js +41 -5
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/defaults.js +0 -1
- package/dist/lib/dev-defaults.js +0 -1
- package/dist/lib/fingerprinting.js +0 -1
- package/dist/lib/norrix-cli.js +0 -1
- package/dist/lib/prod-defaults.js +0 -1
- package/dist/lib/workspace.d.ts +18 -1
- package/dist/lib/workspace.js +378 -24
- package/dist/lib/workspace.js.map +1 -1
- package/package.json +1 -1
package/dist/lib/commands.js
CHANGED
|
@@ -10,15 +10,17 @@ import archiver from 'archiver';
|
|
|
10
10
|
import { configureAmplify, loadCliEnvFiles } from './amplify-config.js';
|
|
11
11
|
import { computeFingerprint, writeRuntimeFingerprintFile } from './fingerprinting.js';
|
|
12
12
|
import { loadNorrixConfig, hasNorrixConfig, saveNorrixConfig } from './config.js';
|
|
13
|
-
import { detectWorkspaceContext, getNxProjectDependencies, getWorkspaceDependenciesFallback, createWorkspaceManifest, logWorkspaceContext, isAtWorkspaceRoot, discoverNativeScriptApps, getWorkspaceContextForApp, } from './workspace.js';
|
|
13
|
+
import { detectWorkspaceContext, getNxProjectDependencies, getWorkspaceDependenciesFallback, createWorkspaceManifest, logWorkspaceContext, isAtWorkspaceRoot, discoverNativeScriptApps, getWorkspaceContextForApp, detectNxBuildConfigurations, } from './workspace.js';
|
|
14
14
|
import { signIn as amplifySignIn, signOut as amplifySignOut, getCurrentUser, fetchAuthSession, } from 'aws-amplify/auth';
|
|
15
15
|
import crypto from 'crypto';
|
|
16
16
|
import { Amplify } from 'aws-amplify';
|
|
17
17
|
import { PROD_DEFAULTS } from './prod-defaults.js';
|
|
18
18
|
import { DEV_DEFAULTS } from './dev-defaults.js';
|
|
19
|
+
import { clearSelectedOrgId, getSelectedOrgId, setSelectedOrgId } from './cli-settings.js';
|
|
19
20
|
let CURRENT_ENV = 'prod';
|
|
20
21
|
let CURRENT_DEFAULTS = PROD_DEFAULTS;
|
|
21
22
|
let API_URL = PROD_DEFAULTS.apiUrl;
|
|
23
|
+
let CURRENT_ORG_ID;
|
|
22
24
|
let IS_INITIALIZED = false;
|
|
23
25
|
function defaultsForEnv(env) {
|
|
24
26
|
return env === 'dev' ? DEV_DEFAULTS : PROD_DEFAULTS;
|
|
@@ -40,6 +42,10 @@ export function initNorrixCli(env = 'prod') {
|
|
|
40
42
|
CURRENT_DEFAULTS = defaultsForEnv(env);
|
|
41
43
|
configureAmplify(env);
|
|
42
44
|
API_URL = process.env.NORRIX_API_URL || CURRENT_DEFAULTS.apiUrl;
|
|
45
|
+
// Load persisted org selection for this env + API URL profile (if any).
|
|
46
|
+
// Allow per-invocation override via NORRIX_ORG_ID env var.
|
|
47
|
+
const envOrg = (process.env.NORRIX_ORG_ID ?? '').toString().trim();
|
|
48
|
+
CURRENT_ORG_ID = envOrg || getSelectedOrgId(env, API_URL);
|
|
43
49
|
IS_INITIALIZED = true;
|
|
44
50
|
}
|
|
45
51
|
function ensureInitialized() {
|
|
@@ -50,19 +56,168 @@ function ensureInitialized() {
|
|
|
50
56
|
/**
|
|
51
57
|
* Return Authorization header containing the current Cognito ID token (if signed in).
|
|
52
58
|
*/
|
|
53
|
-
async function getAuthHeaders() {
|
|
59
|
+
async function getAuthHeaders(options) {
|
|
54
60
|
ensureInitialized();
|
|
61
|
+
const headers = {};
|
|
55
62
|
try {
|
|
56
63
|
const session = await fetchAuthSession();
|
|
57
64
|
const idToken = session.tokens?.idToken?.toString();
|
|
58
65
|
if (idToken) {
|
|
59
|
-
|
|
66
|
+
headers.Authorization = `Bearer ${idToken}`;
|
|
60
67
|
}
|
|
61
68
|
}
|
|
62
69
|
catch (_) {
|
|
63
70
|
/* not signed in */
|
|
64
71
|
}
|
|
65
|
-
|
|
72
|
+
if (options?.includeOrg !== false && CURRENT_ORG_ID) {
|
|
73
|
+
headers['X-Norrix-Org-Id'] = CURRENT_ORG_ID;
|
|
74
|
+
}
|
|
75
|
+
return headers;
|
|
76
|
+
}
|
|
77
|
+
function setCurrentOrgId(orgId) {
|
|
78
|
+
const v = (orgId ?? '').toString().trim();
|
|
79
|
+
CURRENT_ORG_ID = v ? v : undefined;
|
|
80
|
+
}
|
|
81
|
+
async function fetchOrganizations(verbose = false) {
|
|
82
|
+
ensureInitialized();
|
|
83
|
+
try {
|
|
84
|
+
const res = await axios.get(`${API_URL}/orgs`, {
|
|
85
|
+
headers: await getAuthHeaders({ includeOrg: false }),
|
|
86
|
+
});
|
|
87
|
+
const organizations = Array.isArray(res.data?.organizations)
|
|
88
|
+
? res.data.organizations
|
|
89
|
+
: [];
|
|
90
|
+
const selectedOrganizationId = res.data?.selectedOrganizationId
|
|
91
|
+
? String(res.data.selectedOrganizationId)
|
|
92
|
+
: undefined;
|
|
93
|
+
return { organizations, selectedOrganizationId };
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
if (verbose) {
|
|
97
|
+
console.error('--- Verbose error details (orgs fetch) ---');
|
|
98
|
+
console.error(err);
|
|
99
|
+
if (err?.response) {
|
|
100
|
+
console.error('Axios response status:', err.response.status);
|
|
101
|
+
console.error('Axios response data:', err.response.data);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async function ensureOrgSelected(params) {
|
|
108
|
+
ensureInitialized();
|
|
109
|
+
const explicit = (params.orgIdArg ?? process.env.NORRIX_ORG_ID ?? '')
|
|
110
|
+
.toString()
|
|
111
|
+
.trim();
|
|
112
|
+
if (explicit) {
|
|
113
|
+
setCurrentOrgId(explicit);
|
|
114
|
+
return explicit;
|
|
115
|
+
}
|
|
116
|
+
const stored = getSelectedOrgId(CURRENT_ENV, API_URL);
|
|
117
|
+
if (!params.requireSelection) {
|
|
118
|
+
if (stored)
|
|
119
|
+
setCurrentOrgId(stored);
|
|
120
|
+
return stored;
|
|
121
|
+
}
|
|
122
|
+
// Validate stored selection (and discover orgs for prompting).
|
|
123
|
+
const { organizations } = await fetchOrganizations(Boolean(params.verbose));
|
|
124
|
+
const normalizedOrgs = organizations.filter((o) => o && o.id);
|
|
125
|
+
if (stored && normalizedOrgs.some((o) => o.id === stored)) {
|
|
126
|
+
setCurrentOrgId(stored);
|
|
127
|
+
return stored;
|
|
128
|
+
}
|
|
129
|
+
if (stored) {
|
|
130
|
+
clearSelectedOrgId(CURRENT_ENV, API_URL);
|
|
131
|
+
}
|
|
132
|
+
if (normalizedOrgs.length === 1) {
|
|
133
|
+
const only = normalizedOrgs[0];
|
|
134
|
+
setSelectedOrgId(CURRENT_ENV, API_URL, only.id);
|
|
135
|
+
setCurrentOrgId(only.id);
|
|
136
|
+
return only.id;
|
|
137
|
+
}
|
|
138
|
+
if (params.nonInteractive) {
|
|
139
|
+
throw new Error('No organization selected. Use --org <orgId> or run `norrix orgs select`.');
|
|
140
|
+
}
|
|
141
|
+
if (!normalizedOrgs.length) {
|
|
142
|
+
throw new Error('No organizations found for this user.');
|
|
143
|
+
}
|
|
144
|
+
const choices = normalizedOrgs.map((o) => {
|
|
145
|
+
const suffix = o.id.length > 8 ? o.id.slice(-8) : o.id;
|
|
146
|
+
return {
|
|
147
|
+
name: `${o.name} (${o.role}) • …${suffix}`,
|
|
148
|
+
value: o.id,
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
const { orgId } = await inquirer.prompt([
|
|
152
|
+
{
|
|
153
|
+
type: 'list',
|
|
154
|
+
name: 'orgId',
|
|
155
|
+
message: params.promptMessage || 'Select organization:',
|
|
156
|
+
choices,
|
|
157
|
+
},
|
|
158
|
+
]);
|
|
159
|
+
const selected = String(orgId || '').trim();
|
|
160
|
+
if (!selected) {
|
|
161
|
+
throw new Error('Organization selection cancelled.');
|
|
162
|
+
}
|
|
163
|
+
setSelectedOrgId(CURRENT_ENV, API_URL, selected);
|
|
164
|
+
setCurrentOrgId(selected);
|
|
165
|
+
return selected;
|
|
166
|
+
}
|
|
167
|
+
export async function orgsList(verbose = false) {
|
|
168
|
+
ensureInitialized();
|
|
169
|
+
try {
|
|
170
|
+
const { organizations, selectedOrganizationId } = await fetchOrganizations(verbose);
|
|
171
|
+
if (!organizations.length) {
|
|
172
|
+
console.log('No organizations found.');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
console.log('Organizations:');
|
|
176
|
+
for (const o of organizations) {
|
|
177
|
+
const selectedMark = selectedOrganizationId && o.id === selectedOrganizationId ? ' (selected)' : '';
|
|
178
|
+
console.log(`- ${o.name} [${o.role}] ${o.id}${selectedMark}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
ora().fail(`Failed to list organizations: ${err?.message || err}`);
|
|
183
|
+
if (verbose && err?.response) {
|
|
184
|
+
console.error('Axios response status:', err.response.status);
|
|
185
|
+
console.error('Axios response data:', err.response.data);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
export async function orgsSelect(verbose = false) {
|
|
190
|
+
ensureInitialized();
|
|
191
|
+
try {
|
|
192
|
+
await ensureOrgSelected({
|
|
193
|
+
requireSelection: true,
|
|
194
|
+
nonInteractive: false,
|
|
195
|
+
verbose,
|
|
196
|
+
promptMessage: 'Select default organization for this environment:',
|
|
197
|
+
});
|
|
198
|
+
if (CURRENT_ORG_ID) {
|
|
199
|
+
console.log(`✅ Selected organization: ${CURRENT_ORG_ID}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
ora().fail(`Failed to select organization: ${err?.message || err}`);
|
|
204
|
+
if (verbose && err?.response) {
|
|
205
|
+
console.error('Axios response status:', err.response.status);
|
|
206
|
+
console.error('Axios response data:', err.response.data);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
export async function orgsCurrent() {
|
|
211
|
+
ensureInitialized();
|
|
212
|
+
const envOrg = (process.env.NORRIX_ORG_ID ?? '').toString().trim();
|
|
213
|
+
const stored = getSelectedOrgId(CURRENT_ENV, API_URL);
|
|
214
|
+
const current = envOrg || stored;
|
|
215
|
+
if (!current) {
|
|
216
|
+
console.log('No default organization selected for this environment.');
|
|
217
|
+
console.log('Run `norrix orgs select` or pass `--org <orgId>`.');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
console.log(`Current organization for ${CURRENT_ENV} (${API_URL}): ${current}`);
|
|
66
221
|
}
|
|
67
222
|
/**
|
|
68
223
|
* Norrix CLI Command Implementations
|
|
@@ -80,6 +235,16 @@ async function getAuthHeaders() {
|
|
|
80
235
|
// Get dirname equivalent in ESM
|
|
81
236
|
const __filename = fileURLToPath(import.meta.url);
|
|
82
237
|
const __dirname = path.dirname(__filename);
|
|
238
|
+
/**
|
|
239
|
+
* Safely trim a string value, returning undefined for empty/null values.
|
|
240
|
+
* Handles both CLI args and config file values to ensure no trailing/leading whitespace.
|
|
241
|
+
*/
|
|
242
|
+
function trimString(input) {
|
|
243
|
+
if (input == null)
|
|
244
|
+
return undefined;
|
|
245
|
+
const trimmed = String(input).trim();
|
|
246
|
+
return trimmed || undefined;
|
|
247
|
+
}
|
|
83
248
|
function normalizePath(input) {
|
|
84
249
|
if (!input)
|
|
85
250
|
return undefined;
|
|
@@ -715,8 +880,7 @@ async function resolveWorkspaceContext(projectArg, spinner) {
|
|
|
715
880
|
* For Nx workspaces, this includes the app, dependent libs, and workspace config files.
|
|
716
881
|
* For standalone projects, this zips the current directory.
|
|
717
882
|
*/
|
|
718
|
-
async function zipProject(projectName, isUpdate = false, verbose = false) {
|
|
719
|
-
const workspaceCtx = detectWorkspaceContext();
|
|
883
|
+
async function zipProject(projectName, workspaceCtx, isUpdate = false, verbose = false) {
|
|
720
884
|
if (workspaceCtx.type === 'nx') {
|
|
721
885
|
return zipWorkspaceProject(projectName, workspaceCtx, isUpdate, verbose);
|
|
722
886
|
}
|
|
@@ -813,16 +977,39 @@ async function zipWorkspaceProject(projectName, workspaceCtx, isUpdate = false,
|
|
|
813
977
|
});
|
|
814
978
|
archive.pipe(output);
|
|
815
979
|
logWorkspaceContext(workspaceCtx, verbose);
|
|
816
|
-
// Get workspace dependencies using Nx CLI (preferred)
|
|
980
|
+
// Get workspace dependencies using Nx CLI (preferred) with fallback supplementation
|
|
817
981
|
let deps;
|
|
818
982
|
if (workspaceCtx.projectName) {
|
|
819
|
-
deps = getNxProjectDependencies(workspaceCtx.projectName, workspaceCtx.workspaceRoot, verbose
|
|
983
|
+
deps = getNxProjectDependencies(workspaceCtx.projectName, workspaceCtx.workspaceRoot, verbose, workspaceCtx.appRoot // Pass appRoot for webpack alias detection
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
// Always supplement with fallback detection to catch anything Nx might miss
|
|
987
|
+
// (e.g., dynamic imports, SCSS dependencies, transitive deps from source scanning)
|
|
988
|
+
const fallbackDeps = getWorkspaceDependenciesFallback(workspaceCtx, verbose);
|
|
989
|
+
if (deps) {
|
|
990
|
+
// Merge fallback libs into Nx-detected libs
|
|
991
|
+
const mergedLibPaths = new Set(deps.libPaths);
|
|
992
|
+
for (const libPath of fallbackDeps.libPaths) {
|
|
993
|
+
if (!mergedLibPaths.has(libPath)) {
|
|
994
|
+
mergedLibPaths.add(libPath);
|
|
995
|
+
if (verbose) {
|
|
996
|
+
console.log(`[workspace] Fallback added additional lib: ${libPath}`);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
deps.libPaths = Array.from(mergedLibPaths);
|
|
1001
|
+
// Also merge local file deps
|
|
1002
|
+
const mergedLocalFileDeps = new Set(deps.localFileDeps);
|
|
1003
|
+
for (const dep of fallbackDeps.localFileDeps) {
|
|
1004
|
+
mergedLocalFileDeps.add(dep);
|
|
1005
|
+
}
|
|
1006
|
+
deps.localFileDeps = Array.from(mergedLocalFileDeps);
|
|
820
1007
|
}
|
|
821
|
-
|
|
1008
|
+
else {
|
|
822
1009
|
if (verbose) {
|
|
823
|
-
console.log('[workspace] Using fallback dependency detection');
|
|
1010
|
+
console.log('[workspace] Using fallback dependency detection (Nx CLI not available)');
|
|
824
1011
|
}
|
|
825
|
-
deps =
|
|
1012
|
+
deps = fallbackDeps;
|
|
826
1013
|
}
|
|
827
1014
|
// Create manifest for CI
|
|
828
1015
|
const manifest = createWorkspaceManifest(workspaceCtx, deps);
|
|
@@ -909,6 +1096,68 @@ async function zipWorkspaceProject(projectName, workspaceCtx, isUpdate = false,
|
|
|
909
1096
|
archive.directory(absoluteAssetPath, assetPath);
|
|
910
1097
|
}
|
|
911
1098
|
}
|
|
1099
|
+
// 6. Add local file dependencies (file: protocol paths from package.json)
|
|
1100
|
+
// Skip any that are already covered by libPaths to avoid duplicate entries
|
|
1101
|
+
if (deps.localFileDeps && deps.localFileDeps.length > 0) {
|
|
1102
|
+
const addedDirs = new Set();
|
|
1103
|
+
// Filter out local deps that are subdirectories of already-added libs
|
|
1104
|
+
const filteredLocalDeps = deps.localFileDeps.filter(localDep => {
|
|
1105
|
+
// Check if this local dep is inside any of the lib paths
|
|
1106
|
+
for (const libPath of deps.libPaths) {
|
|
1107
|
+
if (localDep.startsWith(libPath + '/') || localDep === libPath) {
|
|
1108
|
+
if (verbose) {
|
|
1109
|
+
console.log(` - ${localDep} (skipped, covered by ${libPath})`);
|
|
1110
|
+
}
|
|
1111
|
+
return false;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
return true;
|
|
1115
|
+
});
|
|
1116
|
+
if (filteredLocalDeps.length > 0) {
|
|
1117
|
+
console.log(`Adding ${filteredLocalDeps.length} local file dependencies`);
|
|
1118
|
+
}
|
|
1119
|
+
for (const localDep of filteredLocalDeps) {
|
|
1120
|
+
const absoluteLocalPath = path.join(workspaceCtx.workspaceRoot, localDep);
|
|
1121
|
+
if (fs.existsSync(absoluteLocalPath)) {
|
|
1122
|
+
const stat = fs.statSync(absoluteLocalPath);
|
|
1123
|
+
if (stat.isFile()) {
|
|
1124
|
+
// For files, ensure the parent directory structure is maintained
|
|
1125
|
+
if (verbose) {
|
|
1126
|
+
console.log(` - ${localDep} (file)`);
|
|
1127
|
+
}
|
|
1128
|
+
archive.file(absoluteLocalPath, { name: localDep });
|
|
1129
|
+
// Also add the directory if it hasn't been added yet (for other potential files)
|
|
1130
|
+
const parentDir = path.dirname(localDep);
|
|
1131
|
+
if (parentDir && parentDir !== '.' && !addedDirs.has(parentDir)) {
|
|
1132
|
+
// We just add the file, not the whole directory
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
else if (stat.isDirectory()) {
|
|
1136
|
+
// Skip if this directory or a parent is already in libPaths
|
|
1137
|
+
const alreadyCovered = deps.libPaths.some(libPath => localDep.startsWith(libPath + '/') || libPath.startsWith(localDep + '/'));
|
|
1138
|
+
if (alreadyCovered) {
|
|
1139
|
+
if (verbose) {
|
|
1140
|
+
console.log(` - ${localDep} (skipped, overlaps with libPaths)`);
|
|
1141
|
+
}
|
|
1142
|
+
continue;
|
|
1143
|
+
}
|
|
1144
|
+
if (verbose) {
|
|
1145
|
+
console.log(` - ${localDep} (directory)`);
|
|
1146
|
+
}
|
|
1147
|
+
archive.directory(absoluteLocalPath, localDep, (entry) => {
|
|
1148
|
+
if (entry.name.includes('node_modules')) {
|
|
1149
|
+
return false;
|
|
1150
|
+
}
|
|
1151
|
+
return entry;
|
|
1152
|
+
});
|
|
1153
|
+
addedDirs.add(localDep);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
else if (verbose) {
|
|
1157
|
+
console.log(` - ${localDep} (not found, skipping)`);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
912
1161
|
archive.finalize();
|
|
913
1162
|
});
|
|
914
1163
|
}
|
|
@@ -921,6 +1170,23 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
921
1170
|
// Normalize options - support both new object and legacy string projectArg
|
|
922
1171
|
const opts = typeof options === 'string' ? { project: options } : (options || {});
|
|
923
1172
|
ensureInitialized();
|
|
1173
|
+
try {
|
|
1174
|
+
await ensureOrgSelected({
|
|
1175
|
+
orgIdArg: opts.org,
|
|
1176
|
+
nonInteractive: opts.nonInteractive,
|
|
1177
|
+
requireSelection: true,
|
|
1178
|
+
verbose,
|
|
1179
|
+
promptMessage: 'Select organization for this build:',
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
catch (error) {
|
|
1183
|
+
ora().fail(`Organization selection failed: ${error?.message || error}`);
|
|
1184
|
+
if (verbose && error?.response) {
|
|
1185
|
+
console.error('Axios response status:', error.response.status);
|
|
1186
|
+
console.error('Axios response data:', error.response.data);
|
|
1187
|
+
}
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
924
1190
|
let spinner;
|
|
925
1191
|
let originalCwd;
|
|
926
1192
|
try {
|
|
@@ -971,6 +1237,31 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
971
1237
|
]);
|
|
972
1238
|
configuration = answer.configuration;
|
|
973
1239
|
}
|
|
1240
|
+
// 2.2 Determine Nx configuration for workspace builds (CLI arg preferred, then prompt if available)
|
|
1241
|
+
let nxConfiguration = opts.nxConfiguration;
|
|
1242
|
+
if (!nxConfiguration && workspaceCtx.type === 'nx' && !opts.nonInteractive) {
|
|
1243
|
+
const nxConfigs = detectNxBuildConfigurations(workspaceCtx.appRoot);
|
|
1244
|
+
if (nxConfigs && nxConfigs.configurations.length > 0) {
|
|
1245
|
+
const choices = nxConfigs.configurations.map((c) => ({
|
|
1246
|
+
name: c === nxConfigs.defaultConfiguration ? `${c} (default)` : c,
|
|
1247
|
+
value: c,
|
|
1248
|
+
}));
|
|
1249
|
+
// Add option to skip/use default
|
|
1250
|
+
choices.unshift({ name: '(none - use defaults)', value: '' });
|
|
1251
|
+
const { chosenNxConfig } = await inquirer.prompt([
|
|
1252
|
+
{
|
|
1253
|
+
type: 'list',
|
|
1254
|
+
name: 'chosenNxConfig',
|
|
1255
|
+
message: 'Nx build configuration (environment):',
|
|
1256
|
+
choices,
|
|
1257
|
+
default: nxConfigs.defaultConfiguration || '',
|
|
1258
|
+
},
|
|
1259
|
+
]);
|
|
1260
|
+
nxConfiguration = chosenNxConfig || undefined;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
// Store resolved nxConfiguration back to opts for later use
|
|
1264
|
+
opts.nxConfiguration = nxConfiguration;
|
|
974
1265
|
const normalizeIosDistribution = (input) => {
|
|
975
1266
|
const v = String(input ?? '')
|
|
976
1267
|
.trim()
|
|
@@ -1098,94 +1389,147 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
1098
1389
|
if (configuration === 'release') {
|
|
1099
1390
|
spinner.stop();
|
|
1100
1391
|
if (platform === 'ios') {
|
|
1101
|
-
//
|
|
1392
|
+
// Resolve values from: CLI flags > config file (trimmed to handle whitespace)
|
|
1102
1393
|
const configTeamId = norrixConfig.ios?.teamId;
|
|
1103
|
-
const
|
|
1104
|
-
|
|
1105
|
-
const
|
|
1106
|
-
const
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1394
|
+
const configP12Path = norrixConfig.ios?.p12Path;
|
|
1395
|
+
const configProfilePath = norrixConfig.ios?.provisioningProfilePath;
|
|
1396
|
+
const configAscKeyId = norrixConfig.ios?.ascApiKeyId;
|
|
1397
|
+
const configAscIssuerId = norrixConfig.ios?.ascIssuerId;
|
|
1398
|
+
const configAscKeyPath = norrixConfig.ios?.ascPrivateKeyPath;
|
|
1399
|
+
const resolvedTeamId = trimString(opts.teamId) || configTeamId;
|
|
1400
|
+
const resolvedP12Path = trimString(opts.p12Path) || configP12Path;
|
|
1401
|
+
const resolvedP12Password = trimString(opts.p12Password);
|
|
1402
|
+
const resolvedProfilePath = trimString(opts.profilePath) || configProfilePath;
|
|
1403
|
+
const resolvedAscKeyId = trimString(opts.ascKeyId) || configAscKeyId;
|
|
1404
|
+
const resolvedAscIssuerId = trimString(opts.ascIssuerId) || configAscIssuerId;
|
|
1405
|
+
const resolvedAscKeyPath = trimString(opts.ascKeyPath) || configAscKeyPath;
|
|
1406
|
+
if (opts.nonInteractive) {
|
|
1407
|
+
// Non-interactive mode: use CLI flags and config values only
|
|
1408
|
+
iosCredentials = {
|
|
1409
|
+
teamId: resolvedTeamId || undefined,
|
|
1410
|
+
p12Base64: readOptionalFileAsBase64(resolvedP12Path),
|
|
1411
|
+
p12Password: resolvedP12Password || undefined,
|
|
1412
|
+
mobileprovisionBase64: readOptionalFileAsBase64(resolvedProfilePath),
|
|
1413
|
+
ascApiKeyId: resolvedAscKeyId || undefined,
|
|
1414
|
+
ascIssuerId: resolvedAscIssuerId || undefined,
|
|
1415
|
+
ascPrivateKey: readOptionalFileAsBase64(resolvedAscKeyPath),
|
|
1416
|
+
};
|
|
1417
|
+
if (resolvedTeamId) {
|
|
1418
|
+
console.log(`Using Apple Team ID: ${resolvedTeamId}`);
|
|
1419
|
+
}
|
|
1420
|
+
if (resolvedAscKeyId) {
|
|
1421
|
+
console.log(`Using ASC API Key: ${resolvedAscKeyId}`);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
else {
|
|
1425
|
+
// Interactive mode: prompt for values with CLI/config as defaults
|
|
1426
|
+
// Flow: Team ID -> ASC Key (recommended) -> If no ASC, then .p12/.mobileprovision
|
|
1427
|
+
const iosAnswers = await inquirer.prompt([
|
|
1428
|
+
{
|
|
1429
|
+
type: 'input',
|
|
1430
|
+
name: 'teamId',
|
|
1431
|
+
message: 'Apple Developer Team ID (required for code signing, e.g. "ABC123XYZ"):',
|
|
1432
|
+
default: resolvedTeamId || '',
|
|
1433
|
+
validate: (input) => {
|
|
1434
|
+
if (!input.trim()) {
|
|
1435
|
+
return true; // Allow empty, workflow will try to proceed without it
|
|
1436
|
+
}
|
|
1437
|
+
if (/^[A-Z0-9]{10}$/.test(input.trim())) {
|
|
1438
|
+
return true;
|
|
1439
|
+
}
|
|
1440
|
+
return 'Team ID should be 10 alphanumeric characters (e.g. "ABC123XYZ"). Leave empty to skip.';
|
|
1441
|
+
},
|
|
1442
|
+
},
|
|
1443
|
+
// ASC API Key is the recommended approach - ask first
|
|
1444
|
+
{
|
|
1445
|
+
type: 'confirm',
|
|
1446
|
+
name: 'useAscKey',
|
|
1447
|
+
message: 'Use App Store Connect API Key for auto-provisioning? (recommended)',
|
|
1448
|
+
default: Boolean(resolvedAscKeyId) || true,
|
|
1449
|
+
},
|
|
1450
|
+
{
|
|
1451
|
+
type: 'input',
|
|
1452
|
+
name: 'ascApiKeyId',
|
|
1453
|
+
message: 'ASC API Key ID:',
|
|
1454
|
+
default: resolvedAscKeyId || '',
|
|
1455
|
+
when: (a) => a.useAscKey,
|
|
1456
|
+
validate: (input) => {
|
|
1457
|
+
if (!input.trim()) {
|
|
1458
|
+
return 'API Key ID is required when using ASC Key';
|
|
1459
|
+
}
|
|
1121
1460
|
return true;
|
|
1122
|
-
}
|
|
1123
|
-
return 'Team ID should be 10 alphanumeric characters (e.g. "ABC123XYZ"). Leave empty to skip.';
|
|
1461
|
+
},
|
|
1124
1462
|
},
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1463
|
+
{
|
|
1464
|
+
type: 'input',
|
|
1465
|
+
name: 'ascIssuerId',
|
|
1466
|
+
message: 'ASC Issuer ID:',
|
|
1467
|
+
default: resolvedAscIssuerId || '',
|
|
1468
|
+
when: (a) => a.useAscKey,
|
|
1469
|
+
validate: (input) => {
|
|
1470
|
+
if (!input.trim()) {
|
|
1471
|
+
return 'Issuer ID is required when using ASC Key';
|
|
1472
|
+
}
|
|
1473
|
+
return true;
|
|
1474
|
+
},
|
|
1475
|
+
},
|
|
1476
|
+
{
|
|
1477
|
+
type: 'input',
|
|
1478
|
+
name: 'ascPrivateKeyPath',
|
|
1479
|
+
message: 'Path to ASC private key .p8:',
|
|
1480
|
+
default: resolvedAscKeyPath || '',
|
|
1481
|
+
when: (a) => a.useAscKey,
|
|
1482
|
+
validate: (input) => {
|
|
1483
|
+
if (!input.trim()) {
|
|
1484
|
+
return 'Path to .p8 key file is required when using ASC Key';
|
|
1485
|
+
}
|
|
1486
|
+
return true;
|
|
1487
|
+
},
|
|
1488
|
+
},
|
|
1489
|
+
// Only ask for .p12/.mobileprovision if NOT using ASC Key
|
|
1490
|
+
{
|
|
1491
|
+
type: 'input',
|
|
1492
|
+
name: 'p12Path',
|
|
1493
|
+
message: 'Path to iOS .p12 certificate (optional):',
|
|
1494
|
+
default: resolvedP12Path || '',
|
|
1495
|
+
when: (a) => !a.useAscKey,
|
|
1496
|
+
},
|
|
1497
|
+
{
|
|
1498
|
+
type: 'password',
|
|
1499
|
+
name: 'p12Password',
|
|
1500
|
+
message: 'Password for .p12 (if any):',
|
|
1501
|
+
mask: '*',
|
|
1502
|
+
default: '',
|
|
1503
|
+
when: (a) => !a.useAscKey && a.p12Path,
|
|
1504
|
+
},
|
|
1505
|
+
{
|
|
1506
|
+
type: 'input',
|
|
1507
|
+
name: 'mobileprovisionPath',
|
|
1508
|
+
message: 'Path to provisioning profile .mobileprovision (optional):',
|
|
1509
|
+
default: resolvedProfilePath || '',
|
|
1510
|
+
when: (a) => !a.useAscKey,
|
|
1511
|
+
},
|
|
1512
|
+
]);
|
|
1513
|
+
// Use resolved teamId from CLI/config, or from prompt
|
|
1514
|
+
const finalTeamId = trimString(iosAnswers.teamId) || resolvedTeamId;
|
|
1515
|
+
iosCredentials = {
|
|
1516
|
+
teamId: finalTeamId || undefined,
|
|
1517
|
+
p12Base64: readOptionalFileAsBase64(iosAnswers.p12Path),
|
|
1518
|
+
p12Password: trimString(iosAnswers.p12Password),
|
|
1519
|
+
mobileprovisionBase64: readOptionalFileAsBase64(iosAnswers.mobileprovisionPath),
|
|
1520
|
+
ascApiKeyId: trimString(iosAnswers.ascApiKeyId),
|
|
1521
|
+
ascIssuerId: trimString(iosAnswers.ascIssuerId),
|
|
1522
|
+
ascPrivateKey: readOptionalFileAsBase64(iosAnswers.ascPrivateKeyPath),
|
|
1523
|
+
// Track paths for config saving (not sent to API)
|
|
1524
|
+
_p12Path: trimString(iosAnswers.p12Path),
|
|
1525
|
+
_mobileprovisionPath: trimString(iosAnswers.mobileprovisionPath),
|
|
1526
|
+
_ascApiKeyId: trimString(iosAnswers.ascApiKeyId),
|
|
1527
|
+
_ascIssuerId: trimString(iosAnswers.ascIssuerId),
|
|
1528
|
+
_ascPrivateKeyPath: trimString(iosAnswers.ascPrivateKeyPath),
|
|
1529
|
+
};
|
|
1530
|
+
if (finalTeamId) {
|
|
1531
|
+
console.log(`Using Apple Team ID: ${finalTeamId}`);
|
|
1532
|
+
}
|
|
1189
1533
|
}
|
|
1190
1534
|
}
|
|
1191
1535
|
else if (platform === 'android') {
|
|
@@ -1232,14 +1576,14 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
1232
1576
|
]);
|
|
1233
1577
|
androidCredentials = {
|
|
1234
1578
|
keystoreBase64: readOptionalFileAsBase64(androidAnswers.keystorePath),
|
|
1235
|
-
keystorePassword: androidAnswers.keystorePassword
|
|
1236
|
-
keyAlias: androidAnswers.keyAlias
|
|
1237
|
-
keyPassword: androidAnswers.keyPassword
|
|
1579
|
+
keystorePassword: trimString(androidAnswers.keystorePassword),
|
|
1580
|
+
keyAlias: trimString(androidAnswers.keyAlias),
|
|
1581
|
+
keyPassword: trimString(androidAnswers.keyPassword),
|
|
1238
1582
|
playServiceAccountJson: readOptionalFileAsBase64(androidAnswers.playJsonPath),
|
|
1239
1583
|
};
|
|
1240
1584
|
// Track Android paths for config saving
|
|
1241
|
-
androidCredentials._keystorePath = androidAnswers.keystorePath
|
|
1242
|
-
androidCredentials._keyAlias = androidAnswers.keyAlias
|
|
1585
|
+
androidCredentials._keystorePath = trimString(androidAnswers.keystorePath);
|
|
1586
|
+
androidCredentials._keyAlias = trimString(androidAnswers.keyAlias);
|
|
1243
1587
|
}
|
|
1244
1588
|
// Offer to save config if no norrix.config.ts exists and we collected useful values
|
|
1245
1589
|
const appRoot = process.cwd();
|
|
@@ -1263,6 +1607,16 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
1263
1607
|
if (iosCredentials._mobileprovisionPath) {
|
|
1264
1608
|
saveableOptions.provisioningProfilePath = iosCredentials._mobileprovisionPath;
|
|
1265
1609
|
}
|
|
1610
|
+
// Save ASC API Key details for future builds
|
|
1611
|
+
if (iosCredentials._ascApiKeyId) {
|
|
1612
|
+
saveableOptions.ascApiKeyId = iosCredentials._ascApiKeyId;
|
|
1613
|
+
}
|
|
1614
|
+
if (iosCredentials._ascIssuerId) {
|
|
1615
|
+
saveableOptions.ascIssuerId = iosCredentials._ascIssuerId;
|
|
1616
|
+
}
|
|
1617
|
+
if (iosCredentials._ascPrivateKeyPath) {
|
|
1618
|
+
saveableOptions.ascPrivateKeyPath = iosCredentials._ascPrivateKeyPath;
|
|
1619
|
+
}
|
|
1266
1620
|
}
|
|
1267
1621
|
if (platform === 'android' && androidCredentials) {
|
|
1268
1622
|
if (androidCredentials._keystorePath) {
|
|
@@ -1277,7 +1631,8 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
1277
1631
|
saveableOptions.distributionType ||
|
|
1278
1632
|
saveableOptions.p12Path ||
|
|
1279
1633
|
saveableOptions.provisioningProfilePath ||
|
|
1280
|
-
saveableOptions.keystorePath
|
|
1634
|
+
saveableOptions.keystorePath ||
|
|
1635
|
+
saveableOptions.ascApiKeyId;
|
|
1281
1636
|
if (hasSaveableValues) {
|
|
1282
1637
|
const { shouldSave } = await inquirer.prompt([
|
|
1283
1638
|
{
|
|
@@ -1309,7 +1664,7 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
1309
1664
|
writeRuntimeFingerprintFile(projectRoot, fingerprint, platform);
|
|
1310
1665
|
spinner.start('Creating project archive...');
|
|
1311
1666
|
// 3. Zip the project (workspace-aware)
|
|
1312
|
-
const { zipPath, workspaceContext } = await zipProject(projectName, false, verbose);
|
|
1667
|
+
const { zipPath, workspaceContext } = await zipProject(projectName, workspaceCtx, false, verbose);
|
|
1313
1668
|
spinner.text = 'Project archive created';
|
|
1314
1669
|
// 4. Upload the project zip to S3
|
|
1315
1670
|
spinner.text = 'Working...';
|
|
@@ -1342,6 +1697,11 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
1342
1697
|
appPath: workspaceContext.relativeAppPath,
|
|
1343
1698
|
projectName: workspaceContext.projectName,
|
|
1344
1699
|
} : undefined;
|
|
1700
|
+
// For standalone projects, use the project option or infer from app ID/package name
|
|
1701
|
+
// This allows env vars to be scoped to specific standalone projects within an org
|
|
1702
|
+
const standaloneProjectName = workspaceContext.type === 'standalone'
|
|
1703
|
+
? (opts.project || inferredAppId || projectName)
|
|
1704
|
+
: undefined;
|
|
1345
1705
|
const response = await axios.post(`${API_URL}/build`, {
|
|
1346
1706
|
projectName,
|
|
1347
1707
|
appId: inferredAppId,
|
|
@@ -1351,12 +1711,18 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
1351
1711
|
version: version || '',
|
|
1352
1712
|
buildNumber: buildNumber || '',
|
|
1353
1713
|
configuration,
|
|
1714
|
+
// Nx configuration (e.g., 'prod', 'stg', 'dev') for monorepo builds
|
|
1715
|
+
...(opts.nxConfiguration ? { nxConfiguration: opts.nxConfiguration } : {}),
|
|
1354
1716
|
...(distributionType ? { distributionType } : {}),
|
|
1717
|
+
// Android package type override (apk or aab) - takes precedence over distributionType
|
|
1718
|
+
...(opts.androidPackageType ? { androidPackageType: opts.androidPackageType } : {}),
|
|
1355
1719
|
fingerprint,
|
|
1356
1720
|
// Provide the relative key (without public/) – the workflow prepends public/
|
|
1357
1721
|
s3Key: s3KeyRel,
|
|
1358
1722
|
// Workspace context for Nx monorepos
|
|
1359
1723
|
...(workspaceInfo ? { workspace: workspaceInfo } : {}),
|
|
1724
|
+
// For standalone projects, include project name for env var scoping
|
|
1725
|
+
...(standaloneProjectName ? { projectName: standaloneProjectName } : {}),
|
|
1360
1726
|
// Only include raw credentials if not encrypted
|
|
1361
1727
|
...(encryptedSecrets ? { encryptedSecrets } : {}),
|
|
1362
1728
|
...(!encryptedSecrets && iosCredentials ? { iosCredentials } : {}),
|
|
@@ -1462,8 +1828,25 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
1462
1828
|
* Submit command implementation
|
|
1463
1829
|
* Submits the built app to app stores via the Next.js API gateway
|
|
1464
1830
|
*/
|
|
1465
|
-
export async function submit(cliPlatformArg, cliTrackArg, verbose = false) {
|
|
1831
|
+
export async function submit(cliPlatformArg, cliTrackArg, verbose = false, options) {
|
|
1466
1832
|
ensureInitialized();
|
|
1833
|
+
try {
|
|
1834
|
+
await ensureOrgSelected({
|
|
1835
|
+
orgIdArg: options?.org,
|
|
1836
|
+
nonInteractive: Boolean(options?.nonInteractive),
|
|
1837
|
+
requireSelection: true,
|
|
1838
|
+
verbose,
|
|
1839
|
+
promptMessage: 'Select organization for this submission:',
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
catch (error) {
|
|
1843
|
+
ora().fail(`Organization selection failed: ${error?.message || error}`);
|
|
1844
|
+
if (verbose && error?.response) {
|
|
1845
|
+
console.error('Axios response status:', error.response.status);
|
|
1846
|
+
console.error('Axios response data:', error.response.data);
|
|
1847
|
+
}
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1467
1850
|
const spinner = ora('Preparing app for submission...');
|
|
1468
1851
|
try {
|
|
1469
1852
|
spinner.start();
|
|
@@ -1727,6 +2110,23 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false, opt
|
|
|
1727
2110
|
// Normalize options - support both new object and legacy string projectArg
|
|
1728
2111
|
const opts = typeof options === 'string' ? { project: options } : (options || {});
|
|
1729
2112
|
ensureInitialized();
|
|
2113
|
+
try {
|
|
2114
|
+
await ensureOrgSelected({
|
|
2115
|
+
orgIdArg: opts.org,
|
|
2116
|
+
nonInteractive: opts.nonInteractive,
|
|
2117
|
+
requireSelection: true,
|
|
2118
|
+
verbose,
|
|
2119
|
+
promptMessage: 'Select organization for this update:',
|
|
2120
|
+
});
|
|
2121
|
+
}
|
|
2122
|
+
catch (error) {
|
|
2123
|
+
ora().fail(`Organization selection failed: ${error?.message || error}`);
|
|
2124
|
+
if (verbose && error?.response) {
|
|
2125
|
+
console.error('Axios response status:', error.response.status);
|
|
2126
|
+
console.error('Axios response data:', error.response.data);
|
|
2127
|
+
}
|
|
2128
|
+
return;
|
|
2129
|
+
}
|
|
1730
2130
|
let spinner;
|
|
1731
2131
|
let originalCwd;
|
|
1732
2132
|
try {
|
|
@@ -1759,6 +2159,33 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false, opt
|
|
|
1759
2159
|
platform = chosenPlatform;
|
|
1760
2160
|
spinner.start('Preparing over-the-air update...');
|
|
1761
2161
|
}
|
|
2162
|
+
// Determine Nx configuration for workspace builds (CLI arg preferred, then prompt if available)
|
|
2163
|
+
let nxConfiguration = opts.nxConfiguration;
|
|
2164
|
+
if (!nxConfiguration && workspaceCtx.type === 'nx' && !opts.nonInteractive) {
|
|
2165
|
+
spinner.stop();
|
|
2166
|
+
const nxConfigs = detectNxBuildConfigurations(workspaceCtx.appRoot);
|
|
2167
|
+
if (nxConfigs && nxConfigs.configurations.length > 0) {
|
|
2168
|
+
const choices = nxConfigs.configurations.map((c) => ({
|
|
2169
|
+
name: c === nxConfigs.defaultConfiguration ? `${c} (default)` : c,
|
|
2170
|
+
value: c,
|
|
2171
|
+
}));
|
|
2172
|
+
// Add option to skip/use default
|
|
2173
|
+
choices.unshift({ name: '(none - use defaults)', value: '' });
|
|
2174
|
+
const { chosenNxConfig } = await inquirer.prompt([
|
|
2175
|
+
{
|
|
2176
|
+
type: 'list',
|
|
2177
|
+
name: 'chosenNxConfig',
|
|
2178
|
+
message: 'Nx build configuration (environment):',
|
|
2179
|
+
choices,
|
|
2180
|
+
default: nxConfigs.defaultConfiguration || '',
|
|
2181
|
+
},
|
|
2182
|
+
]);
|
|
2183
|
+
nxConfiguration = chosenNxConfig || undefined;
|
|
2184
|
+
}
|
|
2185
|
+
spinner.start('Preparing over-the-air update...');
|
|
2186
|
+
}
|
|
2187
|
+
// Store resolved nxConfiguration back to opts for later use
|
|
2188
|
+
opts.nxConfiguration = nxConfiguration;
|
|
1762
2189
|
// Infer version from native project files (same as build)
|
|
1763
2190
|
const appleVersionInfo = platform === 'ios' || platform === 'visionos'
|
|
1764
2191
|
? getAppleVersionFromInfoPlist(platform)
|
|
@@ -1963,7 +2390,7 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false, opt
|
|
|
1963
2390
|
spinner.start('Packaging for over-the-air update...');
|
|
1964
2391
|
// Create the update bundle (workspace-aware) - pass true to include node_modules for updates
|
|
1965
2392
|
const projectName = await getProjectName();
|
|
1966
|
-
const { zipPath, workspaceContext } = await zipProject(projectName, true, verbose);
|
|
2393
|
+
const { zipPath, workspaceContext } = await zipProject(projectName, workspaceCtx, true, verbose);
|
|
1967
2394
|
spinner.text = 'Uploading update to Norrix cloud storage...';
|
|
1968
2395
|
const fileBuffer = fs.readFileSync(zipPath);
|
|
1969
2396
|
const updateFolder = `update-${Date.now()}`;
|
|
@@ -1977,6 +2404,11 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false, opt
|
|
|
1977
2404
|
appPath: workspaceContext.relativeAppPath,
|
|
1978
2405
|
projectName: workspaceContext.projectName,
|
|
1979
2406
|
} : undefined;
|
|
2407
|
+
// For standalone projects, use the project option or infer from app ID
|
|
2408
|
+
// This allows env vars to be scoped to specific standalone projects within an org
|
|
2409
|
+
const standaloneProjectName = workspaceContext.type === 'standalone'
|
|
2410
|
+
? (opts.project || appId)
|
|
2411
|
+
: undefined;
|
|
1980
2412
|
const response = await axios.post(`${API_URL}/update`, {
|
|
1981
2413
|
appId,
|
|
1982
2414
|
platform,
|
|
@@ -1984,10 +2416,14 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false, opt
|
|
|
1984
2416
|
buildNumber: buildNumber || '',
|
|
1985
2417
|
releaseNotes: notes,
|
|
1986
2418
|
fingerprint,
|
|
2419
|
+
// Nx configuration (e.g., 'prod', 'stg', 'dev') for monorepo builds
|
|
2420
|
+
...(opts.nxConfiguration ? { nxConfiguration: opts.nxConfiguration } : {}),
|
|
1987
2421
|
// Provide the relative key (without public/). Consumers will prepend public/
|
|
1988
2422
|
s3Key: s3KeyRel,
|
|
1989
2423
|
// Workspace context for Nx monorepos
|
|
1990
2424
|
...(workspaceInfo ? { workspace: workspaceInfo } : {}),
|
|
2425
|
+
// For standalone projects, include project name for env var scoping
|
|
2426
|
+
...(standaloneProjectName ? { projectName: standaloneProjectName } : {}),
|
|
1991
2427
|
}, {
|
|
1992
2428
|
headers: {
|
|
1993
2429
|
'Content-Type': 'application/json',
|
|
@@ -2079,8 +2515,20 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false, opt
|
|
|
2079
2515
|
* Build Status command implementation
|
|
2080
2516
|
* Checks the status of a build via the Next.js API gateway
|
|
2081
2517
|
*/
|
|
2082
|
-
export async function buildStatus(buildId, verbose = false) {
|
|
2518
|
+
export async function buildStatus(buildId, verbose = false, options) {
|
|
2083
2519
|
ensureInitialized();
|
|
2520
|
+
try {
|
|
2521
|
+
await ensureOrgSelected({
|
|
2522
|
+
orgIdArg: options?.org,
|
|
2523
|
+
nonInteractive: true,
|
|
2524
|
+
requireSelection: false,
|
|
2525
|
+
verbose,
|
|
2526
|
+
});
|
|
2527
|
+
}
|
|
2528
|
+
catch (error) {
|
|
2529
|
+
ora().fail(`Organization selection failed: ${error?.message || error}`);
|
|
2530
|
+
return;
|
|
2531
|
+
}
|
|
2084
2532
|
try {
|
|
2085
2533
|
const spinner = ora(`Checking status of build ${buildId}...`).start();
|
|
2086
2534
|
const response = await axios.get(`${API_URL}/build/${buildId}`, {
|
|
@@ -2117,8 +2565,20 @@ export async function buildStatus(buildId, verbose = false) {
|
|
|
2117
2565
|
* Submit Status command implementation
|
|
2118
2566
|
* Checks the status of a submission via the Next.js API gateway
|
|
2119
2567
|
*/
|
|
2120
|
-
export async function submitStatus(submitId, verbose = false) {
|
|
2568
|
+
export async function submitStatus(submitId, verbose = false, options) {
|
|
2121
2569
|
ensureInitialized();
|
|
2570
|
+
try {
|
|
2571
|
+
await ensureOrgSelected({
|
|
2572
|
+
orgIdArg: options?.org,
|
|
2573
|
+
nonInteractive: true,
|
|
2574
|
+
requireSelection: false,
|
|
2575
|
+
verbose,
|
|
2576
|
+
});
|
|
2577
|
+
}
|
|
2578
|
+
catch (error) {
|
|
2579
|
+
ora().fail(`Organization selection failed: ${error?.message || error}`);
|
|
2580
|
+
return;
|
|
2581
|
+
}
|
|
2122
2582
|
try {
|
|
2123
2583
|
const spinner = ora(`Checking status of submission ${submitId}...`).start();
|
|
2124
2584
|
const response = await axios.get(`${API_URL}/submit/${submitId}`, {
|
|
@@ -2151,8 +2611,20 @@ export async function submitStatus(submitId, verbose = false) {
|
|
|
2151
2611
|
* Update Status command implementation
|
|
2152
2612
|
* Checks the status of an update via the Next.js API gateway
|
|
2153
2613
|
*/
|
|
2154
|
-
export async function updateStatus(updateId, verbose = false) {
|
|
2614
|
+
export async function updateStatus(updateId, verbose = false, options) {
|
|
2155
2615
|
ensureInitialized();
|
|
2616
|
+
try {
|
|
2617
|
+
await ensureOrgSelected({
|
|
2618
|
+
orgIdArg: options?.org,
|
|
2619
|
+
nonInteractive: true,
|
|
2620
|
+
requireSelection: false,
|
|
2621
|
+
verbose,
|
|
2622
|
+
});
|
|
2623
|
+
}
|
|
2624
|
+
catch (error) {
|
|
2625
|
+
ora().fail(`Organization selection failed: ${error?.message || error}`);
|
|
2626
|
+
return;
|
|
2627
|
+
}
|
|
2156
2628
|
try {
|
|
2157
2629
|
const spinner = ora(`Checking status of update ${updateId}...`).start();
|
|
2158
2630
|
const response = await axios.get(`${API_URL}/update/${updateId}`, {
|
|
@@ -2397,4 +2869,310 @@ export async function billingPortal(verbose = false) {
|
|
|
2397
2869
|
}
|
|
2398
2870
|
}
|
|
2399
2871
|
}
|
|
2872
|
+
function normalizeVisibilityType(input) {
|
|
2873
|
+
if (!input)
|
|
2874
|
+
return 'secret';
|
|
2875
|
+
if (input === 'plaintext' || input === 'secret')
|
|
2876
|
+
return input;
|
|
2877
|
+
console.error('Error: --visibility must be either "plaintext" or "secret"');
|
|
2878
|
+
process.exit(1);
|
|
2879
|
+
}
|
|
2880
|
+
/**
|
|
2881
|
+
* Set an environment variable for build-time injection
|
|
2882
|
+
*/
|
|
2883
|
+
export async function envSet(name, value, verbose = false, options = {}) {
|
|
2884
|
+
ensureInitialized();
|
|
2885
|
+
let spinner;
|
|
2886
|
+
try {
|
|
2887
|
+
const visibilityType = normalizeVisibilityType(options.visibilityType);
|
|
2888
|
+
// Validate secret name (env var format)
|
|
2889
|
+
if (!/^[A-Z_][A-Z0-9_]*$/i.test(name)) {
|
|
2890
|
+
console.error('Error: Name must be a valid environment variable name (alphanumeric and underscores, cannot start with a number)');
|
|
2891
|
+
process.exit(1);
|
|
2892
|
+
}
|
|
2893
|
+
spinner = ora(`Setting env var "${name}"...`).start();
|
|
2894
|
+
const payload = {
|
|
2895
|
+
type: 'variable',
|
|
2896
|
+
name,
|
|
2897
|
+
visibilityType,
|
|
2898
|
+
project: options.project,
|
|
2899
|
+
};
|
|
2900
|
+
if (visibilityType === 'plaintext') {
|
|
2901
|
+
payload.value = value;
|
|
2902
|
+
}
|
|
2903
|
+
else {
|
|
2904
|
+
Object.assign(payload, encryptSecretValue(value));
|
|
2905
|
+
}
|
|
2906
|
+
const res = await axios.post(`${API_URL}/env`, payload, {
|
|
2907
|
+
headers: await getAuthHeaders(),
|
|
2908
|
+
});
|
|
2909
|
+
spinner.succeed(`Env var "${name}" set successfully`);
|
|
2910
|
+
if (res.data?.message) {
|
|
2911
|
+
console.log(res.data.message);
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
catch (error) {
|
|
2915
|
+
const apiMessage = error?.response?.data?.error ||
|
|
2916
|
+
error?.response?.data?.message ||
|
|
2917
|
+
error?.message ||
|
|
2918
|
+
String(error);
|
|
2919
|
+
spinner?.fail(`Failed to set env var: ${apiMessage}`);
|
|
2920
|
+
if (verbose) {
|
|
2921
|
+
console.error('--- Verbose error details (env-set) ---');
|
|
2922
|
+
console.error(error);
|
|
2923
|
+
if (error?.response) {
|
|
2924
|
+
console.error('Axios response status:', error.response.status);
|
|
2925
|
+
console.error('Axios response data:', error.response.data);
|
|
2926
|
+
}
|
|
2927
|
+
if (error?.stack) {
|
|
2928
|
+
console.error(error.stack);
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
process.exit(1);
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
/**
|
|
2935
|
+
* Upload an environment file for build-time injection
|
|
2936
|
+
*/
|
|
2937
|
+
export async function envSetFile(name, filePath, verbose = false, options = {}) {
|
|
2938
|
+
ensureInitialized();
|
|
2939
|
+
let spinner;
|
|
2940
|
+
try {
|
|
2941
|
+
const visibilityType = normalizeVisibilityType(options.visibilityType);
|
|
2942
|
+
const resolvedPath = normalizePath(filePath);
|
|
2943
|
+
if (!resolvedPath || !fs.existsSync(resolvedPath)) {
|
|
2944
|
+
console.error(`Error: File not found: ${filePath}`);
|
|
2945
|
+
process.exit(1);
|
|
2946
|
+
}
|
|
2947
|
+
spinner = ora(`Uploading env file "${name}"...`).start();
|
|
2948
|
+
// Read and encode file content
|
|
2949
|
+
const fileContent = fs.readFileSync(resolvedPath);
|
|
2950
|
+
const base64Content = fileContent.toString('base64');
|
|
2951
|
+
const payload = {
|
|
2952
|
+
type: 'file',
|
|
2953
|
+
name,
|
|
2954
|
+
visibilityType,
|
|
2955
|
+
destPath: options.destPath || name,
|
|
2956
|
+
project: options.project,
|
|
2957
|
+
};
|
|
2958
|
+
if (visibilityType === 'plaintext') {
|
|
2959
|
+
payload.value = base64Content;
|
|
2960
|
+
}
|
|
2961
|
+
else {
|
|
2962
|
+
Object.assign(payload, encryptSecretValue(base64Content));
|
|
2963
|
+
}
|
|
2964
|
+
const res = await axios.post(`${API_URL}/env`, payload, {
|
|
2965
|
+
headers: await getAuthHeaders(),
|
|
2966
|
+
});
|
|
2967
|
+
spinner.succeed(`Env file "${name}" uploaded successfully`);
|
|
2968
|
+
if (res.data?.message) {
|
|
2969
|
+
console.log(res.data.message);
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
catch (error) {
|
|
2973
|
+
const apiMessage = error?.response?.data?.error ||
|
|
2974
|
+
error?.response?.data?.message ||
|
|
2975
|
+
error?.message ||
|
|
2976
|
+
String(error);
|
|
2977
|
+
spinner?.fail(`Failed to upload env file: ${apiMessage}`);
|
|
2978
|
+
if (verbose) {
|
|
2979
|
+
console.error('--- Verbose error details (env-set-file) ---');
|
|
2980
|
+
console.error(error);
|
|
2981
|
+
if (error?.response) {
|
|
2982
|
+
console.error('Axios response status:', error.response.status);
|
|
2983
|
+
console.error('Axios response data:', error.response.data);
|
|
2984
|
+
}
|
|
2985
|
+
if (error?.stack) {
|
|
2986
|
+
console.error(error.stack);
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
process.exit(1);
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
/**
|
|
2993
|
+
* List all environment variables/files for a project
|
|
2994
|
+
*/
|
|
2995
|
+
export async function envList(verbose = false, options = {}) {
|
|
2996
|
+
ensureInitialized();
|
|
2997
|
+
let spinner;
|
|
2998
|
+
try {
|
|
2999
|
+
spinner = ora('Fetching env vars...').start();
|
|
3000
|
+
const params = new URLSearchParams();
|
|
3001
|
+
if (options.project) {
|
|
3002
|
+
params.set('project', options.project);
|
|
3003
|
+
}
|
|
3004
|
+
const res = await axios.get(`${API_URL}/env?${params.toString()}`, {
|
|
3005
|
+
headers: await getAuthHeaders(),
|
|
3006
|
+
});
|
|
3007
|
+
spinner.stop();
|
|
3008
|
+
const envs = res.data?.envs || [];
|
|
3009
|
+
const projectName = options.project || res.data?.project || 'default';
|
|
3010
|
+
if (envs.length === 0) {
|
|
3011
|
+
console.log(`\nNo environment variables found for project: ${projectName}`);
|
|
3012
|
+
console.log('\nTo add env vars/files, use:');
|
|
3013
|
+
console.log(' norrix env set <name> <value>');
|
|
3014
|
+
console.log(' norrix env set-file <name> <path>');
|
|
3015
|
+
return;
|
|
3016
|
+
}
|
|
3017
|
+
console.log(`\nEnvironment variables for project: ${projectName}\n`);
|
|
3018
|
+
// Separate variables and files
|
|
3019
|
+
const variables = envs.filter((e) => e.type === 'variable');
|
|
3020
|
+
const files = envs.filter((e) => e.type === 'file');
|
|
3021
|
+
if (variables.length > 0) {
|
|
3022
|
+
console.log('Variables:');
|
|
3023
|
+
for (const variable of variables) {
|
|
3024
|
+
const dateStr = variable.updatedAt
|
|
3025
|
+
? `set ${new Date(variable.updatedAt).toLocaleDateString()}`
|
|
3026
|
+
: '';
|
|
3027
|
+
const visibility = variable.visibilityType || variable.visibility || 'secret';
|
|
3028
|
+
console.log(` • ${variable.name} (${visibility}${dateStr ? `, ${dateStr}` : ''})`);
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
if (files.length > 0) {
|
|
3032
|
+
if (variables.length > 0)
|
|
3033
|
+
console.log('');
|
|
3034
|
+
console.log('Files:');
|
|
3035
|
+
for (const file of files) {
|
|
3036
|
+
const dateStr = file.updatedAt
|
|
3037
|
+
? `set ${new Date(file.updatedAt).toLocaleDateString()}`
|
|
3038
|
+
: '';
|
|
3039
|
+
const visibility = file.visibilityType || file.visibility || 'secret';
|
|
3040
|
+
const destPath = file.destPath || file.name;
|
|
3041
|
+
console.log(` • ${file.name} → ${destPath} (${visibility}${dateStr ? `, ${dateStr}` : ''})`);
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
console.log('');
|
|
3045
|
+
}
|
|
3046
|
+
catch (error) {
|
|
3047
|
+
const apiMessage = error?.response?.data?.error ||
|
|
3048
|
+
error?.response?.data?.message ||
|
|
3049
|
+
error?.message ||
|
|
3050
|
+
String(error);
|
|
3051
|
+
spinner?.fail(`Failed to list env vars: ${apiMessage}`);
|
|
3052
|
+
if (verbose) {
|
|
3053
|
+
console.error('--- Verbose error details (env-list) ---');
|
|
3054
|
+
console.error(error);
|
|
3055
|
+
if (error?.response) {
|
|
3056
|
+
console.error('Axios response status:', error.response.status);
|
|
3057
|
+
console.error('Axios response data:', error.response.data);
|
|
3058
|
+
}
|
|
3059
|
+
if (error?.stack) {
|
|
3060
|
+
console.error(error.stack);
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
process.exit(1);
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
/**
|
|
3067
|
+
* Delete an environment variable
|
|
3068
|
+
*/
|
|
3069
|
+
export async function envDelete(name, verbose = false, options = {}) {
|
|
3070
|
+
ensureInitialized();
|
|
3071
|
+
let spinner;
|
|
3072
|
+
try {
|
|
3073
|
+
spinner = ora(`Deleting env var "${name}"...`).start();
|
|
3074
|
+
const res = await axios.delete(`${API_URL}/env`, {
|
|
3075
|
+
data: {
|
|
3076
|
+
type: 'variable',
|
|
3077
|
+
name,
|
|
3078
|
+
project: options.project,
|
|
3079
|
+
},
|
|
3080
|
+
headers: await getAuthHeaders(),
|
|
3081
|
+
});
|
|
3082
|
+
spinner.succeed(`Env var "${name}" deleted successfully`);
|
|
3083
|
+
if (res.data?.message) {
|
|
3084
|
+
console.log(res.data.message);
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
catch (error) {
|
|
3088
|
+
const apiMessage = error?.response?.data?.error ||
|
|
3089
|
+
error?.response?.data?.message ||
|
|
3090
|
+
error?.message ||
|
|
3091
|
+
String(error);
|
|
3092
|
+
spinner?.fail(`Failed to delete env var: ${apiMessage}`);
|
|
3093
|
+
if (verbose) {
|
|
3094
|
+
console.error('--- Verbose error details (env-delete) ---');
|
|
3095
|
+
console.error(error);
|
|
3096
|
+
if (error?.response) {
|
|
3097
|
+
console.error('Axios response status:', error.response.status);
|
|
3098
|
+
console.error('Axios response data:', error.response.data);
|
|
3099
|
+
}
|
|
3100
|
+
if (error?.stack) {
|
|
3101
|
+
console.error(error.stack);
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
process.exit(1);
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
/**
|
|
3108
|
+
* Delete an environment file
|
|
3109
|
+
*/
|
|
3110
|
+
export async function envDeleteFile(name, verbose = false, options = {}) {
|
|
3111
|
+
ensureInitialized();
|
|
3112
|
+
let spinner;
|
|
3113
|
+
try {
|
|
3114
|
+
spinner = ora(`Deleting env file "${name}"...`).start();
|
|
3115
|
+
const res = await axios.delete(`${API_URL}/env`, {
|
|
3116
|
+
data: {
|
|
3117
|
+
type: 'file',
|
|
3118
|
+
name,
|
|
3119
|
+
project: options.project,
|
|
3120
|
+
},
|
|
3121
|
+
headers: await getAuthHeaders(),
|
|
3122
|
+
});
|
|
3123
|
+
spinner.succeed(`Env file "${name}" deleted successfully`);
|
|
3124
|
+
if (res.data?.message) {
|
|
3125
|
+
console.log(res.data.message);
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
catch (error) {
|
|
3129
|
+
const apiMessage = error?.response?.data?.error ||
|
|
3130
|
+
error?.response?.data?.message ||
|
|
3131
|
+
error?.message ||
|
|
3132
|
+
String(error);
|
|
3133
|
+
spinner?.fail(`Failed to delete env file: ${apiMessage}`);
|
|
3134
|
+
if (verbose) {
|
|
3135
|
+
console.error('--- Verbose error details (env-delete-file) ---');
|
|
3136
|
+
console.error(error);
|
|
3137
|
+
if (error?.response) {
|
|
3138
|
+
console.error('Axios response status:', error.response.status);
|
|
3139
|
+
console.error('Axios response data:', error.response.data);
|
|
3140
|
+
}
|
|
3141
|
+
if (error?.stack) {
|
|
3142
|
+
console.error(error.stack);
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
process.exit(1);
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
/**
|
|
3149
|
+
* Encrypt a secret value using the Norrix public key (envelope encryption)
|
|
3150
|
+
*/
|
|
3151
|
+
function encryptSecretValue(value) {
|
|
3152
|
+
const publicKeyPem = process.env.NORRIX_BUILD_PUBLIC_KEY;
|
|
3153
|
+
if (!publicKeyPem) {
|
|
3154
|
+
// If no public key is configured, send value in a format the server can handle
|
|
3155
|
+
// The server will encrypt at rest using its own key
|
|
3156
|
+
return {
|
|
3157
|
+
encryptedValue: Buffer.from(value, 'utf8').toString('base64'),
|
|
3158
|
+
encryptedKey: '',
|
|
3159
|
+
iv: '',
|
|
3160
|
+
tag: '',
|
|
3161
|
+
};
|
|
3162
|
+
}
|
|
3163
|
+
const nodeCrypto = crypto;
|
|
3164
|
+
const aesKey = nodeCrypto.randomBytes(32); // AES-256
|
|
3165
|
+
const iv = nodeCrypto.randomBytes(12); // GCM nonce
|
|
3166
|
+
const cipher = nodeCrypto.createCipheriv('aes-256-gcm', aesKey, iv);
|
|
3167
|
+
const plaintext = Buffer.from(value, 'utf8');
|
|
3168
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
3169
|
+
const tag = cipher.getAuthTag();
|
|
3170
|
+
const encryptedKey = nodeCrypto.publicEncrypt({ key: publicKeyPem, oaepHash: 'sha256' }, aesKey);
|
|
3171
|
+
return {
|
|
3172
|
+
encryptedValue: ciphertext.toString('base64'),
|
|
3173
|
+
encryptedKey: encryptedKey.toString('base64'),
|
|
3174
|
+
iv: iv.toString('base64'),
|
|
3175
|
+
tag: tag.toString('base64'),
|
|
3176
|
+
};
|
|
3177
|
+
}
|
|
2400
3178
|
//# sourceMappingURL=commands.js.map
|